Class: VComponent

Oracle® JavaScript Extension Toolkit (JET)
9.0.0

F24343-01

Signature:

abstract class VComponent<P extends object = any, S extends object = any>

QuickNav

Fields

VComponent

Version:
  • 9.0.0
Since:
  • 9.0.0
Module:
  • ojvcomponent

Module usage

See JET Module Loading for an overview of module usage within JET.

Typescript Import Format
//To import this class, use the format below.
import {VComponent} from "ojs/ojvcomponent";

//This module also exports compiler decorators. To import a decorator use the named import format, for example:
import {customElement} from "ojs/ojvcomponent";
Generic Parameters
ParameterDescription
PType of the props object
SType of the state object

JET In Typescript

A detailed description of working with JET elements and classes in your typescript project can be found at: JET Typescript Usage.

Description

The VComponent base class provides a mechanism for defining JET Custom Components. Like the JET Core Components and composite components, VComponent-based components are exposed as custom elements. From the application developer’s perspective, these custom elements are (essentially) indistinguishable from JET’s other component types. Where VComponents differ is in the component implementation strategy: VComponents produce content via virtual DOM rendering.

To create a new VComponent-based custom component, the component author typically does the following:

  • Implements a class that extends VComponent. This class must be authored in TypeScript.
  • Overrides the render() method to return a virtual DOM representation of the component’s content.
  • Sets the @customElement() decorator with the custom element tag name passed in as a parameter.
  • Defines the public contract of the custom element.
    • Properties: defined as members of the Props class.
    • Methods: defined as methods of the VComponent class and marked for exposure on the custom element using the @method() decorator.
    • Events: defined as members of the Props class using the naming convention on[EventName] and having type Action or CancelableAction.
    • Slots: defined as members of the Props class having type Slot.

Given the above, JET generates an HTMLElement subclass and registers this as a custom element with the browser. These VComponent-based custom elements can then be used anywhere that other JET components are used, and application developers can leverage typical JET functionality such as data binding, slotting, etc.

A minimal VComponent subclass is shown below:


import { h, VComponent, customElement } from "ojs/ojvcomponent";
import "ojs/ojavatar";

class Props {
  initials?: string = '';
  fullName?: string = '';
  department?: 'Billing' | 'Sales' | 'Engineering';
  rank?: Rank = { level: 1, title: 'entry level' };
}

type Rank = {
  level: number,
  title: string
};

@customElement('oj-sample-employee')
export class SampleEmployee extends VComponent<Props> {

  protected render(): VComponent.VNode {
    return (
      <div>
        <oj-avatar initials={this.props.initials} />
        <span>{this.props.fullName}</span>
      </div>
    );
  }
}

Rendering

Every VComponent class must provide an implementation of the render() method. This method returns a tree of virtual DOM nodes that represents the component's content. The return value can take one of two forms:

  1. The render function can return a single virtual DOM node representing the root custom element and any child content specified as virtual DOM children. If the component needs to modify root attributes, needs to set a ref callback on the root custom element, or contains multiple virtual DOM children, this return value form must be used.
  2. If the component's content consists of a single virtual DOM child, a single virtual DOM node representing the child node may be returned, omitting a node representing the root custom element.
While it is always acceptable to include a virtual DOM node representing the root custom element as in #1, the return form in #2 is supported as a convenience. In many cases, the root virutal DOM node can be omitted.

Virtual DOM nodes are plain old JavaScript objects that specify the node type (typically the element’s tag name), properties and children. This information is used by the underlying virtual DOM engine to produce live DOM (i.e. by calling document.createElement()).

Virtual DOM nodes can be created in one of two ways:

  • By calling the virtual DOM node factory function, which is exported from the ojs/ojvcomponent module under the name "h". The h factory function takes the type, properties and children and returns a virtual DOM node.
  • Declaratively via TSX (a TypeScript flavor of JSX).

The latter approach is strongly preferred as it results in more readable code. A build-time transformation step will ultimately convert the TSX markup into calls to h() that will be executed at run-time.

Note that in either case, the virtual DOM factory function must be imported as an import named "h".

