Best Practices for Web Component Creation

Best practices for creating Oracle JET Web Components include required and recommended patterns, configuration, coding practices, and styling standards. Follow best practices to ensure interoperability with other Web Components and consuming frameworks.

Recommended Standard Patterns and Coding Practices

Recommended patterns and coding practices for Oracle JET Web Components include standards for configuration, versioning, coding, and archival.

Component Versioning

Your Web Component must be assigned a version number in semantic version format.

When assigning and incrementing the version number associated with your components, be sure to follow semantic version rules and update Major, Minor and Patch version numbers appropriately. By doing so, component consumers will have a clear understanding about the compatibility and costs of migrating between different versions of your component.

To assign a version number to your Web Component, see semantic version.

JET Version Compatibility

You must use the semantic version rules to specify the jetVersion of the supported JET version(s). Web Component authors should not specify a semantic version range that includes unreleased JET major versions as major releases may contain non backwards compatible changes. Authors should instead recertify Web Components with each major release and update the component metadata or release a new version that is compatible with the new release changes.

Translatable Resources

Developers who want to localize Web Component translatable resources now get a resource bundle (template) when they create their Web Component. These components should use the standard Oracle JET mechanism using the ojL10n requireJS plugin. You must store the translation bundles in a resources/nls subdirectory within your Web Component’s root folder. You can declare the languages and locales that you support in the Web Component metadata.

Peer-to-Peer Communication

Components must prefer a shared observable provided by the consumer over any kind of secret signaling mechanism when you are dealing with a complex integration. For example, a filter component and a data display component. By using a shared observable you can pre-seed and programmatically interact with the components through the filter.

Alternatively, you can use events and public methods based on one of the following approaches being used:

  • A hierarchical relationship between the source and receiver of the event.

  • The identity of the source being passed to the receiver.

    Note that in some runtime platforms, the developer doing the wiring may not have access to component IDs to pass the relevant identity.

  • Listeners attached by components at the document level.

    In this case, you are responsible for the cleanup of those listeners, management of duplicates, and so on. Also, such listeners should preferably be based on Web Component events, not common events such as click, which might be overridden by intermediate nodes.

Note:

Under the web-component standards (shadow DOM), events will be re-targeted as they transition the boundary between the component and the consuming view. That is, the apparent identity of the raising element might be changed, particularly in the case of Nested Web Component architecture where the event would get tagged with the element representing the outer Web Component rather than the inner Web Component. Therefore, you should not rely on the event.target attribute to identify the Web Component source when listening at the document level. Instead, the event.deepPath attribute can be used to understand the actual origin of the event.

Access to External Data

Web Components do not permit the usage of the knockout binding hierarchy to obtain data from outside the Web Component context, for example, $root, $parent[1], and so on. All data transfer in and out of the component must be through the formal properties, methods, and events.

Object Scope

All properties and functions of Web Components should be confined to the scope of the view model. No window or global scope objects should be created. Similarly, the existence of window scope objects should not be assumed by the Web Component author. If a consumer Web Component defined externally at window or global level is required for read or write then that component must be passed in by the consuming view model through a formal property. Even if a well known global reference is needed from outside of the component, for example jQuery, it should be formally injected using the require define() function and declared as a dependency in the Web Component metadata.

External References

If a Web Component must reference an external component, it should be part of the formal API of the component. The formal API passes the component reference through a property. For example, to allow the registration of a listener, the Web Component code requires a component reference defined externally. You must not allow Web Components to obtain IDs from hard-coded values, global storage, or walking the DOM.

Subcomponent IDs

Within the framework if any component needs a specific ID, use context.unique or context.uniqueId value to generate the ID. This ID is unique within the context of the page.

ID Storage

Any generated IDs should not be stored across invocation, such as in local storage or in cookies. The context.unique value for a particular Web Component may change each time a particular view is executed.

LocalStorage

It is difficult to consistently identify a unique instance of a Web Component within an application. So, it is advised not to allow a Web Component to utilize the local storage of a browser for persisting information that is specific to an instance of that Web Component. However, if the application provides a unique key through the public properties of the component you can then identify the unique instance of the component.

Additionally, do not use local storage as a secret signaling mechanism between composites. You cannot assure the availability of the capability and so it is recommended to exchange information through a shared JavaScript object or events as part of the public API for the component(s).

String Overrides

Web Components will often contain string resources internally to service their default needs for UI and messages. However, sometimes you may want to allow the consumer to override these strings. To do this, expose a property for this purpose on the component. By convention such a property would be called translations, and within it you can have sub-properties for each translatable string that relates to a required property on the component, for example requiredHint, requiredMessageSummary, and so on. These properties can then be set on the component tag using sub-property references. For example:

"properties" : {
  "translations": {
    "description": "Property to allow override of default messages",
    "writeback" : false, 
    "properties" : {
      "requiredHint": {
        "description": "Change the text of the required hint", 
        "type": "string"
         },
      "requiredMessageSummary": {
        "description": "...",
        "type": "string"
        },  
      "requiredMessageDetail": {
          "description": "...",
          "type": "string"
        } 
      }
     } 
   }
}

