npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

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

Readme

Build Status

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.)