---
parts:
  - How To
  - Create a custom field
title: 'How To: Create a custom field'
eleventyNavigation:
  key: 'How To: Create a custom field'
  order: 20
  parent: How To
  title: Create a custom field
---

# How To: Create a custom field

```js script
import { html, css, LitElement } from '@mdjs/mdjs-preview';
import { LionField } from '@lion/ui/form-core.js';
import '@lion/ui/define/lion-field.js';
import '@lion/ui/define/lion-validation-feedback.js';

import '../../fundamentals/systems/form/assets/h-output.js';

// A) the custom [slot=input] or 'HTMLElementWithValue'
class DummySlider extends LitElement {
  // A1) it should have a .value property of type 'string'
  static properties = { value: String, reflect: true };

  static styles = [
    css`
      :host {
        display: block;
        padding-top: 16px;
        padding-bottom: 16px;
      }

      [part='rail'] {
        position: relative;
        display: block;
        background: #eee;
        height: 8px;
        border-radius: 8px;
      }

      [part='thumb'] {
        position: absolute;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 10px;
        background: black;
        color: white;
        border-radius: 50%;
        height: 24px;
        width: 24px;
        top: 50%;
        transform: translateY(-50%) translateX(-50%);
        transition: left 0.5s ease 0s;
      }
    `,
  ];

  constructor() {
    super();
    this.value = '0';
    this.addEventListener('click', ev => {
      this.value = `${Math.round(
        ((ev.clientX - this.getClientRects()[0].x) / this.offsetWidth) * 5,
      )}`;
      // A2) it should have a way to tell LionField its value changed
      this.dispatchEvent(new Event('dummy-slider-changed', { bubbles: true }));
    });
  }

  connectedCallback() {
    super.connectedCallback();
    this.setAttribute('tabindex', 0);
  }

  render() {
    return html` <div part="rail">
      <span part="thumb" style="left:${Number(this.value) * 20}%;">${this.value}</span>
    </div>`;
  }
}
if (!customElements.get('dummy-slider')) {
  customElements.define('dummy-slider', DummySlider);
}
```

Custom Fields can be created in just a few steps. All you need is an interaction element (like for instance a slider, a listbox or a combobox) and connect it to the [LionField](https://github.com/ing-bank/lion/blob/57099bec108436e2c9270e4d5cefc5b05c092050/docs/components/input/overview.md).

> In case you want to extend a native element, follow [Extend a native Input](https://github.com/ing-bank/lion/blob/57099bec108436e2c9270e4d5cefc5b05c092050/docs/guides/how-to/extend-a-native-input.md).

## A) an interaction element

An interaction element (.\_inputNode) provides the means for the end user to enter a certain value,
just like native elements provide in this (think of input, textarea and select).
An example of a non native element is the [slider design pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#slider) described here.

For this tutorial, we create a dummy component 'dummy-slider' that exposes its value via
property `.value` and sends an event `dummy-slider-changed` on every value change.
To make it focusable, it has a tabindex=“0” applied.

```js preview-story
export const createAnInteractiveElement = () => {
  // A) the custom [slot=input] or 'HTMLElementWithValue'
  class DummySlider extends LitElement {
    // A1) it should have a .value property of type 'string'
    static properties = { value: String };

    constructor() {
      super();
      this.value = 0;
      this.addEventListener('click', ev => {
        this.value = `${Math.round(
          ((ev.clientX - this.getClientRects()[0].x) / this.offsetWidth) * 5,
        )}`;
        // A2) it should have a way to tell LionField its value changed
        this.dispatchEvent(new Event('dummy-slider-changed', { bubbles: true }));
      });
    }

    connectedCallback() {
      super.connectedCallback();
      this.setAttribute('tabindex', 0);
    }

    render() {
      return html` <div part="rail">
        <span part="thumb" style="left:${Number(this.value) * 20}%;">${this.value}</span>
      </div>`;
    }
  }
  return html`<dummy-slider></dummy-slider>`;
};
```

## B) your LionField extension

Now we want to integrate the slider in our form system to enrich the user interface, get
validation support and all other [benefits of LionField](https://github.com/ing-bank/lion/blob/57099bec108436e2c9270e4d5cefc5b05c092050/docs/components/input/overview.md).
We start by creating a component `<slider-field>` that extends from `LionField`.
Then we follow the steps below:

- **Add your interaction element**

  Here you return the element the user interacts with. By configuring it as a slot, it will end up in
  light DOM, ensuring the best accessibility for the end user.

- **Connect modelValue**

  The `user-input-changed` event is listened to by the FormatMixin: it should be regarded as the
  equivalent of the `input` event of the platform, but for custom built interaction elements.
  You now synchronized [modelValue](https://github.com/ing-bank/lion/blob/57099bec108436e2c9270e4d5cefc5b05c092050/docs/fundamentals/systems/form/model-value.md), which can be regarded as
  the glue to integrate all other functionality like parsing/formatting/serializing, validating,
  tracking interaction states etc.

> N.B. Make sure you never override other property getters than the one mentioned in this tutorial,
> because those properties will lose their reactivity (they won't be considered anymore in the
> update loop of LitElement).
> Whenever a .modelValue/.formattedValue/.serializedValue needs to be computed, use
> [parser/formatter/serializer](https://github.com/ing-bank/lion/blob/57099bec108436e2c9270e4d5cefc5b05c092050/docs/fundamentals/systems/form/formatting-and-parsing.md)

Implement with the following code:

```js preview-story
export const createAField = () => {
  // B) your extension with all the Field goodness...
  class SliderField extends LionField {
    // B1) Add your interaction element as ‘input slot'
    get slots() {
      return {
        ...super.slots,
        input: () => document.createElement('dummy-slider'),
      };
    }

    // B2) Connect modelValue
    constructor() {
      super();
      this.addEventListener('dummy-slider-changed', ev => {
        ev.stopPropagation();
        this.dispatchEvent(new Event('user-input-changed'));
      });
    }

    get value() {
      // Remember we should always return type 'string' here
      return this._inputNode.value;
    }

    set value(newV) {
      this._inputNode.value = newV;
    }
  }
  customElements.define('slider-field', SliderField);

  return html`<slider-field
      label="SliderField"
      help-text="Press to see how modelValue is synchronized"
    ></slider-field>
    <h-output .show="${['modelValue', 'touched', 'dirty', 'focused']}"></h-output>`;
};
```

That was all!
Now that your .modelValue is connected your component is fully compatible with our form system

> Is your `.modelValue` still undefined? Please make sure your `.value` is of type 'string'.

Now you can enhance your slider by writing custom Validators for it or by
writing a parser to get a custom modelValue type.
