Defining New Components

Components are defined using layerUI.registerComponent; once a component has been defined, you can use that component directly in your HTML <my-component></my-component> or in a document.createElement('my-component').

A new component is created using the following keys:

Name Type Description
properties Object Defines all of the properties of the component
methods Object Defines all of the methods of the component
mixins Array[Object] Provides an array of mixins to add to the object
events Array[String] Defines an array of event names that will be published by this component
listeners Object Defines events to listen for and the methods to call when they occur
template String Define the template for this component
style String Define the styles for this component
 var componentDefinition = {
   events: ['event-one', 'event-two', 'event-three', 'event-four'],
   mixins: [mixinObj1, mixinObj2, mixinObj3],
   properties: {
      prop1: {
          set: function(value) {
              this.myRenderer();
          }
      },
      prop2: {
          get: function() {
              return this.scrollTop;
          },
          set: function(newValue, oldValue) {
              this.scrollTop = newValue;
          }
      },
      prop3: {
          value: "Frodo is a Dodo"
      },
      prop4: {
          type: Function
      }
   },
   methods: {
     onCreate: function() {
        alert("The widget has been created");
     },
     myRenderer: function() {
        this.innerHTML = this.properties.prop1;
     }
   }
 };

A component defined this way can be registered as follows:

 var layerUI = require('layer-ui-web');
 layerUI.registerComponent(tagName, componentDefinition);

Properties

A property definition can be as simple as:

 layerUI.registerComponent(tagName, {
    properties: {
       prop1: {}
    }
 });

The above code declares prop1 to be a property, sets up a setter that writes widget.properties.prop1 any time widget.prop1 is set, and sets up a getter to read the value from widget.properties.prop1. It also insures that at initialization time, if a prop1 attribute is found, it will be used as the initial value for the prop1 property.

Property Definitions support the following keys:

Name Description
set A setter function to be called any time your property changes.
get A getter is needed if this.properties.propName does not contain the latest value.
value The default value for the property
type Converts the input to the specified type
noGetterFromSetter (Advanced) Do not use the getter function from within the setter
propagateToChildren (Advanced) Propagate any values of this property to all child nodes
order (Advanced) Should this property be evaluated prior to or after other properties are evaluated?
mode (Advanced) If mixins are used to modify this property, which setters for this property should be called in what order?

Property Definition set

Note that your setter function is called AFTER this.properties.propName has been set with the new value; your setter is for any side effects, rendering updates, or additional processing and NOT for writing the value itself.

 layerUI.registerComponent(tagName, {
    properties: {
       title: {
         set(newValue, oldValue) {
           this.innerHTML = newValue;
         }
       }
    }
 });

Setters are called any time a value changes; a setter will not be called if the new value is equivalent to the old. Setters are called with the new value, and the prior value.

// This calls the setter with ("Frodo the Dodo", null);
widget.title = "Frodo the Dodo";

// This calls the setter with ("New Title", "Frodo the Dodo");
widget.title = "New Title";

// This does not call the setter
widget.title = "New Title";

A common use for the prior value is to remove any event handlers from the prior value:

 layerUI.registerComponent(tagName, {
    properties: {
       conversation: {
         set(newValue, oldValue) {
           if (oldValue) oldValue.off(null, null, this);
           if (newValue) newValue.on('conversations:change', this.onRerender, this);
         }
       }
    }
 });

Property Definition get

Typically, you do not need a get property on your property definition. The default getter returns widget.properties.propertyName. But perhaps the getter needs to return values/state from subcomponents rather than from whatever was last set on a property:

 layerUI.registerComponent(tagName, {
    properties: {
       value: {
         set(newValue, oldValue) {
           this.nodes.input.value = newValue;
         },
         get() {
           return this.nodes.input.value;
         }
       }
    }
 });

Property Definition value

