Layer UI Component Definitions

Before customizing or creating a Component, its worth having a basic familiarity with the building blocks used to define a component. There are many details here that can be ignored until you work on deeper customization projects.

There are two techniques available for customizing an exiting component:

  • Use mixins to modify the existing definition
  • Create a Custom Component to replace the original definition

Creating a Custom Component can be done with any Webcomponent Framework. While documentation here will go on at length about using Layer UI to define your webcomponents, any webcomponent will be accepted.

However, if you want to Customize an Existing Component, you will need to understand this framework. How methods, properties and mixins are used in this framework are defined at length in Defining Components.

For purposes of maintenance, adding a mixin to your widget is both easier and more maintainable.

Defining a new widget gives you full control over the widget, but also makes it hard to take advantage of updates and fixes provided in new releases of Layer UI. All maintenance becomes entirely your problem. Defining new widgets is best for defining entirely new subcomponents that you will put within your templates; for example, a Message Item’s template could add a <custom-selection-widget />.

For simpler widgets such as <layer-date /> or <layer-avatar />, it may make more sense to build your own from scratch as these are relatively simple widgets and you may want very different rendering or behaviors for them.

Working with Mixins

A mixin can be used to:

  • Add new Events
  • Add new properties
  • Add new side effects for setting existing properties
  • Add new methods
  • Add new side effects for calling existing methods
  • Overwrite existing methods

Before reading about using mixins, it is strongly recommended that you become familiar with the Component Lifecycle. The lifecycle methods are typically what your mixin will hook into.

Adding an Event

Lets suppose that we want to add a button to an existing widget. Any time that button is clicked, an event will be triggered that your application (i.e. components that are not part of the Layer UI Framework) will listen for and respond to.

A button can be added by customizing the Template… but for this example, we will simply add it by adding a mixin with a custom onCreate method. Why onCreate? Because this is DOM manipulation that should be done during initialization, and which does not depend upon any property values.

Widgets trigger DOM events by calling:

this.trigger('my-custom-button-click', {
  frodo: "dodo",
  sauruman: "the wise",
  message: this.item
});

Such an event will bubble up the DOM, and can be received using:

document.body.addEventListener('my-custom-button-click', function(evt) {
  var frodo = evt.detail.frodo;
  var sauruman = evt.detail.sauruman;
  var message = evt.detail.message;
  alert(frodo + ' says ' + message.parts[0].body + ' to sauruman');
});

Here is a complete solution:

// Define the mixin
var mixinObj = {
  events: ['my-custom-button-click'],
  methods: {
    onCreate: function() {
      this.nodes.button = document.createElement('button');
      this.appendChild(this.nodes.button);
      this.nodes.button.addEventListener('click', this._onMyCompanyButtonClick.bind(this));
    },
    _onMyCompanyButtonClick: function(evt) {
      this.trigger('my-custom-button-click', {
        frodo: "dodo",
        sauruman: "the wise",
        message: this.item
      });
    }
  }
};

// Add the mixin to <layer-message-item-sent> and <layer-message-item-received>
layerUI.init({
  appId:  'layer:///apps/staging/UUID',
  mixins: {
    'layer-message-item-sent': mixinObj,
    'layer-message-item-received': mixinObj
  }
});

// Create the modified widget
var widget = document.createElement('layer-message-item-sent');

// We can use either  document.body.addEventListener('my-custom-button-click'),
// Or we can set the callback function:
widget.onMyCustomButtonClick = function(evt) {
  var message = evt.detail.message;
  alert('You have clicked a button.  Why did you do that?');
};

The Mixin above modifies the layer-message-item-sent and layer-message-item-received components:

  1. Adds a button to each Message Item
  2. Trigger the my-custom-button-click event whenever user clicks the button.
  3. Adds a onMyCustomButtonClick property. This property is automatically created as a result of listing my-custom-button-click in the events array, and it is automatically called when you call trigger on that event.

Note that the use of the mixins adds behaviors to onCreate but does not prevent other onCreate code from executing.

Add new behaviors to existing properties

Mixins can define new properties or add behaviors to existing properties. The syntax used in the mixin is the same. The only difference is that if modifying an existing property, only set is used; other attributes such as value and get are ignored.

Add a set method to your mixin to add side effects that trigger whenever that property is set:

var mixinObj = {
  properties: {
    client: {
      set: function(client) {
        this.properties.user = client.user;
      }
    }
  }
};

