js/runtime/router_state.js
// NPM IMPORTS
// import assert from 'assert'
import { fromJS } from 'immutable'
// COMMON IMPORTS
import T from '../../../node_modules/devapt-core-common/dist/js/utils/types'
import Stateable from '../../../node_modules/devapt-core-common/dist/js/base/stateable'
// BROWSER IMPORTS
import DisplayCommand from '../commands/display_command'
const context = 'browser/runtime/router_state'
/**
* Initial route state
*/
const initial_state_js = {
history:{
items:[],
index:-1
}
}
/**
* @file Browser navigation router class.
*
* @author Luc BORIES
*
* @license Apache-2.0
*/
export default class RouterState extends Stateable
{
/**
* Create a RouterState instance.
* @extends Stateable
* @abstract
*
* A RouterState instance manages a navigation history state and update the page on navigation change.
*
* Navigation history is an array stored into the Redux store.
*
* Actions are:
* * display_content(view name, menubar name): update page content with given view and menubar.
* * go_backward(): update page with history previous content if available.
* * go_forward(): update page with history next content if available.
* * update_history(arg_history_values): replace history array.
* * clear_history(): reset history array.
*
* Corresponding method to subclass are:
* * display_content_self(view name, menubar name):Promise
* * go_backward_self():Promise
* * go_forward_self():Promise
*
* @param {string} arg_log_context - context of traces of this instance (optional).
*
* @returns {nothing}
*/
constructor(arg_log_context)
{
const runtime = window.devapt().runtime()
const log_context = arg_log_context ? arg_log_context : context
const default_settings = {}
super(default_settings, runtime, initial_state_js, log_context)
this.is_router_state = true
this.state_path = ['router']
}
/**
* Get name.
*
* @returns {string}
*/
get_name()
{
return 'router'
}
/**
* Load and apply a container component configuration.
*
* @param {Immutable.Map|undefined} arg_state - component state to load (optional).
*
* @returns {nothing}
*/
load(arg_state)
{
const state = arg_state ? arg_state : this.get_state()
super.load(state)
// console.info(context + ':load:loading ' + this.get_name())
if (! state)
{
this.error('load:no available state')
return
}
}
/**
* Handle state changes.
*
* @param {Immutable.Map} arg_previous_state - previous state map.
* @param {Immutable.Map} arg_new_state - new state map.
*
* @returns {nothing}
*/
handle_state_change(arg_previous_state, arg_new_state)
{
if (! arg_previous_state)
{
// console.info(context + ':handle_state_change:update initial items')
this.do_action_clear_history()
return
}
if ( arg_previous_state && arg_new_state && arg_previous_state.has('history') && arg_new_state.has('history') )
{
if ( arg_previous_state.hasIn(['history', 'items']) && arg_new_state.hasIn(['history', 'items'] ) )
{
const previous_history_items = arg_previous_state.getIn(['history', 'items'])
const new_history_items = arg_new_state.getIn(['history', 'items'])
if ( ! previous_history_items.equals(new_history_items) )
{
const new_items = new_history_items.toArray().toJS()
if (new_items.length == 0)
{
this.do_action_clear_history()
return
}
this.do_action_update_history(new_items)
}
}
if ( arg_previous_state.hasIn(['history', 'index']) && arg_new_state.hasIn(['history', 'index'] ) )
{
const previous_history_index = arg_previous_state.getIn(['history', 'index'])
const new_history_index = arg_new_state.getIn(['history', 'index'])
// TODO CHECK IMMUTABLE RETURNS A NUMBER
if ( previous_history_index != new_history_index )
{
this.do_action_move_history(new_history_index)
}
}
}
}
/**
* Goto the page content with given view (update only the hash).
*
* @param {string} arg_view_name - view name.
* @param {string} arg_menubar_name - menubar name.
*
* @returns {nothing}
*/
do_action_display_content(arg_view_name, arg_menubar_name)
{
this.enter_group('do_action_display_content')
this.debug('arg_view_name:', arg_view_name)
this.debug('arg_menubar_name:', arg_menubar_name)
if ( T.isString(arg_view_name) || T.isString(arg_menubar_name) )
{
this.dispatch_action('display_content', { view:arg_view_name, menubar:arg_menubar_name } )
}
this.leave_group('do_action_display_content')
}
/**
* Display history previous content if available.
*
* @returns {nothing}
*/
do_action_go_backward()
{
this.enter_group('do_action_go_backward')
this.dispatch_action('go_backward', {} )
this.leave_group('do_action_go_backward')
}
/**
* Display history next content if available.
*
* @returns {nothing}
*/
do_action_go_forward()
{
this.enter_group('do_action_go_forward')
this.dispatch_action('go_forward', {} )
this.leave_group('do_action_go_forward')
}
/**
* Clear history.
*
* @returns {nothing}
*/
do_action_clear_history()
{
this.enter_group('do_action_go_forward')
this.dispatch_action('clear_history', {} )
this.leave_group('do_action_go_forward')
}
/**
* Update history.
*
* @param {array} arg_new_items - history array items.
*
* @returns {nothing}
*/
do_action_update_history(arg_new_items)
{
this.enter_group('do_action_go_forward')
this.debug('arg_new_items', arg_new_items)
this.dispatch_action('update_history', { values: arg_new_items } )
this.leave_group('do_action_go_forward')
}
/**
* Move history current position.
*
* @param{Number} arg_new_index - history position index.
*
* @returns {nothing}
*/
do_action_move_history(arg_new_index)
{
this.enter_group('do_action_move_history')
this.debug('arg_new_index', arg_new_index)
this.dispatch_action('update_history', { index: arg_new_index } )
this.leave_group('do_action_move_history')
}
/**
* Store actions reducer pure function.
*
* @param {object} arg_previous_state - previous state.
* @param {object} arg_action - store action: { type:'', component:'', ...}
*
* @returns {object} - new state
*/
reduce_action(arg_previous_state, arg_action)
{
const initial_state_js = {
}
if (! arg_previous_state)
{
arg_previous_state = fromJS(initial_state_js)
}
// console.log(context + ':reduce_action:prev state', arg_previous_state.toJS())
switch(arg_action.type)
{
case 'display_content': {
if ( ! T.isString(arg_action.view) || ! T.isString(arg_action.menubar) )
{
return arg_previous_state
}
// UPDATE PAGE URL
this.update_hash(arg_action.view, arg_action.menubar)
// UPDATE ROUTER STORED STATE
const history = arg_previous_state.get('history')
let next_state = history.get('items').push( { view:arg_action.view_name, menubar:arg_action.menubar_name } )
next_state = history.set('index', history.get('index') )
// UPDATE PAGE CONTENT
this.display_content(arg_action.view, arg_action.menubar)
return next_state
}
case 'go_backward': {
const history = arg_previous_state.get('history')
const previous_index = history.get('index')
if (previous_index > 0)
{
const new_index = previous_index - 1
const item = history.get('items').get(new_index, undefined)
if (item)
{
this.display_content(item.view, item.menubar)
const next_state = history.set('index', new_index)
return next_state
}
}
return arg_previous_state
}
case 'go_forward': {
const history = arg_previous_state.get('history')
const previous_index = history.get('index')
if (previous_index + 1 < history.get('items').count() )
{
const new_index = previous_index + 1
const item = history.get('items').get(new_index, undefined)
if (item)
{
this.display_content(item.view, item.menubar)
const next_state = history.set('index', new_index)
return next_state
}
}
return arg_previous_state
}
case 'clear_history': {
const next_state = arg_previous_state.set('history', fromJS([]) )
return next_state
}
case 'update_history': {
let items = arg_previous_state.get('history').concat(arg_action.values)
const next_state = arg_previous_state.set('history', items)
return next_state
}
case 'move_history': {
const history = arg_previous_state.get('history')
const new_index = arg_action.index
if (new_index > 0 && new_index < history.get('items').count() )
{
const item = history.get('items').get(new_index, undefined)
if (item)
{
this.display_content(item.view, item.menubar)
const next_state = history.set('index', new_index)
return next_state
}
}
}
}
return arg_previous_state
}
/**
* Display the page content with given view and menubar.
*
* @param {string} arg_view_name - view name
* @param {string} arg_menubar_name - menubar name
*
* @returns {Promise} - Resolved result is a boolean: success or failure
*/
display_content(arg_view_name, arg_menubar_name, arg_breadcrumbs=undefined)
{
this.enter_group('display_content')
this.debug('arg_view_name=' + arg_view_name)
this.debug('arg_menubar_name=' + arg_menubar_name)
this.debug('arg_breadcrumbs=' + arg_breadcrumbs)
let promise = null
try
{
const cmd_settings = {
name:'router.display_content',
type:'display',
view:arg_view_name,
menubar:arg_menubar_name,
breadcrumbs:arg_breadcrumbs
}
const cmd = new DisplayCommand(this._runtime, cmd_settings)
this.get_runtime().ui().pipe_display_command(cmd)
}
catch(e)
{
console.error(e, context)
promise = Promise.resolve(false)
}
this.leave_group('display_content')
return promise
}
/**
* Update page url with given view and menubar (update only the hash).
*
* @param {string} arg_view_name - view name.
* @param {string} arg_menubar_name - menubar name.
*
* @returns {nothing}
*/
update_hash(arg_view_name, arg_menubar_name)
{
this.enter_group('update_hash')
this.debug('arg_view_name', arg_view_name)
this.debug('arg_menubar_name', arg_menubar_name)
try
{
if ( T.isFunction(this.update_hash_self) )
{
this.update_hash_self(arg_view_name, arg_menubar_name)
}
}
catch(e)
{
console.error(e, context)
this.error(e)
}
this.leave_group('update_hash')
}
}