core_States.js

import { Helper } from "../util/Helper.js";
import { DefaultIndexState } from "../base/DefaultIndexState.js";
import { Events } from "../IF.js";


/**
 * States - The state manager.
 * You can create multiple States instances in your application logic, e.g. for dealing with sub-states etc.
 */
class States
{
    static DEFAULT_INDEX_STATE_ID = 'INFRONT_DEFAULT_INDEX_STATE';
    static DEFAULT_NOT_FOUND_STATE_ID = 'INFRONT_DEFAULT_NOTFOUND_STATE';

    /**
     * Constructor
     * @param {App} appInstance - Instance of app
     */
    constructor( appInstance )
    {
        this._states =  {};
        this.app = appInstance;
        this.currentState = null;
        this.stateNotFoundClass = null;
    }

    /**
     * Add state class
     *
     * @param {...DefaultBaseState} stateClasses - State class to be added.
     * @throws {Error}  - Throws an error when adding state is not possible
     * @returns {boolean} - Returns wheter or not adding was successful
     */
    add( ...stateClasses )
    {
        for( const stateClass of stateClasses )
        {
            if ( false === Helper.isClass( stateClass ) )
            {
                throw new Error( 'States.addState expects a class/subclass of State.' );
            }

            // Autogeneratre id in case it is not valid
            if ( false === Helper.isString( stateClass.ID ) )
            {
                console.warn( 'StateClass doesnt have a valid ID.' );
                stateClass.ID = Helper.createUid();
            }

            if ( true === this._states.hasOwnProperty( stateClass.ID ) )
            {
                console.warn( `StateClass not added. ID ${stateClass.ID} already exists.` );
                return false;
            }

            this._states[ stateClass.ID ] = stateClass;

            if ( Helper.isString( stateClass.ROUTE ) )
            {
                this.app.router.addRoute( stateClass.ROUTE, stateClass );
            }
            else if ( Helper.isArray( stateClass.ROUTE ) )
            {
                for ( let route of stateClass.ROUTE )
                {
                    this.app.router.addRoute( route, stateClass );
                }
            }
        }
    }

    /**
     * Create an instance of given state id
     *
     * @param {string} stateId - The state id to be instantiated
     * @param {RouteParams} routeParams - Current RouteParams
     * @returns {null}
     */
    create( stateId, routeParams )
    {
        let stateInstance = null;

        if ( this._states.hasOwnProperty( stateId ) )
        {
            stateInstance = new this._states[ stateId ]( this.app, routeParams );
        }
        else if ( stateId === States.DEFAULT_INDEX_STATE_ID && true === this.app.settings.get( "states.useDefaultIndexState" ) )
        {
            stateInstance = new DefaultIndexState( this.app, routeParams );
        }
        else if ( null !== this.stateNotFoundClass )
        {
            stateInstance = new this.stateNotFoundClass( this.app, routeParams );
        }
        else
        {
            console.error( `State ${stateId} does not exist.` );
        }

        return stateInstance;
    }

    /**
     * Checks if given stateId already exists.
     *
     * @param {string} stateId
     * @return {boolean}
     */
    exists( stateId )
    {
        return this._states.hasOwnProperty( stateId );
    }

    /**
     * Set the StateNotFound class
     *
     * @param {State} notFoundClass - State class used in case when a state is not found
     * @throws {Error} - Thrown when provided param is not of type State
     */
    setStateNotFoundClass( notFoundClass )
    {
        if ( false === Helper.isClass( notFoundClass ) )
        {
            throw new Error( 'States.setNotFoundClass expects a class/subclass of State.' );
        }

        this.stateNotFoundClass = notFoundClass;
    }

    isNotFoundStateEnabled()
    {
        return ( this.stateNotFoundClass !== null );
    }

    /**
     * Switch to given state
     * @param {DefaultBaseState} newState - Instance of state to switch to
     * @throws {Error} - Throws an error if given state cannot be entered or if enter() function throws an error.
     * @returns {Promise<boolean>}
     */
    async switchTo( newState )
    {
        let previousStateId = null;
        let currentStateId = this.currentState ? this.currentState.getId() : null;

        this.app.emit(
            Events.EVENT.BEFORE_STATE_CHANGE,
            {
                currentStateId: currentStateId,
                nextStateId : newState ? newState.getId() : null
            }
        );

        if ( false === newState.canEnter() )
        {
            const redirectUrl = newState.getRedirectUrl();
            if ( redirectUrl )
            {
                this.app.router.redirect( redirectUrl );
                return false;
            }

            throw Error( 'Forbidden to enter new state:' + newState.getId() );
        }

        if ( this.currentState )
        {
            previousStateId = this.currentState.getId();
            await this.currentState.exit();
            delete this.currentState;
        }

        this.currentState = newState;
        await newState.enter();
        currentStateId = this.currentState.getId();

        this.app.emit(
            Events.EVENT.AFTER_STATE_CHANGE,
            {
                previousStateId : previousStateId,
                currentStateId: currentStateId
            }
        );
    }

}

export { States };