JET Custom Components

Oracle® JavaScript Extension Toolkit (JET)
15.1.0

F83698-01

Overview

JET allows developers to create custom Web Components which can be composites of other components, HTML, JavaScript, or CSS. These reusable pieces of UI can be embedded as custom HTML elements and are registered using the Composite API. These custom Web Components will be referred to as "composites" throughout the rest of this doc.

Using a Composite

Once registered within a page, a composite component can be used in the DOM as a custom HTML element like in the example below. A composite element will be recognized by the framework only after its module is loaded by the application. Once the element is recognized, the framework will register a busy state for the element and will begin the process of 'upgrading' the element. The element will not be ready for interaction (e.g. retrieving properties or calling methods) until the upgrade process is complete with the exception of property setters and the setProperty and setProperties methods. The application should listen to either the page-level or an element-scoped BusyContext before attempting to interact with any JET custom elements. See the BusyContext documentation on how BusyContexts can be scoped.


<my-chart type="bubble" data="{{dataModel}}"></my-chart>

The upgrade of JET composite elements relies on any data binding resolving, the management of which is done by a binding provider. The binding provider is responsible for setting and updating attribute expressions and any custom elements within its managed subtree will not finish upgrading until it applies bindings on that subtree. By default, there is a single binding provider for a page, but subtree specific binding providers can be added by using the data-oj-binding-provider attribute with values of "none" and "knockout". The default binding provider is knockout, but if a page or DOM subtree does not use any expression syntax or knockout, the application can set data-oj-binding-provider="none" on that element so its dependent JET composite custom elements do not need to wait for bindings to be applied to finish upgrading.

Since a composite is registered as a custom HTML element, the same set of rules for attribute/property setting, data binding, method access, slotting, and event listening that apply to JET custom elements apply to composites. Please see the general JET Component Overview documentation to for more details.

Writing a Composite

Packaging and Registration

Composite components should be packaged as a standalone module in a folder matching the tag name it will be registered with, e.g. 'my-chart'. An application would use a composite by requiring it as a module, e.g. 'jet-composites/my-chart/loader'. The composite module could be stored locally in the app which is the recommended approach, but could also be stored on a different server, or a CDN. Note that there are XHR restrictions when using the RequireJS text plugin which may need additional RequireJS config settings. Please see the text plugin documentation for the full set of limitations and options. By using RequireJS path mappings, the application can control where individual composites are loaded from. See below for a sample RequireJS composite path configuration. Note that the 'jet-composites/my-chart' mapping is only required if the 'my-chart' composite module maps to a folder other than 'someSubFolder/jet-composites/my-chart' using the configuration below.


requirejs.config(
{
  baseUrl: 'js',
  paths:
  {
    'jet-composites': 'someSubFolder/jet-composites',
    'jet-composites/my-chart': 'https://someCDNurl',
    'jet-composites/my-table': 'https://someServerUrl'
  }
}

All composite modules should contain a loader.js file which will handle registering and specifying the dependencies for a composite component. We recommend using RequireJS to define your composite module with relative file dependencies. Registration is done via the Composite.register API. By registering a composite component, an application links an HTML tag with provided Metadata, View, ViewModel and CSS which will be used to render the composite. These optional pieces can be provided via a descriptor object passed into the register API. See below for sample loader.js file configurations.

Note that in this example we are using require-css, a RequireJS plugin for loading css which will load the styles within our page so we do not need to pass any css into the register call. This is the recommended way to load CSS, especially for cases where the composite styles contain references to any external resources.

define(['ojs/ojcomposite', 'text!./my-chart.html', './my-chart', 'text!./my-chart.json', 'css!./my-chart'],
  function(Composite, view, viewModel, metadata) {
    Composite.register('my-chart',
    {
      metadata: JSON.parse(metadata),
      view: view,
      viewModel: viewModel
    });
  }
);
This example shows how to register a custom parse function which will be called to parse attribute values defined in the metadata.

define(['ojs/ojcomposite', 'text!./my-chart.html', './my-chart', 'text!./my-chart.json'],
  function(Composite, view, viewModel, metadata) {
    var myChartParseFunction = function(value, name, meta, defaultParseFunction) {
      // Custom parsing logic goes here which can also return defaultParseFunction(value) for
      // values the composite wants to default to the default parsing logic for.
      // This function is only called for non bound attributes.
    }

    Composite.register('my-chart',
    {
      metadata: JSON.parse(metadata),
      view: view,
      viewModel: viewModel,
      parseFunction: myChartParseFunction
    });
  }
);

Metadata

Composite Metadata should be provided in a component.json file and contains information about the composite used at run time to wire properties accessors and methods on the custom element. The JET Metadata schema also allows composite authors to include information that would benefit a design time environment and is described in more detail in the Metadata Overview doc.

Example of Run Time Metadata

The JET framework will ignore "extension" fields. Extension fields cannot be defined at the first level of the "properties", "methods", "events", or "slots" objects.


{
 "name": "demo-card",
 "version": "1.0.2",
 "jetVersion": ">=3.0.0 <5.0.0", "properties": { "currentimage" : "type": "string", "readonly": true }, "images": "array"
   },
   "isShown": {
     "type": "boolean",
     "value": true
   }
 },
 "methods": {
   "nextImage": {
     "internalName": "_nextImg"
     "extension": "This is where a composite can store additional data."
   },
   "prevImage": {}
  },
  "events": {
    "cardclick": {}
  }
}

