WebApp components
Page content
Components are used to organize a WebApp's UI. A component should be seen as a logical block of elements. A WebApp's UI consists of at least one component (main.js), but most WebApps are organized into multiple components. A component collects state from the WebApp's store.
A List Component is also available to simplify working with collections.
main.js
main.js is the main component of a WebApp. It must be stored in the root of the WebApp archive. Rendering will always start from main.js.
Component
Components, except for the main component, are stored in the /components
folder. WebApps encourage modular code. Given that, a file will only contain one component using the following pattern:
define(function(require) {
'use strict';
var Component = require('Component');
return Component.extend({
// code...
});
});
State
WebApps use Redux as state container. Make sure you understand the core principles found in the state management section before going on.
A component's state is collected from the WebApp's store through the filterState
function. filterState
is called with store.getState()
as parameter and is executed when a component is initialized. The filtered state will be the local state for the component and is accessible via this.state
in a component context.
// index.js
(function() {
'use strict';
var router = require('router');
router.get('/', function(req, res) {
var data = {
name: 'foo'
};
// data will be the initial state for the WebApp's store
res.render('/', data);
});
}());
// main.js
define(function(require) {
'use strict';
var Component = require('Component');
return Component.extend({
onRendered: function() {
console.log(this.state.name); // foo
},
// collect the information a component needs from the store
// always return a new object
filterState: function(state) {
return _.extend({}, {name: state.name});
}
});
});
Passing options to filterState [@since 6.1]
When rendering sub components it is possible to pass properties to the component. Properties will be available on this.options
and passed in as second parameter to filterState
. This can be useful when nesting List Components.
// render a sub component
<%= renderer.renderComponent('MySubComponent', {foo: 'bar'}) %>
// MySubComponent.js
define(function(require) {
'use strict';
var Component = require('Component');
return Component.extend({
onRendered: function() {
console.log(this.options.foo); // 'bar'
},
filterState: function(state, options) {
// options -> {foo: 'bar'}
return Object.assign({}, state);
}
});
});
Setting state
Since the WebApp's store is the single source of truth, all state updates should be handled by dispatching an action to the store's reducer which will update the store.
To use the new state, a component can subscribe to the store and register a callback when the store is updated. Subscribing to the store is easily done by adding the 'store'-event in the events hash. The callback will be called with the new state for the component. Note that the callback will only be triggered if the store change affects the component. Use the store.subscribe(callback)
function to listen for all store changes.
events: {
dom: {
'click [data-update-name]': 'handleUpdateName'
},
self: {
'state:changed': 'render'
},
// listen to store changes that affect the component
store: 'handleStoreUpdate'
},
handleUpdateName: function() {
// dispatch an action to update the store
// do NOT modify this.state here
store.dispatch({
type: 'SET_NAME',
name: 'WebApp'
});
},
handleStoreUpdate: function(newState) {
// updated state is passed in from the store
// update component's state which will trigger a render
this.setState(newState);
}
Dynamically created components
Components can also be created dynamically.
define(function(require) {
'use strict';
var
Component = require('Component'),
Popover = require('/component/Popover');
return Component.extend({
handlePopover: function(e) {
...
// data will be state for the Popover component
var popover = new Popover(data);
popover.render().$el.insertAfter(e.currentTarget);
}
});
});
Properties
tagName (may be defined as a function [@since 5.0])
Decides which tag should be used as component container. Defaults to 'div'.
className (may be defined as a function)
A CSS class name for the container.
attributes (may be defined as a function)
A hash of attributes that will be set on the container element.
define(function(require) {
'use strict';
var Component = require('Component');
return Component.extend({
tagName: 'header',
className: 'env-d--flex',
attributes: {
style: 'margin-top: 1em'
}
});
});
template
A component's primary purpose is to render a template. Read more about templates here. The following syntax is used to setting a template:
define(function(require) {
'use strict';
var
Component = require('Component'),
indexTemplate = require('/template/index');
return Component.extend({
template: indexTemplate
});
});
templateFunctions (may be defined as a function)
Extend the template context with custom functions and data. Read more here.
Events
The events hash is used to specify which events the component should respond to. Events are split into six different types:
- dom
- DOM events within the component (e.g. a button click)
- DOM events within the component (e.g. a button click)
- router
- Events when path or query changes in the URL
- self
- Events when component state changes
- store
- Event for store updates that affect the component
App and global events
- app
- Events triggered within the WebApp
App events are triggered from the app
-module:
var app = require('app');
...
app.trigger('products:updated');
- global
- Events triggered by another WebApp
Global events are triggered from the events
-module:
var events = require('events');
...
events.trigger('item:added');
An events hash may look similar to this:
// {'event': 'callback'}
events: {
dom: {
'click button': 'handleButtonClick' // user clicks a button
},
self: {
'state:changed': 'render', // any state change
'state:changed:price': 'render' // specific state change
},
router: {
'query:changed': 'render', // any query string change
'query:changed:layout': 'setLayout', // specific query string change
'path:changed' : 'handlePathChanged' // route path change
},
store: 'handleStoreUpdate', // store update that affects the component
app: {
'products:updated': 'toggleShowMore' // app.trigger('products:updated');
},
global: {
'item:added': 'handleItemAdded' // events.trigger('item:added');
}
}
Arguments passed to event callbacks will look similiar to this:
// dom event
callback: function(e) {
console.log(e); // jQuery event object
}
// app and global events
callback: function(options) {
console.log(options); // options passed when triggered
}
// self (state:changed)
callback: function(changedState) {
console.log(changedState); // object containing changed properties
}
// self (state:changed:property)
callback: function(newValue) {
console.log(newValue); // new value for changed property
}
// router (path:changed)
callback: function(options) {
console.log(options); // {path: newPath, url: url}
}
// router (query:changed)
callback: function(options) {
console.log(options); // {queryParams: queryParamsObject, url: url}
}
// store event
callback: function(newState) {
console.log(newState); // updated state from the WebApp's store
}
Utilities
$
Runs a component scoped find
. Short for this.$el.find()
$el
A reference to a cached jQuery object for the component's container.
getTemplate
Apply when logic is needed to decide which template to render. Read more about template rendering here.
renderTemplate(template, options)
Renders a template. Read more about template rendering here.
Lifecycle methods
render
Renders the component. Should not be overridden.
destroy
Destroys the component. Unbinds events and removes the element. Should not be overridden.
Lifecycle event callbacks
onInit
Called when component is initialized.
onRendered
Called when component is rendered.
onAttached
Called when component is attached to the document
onDestroy
Called when component is destroyed.
Example using lifecycle event callbacks:
define(function(require) {
'use strict';
var Component = require('Component')
return Component.extend({
onRendered: function() {
this.$('.env-is-active').addClass('highlight');
},
onDestroy: function() {
this.$('body').off('custom-event');
}
});
});
List Component
A List Component is used when rendering a collection. The List Component is an extension of the regular Component with a few custom properties/methods. One important difference is that the List Component does not have a template. It is simply a container for its children.
Properties
childProperty
The property on the state object that holds the collection that should be rendered. Must be an array. Remember to use filterState
to collect the correct property from the store.
childComponentPath
The path, from component/
, to the component representing a child in the collection.
childOptions (may be defined as a function)
Options that will be passed to the child component.
define(function(require) {
'use strict';
var ListComponent = require('ListComponent');
return ListComponent.extend({
tagName: 'ul',
childProperty: 'productItems',
childComponentPath: 'ProductItem',
childOptions: function() {
return {
productPageUrl: this.state.productPageUrl,
imageServerUrl: this.state.imageServerUrl,
currency: this.state.currency
};
},
filterState: function(state) {
return _.extend({}, {productItems: state.productItems});
}
});
});
Methods
addItem(item, options)
Dynamically add an item to the list. Pass {prepend: true}
as options to prepend the item, defaults to append.
getChildAt(index)
Get the component instance for a child.
getChildById(id) [@since 6.2]
Get the component instance of a child
removeChildById(id) [@since 6.2]
Removes the specified child from the list
triggerChildren(event, data)
Trigger event on all children.
define(function(require) {
'use strict';
var ListComponent = require('ListComponent');
return ListComponent.extend({
...
addItemDynamically: function(data) {
this.addItem(data);
},
navigateToActive: function() {
// trigger 'navigate' on the child component at index 0
this.getChildAt(0).trigger('navigate');
}
});
});
State for child components [@since 6.1]
A child component receives a second parameter (options) which contains the id of the specific item it renders (as well as childOptions, if specified). The id should be used to retrieve data from the store state.
// main.js
define(function(require) {
'use strict';
var Component = require('Component');
return Component.extend({
filterState: function(state, options) {
const entry = state.entries.find(entry => entry.id === options.id);
return Object.assign({}, entry);
}
});
});
Always provide an "id" attribute for list items.