If a value is provided, then this will be the default value of your property, to be used if a value is not provided by the component creator. This calls the setter.

 layerUI.registerComponent('widget-with-title', {
    properties: {
       value: "I am a Title",
       title: {
         set(newValue, oldValue) {
           this.innerHTML = newValue;
         }
       }
    }
 });

 document.createElement('widget-with-title');

This calls the setter with “I am a title”. However, if instead we did:

 var el = document.createElement('widget-with-title');
 el.title = "I am a subtitle";

Then the setter would never be called with “I am a Title”, but would only be called with “I am a subtitle”.

Property Definition type

Currently accepts

  • Boolean
  • Number
  • Function

Using a type makes the system more forgiving when processing strings. This exists because attributes frequently arrive as strings due to the way HTML attributes work.

For example:

  • if type is Boolean, then the strings “false”, “null”, “undefined”, “” and “0” are evaluated as false; all other values are true
  • Using this with functions will cause your function string to be “evaled”, but will lose your function scope (no access to this).
  • Using this with a number will turn “1234” into 1234

Typically, we only specify a type on components that will be directly created by applications; subcomponents that go within our components would expect to have their properties set directly, and not via Attributes. This means only components that applications directly create should expect Attributes, and must handle the possiblity that they will be strings rather than the desired type.

var MainComponent = require('layer-ui-web/lib-es5/mixins/main-component');
layerUI.registerComponent('my-main-component', {
  mixins: [MainComponent],
  properties: {
    isEnabled: {
      type: Boolean
    }
  }
});

The above code insure that widget.isEnabled will always return a Boolean (or null if no value has been set)

<my-main-component is-enabled="true"></my-main-component>

The above code initizlies isEnabled to a boolean of true.

Property Definition noGetterFromSetter

Note

This is an advanced property definition that is not needed for most components

Your setter is called with (newValue, oldValue). oldValue is retrieved using a getter. This insures it gets the correct and latest value (see getter up above for why this could differ from widget.properties.propertyName). However, there are times when the getter does too much, and you want to just leave oldValue as whatever is in widget.properties.propertyName. Use noGetterFromSetter to insure that your getter isn’t unnecessarily called. Here’s an exagerated example:

layerUI.registerComponent('my-exagerated-component', {
  properties: {
    status: {
      get() {
        var result;
        xhr({
          sync: true,
          method: "GET",
          url: "/status"
        }, function(response) {
          result = response.data;
        });
        return result;
      },
      set(newValue, oldValue) {
        xhr({
          method: "POST",
          url: "/status",
          data: newValue
        });
      },
      noGetterFromSetter: true
    }
  }
});

In the above example, we would want to avoid wasted calls to the getter!

Property Definition propagateToChildren

Note

This is an advanced property definition that is not needed for most components

There are some properties which should be propagated to all children. Such properties, any time they are set, will pass that value on to each node listed in widget.nodes. Suppose you have a property, “reduxState”; any time that state changes, you want it to be available in all of the subcomponents of your component. You could write a setter for each of your components to insure it gets propagated down the subtree. Or you can use propagateToChildren.

layerUI.registerComponent('my-redux-friendly-component', {
  template: "<my-friendly-subcomponent layer-id='subcomponent'></my-friendly-subcomponent>
  properties: {
    reduxState: {
      propagateToChildren: true,
      set: function() {
        this.onStateChange();
      }
    }
  },
  methods: {
    onStateChange() {}
  }
});


layerUI.registerComponent('my-friendly-subcomponent', {
  template: "<another-component layer-id='anothercomponent'></another-component>
  properties: {
    reduxState: {
      propagateToChildren: true,
      set: function() {
        this.onStateChange();
      }
    }
  },
  methods: {
    onStateChange() {}
  }
});

Using the two component definitions above, if the following code were run:

widget.reduxState = myState;
  1. The my-redux-friendly-component setter would be called, in the example above, the onStateChange method would be called.
  2. Every subcomponent with a layer-id will have its reduxState property set; so my-friendly-subcomponent defined to be a subcomponent using the template string will have its reduxState property set.
  3. The my-friendly-subcomponent setter will be called
  4. my-friendly-subcomponent will apply its setter to its subcomponents such as another-component

