The widgetsService is available in live/services/widgets. It is javascript module that allows registering custom widget implementation to Live run-time. Every widget must have a unique string to represent its type. In this tutorial example we choose my-simple-chart as shown below:
// Widgets service usage:import i18n from'live/services/i18n'import widgetsService from'live/services/widgets'// The widget implementationimport SimpleChart from'./my-simple-chart'consttype='my-simple-chart'// Examples on: "Registering the widget" sectionswidgetService.register(...)
Creating the Widget types
First we need to create types. If you do not have all definitions in mind at first, don't worry, create some placeholder types and enrich them at a later point.
types.ts
// 1. Typing the widget State shape:// State will be the consolidated value derived// from the events flow by the event reducer// on this example we'll keep this simple shape:typeChartState= { value:number}// 2. Typing data events// The shape of these events in runtime// are defined by the query of the widget// a. Event shapetypeChartEvent=LiveEvent<{ fieldA:number fieldB:number}>// b. Describe your message shapetypeChartMessage=FlowMessage<ChartEvent>// 3. Typing the widget configuration:typeConfigs= { widget:LiveWidget<{ color:string }>}
Creating the Data Reducer
To get from events a consolidated current state we offer the abstraction of a data reducer. It is as simple as a switch-case statement that will always return an updated state value from each message received.
reducer.ts
constSimpleReducer:WidgetReducerType<ChartState,ChartMessage> = (state =getInitialState(), message) => {// test for initialState callsif (!message) {return state }if (message.baseline &&message.type !=='event') {return state }switch (message.type) {case'start':returngetInitialState()case'event':return { value:message.events[0].fieldA +message.events[0].fieldB }default:return state }}
On the reducer above we're getting fields fieldA and fieldB defined in ChartEvent to consolidate our state value on data events. All event types definitions.
Creating a pure Javascript Widget
Widget implementation
Create a simple class that implements ChartInterface
classSimpleChartimplementsChartInterface<ChartState,MyMessage,ChartConfigs> {// Additionally: create custom instance properties cfg:ChartConfigs el:HTMLElement// Additionally: use the constructor to get needed valuesconstructor({ options, el }:ChartConstructorOptions<ChartState,FlowMessage,ChartConfigs>) {this.cfg =options.cfgthis.el = el }init():void {this.el.innerHTML ='Chart has been initialized' }render(state:ChartState, configs:Configs, size:Dimensions):void {this.el.innerHTML =`value is ${state.value}, size is ${size.width}x${size.height}`this.el.style.color =configs.widget.jsonConfig.color }destroy():void {this.el.remove() }}
That is all on the chart implementation.
Registering the widget
Wrap your widget with the widgetWrapper decorator
init.ts
import widgetService from'live/services/widgets'import { widgetWrapper } from'live/widgets/decorators/pure'import SimpleChart from'./SimpleChart'widgetService.register({ type:'SimpleChart',getname() {returni18n('Simple chart') },getdescription() {returni18n('This chart shows the number 42') }, config: { color:'green' },// the wrapper is needed to enable the reducer mechanism and comply the API impl:widgetWrapper<ChartState,ChartMessage,ChartConfigs>(SimpleChart, simpleReducer)})
You are all set. A new chart implementation has been added to Live and can now be chosen on the Pipes widget editor.
Complete example
classSimpleChartimplementsChartInterface<ChartState,MyMessage,ChartConfigs> {// Additionally: create custom instance properties cfg:ChartConfigs el:HTMLElement// Additionally: use the constructor to get needed valuesconstructor({ options, el }:ChartConstructorOptions<ChartState,FlowMessage,ChartConfigs>) {this.cfg =options.cfgthis.el = el }init():void {this.el.innerHTML ='Chart has been initialized' }render(state:ChartState, configs:Configs, size:Dimensions):void {this.el.innerHTML =`value is ${state.value}, size is ${size.width}x${size.height}`this.el.style.color =configs.widget.jsonConfig.color }destroy():void {this.el.remove() }}
import widgetService from'live/services/widgets'import { widgetWrapper } from'live/widgets/decorators/pure'import SimpleChart from'./SimpleChart'import { reducer } from'./reducer'widgetService.register({ type:'SimpleChart',getname() {returni18n('Simple chart') },getdescription() {returni18n('This chart shows the number 42') }, config: { color:'green' },// the wrapper is needed to enable the reducer mechanism and comply the API impl:widgetWrapper<ChartState,ChartMessage,ChartConfigs>(SimpleChart, simpleReducer)})
// 1. Typing the widget State shape:// State will be the consolidated value derived// from the events flow by the event reducer// on this example we'll keep this simple shape:typeChartState= { value:number}// 2. Typing data events:// a. Event shapetypeChartEvent=LiveEvent<{ fieldA:number fieldB:number}>// b. Describe your message shapetypeChartMessage=FlowMessage<ChartEvent>// Typing the widget configuration:typeConfigs= { widget:LiveWidget<{ color:string }>}
exportconstreducer:WidgetReducerType<ChartState,ChartMessage> = (state =getInitialState(), message) => {// test for initialState callsif (!message) {return state }if (message.baseline &&message.type !=='event') {return state }switch (message.type) {case'start':returngetInitialState()case'event':return { value:message.events[0].fieldA +message.events[0].fieldB }default:return state }}
Creating a React widget
Widget implementation
To create a React widget simply create a React component, like so:
// SimpleChart.tsxconstSimpleChart= (props:LiveReactChartProps<ChartState,ChartMessage,Configs>):JSX.Element=> {const { state,width,height,widget } = propsconst { value } = statereturn (<div style={{ color:widget.jsonConfig.color }}> value is {value}, size is {width}x{height}</div> )}
React widget heads up
If your component is a Memoremember to return a new state at the reducer when you want it to be updated, a shallow copy is enough.
React widget tip: split data and config components
By doing this you can restrict React updates to state and configs in different sub-trees.
eg: <ChartData data={state} /> and <ChartConfig config={widget.jsonConfig> width={width} height={height} />
Registering the widget
Wrap your widget with the reactWidgetWrapper decorator
// init.tsimport widgetService from'live/services/widgets'import { reactWidgetWrapper } from'live/widgets/decorators/react'widgetService.register({ type:'SimpleChart',getname() {returni18n('Simple chart') },getdescription() {returni18n('This chart shows the number 42') }, config: { color:'green' },// the wrapper is needed to enable the reducer mechanism and comply the API impl:reactWidgetWrapper<ChartState,ChartMessage,ChartConfigs>(SimpleChart, simpleReducer)})
You are all set. A new chart implementation has been added to Live and can now be chosen on the Pipes widget editor.
Complete example
// SimpleChart.tsxconstSimpleChart= (props:LiveReactChartProps<ChartState,ChartMessage,Configs>):JSX.Element=> {const { state,width,height,widget } = propsconst { value } = statereturn (<div style={{ color:widget.jsonConfig.color }}> value is {value}, size is {width}x{height}</div> )}
import widgetService from'live/services/widgets'import SimpleChart from'./SimpleChart'import { reducer } from'./reducer'import { reactWidgetWrapper } from'live/widgets/decorators/react'widgetService.register({ type:'SimpleChart',getname() {returni18n('Simple chart') },getdescription() {returni18n('This chart shows the number 42') }, config: { color:'green' },// the wrapper is needed to enable the reducer mechanism and comply the API impl:reactWidgetWrapper<ChartState,ChartMessage,ChartConfigs>(SimpleChart, simpleReducer)})
// 1. Typing the widget State shape:// State will be the consolidated value derived// from the events flow by the event reducer// on this example we'll keep this simple shape:typeChartState= { value:number}// 2. Typing data events:// a. Event shapetypeChartEvent=LiveEvent<{ fieldA:number fieldB:number}>// b. Describe your message shapetypeChartMessage=FlowMessage<ChartEvent>// Typing the widget configuration:typeConfigs= { widget:LiveWidget<{ color:string }>}
exportconstreducer:WidgetReducerType<ChartState,ChartMessage> = (state =getInitialState(), message) => {// test for initialState callsif (!message) {return state }if (message.baseline &&message.type !=='event') {return state }switch (message.type) {case'start':returngetInitialState()case'event':return { value:message.events[0].fieldA +message.events[0].fieldB }default:return state }}
Legacy API - deprecated
Deprecated since Live 2.27.0, this became an underneath API that shouldn't be used directly anymore. Prefer the Standard APIto create new charts.
Live prior to 2.27 continues to support Legacy API as default API for chart creation. Some other helpers will be dropped soon also. For example, live/lib/react-widget is deprecated in favor of live/widgets/decorators/react.
app.js
import i18n from'live/services/i18n'import WidgetsService from'live/services/widgets'// The widget implementationimport TutorialWidget from'./widget'// required:// registering widget name and descriptionWidgetsService.name('tutorial',i18n('tutorial widget title'))WidgetsService.description('tutorial',i18n('tutorial widget description'))// required:// registering optional its default config, it also acts a config mask// you should put every possible configuration here.WidgetsService.config('tutorial', { textColor:'green' })// required:// registering the implementationWidgetsService.impl('tutorial', TutorialWidget)// optional:// configurations that changes the behavior// of the default editor and also the widget renderingWidgetsService.policies('tutorial', {})WidgetsService.properties('tutorial', {})
Live provides a factory function in live/widgets/helpersto proper setup the prototype to use a function constructor for the Widget instance creation.
Live is implemented using ReactJS, however the Live Widget API doesn't require widgets developers to write a React Component. The Widget implementation must provide the following methods:
setEl(element) is called right after the DOM element creation for the widget container. The elementparameter contains the divthat acts as container and the developer could save the elementreference at its instance for future usage. This method should return the current widget instance.
setEl(el: HTMLElement) {this.el = elreturnthis}
Widget.render
render() is called once at very first rendering of the Widget (be careful to avoid misunderstanding with render behavior of the React Components). This method is executed as a initialization phase at Widget Life-Cycle, it is commonly used to get the DOM ready for the events arrival. Besides the elementcontainer also receives a CSS class named using the -widget suffix over the widget type string, in our example: tutorial-widget. So the developer will be able to customize the container as necessary and append child elements as well, for example:
render() {this.el.insertAdjacentHTML('beforeend','<span class="value"> - </span>')this.el.insertAdjacentHTML('beforeend','<span class="date"> - </span>')this.el.insertAdjacentHTML('beforeend','<span class="control">waiting</span>')// Widget implementation MUST call the `onReady` function.if (typeofthis.options.onReady =="function")this.options.onReady.apply(this)returnthis}
The `render` return types
Object with defined `options` property
{ options: LiveWidgetImplOptions }
If the render function returns a object with options it tells Live that the widget handles its own rendering process. Then you must handle by your own any dynamic change to your own properties. Of course, in event flow method (as we describe next) you can handle how to change anything in your widget since new events arrive.
The widgetFactory constructor assigns a default empty object to options. Keep that in mind if your render method returns this.
If the render function returns a Function, the return value is considered to be a React Component. Keep in mind this function definition must not define options property. The component will be created like so:
<WidgetComponentmode={this.props.mode} // the dashboard mode or mode 'editor'event={this.state.event} // the last eventbatch={this.state.batch} // the last event batchwidget={widget} // the widget configurationheight={this.state.height} // current heightwidth={this.state.width} // current width/>
Object with `portal` property: property considered to be a React Component
The portal property is also considered to be a React Component. It differs from returning a function because this PortalComponent will be rendered as a sibling of the widget's HTMLElement reference element.
The `onReady` function
The onReady function received on the Widget's constructor and assigned to this.options.onReady is a function that as soon as it is called it flags the widget as ready to receive data.That said, calling this.options.onReady is mandatory.
Widget.flow
flow(type, events, layer, baseline, preventRedraw) is called as soon as onReady Promise is resolved and will be executed every time the new data event or event control arrives. In strict sense of a Pipes Widgets, this is the most important method in widget implementation. It should be faster, be careful to avoid memory consumption and any delays on it will result in lower event processing throughput.
The event type string describes what kind of events is coming. Live organizes them in two categories data eventsandcontrol events. For further understanding what event exists and how they like, see Live Events Types section.
The batch of events is an array of objects and its format depends on Pipes Expression (see start event at Live Event Types). If only single events are emitted each time, events.length will be always 1. But Pipes has support to emit at every N items collected, so the event could arrive in batch mode.
Live Widget could have separated layersto execute different Pipes queries at the same time (the widget configuration could choose if multiple layers are supported). The layersparameter is a number with the layer index. The baseline and preventRedraw parameters are boolean.
An illustrative example how implement the flow function is given:
flow(type, events, layer, baseline, preventRedraw) {switch (type) {// data eventscase'event':constliveevent= events[0] // we take only one but it could be a batchconsttimestamp=liveevent.timestamp /1000constevent2str=JSON.stringify(liveevent,null,2)consteventdate='event emitted in '+moment.unix(timestamp).toString()this.el.querySelector('.value').innerHTML = event2strthis.el.querySelector('.date').innerHTML = eventdatebreak// control eventscase'start':this.el.querySelector('.control').innerHTML ='starting...'breakcase'warning':this.el.querySelector('.control').innerHTML =events.messagebreakcase'endHistory':this.el.querySelector('.control').innerHTML ='query duration was '+events.duration +'ms !'breakdefault:break }// debugging only event controlsif (type !=='event')console.debug('event control "'+ type +'" has arrived ', events)}
The initialize and destroy methods are optional. Initialize is called at instantiation time and destroy is the last call before the Live removes the container from DOM.
The screenshot below shown an example of a chart instance of Tutorial Pipes Widget using the Pipes Expression => random() every 5 sec that produces only real-time events that spawn every 5 seconds.
Widget Life Cycle
Live Widget instance is created at very first time when the user click in widget name at side bar of widget editor (as shown above). But at this point the widget isn't part of a dashboard yet, the user must save it. After saved, now the widget gains an unique ID in dashboard context and will be instantiate every time the dashboard be open (either in edit mode or view mode). Internally a Live Widget instance is named Chart.
Some basic examples of properties of a widget instance are shown below:
this.options.id : number that identifies the widget in dashboard
this.options.name : string that identifies the widget name in dashboard
this.options.mode : string that indicates one of:
editorwhen widget itself is being edited
edit when dashboard is being edited
viewwhen dashboard is opened in view mode
fullscreenwhen dashboard is opened in fullscreen mode
The Live Widget Configuration section presents the complete reference of the widget configuration type.
Appendix: complete example files
See the complete example files discussed in the previous headings. You could create a webappdirectory, put all the following files on it and read the Live Widget Packaging section to learn how to build a single bundle.js using Webpack.
import i18n from'live/services/i18n'import WidgetsService from'live/services/widgets'import TutorialWidget from'./widget'try {document.addEventListener('DOMContentLoaded',function () {require('./init') })// required:// registering widget name and descriptionWidgetsService.name('tutorial',i18n('tutorial widget title'))WidgetsService.description('tutorial',i18n('tutorial widget description'))// required:// registering optional its default config, it also acts a config mask// you should put every possible configuration here.WidgetsService.config('tutorial', { textColor:'green' })// required:// registering the implementationWidgetsService.impl('tutorial', TutorialWidget)} catch (e) {console.error(e)}require('../scss/index.scss')
import moment from'moment'import { extend } from'lodash'import { widgetFactory } from'live/widgets/helpers'constTutorialWidget=widgetFactory()extend(TutorialWidget.prototype, {initialize() { },destroy() {console.log('widget.destroy() is called before DOM element removal')while (this.el.firstChild &&!this.el.firstChild.remove())this.el.innerHtml ='' },render() {console.debug('widget.render() is called only at first widget rendering')this.el.insertAdjacentHTML('beforeend','<span class="value"> - </span>')this.el.insertAdjacentHTML('beforeend','<span class="date"> - </span>')this.el.insertAdjacentHTML('beforeend','<span class="control">waiting</span>')// Widget implementation MUST call the `onReady` function.if (typeofthis.options.onReady =="function")this.options.onReady.apply(this)returnthis },setEl(el) {this.el = elreturnthis },flow(type, events, layer, baseline, preventRedraw) {switch (type) {// data eventscase'event':constliveevent= events[0] // we take only one but it could be a batchconsttimestamp=liveevent.timestamp /1000constevent2str=JSON.stringify(liveevent,null,2)consteventdate='event emitted in '+moment.unix(timestamp).toString()this.el.querySelector('.value').innerHTML = event2strthis.el.querySelector('.date').innerHTML = eventdatebreak// control eventscase'start':this.el.querySelector('.control').innerHTML ='starting...'breakcase'warning':this.el.querySelector('.control').innerHTML =events.messagebreakcase'endHistory':this.el.querySelector('.control').innerHTML ='query duration was '+events.duration +'ms !'breakdefault:break }// debugging only event controlsif (type !=='event')console.debug('event control "'+ type +'" has arrived ', events) }})module.exports= TutorialWidget
exportdefault { values: {'tutorial widget title':'Tutorial','tutorial widget title options':'Opções para widget Tutorial','tutorial widget description':'Um widget simples para exercitar a extensibilidade do Live', }}
exportdefault { values: {'tutorial widget title':'Tutorial','tutorial widget title options':'Tutorial widget options','tutorial widget description':'A simple widget to exercise Live extensibility features', }}