Deprecated

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)
  • 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.