0.2.2 • Published 4 years ago

lit-element-effector v0.2.2

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

lit-element-effector

Minimal mixin to attach an Effector Store to lit-element.

  • Automatically request element update on effector store change.
  • Supports typescript.
  • Just a tiny wrapper, about 1kB minified.
  • Supports a pattern for inheritance.
  • Safe: makes a copy of the store into the custom element.
  • Built with best practices in mind.

/!\ currently there is an issue with ts-loader, but rollup and standalone webpack work.

Usage

EffectorMxn( BaseClass, Store, FxAPI? ) => class extends BaseClass

The mixin takes as input parameters a BaseClass which must inherit from a LitElement, an Effector Store and an optional object, FxAPI. This last one is meant as dependency injection of the custom-element's effect interface, its values are expected to be either effector effects or events (or functions).

import {EffectorMxn} from "lit-element-effector"
import {html, LitElement} from "lit-element"
import {createStore} from "effector"

const store01 = createStore( {greetings:"hello"} );

class example01 extends EffectorMxn( LitElement, store ){
    render(){
        // the store state is available with the `$` property
        return html`<h1> ${this.$.greetings} world! </h1>`
    }
}

customElements.define("example-01",example01);

The provided store is reflected to the LitElement property $. Supports any store types, from boolean to objects. The store state is deeply-copied to $. Direct assignment to the property $ should be avoided, altough it cannot affect the state.

Event and Effect API Helper

import {createEvent, createEffect} from "effector"

const evnt = createEvent();
const Fx = createEffect(/* some network call*/);

store01.on( evnt, (_,p) => { return {greetings:p} } );

const API = { changeGreetings: evnt,  networkCallFx: Fx } ;

class example02 extends EffectorMxn(LitElement, store01, API){
    render(){
        return html`
            <h1> ${this.$.greetings} world! </h1>
            <button @click="${this.clickme}"></button>
        `
    }

    clickme(){
        // the effects API is available under `dispatch` prop
        this.dispatch.changeGreetings("Hey");
    }
}

If defined, the effect API is injected into the dispatch getter property. This is no more than a recommendation, it helps to keep the custom-element decoupled from the app-state (see an example in the test section below). In some cases can be more convenient to override the dispatch getter, if you do so is a good practice to return a shallow copy of the API object (since it could be used in multiple places).

React on Store change with user defined function

class example03 extends EffectorMxn(LitElement, store){
    
    on_store_update(stateCopy)
    {
        /* Do something on update */
    }
    render() { /* render component */ }
}

If defined, the function on_store_update will be executed any time a store change is triggered. The only argument passed to the function is a copy of the current store. This function will run after the property $ is set, but before any of the element's update/render.

Inheritance

import {combine} from "effector";
const store02 = createStore( {username:"Alex"} )
const combinedStore = combine(store01,store02, (a,b) => Object.assign({}, a,b) );

// here applying the mixing to the previous example class
class example04 extends EffectorMxn(example01, combinedStore){ 
    
    // adding a reflected attribute on top of the inherited ones
    @property() type = "dark";
    
    render(){
        return html `
            ${super.render()}
            <p class="${this.type}"> This is ${this.$.username}. </p>
        `
    }
}

LitElement makes sure that reflected properties of inherited classes are present and functioning. One thing to notice is that when applying the mixin multiple times the store is actually swapped, so the provided store of a child class must be a combination of the parent store and the additional wanted properties.

Testing Helpers

      it("Detaches from current store, send fake data", async ()=>{

        var ex01 = <example01>document.createElement("example-01");

        // this detaches from store (replace with undefined)
        ex01.replaceStore(); 
        //ex01.replaceStore(newStore); 

        // the element is not initialized until it is connected
        document.body.appendChild(ex01);
        await ex01.updateComplete;  // wait for render

        expect(ex01.$).to.be.undefined; // no store

        // simulate a store update with fake data 
        ex01.store_update_handler( {greetings: "Ciao" } );

        expect(ex01.$).to.deep.equal( {greetings: "Ciao" } );
    });

There are a few convenient helpers to aid testing a custom-element with attached store. A function replaceStore is provided to swap the store with a fake one. The store_update_handler function can be used to simulate a store update with fake data, you can also use this to initialize the element before appending the element to DOM.

  it("Reassign the event API of an instance",()=>{
        customElements.define("example-02",example02);
        var ex02 = document.createElement("example-02");
        
        ex02.dispatch.networkCallFx = ( ) => {};

        document.body.appendChild(ex02);

        /* go on with network related call disabled */

    });

Many times during testing we would mock or stub effects that make network calls, here you can simply reassign the instance API injecting a different function. The dispatch getter returns a shallow copy of the effect-API, this means that you can swap the keys of that instance with fakes without affecting the overall element class nor the original effect-API.