Introduction
Components in App Connect are elements that are placed in the html dom and can be used for different tasks. The following tasks are possible with components:
- Acts as a data source
- Render dynamic contents based on its attributes
- Update its content on data changes
- Trigger events that can call actions
- Has methods that can be called to perform an action
The component can be placed in the html using a custom tag or by defining it in the is attribute. All App Connect components are prefixed with dmx-.
Custom tag name
<dmx-value id="name" value="Patrick"></dmx-value>
Using the is attribute
<table is="dmx-datatable" dmx-bind:data="myDataset"></table>
Passing data is done using attributes. Values passed with attributes are static, to bind it to dynamic data use the dmx-bind: prefix on the attribute name and pass an expression as value.
Events handlers can also be attached using attributes, they start with on like onclick or onchange. In the attribute value you write a JavaScript expression. To call App Connect actions you use the prefix dmx-on: like dmx-on:click. You can call actions from other components and use dynamic data that is available in the current scope. The dynamic events also has some extra modifiers, for example dmx-on:submit.prevent will prevent the default action. Some other modifiers are stop, self, ctrl, alt, shift etc.
Example of keydown event which only triggers the action when enter key is pressed
<input name="search" dmx-on:keydown.enter="search(value)">
Creating a component
A component definition has multiple properties and methods, most of them are optional depending on your needs. Registering a component is done like dmx.Component(name, definition). Where name is the name of the component without the dmx- prefix and the defination is an object with properties and methods on it which we will explain later.
The Value Component
dmx.Component('value', {
initialData: {
value: null
},
attributes: {
value: {
default: null
}
},
methods: {
setValue: function(value) {
if (this.data.value !== value) {
this.set('value', value);
dmx.nextTick(function() {
this.dispatchEvent('updated');
}, this);
}
}
},
events: {
updated: Event
},
init: function() {
this.set('value', this.props.value);
},
render: false,
preformUpdate: function(updatedProps) {
if (updatedProps.has('value')) {
this.set('value', this.props.value);
dmx.nextTick(function() {
this.dispatchEvent('updated');
}, this);
}
}
});
The initialData contains default values of the data from the component, you can also use a function that returns an object. The data set here can be accessed using expressions. Within the component we can access the data using this.data, setting/updating the data should be done using this.set() method. When the data value was changed it will request an update of all components on the page.
The attributes object describes all the attributes that the component has. The key is the attribute name, in our sample that attribute name is value. The default value for the attribute is null. You can also define a type like String, Number or Boolean. A Boolean type will be true when the attribute is on the element and false when it is not. The values are accessable using this.props. So this.props.value returns the value that is being set on the component like value="Patrick" or with dmx-bind:value="name".
[!note]
Attributes likedata-source=""are defined in the component asdataSource.
The methods are the actions added to the component, they can be called from dynamic events or in flow actions.
In the events it lists the available events. You need to list all events here which you want to dispatch within your component.
The init and render functions are called once when the DOM is being parsed on DOM ready. The init function is where you do any initialization and the render function you do the DOM manipulations. Setting render to false will prevent any rendering and it will not process any children of the component.
The performUpdate function is called when somewhere data/state was updated, the updatedProps argument returns a Map object with the updated properties and their previous values while with this.props you get the new values, you can then compare them to see what was changed and do any necessary updates.
In the value component we also see this.dispatchEvent('updated'); which triggers the updated event after a value was updated. The dmx.nextTick was added to have a small delay so other components on the page can update first before the event handler is called.
The components are updated in order how they appear in the DOM. This means that if a component was depending on the value of this component and it is located before this component in the DOM it will not have the updated value and an extra update is required.
The component API
Let's go over some methods that are available on the component and when they are called.
{
// here you set the initial data for your component
initialData: {} || function() {},
// called once on initial parse
init: function(node) {},
// render is called after init, setting it to false will prevent any rendering and parsing of children
render: function(node) {} || false,
// the performUpdate method is where you do your update logic like updating its own data or html
performUpdate: function(updatedProps) {},
// the destroy is where you do remove event listeners and cleanup references
// after the destroy the component does some internal cleanup like destroying its children and bindings
destroy: function() {},
// the destroyed is called after the node is removed from DOM and all the
// children are also destroyed
destroyed: function() {},
Most of the methods do not have some default functionality, the exception is for the render method which default will call this.$parse() to parse the children. When you overwrite and have children that also needs to be parsed and rendered you'l need to call the $parse method.
// default render method just parses its inner html
// setting the render property to false will prevent parsing
render: function(node) {
if (this.$node) {
this.$parse();
}
}
[!abstract] Internal Properties
this.$node: a reference to the DOM Node
this.parent: the parent component
this.children: array with child components
this.props: the properties (values from attributes)
this.data: the data (exposed for expressions)
this.name: name of the component
[!abstract] Internal Methods
this.$parse(node): parse the DOM, you should call this in the render when you have html inside the component
this.$watch(expression, callback): watch an expression, when the output changes the callback will be called
this.$createChild(name, node): create a child component
this.$removeChild(child): remove a child component
this.$destroy(): will remove the component from dom and calls destoy on all its children
this.parse(expression): parse an expression in the component scope
this.find(name): find a child component by name
this.addEventListener(type, handler): add an event listerer to the component
this.removeEventListener(type, handler): remove an event listener
this.dispatchEvent(event, props, data, nsp): trigger an event on the component, will also trigger a native DOM event
this.get(name, ignoreParents): get a value from the data, if it isn't found on the current object it will look in the parent up to the global scope
this.add(name, value): add a value to a data, will create an array or add new item to an existing array
this.set(name, value): set a value in the data, will overwrite/update existing value
this.del(name): remove value from data
Example Components
Weather Datasource Component
This sample component will use the weather api to get the current weather at a specific location. You probably don't want to expose the api key in the html normally, but in this case it is for explaining the workings. If you want to use it in production you probably want to do the api calls in a server action and call the server action from the component. We will here call the api directly for simplicity.
[!example]- Component Code
dmx.Component('weather-api', { initialData: { location: {}, current: {} }, attributes: { // api key from weatherapi.com key: { type: String, default: '' }, q: { type: String, default: 'auto:ip' } }, methods: { refresh () { this._refresh(); } }, events: { updated: Event }, init (node) { // we call the refresh method on our initial render to get the weather data this._refresh(); }, // this component doesn't have anything to render render: false, performUpdate (updatedProps) { // if props are updated we need to refresh the data this._refresh(); }, _refresh () { // our refresh function will call the weather api and then update the component data fetch(`http://api.weatherapi.com/v1/current.json?> key=${this.props.key}&q=${this.props.q}`) // get the json response .then(response => response.json()) // set our own data with the response from the api .then(data => this.set(data)) // dispatch an updated event .then(() => this.dispatchEvent('updated')); } });
usage
<dmx-weather-api id="weather" key="apikey"></dmx-weather-api>
The weather in {{weather.location.name}} is {{weather.current.temp_c}}℃ and it is {{weather.current.condition.text}}.
Weather Widget Component
For the weather widget we are going to extend the Weather Datasource Component and render some html that will visually show the current weather.
[!example]- Component Code
dmx.Component('weather-widget', { extends: 'weather-api', render (node) { // we going to listen to our own updated event // when data was updated we will render the widget this.addEventListener('updated', () => this._renderWidget()); }, _renderWidget () { const { current, location } = this.data; // we keep the render simple by just setting the innerHTML // using a template string this.$node.innerHTML = ` <div style="background: #036; background-image: linear-gradient(180deg, black, transparent); color: #fff; border-radius: 1rem; font-family: sans-serif; padding: 1rem;"> <div style="display: flex; flex-direction: column; gap: 1rem;"> <div style="font-size: 1.5rem; text-align: center">${location.name}, ${location.country}</div> <div style="display: flex; gap: 1rem; justify-content: space-around"> <div style="display: flex; flex-direction: column; align-items: center;"> <img style="height: 100%" src="${current.condition.icon}" alt="${current.condition.text}"> <div style="font-size: .825rem">${current.condition.text}</div> </div> <div style="text-align: center;"> <div style="font-size: 5rem">${current.temp_c}℃</div> <div style="font-size: .825rem">Feels like ${current.feelslike_c}℃</div> </div> <div style="font-size: .825rem"> Wind: ${current.wind_kph} kmph ${current.wind_dir}<br> Precip: ${current.precip_mm} mm<br> Presure: ${current.pressure_mb} mb<br> Humidity: ${current.humidity}%<br> UV Index: ${current.uv} </div> </div> <div style="font-size: .825rem; text-align: center"> Data last updated from weatherapi.com at ${current.last_updated} </div> </div> </div> `; } });
usage
<dmx-weather-widget id="weather" key="apikey"></dmx-weather-widget>
Datatable Component
Simple component that renders a table based on a data source
[!example]- Component Code
dmx.Component('datatable', { attributes: { data: { type: Array, default: [] } }, render (node) { // render the table this._renderTable(); }, performUpdate (updatedProps) { this._renderTable(); }, _renderTable () { // Data is null/undefined, probably loading the data if (!Array.isArray(this.props.data)) { this.$node.innerHTML = ` <tr> <td>Loading...</td> </tr> `; return; } // Empty results or no data set if (!this.props.data.length) { this.$node.innerHTML = ` <tr> <td>There are no results...</td> </tr> `; return; } this.$node.innerHTML = ` <thead> <tr> ${Object.keys(this.props.data[0]).map(key => `<th>${key}</th>`).join('')} </tr> </thead> <tbody> ${this.props.data.map(row => ` <tr> ${Object.values(row).map(value => `<td>${value}</td>`).join('')} </tr> `).join('')} </tbody> `; } });
usage
<table class="table" is="dmx-datatable" dmx-bind:data="myDataset"></table>
Resize Observer Component
The resize oberser component keeps track of changes to the dimensions of the element, the width and height are available as data properties and can be used in expressions. When the dimensions of the element changes it will also trigger the resize event which can be listened to.
[!example]- Component Code
dmx.Component('resize-observer', { initialData () { const rect = this.$node.getBoundingClientRect(); return { width: rect.width, height: rect.height }; }, events: { resize: Event }, init (node) { this._observer = new ResizeObserver((entries) => { this.set('width', entries[0].contentRect.width); this.set('height', entries[0].contentRect.height); this.dispatchEvent('resize'); }).observe(node); }, destroy () { // cleanup here this._observer.unobserver(this.$node); delete this._observer; } });
usage
<div id="resizable" is="dmx-resize-observer">
{{resizable.width}}x{{resizable.height}}
</div>