Note that the example above is not something you need to implement yourself; there is a state property built into all Components designed to insure that application state can be passed around to all components.

Property Definition order

Note

This is an advanced property definition that is not needed for most components

For most properties, order of evaluation has little impact; this is rarely needed. Most developers should ignore this section.

However, there are some properties where order matters. For example, perhaps you want to insure that you have a client property before allowing another setter to be called.

Under normal conditions, that isn’t a problem; consider the following widget:

var el = document.createElement("my-widget");
el.conversationTitle = "New Title";
el.client = client;

Now suppose that the conversationTitle setter needs the client because its going to perform some operation using the client.

At the time that the setters are called above, BOTH properties have their values available; widget.properites.propertyName would both be written before any setters are called in the above example. Therefore, no ordering is needed.

Consider instead this example:

layerUI.registerComponent({
  properties: {
    appId: {
      order: 1,
      set(value) {
        this.client = layer.Client.getClient(value);
      }
    },
    client: {},
    conversationId: {},
    conversationTitle: {
      set(value) {
        var conversation = this.client.getConversation(this.conversationId);
        conversation.setMetadataProperties({
          conversationName: value
        });
      }
    }
  }
});


var el = document.createElement("my-widget");
el.conversationTitle = "New Title";
el.appId = appId;

The converationTitle still depends upon a client. But rather than widget.properties.client being set, widget.properties.appId is set.

Only by calling the appId setter first will client be available to conversationTitle. Any property with an order will be called prior to any property without an order. For the above component definition, appId setter will always be called first and set the client prior to conversationTitle setter being called.

  • Properties with order have their setters called before those without order
  • Properties with a lower order have their setters called before those with a higher order

Property Definition mode

Note

This is an advanced property definition that is not needed for most components

The mode property definition is for use with Mixins. A mixin allows one to add additional setters to a property:

var conversationPanelMixin = {
  properties: {
    conversationId: {
      mode: layerUI.registerComponent.MODES.BEFORE,
      set(newValue, oldValue) {
        if (newValue.indexOf("layer:///conversations") !== 0) this.properties.conversationId = "";
      }
    }
  }
};

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

Under normal conditions, a mixin such as the example above provides a second setter for the conversationId property. Order of execution of setters is typically arbitrary. After all, setters are not for assigning the value; thats already written to widget.properties.conversationId. Its for side-effects. Side-effects typically don’t impact each other.

The mode Property Definition allows you to declare that a setter for the property gets called before other setters for the property. In the example above, we are conditionally clearing the property value before any other setter can use it. The layerUI.registerComponent.MODES.BEFORE mode is used to insure its called before the default setter.

Possible values for mode are:

  • layerUI.registerComponent.MODES.BEFORE
  • layerUI.registerComponent.MODES.AFTER

Methods

You may provide any methods you want within the methods hash; be aware though that some methods names are reserved for use by the framework, and some have specific Lifecycle implications for the widget.

At their simplest, a method can be defined simply with:

layerUI.registerComponent(tagName, {
  methods: {
    myMethod1: function() {

    },
    myMethod2: function() {

    }
  }
});

Reserved Method Names

The following method names are reserved by the Webcomponents API:

  • createdCallback
  • attachedCallback
  • detachedCallback
  • attributeChangedCallback

Method Definition mode

Note

This is an advanced method definition that is not needed for most components

Mixins can be used to add side effects or provide a different implementations of a method. By default, the following code will call both myMethod functions in an arbitrary order.

var mixin = {
  methods: {
    myMethod: function() {
      console.log("Mixin Called");
    }
  }
};

// Define the component using the above mixin
layerUI.registerComponent('mixin-demo', {
  mixins: [mixin],
  methods: {
    myMethod: function() {
      console.log("Original Called");
    }
  }
});

var el = document.createElement('mixin-demo');
el.myMethod();

