Skip to content

Component

Brod edited this page Jul 20, 2018 · 3 revisions

Overview

Components are used to create HTML and manage changes to the DOM.

Register

The jackle.component() method accepts a single Handler or an array or Handlers.

jackel.component(my_component);
// or
jackel.handler([ my_component, my_other_component ]);

Interface

The Handler interface and it's types are available under the Jagwah.handler namespace.

export module Jackle {
  ...
  export module component {
    export type html = hyperhtml.BoundTemplateFunction<HTMLElement>;
    export type template = (state: Jackle.state, node: Jackle.component.html, controller?: { [name: string]: any }) => void;
    export type controller = (state: Jackle.state) => { [name: string]: any };
  }
  export interface component {
    selector: string;
    template: Jackle.component.template;
    controller?: Jackle.component.controller;
  }
}

selector

The selector property should be a reference to an element on the DOM which is accessible via the document.querySelector(selector) method.

template

The template property will be called every time the state changes, the template property can be async and will be given three arguments, state, html and controller.

The state argument is a frozen (Object.freeze()) copy of the state, this is because all changes to the state should be performed by calling the change method.

The html argument is a hyperhtml.BoundTemplateFunction<HTMLElement> tagged template function, which, more or less takes a string and turns it into html, learn more here.

The controller argument will only be provided if there was a controller defined for the component.

controller

The controller property will also be called every time the state changes and accepts a single state argument, it should return a keyed object.

Example

The example below will find an element matching #form and then output the tagged template literal result to that element.

const form_component: Jackle.component = {
  selector: '#form',
  template: async (state, html) => {
    html`
      <form>
        <input type="text" value=${state.temp.text}>
      </form>
    `;
  }
}
<body>
  <div id="form">
    <!-- <form> element will be placed here -->
  </div>
</body>

That example is pretty straight forward, to take it further we can bind the input element's value with the state, to do this we'll call the change() method when the input element value changes. To implement this we can set an event handler for the input elements onchange or oninput events.

const form_component: Jackle.component = {
  selector: '#form',
  template: async (state, html) => {
    html`
      <form>
        <input type="text" oninput=${(event: Event) => state.change('update:temp', e.target)} value=${state.temp.text}>
      </form>
    `;
  }
}

However, writing long lambdas within element attributes can be hard to read, to resolve this we'll use a simple controller for handling interactions.

const form_component: Jackle.component = {
  selector: '#form',
  controller: (state) => {
    return {
      oninput: (event: Event) => {
        state.change('update:temp', event.target);
      }
    }
  },
  template: async (state, html, controller) => {
    html`
      <form>
        <input type="text" oninput=${controller.oninput} value=${state.temp.text}>
      </form>
    `;
  }
}

Personally I'm a fan of keeping file counts to a minimum but since Components are simple objects you can move the controller and template properties into separate files and change the structure to suit your needs.

components/form/form.component.ts

import { controller } from './form.controller';
import { template } from './form.template';

const form_component: Jackle.component = {
  selector: '#form',
  controller: controller,
  template: template,
}

components/form/form.controller.ts

export const controller: Jackle.component.controller = (state) => {
  return {
    oninput: (event: Event) => {
      state.change('update:temp', event.target);
    }
  }
}

components/form/form.template.ts

export const template: Jackle.component.template = async (state, html, controller) => {
  html`
    <form>
      <input type="text" oninput=${controller.oninput} value=${state.temp.text}>
    </form>
  `;
}

Doing this does mean you'll lose the easily inferred types, so you'll need to wire them out separately.