Customizing elements

You can extensively customize Elix elements for the context of your application. There are several techniques for doing this:

Slots

Most Elix elements accept content via their default slot. Some complex elements have multiple slots. E.g., Carousel defines slots inside its left and right arrow buttons. You can fill those slots with custom arrow icons or other content:

<elix-carousel>
  <div slot="arrowButtonLeft"></div>
  <div slot="arrowButtonRight"></div>
  <img src="image01.jpg">
  <img src="image02.jpg">
  <img src="image03.jpg">
  <img src="image04.jpg">
  <img src="image05.jpg">
</elix-carousel>

The custom icons are shown inside the arrow buttons:

Mountain lake Terraced farm Winter trees Forest river Red panda
Demo: Carousel with custom arrow button icons

Render method

When the state of an Elix element changes, its internal.render method is invoked. This gives the component the opportunity to update the component's host element and its shadow elements to reflect the new state. This system allows for the appearance and behavior of an element to be collectively defined by the element class, its base classes, and any mixins applied to it.

You can tap into this system by subclassing an Elix element and defining an render method that sets styles and other properties on the element or its subelements.

Element roles

Complex Elix elements like Carousel and Tabs have templates with various key subelements. Such complex elements expose properties you can set to determine what standard or custom HTML elements are used to fill key roles in the template.

E.g., Carousel has a key element role called the "stage", which shows a single selected item (usually an image) at a large size. Carousel also defined another role called the "proxy" for the element that will be used to represent a specific item; by default, the proxy is a small PageDot.

You can override these roles on an element-by-element basis by setting role properties on the element in markup or before the component is attached to the DOM. Element roles are filled by supplying a descriptor that is either:

  • A component class (FooElement) that will be instantiated to fill the role.
  • A string representing the tag name ("foo-element") that will be instantiated to fill the role.
  • An HTMLTemplateElement that will be cloned to the fill the role.

Example: if you have a defined your own custom elements for a carousel's arrow buttons and page dots, you could use those with a specific Elix Carousel. You could instantiate the Carousel in markup, then fill the role by setting properties for the element's arrowButtonRole and proxyRole. In the case of markup, this can be done by identifying the tag name for the desired custom elements:

<script type="module" src="node_modules/elix/src/Carousel.js"></script>
<script type="module" src="MyArrowButton.js"></script>
<script type="module" src="MyPageDot.js"></script>

<body>
  <elix-carousel arrow-button-role="my-arrow-button" proxy-role="my-page-dot">
    <img src="image1.jpg">
    <img src="image2.jpg">
    <img src="image3.jpg">
  </elix-carousel>
</body>

By specifying what standard or custom element should be used for that key subelements, you can provide arbitrary customizations of the Carousel's appearance and behavior.

You can also define roles on a class basis. Suppose you want to create a custom carousel that always uses your custom page dot to represent the items in the carousel. You can do this in the constructor by setting the relevant roles as default state:

import Carousel from 'elix/src/Carousel.js';
import MyArrowButton from 'MyArrowButton.js';
import MyPageDot from 'MyPageDot.js';

class MyCarousel extends Carousel {
  get [internal.defaultState]() {
    return Object.assign(super[internal.defaultState], {
      arrowButtonRole: MyArrowButton,
      proxyRole: MyPageDot
    });
  }
}

The result has all the functionality of the base Carousel, with the customized appearance of the arrows and page dots:

Mountain lake Terraced farm Winter trees Forest river Red panda
Demo: Carousel with custom arrow buttons and page dots

You can create some fairly unusual combinations of components with this role technique. For example, the following customized AutoCompleteComboBox uses a Carousel to fill its list role, which is normally filled by a ListBox:

Mountain lake Terraced farm Winter trees Forest river Red panda
Demo: A combo box with a carousel inside it