View

The composite View is the template HTML that will be stamped into the DOM when bindings are applied to the composite.

View Variables

The View has access to several '$' variables along with any public variables defined in the composite's ViewModel. Public ViewModel variables can be accessed by referencing the variable names directly without needing to access them from another object. The View's '$' variables are similar to what is available on context object passed to the composite ViewModel constructor and are listed below.

  • $properties: A map of the composite component's current properties and values.
  • $slotCounts: A map of slot name to assigned nodes count for the View.
  • $unique: A unique string that can be used for unique id generation.
  • $uniqueId: The ID of the composite component if specified. Otherwise, it is the same as unique.

Slotting

The View can also contain slots where application provided DOM can go. Complex composite components which can contain additional composites and/or content for child facets defined in its associated View can be constructed via slotting. There are two ways to define a composite's slots, using either an oj-bind-slot element or an oj-bind-template-slot element to indicate that that slot's content will be stamped using an application provided template. See the relevant slot API docs for more information.

Binding Order

The following steps will occur when processing the binding for a composite component:

  1. Apply bindings to children using the composite component's binding context.
  2. Create a slot map assigning component child nodes to View slot elements.
    1. At this point the component child nodes are removed from the DOM and live in the slot map.
  3. Insert the View and apply bindings to it with the ViewModel's binding context.
    1. The composite's children will be 'slotted' into their assigned View slots.
    2. The oj-bind-slot's slot attribute, which is "" by default, will override its assigned node's slot attribute.

ViewModel

If a constructor function is provided at registration, a new instance of the ViewModel will be created for each composite element. The ViewModel is where the composite's internal logic lives and can expose public variables that the View can bind to. The ViewModel constructor is called with a context object that allows the ViewModel logic to access the composite element, the current set of properties, and other keys.

Interacting with Properties

The ViewModel can access and update properties from the context.properties object. Any updates to context.properties will be interpreted as an internal property change. This is where readOnly and writeback property updates can be done, e.g. context.properties.value = 'new value'. Updates to properties on the composite element will be interpreted as coming from the application and updates to readOnly properties will be blocked. These internal property updates will trigger [property]Changed events and can be differentiated from application triggered property udpates using the updatedFrom field on the [property]Changed events. All ViewModel property updates will result in 'internal' set as the updatedFrom field.

The context.properties object also has setProperty, getProperty, setProperties methods similar to the composite element. The setProperty method should be used when updating subproperties so [property]Changed events are fired with the subproperty field populated correctly.

The ViewModel can listen to property changes by registering a propertyChanged callback. The callback is called with a context object that has the same set of properties as a [property]Changed event. This listener will be called before the [property]Changed event is fired allowing the composite to react first.

Lifecycle Callbacks

A set of optional callback methods can be implemented on the ViewModel and will be called at each stage of the composite lifecycle. These callbacks can be used to delay composite rendering, perform expensive actions like data fetches, and react to property changes. To see the full set of lifecycle callbacks, see the Composite.ViewModel typedef for details.

Firing Custom Events

If the composite needs to fire any events, it should done using the CustomEvent constructor, which JET provides a polyfill for on IE. Events should be documented in the metadata for design time environments.

CSS

Composite component styling can be done via provided CSS. The JET framework will add the oj-complete class to the composite DOM element after metadata properties have been resolved. To prevent a flash of unstyled content before the composite properties have been setup, the composite CSS can include the following rule to hide the composite until the oj-complete class is set on the element.


my-chart:not(.oj-complete) {
  visibility: hidden;
}

Composite CSS will not be scoped to the composite component and selectors will need to be appropriately selective. We recommend scoping CSS classes and prefixing class names with the composite name as seen in the example below. Note that we do not recommend overriding JET component CSS. Composites should only update JET component styling via SASS variables.


my-chart .my-chart-text {
  color: white;
}