Module: ojvcomponent

Oracle® JavaScript Extension Toolkit (JET)
15.1.0

F83698-01

QuickNav

JET Modules

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

Description

The VComponent API is a mechanism for creating virtual DOM-based Web Components. VComponents are authored in TypeScript as either Preact class-based components or function-based components. Class-based components specify their custom element tag via the @customElement decorator:

import { Component, ComponentChild } from 'preact';
import { customElement, GlobalProps } from 'ojs/ojvcomponent';

@customElement('oj-hello-world')
export class HelloWorld extends Component<GlobalProps> {
  render(): ComponentChild {
    return <div>Hello, World!</div>;
  }
}
Function-based components register their custom element tag with the VComponent framework via the registerCustomElement function:
import { registerCustomElement } from 'ojs/ojvcomponent';

export const HelloWorld = registerCustomElement(
  'oj-hello-world',
  () => {
    return <div>Hello, World!</div>;
  }
);

In order to prepare the component for use, the VComponent must be run through the ojet CLI build process. Running ojet build will do the following:

  • Inject necessary information into the module to enable Web Component usage.
  • Generate a type definition that includes Web Component type info.
  • Generate a component.json metadata file for enabling integration with Oracle Visual Builder

Once the VComponent has been built, it can either be consumed as a plain old Web Component in HTML content, for example:

<!-- This is HTML: -->
<oj-hello-world></oj-hello-world>

Or via the Preact component class in JSX:

// This is JSX:
const hw =  <HelloWorld />

These both produce the same DOM content. That is, in both cases, an <oj-hello-world> custom element is created and added to the DOM. In the case where the VComponent is referenced via its Preact component class, this custom element is automatically created and wrapped around the rendered content (the <div> in the above example) by the VComponent framework.

Creating VComponents

VComponents can be created either by hand or via the ojet CLI's "create component" command, for example:

$ ojet create component oj-hello-world --vcomponent

When running the ojet create component commmand, the custom element tag name is specified as an argument, and the --vcomponent flag indicates that a VComponent (as opposed to a Composite Component) should be created.