The render() method will be called whenever component state or properties change to return the new VDOM. The virtual component will then diff the VDOM and patch the live DOM with updates. As custom elements, these virtual components are used in the same way as other JET components, supporting data binding and slotting.

JSX Syntax

Virtual component render functions support the use of JSX which is an XML syntax that looks similar to HTML, but supports a different attribute syntax.

JSX Attributes

Component properties, global HTMLElement properties, event listeners, ref, and key attributes can all be specified using the virtual component JSX attribute syntax. JSX expects the HTMLElement property names for all JSX attributes except for class and for. In cases where an attribute does not have an equivalent property on the HTMLElement (data-, aria-, role, etc), the attribute name should be used. The style attribute is special cased and only supports object values e.g. style={ {color: 'blue', fontSize: '12px'} }. Primitive JSX attribute values can be set directly using the equal operator, or within {...} brackets for JavaScript values e.g. the style example above.

The JET data binding syntax using double curly or square brackets is not supported when using JSX. Additionally, subproperty syntax (e.g. complexProperty.subProperty={...}) is not supported; when dealing with complex-typed properties, the full value must be specified (i.e. complexProperty={ {subProperty: ...} }).

class

The class JSX attribute supports space delimited class names in addition to an Object whose keys are individual style classes and whose values are booleans to determine whether those style classes should be present in the DOM. (e.g. class={ {'oj-hover': isHovered} }).

Event Listeners

Event listeners follow a 'on'[EventName] naming syntax e.g. onClick={clickListener} and unlike data bound on-click listeners set on the root custom element, JSX event listeners will only receive a single event parameter. Use the @listener decorator to bind an event listener to the component instance - 'this'. The @listener decorator accepts an options object that would be passed to the DOM addEventListener() method for specifying capture or passive listeners.


import { h, VComponent, customElement, listener } from "ojs/ojvcomponent";

@customElement('oj-sample-component')
export class SampleComponent extends VComponent {

  @listener({ passive: true })
  private _touchStartHandler(event) {
    // handler code
  }

  protected render(): VComponent.VNode {
    return (
      <div onTouchstart={this._touchStartHandler}>
        …
      </div>
    );
  }
}

Refs

While we recommend that rendering is done declaratively, for use cases where a reference to a DOM node is necessary, a ref attribute along with a callback can be set on the virtual node within the render function. The callback function will be called with either a DOM node when using the element syntax or a VComponent instance when using the class syntax after the node has been inserted into the DOM. The ref callback will be called again with null when the node has been unmounted. See the lifecycle doc for ref callback ordering in relation to other lifecycle methods.


import { h, VComponent, customElement } from "ojs/ojvcomponent";

@customElement('oj-sample-component')
export class SampleComponent extends VComponent {
  private _scrollingDiv: HTMLDivElement;

  protected render(): VComponent.VNode {
    return (
      <div ref={this._setScrollingDiv}>
        …
      </div>
    );
  }

  protected mounted(): void {
    this._adjustScrollingDiv();
  }

  protected updated(oldProps: Readonly<Props>, oldState: Readonly<State>): void
    this._adjustScrollingDiv();
  }

  private _setScrollingDiv = (elem) => {
    this._scrollingDiv = elem as HTMLDivElement;
  }

  private _adjustScrollingDiv(): void {
    // Perform some calculations
    …
    this._scrollingDiv.style.height = calculatedValue;
  }

}

Keys

When rendering lists of virtual nodes, it may be beneficial to set key attributes in JSX to help distinguish between insertions, deletions, and updates. Without keys, the VComponent diffing logic will compare the old and new virtual node lists in order, so an insertion before the first virtual node will result in a diff for all subsequent virtual nodes without the key attribute. The key can be of type string or number.

Root Attributes

In general, we do not recommend modifying core HTML properties on the custom element to avoid overriding application set values. However in cases where this is necessary (e.g. moving or copying attributes for accessibility), authors should register properties they plan to update or listen to changes from as members of their Props class, marked with the @rootProperty decorator. These root properties will then be populated in the component's this.props object as long as they are present in the live DOM; unlike component properties, no default values will be made available in this.props for root properties. When rendering, only core HTML properties that are specifically marked with the @rootProperty decorator will be reflected in the live DOM on the root custom element; any other core HTML properties will be ignored. Components will be notified of changes to root properties similar to component properties and trigger a rerender.

