1.8.10 • Published 2 years ago

@joist/component v1.8.10

Weekly downloads
14
License
MIT
Repository
github
Last release
2 years ago

@joist/component

Installation

npm i @joist/component @joist/di

Component

Components are created via the "component" decorator and defining a custom element. The render function will be called whenver a components state is updated. You can register your custom element either by passing in a tagName or my manually calling customElements.define

import { component, JoistElement } from '@joist/component';

@component({
  tagName: 'app-root', // register now
  state: {
    title: 'Hello World'
  },
  render({ state, host }) {
    host.innerHTML = state.title;
  }
})
class AppElement extends JoistElement {}

// register later: customElements.define('app-root', AppElement);

Once your component templates become more complicated you will probably reach for a view library. Joist ships with out of the box support for lit-html.

npm i lit-html
import { component, JoistElement } from '@joist/component';
import { template, html } from '@joist/component/lit-html';

@component({
  tagName: 'app-root',
  state: {
    title: 'Hello World'
  },
  render: template(({ state }) => {
    return html`<h1>${state.title}</h1>`
  })
})
class AppElement extends JoistElement {}

Dependency injection (DI)

Sometimes you have code that you want to share between elements. One method of doing this is with Joist's built in dependency injector. The @get decorator will map a class property to an instance of a service. One service can also inject another as an argument via the @inject decorator. The @service decorator ensures that your class will be treated as a global singleton.

Property based DI with @get is "lazy", meaning that the service won't be instantiated until the first time it is requested.

import { component, JoistElement, get } from '@joist/component';
import { service, inject } from '@joist/di'

@service()
class FooService {
  sayHello() {
    return 'Hello World';
  }
}

@service()
class BarService {
  constructor(@inject(FooService) private foo: FooService) {}

  sayHello() {
    return this.foo.sayHello();
  }
}

@component({
  tagName: 'app-root',
})
class AppElement extends JoistElement {
  @get(BarService)
  private myService!: BarService;

  connectedCallback() {
    super.connectedCallback();

    console.log(this.myservice.sayHello());
  }
}

Component State

A component render function is only run when a component's state is updated. A component's state can be accessed and updated via it's State instance which is available using @get

import { component, State, JoistElement, get } from '@joist/component';

@component<number>({
  tagName: 'app-root',
  state: 0,
  render({ state, host }) {
    host.innerHTML = state.toString();
  }
})
class AppElement extends JoistElement {
  @get(State)
  private state!: State<number>;

  connectedCallback() {
    super.connectedCallback();

    setInterval(() => this.update(), 1000);
  }

  private update() {
    const { value } = this.state;

    this.state.setValue(value + 1);
  }
}

Async Component State

Component state can be set asynchronously. This means that you can pass a Promise to setState and patchState.

import { component, State, JoistElement, get } from '@joist/component';
import { service } from '@joist/di';

@service()
class UserService {
  fetchUsers() {
    return fetch('https://reqres.in/api/users').then(res => res.json());
  }
}

interface AppState {
  loading: boolean;
  data: any[];
}

@component<AppState>({
  tagName: 'app-root',
  state: {
    loading: false,
    data: []
  },
  render({ state, host }) {
    host.innerHTML = JSON.stringify(state);
  }
})
class AppElement extends JoistElement {
  @get(State)
  private state!: State<AppState>;

  @get(UserService)
  private user!: UserService;

  connectedCallback() {
    super.connectedCallback();

    this.state.setValue({ data: [], loading: true });

    const res: Promise<AppState> = this.user.fetchUsers().then(data => {
      return { loading: false, data }
    });

    this.state.setValue(res);
  }
}

Component Props

Since joist just uses custom elements any properties on your element will work. You can use custom getters and setters or decorate your props with @property which will cause onPropChanges to be called.

import { component, State, JoistElement, property, OnPropChanges, get, PropChange } from '@joist/component';

@component({
  tagName: 'app-root',
  state: ''
  render({ state, host }) {
    host.innerHTML = state;
  },
})
class AppElement extends JoistElement implements OnPropChanges {
  @get(State)
  private state!: State<string>;

  @property()
  public greeting = '';

  onPropChanges(_change: PropChange) {
    this.state.setValue(this.greeting);
  }
}

Component Handlers

Component handlers allow components to respond to actions in a components view. Decorate component methods with @handle('name') to handle whatever is run. Multiple methods can be mapped to the same key. And a single method can be mappped to multiple 'actions'. A handler can also match using a RegExp.

import { component, State, handle, JoistElement, get } from '@joist/component';
import { template, html } from '@joist/component/lit-html';

@component<number>({
  tagName: 'app-root',
  state: 0,
  render: template(({ state, run }) => {
    return html`
      <button @click=${run('dec')}>Decrement</button>

      ${state}

      <button @click=${run('inc')}>Increment</button>
    `
  })
})
class AppElement extends JoistElement {
  @get(State)
  private state!: State<number>;

  @handle('inc') increment() {
    this.state.setValue(this.state.value + 1);
  }

