Options
All
  • Public
  • Public/Protected
  • All
Menu

Class DynamicContainerControl

A ContainerControl that adds/removes child controls during a session.

Purpose:

  • A DynamicContainerControl delays and perhaps avoids the addition of child controls that are only needed occasionally. By adding controls on-demand the control tree remains compact and easy to reason about. The alternative is to include all possible child controls and set them to be inactive until needed; this alternative may be simpler when there are few potential controls but is less convenient when there are many.

  • A potential purpose is to support an unbounded number of child controls of a certain type (e.g. add-another-phone-number, add-another-address). By adding additional controls the user can refer to any of the children at any time (via each control's target prop) and each control can have its own durable state. However, if the user will only discuss one 'active' item at a time it may be simpler to use a regular container control that manages the list data directly and which reconfigures static child controls whenever the active item changes (see the FruitShop demo skill for an example of this approach).

Details:

The tricky part of managing a DynamicContainerControl is the re-initialization of the control at the start of each turn. To accomplish this, DynamicContainerControl introduces new conventions:

  1. The minimal specification for each dynamic control is tracked in this.state.dynamicChildSpecifications.
  2. The built-in method this.addDynamicChildBySpecification(specification) calls this.createDynamicChild(specification) to actually instantiate the control. this.addDynamicChildBySpecification() also records that the child was created in this.state.dynamicChildSpecifications.

Usage:

  1. implement the abstract function createDynamicChild(specification) to create a control from a specification object.

  2. in handle(), use this.addDynamicChildBySpecification(specification) to add a dynamic child and this.removeDynamicChild(control) to remove a dynamic child.

    Example:

    class ContactInfoControl extends DynamicContainerControl: {
    handle(input, resultBuilder){
     // adding a new child control during handling.
     if(userWantsToAddFaxNumber){
       this.addDynamicChildBySpecification({id: 'faxNumber'})
     }
    }
    
    createDynamicChild(spec: DynamicControlSpecification): Control {
    switch(spec.id){
       case 'faxNumber': return new ListControl( ...propsForFaxNumber...)
       default: throw new Error('unknown child info');
    }
    }
    }

    Q & A: Why is all this necessary?

    The problem being solved by ControlManager.createControlTree() and Control.reestablishState() is to recreate a tree of controls in which each control includes both configuration props and state. The first complication is that the configuration props are generally not serializable as they may contain functions and deep references. Dynamic controls further complicate matters as we cannot know which controls to rebuild until we have reestablished some state.

    By rebuilding controls statically (normal case) and from POJO specifications (dynamic case) we can limit the information that must be tracked and still rebuild controls with all their complex props and state. Overall, these patterns allow for arbitrarily complex props while ensuring that only the critical information is tracked between turns.

    The dynamic-control pattern is standardized in DynamicContainerControl to reduce the need for developers to reinvent the wheel.

Hierarchy

Implements

Constructors

constructor

Properties

children

children: Control[] = []

Readonly id

id: string

props

rawProps

selectedHandlingChild

selectedHandlingChild: Control | undefined

selectedInitiativeChild

selectedInitiativeChild: Control | undefined

state

Methods

addChild

  • Add a control as a child.

    The control is appended to the end of the this.children array.

    Parameters

    Returns this

    the container

addDynamicChildBySpecification

  • Adds a new dynamic child control.

    The child is added to the end of the this.children array.

    Parameters

    • specification: DynamicControlSpecification

      object defining the control to create. The specification should be minimal information that enables the creation of a complete control. An id is mandatory, and any other information is optional. The specification must be serializable or convertible to a serialized form.

    Returns void

canHandle

canHandleByChild

  • Determines if a child control can handle the request.

    From the candidates that report canHandle = true, a winner is selected by this.decideHandlingChild(candidates).

    The selected "winner" is recorded in this.selectedHandlingChild.

    Parameters

    Returns Promise<boolean>

canTakeInitiative

canTakeInitiativeByChild

  • canTakeInitiativeByChild(input: ControlInput): Promise<boolean>
  • Determines if a child control can take the initiative.

    From the candidates that report canTakeInitiative = true, a winner is selected by this.decideInitiativeChild(candidates).

    The selected "winner" is recorded in this.selectedInitiativeChild.

    Parameters

    Returns Promise<boolean>

Abstract createDynamicChild

  • Create a Control from a DynamicControlSpecification

    Purpose:

    • This method is called to create a dynamic control at the time of first creation and during the initialization phase of every subsequent turn.

    Usage:

    • Inspect the specification object and determine the type of Control to instantiate.
    • Instantiate a new control and ensure control.id = specification.id.

    Parameters

    Returns Control

decideHandlingChild

  • Decides a winner from the canHandle candidates.

    The candidates should be all the child controls for which canHandle(input) = true

    Default logic:

    1. Choose the most-recent initiative control if is a candidate.
    2. Otherwise, choose the first candidate in the positional order of the this.children array.
    3. In the special case of input===FallbackIntent, only the most-recent initiative control is considered. If it is not a candidate, then no child is selected and this method returns undefined.

    Remarks:

    • The special case for FallbackIntent exists because that intent is not user-initiative -- rather it indicates a failure to understanding the user. In cases of misunderstanding, only active controls should be considered.

    Parameters

    • candidates: Control[]

      The child controls that reported canHandle = true

    • input: ControlInput

      Input

    Returns Promise<Control | undefined>

decideInitiativeChild

  • Decide a winner from the canTakeInitiative candidates.

    The eligible candidates are child controls for which canTakeInitiative(input) = true.

    Default logic:

    1. choose the most-recent initiative control if is a candidate.
    2. otherwise choose the first candidate in the positional order of the this.children array.

    Parameters

    • candidates: Control[]

      The child controls that reported canTakeInitiative = true

    • input: ControlInput

      Input

    Returns Promise<Control | undefined>

evaluateAPLProp

  • Evaluate an APL document/data source prop.

    Parameters

    • act: SystemAct

      act

    • input: ControlInput

      The input object

    • propValue: object | function

      Constant or function producing a map of key:value pairs

    Returns object

    • [key: string]: any

evaluateBooleanProp

  • evaluateBooleanProp(propValue: boolean | function, input: ControlInput): boolean
  • Evaluate a boolean prop.

    Parameters

    • propValue: boolean | function

      Constant or function producing boolean

    • input: ControlInput

      The input object

    Returns boolean

evaluateFunctionProp

  • evaluateFunctionProp<T>(prop: T | function, input: ControlInput): T

evaluatePromptProp

gatherHandlingCandidates

gatherInitiativeCandidates

getSerializableState

  • getSerializableState(): any
  • Gets the Control's state as an object that is serializable.

    Only durable state should be included and the object should be serializable with a straightforward application of JSON.stringify(object).

    Default: {return this.state;}

    Usage:

    • This method must be idempotent (multiple calls must not change the result).
    • The default is sufficient for Controls that use the .state variable and only store simple data.
      • Non-simple data includes functions, and objects with functions, as these will not survive the round trip.
      • Other non-simple data include types with non-enumerable properties.
    • It is safe to pass the actual state object as the framework guarantees to not mutate it.
    • Functions that operate on the Control's state should be defined as member function of the Control type, or as props.

    Framework behavior:

    • During the shutdown phase the state of the control tree is collected by calling this function for each control.
    • The framework serializes the data use a simple application of JSON.stringify.
    • On the subsequent turn the control tree is rebuilt and the state objects are re-attached to each Control via control.setSerializableState(serializedState).

    Returns any

    Serializable object defining the state of the Control

handle

handleByChild

isReady

  • Determines if the Control's value is ready for use by other parts of the skill.

    Note:

    • A basic invariant is isReady === !canTakeInitiative because isReady implies that no further discussion is required and thus there is no need to take the initiative.

    Parameters

    Returns Promise<boolean>

    true if the control has no further questions to ask the user such as elicitation, clarification or confirmation.

reestablishState

  • reestablishState(state: any, controlStateMap: object): void
  • Parameters

    • state: any
    • controlStateMap: object
      • [index: string]: any

    Returns void

removeDynamicControl

  • removeDynamicControl(id: string): void
  • Removes a dynamic control.

    The control is removed from this.children and the specification is removed from this.state.dynamicChildSpecifications

    Parameters

    • id: string

    Returns void

renderAPLComponent

  • Add response APL component by this control.

    This is intended to be used to provide APL rendering component for a control to process inputs, provide feedback, elicitation etc through touch events on APL screens.

    Parameters

    Returns object

    • [key: string]: any

renderAct

  • Add response content for a system act produced by this control.

    This is intended to be used with the default ControlManager.render() which implements a simple concatenation strategy to form a complete response from multiple result items.

    Parameters

    Returns void

setSerializableState

  • setSerializableState(serializedState: any): void
  • Sets the state from a serialized state object.

    Default: {this.state = serializedState;}

    Usage:

    • This method must be idempotent (multiple calls must not change the result).
    • It is safe to use serializedState without copying as the framework guarantees to not mutate it.

    Framework behavior:

    • During the initialization phase the control tree is rebuilt and state objects are re-attached to controls by calling this method for each control.

    Parameters

    • serializedState: any

      Serializable object defining the state of the Control

    Returns void

stringifyStateForDiagram

  • stringifyStateForDiagram(): string

takeInitiative

takeInitiativeByChild

throwUnhandledActError

  • throwUnhandledActError(act: SystemAct): never

Static mergeWithDefaultProps