Reference Source

js/base/stateable.js

// NPM IMPORTS
import assert from 'assert'
import Immutable from 'immutable'

// COMMON IMPORTS
import T from '../utils/types'
import Settingsable from './settingsable'


/**
 * Contextual constant for this file logs.
 * @private
 */
const context = 'common/base/stateable'



/**
 * Stateable base class.
 * @abstract
 * 
 * @author Luc BORIES
 * @license Apache-2.0
 * 
 * @example
* API:
* 		->get_initial_state():plain object - get state at creation.
* 		->get_state():plain object - get current state.
* 		->get_state_store():object - get state store.
* 		->get_state_path():array|string - get state path into store.
* 		->get_state_value(arg_key_or_path, arg_default_value=undefined):any - get a state value at path.
* 
* 		->handle_state_change(arg_previous_state, arg_new_state):nothing - handle state changes (to be implemented in sub classes)
* 		->register_state_value_change_handle(arg_path, arg_listener):nothing - Register a state value change listener.
* 
* 		->dispatch_action(arg_action_type, arg_options):nothing - dispatch state changes actions.
* 
* 		->get_name():string - get instance name.
* 
 */
export default class Stateable extends Settingsable
{
	
	/**
	 * Creates an instance of Stateable: an object with an observable mutable state.
	 * 
	 * @param {Immutable.Map|object} arg_settings - settings plain object
	 * @param {object} arg_runtime - client runtime.
	 * @param {object} arg_state - component state.
	 * @param {string} arg_log_context - context of traces of this instance (optional).
	 * @param {LoggerManager} arg_logger_manager - logger manager object (optional).
	 * 
	 * @returns {nothing}
	 */
	constructor(arg_settings, arg_runtime, arg_state, arg_log_context, arg_logger_manager)
	{
		const log_context = arg_log_context ? arg_log_context : context
		super(arg_settings, log_context, arg_logger_manager)
		
		/**
		 * Class type flag.
		 * @type {boolean}
		 */
		this.is_stateable = true

		// GET RUNTIME
		/**
		 * Runtime isntance.
		 * @type {Runtime}
		 */
		this._runtime = arg_runtime
		if ( ! arg_runtime)
		{
			if (arg_settings.get)
			{
				this._runtime = arg_settings.get('runtime', undefined)
			} else {
				this._runtime = (arg_settings && arg_settings.runtime) ? arg_settings.runtime : undefined
			}
		}
		assert( T.isObject(this._runtime) && this._runtime.is_base_runtime, context + ':constructor:bad this._runtime instance (' + log_context + ')')

		/**
		 * Initial state.
		 * @type {object}
		 */
		this._initial_state = arg_state

		/**
		 * State store.
		 * @type {StateStore}
		 */
		this._state_store = this._runtime.get_state_store()
		assert( T.isObject(this._state_store), context + ':constructor:bad state_store object (' + log_context + ')')
		
		// SET STATE PATH
		/**
		 * State path in runtime state.
		 * @type {array}
		 */
		this.state_path = undefined
		if ( arg_state && T.isFunction(arg_state.has) && T.isFunction(arg_state.get) && arg_state.has('state_path') )
		{
			this.state_path = arg_state.get('state_path').toArray()
		}

		/**
		 * State changes handlers.
		 * @type {array}
		 */
		this._state_value_listeners = []

		// console.info(context + ':constructor:creating component ' + this.get_name())
	}



	/**
	 * Get runtime instance.
	 * 
	 * @returns {RuntimeBase}
	 */
	get_runtime()
	{
		return this._runtime
	}
	
	
	
	/**
	 * Get initial state, an immutable object from a Redux data store.
	 * 
	 * @returns {Immutable.Map} - component state.
	 */
	get_initial_state()
	{
		return this._initial_state
	}
	
	
	
	/**
	 * Get current state, an immutable object from a Redux data store.
	 * 
	 * @returns {Immutable.Map} - component state.
	 */
	get_state()
	{
		const path = this.state_path
		
		// console.log(context + ':get_state', this._state_store.get_state().toJS())
		// console.log(context + ':state_path', this.state_path)
		// console.log(context + ':state', this._state_store.get_state().getIn(path))
		// console.log(context + ':state js', this._state_store.get_state().getIn(path).toJS())
		
		const current_state = this._state_store.get_state().getIn(path)
		return current_state ? current_state : this._initial_state
	}
	
	
	
