WebApp components

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 use RequireJS to 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});
      }
   });
});

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)

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 specify which events the components should respond to. Events are split into six different types.

  • dom
    • DOM events within the component (e.g. a button click).
  • app
    • Events triggered within the WebApp
  • router
    • Events when path or query changes in the URL
  • global
    • Events triggered by another WebApp
  • self
    • Events when component state changes
  • store
    • Event for store updates that affect the component

An events hash may look similar to this:

// {'event': 'callback'}

events: {
   app: {
      'products:updated': 'toggleShowMore' // app.trigger('products:updated')
   },
   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
   },
   global: {
      'item:added': 'handleItemAdded' // events.trigger('item:added')
   },
   store: 'handleStoreUpdate' // store update that affects the component
}	

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.

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 a child component

A component that is part of a List Component will automatically have its state populated with data from the corresponding entry in the collection. Use the filterState function to collect any additional information from the WebApp's store.