Skip to content

Releases: LevelbossMike/ember-statecharts

v0.7.0

19 Apr 07:22
Compare
Choose a tag to compare

From this release on ember-statecharts will use xstate's interpreter-functionality internally. This brings several advantages over the prior approach of providing a custom interpreter. Most importantly we now support all features that xstate provides 🎉 - This means ember-statecharts now also supports new features like delays and activities.

!!Warning!!

This is a 💥 breaking release. To upgrade your projects you have to update your statechart configurations:

Parameter order for statechart actions has changed

To align with xstate we don't change the order of params passed to actions anymore.

This means:

// ....
statechart: statechart({
  // ....
}, {
  actions: {
    doSomething(data, context) {
      context.anInternalComponentAction();
    }
  }
});

needs to be changed to:

// ....
statechart: statechart({
  // ....
}, {
  actions: {
    doSomething(context, eventObject) {
      // see the rest of the relase notes for changes regarding the `data`-param
      context.anInternalComponentAction();
    }
  }
});

No this in actions anymore

statechart actions won't have their implementing object set as the this-context anymore.

This means actions like this:

// ....
statechart: statechart({
  // ....
}, {
  actions: {
    doSomething() {
      this.anInternalComponentAction();
    }
  }
});

will need to be changed to this:

// ....
statechart: statechart({
  // ....
}, {
  actions: {
    doSomething(context, /* eventObject */) {
      context.anInternalComponentAction();
    }
  }
});

No magic data wrapping of data passed to events

To align with xstate's behavior we won't wrap data passed to events into a magic data-property anymore - we will use xstate's eventObjects instead:

This means:

statechart: statechart({
   // ...
}, {
  actions: {
    sayName(context, data) {
      const { name } = data;
      // name: 'tomster'     
      // ...
    }
  }
  
}),

will need to be changed to:

statechart: statechart({
   // ...
}, {
  actions: {
    sayName(context, eventObject) {
       const { type, name } = eventObject;
       // name: 'tomster', type: 'sayHello'     
      // ...
    }
  }
  
}),

Summary

Overall this is a very exciting release that aligns ember-statecharts with xstate and makes it future-proof. We are very excited to finally support all of xstate's features and we feel this release cleans up ember-statecharts significantly.

We are sorry for the caused churn but we are certain the additional features make the effort of changing your statechart configurations worthwhile.

You can have a look at the changes we had to do to make ember-statecharts test-suite compatible with the interpreter changes in this release in the PR that added this enhancement.

v0.6.1

02 Dec 12:18
Compare
Choose a tag to compare

This is a maintenance release that updates ember packages to version 3.5.x

Thanks @loganrosen for submitting the update PR #12 🎉

v0.6.0

05 Nov 19:17
Compare
Choose a tag to compare

This updates the project to make use of xstate 4.0.x. Quite a bit has changed - You can look at the xstate release-notes to see the details what needs to be changed in your statechart-configuration. Overall the new way to declare statecharts cleans everything quite a bit.

!!Warning!!

This is a breaking release. To upgrade your projects you have to update your statechart configurations to the syntax that xstate 4.0.x expects.

Please have a look at the xstate 3.x-4.x migration guide to see what you need to do to migrate your statechart configurations.

Notable changes

String actions that will call functions on the statechart's context object won't work anymore. Anonymous action-functions only declared inline in the statechart config also stop working. Actions need to be declared on the options passed to the statechart config.

Example:

// this won't work anymore
O.extend({
  machineIsOn: false,

  statechart: statechart({
    initial: 'powerOff',
    states: {
      powerOff: {
        onEntry: ['_turnMachineOff'],

        on: {
          power: {
            target: 'powerOn',
            actions: ['_turnMachineOn']
          }
        }
      },
      powerOn: {
        on: {
           // "simple" transitions still work as usual
           power: 'powerOff'
        }
      }
    }
  }),

  _turnMachineOn(/* data, context*/) {
        this.set('machineIsOn', true);
   },

   _turnMachineOff() {
      this.set('machineIsOn', false); 
    },
   // ...
});

Instead you can declare the referenced actions in the options passed to the statechart configuration - the same as you would do with guards referenced by strings. This is nice because you now have a clear differentiation between actions triggered by the statechart and internal methods of your objects. To make your life easier the this-context of actions of your statechart configuration is always set to the object implementing the statechart:

O.extend({
  machineIsOn: false,

  statechart: statechart({
    initial: 'powerOff',
    states: {
      powerOff: {
        onEntry: ['turnMachineOff'],

        on: {
          power: {
            target: 'powerOn',
            actions: ['turnMachineOn']
          }
        }
      },
      powerOn: {
        on: {
           // "simple" transitions still work as usual
           power: 'powerOff'
        }
      }
    }
  }, {
    actions: {
      turnMachineOn(/* data, context*/) {
        // `this` is the object that implements the statechart
        this.set('machineIsOn', true);
      },
      turnMachineOff() {
        this.set('machineIsOn', false); 
      }
    }
  })
})

