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

@spope/glucose

v1.0.4

Published

Custom Component with local / global state management and router.

Downloads

1

Readme

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.