The above mixin can be added to any widget; it will be called any time the client property is set. If the widget already has a client property, both the widget’s setter and your setter will be called; order of call is not predetermined.

See Property Setters for more information on how setters work, and how mixins affect them.

Add new behaviors to existing methods

You can use the Mixin to add any method your widget needs. If the method already exists, both methods will be called. Above, is an example of adding a new method called _onMyCompanyButtonClick().

Recommended practices for adding mixin methods are:

  1. Adding entirely new method names is considered good and safe.
  2. Adding new methods that hook into lifecycle methods such as onCreate, onAfterCreate, onRender, onAttach, onDetach and onDestroy is good and is typically sufficient for most customizations. See Component Lifecycle methods for more detail.
  3. Adding new methods that hook into public methods that are not lifecycle methods is acceptable, and sometimes necessary, but always look to lifecycle methods first. Example: <layer-conversation-list /> provides an onClick method that is a natural method to hook into to cause side effects when a Conversation is selected.
  4. Adding new methods that hook into private methods is dangerous.
  5. Typically a method that is recommended for a mixin to hook into will have a name starting with on.

The following example shows a customization to onCreate:

var mixin = {
  methods: {
    onCreate: function() {
      var input = document.createElement("input");
      input.type = "search";
      this.appendChild(input);
    }
  }
};

layerUI.init({
  appId:  'layer:///apps/staging/UUID',
  mixins: {
    'layer-conversation-panel': mixin
  }
});

See Method Mixins for more information on how to hook into methods using mixins.

Creating Components

There are two reasons an app might want to create a new Component:

  1. To replace a Component provided by Layer. Perhaps you don’t like our <layer-avatar /> and want to replace it with your own.
  2. To create new subcomponents that show up in templates for your widgets. Perhaps your <layer-message-item-received /> needs a Menu button with some menu options; you can add <custom-message-item-menu-button /> to your template, and create a Custom Component named custom-message-item-menu-button.

Lets say we want to replace <layer-avatar /> with an entirely different way of presenting and interacting with the Avatar. Ultimately, your task is to use any Javascript Webcomponent framework you like to define a new layer-avatar HTML Tag (you can use the raw Webcomponent Polyfill, the framework provided here, or another of your choice). But first, we need to insure that there are not multiple declarations of the layer-avatar widget.

Note

Once an HTML tag such as layer-avatar is defined, it can not be redefined.

To understand how replacing a custom component works, its important to understand how layerUI.registerComponent works:

  • If layerUI.init() has not yet been called, your call to layerUI.registerComponent will register your component with Layer UI, but not (yet) with the browser
  • When layerUI.init() is called, all components registered with Layer UI will be registered with the browser
  • If layerUI.init() has yet been called, your call to layerUI.registerComponent will register your component with the browser

Tell the build to NOT register the built-in definition of <layer-avatar /> before the init() method using unregisterComponent:

// This will unload the definition of "layer-avatar" from layerUI
layerUI.unregisterComponent('layer-avatar');

// Register all components with the browser, except layer-avatar
layerUI.init({
  appId:  'layer:///apps/staging/UUID'
});

// Register a new definition of layer-avatar with the browser
layerUI.registerComponent('layer-avatar', customAvatarDefinition);

What is the significance of this? Once you have defined an HTML tag layer-avatar, it can not be redefined; but the above unregisterComponent() call insures that layer-avatar is not defined, allowing you to provide your own custom component at a later time. Note that once layerUI.init() is called, all components with Layer UI will be registered with the document.

Alternatively, you could simply overwrite the layer-avatar definition before calling layerUI.init():

// This will load the definition of "layer-avatar"
import layerUI from 'layer-ui-web';

// This will overwrite the definition of "layer-avatar" within layerUI
layerUI.registerComponent('layer-avatar', customAvatarDefinition);

// This will register the new "layer-avatar" with the browser
layerUI.init({
  appId:  'layer:///apps/staging/UUID'
});

Alternatively, if you want to use Webcomponents directly, you won’t be overwriting the Layer UI definition of layer-avatar, and should simply call unregisterComponent:

// This will register "layer-avatar" with the browser using raw webcomponents API
document.registerTag('layer-avatar', customAvatarDefinition);

// prevent layer-ui from trying to write a new definition for layer-avatar which would throw errors
layerUI.unregisterComponent('layer-avatar');

// Register all components with the browser, except layer-avatar
layerUI.init({
  appId:  'layer:///apps/staging/UUID'
});

Defining your Component

