customelement-store-binding
v0.4.1
Published
Connect redux-ish stores to your web components (however you write them) without effort or tight coupling
Downloads
5
Maintainers
Readme
Customelements Store Binding
About
Minimal boilerplate redux-ish store bindings for web components with the following features:
- Based on decorators rather than connect / map function boilerplate
- Test-friendly without forcing an approach to the user
- Scopes: No direct binding to the store and support for multiple stores (if you want that)
- Minimal footprint and dependencies
- Support for vanilla web-components, lit-element and stencil (more coming)
tl;dr
A simple WebComponent using this library (and lit-Element, which is not required):
// this is LitElement, which is not required, but makes the example less verbose
@customElement("todo-count")
// This registers the default store
@useStore({ renderFn: LIT_ELEMENT })
export class TodoCountComponent extends LitElement {
// A simple selector
@bindSelector((x: AppRootState) => x.todos.length)
private nrOfItems: number = 0;
// Another selector - this can also be a reselect function
@bindSelector((x: AppRootState) => x.todos.filter(x => x.done).length)
private nrOfFinishedItems: number = 0;
render() {
return html`
<div>
${this.nrOfFinishedItems} / ${this.nrOfItems} Finished
</div>
`;
}
}
Getting Started
A more detailed explanation can be found here.
1. Installation
npm install customelement-store-binding
2. Register your store
In most cases setup is done like this:
import { registerDefaultStore } from "customelement-store-binding";
const store = // however you setup your store
// Register the store as the default
registerDefaultStore(store);
3. Bind your components to the scope
import {useStore, bindSelector} from 'customelement-store-binding';
// This enables redux support for this component using the default store
// You can use a custom render function that should be triggered on state changes using renderFn.
// Default functions for e.g. LitElement are already provided
@useStore({renderFn: el => el.render() })
class MyComponent extends HTMLElement {
// By using @bindSelector the value of the field will
// be updated
@bindSelector((x: MyRootState) => x.someValue)
private value: string = "";
render() {
this.innerHTML = `<div>${value}</div>`;
}
}
customElements.define('my-component', MyComponent);:W
For Stencil, see the Stencil Example for how to setup (Stencil does not support decorators for classes).
4. Dispatch actions
Actions can be dispatched by talking directly to the store, but this couples the web component to the redux implementation. The preferred approach in DOM enabled environments is to use DOM Events and the storeAction()
function that wraps elements in a CustomEvent which will be forwarded to the store
import { storeAction } from "customelement-store-binding";
class MyComponent extends HTMLElement {
private triggerStuff() {
// normally this will be defined in a central place, but let's keep it simple
const action = { type: "triggerAction" };
this.dispatchEvent(storeAction(action));
}
}
Notice that you do not need any decorators for dispatching actions. DOM Events ftw!
Stencil
For libraries like stencil, which do not extend HTMLElement
, the @dispatch
annotation can be used:
@dispatcher()
private dispatchAction: ActionDispatcher;
private finishTodo(todoId: string) {
const action = finishTodo(todoId);
this.dispatchAction(action);
}
Testing
Testing is quite easy and can be done either in a unit-test like way or in a more integrative way. The first approach is using the provided MockStore and provides state changes directly by setting the state, while actions are only observed. the latter approach creates an actual redux store and tests your component against this store, focusing on the real behaviour while sacrifying stricter test boundaries.
You can find full examples of both ways in the examples, but as a reference:
Testing against the mock store
Register a mock store and modify it directly in your tests:
let store: MockStore<AppRootState>;
beforeEach(() => {
store = new MockStore<AppRootState>({ todos: [] });
registerDefaultStore(store);
});
afterEach(() => {
resetStoreRegistry();
});
it("should display all todos from the store is updated", async () => {
const root = (await createElement()).shadowRoot as ShadowRoot;
store.updateState({ todos: [{ id: "1234", title: "hello", done: false }] });
expect(root?.querySelectorAll("li").length).toBe(1);
});
Testing against a real store
Create a real store (with the important reducer subset) and run your tests against it:
beforeEach(() => {
// use the actual store
registerDefaultStore(configureStore({ reducer: todos }));
});
afterEach(() => {
resetStoreRegistry();
});
it("should add a todo when entering a text and clicking on add", async () => {
const expectedText = "New Todo";
const root = (await createElement()).shadowRoot as ShadowRoot;
enterTodoText(root, expectedText);
clickAddButton(root);
await tick();
const todoItems = root?.querySelectorAll("li > span") as NodeListOf<HTMLSpanElement>;
expect(todoItems.length).toBe(1);
expect(todoItems[0].innerText.trim()).toMatch(expectedText);
});
Open Topics
- Improve/Enforce type-safety better
- Evaluate pure JavaScript Examples
- Add more integration examples and connectors (Angular, React)
- Add more examples for non-redux stores (mobx, akita, etc.)