X

Technical Articles relating to Oracle Development Tools and Frameworks

  • JET
    January 24, 2017

JET Custom Components II - The Basics

Duncan Mills
Architect

Last Updated Dec 2018 for JET 6

Introduction

In this primer article on Custom JET Web Components (aka JET Composites or CCA), I'll just be looking at the basic tasks involved in creating a simple custom component. Think of this as a "hello world" tutorial equivalent. In later articles I'll be expanding on many of the topics that are only touched on here, so if you work through the articles in sequence everything will make sense as a whole and you'll get a good grounding in this feature.
Note that this article, and in fact this entire series, has been upgraded over time and so, as such, you need to be using at least JET 4.0 to follow along. In places where there are differences between the latest version of JET and older versions I'll call those out, so pay attention to the versions mentioned at the top of this article and the version you are actually using. 

Preparation

Throughout this series of articles I'll be building up some example components that you can follow along with should you wish. So to start off, I'll just create a new JET Application as a workspace for the subsequent code to live in:

ojet create myfirstcomponent --template=basic

Setting up the JET Application to use Custom JET Components

RequireJS Configuration 

The first thing that should be on your checklist when consuming Custom JET Components into an application is to make sure that both the text and css requireJS plugins are configured up. If you create an application using the ojet command line tool (ojet-cli), (or using the Yeoman scaffold in versions of JET prior to 4.0.0),  this will be done for you. However, if you are working in really old versions of JET (prior to 2.3.0), you will have the text plugin defined already, but not the css plugin. To either check this, or indeed carry out the task of configuration, locate your requirejs.config{}. This configuration will usually be found in js/main.js if you have used the ojet-cli or scaffolds, but this may differ of course if you have hand crafted your application, or perhaps if you have more than one configuration (e.g. for unit-testing). Your basic requireJS configuration should look a little like this (notwithstanding additional paths and configurations that you may have added):

requirejs.config({
  baseUrl: 'js',
  // Path mappings for the logical module names
  paths:
  //injector:mainReleasePaths
  {
    'knockout': 'libs/knockout/knockout-3.4.2.debug',
    'jquery': 'libs/jquery/jquery-3.3.1',
    'jqueryui-amd': 'libs/jquery/jqueryui-amd-1.12.1',
    'promise': 'libs/es6-promise/es6-promise',
    'hammerjs': 'libs/hammer/hammer-2.0.8',
    'ojdnd': 'libs/dnd-polyfill/dnd-polyfill-1.0.0',
    'ojs': 'libs/oj/v6.0.0/debug',
    'ojL10n': 'libs/oj/v6.0.0/ojL10n',
    'ojtranslations': 'libs/oj/v6.0.0/resources',
    'text': 'libs/require/text',
    'signals': 'libs/js-signals/signals',
    'customElements': 'libs/webcomponents/custom-elements.min',
    'proj4': 'libs/proj4js/dist/proj4-src',
    'css': 'libs/require-css/css',
    'touchr': 'libs/touchr/touchr'
  }
  //endinjector
  });

In the above1 path configuration you can see both the text and ccs plugins are defined as we require.

When you are consuming Custom JET Components from outside of your codebase, you may be asked to add additional library paths to this configuration as well, just consult the supplied documentation for the Component.

ojknockout Dependency

Custom components do depend on the ojs/ojknockout module being loaded and bindings applied.  When you create a JET application using the ojet-cli, or one of the Yeoman scaffolds, this will automatically be done for you via the require([...]) in the generated main.js, which also calls ko.applyBindings().  So this will only be an issue for you if you hand-craft a JET project from scratch. The symptom of not applying bindings would be that your custom component viewModel code will get executed but you'll just not see anything in the page as the view part will not be rendered. 

On to Creating a Custom Component...

In this first example, I'm going to walk through the task of creating a very basic component that displays an image and a name which will be passed in as parameters to the component2. I'll call the component ccdemo-name-badge. Our aim here is to create something a little like (but simpler than) the basic Composite Component demo in the cookbook.

Note: In this basic article I'll be leaving a lot of detail out, but rest assured each topic of interest will be covered in much more detail later.