This isn't to say that this particular combination is a great idea, but rather that such combinations are possible and largely work as expected. Here, the carousel's swiping behavior work as expected, as does the input area's auto-completion. The rigorous testing criteria for Elix components, including those in the Gold Standard Checklist for Web Components, help make such combinations work predictably.

Defining element roles

If you'd like to define element roles for your own component, this can be done in your component's internal.render method.

For example, DropdownList defines a valueRole that lets developers using that component customize what kind of component should be used to render the component's currently-selected value.

class DropdownList extends ReactiveElement {

  get [internal.defaultState]() {
    return Object.assign(super[internal.defaultState], {
      valueRole: 'div'
    });
  }

  [internal.render](changed) {
    super[internal.render](changed);
    if (changed.valueRole) {
      template.transmute(this[internal.ids].value, this[internal.state].valueRole);
    }
  }

  get valueRole() {
    return this[internal.state].valueRole;
  }
  set valueRole(valueRole) {
    this[internal.setState]({ valueRole });
  }

}

By default, the component uses a div for the value role. But if someone sets the component's valueRole property, the component's state will update. ReactiveMixin will invoke the component's internal.render method, passing in a changed object where valueRole is true. The DropdownList then uses the transmute helper to convert the div into whatever new element class is going to fill the value role.

As shown above, your component's internal.render implementation must call super in order to let other base classes and mixins perform their work too.

Template patching

Many Elix elements are specializations of other types of elements. Often such relationships are expressed in a class hierarchy. For example, a DropdownList is a specialized type of MenuButton, so DropdownList is defined as a subclass as MenuButton. MenuButton is in turn is a special type of PopupSource, and again the former is defined as a subclass of the latter.

Such specialized classes often need to add additional elements to the template defined by their parent classes. A common Elix pattern is to have a component define its template by obtaining a template from its parent class and return some modified version of it.

This is generally done in a component's internal.template property. The property implementation will ask for the super template, perform some modifications, then return the result as its own template.

class CustomElement extends BaseElement {
  get [internal.template]() {
    const result = super[internal.template];
    /* Perform modifications to the result here. */
    return result;
  }
}

The modifications often take advantage of helper functions in the template module, such as template.concat in the example below.

Appending an additional stylesheet

One particularly common form of template patching is having a subclass append an additional stylesheet to the template defined by the base class. This has the benefit of simplicity, and ensures the subclass' desired styles can cleanly override styles defined by the base class — since the subclass's appended stylesheet comes after any stylesheet(s) defined by the base class.

Elix components perform this type of template patching using the helper function template.concat, which combines two or more HTML templates into a single template. The example below shows a custom subclass invoking template.concat:

import * as template from 'elix/src/template.js';

class BaseElement {
  get [internal.template]() {
    return template.html`
      <style>
        button { background: white; color: black; }
      </style>
      <button>Ok</button>
    `;
  }       
}

class CustomElement extends BaseElement {
  get [internal.template]() {
    return template.concat(super[internal.template], template.html`
      <style>
        button { color: red; }
      </style>
    `);
  }
}

In this example, the resulting CustomElement subclass will have both the template content defined by BaseElement and the custom template content it adds through concat. The resulting template for a CustomElement instance will look like:

<style>
  button { background: white; color: black; }
</style>
<button>Ok</button>
<style>
  button { color: red; }
</style>

As a result, the button inside a CustomElement will have a white background color and a red foreground color.

Reusing mixins

Sometimes you want to create a component that's substantially similar to an existing Elix element, but which is different enough that the techniques above are insufficient. In such cases, you may still be able to reuse much of the code for the Elix element in question by creating your own component from the same set of mixins.

The vast majority of the behavior for all Elix elements is defined by mixins. The documentation for each element will indicate what mixins it uses; inspecting the source code is obviously helpful as well. Having identified that set of mixins, you can apply that same set of mixins to a base class like HTMLElement or ReactiveElement to create a fundamentally new component that nevertheless reuses a considerable degree of code. This allows you to both create components more quickly, and at a higher degree of quality.