The statechart-computed will now only work with xstate 4.x compliant configurations.

Other relevant changes:

The statechart will now execute the onEntry actions of the initialState when being created. Earlier releases of ember-statecharts did not execute the onEntry-function of the initial state because the initialState wasn't entered via a transition. This is now fixed but might lead to surprising behavior if you relied on this unintuitive behavior. Example:

Example:

O.extend({
  statechart: statechart({
    initial: 'willInit',
     states: {
       willInit: {
          onEntry: ['initAsync'],
          on: {
             resolve: 'success',
             reject: 'error'
           }
        },
        // ...
      }
  }, {
    actions: {
      initAsync() {
        return this.somethingAsync()
          .then(() => this.statechart.send('resolve'))
          .catch(() => this.statechart.send('reject'));
      }
    }
  })
})

When statechart initialization is async send calls will be enqueued until initialization

Example:

const o = O.extend({
  statechart: statechart({
    initial: 'willInit',
     states: {
       willInit: {
          onEntry: ['initAsync'],
          on: {
             click: 'busy',
           }
        },
        busy: {
          onEntry: ['beBusy']
        }
        // ...
      }
  }, {
    actions: {
      initAsync() {
        return this.somethingAsync();
      },
      beBusy() {
        console.log("I'm busy now");
      }
    }
  })
}).create();

// this will initialize the statechart and execute `initAsync`
const sc = o.get('statechart');

// we don't wait and send an event right away
sc.send('click');

// after `initAsync` has finished
// => "I'm busy"

Although this works modeling this explicitely via a transient state is most likely the better option:

O.extend({
  statechart: statechart({
    initial: 'willInit'
    states: {
      willInit: {
        on: {
           init: 'initializing'
        }
      },
      initializing: {
        onEntry: ['asyncCall'],
        on: {
          resolve: 'success',
          reject: 'error'
        }
      }
     },
     // ...
  }, {
    actions: {
      asyncCall() {
        // ...
      }
    }
  })
}) 

Notes

xstate now supports activities and delays. ember-statecharts does not support those featues because there's already a way to do this in the ember ecosystem with the existing functionality - you are encouraged to use ember-concurrency instead.

Example:

const trafficLight = O.extend({
  statechart: statechart({
    initial: 'red',
    states: {
      red: {
        onEntry: ['startTimer'],
        on: {
          timer: 'yellow'
        }
      },
      yellow: {
        // ...
      },
      // ...
    }
  }, {
    actions: {
      startTimer() {
        this.timerTask.perform();
      }
     }
  }),
  timerTask: task(function *() {
    yield timeout(TIMEOUT);
    this.statechart.send('timer');
  })
});

v0.5.0

01 Oct 10:52
Compare
Choose a tag to compare

This is a breaking release. You need to update your statechart configurations going from 0.4.0 to 0.5.0.

No more Statechart-mixin

Instead of relying on magically adding functionality to your Ember-Objects via a mixin ember-statechart will now rely on a statechart-computed-property-macro to add statechart functionality to your Ember-Objects.

You have to refactor the old way:

import Component from '@ember/component';
import Statechart from 'ember-statecharts/mixins/statechart';
import { matchesState } from 'ember-statecharts/computed';

export default Component.extend(Statechart, {
  statechart: computed(function() {
    initial: 'powerOff',
    states: {
      powerOff: {
        on: {
          power: 'powerOn'
        }
      },
      powerOn: {
        on: {
          power: 'powerOff'
        },
        initial: 'stopped',
        states: {
          stopped: {},
          playing: {}
        }
      }
    }
  }),

  playerIsStopped: matchesState({
    powerOn: 'stopped'
  }),
  
  actions: {
    power() {
      return this.states.send('power');
    }
  }
})

To this:

import Component from '@ember/component';
import { statechart, matchesState } from 'ember-statecharts/computed';

export default Component.extend({
  statechart: statechart({
    initial: 'powerOff',
    states: {
      powerOff: {
        on: {
          power: 'powerOn'
        }
      },
      powerOn: {
        on: {
          power: 'powerOff'
        },
        initial: 'stopped',
        states: {
          stopped: {},
          playing: {}
        }
      }
    }
  }),
  
  playerIsStopped: matchesState({
    powerOn: 'stopped'
  }),

  actions: {
    power() {
      return this.statechart.send('power');
    }
  }
})

Overall this is a very minimal change. If you want to use statechart-options (for guards via string references introduced in v0.4.0) this is still possible. Instead of an array of parameters you will pass two parameters directly instead:

const wat = EmberObject.extend({
  power: 1,

  statechart: statechart(
    {
      initial: 'powerOff',
      states: {
        powerOff: {
          on: {
            power: {
              powerOn: {
                cond: 'enoughPowerIsAvailable'
              }
            }
          }
        },
        powerOn: {
          initial: 'stopped',
          states: {
            stopped: {},
            playing: {}
          },
          on: {
            power: 'powerOff'
          }
        }
      }
    },
    {
      guards: {
        enougPowerIsAvailable: (context, eventData) => {
          return context.get('power') > 9000;
        }
      }
    }
  })
});


wat.get('statechart').send('power'); // won't transition power is not over 9000

wat.set('power', 9001);

wat.get('statechart').send('power') // will transition to `powerOn` as power is now over 9000

v0.4.0

24 Sep 07:46
Compare
Choose a tag to compare

This release brings some development ergonomics improvements.

You can now use the matchesState-computed-macro to check if a statechart is in a specified state without relying on writing your own computed based on Xstate's matchesState-function. Example:

matchesState- and debugState-computeds

import Component from '@ember/component';
import Statechart from 'ember-statecharts/mixins/statechart';
import { matchesState } from 'ember-statecharts/computed';

export default Component.extend(Statechart, {
  statechart: computed(function() {
    initial: 'powerOff',
    states: {
      powerOff: {
        on: {
          power: 'powerOn'
        }
      },
      powerOn: {
        on: {
          power: 'powerOff'
        },
        initial: 'stopped',
        states: {
          stopped: {},
          playing: {}
        }
      }
    }
  }),
  playerIsStopped: matchesState({
    powerOn: 'stopped'
  })
})

The matcheState-computed is a convenience-wrapper around XState's matchesState so please have a look at the docs for that function to get an idea of how to use it correctly.

We also added the debugState-computed that can be used to print a string representation of the statechart's currentState-value. This can be useful when developing and saves you from declaring the same function in your own code.

Example:

import { debugState } from 'ember-statecharts/computed';

const wat = EmberObject.extend(Statechart, {
  statechart: computed(function() {
     return {
      initial: 'powerOff',
      states: {
        powerOff: {
          on: {
            power: 'powerOn'
          }
        },
        powerOn: {
          initial: 'stopped',
          states: {
            stopped: {},
            playing: {}
          },
          on: {
            power: 'powerOff'
          }
        }
      }
    };
  }),

  _debugState: debugState()
});


wat.get('_debugState') // => "powerOff"

wat.get('states').send('power') // => "{ powerOn: 'stopped' }"

Guard references by string

This release also adds the possibility to declare guards / conditional transitions in a DRY-way by referencing guards via strings and passing options to the statechart configuration. This allows multiple transitions to reuse the same guard functions and leads to arguably more readable statechart-configurations.

const wat = EmberObject.extend(Statechart, {
  power: 1,
  statechart: computed(function() {
    return [
      {
        initial: 'powerOff',
        states: {
          powerOff: {
            on: {
              power: {
                powerOn: {
                  cond: 'enoughPowerIsAvailable'
                }
              }
            }
          },
          powerOn: {
            initial: 'stopped',
            states: {
              stopped: {},
              playing: {}
            },
            on: {
              power: 'powerOff'
            }
          }
        }
      },
      {
        guards: {
          enougPowerIsAvailable: (context, eventData) => {
            return context.get('power') > 9000;
          }
        }
      }
    ];
  })
});


wat.get('states').send('power'); // won't transition power is not over 9000

wat.set('power', 9001);

wat.get('states').send('power') // will transition to `powerOn` as power is now over 9000

Please have a look at the xstate-docs to see how to get the most out of this feature.

v0.3.0

03 Sep 07:28
Compare
Choose a tag to compare

From now on ember-auto-import will be used to import npm dependencies (xstate).

v0.2.0

31 Aug 11:14
Compare
Choose a tag to compare

Updated dependencies:

  • xstate 3.3.3
  • ember packages 3.3.0

v0.1.0

05 Jul 09:16
Compare
Choose a tag to compare

No changes compared to v0.1.0-beta.0

Statecharts are now implemented based on xState. This is a breaking change as you will need to update your statechart configuration to match what xstate expects.

v0.1.0-beta.0

22 May 09:26
Compare
Choose a tag to compare
v0.1.0-beta.0 Pre-release
Pre-release

This release refactors the implementation to use xstate internally to do the heavy lifting for us when implementing statechart functionality.

This is a breaking change as you will need to update your statechart configuration to match what xstate expects. Please look at xstate's docs to learn more.

This release also brings the ability to use orthogonal states in your statecharts 🎉

v0.0.2

21 May 09:06
Compare
Choose a tag to compare
Release v0.0.2