Skip to content

Custom controls

When none of the controls provided fits, you write your own. The example in this chapter is a circular gauge that shows a percentage, drawn in SVG.

Derive SvgComponent, the base SVG component, and build the drawing with SvgBuilder.

import { SvgBuilder, SvgComponent, SvgProps } from 'x4js';
interface MyGaugeProps extends SvgProps {
percent: number;
}
class MyGauge extends SvgComponent<MyGaugeProps> {
constructor( props: MyGaugeProps ) {
// the drawing is made in a 100 x 100 space
super( { ...props, viewbox: "0 0 100 100" } );
this.setPercent( props.percent );
}
setPercent( percent: number ) {
const svg = new SvgBuilder( );
// the track of the gauge
svg.circle( 50, 50, 45 ).no_fill( ).stroke( "lightgray", 5 );
// the arc of the value
svg.path( ).arc( 50, 50, 45, 0, percent*360 ).no_fill( ).stroke( "red", 5 );
// the text in the centre
svg.text( 50, 50, Math.round( percent*100 )+"%" )
.fontSize( 25 )
.textAlign( "center" )
.verticalAlign( "center" );
this.setSvg( svg );
}
}

The gauge is used like any component:

const gauge = new MyGauge( { percent: 0.5, width: 120, height: 120 } );
// later
gauge.setPercent( 0.75 );

Three points to remember:

  1. Properties are typed. The MyGaugeProps interface extends that of the base component, and the class takes it as a type parameter. The editor then suggests percent, and rejects a typo.
  2. Properties are passed as they are to the parent. They remain available afterwards in this.props.
  3. The component updates itself. setPercent redraws the gauge: the author of the component decides what to do when a value changes.

Suppose the gauge must react to the mouse wheel. Connect a handler to the browser event with addDOMEvent:

constructor( props: MyGaugeProps ) {
super( { ...props, viewbox: "0 0 100 100" } );
this.addDOMEvent( "wheel", ( ev ) => this.onWheel( ev ) );
this.setPercent( props.percent );
}
private onWheel( ev: WheelEvent ) {
// ...
}

addDOMEvent is meant for writing controls. On a standard control, use its properties and its events.

The control must not decide by itself what the wheel means: it notifies whoever uses it. Declare the event, then fire it with fire.

import { ComponentEvent, ComponentEvents, EventCallback, SvgBuilder, SvgComponent, SvgProps } from 'x4js';
// what the event carries
interface EvGaugeWheel extends ComponentEvent {
delta: number;
}
// the events of the control
interface MyGaugeEvents extends ComponentEvents {
wheel: EvGaugeWheel;
}
interface MyGaugeProps extends SvgProps {
percent: number;
wheel?: EventCallback<EvGaugeWheel>; // short form, in the properties
}
class MyGauge extends SvgComponent<MyGaugeProps,MyGaugeEvents> {
constructor( props: MyGaugeProps ) {
super( { ...props, viewbox: "0 0 100 100" } );
// connect the handler received in the properties
this.mapPropEvents( props, "wheel" );
// turn the browser event into an event of the control
this.addDOMEvent( "wheel", ( ev ) => this.fire( "wheel", { delta: ev.deltaY } ) );
this.setPercent( props.percent );
}
// setPercent as above
}

Whoever creates the gauge can then write, with the help of the editor:

const gauge = new MyGauge( {
percent: 0.5,
wheel: ( ev ) => console.log( ev.delta ),
});

or subscribe later with gauge.on( "wheel", … ).

A CSS class named after the control is associated with it automatically: here mygauge. Put colors and sizes there, preferably as CSS variables, so that the control follows the theme of the application. See Themes and CSS.

In the example, colors are written in the code to keep it short. In a real control, give each element of the drawing a class with addClass instead, and set the colors in the stylesheet.

For an input control to be picked up by a Form, it must have a name and answer the form-element interface, that is: give its value, receive it, and say whether it is valid. The controls provided, such as Input or Select, are good models to read in the sources.