0.5.6 • Published 5 years ago

shinken v0.5.6

Weekly downloads
24
License
GPL-3.0
Repository
github
Last release
5 years ago

Shinken Framework

npm.io

Shinken is a React-style framework written in TypeScript for educational purposes.

N.B. The public API described below is still in alpha and is liable to change.

Table of Contents

Installation

$ npm install --save shinken

Concepts

Shinken borrows concepts from popular JavaScript frameworks, so learning to use it should be easy. As it aims to provide a simple interface to work with, the brief overviews that follow will help to get you up to speed if you are new to such frameworks (once you've mastered this one, go and play around with React or Angular).

Components

Everything in the Shinken framework is a component. A component can either be bound directly to a live DOM element or injected into another component.

Creating a new component and binding it to a DOM element is as simple as:

import ComponentName from './Path/To/Component/ComponentName';

let domElement = document.getElementById('domIdGoesHere');
let properties = {'optional': 'properties'};
let component  = new ComponentName(properties, domElement);

If no properties are required, an empty object {} can be passed instead.

To begin with, the contents of your component should look something like this:

import { Component } from 'shinken';

export default class ComponentName extends Component
{

    protected render(): string
    {
        return `Component content goes here`;
    }

}

You can add whatever public, protected or private methods and properties you might require to support building the output of the component, as long as the final markup is returned by the render() method.

It is recommended to use template literals (backtick characters) to quote markup returned from render(), as it permits you to return multiline content and to insert expressions or variables with the ${} syntax, thus avoiding ugly-looking concatenation. Examples of this style being used can be seen elsewhere in this document.

If required, the DOM Element object to which the component was bound can be retrieved be calling the component's getDomElement() method.

Subcomponents

In addition to binding components directly to DOM elements, they can be injected into existing components. This is as simple as:

import { Component } from 'shinken';
import OtherComponent from './OtherComponent';

export default class ComponentName extends Component
{

    protected render(): string
    {

        let otherComponent = new OtherComponent();

        return `Component content goes here ${ this.injectComponent(otherComponent) }`;

    }

}

As with binding directly to a DOM element, the first argument for the subcomponent constructor is an optional properties object. When injecting a component, however, you do not have to specify a DOM object as the second argument (Shinken handles this for you). As such, if you do not have any properties you can omit all arguments, as shown above.

By default subcomponents are injected into a <span> element with inline-block styling that is created on the fly. If you wish to set the styling to block or any other value, it can be passed as the second argument to injectComponent().

If you wish to add a custom class to the subcomponent's <span> element, it can be passed as the third argument to injectComponent().

In practice you will probably want to store subcomponents in a property so they can be reused rather than instantiated from scratch every time their parent re-renders. You could do this in one of the lifecycle methods described further below.

Routes

In addition to embedding single components into DOM elements, it is possible to set up URL routes and specify top-level components to be rendered in a DOM element based on the URL that has been accessed. Each such component would typically represent a single page.

In the example application, routes are defined in src/config/routes.ts. However, an example that does not use this file might look like:

import { IProperties, addRoute, findAndExecuteRoute, Middleware } from 'shinken';
import HomePageComponent from './Path/To/Component/HomePageComponent';
import MiddlewareOne from './Path/To/Middleware/Class';
import MiddlewareTwo from './Path/To/Other/Middleware/Class';

let routeName: string        = 'productPage';
let route: string            = '/shop/{category}/product/{id}';
let domElement: Element      = document.getElementById('app');
let middleware: Middleware[] = [new MiddlewareOne(), new MiddlewareTwo()];
let closure: Function        = (domElement, params) =>
{

    let properties: IProperties =
        {
            'category': params.category,
            'id': params.id
        };

    new HomePageComponent(properties, domElement);

};

addRoute(routeName, route, domElement, closure, middleware);

findAndExecuteRoute();

Sections of the URL route wrapped in {} characters are treated as wildcards and are made available as properties of the same name within the params argument passed to the closure passed to addRoute() — this closure is only executed if and when the route matches, so the route's target component should be created within it.

Routes are checked against the current URL in the order that they are declared, so if multiple routes could potentially match the same URL, the most specific routes should be declared first.

The middleware argument is optional and can be omitted, but if present must consist of an array of middleware object instances that extend the base Middleware class — the new URL and matching route will be passed to the middleware's handle() method, which must return a boolean value to indicate whether or not the route should finish loading. Other actions (such as redirecting to a new route) can be carried out within the handle() method.

It is also possible to set a special route to act in a '404' capacity, to be executed when no other routes match:

import { setNotFoundRoute } from 'shinken';
import NotFoundComponent from './Path/To/Component/NotFoundComponent';

let domElement: Element = document.getElementById('app');
let closure: Function   = (domElement) =>
{
    new NotFoundComponent({}, domElement);
};

setNotFoundRoute(domElement, closure);

To load a new route as a navigable page in the browser's history, the following helper can be used:

import { navigateTo } from 'shinken';

navigateTo('routeName', {'url': 'params'});

If a route component (or one of its subcomponents) listens for events or utilises timers, you will likely want to ensure that they only act when the component is active in the DOM and has not been cleared by a route change. This can be achieved by calling the component's isRouteActive() method, which reports whether or not the route/page from which the component was instantiated is still active.

Properties

As shown in the above examples, properties can be passed to a component when it is created. A component's properties can be obtained from within the component class by calling:

this.getProperty('propertyName');

If a property has not been set, null will be returned from this method. An optional second argument can be passed to override this and be returned in place of null.

Properties can be used in any way you desire, although it makes the most sense if they are used to help set the initial state of the component (an explanation of this is provided below).

State

Every component has a state. Much like properties, the state is a key/value object. It can be written to and read from in the following way:

this.setState({'stateName', 'stateValue'});
this.getState('stateName');

Any number of state values can be updated at a time by including them in the new state object. By default, values included in the state object will merge with the existing state — if you wish to overwrite the entire existing state with only the values in the state object, pass true as a second argument to setState().

As with getProperty(), if a state item has not been set, null (or a custom value specified in the second argument) will be returned.

If multiple components need to share parts of a state, they can listen for changes in a state store, as in the following example:

import { Component } from 'shinken';

class MyComponent extends Component
{

    protected componentInitiallyLoaded(): void
    {
        this.subscribeToStateStore('storeNameGoesHere');
    }

}

If a state store with the given name exists, the component will inherit all state objects that have been passed to the store. Any component can push a state object to a store in the following way:

import { IState } from 'shinken';

let newState: IState =
    {
        'key1': 'value1',
        'key2': 'value2'
    };

this.setState(newState).store('storeNameGoesHere');

Properties and states may sound very similar, but they differ in two key ways:

  • Properties are set once at the time of creation, whereas state items can change at runtime;
  • When a state item is updated, the component is re-rendered.

Lifecycle Events

Components have several lifecycle events, each of which can be taken advantage of by adding methods to the component class, as in the following example:

import { Component, IState } from 'shinken';

export default class ComponentName extends Component
{

    protected componentInitiallyLoaded(): void
    {
        // Called after the component is created — set inital state here
    }

    protected stateAboutToChange(oldState: IState, newState: IState): void
    {
        // Called just before new state is set — do not update the state here
    }

    protected allowComponentUpdate(oldState: IState, newState: IState): boolean
    {
        // Called just after new state is set -- return FALSE to prevent re-rendering
    }

    protected componentAboutToUpdate(): void
    {
        // Called before the DOM is updated — do not update the state here
    }

    protected render(): string
    {
        // Called whenever the state changes
    }

    protected componentFinishedUpdating(): void
    {
        // Called after the DOM is updated
    }

}

Environment

By default the framework operates in the production environment. Internally, this only means that the Audit logger does not output to the console (console logging is enabled when in the development environment).

The environment can be set or retrieved for any purpose by using the following methods:

import { getEnv, setEnv } from 'shinken';

getEnv(); // Defaults to 'production'

setEnv('development');

Hooks

If components need to broadcast changes to other components in a way that is not possible with states, the listen() and broadcast() functions can be utilised:

import { listen } from 'shinken';

listen('eventName', (payload) =>
{
    // Do something with the payload
});
import { broadcast } from 'shinken';

broadcast('eventName',
{
    'payload': 'object',
    'goes': 'here'
});

If a component registers a listener, it is likely that the listener will become redundant once the route changes and the component is removed. To avoid this situation, true can be passed as a third argument to listen() — any listeners registered in this way will be dropped immediately before a route change.

A small number of lifecycle events are fired by the router for internal use, although they can be hooked into by any component if required:

  • preUrlUpdate is broadcast just before the router loads a new page component or a new URL is set;
  • postUrlUpdate is broadcast just after the router loads a new page component or a new URL is set;
  • initialRouteExecution is broadcast just after the first route has been executed, e.g. when the page is first loaded or the browser is refreshed.

Local Storage

Data can be persisted between sessions using the browser's localStorage, for which a simple wrapper is provided:

import { getFromStorage, setInStorage, removeFromStorage, clearStorage } from 'shinken';

// Insert or update a value in storage
setInStorage('authToken', TOKEN_GOES_HERE);

// Get a value from storage
let value: string = getFromStorage('authToken');

// Remove a value from storage
removeFromStorage('authToken');

// Remove all values from storage
clearStorage();

Bundled Widgets

As detailed in the sections below, some useful widgets are bundled with the framework.

Link

If you require a link that has an onClick event (as opposed to a regular href link), you can inject a Link widget:

import { Widget, IProperties } from 'shinken';

let clickEvent: Function = () =>
{
    // Click logic goes here
};

let properties: IProperties =
    {
        'text': 'Click here',
        'onClick': clickEvent,
        'class': 'class-name'
    };

let link = new Widget.Link(properties);

The class property is optional, and if defined will add the string as CSS class(es) to the resultant <a> element.

If you simply require a link to a route, the code is even simpler:

import { Widget, IProperties } from 'shinken';

let properties: IProperties =
    {
        'text': 'Click here',
        'to': 'routeName',
        'params':
            {
                'dynamic': 'url',
                'params': 'here'
            }
    };

let link = new Widget.Link(properties);

When the to property is defined, Link widget components will be given a CSS class called active when the URL matches the route that they link to. It is possible to change the name of this class by passing a property called activeClass with the desired class name as the property value.

Calendar

A full example of the calendar widget is shown here, but please note that all of the properties are optional:

import { Widget } from 'shinken';

let calendar: Widget.Calendar = new Widget.Calendar(
    {
        'year': 2000,
        'month': 1,
        'selectedYear': 2000,
        'selectedMonth': 2,
        'selectedDay': 15,
        'onChange': (year, month, day) =>
            {
                // Do something with the new selection
            }
    });

In the above example, year and month define the initial month to display (if omitted, will fall back to the current month) and selectedYear, selectedMonth & selectedDay define the initial date to be selected (if omitted, will fall back to the current date).

Important Notes

Component Constructors

The constructor() method of a component should ideally not be declared, as the base Component class's constructor sets things up. If you have to declare a constructor in a component, it must be implemented as follows and pass the arguments on to super():

public constructor(protected _properties: IProperties = {}, protected _domElement: Element|null = null)

Sanitising Text

The HTML returned from a render() method will be inserted directly into the component's DOM element. If you have user-supplied strings that could contain XSS attacks, you can pass them through the sanitise() utility function:

import { Component, Util } from 'shinken';

export default class ComponentName extends Component
{

    protected render(): string
    {

        let firstName: string = this.getProperty('firstName');

        return `Welcome, ${ Util.sanitise(firstName) }`;

    }

}
0.5.6

5 years ago

0.5.5

5 years ago

0.5.4

5 years ago

0.5.3

5 years ago

0.5.2

5 years ago

0.5.1

6 years ago

0.5.0

7 years ago

0.4.0

7 years ago

0.3.1

7 years ago

0.2.0

7 years ago

0.1.2

7 years ago

0.1.1

7 years ago

0.1.0

7 years ago

0.0.7

7 years ago

0.0.6

7 years ago

0.0.5

7 years ago

0.0.4

7 years ago

0.0.3

7 years ago

0.0.2

7 years ago

0.0.1

7 years ago

0.0.0

7 years ago