The above code will log “Mixin Called” and “Original Called” in arbitrary order.

We can control the order using the mode Method Definition:

var mixin = {
  methods: {
    myMethod: {
      mode: layerUI.registerComponent.MODES.BEFORE,
      value: function() {
        console.log("Mixin Called");
      }
    }
  }
};

// Define the component using the above mixin
layerUI.registerComponent('mixin-demo', {
  mixins: [mixin],
  methods: {
    myMethod: function() {
      console.log("Original Called");
    }
  }
});

var el = document.createElement('mixin-demo');
el.myMethod();

The above code will always log Mixin Called prior to Original Called.

Possible values for mode are:

  • layerUI.registerComponent.MODES.BEFORE: Call this method before other versions of the method
  • layerUI.registerComponent.MODES.AFTER: Call this method after other versions of the method
  • layerUI.registerComponent.MODES.DEFAULT: Leave order unchanged; typically not needed
  • layerUI.registerComponent.MODES.OVERWRITE: Replace all other definitions of this method with this one alone

Method Definition conditional

Note

This is an advanced method definition that is not needed for most components

A conditional key may be added to your method definition to determine if the method is allowed to be called. Multiple conditional keys are allowed as long as only one is provided by each Mixin.

var mixin = {
  methods: {
    myMethod: {
      mode: layerUI.registerComponent.MODES.BEFORE,
      value: function() {
        console.log("Mixin Called");
      },
      conditional: function() {
        return this.value > 5;
      }
    }
  }
};

// Define the component using the above mixin
layerUI.registerComponent('mixin-demo', {
  mixins: [mixin],
  property: {
    value: {}
  },
  methods: {
    myMethod: function() {
      console.log("Original Called");
    },
    conditional: function() {
      return this.value < 10;
    }
  }
});

var el = document.createElement('mixin-demo');
el.myMethod();

The above mixin defines a method myMethod that can only be called if the value property is greater than 5 and less than 10. If either of these conditions fail, no versions of myMethod will be called.

Mixins

Mixins can be added to a widget in two ways:

  • A Component may add a mixins array to its definition
  • An Application, initializing the framework via layerUI.init() may pass in mixins into the init call.

Using Mixins from the Component

A component can include any number of Mixins by adding them to the mixins Array:

// Define a Mixin that can contains `properties`, `methods` and `events`:
var mixinObj1 = {
   properties: {
     prop2: {}
   }
});

var mixinObj2 = {
  properties: {
     prop3: {}
   }
});

// Add mixinOb1 and 2 to our Component
var componentDefinition = {
   mixins: [mixinObj1, mixinObj2],
   properties: {
      prop1: {}
   }
});

// Create a Component with prop1, prop2 and prop3
registerComponent(tagName, componentDefinition);

Using Mixins layerUI.init

An app can modify an existing component by adding custom mixins to it using layerUI.init(). The mixins parameter takes as key the tag-name for any widget you want to customize; (e.g layer-messages-item-sent, layer-messages-list, layer-conversation-panel, etc…)

The following example adds a search bar to the Message List:

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

 layerUI.init({
   appId: 'my-app-id',
   mixins: {
     'layer-messages-list': mixinObj
   }
 });

Events

As part of your layerUI.components.Component.registerComponents call you can pass in an events array; this is an array of strings representing events to listen for, and provide as property-based event listeners.

Example:

 layerUI.registerComponent(tagName, {
    events: ['layer-something-happening', 'layer-nothing-happening', 'your-custom-event']
 });

The above component definition will result in:

  1. The component will listen for the 3 events listed, regardless of whether this component triggers the event, or its child components triggers the event.
  2. The component will define the following properties: onSomethingHappening, onNothingHappening and onYourCustomEvent. These properties are defined for you, you do not need to do anything more than list the events in the events array for these to be defined.
  3. Your app can now use either event listeners or property callbacks as illustrated below:

Event Listeners:

