Custom Extended Types

Page authors can implement a Visual Builder type class using either the Extended Type mechanism (that extends from the vb/types/extendedType class module) or use the Instance Factory mechanism. The latter is much simpler to use since authors can simply plug their type into a Visual Builder variable without writing any extra JavaScript code (which was needed with the Extended Type system).

At runtime the instance of the custom type class can automatically make use of the redux framework to store its 'value' (state). Visual Builder variables generally have a type that points to a class or a type definition or can be a JavaScript primitive or an object. The Visual Builder runtime discovers built-in types and custom types by detecting a forward slash in the type name (for example, my/ComicStripType). The type is assumed to be a require path to a type module and loads it.

An example:

"myVariable": {
  "type": "my/ComicStripType",
  "defaultValue": {}
}

Reserved Properties

value

The state of an extended type is generally referred to as its value and its default value can be specified using the 'defaultValue' property of a variable. For example, the comicStripType specifies its default value, an Object, by providing defaults for 'name', 'publicationType' etc. Also note that charactersADP is a reference to a variable of type vb/ArrayDataProvider2.

The type of the value is defined via the 'getTypeDefinition' function (see below). In this example, this would be the properties in the defaultValue object: name, publicationType, publications, etc.

In order to make the value accessible in expressions via '<$scope>.variables.comicStripVar.value' where $scope is $page/$flow etc., and 'comicStripVar' is the type instance of the custom type that is created, 'value' is a special property defined on the extended type instance and for this reason, will overlay any local 'value' property defined in your implementation. For this reason, take care not to use this property internally! Property accessors to read (see getValue() method) and write (see setValue() method) the value are provided.

"comicStripVar": {
  "type": "vb/sample/types/comicStripType",
  "defaultValue": {
    "name": "flowPage-Calvin & Hobbes",
    "publicationType": "flowPagePublicationType",
    "publications": [
      {
        "publication": "Universal Press Syndicate",
        "volumes": 24,
        "author": "Bill Watterson",
        "title": "The Doghouse",
        "year": 1987,
        "launchDate": "1985-11-18T08:00:00.000Z"
      },
      {
        "publication": "United Feature Syndicate",
        "volumes": 250,
        "author": "Bill Watterson",
        "title": "Calvin and Hobbes",
        "year": 1990,
        "launchDate": "1990-06-01T08:00:00.000Z"
      }
    ],
    "charactersADP": "{{ $variables.flow1SecondComicCharactersAdpVar }}"
  }
}

internalState

In addition to 'value', extended type instances are provided an 'internalState' property. Custom types can externalize their internal state so that it can be captured in redux by using this 'internalState' property. More specifically they can use property accessors to read (see getInternalState() method) and write (see setInternalState() method) the internal state are provided.

Methods

getTypeDefinition

As stated before, the type definition for the value of an extended type must be provided via the 'getTypeDefinition' function. This method is called at the time the type instance is created. The example below returns the type definition of the state (value) of comicStripType. name, publicationType, publications and charactersADP represent its state.

class ComicStripExtendedType extends ExtendedType {
  getTypeDefinition(variableDef, scopeResolver) {
    let publicationsDef = 'any';
    if (variableDef.defaultValue && variableDef.defaultValue.publicationType) {
      // responseType is specified in the defaultValue
      const { publicationType } = variableDef.defaultValue;

       if (typeof publicationType === 'string') {
        publicationsDef = `${publicationType}[]`;
      }
    }
    return {
      type: {
        name: 'string',
        publicationType: 'string',
        publications: TypeUtils.getType(`${this.getId()}:${publicationsDef}`,
          { type: publicationsDef }, scopeResolver),
        charactersADP: 'vb/ArrayDataProvider2',
      },
      resolved: true, // because we are pre-resolving type references
    };
  }
}
hoistValueObjectProperties

As a convenience, if the type of this variable as defined in 'getTypeDefinition' is 'object', all root properties of the values will be hoisted to the root variable type instance. This allows these properties to be accessible via expressions like '$scope.variables.theInstance.property'. If this is not desired, return false from 'hoistValueObjectProperties'.

init / activate / dispose (lifecycle methods)

A Visual Builder variable goes through various lifecycle stages. Extended type instances will be notified of these stages via the init, activate and dispose methods.

  • activate

    The 'activate' method is called when this and other variables in the current scope have been created and its initial (default) values determined. This method is called right before the 'vbEnter' event and the value of the variable, and can be a good time for types to do other setup using the resolved value. It is important to note that at the time 'activate' is called, any value assigned, to the extended type variable or the variables it depends on, in the vbEnter action chains will not be available.

  • dispose

    The 'dispose' method is called when the current scope is being torn down and all variables, including this variable is being disposed. This would be a good time to cleanup state for the extended type. It is important to note that any outstanding async tasks that are pending, would be the responsibility of the extended type to wind down gracefully.

handlePropertyValueChanged

When the value of an extended type variable changes (say via assignVariablesAction) it will be notified of the change via this method.

invokeEvent

Additionally, custom type implementations have the ability to fire a custom event using 'invokeEvent', providing a name, payload. For example, 'comicStripUpdate' is an event fired by the ComicStripType in the sample provided below.

getType

Custom extended types can retrieve the exploded type structure given a type definition, using the 'getType' method.

Sample Extended Type - ComicStripType

Implementation

'use strict';
  define(['vb/types/extendedType', 'vb/types/typeUtils'], (ExtendedType, TypeUtils) => {
  class ComicStripType extends ExtendedType {
      getTypeDefinition(variableDef, scopeResolver) {
      let publicationsDef = 'any';
      if (variableDef.defaultValue && variableDef.defaultValue.publicationType) {
        const { publicationType } = variableDef.defaultValue;
          if (typeof publicationType === 'string') {
          publicationsDef = `${publicationType}[]`;
        }
      }
      return {
        type: {
          name: 'string',
          publicationType: 'string',
          publications: TypeUtils.getType(`${this.getId()}:${publicationsDef}`,
            { type: publicationsDef }, scopeResolver),
          charactersADP: 'vb/ArrayDataProvider2',
        },
        resolved: true, // because we are pre-resolving type references
      };
    }
    
    activate() {
      console.log('activate called on variable', this.id);
        const value = this.getValue();
      const { name } = value;
      const { publicationType } = value;
      const { publications } = value;
      const { charactersADP } = value;
      let charactersADPVValue;
      if (charactersADP) {
        charactersADPVValue = charactersADP.getValue();
      }
 
      const initialValue = {
        name, publications, publicationType, charactersADPVValue,
      };
      
      this.setInternalState('opStatus', 'not-started');
      console.log('initial evaluated value for variable', this.id, 'is', finalValue);
    }
    
    handlePropertyVariableChangeEvent(e) {
      if (e.name.endsWith('value')) {
        if (e.diff) {
          if (e.diff.publications) {
            // process value change here
          }
        }
      }
    }
  
    /**
     * a sample method provided by this type that fakes a async op and updates the internalState
     * @returns {Promise<T>}
     */
    callAsyncMethod() {
      this.setInternalState('opStatus', 'started');
        return Promise.resolve().then(() => {
        // call some other async method; set some internalState and fire an event
        callAnotherAsyncMethod().then((res) => {
          const result = res;
          this.setInternalState('opStatus', 'completed');
          this.invokeEvent('comicStripUpdate', { status: 'success', result });
        });
      });
    }
  }
  
  return ComicStripType;
});