Style and class properties can be set on the root custom element and are applied additively to the application-provided style and class. Event listeners can be added using the on[PropertyName] syntax in the root element within the component's render() method and will be added or removed using the DOM's addEventListener and removeEventListener methods. Style, class, and event listeners can always be specified on the root custom element and do not need to be declared as members of the Props class unlike other root properties.


  protected render(): VComponent.VNode {
    return (
      <oj-sample-component onClick={this._clickListener} style={ {color: red} } class='my-class-name'>
        <div>…</div>
      </oj-sample-component>
    );
  }

Components often need to generate unique IDs for internal DOM. The uniqueId() method can be called to retrieve an id that is unique to the component instance (matching the live DOM if it has been specified) that can be used e.g. as a prefix for IDs on internal DOM.

State Updates

Components may track internal state that is not reflected through their properties. There is a state mechanism for supporting this. Components should initialize their state objects in their constructors. After that, components should treat their this.state objects as immutable and call updateState to request a state update.

The updateState() method does not immediately update the state of the component, it just puts the update in a queue to be processed later. The framework will batch multiple updates together to make rendering more efficient. It will schedule the change and rerender the component. Consider using function callback instead of an object when updating the state in order to avoid stale data for the state. Calls to updateState() will only cause the component to rerender if the end result differs from the original state.


import { h, VComponent, customElement } from "ojs/ojvcomponent";

class Props { … }

type State = {
  foo: boolean,
  bar: boolean
}

@customElement('oj-sample-component')
export class SampleComponent extends VComponent<Props, State> {
  constructor(props: Readonly<Props>) {
    // State should be instantiated in the constructor
    this.state = {
      foo: true,
      bar: false
    }
  }

  @listener()
  private _handleClick() {
    // Update state in response to user interaction, triggering a
    // re-render.
    this.updateState({ foo: false });
  }
}

Default Values

Static default values for components can be provided using direct value assignments on the corresponding members in the Props class. In addition, dynamic default values can be specified using the @dynamicDefault() decorator with a parameter representing a method that should be called to retrieve the default value at runtime.

Object- or Array-typed default values will recursively frozen before being returned as property values to prevent subsequent modification. Any Objects that are not POJOs will not be frozen (including references to them inside other Objects or Arrays) and it is the component's responsibility to ensure that default values of this type (e.g. class instances) are immutable.


import { h, VComponent, customElement, dynamicDefault } from "ojs/ojvcomponent";

function computeDynamicDefault(): string { … }

class Props {
  primitiveProperty?: number = 0;
  complexProperty?: {index: number} = {index: 0};
  classProperty?: MyType = new ImmutableMyTypeImpl();
  @dynamicDefault(computeDynamicDefault) dynamicProperty?: string;
}

@customElement('oj-sample-component')
export class SampleComponent extends VComponent<Props> {
  …
}

Lifecycle Methods

In addition to the required render method, virtual components have several optional lifecycle methods that give the component hooks to setup/cleanup global listeners, do geometry management, and update state. See the API doc for each lifecycle method for details.

Mount

Update

Unmount

Slotting

Component authors declare their expected slots as members of their Props class. The various slot types are exposed as follows:

  • Default slot - exposed through the children member with type VNode[].
  • Ordinary slot - exposed through a member with type Slot whose name corresponds to the slot name.
  • Template slots - exposed through a member with type Slot<SlotContextType> whose name corresponds to the slot name. SlotContextType represents the data type for this template slot (corresponding to the type of the $current object).
  • Dynamic slots - any slots that are not explicitly declared as one of the three previous types will be exposed in a Map of type DynamicSlots. This may be useful in cases where the set of expected slots cannot be statically defined, but is determined by the component through other means at runtime. The dynamic slot map may contain either ordinary slots or template slots.
Note that in all of the cases above, the component author must declare the corresponding properties in their Props class in order to receive access to slot content.