Logging

Use oj.Logger to write log messages from your code in preference to console.log(). The Web Components should respect the logging level of the consuming application and not change it. You should ideally prefix all log messages emitted from the component with an identification of the source Web Component. As a preference, you can use the Web Component name. The components should not override the writer defined by the consuming application.

Expensive Initialization

Web Components should carry out minimum work inside the constructor function. Expensive initialization should be deferred to the activated lifecycle method or later. The constructor of a Web Component is invoked even if the component is not actually added to the visible DOM. For example, if a constructor is invoked within a Knockout if block. The further lifecycle phases will only occur when the component is actually needed.

Service Classes

The use of global service classes, that is functionality shared across multiple Web Components, can constitute an invisible contract that the consumer of your Web Component has to know about. To avoid this, we recommend:

  • Create the service as a module that every Web Component can explicitly set it as require() block, thus removing the need for the consumer to do this elsewhere.

  • Consider the timing issues that might occur if your service class needs some time to initialize, for example fetching data from a remote service. In such cases, you should be returning promises to the service object so that the components can safely avoid trying to use the information before it is actually available.

Using ojModule

If you use ojModule in a Web Component and plan to distribute the Web Component outside of your application, you must take additional steps to ensure that the contained ojModule could be loaded from the location relative to the location of the Web Component. Unless the View and ViewModel instances are being passed to ojModule directly, you will need to provide the require function instance and the relative paths for views and view models. The require function instance should be obtained by the component loader module by specifying require as a dependency.

<div data-bind="ojModule: {require: {instance: require_instance, viewPath: "path_to_Web_Component_Views", modelPath: "path_to_cWeb_Component_ViewModels"}}"></div> 
require Option Type Description

instance

Function

Function defining the require instance

viewPath

String

String containing the path to the Web Component’s Views

modelPath

String

String containing the path to the Web Component’s ViewModels

For additional information about working with ojModule, see ojModule.

Archiving Web Components for Distribution

If you want to create a zip file for packaging, create an archive with the same name as the component itself. You may add version-identifying suffixes to the zip file name for operational reasons. The Web Component artifacts must be placed in the root of the zip file, and there should be no intermediate directory structure before reaching the files.

Using Lifecycle Methods

If a ViewModel is provided for a Web Component, the following optional callback methods can be defined on its ViewModel that will be called at each stage of the Web Component's lifecycle. Some of the callback methods that can be used are listed below:

  • activated(context): Invoked after the ViewModel is initialized.

  • connected(context): Invoked after the View is first inserted into the DOM and then each time the Web Component is reconnected to the DOM after being disconnected.

  • bindingsApplied(context): Invoked after the bindings are applied on the View.

  • propertyChanged(context): Invoked when properties are updated before the [property]Changed event is fired.

  • disconnected(element): Invoked when this Web Component is disconnected from the DOM.

For additional information on Web Component lifecycle methods, see oj.Composite - Lifecycle.

Template Aliasing

JET components that support inline templates can now use an optional data-oj-as attribute to provide template specific aliases for $current variable at the application level. In the instances where the component must support multiple template slots as in the case of chart and table components, a single alias may not be sufficient. In such cases, you can use an optional data-oj-as attribute on the template element. For more information on the usage of this optional attribute with template slots, see oj-bind-template-slot API documentation.

CSS and Theming Standards

Oracle JET Web Components should comply with all recommended styling standards to ensure interoperability with other Web Components and consuming applications.

For information on the generic best practices for using CSS and Themes, see Best Practices for Using CSS and Themes.

Standard Details Example

Prevent flash of unstyled content

Oracle JET will add the oj-complete class to the Web Component DOM element after metadata properties have been resolved. To prevent a flash of unstyled content before the component properties have been setup, the component’s CSS should include a rule to hide the component until the oj-complete class is set on the element.

Note that this is an element selector, and there should not be a dot (.) before acme-branding.

acme-branding:not(.oj-complete) {
  visibility: hidden;
}

Add scoping

Use an element selector to minimize the chance that one of your classes is used by someone outside of your component and becomes dependent on your internal implementation. In the example to the right, if someone tries to apply the class acme-branding-header, it will have no effect if it's not within an acme-branding tag.

acme-branding .acme-branding-header {
  color: white;
  background: blue;
}

IMPORTANT: If your component also includes a dialog, then when displayed, that dialog will be attached to the main document DOM tree and will not be a child of your component. Therefore, if you define a style to apply to a dialog defined by your component, you cannot scope it to the component name as that's not the actual container for the dialog when displayed.

To resolve, use the component name as the prefix instead:

.acme-branding-dialog-background{
  color: white;
  background: blue;
}

Avoid element styling

The application will often style HTML tag elements like headers, links, and so on. In order to blend in with the application, avoid styling these elements in your Web Component. For information about Oracle JET’s use of styles on HTML tags, see Use Tag Selectors or Classes.

Warning Icon Avoid styling on elements like headers.

acme-branding .acme-branding-header h3{
  font-size: 45px;
}