(Note: if the application was originally created using the --vdom flag, the ojet create component command will default to creating VComponents and the --vcomponent flag can be omitted.

The ojet create component command creates some supporting artifacts, including:

  • A loader module
  • A style sheet and SASS files for theming
  • A resource module for translated strings

In addition, ojet will ensure that the path to the VComponent is properly configured in the application's tsconfig.json

Properties

VComponents/Preact components declare their supported properties via a type parameter. For example, as we saw above:

export class HelloWorld extends Component<GlobalProps> {

With this declaration, the component indicates that it supports the properties specified via the GlobalProps type. This type includes a subset of the global HTML attributes which represent the minimally required set of properties that all VComponents must support.

VComponents can, of course, expose other non-global, component-specific properties as well. This is typically done by declaring a type alias:

type Props = {
  greeting?: string;
  name?: string;
}

And associating this type with the first type parameter in the Component class declaration.

Since VComponents are minimally required to support the global HTML attributes defined by GlobalProps, the component-specific props must be combined with GlobalProps. The VComponent API includes a utility type to help with this: ExtendGlobalProps. Using ExtendGlobalProps, a component with the above Props type (including some default values) ends up looking like:

import { Component, ComponentChild } from 'preact';
import { customElement, ExtendGlobalProps } from 'ojs/ojvcomponent';

type Props = {
  greeting?: string;
  name?: string;
}

@customElement('oj-hello-world-with-props')
export class HelloWorld extends Component<ExtendGlobalProps<Props>> {
  render(props: Readonly<Props>): ComponentChild {
    const { greeting, name } = props;

    return <div>{ greeting }, { name }!</div>;
  }

  static defaultProps: Props = {
    greeting: "Hello",
    name: "World"
  };
}

In Preact, properties can be accessed either through this.props or via the first argument of the render method.

Properties can be set on the component in various ways, including:

  1. As attributes in HTML markup
  2. As properties in JSX
  3. As properties on the DOM element

One note on naming conventions: when referencing properties within JSX or on a DOM element (#2 and #3), the property name as specified in the component type is always used.

However, for attributes in HTML markup (#1), JET uses a different naming convention for multi-word attributes. As discussed in Property-to-Attribute Mapping, JET converts camelCased property names into hyphen-separated, kebab-case attribute names. As such, given the following property:

type Props = {
    preferredGreeting?: string;
}

The attribute name "preferred-greeting" is used in HTML markup:

<!-- This is HTML -->
<oj-hello-world preferred-greeting="Hi"></oj-hello-world>

And the unmodified property name is used everywhere else:

// This is JSX
function Parent() {
  return <HelloWorld preferredGreeting="Hi" />
}

// This is also JSX
function ParentOfCustomElement() {
  return <oj-hello-world preferredGreeting="Hi" />
}

// This is plain old DOM
const helloWorld = document.createElement("oj-hello-world");
helloWorld.preferredGreeting = "Hi";

Children and Slots

Many Web Components allow children to be specified, either as direct children or via named slots. A VComponent indicates that it takes arbitrary (non-named) children by declaring a "children" property using Preact's ComponentChildren type. Named slots are declared in a similar fashion, using the VComponent-specific Slot type:

import { Component, ComponentChildren } from 'preact';
import { customElement, ExtendGlobalProps, Slot } from 'ojs/ojvcomponent';

type Props = {
  // This indicates that the VComponent accepts arbitrary
  // (non-slot) children:
  children?: ComponentChildren;

  // And this indicates that the VComponent accepts a
  // slot named "start"
  start?: Slot;
}

Both children and slots can be embedded directly in a virtual DOM tree:

  render(props: Readonly<Props>): ComponentChild {
    return (
      <div>
        // Place the start slot before our greeting
        { props.start }

        Hello, World!

        <div>
          // And dump any other children in a wrapper div
          { props.children }
        </div>
      </div>
    );
  }

In cases where the VComponent needs to inspect children or slot content, these values must first be normalized to a flattened array by calling Preact's toChildArray.

When consuming the VComponent as a custom element, slots are specified using the slot attribute:

      <!-- This is HTML -->
      <oj-hello-world-with-children>
        <!-- This is the start slot content: -->
        <oj-avatar slot="start" initials="HW"></oj-avatar>

        <!-- This is other child content: -->
        <span>Child content</span>
      </oj-hello-world-with-children>

However, when referencing the VComponent as a Preact component, slots are configured as plain old component properties:

function Parent() {
  return (
    <HelloWorldWithChildren start={ <oj-avatar initials="OJ" /> }>
      <span>Child content</span>
    </HelloWorldWithChildren>
  )
}

In some scenarios, JET application developers may require runtime access to a BusyContext scoped to a VComponent's children or to a particular named slot's contents. For example, application developers may require a mechanism for waiting until all of a slot's component contents have been created and initialized before programmatically interacting with the contents. For these scenarios, the VComponent API provides a ImplicitBusyContext marker type that can be included via intersection with the ComponentChildren or Slot property declaration.

Template Slots

As with other JET components, VComponents can expose template slots. Template slots differ from plain old slots in that they are invoked with some context. Template slots are most commonly used within iterating "collection" components, which need to stamp out some bit of content corresponding to each value/row/item in a data set.

Like other slots, template slots are declared as properties. Rather than using the Slot type, the TemplateSlot type is used:

import { Component } from "preact";
import { customElement, ExtendGlobalProps, TemplateSlot } from "ojs/ojvcomponent";

type Props = {
  // This indicates that the VComponent exposes a template
  // slot named "itemTemplate":
  itemTemplate?: TemplateSlot<ItemContext>;
}

// This is the type for the context that we'll
// pass into the itemTemplate slot
type ItemContext = {
  index?: number;
  label?: string;
  value?: string;
}

TemplateSlot is a function type that takes a single parameter: the context that is passed in when the template slot is rendered. The VComponent invokes the template slot function with this context and embeds the results in the virtual DOM tree:

// Invoke the template slot and embed the results
// in a list item:
<li> {
  props.itemTemplate?.({
    index: currentIndex,
    label: currentLabel,
    value: currentValue
  })
}
</li>

Note that while component consumers specify the contents for a template slot using a <template> element and standard HTML markup, the component implementation sees template slots as functions that return virtual DOM nodes. This transformation between live DOM and virtual DOM is performed through the use of a CSPExpressionEvaluator instance. This has two consequences:

  • expressions used within VComponent template slots are subject to the syntax limitations documented by CSPExpressionEvaluator;
  • globally-scoped variables are not available within expressions unless the application has registered a CSPExpressionEvaluator with a global scope that exposes these variables.

Actions and Events

As with other types of Web Components, VComponents can be the source of events, typically fired in response to some user interaction. VComponents specify their event contracts by declaring properties of the form "on<PascalCaseEventName>" of type Action. For example, the following declaration indicates that the component fires a "greetingComplete" event:

import { customElement, ExtendGlobalProps, Action } from 'ojs/ojvcomponent';

type Props = {
  onGreetingComplete?: Action;

  // Other component props...
}

Action is a function type that optionally takes an argument representing the event detail payload. For events that include a detail payload, the detail type is specified via a type parameter:

type Detail = {
  status: "success" | "failure";
}

type Props = {
  onGreetingComplete?: Action<Detail>;
}

The VComponent triggers the event by invoking the Action property and providing the detail payload:

  this.props.onGreetingComplete?.({ status: "success"});

When used in custom element form, this dispatches an actual DOM CustomEvent. See the Events and Listeners topic for details on how to respond to these events.

By default, events that are dispatched by the VComponent framework do not bubble. See the Bubbles type for info on how to declare bubbling events.

When consumed as a Preact component, no DOM events are created or dispatched. Instead, the Action callback is simply invoked directly. There is no automatic event bubbling-like behavior in this case.

The VComponent API also supports a contract for actions/events that can be vetoed by the consumer. See the CancelableAction type for details.

Methods

Some Web Components need to expose non-standard, component specific methods on their custom elements. For example, the <oj-popup> custom element exposes open, isOpen and close methods.

While it is preferable to favor declarative, property-driven APIs over imperative, method-centric contracts, the VComponent API does allow components to expose methods on their custom elements.

By default, no methods defined on a VComponent class are surfaced on the custom element. To indicate that a VComponent class method should be included in the custom element's public API, simply mark the method with the @method decorator.

Function-based VComponents that wish to expose public methods must wrap their Preact functional component implementation in a call to forwardRef at the time of registration with the VComponent framework, and leverage the useImperativeHandle hook within their Preact implementation. See the registerCustomElement function for further details.

Writeback Properties

JET-based Web Components support a mechanism by which components can trigger changes to their own properties. For example, the JET input and select components notify consumers of changes to the value property as the user enters new values. When combined with two-way binding, this can be used to update values referenced via JET binding expressions. This process is known as "writeback". See the Writeback section in the JET Web Components topic for background.

VComponents declare writeback properties by pairing a normal property declaration with a companion callback property that is invoked when the component wants to trigger a writeback. For example, this plain old (non-writeback) value property:

type Props = {

  // This is a plain old (non-writeback) value property:
  value?: string;
}

Can be converted into a writeback property by adding a second property named "onValueChanged":

import { customElement, ExtendGlobalProps, PropertyChanged } from "ojs/ojvcomponent";

type Props = {
  value?: string;

  // The presence of this callback promotes "value" into a
  // writeback property
  onValueChanged?: PropertyChanged<string>;
}

Both the event name and type are significant. In order to be recognizable as a writeback property, the companion callback property must follow the naming convention "on<PropertyName>Changed" and must be of type PropertyChanged.

Similar to Actions, invoking a PropertyChanged callback has different implications depending on whether the VComponent is being consumed as a custom element or as a Preact component.

When the VComponent is used in its custom element form, invoking the PropertyChanged callback results in an actual DOM propertyChanged event being created and dispatched. This allows JET's two-way binding mechanism to kick in. If the property is configured with a two-way binding, the new value will be written back into the expression.

In addition, when used as a custom element, triggering a writeback automatically queues a render of the VComponent, allowing the VComponent to re-render with the new value.

When the VComponent is used via its Preact component class, no DOM event is created or dispatched. Instead, the PropertyChanged callback is simply invoked with the new value. The parent component is then responsible for deciding whether re-render with the new value or not.

The VComponent API also supports writeback properties which can be read/observed by the consumer, but are only ever written by the component itself. These are known as read-only writeback properties. See the ReadOnlyPropertyChanged type for info on how to configure these properties.

Observed Global Properties

As discussed above, all VComponents minimally support the set of global HTML attributes defined by the GlobalProps/ExtendGlobalProps types. This means that when consuming a VComponent either via its custom element tag or VComponent class, global attributes (e.g., id, tabIndex, title, etc...) can be specified:

function Parent() {
  // We can pass GlobalProps like id into any VComponent:
  return <HelloWorld id="hw" />
}

The VComponent framework automatically transfers any global properties through to the underlying custom element in the DOM.

In some cases, the VComponent implementation may need to inspect the values of these global properties. In addition, VComponents may need to respond by re-rendering themselves when a global property is modified on the custom element. In such cases, VComponents can express interest in specific global properties via the ObservedGlobalProps utility type. This type allows specific global properties to be selected for observation via a type parameter. This type is combined with the component's other properties as part of the property declaration.

The following property declaration indicates that the component exposes "greeting" and "name" properties and also observes the global "id" and "tabIndex" props:

import { customElement, ExtendGlobalProps, ObservedGlobalProps } from 'ojs/ojvcomponent';

type Props = {
  greeting?: string;
  name?: string;
} & ObservedGlobalProps<'id' | 'tabIndex'>

Any props that are specified via ObservedGlobalProps are automatically included in the custom element's observed attributes set. As a result, any mutations to the one of these attributes on the custom element will automatically trigger a re-render of the VComponent with the new values.

Global attributes referenced with the ObservedGlobalProps utility type do not appear in the VComponent's generated API Doc. If any observed global properties require context-specific documentation in the generated API Doc, then an alternate syntax is also supported: simply include the global prop in the VComponent's Props definition, specifying as its declaration a GlobalProps indexed access type reference to the same global prop. Note that the following variation of the previous example is functionally equivalent:

import { customElement, ExtendGlobalProps, GlobalProps, ObservedGlobalProps } from 'ojs/ojvcomponent';

type Props = {
  greeting?: string;
  name?: string;

  /**
   * Specifies this custom element's focusable behavior during sequential keyboard navigation.
   * A value of -1 indicates that this component is not reachable through keyboard navigation.
   */
  tabIndex?: GlobalProps['tabIndex'];  // include 'tabIndex' in the observed attribute set AND the API Doc

} & ObservedGlobalProps<'id'>    // include 'id' in the observed attribute set

Root Element

In all of the VComponents that we have seen so far, the root custom element is not included in the rendered output. Instead, this element is implicitly injected into the DOM by the VComonent framework.

In some rare cases, it may be necessary to have more control over how the the root element is rendered.

For example, consider this case of a VComponent that renders a link:

import { customElement, ExtendGlobalProps, ObservedGlobalProps } from "ojs/ojvcomponent";
import { Component, ComponentChild } from "preact";

type Props = {
  href?: string;
} & ObservedGlobalProps<'tabIndex'>;

@customElement("oj-demo-link")
export class OjDemoLink extends Component<ExtendGlobalProps<Props>> {

  render(props: Props): ComponentChild {
    return <a href={ props.href } tabIndex={ props.tabIndex }>Hello, World</a>;
  }
}

The intent is that the value of the global tabIndex attribute will be transferred from the root element down to the link.

However, since the tabIndex value will be automatically rendered on the root custom element, we end up with the tabIndex on two elements: on the root <oj-demo-link> and on the <a>.

To address this, we can update the render method to render both the link *and* the root custom element. The VComponent API includes a simple component that acts as a placeholder for the root element, named "Root". The Root component is exported from the "ojs/ojvcomponent" module, so we add this in our import list:

import { customElement, ExtendGlobalProps, ObservedGlobalProps, Root } from "ojs/ojvcomponent";

And then we can include the Root component in the virtual DOM tree, adjusting exactly which properties are rendered:

  render(props: Props): ComponentChild {
    return (
      // Suppress the tabIndex on the root custom element since
      // we are transferring this to the link
      <Root tabIndex={ -1 }>

        // Render the tabIndex here:
        <a href={ props.href } tabIndex={ props.tabIndex }>Hello, World</a>
      </Root>
    );
  }

The presence of the Root component impacts how global properties are managed. When the Root component is omitted, all global properties, both observed and non-observed, are automatically passed through to the root custom element. When the Root component is included at the root of the rendered virtual DOM tree, non-observed global properties are still passed through to the root custom element. However only those observed global properties that are explicitly rendered on the Root component will be passed through.

Functions

getUniqueId : {string}

For the most part, VComponents should not need to render ids on child content. However, in some cases this may be necessary. For example, in order to set up a relationship between a label and the element that the label references, the label and labeled content must rendezvous on a common id. Specifying fixed ids is problematic as this can lead to conflicts with other ids on the page. The getUniqueId() method helps solve this problem by creating producing an id that is guaranteed not to conflict with ids rendered by other components.

The id returned by getUniqueId() is typically used to provide a prefix (or suffix) for what would otherwise be a static id for some element rendered by the VComponent.

The usage model is:

  1. In the VComponent's constructor, check to see whether the VComponent already has a props.id value. If so, this can be used as a prefix for other ids and calling getUniqueId() is unnecessary.
  2. Otherwise, call getUniqueId() to retrieve the unique prefix for this component
  3. Store the result of the #1/#2 in an instance variable for later use.
  4. When rendering, use the previously stored prefix to generate unique ids for any elements that need them.
  5. Don't forget to include "id" in the list of ObservedGlobalProps in order to ensure that the VComponent receives the value of this global HTML attribute.

Putting this all together, we end up with a component like this:

import { Component, ComponentChild } from 'preact';
import { customElement, ExtendGlobalProps, ObservedGlobalProps, getUniqueId } from 'ojs/ojvcomponent';
import "ojs/ojinputtext";
import "ojs/ojlabel";

export type Props = ObservedGlobalProps<'id'>;

@customElement('oj-demo-unique-id')
export class DemoUniqueId extends Component<ExtendGlobalProps<Props>> {

  private uniquePrefix: string;

  constructor(props: Readonly<Props>) {
    super(props)

    this.uniquePrefix = props.id ?? getUniqueId();
  }

  render(): ComponentChild {

    const inputTextId = `${this.uniquePrefix}_input`;

    return (
      <div>
        <oj-label for={ inputTextId }>Label</oj-label>
        <oj-input-text id={ inputTextId } value="Value"/>
      </div>
    );
  }
}
Returns:
Type
string

registerCustomElement<P, M extends Record<string, (...args) => any> = {}>(tagName, functionalComponent, options) : {VComponent}

Class-based VComponents use the @customElement decorator to specify the VComponent's custom element tag name (also known as its full name) and to register the custom element with the JET framework. However, function-based VComponents cannot utilize this approach because decorators are only supported for classes and their constituent fields.

JET provides an alternate mechanism for registering a functional VComponent and specifying its custom element tag name. The registerCustomElement method accepts three arguments: the custom element tag name to be associated with the VComponent, a reference to the Preact functional component that supplies the VComponent implementation, and a reference to additional options that can be specified when registering the functional VComponent (see Options for futher details). It returns a higher-order VComponent that is registered with the framework using the specified custom element tag name.

Here is a simple example:


import { registerCustomElement } from 'ojs/ojvcomponent';

export type Props = Readonly<{ message?: string; }>;

export const DemoFunctionalVComp = registerCustomElement(
  'oj-demo-functional-vcomp',
  ({ message='This is a functional VComponent!' }: Props) => {
    return <div>{message}</div>;
  }
);

There are some other considerations to keep in mind when implementing functional VComponents:

  • Function-based VComponents will typically use an anonymous function to implement their Preact functional component, and expose the returned higher-order VComponent as their public API.
  • The registration call ensures that the returned higher-order VComponent extends the Preact functional component's custom properties with the required global HTML attributes defined by GlobalProps.
  • Default custom property values are specified using destructuring assignment syntax in the function implementation.
  • TypeScript can typically infer the type parameters to the registerCustomElement call without having to explicitly specify them in your code. However, if the function-based VComponent exposes public methods then the second M type parameter (which maps public method names to their function signatures) must be provided. In addition:
    1. the Preact functional component that implements VComponent must be wrapped inline with a forwardRef call, and
    2. the useImperativeHandle hook must be used within the Preact implementation.
    See the Options type for a detailed example.

Parameters:
Name Type Argument Description
tagName string The custom element tag name for the registered functional VComponent.
functionalComponent function The Preact functional component that supplies the VComponent implementation.
options VComponent.Options.<P, M> <optional>
Additional options for the functional VComponent.
Returns:

Higher-order VComponent that wraps the Preact functional component.

Type
VComponent

Root

Root is a Preact component that can be used to wrap the VComponent's child content. This component should only be used for cases where the VComponent needs to control how observed global properties are rendered on the component's root custom element. In all other cases the non-wrapped child content should be returned directly.

See the Root Element section for more details.

Decorators

consumedContexts(contexts)

Class decorator for VComponent custom elements that specifies a list of Preact Contexts whose values should be made available to the inner virtual dom tree of the VComponent when rendered as an intrinsic element. This allows the inner virtual dom tree to have access to the Context values from the parent component when rendered either directly as part of the parent component's virtual dom tree or when rendered as template slot content in a parent VComponent. Note that any intrinsic elements within the inner virtual dom tree must also specify a list of Contexts to further propagate their values.
Parameters:
Name Type Description
contexts Array.<Context.<any>> The list of Preact Contexts

customElement(tagName)

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

method

Method decorator for VComponent class methods that should be exposed on the custom element as part of its public API. Non-decorated VComponent class methods will not be made available on the custom element.

Type Definitions

Action<Detail extends object = {}>

The Action type is used to identify properties as action callbacks. Actions are functions that the VComponent invokes when it wants to communicate some activity to the outside world. When the VComponent is being consumed as a custom element, this results in an actual DOM CustomEvent being dispatched. Alternatively, when the VComponent is referenced via its Preact Component class, the provided callback is invoked directly and no CustomEvent is produced.

Actions have an optional detail type. If specified, the detail value is either passed to the consumer via the CustomEvent detail payload for the custom element case, or directly into the callback for the Preact component case.

Note that Action properties must adhere to a specific naming convention: "on<PascalCaseEventName>". For the custom element case, the type of the CustomEvent will be derived by converting the first character of the event name to lower case. Thus, the "onGreetingComplete" property will generate a CustomEvent of type "greetingComplete".

See Actions and Events for more info.

Signature:

(detail?: Detail) => void

Bubbles

As discussed in Actions and Events, the custom events generated by Actions do not bubble by default. The Bubbles marker type can be combined with the Action type to indicate that the Action's custom events should bubble.

type Props = {
  // This action generates bubbling custom events
  onThisActionBubbles?: Action & Bubbles;

  // This action generates non-bubbling custom events
  onThisActionDoesNotBubble?: Action;
}

CancelableAction<Detail extends object = {}>

Some JET Web Components support an asynchronous, event-based cancelation contract where prior to performing some operation, the component dispatches a "cancelable" event. If the application cancels the event, the operation is not performed. The <oj-file-picker>'s ojBeforeSelect event is one example of such an API.

The VComponent API has built-in support for this pattern via the CancelableAction type. Like the plain old Action type, CancelableAction is a function type that is used for defining callback-centric properties. One key difference between these types is that CancelableAction returns a Promise. If the Promise resolves successfully, the action is considered to be accepted. If the Promise is rejected, the action is canceled.

As with Action-typed properties, CancelableActions exhibit different behavior depending on whether the VComponent is being consumed as a custom element or via its Preact Component class.

When consumed as a custom element, invoking a CancelableAction results in a CustomEvent being created and dispatched. The detail payload of this custom event is augmented with one extra field: an "accept" method. The accept method takes a single argument: the Promise that produces the cancelation result.

When consumed via the Preact Component class, no custom event is dispatched. Instead, the callback returns the cancelation promise directly.

Signature:

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

Contexts

The Contexts type allows a functional VComponent to specify a list of Preact Contexts whose values should be made available to the inner virtual dom tree of the VComponent when rendered as an intrinsic element. This allows the inner virtual dom tree to have access to the Context values from the parent component when rendered either directly as part of the parent component's virtual dom tree or when rendered as template slot content in a parent VComponent. Note that any intrinsic elements within the inner virtual dom tree must also specify a list of Contexts to further propagate their values.

Signature:

{ consume?: Array<Context<any>> }

DynamicSlots

In most cases when a Web Component accepts slot content, the number and names of the slots are known, as these are defined by the component's public API. However, in some cases components may allow an arbitrary number of slots to be specified, where the slot names are not known up front. The <oj-switcher> component is an example of a component that accepts a dynamically defined (rather than predefined) set of slots.

The VComponent API supports such cases via the DynamicSlots and DynamicTemplateSlots types. A single property can be marked with the DynamicSlots type:

type Props = {

  // This property will be populated with all
  // "unknown" slots.
  items?: DynamicSlots;

  // Other properties go here...
}

When the VComponent is consumed in custom element form, this property will be populated with entries for each "dynamic" slot. That is, an entry will be added for each child element with a slot attribute that does not correspond to a known Slot-typed property. The property acts as a map from slot name to Slot instance. The VComponent can use whatever heuristic it prefers to decide which (if any) slots should be included in the rendered output.

Signature:

Record<string, VComponent.Slot>

DynamicTemplateSlots<Data>

The DynamicTemplateSlots type is a companion to DynamicSlots that is used in cases where the component accepts an arbitrary number of template slots. VComponents may declare a single property of type DynamicTemplateSlots. When the component is used as a custom element, this property will be populated with one entry for each "dynamic" template slot, where the key is the slot name and the value is a TemplateSlot function.

Note that each VComponent class can only contain a single dynamic slot property. That is, each VComponent can have one property of type DynamicSlots or one property of type DynamicTemplateSlots, but not both.

Signature:

Record<string, VComponent.TemplateSlot<Data>>

ElementReadOnly<T>

By default, writeback property mutations can be driven either by the component, typically in response to some user interaction, or by the consumer of the component. In some cases, writeback properties are exclusively mutated by the component itself. Writeback properties that cannot be mutated by the consumer are known as read-only writeback properties. The <oj-input-text>'s rawValue property is an example of such a property.

Read-only writeback properties are declared in a similar manner to plain old writeback properties, with one important difference: the ElementReadOnly utility type is used as a marker to identify the that the property is read-only.

Declarations for both forms of writeback properties can be seen below:

type Props = {

  // This is a normal writeback property:
  value?: string;
  onValueChanged?: PropertyChanged<string>

  // This is a read-only writeback property:
  rawValue?: ElementReadOnly<string>;
  onRawValueChanged?: PropertyChanged<string>
}
Signature:

T

Deprecated:
Since Description
12.0.0 Use the ReadOnlyPropertyChanged type instead.

ExtendGlobalProps<Props>

As discussed in the Properties section, all VComponents must minimally include the GlobalProps in their property types. ExtendGlobalProps is a convenience type for combining component-specific properties with GlobalProps, e.g.:

import { customElement, ExtendGlobalProps } from 'ojs/ojvcomponent';

// These are the component-specific props:
type Props = {
  greeting?: string;
  name?: string;
}

// Below we merge the component props with the
// global props using ExtendGlobalProps
@customElement('oj-hello-world-with-props')
export class HelloWorld extends Component<ExtendGlobalProps<Props>> {
Signature:

Readonly<Props> & ojvcomponent.GlobalProps

GlobalProps

The GlobalProps type defines the set of global properties that are supported by all VComponents. This includes three categories of properties:

  1. Global HTML attributes
  2. ARIA attributes
  3. Global event listeners

The following properties are included from category 1:

  • accessKey
  • autocapitalize
  • autofocus
  • class
  • contentEditable
  • dir
  • draggable
  • enterKeyHint
  • hidden
  • id
  • inputMode
  • lang
  • role
  • slot
  • spellcheck
  • style
  • tabIndex
  • title
  • translate

The following ARIA-specific attributes are included from category 2:

  • aria-activedescendant
  • aria-atomic
  • aria-autocomplete
  • aria-busy
  • aria-checked
  • aria-colcount
  • aria-colindex
  • aria-colspan
  • aria-controls
  • aria-current
  • aria-describedby
  • aria-details
  • aria-disabled
  • aria-errormessage
  • aria-expanded
  • aria-flowto
  • aria-haspopup
  • aria-hidden
  • aria-invalid
  • aria-keyshortcuts
  • aria-label
  • aria-labelledby
  • aria-level
  • aria-live
  • aria-modal
  • aria-multiline
  • aria-multiselectable
  • aria-orientation
  • aria-owns
  • aria-placeholder
  • aria-posinset
  • aria-pressed
  • aria-readonly
  • aria-relevant
  • aria-required
  • aria-roledescription
  • aria-rowcount
  • aria-rowindex
  • aria-rowspan
  • aria-selected
  • aria-setsize
  • aria-sort
  • aria-valuemax
  • aria-valuemin
  • aria-valuenow
  • aria-valuetext

The following event listener properties are included from category 3:

  • onBlur
  • onClick
  • onContextMenu
  • onDblClick
  • onDrag
  • onDragEnd
  • onDragEnter
  • onDragExit
  • onDragLeave
  • onDragOver
  • onDragStart
  • onDrop
  • onFocus
  • onKeyDown
  • onKeyPress
  • onKeyUp
  • onMouseDown
  • onMouseEnter
  • onMouseLeave
  • onMouseMove
  • onMouseOut
  • onMouseOver
  • onMouseUp
  • onPointerOver
  • onPointerEnter
  • onPointerDown
  • onPointerMove
  • onPointerUp
  • onPointerCancel
  • onPointerOut
  • onPointerLeave
  • onTouchCancel
  • onTouchEnd
  • onTouchMove
  • onTouchStart
  • onWheel

The above event listener properties can also be specified with the "Capture" suffix (e.g., "onClickCapture") to indicate that the listener should be registered as a capture listener.

Finally, onfocusin and onfocusout properties are also available, though technically speaking these are not global events.

ImplicitBusyContext

As discussed in Children and Slots, JET application developers may require a mechanism for waiting at runtime until all of a slot's component contents have been created and initialized before programmatically interacting with the contents. VComponent developers can request that a BusyContext instance, scoped to a VComponent's children or to a particular named slot's contents, be created at runtime by using the ImplicitBusyContext marker type. This marker type can be combined with Preact's ComponentChildren type or with the Slot type as needed:

import { Component, ComponentChildren } from 'preact';
import { customElement, ExtendGlobalProps, ImplicitBusyContext, Slot } from 'ojs/ojvcomponent';

type Props = {
  // This indicates that the VComponent accepts arbitrary (non-slot) children,
  // and that a BusyContext scoped to this child content should be created at runtime
  children?: ComponentChildren & ImplicitBusyContext;

  // This indicates that the VComponent accepts a slot named "end",
  // and that a BusyContext scoped to the slot's content should be created at runtime
  end?: Slot & ImplicitBusyContext;
}

Methods<M>

The Methods type specifies optional design-time method metadata that can be passed in the options argument when calling registerCustomElement to register a functional VComponent that exposes custom element methods.

The Methods type makes several adjustments to the MetadataTypes.ComponentMetadataMethods type:

  • The internalName property does not apply to VComponents.
  • The return type and parameter types are explicitly omitted from MetadataTypes.ComponentMetadataMethods and MetadataTypes.MethodParam respectively, as these should come from the function signatures passed as a type parameter to the registerCustomElement call.
  • Optional apidocDescription and apidocRtnDescription properties are added to specify markup text for inclusion in the generated API Doc describing the method and its return value, respectively. If apidocDescription is unspecified, then the description property is used in the API Doc.

Signature:

{Partial<Record<keyof M, Omit<Metadata.ComponentMetadataMethods, 'internalName' | 'params' | 'return'> & { params?: Array<Omit<Metadata.MethodParam, 'type'>>; apidocDescription?: string; apidocRtnDescription?: string; }>>}

ObservedGlobalProps

The ObservedGlobalProps type is used to identify the subset of GlobalProps that the VComponent implementation needs to observe. When a VComponent is used as a custom element, ObservedGlobalProps determines which of the GlobalProps values will be extracted from the DOM and passed into the VComponent. Properties that are selected using ObservedGlobalProps are also included in the custom element's observedAttributes list. As a result, updates to observed global properties will trigger the VComponent to render with the new values.

The ObservedGlobalProps type acts as a Pick type, where properties are implicitly picked from GlobalProps. The resulting type is typically merged with any component-specific properties via a union.

See the Observed Global Properties section for more details.

Options<P, M extends Record<string, (...args) => any> = {}>

The Options type specifies additional options that can be passed when calling registerCustomElement to register a functional VComponent with the JET framework.

These additional options come into play under certain circumstances:

  • Optional bindings metadata (see PropertyBindings for further details) are only honored when the VComponent custom element is used in a Knockout binding environment.
  • Optional contexts metadata (see Contexts for further details) are only honored when the VComponent is rendered as an intrinsic element in a virtual dom tree.
  • Optional methods metadata (see Methods for further details) are only honored if a type parameter mapping public method names to their function signatures is specified in the registerCustomElement call, and if the Preact functional component implementation is wrapped in a call to forwardRef.

In the following example:

  • FormFunctionalComponent is a VComponent implementing a form that consumes 'labelEdge' and 'readonly' properties from its parent container. It also provides values for 'labelEdge' and 'readonly' properties to its children with any necessary transformations.
  • The implementation includes an input element, and the parent function-based VComponent exposes a public focusInitialInput method to set the focus on this element as needed.


import { h, Ref } from 'preact';
import { useImperativeHandle, useRef } from 'preact/hooks';
import { forwardRef } from 'preact/compat';
import { registerCustomElement } from 'ojs/ojvcomponent';

type Props = Readonly<{ labeledge?: 'inside' | 'start' 'top'; readonly?: boolean; }>;

type FormHandle = {
  focusInitialInput: () => void;
};

export const FormFunctionalComponent =
  registerCustomElement<Props, FormHandle>(
    'my-form-functional-component',
    forwardRef(
      ({ labelEdge = 'inside', readonly = false }: Props, ref: Ref<FormHandle>) => {
        const formInputRef = useRef<HTMLInputElement>(null);

        useImperativeHandle(ref, () => ({
          focusInitialInput: () => formInputRef.current?.focus()
        }));

        return (
          <input
            ref={formInputRef}
            readOnly={readonly}
            ...
          />
          ...
        );
      }
    ),
    {
      bindings: {
        // Indicate that the component's 'labelEdge' property will consume
        // the 'containerLabelEdge' variable provided by its parent, as well as
        // provide the 'labelEdge' property value under different keys and with
        // different transforms as required for different consumers.
        labelEdge: {
          consume: { name: 'containerLabelEdge' },
          provide: [
            { name: 'containerLabelEdge', default: 'inside' },
            { name: 'labelEdge', default: 'inside', transform: { top: 'provided', start: 'provided' } }
          ]
        },
        // Indicate that the component's 'readonly' property will consume
        // the 'containerReadonly' variable provided by its parent, as well as
        // provide the 'readonly' property value under different keys for different
        // consumers.
        readonly: {
          consume: { name: 'containerReadonly' },
          provide: [
            { name: 'containerReadonly' },
            { name: 'readonly' }
          ]
        }
      },
      methods: {
        focusInitialInput: {
          description: 'Sets the focus on this form.',
          apidocDescription: 'Sets the focus on the initial &lt;code&gt;FormInput&lt;/code&gt; control in this form.'
        }
      }
    }
  );

Signature:

{ bindings?: VComponent.PropertyBindings<P>, contexts?: VComponent.Contexts, methods?: VComponent.Methods<M> }

PropertyBindings<P>

The PropertyBindings type maps functional VComponent property names to their corresponding PropertyBinding metadata.

Signature:

Partial<Record<keyof P, MetadataTypes.PropertyBinding>>

PropertyChanged<T>

The PropertyChanged type is used to identify callback properties that notify VComponent consumers of writeback property mutations. Writeback property callbacks must adhere to the naming convention of "on<PropertyName>Changed", where "PropertyName" is the name of the writeback property with the first character converted to upper case:

type Props = {
  // This is a writeback property
  value?: string;

  // This is the corresponding property changed callback
  onValueChanged?: PropertyChanged<string>;
}

See the Writeback Properties section for more details.

Signature:

(value: T) => void

ReadOnlyPropertyChanged

By default, writeback property mutations can be driven either by the component, typically in response to some user interaction, or by the consumer of the component. In some cases, writeback properties are exclusively mutated by the component itself. Writeback properties that cannot be mutated by the consumer are known as read-only writeback properties. The <oj-input-text>'s rawValue property is an example of such a property.

The ReadOnlyPropertyChanged type is used to identify callback properties that notify VComponent consumers of read-only writeback property mutations. Read-only writeback property callbacks must adhere to the naming convention of "on<PropertyName>Changed", where "PropertyName" is the name of the writeback property with the first character converted to upper case.

Note that, unlike normal writeback properties that are declared by pairing a normal property declaration with a companion callback property, a read-only writeback property is declared solely by its callback property.

Declarations for both forms of writeback properties can be seen below:

type Props = {

  // The following two fields establish a writeback property
  // named 'value'
  value?: string;
  onValueChanged?: PropertyChanged<string>

  // The following field establishes a read-only writeback property
  // named 'rawValue'
  onRawValueChanged?: ReadOnlyPropertyChanged<string>
}

Slot

The Slot type identifies properties as representing named slot children. This type is an alias for Preact's ComponentChildren type. As such, the value of a slot property can either be embedded directly in a virtual DOM tree or can be passed to Preact's toChildArray.

See Children and Slots for more details.

Signature:

ComponentChildren

TemplateSlot<Data extends object>

The TemplateSlot type identifies properties as representing named template slot children. Unlike the Slot type, TemplateSlot is a functional type that takes some context and returns the slot children.

See the Template Slots section for more details.

Signature:

(data: Data) => VComponent.Slot