import TaglessComponent from './-tagless'; // @ts-ignore: Ignore import of compiled template import layout from '../templates/components/step-manager'; import { get, set } from '@ember/object'; import { isEmpty, isPresent } from '@ember/utils'; import { schedule } from '@ember/runloop'; import { assert } from '@ember/debug'; import { computed } from '@ember-decorators/object'; import CircularStateMachine from 'ember-steps/-private/state-machine/circular'; import LinearStateMachine from 'ember-steps/-private/state-machine/linear'; import StateMachine from 'ember-steps/-private/state-machine/-base'; import StepComponent from './step-manager/step'; /** * A component for creating a set of "steps", where only one is visible at a time * * ```hbs * {{#step-manager as |w|}} * {{#w.step}} * The first step * {{/w.step}} * * {{#w.step}} * The second step * {{/w.step}} * * * {{/step-manager}} * ``` * * @class StepManagerComponent * @yield {hash} w * @yield {Component} w.step Renders a step * @yield {Action} w.transition-to * @yield {Action} w.transition-to-next Render the next step * @yield {Action} w.transition-to-previous Render the previous step * @yield {string} w.currentStep The name of the current step * @yield {Array} w.steps All of the step names that are currently defined, in order * @public * @hide */ export default class StepManagerComponent extends TaglessComponent { layout = layout; /** * Optionally can be provided to override the initial step to render * * @property {string} initialStep the initial step * @public */ initialStep: string; /** * The `currentStep` property can be used for providing, or binding to, the * name of the current step. * * If provided, the initial step will come from the value of this property, * and the value will be updated whenever the step changes * * @property {string} currentStep the current active step * @public */ currentStep: string; /** * @property {boolean} boolean * @public */ linear: boolean; /** * @property {StateMachine} transitions state machine for transitions * @private */ transitions: StateMachine; constructor() { // @ts-ignore: Ember type definition is incorrect super(...arguments); const initialStep: string = get(this, 'initialStep') || get(this, 'currentStep'); if (!isPresent(this.linear)) { this.linear = true; } const StateMachine = this.linear ? LinearStateMachine : CircularStateMachine; set(this, 'transitions', new StateMachine(initialStep)); } @computed('transitions.currentStep') get hasNextStep() { return isPresent(this.transitions.pickNext()); } @computed('transitions.currentStep') get hasPreviousStep() { return isPresent(this.transitions.pickPrevious()); } /** * Used internally to transition to a specific named step * * @method doTransition * @param {string} to the name of the step to transition to * @param {string} from the name of the step being transitioned * @private */ doTransition(to) { // Update the `currentStep` if it's mutable if (!isEmpty(this.currentStep)) { set(this, 'currentStep', to); } // Activate the next step this.transitions.activate(to); } didUpdateAttrs() { this._super(...arguments); const newStep = this.currentStep; if (typeof newStep === 'undefined') { const firstStep: string = this.transitions.firstStep; this.transitions.activate(firstStep); } else { this.transitions.activate(newStep); } } actions = { /** * Register a step with the manager * * Adds a step to the internal registry of steps by name. * * @action register-step-component * @param {string} name the name of the step being registered * @private */ registerStepComponent( this: StepManagerComponent, stepComponent: StepComponent ) { const name = get(stepComponent, 'name'); const transitions = this.transitions; stepComponent.transitions = transitions; schedule('render', transitions, transitions.addStep, name); }, /** * Remove a step from the manager. * * Removes a step by name. It's scheduled after the addition to avoid * removing a step after immediately adding it. */ removeStepComponent( this: StepManagerComponent, stepComponent: StepComponent ) { const name = get(stepComponent, 'name'); const transitions = this.transitions; schedule('actions', transitions, transitions.removeStep, name); }, /** * Transition to a named step * * @action transition-to * @param {string} to the name of the step to transition to * @param {*} value the value to pass to the transition actions * @public */ 'transition-to'(this: StepManagerComponent, to: string) { this.doTransition(to); }, /** * Transition to the "next" step * * When called, this action will advance from the current step to the next * one, as defined by the order of their insertion into the DOM (AKA, the * order in the template). * * @action transition-to-next * @public */ 'transition-to-next'(this: StepManagerComponent) { const to = this.transitions.pickNext(); assert('There is no next step', !!to); this.doTransition(to); }, /** * Transition to the "previous" step * * When called, this action will go back to the previous step according to * the step which was visited before entering the currentStep * * @action transition-to-previous * @public */ 'transition-to-previous'(this: StepManagerComponent) { const to = this.transitions.pickPrevious(); assert('There is no previous step', !!to); this.doTransition(to); } }; }