document.body.addEventListener('layer-something-happening', myFunc);
document.body.addEventListener('layer-nothing-happening', myFunc);
document.body.addEventListener('your-custom-event', myFunc);

Property callbacks:

widget.onSomethingHappening = myFunc;
widget.onNothingHappening = myFunc;
widget.onYourCustomEvent = myFunc;

You can trigger events using the trigger(eventName, details) call:

layerUI.registerComponent(tagName, {
    events: ['layer-something-happening', 'layer-nothing-happening', 'your-custom-event']
    methods: {
      myMethod: function() {
        this.trigger('layer-something-happening', {
          frodo: "dodo",
          sauruman: "the wise"
        });
      }
    }
});

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

document.body.addEventListener('layer-something-happening', function(evt) {
  var frodo = evt.detail.frodo;
  var sauruman = evt.detail.sauruman;
  alert(frodo + ' fled ' + sauruman);
});

Note that all paramters of your triggered event are accessed via event.detail as illustrated above.

Listeners

A component can be configured to listen for events that have bubbled up to document.body; if they haven’t been intercepted/handled, then this component will handle those events.

layerUI.registerComponent('my-widget', {
    listeners: {
      'layer-notification-click': function notificationClick(evt) {
        const message = evt.detail.item;
        const conversation = message.getConversation();
        // do something...
      },
      'layer-send-message': function notificationClick(evt) {
        const message = evt.detail.item;
        // do something...
      },
    }
});

Note that the listeners will listen to all events matching the keys; in the above example, it listens for layer-notification-click and layer-send-message and calls the specified method if they are triggered.

The method is only called if the event was emitted from a node that this widget has named in its listenTo property.

<layer-conversations-list id='list'></layer-conversations-list>
<layer-notifier id='notifier'></layer-notifier>
<layer-conversation-panel listen-to='list, notifier'></layer-conversation-panel>

The layer-conversation-panel will only call its listeners on events that are triggered by the widget whose id is list, and the widget whose id is notifier as is specified in the listen-to attribute. Any other source of events will be ignored.

Any widget can do this; using the my-widget example above:

<my-widget listen-to="composer1"></my-widget>
<layer-composer id="composer1"></layer-composer>

Any time the layer-composer triggers a layer-send-message event, my-widget will have its listener method called.

Template

While you can load your template from an html file using:

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

This involves more requests and more work at runtime. You can instead embed your template into your template property as a string. This will help you generate a single build file that loads everything you need… but is also harder to edit and maintain. For a more efficient solution: edit a dedicated html file, and then have a build process that reads that file and converts it to a string.

The following is a valid way to declare your template:

layerUI.registerComponent('my-friendly-component', {
  template: "<my-friendly-subcomponent layer-id='subcomponent'></my-friendly-subcomponent>"
});

The template property should not contain <style /> tags, nor <script /> tags. It should contain your child nodes only.

Style

Your template may contain some styling. If so, and your converting your template into a string, you’ll need to provide your styling separately. The style property only accepts CSS rules, and no HTML tags such as <style>.

layerUI.registerComponent('my-friendly-component', {
  template: "<my-friendly-subcomponent layer-id='subcomponent'></my-friendly-subcomponent>",
  style: "my-friendly-subcomponent {display: block;}"
});

Note that the styles that should be built into your component should deal only with how nodes within your widget are laid out, and should leave colors and styling to your application themes. See Theming for more details.

React Components

It is possible to define a webcomponent that contains code from other frameworks. This example shows how one can define a custom widget that uses a React Component MyReactComponent to handle all rendering and state:

layerUI.registerComponent('my-react-component', {
  properties: {
    message: {
      set(newValue, oldValue) {
        this.onRender();
        if (newValue) newValue.on('messages:change', this.onRender, this);
        if (oldValue) oldValue.off(null, null, this);
      }
    },
  },
  methods: {
    onRender: function() {
      ReactDOM.render(<MyReactComponent message={this.message} />, this);
    }
  }
});
Lifecycle Modifying Components