If you are in JET 4.0.0 or above, you can use the ojet command line interface to quickly scaffold up a new component with all the basic parts described here. See this article for details.

cd myfirstcomposite
ojet create component ccdemo-name-badge

And you are done!

Testing the generated Custom JET Component

After using the ojet-cli we can actually run the component to check that it works - it won't really do much yet, but it proves that the basic generation step was OK. To do this, edit the myfirstcomponent/js/appController.js file in your preferred editor and change the define() block to the following:

JET 5.2.0 and Above

define(['ojs/ojcore', 'knockout', 'ojs/ojknockout','ccdemo-name-badge/loader'],

JET 5.1.0 and Earlier

define(['ojs/ojcore', 'knockout', 'ojs/ojknockout','jet-composites/ccdemo-name-badge/loader'],

Just like any normal JET component you need to tell the viewModel to require (i.e. load) the component before you use it and that's exactly what the reference to the ccdemo-name-badge/loader file above is doing. The difference between the two versions shown above is that in JET 5.2.0 we introduced the automatic creation of a requireJS path with the same name as the component which makes loading it a little easier and more reliable.

Next, edit the index.html in the root of the myfirstcomponent application source directory. Look for a div element with role="main" and inside of it put the Custom JET Component tag:

<ccdemo-name-badge></ccdemo-name-badge>

Save everything, and run the application using the ojet serve command. The page will load in the browser and the component will simply show as a string on the screen "Hello from Example Component". Great now we know the generated component is basically working, albeit not doing much yet.

Configuring the generated Custom JET Component

We've shown that the ojet-cli has created all of the basic artefacts of a custom component for you, but you'll now need to go and configure those to add the extra functionality we need.

Step 1 - Define The Custom JET Component Metadata

The JSON format metadata file is a key part of your component. It provides a declarative definition of the component in terms of the properties, the methods and any events that it supports i.e. its API. In this first simple example we're only going to worry about a couple of attributes. Methods and events will be covered in detail later in this series.

The ojet-cli has pre-created a JSON file called component.json which you will find in the  js/jet-composites/ccdemo-name-badge directory, go ahead and edit that file.

In this example component we just want to define two properties badgeImage and badgeName. To do this the JSON file needs to define a definition of a properties object, which in turn contains the definition of each of the above. Harder to say than actually do, here's what it would look like:

{
  "name": "ccdemo-name-badge",
  "displayName": "ccdemo-name-badge",
  "description": "Describe your component here",
  "version": "1.0.0",
  "jetVersion": "6.0.0",
  "properties":{
    "badgeName" : {
      "type" : "string"
    },
    "badgeImage" : {
      "type" : "string"
    }
  }
}

And that's it, both properties will be strings as the type attribute indicates. You can ignore or delete the methods and slots attributes, we'll cover those in later articles.

Step 2 - Define the Template HTML

In some cases the HTML template may be totally dynamic and impossible to define ahead of time. In our simple example, however, that's not the case. We have a well defined structure which we can create as an HTML template for the layout for the component. The ojet-cli has pre-created a starter HTML template for us called view.html, also in the js/jet-composites/ccdemo-name-badge directory. Edit this file and replace the generated contents with the following:

<div class="badge-face">
  <img class="badge-image"
       data-bind="attr:{src: $properties.badgeImage, alt: $properties.badgeName}">
  <h3><oj-bind-text value="[[$properties.badgeName]]"></oj-bind-text></h3>
</div>

So in this template you will notice the the knockout data-bind attribute is being used to define the attributes for the IMG and the <oj-bind-text> tab in the H3. We are then binding to the $properties object which the framework provides to give you direct access to the configured properties of the Custom JET Component tag, in this case the badgeName and badgeImage attributes.

In later articles I'll show how you can bind to values other than just the properties via $properties in this way.

Step 3 - Define the Component CSS

In nearly every case you will have some style information associated with your component. As a standard you should always include a style for the component as a whole which will suppress the display of the component until everything is ready to go. The framework will automatically set the .oj-complete class onto the component to trigger the display when everything is ready. In this simple example, we supply this standard base class for the component as a whole that will control this visibility. Aside of that, there will be an additional badge-face class to style the actual badges and a further class for the badge image. The generated CSS file is called styles.css and is also placed in the js/jet-composites/ccdemo-name-badge directory, the first bit will have been generated for you:

ccdemo-name-badge:not(.oj-complete){
  visibility: hidden;
}
ccdemo-name-badge{
  display : block;
  width : 200px;
  height: 300px;
  margin : 10px;
}
ccdemo-name-badge .badge-face {
  height : 100%;
  width : 100%;
  background-color : #80C3C8;
  border-radius: 5px;
  text-align: center;
  padding-top: 30px;
}
ccdemo-name-badge .badge-image {
  height : 100px;
  width : 100px;
  border-radius: 50%;
  border:3px solid white;
}

Step 4 - Define the Component ViewModel

In the long run, all Custom JET Components are going to exhibit some sort of behavior (if not then you should probably not be using a component in the first place). As such, we'll need somewhere - a ViewModel JavaScript file - to act as the home for that behavior code. In this first simple example, we can just use the basic ViewModel.js that was generated for us by the ojet-cli. The file is of course in the js/jet-composites/ccdemo-name-badge directory and will look like this in JET 6 (there may be minor differences if you are using an earlier version but just go with the flow if that's the case):

'use strict';
define(
    ['knockout', 'jquery', 'ojL10n!./resources/nls/ccdemo-name-badge-strings'], function (ko, $, componentStrings) {

    function ExampleComponentModel(context) {
        var self = this;
        
        //At the start of your viewModel constructor
        var busyContext = oj.Context.getContext(context.element).getBusyContext();
        var options = {"description": "CCA Startup - Waiting for data"};
        self.busyResolve = busyContext.addBusyState(options);

        self.composite = context.element;

        //Example observable
        self.messageText = ko.observable('Hello from Example Component');
        self.properties = context.properties;
        self.res = componentStrings['ccdemo-name-badge'];
        // Example for parsing context properties
        // if (context.properties.name) {
        //     parse the context properties here
        // }

        //Once all startup and async activities have finished, relocate if there are any async activities
        self.busyResolve();
    };
    
    //Lifecycle methods - uncomment and implement if necessary 
    //ExampleComponentModel.prototype.activated = function(context){
    //};

    //ExampleComponentModel.prototype.connected = function(context){
    //};

    //ExampleComponentModel.prototype.bindingsApplied = function(context){
    //};

    //ExampleComponentModel.prototype.disconnect = function(context){
    //};

    //ExampleComponentModel.prototype.propertyChanged = function(context){
    //};

    return ExampleComponentModel;
});

You can see the variable that is supplying the default "Hello" message here, but you can delete that, everything else is just boilerplate and can be left for now. For neatness you may want to change the name of the object defined here from "ExampleComponentModel" to a more descriptive name such as "CCDemoNameBadgeComponentModel"

Note that the return statement for the component viewModel is not creating an instance of the model, but rather is returning the constructor function itself. Taking this approach, each instance of the Composite Component that is used will have it's own distinct component viewModel. This is what you want in most cases.

Step 5 - Examine the Component Bootstrap Script

The final piece of the component creation puzzle is the bootstrap file used to register the Custom JET Component. This little script pulls together all of the artifacts that we have already referenced and uses them to register the component with the framework. Without this script, nothing would happen! You've seen this already, it's that loader reference that you added to the appController.js to test the component. Have a look at it now, again it can be found in the js/jet-composites/ccdemo-name-badge directory and will look like this (Again if you use older versions this may look slightly different but just go with what is generated for you):

define(['ojs/ojcomposite', 
        'text!./ccdemo-name-badge-view.html', 
        './ccdemo-name-badge-viewModel', 
        'text!./component.json', 
        'css!./ccdemo-name-badge-styles'],
  function(Composite, view, viewModel, metadata) {
    Composite.register('ccdemo-name-badge', {
      view: view,
      viewModel: viewModel,
      metadata: JSON.parse(metadata)
    });
  }
);

Because this script is so important to the functioning of the Custom JET Component, it's worth examining it in a little more depth.

Looking at the define block first of all, you can see that it defines a requirement for each of the four previous files:

  1. 'text!./ccdemo-name-badge-view.html' → the HTML template loaded using the requireJS text plugin
  2. './ccdemo-name-badge-viewModel' → viewModel.js which defines the component model
  3. 'text!./component.json' → the component metadata file, also loaded using the requireJS text plugin
  4. 'css!./ccdemo-name-badge-styles' → the styles.css file loaded using the requireJS css plugin

You can see from the above why it is so important to ensure that you have the text and css plugins defined for requireJS at the very start.

These scripts and streams are injected into the bootstrap function as view, viewModel and metadata respectively (the css file is actually loaded by the css! plugin so we don't need to do anything further with that). You are free to change the names of these function arguments of course, but using these names makes it pretty clear what each one will do.

Within the bootstrap function, the code makes the all important call to Composite.register(). (or oj.Composite.register on versions prior to JET 6). This function does all the actual work of registering the Custom JET Web Component with the framework. The first argument to the register function is name name that you want to assign to the tag - in this case ccdemo-name-badge. This name does not have to really match the name of the directory we have created as a home for the component or indeed the names we have used for the various files. However, it keeps everything much more organized and understandable if your component name matches the directory name that defines it, so there is no reason not to stick to this standard.

The second argument to the register function takes an object which as we see here has four attributes:

  1. metadata
  2. viewModel
  3. view
  4. css (deprecated in JET 4.1.0 - not needed)

Each of these is mapped to the corresponding objects passed into the main bootstrap function. Note how the metadata value is mapped through JSON.parse(). This is because the requireJS text plugin has just been used to read this file and we need to convert from text stream that that produced back into a JS object.

In version of JET prior to JET 5.0 you may also note that each of the above attributes are passed the injected file contents as an object with an attribute name of inline e.g.

viewModel: {inline: viewModel},

This is simply telling the component registration process that the passed object is the actual value that it should use for that attribute. In that older version it was  possible to pass a promise rather than the object directly, however, that is a legacy option and should not be used.

There is more that can happen as part of the registration process, but really, this simple version is all that most Custom JET Components will need, so I'll leave it there.

Testing the Updated Component

So now that our ccdemo-name-badge is "complete" we can test it again in the application. All we need to do is to set the two new properties on the tag:

  <ccdemo-name-badge badge-name="Duke Mascot" 
                     badge-image="/images/duke.png">
  </ccdemo-name-badge>

Note: You'll notice here how the tag attributes are different from the defined property name. I explain this more in later articles, but the basic rule is that camel-case property names are converted into case-insensitive HTML element attributes with hyphens at the camel-case break point of the original name. Thus badgeName gets mapped to an HTML element attribute called badge-name.

Now run the application again using ojet serve, and this is what you would see:

Image of completed composite component

What Next?

Now that you've seen how to create a basic Custom JET Component we can start to drill down and explore various sub-topics in more detail. The next article in this series spends a little time on establishing your standards and conventions for creating components.


JET Custom Component Series

If you've just started with Custom JET Components and would like to learn more, then you can access the whole series of articles on the topic from the Custom JET Component Learning Path


1 The version numbers shown in the fragment above are reflective of the Oracle JET 6.0.0 release and may well be different if you are consuming a different version.

2 This component is not intended to be actually useful, just to show the basics.

Join the discussion

Comments ( 4 )
  • Thomas Palzkill Thursday, December 20, 2018
    Thanks for putting this together Duncan, it's awesome.

    One note: With Jet 6 you no longer need (can) specify the jet-composites directory in the AppLoader define statement.
    The statement now looks like:
    define(['ojs/ojcore', 'knockout', 'ojs/ojknockout','jet-composites/ccdemo-name-badge/loader'],
  • Anirban Mukherjee Wednesday, December 26, 2018
    Thank you for this series Duncan. This has been of great help. Your little info on the camel case changing to hyphenated attribute was a life saver!
  • Utkarsh Monday, April 22, 2019
    Can we use same name component tag two times in view module.
  • Duncan Tuesday, April 23, 2019
    Yes, as long as you assign unique values to the id attribute you can use as many instances as you need
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.