	/**
	 * Get current state, an immutable object from a Redux data store.
	 * 
	 * @returns {object} - component state.
	 */
	get_state_js()
	{
		const state = this.get_state()
		return state && state.toJS ? state.toJS() : {}
	}
	
	
	
	/**
	 * Get state store.
	 * 
	 * @returns {object} - State store.
	 */
	get_state_store()
	{
		return this._state_store
	}
	
	
	
	/**
	 * Get state path into a Redux data store.
	 * 
	 * @returns {array} - component state path.
	 */
	get_state_path()
	{
		return this.state_path
	}



	/**
	 * Get a state entry.
	 * 
	 * @param {string|array} arg_key_or_path - key string or strings path array.
	 * @param {any} arg_default_value - returned value on not found value result (optional)(default:undefined). 
	 * 
	 * @returns {any} - js value (plain object, number, string, array, boolean)
	 */
	get_state_value(arg_key_or_path, arg_default_value=undefined)
	{
		const state = this.get_state()

		if ( T.isString(arg_key_or_path) )
		{
			const value = state.has(arg_key_or_path) ? state.get(arg_key_or_path) : arg_default_value
			return value && T.isFunction(value.toJS) ? value.toJS() : value
		}

		if ( T.isArray(arg_key_or_path) )
		{
			const value = state.hasIn(arg_key_or_path) ? state.getIn(arg_key_or_path) : arg_default_value
			return value && T.isFunction(value.toJS) ? value.toJS() : value
		}

		return arg_default_value
	}
	
	
	
	/**
	 * Handle component 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)
		{
			return
		}
		
		if ( T.isArray(this._state_value_listeners) && this._state_value_listeners.length > 0 )
		{
			this._state_value_listeners.forEach(
				(handle)=>{
					const prev_value = arg_previous_state.getIn(handle.path)
					const new_value = arg_new_state.getIn(handle.path)

					// console.log(context + ':handle_state_change', prev_value, new_value)

					if ( ! Immutable.is(prev_value, new_value) )
					{
						handle.listener(handle.path, prev_value, new_value)
					}
				}
			)
		}
	}



	/**
	 * Register a state value change listener.
	 * 
	 * @param {string|array} arg_path - component state value path array or string with a dot separator.
	 * @param {string|function} arg_listener - state value change listener, function or method name.
	 * 
	 * @returns {nothing}
	 */
	register_state_value_change_handle(arg_path, arg_listener)
	{
		// CHECK PATH
		if ( T.isString(arg_path) )
		{
			arg_path = arg_path.split('.')
		}
		assert( T.isArray(arg_path), context + ':handle_state_path_change:bad path array')

		// CHECK LISTENER
		if ( T.isString(arg_listener) )
		{
			assert( arg_listener in (this), context + ':handle_state_path_change:listerner method string not found')
			arg_listener = this[arg_listener].bind(this)
		}
		assert( T.isFunction(arg_listener), context + ':handle_state_path_change:bad listener function')
		
		this._state_value_listeners.push( { path:arg_path, listener:arg_listener })
	}

	
	
	/**
	 * Dispatch a state action.
	 * 
	 * @param {string|object} arg_action_type - action type string or action object.
	 * @param {object|undefined} arg_options - action options object (optional).
	 * 
	 * @returns {nothing}
	 */
	dispatch_action(arg_action_type, arg_options)
	{
		let action = undefined
		
		if ( T.isString(arg_action_type) )
		{
			action = { type:arg_action_type }
		}
		else if ( T.isString(arg_action_type) && T.isString(arg_action_type.type) )
		{
			action = arg_action_type
		}
		else
		{
			assert(false, context + ':dispatch_action:bad action object')
		}
		
		if ( T.isObject(arg_options) )
		{
			action = Object.assign(action, arg_options)
		}
		
		if ( !('component' in action) )
		{
			action.component = this.get_name()
		}
		
		// console.info(context + ':dispatch_action:type=' + action.type + ' for ' + action.component, action)
		
		// assert( T.isObject(this._state_store), context + ':dispatch_action:bad state_store object')
		this._state_store.dispatch_action(action)
	}
	
	
	
	/**
	 * Get name.
	 * 
	 * @returns {string} - instance name.
	 */
	get_name()
	{
		return this.get_state_value('name', undefined)
	}
}