During component rendering, default slot content can simply be inlined as with any other virtual DOM content. Components can test for the existence of the children property to decide whether to render default content.


class Props {
  children?: VNode[];
}

@customElement('oj-sample-component')
export class SampleComponent extends VComponent<Props> {

  protected render(): VComponent.VNode {
    return (
      <div style="border-style: solid; width:200px;">
        { this.props.children || <span>Default Content</span> }
      </div>
    );
  }
}

Ordinary slots are exposed as render functions at runtime and can simply be called to retrieved the corresponding content.


class Props {
  header?: Slot;
}

@customElement('oj-sample-component')
export class SampleComponent extends VComponent<Props> {

  protected render(): VComponent.VNode {
    return (
      <div style="border-style: solid; width:200px;">
        { this.props.header?.() || <span>Default Header Content</span> }
      </div>
    );
  }
}

Template slots are also exposed as render functions at runtime, but additional take an argument representing the template data.


type Item {
  index: number;
  text: string;
}

class Props {
  itemTemplate?: Slot<Item>;
  items?: string[];
}

@customElement('oj-sample-component')
export class SampleComponent extends VComponent<Props> {

  protected render(): VComponent.VNode {
    return (
      const templateFunction = this.itemTemplate || this._defaultTemplate;
      <ul>
        items.map( (item, index) =>
          <li>
            { templateFunction({index: index, text: item}) }
          </li>);
      </ul>
    );
  }
}

Dynamic slots are rendered exactly like ordinary and template slots once the component determines what slot to render.


class Props {
  cards?: DynamicSlots;
  currentCard: string;
}

@customElement('oj-sample-component')
export class SampleComponent extends VComponent<Props> {

  protected render(): VComponent.VNode {
    return (
      <div style="border-style: solid; width:200px;">
        { this.props.cards?.[this.props.currentCard]?.() }
      </div>
    );
 }
}

Performance Considerations

Every time a component's render function is called, everything contained is created anew. As a result, complex properties (e.g. non-primitive values like Object types, event listeners), should be created outside of the render function's scope. Otherwise, e.g. the component would be specifying a different instance of an event listener each time the component is rendered which would result in unnecessary DOM changes. Event listeners should be declared as instance functions marked with the @listener decorator which will ensure that they are property bound. Non-primitive values should be saved in variables outside of the render function.


import { h, VComponent, customElement, listener } from "ojs/ojvcomponent";

class Props {…}

@customElement('oj-sample-collection')
export class SampleCollection extends VComponent<Props> {
  constructor(props: Readonly<Props>) {
    super(props);
  }

  @listener()
  private _handleClick(event) { … }

  protected render(): VComponent.VNode {
    return (
      <div onClick={this._handleClick}/>
    );
  }
}

Constructor

new VComponent(props)

Parameters:
Name Type Description
props Readonly<P> The passed in component properties

Fields

protected props :Readonly<P>

The passed in component properties. This property should not be directly modified e.g. this.props = {} or this.props.someProp = 'foo'.
Default Value:
  • {}

protected state :Readonly<S>

The component state. State updates should be done through the updateState or updateStateFromProps methods and not by direct modification of this property in order to ensure that the component is rerendered.
Default Value:
  • {}

Methods

protected (static) initStateFromProps(props, state) : {Partial<S>|null}

An optional static lifecycle method used to initialize derived state. Called before the render method on the first flow through the lifecycle. Components should return a partial state that will be merged into any state that was initialized in the constructor, or null if no changes are needed.
Parameters:
Name Type Description
props Readonly<P> The component's initial properties
state Readonly<S> The component's initial state
Returns:
Type
Partial<S>|null

protected (static) updateStateFromProps(props, state) : {Partial<S>|null}

An optional static lifecycle method used to update derived state. Called before the render method on update flows through the lifecycle. Components should return either a partial state that will be merged into component state or null if no changes are needed. Logic that relies on old and new state or property values should be done in updated() instead.
Parameters:
Name Type Description
props Readonly<P> The new component properties
state Readonly<S> The new state
Returns:
Type
Partial<S>|null

protected mounted() : {void}

