Customization

A core design goal of this library is ease of customization so that both look-and-feel as well as behaviors can match your design goals.

Custom Cards

Each <layer-message-item /> renders its message contents within the DOM node referenced by its listItem.nodes.content property. It does this by finding a suitable component capable of rendering that type of Message. A suitable component is found by iterating over all registered message handlers for one that is willing to handle the message of the given set of MIME Types. Some default handlers come with this framework for rendering text/plain messages and images.

You can register a custom handler as easily as:

layerUI.registerMessageHandler({
  tagName: 'text-mountain-part',
  label: 'Mountain received',
  handlesMessage: function(message) {
    return message.parts.length === 1 && message.parts[0].mimeType === 'text/mountain';
  }
});

The call has the following properties:

  • tagName: HTML tag name to use for messages matching this handler; in this case: <text-mountain-part />
  • label: Label to use when there isn’t space to render the full content. Typically seen when rendering the most recent message next to a Conversation title within the Conversation List.
  • handlesMessage: Returns true/false to indicate if it knows how to render this message.

After registering the above handler, for any Message that passes the handlesMessage test, the following call will be made:

var messageHandler = document.createElement('text-mountain-part');
messageHandler.message = message;
this.nodes.content.appendChild(messageHandler);

For the above code to work, you must have registered a tag name of text-mountain-part, which you can do using any Webcomponents framework. If using the Webcomponent framework provided here, you can use the convenience method to both define the handler and register it: layerUI.registerMessageComponent

import layerUI from 'layer-ui-web';
const MessageHandler = layerUI.mixins.MessageHandler;

layerUI.registerMessageComponent('text-mountain-part', {
  mixins: [MessageHandler],
  properties: {
    label: {
      value: "Mountain of Data Received"
    }
  },
  methods: {
    handlesMessage: function(message) {
      return message.parts[0].mimeType === 'text/mountain';
    },
    onCreate: function() {
    },
    onRender: function() {
      this.innerHTML = 'Mountains ' + this.properties.mountain +
        ' are taller than Plains. And who Texts a Plain?';
    },
    onRerender: function() {

    },
    onSent: function() {

    }
  }
});

The above Message Handler does not (yet) render an interactive card, but by defining a Template for text-mountain-part, and adding events, interactions, styling we can assemble a rich interactive card.

The Message Handler Mixin provides the following:

  • Defines the message property that will receive the message to be rendered
  • Wires up onRender to be called once we have a message
  • Wires up onRerender to be called both by onRender, and any time the Message is updated – typically done via read and delivery receipts being received from other users.
  • If the message was created and message.presend() called instead of message.send() then onSent() will be called allowing you to update state and rendering once the Message is sent. Presend lets you show a preview of the Message that is being generated before its sent… also commonly used to show what is being prepared for sending if there are asynchronous steps to gather data prior to sending.

Note that when calling layerUI.registerMessageComponent instead of layerUI.registerMessageHandler:

  • label is a property of your component
  • handlesMessage is a method of your component
  • tagName is the first argument of registerMessageComponent

Custom Text Processing

There is a lot processing that can be done to text. This framework ships with some of these:

  • Turning emojis into images (enabled by default)
  • Turning URLs to images into actual images (enabled by default)
  • Turning Youtube video links into Youtube Video Player (enabled by default)
  • Rendering urls as hyperlinked urls (enabled by default)
  • Turning newline characters into <br/> tags (enabled by default)
  • code-blocks: Those typing ticks into the composer as part of sending code blocks can receive rendered code (disabled by default)

There are two types of customization you may want to do with text processing:

  1. Select handlers other than the set that has been preselected for you
  2. Defining new text processors

Selecting default handlers

If you don’t like the default set of handlers being used for your app (perhaps rendering of youtube videos is distracting for your users), you can name which handlers are used in your init() call:

layerUI.init({
  appId: 'layer:///apps/staging/UUID',
  textHandlers: ['emoji', 'autolinker', 'newline']
});

