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 Jan 2020 for JET 8

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 7.0.0 to follow along. In places where there are differences between the latest version of JET and older versions I'll try call those out, so pay attention to the versions mentioned at the top of this article and the version you are actually using. 

Further Note:  I'll be using the Oracle JET command line tool (ojet) throughout as this has evolved to do most of the boring bits for you. I'll also stick to doing everything in JavaScript.  TypeScript will be covered in a later article.

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

Note on 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. 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.

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 component1. 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 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": "Name badge",
  "description": "This is my first example component",
  "version": "1.0.0",
  "jetVersion": "^8.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 ccdemo-name-badge-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" :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 normal SRC and ALT attributes of the HTML <img> tag are prefixed with a colon.  This syntax tells the framework to treat the value of the attribute in a special way.  If the string in the value is enclosed in double square brackets then it will be evaluated as an expression rather than a literal. In this case we are getting those values from 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. Within the <h3> element we have something similar using the <oj-bind-text> component which just creates a text node, again from an expression enclosed in double square brackets.

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 ccdemo-name-badge-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 ccdemo-name-badge-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 8 (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', 
     'ojL10n!./resources/nls/ccdemo-name-badge-strings', 
     'ojs/ojcontext', 
     'ojs/ojknockout'], 
    function (ko, componentStrings, Context) {
    
    function ExampleComponentModel(context) {
        var self = this;
        
        //At the start of your viewModel constructor
        var busyContext = Context.getContext(context.element).getBusyContext();
        var options = {"description": "Web Component 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. 

Step 5 - Examine the Component Bootstrap / Loader 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 the name that you want to assign to the HTML 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 three attributes:

  1. metadata
  2. viewModel
  3. view

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 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.