An optional lifecycle method called after the virtual component has been initially rendered and inserted into the DOM. Data fetches and global listeners can be added here. This will not be called for reparenting cases. State and property updates should be done here instead of the constructor.
Returns:
Type
void

protected (abstract) render() : {VComponent.VNode}

Required lifecycle method which returns the component's virtual subtree.
Returns:
Type
VComponent.VNode

protected uniqueId() : {string}

The uniqueId() method can be called to retrieve an id that is unique to the component instance (matching the live DOM if it has been specified) that can be used e.g. as a prefix for IDs on internal DOM. For components needing to generate a unique ID for internal DOM, this utility method will return either the id set on the VComponent by the parent or a unique string that can be used for a prefix for child elements if one wasn't set by the parent. This method can only be called after the VComponent has been instantiated and will return undefined if called from the constructor.
Returns:
Type
string

protected unmounted() : {void}

An optional component lifecycle method called after the virtual component has been removed from the DOM. This will not be called for reparenting cases. Global listener cleanup can be done here.
Returns:
Type
void

protected updated(oldProps, oldState) : {void}

An optional component lifecycle method called after the render method in updating (state or property change) cases. Additional DOM manipulation can be done here. State and property updates that need access to old and values should also be done here. Note that when updating state or property in updated(), the component should compare old and new values.
Parameters:
Name Type Description
oldProps Readonly<P> The previous value of the component properties.
oldState Readonly<S> The previous value of the component state.
Returns:
Type
void

protected updateState(state) : {void}

Updates an internal component state. State updates always trigger an asynchronous rerender.
Note that the method accepts either partial state for the component or a callback that returns a partial state. The callback receives up-to-date component state and property values and can be used to dynamically compute the next state. State updates that rely on state or property values should use the callback form to ensure the latest values are used.
Parameters:
Name Type Description
state ((state: Readonly<S>, props: Readonly<P>) => Partial<S>) | Partial<S> Accepts a partial state object or a callback that returns a partial state object that will be merged into component state. The updater function takes a reference to the component state at the time the change is being applied and the component properties object.
Returns:
Type
void

Type Definitions

Action<Detail extends object = {}>

Signature:

(detail?: Detail) => void

CancelableAction<Detail extends object = {}>

Signature:

(detail?: Detail) => Promise<void>

DynamicSlots<Data = undefined>

Signature:

Record<string, VComponent.Slot<Data>

RenderFunction<P>

Signature:

(props: P, content: VComponent.VNode[]) => VComponent.VNode

Slot<Data = undefined>

Signature:

(data?: Data) => VComponent.VNode[]

VComponentClass<P>

Signature:

new (props: P) => VComponent<P, any>

VNodeType<P>

Signature:

(string|VComponent.VComponentClass.<P>|VComponent.RenderFunction.<P>)

Decorators

@customElement(tagName) : {Function}

Class decorator for VComponent custom elements. Takes the tag name of the custom element.
Parameters:
Name Type Description
tagName string The custom element tag name

@dynamicDefault(defaultGetter) : {Function}

Property decorator for VComponent properties whose default value is determined at runtime and returned via the getter method passed to the decorator.
Parameters:
Name Type Description
defaultGetter function The method to call to retrieve the default value

@event(options) : {Function}

Property decorator for VComponent event callback properties to indicate they bubble.
Parameters:
Name Type Argument Description
options object <optional>
The options for this decorator
Properties
Name Type Description
bubbles boolean True if only the component can update the property

@listener(options) : {Function}

Method decorator for VComponent that binds a specified method to the component instance ('this') and passes provided options to the addEventListener()/removeEventListener() calls, when the method is used as a listener.
Parameters:
Name Type Argument Description
options {capture?: boolean = false, passive?: boolean} <optional>
Listener options, e.g. {passive:true}

@method() : {Function}

Method decorator for VComponent methods that should be exposed on the custom element. Non decorated VComponent methods will not be made available on the custom element.

@rootProperty() : {Function}

Property decorator for VComponent properties which are not component properties, but are global properties that the VComponent wishes to get updates for, e.g. tabIndex or aria-label.