Customizing XDK UI Components

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 three techniques available for customizing an exiting component:

  • The The replaceableContent Property enables the addition of custom html and components to predefined parts of a UI 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. However, only Webcomponents built using the Web XDK implement the expected lifecycle methods, and therefore it is recommended that webcomponents be built using approaches documented here.

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.

Adding a mixin to components is both easier and more maintainable than replacing an existing component by defining your own component from scratch.

Defining a new component gives you full control over the component, but also makes it hard to take advantage of updates and fixes provided in new releases of the Web XDK. All maintenance becomes entirely your problem. Defining new components 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-component />.

For simpler components such as <layer-date /> or <layer-avatar />, it may make more sense to build your own from scratch as these are relatively simple components 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 when setting existing properties
  • Add new methods
  • Add new side effects when 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.

Mixins are registered with Layer using the Layer.init() call. This call takes a mixin parameter that:

  • Is a hash of component names you are customizing
  • For each component, provides a single mixin or an array of mixins
Layer.init({
  mixins: {
    'layer-message-item-sent': [mixinObj1, mixinObj2],
    'layer-message-item-received': mixinObj
  }
});

Adding an Event

Let us suppose that we want to add a button to an existing component. Any time that button is clicked, an event will be triggered that your application (i.e. components that are not part of the Web XDK) will listen for and respond to.

A button can be added by customizing the Template or replaceableContent 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 is done during once, during initialization, and which does not depend upon any property values.

How Events are Triggered

Components 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;
  var messageTypeModel = message.createModel();
  alert(frodo + ' says ' + messageTypeModel.quotableStatementFromSillyCharacter + ' to sauruman');
});
Example of Adding an Event

Here is a complete solution:

import Layer from '@layerhq/web-xdk';

// 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>
var client = Layer.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 init() call above modifies the MessageListPanel.SentItem and MessageListPanel.ReceivedItem components using the events and methods of mixinObj:

  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; its value is a function that the consumer of the widget provides, and that function is automatically called when that event is triggered via the trigger() method on it or any of its child nodes.

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 regardless of whether defining a new property or modifying an existing one. When modifying properties, its important to understand that the set method is the only property of a Property Definition that can have many definitions (every setter gets called) while other parts of the Property Definition can only have one value (there can only be one default value, one getter, etc…). This means that if a value or get is already defined, your mixin’s value for these will be ignored; the set method will always be used.

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

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

The above mixin can be added to any widget; it will be called any time the disabled property is set. If the widget already has a disabled 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:

import Layer from '@layerhq/web-xdk';
var mixin = {
  methods: {
    onCreate: function() {
      var input = document.createElement("input");
      input.type = "search";
      this.appendChild(input);
    }
  }
};

var client = Layer.init({
  appId:  'layer:///apps/staging/UUID',
  mixins: {
    'layer-conversation-view': 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 Layer’s <layer-avatar /> component doesn’t suit a particular design, and needs to be replaced it with a custom built Avatar.
  2. To create new subcomponents that show up in templates for your components. Perhaps the <layer-message-item-received /> needs a Favorites button so users can flag important messages; you can add <my-favorites-button /> to your template (or to your replaceableContent), and create a Custom Component named my-favorites-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 Layer.UI.registerComponent works:

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

There are two ways to use this process to insure that only your definition gets used:

  1. Overwrite the existing definition
  2. Unregister the existing definition

Overwrite Existing Component Definitions

If you are defining the new component before calling Layer.init() then you can simply overwrite the existing definition:

// This will load the original definition of "layer-avatar" and register it with the Web XDK
import Layer from '@layerhq/web-xdk';

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

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

Unregister Existing Component Definitions

Unregistering a previous Component Definition is useful if you either will be defining your component after calling Layer.init() or you cannot gaurentee the order of events.

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

// This will load the original definition of "layer-avatar" and register it with the Web XDK
import Layer from '@layerhq/web-xdk';

// This will unload the original definition of "layer-avatar" from the Web XDK
Layer.UI.unregisterComponent('layer-avatar');

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

// Register a new definition of layer-avatar with the browser
Layer.UI.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 Layer.init() is called, all components registered with the XDK will be registered with the document.

Unregistering is also helpful if you want to use Webcomponents directly, because other Webcomponent libraries won’t be overwriting the XDK definition of layer-avatar.

// 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
Layer.UI.unregisterComponent('layer-avatar');

// Register all components with the browser, except layer-avatar
Layer.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 XDK framework for simplicity. Note that its also best tested with Layer’s UI Framework defined components. The full API XDK for Web provides for defining Widgets can be seen in Defining Components.

Step 1: define your starting class:

import Layer from '@layerhq/web-xdk';
const UI = Layer.UI;
const registerComponent = UI.registerComponent;

registerComponent('layer-avatar', {
});

Step 2: Setup your template:

registerComponent('layer-avatar', {
  template: `
    <img layer-id='image' />
  `,
});

Step 3: Setup your layout specific CSS:

registerComponent('layer-avatar', {
  template: `
    <img layer-id='image' />
  `,
  style: `
    layer-avatar {
      display: block;
    }
  `
});

Step 4: Define your properties

Note that since we are providing a replacement for Avatar we must provide an item and users properties:

registerComponent('layer-avatar', {
  properties: {
    item: {
      set: function(value) {
        this.users = [this.item];
      }
    },
    users: {
      set: function(value) {
        this.onRender();
      }
    }
  }
});

Step 5: Define your rendering

registerComponent('layer-avatar', {
  methods: {
    // Called any time the `users` or `item` 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.image.src = 'https://myserver.co/avatars/' + user.id + '.png';
      }
    }
  }
});

**Step 6: Setup your event handlers

registerComponent('layer-avatar', {
  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);
    }
  }
});

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
    • Note: Only child components that have a layer-id defined within a template file are passed state values
  • 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 Layer from '@layerhq/web-xdk';
const { ConversationView } = Layer.UI.adapters.react(React, ReactDom);
...
render() {
  <ConversationView
    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 Introduction