  @handle('dec') decrement() {
    this.state.setValue(this.state.value - 1);
  }

  @handle('inc')
  @handle('dec')
  either() {
    console.log('CALLED WHEN EITHER IS RUN')
  }

  @handle(/.*/) all(e: Event, payload: any, name: string) {
    console.log('CALLED WHEN REGEX MATCHES');
    console.log('TRIGGERING EVENT', e);
    console.log('payload', payload);
    console.log('matched name', name);
  }
}

Dispatching Events

In addition to calling this.dispatchEvent you can also use the dispatch function passed to your render function.

import { component, handle, JoistElement } from '@joist/component';
import { template, html } from '@joist/component/lit-html';

@component({
  tagName: 'app-root',
  render: template(({ dispatch }) => {
    return html`
      <button @click=${dispatch('custom_event')}>Custom Event</button>
    `
  })
})
class AppElement extends JoistElement {}

Testing

The simplest way to test your components is to just create a new instance using document.createElement

import { AppElement } from './app.element';

describe('AppElement', () => {
  let el: AppElement;

  beforeEach(() => {
    el = document.createElement('app-root') as AppElement;
  });

  it('should work', () => {
    expect(el).toBeTruthy();
  });
});

If you want to make use of mock providers you can manually bootstrap your environment.

import { defineEnvironment, clearEnvironment } from '@joist/component';

import { AppElement } from './app.element';
import { Myservice } from './my.service'

describe('AppElement', () => {
  beforeEach(() => {
    defineEnvironment([
      {
        provide: MyService,
        use: class implements Myservice {
          sayHello() {
            return 'GOTCHA!';
          }
        }
      },
    ]);
  });

  afterEach(clearEnvironment);

  it('should work', () => {
    const el = new AppElement();

    expect(el.service.sayHello()).toBe('GOTCHA!');
  });
});

Use with Vanilla Custom ELements

Joist components are an opinionated way to write elements, If you want to use the Joist DI system by don't want to use Joist components it is easy enough to use vanilla custom elements or whatever else you like. As long as your element implements InjectorBase you can use Joist DI.

import { withInjector, get } from '@joist/component';
import { service } from '@joist/di';

@service()
class FooService {
  sayHello(name: string) {
    return `Hello, ${name}`;
  }
}

export class MyElement extends withInjector(HTMLElement) {
  @get(FooService)
  private foo: FooService;

  name = 'World';

  connectedCallback() {
    this.innerHTML = `<p>${this.foo.sayHello(this.name)}!`
  }
}
1.8.10

2 years ago

1.8.9

2 years ago

1.8.8

3 years ago

1.8.7

3 years ago

1.8.2

3 years ago

1.8.1

3 years ago

1.8.0

3 years ago

1.8.6

3 years ago

1.8.5

3 years ago

1.8.4

3 years ago

1.8.3

3 years ago

1.7.0

3 years ago

1.6.0

4 years ago

1.5.0

4 years ago

1.4.0

4 years ago

1.3.0

4 years ago

1.2.2-canary.0

4 years ago

1.2.1

4 years ago

1.1.4-alpha.0

4 years ago

1.1.4-next.0

4 years ago

1.1.1

4 years ago

1.1.3

4 years ago

1.1.2

4 years ago

1.1.0

4 years ago

1.0.9

4 years ago

1.0.8

4 years ago

1.0.7

4 years ago

1.0.6

4 years ago

1.0.5

4 years ago

1.0.4

4 years ago

1.0.3

4 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago

1.0.0-beta.3

4 years ago

1.0.0-beta.2

4 years ago

1.0.0-beta.0

4 years ago

1.0.0-beta.1

4 years ago

1.0.0-alpha.30

4 years ago

1.0.0-alpha.29

4 years ago

1.0.0-alpha.27

4 years ago

1.0.0-alpha.28

4 years ago

1.0.0-alpha.21

4 years ago

1.0.0-alpha.23

4 years ago

1.0.0-alpha.22

4 years ago

1.0.0-alpha.25

4 years ago

1.0.0-alpha.24

4 years ago

1.0.0-alpha.20

4 years ago

1.0.0-alpha.19

4 years ago

1.0.0-alpha.17

4 years ago

1.0.0-alpha.16

4 years ago

1.0.0-alpha.15

4 years ago

1.0.0-alpha.10

4 years ago

1.0.0-alpha.12

4 years ago

1.0.0-alpha.11

4 years ago

1.0.0-alpha.14

4 years ago

1.0.0-alpha.13

4 years ago

1.0.0-alpha.9

4 years ago

1.0.0-alpha.8

4 years ago

1.0.0-alpha.7

4 years ago

1.0.0-alpha.6

4 years ago

1.0.0-alpha.5

4 years ago

1.0.0-alpha.4

4 years ago

1.0.0-alpha.3

4 years ago

1.0.0-alpha.2

4 years ago

1.0.0-alpha.1

4 years ago

1.0.0-alpha.0

4 years ago