Defining your own layer-avatar widget can be done with any webcomponent framework, but this example will use the Layer UI framework for simplicity. The full API Layer UI for Web provides for defining Widgets can be seen in Defining Components.

Step 1: In your HTML file, load your custom component:

<link rel="import" href="my-webcomponents/my-layer-avatar.html">

Step 2: Define your component in my-layer-avatar.html (or any name you choose).

Note that the component below has random styling and behaviors… its here to show the structure of HOW to define a component, but isn’t intended to be useful code.

<template>
  <style>
    layer-avatar {
      display: block;
      border: solid 1px #ccc;
      padding: 5px 8px;
      height: 1.3em;
      border-radius: 4px;
      box-shadow: 2px 2px 6px #ccc;
      cursor: pointer;
    }
    layer-avatar:hover {
      box-shadow: 2px 2px 3px #ccc;
    }
  </style>
  <div class='name' layer-id='myName'></div>
  <button class='more-info' layer-id='infoButton'>More Info</button>
</template>
<script>
// layerUI must be defined as a global for layerUI.registerComponent() to be a function.
layerUI.registerComponent('layer-avatar', {
  properties: {
    /*
     * The existing layer-avatar takes an array of Users as its key property. Before replacing a
     * component you need to know what properties will be passed to it
     * from its parent components.  The API reference should make this clear.
     *
     * The new value is written to this.properties.users BEFORE this setter is called,
     * so no need to store the value input.
     */
    users: {
      set: function(value) {
        this.onRender();
      }
    }
  },
  methods: {
    // Called when the widget is created
    onCreate: function() {
      // the nodes object is setup using the layer-id attribute
      this.nodes.infoButton.addEventListener('click', this.onClick.bind(this));
    },

    // Called when the user clicks the infoButton
    onClick: function() {
      var user = this.users[0].toObject();
      var text = '';

      // Generate some text describing the user
      Object.keys(user).forEach(function(keyName) {
        text += keyName + ': ' + user[keyName] + '\n';
      });

      // Show an alert with all known info about this user.
      // OK, a dialog Would be nicer here...  as would some html formatting...
      alert(text);
    },

    // Called any time the `users` is set; also called as part of the widget lifecycle.
    onRender: function() {
      var user = this.properties.users[0];
      if (user) {
        // Show the first name, or the displayName for the first user.
        this.nodes.myName.innerHTML = user.firstName || user.displayName;
      }
    }
  }
});

// Initialize the template for this component. See examples elsewhere for full
// registerTemplate usage.
layerUI.registerTemplate('layer-avatar');
</script>

One issue with the approach described here: you might prefer to build all of your javascript into one build file, and not have your templates downloaded separately. See Defining Build Friendly Templates. Also see the Template Property and the Style Property

Using a Template String

To create a build file that contains your entire component definition, you can move template and styling into the template and style properties. It may be helpful to develop your template in a separate file, and have a build script that moves all HTML tags into template and all CSS tags into style.

layerUI.registerComponent('layer-avatar', {
    template: '<div class="div-one"><button/></div>',
    style: 'layer-avatar {display: block;}',
    properties: {...},
    methods: {...}
});

Note that you should not include <template> nor <style> in your template string, nor <style> or any other html tags within your style string.

Customizing with a Flux Architecture

Note that there is an example of how to make a custom component that uses a React Component in Defining A Component. Lets suppose however that your just modifying an existing component, and your code needs to trigger redux actions and access redux state.

To address this, all components support a state property, and any time the state is set:

  • all of its child components also have their state properties updated
  • Each time a state property is updated, its onRenderState method is called.

This means that any and all components you customize should expect to have the same state, and that you can put all rendering code that triggers whenever state changes within your onRenderState; you may also want to call this from your onRender method.

var composerMixin = {
  methods: {
    onCreate: function() {
      // Block the default sending behavior, and use the redux actions instead
      this.addEventListener('layer-send-message', function(evt) {
        evt.preventDefault();
        this.state.reduxActions.send(message, notification);
      }.bind(this));
    },
    onRenderState() {
      if (this.state.reduxState.disabled) {
        this.classList.add('disabled');
      } else {
        this.classList.remove('disabled');
      }
    }
  }
};

Then within your React render method:

import layerUI from 'layer-ui-web';
const { ConversationPanel } = layerUI.adapters.react(React, ReactDom);
...
render() {
  <ConversationPanel
    state={this.state} />
}

Be aware that state contains whatever you want it to contain, and that means you control how properties/actions/etc… are to be accessed from the state property.

Defining a Component Custom Messages