[Aurelia Store]: simplify access from classes


#1

Hi,
I’m working with the Aurelia Store and I’d like to get a quite similar flexibility as it is proposed in VueJS. The idea is to get the dispatchified actions easily accessible from any class.
I got the following code working as proof of concept. The idea is to have a central place for the actions definition and implementation. Then, when inovking the actions, we in fact invoke the dispatchified version of it.

store.ts

import { dispatchify, connectTo, Store, Reducer } from 'aurelia-store';
import { State, initialState } from 'state';
import { autoinject } from 'aurelia-framework';

class Actions 
{
  protected state: State;
  protected newState: State;

  public addName(name: string){    
    this.newState.names = [...this.newState.names, name];
    return this.newState;
  };

  public setState(newState: State){
    return newState;
  };

  public resetState()
  {
    return initialState;
  }
}

// Transforms action into dispatchable action
// => adds state as first argument and add a clone of state
// to work with
function actionify(func: Function)
{
  return function(state: State, ...args)
  {
    let context ={
      state: state,
      newState: Object.assign({}, state)
    };
    return func.apply(context, args);
  }
} 

/**
 * StoreAction: extend from this class to get
 * access to implemented actions from the class Actions
 * => call to the action method will in fact invoke the dispatchified version
 * of the action.
 */
@autoinject()
@connectTo()
export class StoreAction extends Actions
{ 
  constructor(protected store: Store<State>)
  {     
    super();
    for(let action of Object.getOwnPropertyNames(Actions.prototype))
    {
      if(action != 'constructor')
      {
        this.store.registerAction(action, actionify(Actions.prototype[action]));
        StoreAction.prototype[action] = dispatchify(action);
      }
    }    
  }
}

app.ts

import { StoreAction } from './store';
import { autoinject } from 'aurelia-framework';

@autoinject()
export class App extends StoreAction
{    
  message = 'Hello World! Test';

  onClick()
  {    
     console.log(this.state)            // <--   We have access to the state
    this.addName("my name"); // <---   We dispatch an action here
  }
}

What do you think?


#2

Hi,
Here is an enhanced version which supports undo as well.
I also created an async action to show that it perfectly flies too :slight_smile: .

main.ts

  // Configure store
  aurelia.use.plugin(PLATFORM.moduleName("aurelia-store"), {
    initialState, 
    history: {
      undoable: true,
      limit: 10
    }
  });

store.ts

import { dispatchify, connectTo, Store, Reducer, StateHistory, nextStateHistory } from 'aurelia-store';
import { autoinject } from 'aurelia-framework';
import { Subscription } from 'rxjs';

// STATE
export interface  State
{
  names: string[],
  types: string[]
}

export const initialState: State = {
  names: ["first", "second"],
  types: ["one"]
};

/**
 * Actions on store state
 *
 * @class Actions
 */
class Actions 
{
  public addType(type: string){
    return new Promise((resolve, reject) => {
      this.cloneState.types = [...this.cloneState.types, type];
      setTimeout(() => {
        resolve(nextStateHistory(this.state, this.cloneState));
      }, 2000);      
    });    
  };

  public addName(name: string){    
    this.cloneState.names = [...this.cloneState.names, name];
    return nextStateHistory(this.state, this.cloneState);
  };

  public setState(newState: State){
    return nextStateHistory(this.state, newState);
  };

  public resetState()
  {
    return nextStateHistory(this.state, initialState);
  }

  /**
   * Store state
   *
   * @protected
   * @type {StateHistory<State>}
   * @memberof Actions
   */
  protected state: StateHistory<State>;
  /**
   * Present store state
   *
   * @protected
   * @type {State}
   * @memberof Actions
   */
  protected currentState: State;
  /**
   * Cloned version of present store state
   *
   * @protected
   * @type {State}
   * @memberof Actions
   */
  protected cloneState: State;

  /**
   * Lists all action methods
   *
   * @static
   * @returns {string[]}
   * @memberof Actions
   */
  public static getMethods() : string[]
  {
    return Object.getOwnPropertyNames(Actions.prototype).filter((item) => item != 'constructor');
  }
}

/**
 *  Transforms action into dispatchable action
 *  => adds state as first argument and add a clone of state
 *  to work with
 * 
 * @param {Function} func
 * @returns
 */
function actionify(func: Function)
{
  return function(state: StateHistory<State>, ...args)
  {
    let context ={
      state,
      currentState: state.present,
      cloneState: Object.assign({}, state.present)
    };
    return func.apply(context, args);
  }
} 

/**
 * StoreAction: extend from this class to get
 * access to implemented actions from the class Actions
 * => call to the action method will in fact invoke the dispatchified version
 * of the action.
 *
 * @export
 * @class StoreAction
 * @extends {Actions}
 */
@autoinject()
export class StoreAction extends Actions
{ 
  private subscription: Subscription;

  constructor(public store: Store<StateHistory<State>>)
  {         
    super();
    for(let action of Actions.getMethods())
    {
      this.store.registerAction(action, actionify(Actions.prototype[action]));
      StoreAction.prototype[action] = dispatchify(action);
    }          
  }

  bind() 
  {
    this.subscription = this.store.state.subscribe(
      (state: StateHistory<State>) => this.state = state
    );      
  }

  unbind() {
    this.subscription.unsubscribe();
  }

}

Then as an example of usage:

app.ts

import { StoreAction } from '@sb/store';
import { autoinject } from 'aurelia-framework';
import { jump } from 'aurelia-store';

@autoinject()
export class App extends StoreAction
{    
  message = 'Hello World! Test';

  onClick()
  {    
    this.addType("my name");
  }

  onBack() {
    // Go back one step in time
    this.store.dispatch(jump, -1);
  }

}

app.html

<template>  
<div class="container-fluid">
  <hr>
  <button class="btn btn-primary" click.delegate="onClick()">ADD ITEM</button>
  <hr>
  <button class="btn btn-secondary" click.delegate="onBack()">BACK</button>
  <hr>
  <h1>Names</h1>

  <ul>
    <li repeat.for="name of state.present.names">${name}</li>
  </ul>

  <hr>
  <h1>Types</h1>

  <ul>
    <li repeat.for="type of state.present.types">${type}</li>
  </ul>
</div>

</template>

#3

Note that this is just a shallow copy though. You can use libs like immer.js or simply any npm package like deepclone etc. to pass deeply cloned objects.

Besides that a very nice idea and something similar that just didnt make it straight into the plugins core. But pretty great how you described it and Im sure its a helpful approach for class-based focus.