1.0.4 • Published 4 years ago

@spope/glucose v1.0.4

Weekly downloads
-
License
GPL-3.0-or-later
Repository
github
Last release
4 years ago

This library provide HTML Custom Elements based Components, Pages coupled with a Router and a global State, rendered with the amazing uhtml library.

Install

you can install it using npm npm install @spope/glucose. Because glucose use standard Javascript API, no bundling / transpiling / compilation is needed, and it can be used in the browser directly by downloading it or using a cdn https://cdn.jsdelivr.net/npm/@spope/glucose@latest/build/index.js

You can use Glucose via ESM import {Component, html, render} from "@spope/glucose" or include it in the page and use it like that :

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@spope/glucose@latest/build/index.js"></script>

<script type="text/javascript">
    const {Component, render, html} = glucose;
</script>

Usage

tl;dr : Basic application example composed of 2 pages here.

Components

Glucose provide Class components. Those components are HTML Custom Element with their API. Glucose provide some methods on top of that.

Component have a local state. This state can be initialized within the getter initialState. Component state can only be changed within the setState method. Nested properties can be accessed with a dot notation:

class CounterComponent extends Component {
  // Initializing Component's state
  static get initialState() {
    return {
      count: 0
    };
  }
  inc() {
    this.setState({
        count: this.getState('count') + 1
    });
  }
  dec() {
    this.setState({
        count: this.getState('count') - 1
    });
  }
  renderComponent() {
    render(this, html`
      <button onclick=${e => this.dec()}>-</button>
      <div>${this.getState('count')}</div>
      <button onclick=${e => this.inc()}>+</button>
    `);
  }
}

customElements.define("counter-component", CounterComponent);

You can test it on this codepen

For HTML events binding, attibut, keyed renders, see uhtml doc for more.

Component also have Custom Elements lifecycle callbacks :

  • conectedCallback
  • attributeChangeCallback (with standard getter observedAttributes)
  • disconnectedCallback

State

Glucose embed a global State, accessible and editable from everywhere. This state can be initialized by calling setInitialGlobalState method :

import {setInitialGlobalState} from '@spope/glucose';

setInitialGlobalState({
    count: 0
})

To update this global state, we need to register Actions first. These Actions will return only the update part of the state. To do so, Actions will receive a clone of the state, and a payload. The given state is a clone of the global state, and can be mutated without affecting the global state.

import {register} from '@spope/glucose';

register('increment', (state, payload) => {
  const incrementValue = (payload.value !== null ? payload.value : 1);
  return {
    count: state.count + incrementValue
  };
});

We'll see in the next section how to organize those actions.

Once Actions are registered, they can be dispatched with a payload. Actions type are accessible under Actions property.

import {Actions, dispatch} from '@spope/glucose';

...
    send(event) {
        dispatch(Actions.increment, value: 2);
    }
...

Test on this codepen

Use global state with components

Components use local state by default, and should define which properties of the global state they should listen / read. To do so we define the mappedProperties getter. When a component is mounted, it start listening for changes of these properties into the global state, and when it is unmounted, those listeners are removed.

    static get mappedProperties() {
        return [
            'property.path',
            'count'
        ]
    }

Those mapped property will be accessible using the this.getState('count') function. Be careful to not use setState on a mappedProperty. this.setState({count: 5}) would set value into the component's local state, but this.getState('count') would return global state value. An update of a mappedProperty into the global state will trigger the render of the component.

So when an action is dispatched, its callbacks are called, the global state is updated, and the component mapped on one of the updated property will be re-rendered. Those components are added to a queue and will be re-rendered on the next frame. Doing so will prevent to render some components multiple times when two listened properties are updated at the same time.

Using global state outside of glucose components

Global state can be accessed from outside of glucose component :

import {readState, subscribeToState, unsubscribeFromState} from '@spope/glucose';

// Read a value from global state
let value = readState('path.to.props');

// Subscribe to a value from global state
const callback = function(oldValue, newValue) {
    console.log(`Value was ${olValue} and is now ${newValue}`);
}
subscribeToState('path.to.prop', callback);

// Unsubscribe to a value from global state
unsubscribeFromState('path.to.prop', callback)

Page

Page are basically components with the same API, extended with an action registry.

Page component will carry every actions available on that page. The registry will be loaded and unloaded on page connectedCallback / disconnectedCallback. Page component will also be in charge of rendering components of the page, and can use local / global state to do so.

import {Page, html, render} from '@spope/glucose';

class PageIndex extends Page {
    actionsRegistry = {
        'MyAction': [
            (state, payload) => {
                state.test = "mutation";        // Will not affect app global state
                return {"new.state": payload};
            }
        ]
    }

    renderComponent() {
        render(this, html`
            <some-component />
        `);
    }
}

customElements.define("test-page", PageIndex);

Page can be responsible for the global state (see more into state section). It is possible to initialize the global state on page construction. On each page change, a new state can be generated, and pages can be configured to save page's state on page change.

class PageIndex extends Page {

    preserveState = true;  // Save global state on page change.
                           // Will be restored on next page visit (without page reload).
    constructor() {
        super();

        setInitialGlobalState({
            property: 'value'
        });
    }
    ...
}

Doing so each page can have its proper state, saved for later without collision between pages. If no setInitialGlobalState directive is set on a page, global state will be shared between pages.

Router

A page can be rendered as a single component, or we can use the router to display page dynamically. First we define some routes. A route defines a Page component for a given URL.

import {Router} from '@spope/glucose';

addEventListener('DOMContentLoaded', () => {
    const routes = [
        {
            name: 'index',
            url: '',
            page: PageIndex
        },
        {
            name: 'showProduct',
            url: 'show-product/{{productId}}/',
            page: PageShowProduct
        }
    ];

    Router.initRouter(routes, {
        baseUrl: 'glucose-test/',
        errorPage: PageError
    });

    const myView = Router.getView();

    document.body.append(myView);
}, {once: true});

Note the Router.initRouter() accept the array of routes as first argument, the second one is an object with optional parameters (baseUrl and errorPage).

Glucose embed a custom built-in component to generate Anchor, with a url function to generate the url with its parameters.

import {html, Router} from '@spope/glucose';

html`<a is="glucose-a" href=${Router.url('showProduct', {productId: 36})}>Show product</a>`;

If you add other parameters that are not defined into the url, those will be appended to the query string :

Router.url('showProduct', {productId: 36, lang: 'en_GB'})}
//will return
"https://spope.fr/show-product/36/?lang=en_GB"

To programmatically navigate to a Glucose route (from its url), the navigate function is available.

import {Router} from '@spope/glucose';

Router.navigate(Router.url('index'));

Route name, parameters and queryString are all accessible from the global state, along with the referer route name, parameters and queryString. The state, with a referer, looks like this :

{
    "glucose": {
        "location": {
            "route": "showProduct",
            "url": "show-product/36/",
            "parameters": {
                "productId": 35
            },
            "queryString": {
                "lang": "en_GB"
            },
            "referer": {
                "route": "index",
                "url": "",
                "parameters": null,
                "queryString": {
                    "lang": "en_GB"
                }
            }
        }
    }
}

Here is a codepen with Router, Pages, Component, Global State, and Actions.

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

0.0.4

4 years ago

0.0.3

4 years ago

0.0.2

4 years ago

0.0.1

4 years ago