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