This specifies that emojis, linked urls and newline => BR tag processing will be done, but that other text processors will not be used.

You can find the available text processors in src/handlers/text, and the names used to enable them should match the file name.

Defining new handlers

Any text processor you register via layerUI.registerTextHandler will be enabled regardless of what parameters are used in the init() method.

A text processor can affect the result in two ways:

  1. It can update the text of the message (i.e. replace a url with an anchor tag)
  2. It can specify something to be rendered after the Message (i.e. an image, video, etc…)

Updating the message text

To illustrate the first, lets suppose we wanted to replace the word Layer with the Layer Logo everywhere it appears within a Message:

layerUI.registerTextHandler({
  name: 'layer-logo-replacer',
  order: 300,
  handler: function(textData) {
    textData.text = textData.text.replace(/\blayer\b/gi,
      ' <img src="https://cdn.layer.com/web/logo-black.svg" /> ');
  }
});

This updates the textData object’s text property which is the text that will be rendered after all text processors have run, and each completed their modifications to the textData.text.

Be warned however that updating text with multiple text handlers can cause problems. Running a text handler that replaces urls with images, and then running a second text handler that replaces urls with links may result in images with anchor tags within them.

You can control the order that text processors run using the order property. However, for best results, minimize modification of text and favor modifying textData.afterText where possible.

Appending new content to show after the text

This youtube parser pushes its HTML into the textData.afterText array; it will be rendered after the text portion of the message. The text parsers will not parse values in afterText so order will not matter for this technique.

window.layerUI.registerTextHandler({
  name: 'my-youtube-processor',
  order: 300,
  handler: function(textData) {
    var matches = textData.text.match(/https:\/\/youtu\.be\/([a-zA-Z0-9\-]+)/g) || [];
    matches.forEach(function(match) {
      var videoId;
      var matches = match.match(/https:\/\/youtu\.be\/(.*)$/);
      if (matches) videoId = matches[1];
      if (videoId) {
        textData.afterText.push(
          '<iframe width="560" height="315" src="https://www.youtube.com/embed/' + videoId +
            '" frameborder="0" allowfullscreen></iframe>'
        );
      }
    });
  }
});

Appending new content with asynchronous behaviors

First of all, its important that the vertical space allocated for your message does not change. That means that even if you don’t know how tall of a space your asynchronously loaded content needs, you should still preallocate a fixed height before it loads, and attempt to constrain yourself to that height after it loads.

A simple example of using an asynchronous text handler uses the Layer UI framework for defining a component:

Step 1: Register a handler that finds any occurance of ipsum lorem and pushes a <ipsum-lorem-handler/> into afterText:

layerUI.registerTextHandler({
  name: 'ipsum',
  handler: function(textData, message, isMessageListItemComponent) {
    if (isMessageListItemComponent) {
      var matches = textData.text.match(/ipsum lorem/);
      if (matches) {
        textData.afterText.push('<ipsum-lorem-handler></ipsum-lorem-handler>');
      }
    }
  }
});

Step 2: The <ipsum-lorem-handler/> preallocates height within its style property; sets an overflow-y: auto just in case its not enough height, and then manages the asynchronous activity of fetching and rendering data internally. Note that as long as only a single custom component defined using Layer UI is pushed into afterText, the widget.properties.parentComponent will be set and will point to the Layer Message Item.

layerUI.registerComponent('ipsum-lorem-handler', {
  style: 'ipsum-lorem-handler {height: 100px; display: flex; flex-direction: column; justify-content: center; overflow-y: auto;}',
  methods: {
    onAfterCreate() {
      layer.xhr({
        url: "https://baconipsum.com/api/?type=meat-and-filler&paras=1&start-with-lorem=1&format=text",
      }, this._processResult.bind(this));
    },
    _processResult(result) {
      setTimeout(function() {
        this.innerHTML = this.parentComponent.message.parts[0].body.replace(/ipsum lorem/, result.data);
      }.bind(this), 1000);
    },
  }
});
Modifying Components API Reference