Creating Composite Components

You can create your own composite components for use within your own application or for sharing with other developers and applications.

The following image shows a simple Oracle JET composite component configured to display contact cards with the contact’s name and image if available. When the user selects the card, the content flips to show additional detail about the contact. You can find the complete example at Composite Component - Basic.

To create this composite component or one of your own:

  1. Determine a name for your composite component, and create a folder with that name.
    The Web Component specification restricts custom element names as follows:
    • Names must start with a lowercase ASCII letter.

    • Names must not contain any uppercase ASCII letters.

    • Names must contain a hyphen.

    • Names must not be any of the reserved names. For the complete list, see valid custom element name.

      Note:

      The Oracle JET framework also reserves the oj namespace and prefixes.

    For example, use demo-card to duplicate the contact card example shown in this example and in the cookbook at Composite Component - Basic, and create a jet-composites/demo-card folder inside your application’s src/js folder. Since composites use HTML5 custom element syntax, they will extend the HTMLElement prototype and inherit all the same base properties and methods. Composite writers should thus avoid overloading existing HTMLElement properties to avoid confusion and issues such as providing their own definition of a title property which is used for tooltips and accessibility for standard HTML elements.

  2. Determine the properties, methods, and events that your composite component will support and add them to a component.json file in the composite component’s folder.

    The demo-card example defines properties for the contact’s full name, employee image, title, work number, email address, and background image. The required metadata are highlighted in bold.

     {
      "name": "demo-card",
      "description": "A card element that can display an avatar or initials on one side and employee information on the other.",
      "version": "1.0.1",
      "displayName": "Demo Card",
      "icon":{
         "iconPath":"extension/dt/images/components/card.png",
         "selectedIconPath":"extension/dt/images/components/card_selected.png",
         "hoverIconPath":"extension/dt/images/components/card_hover.png",
         },
      "jetVersion": "^3.0.0",
      "compositeDependencies": {
        "demo-card": "1.0.1"  
        }
      "properties": {
         "name": {
           "description": "The employee's full name.",
           "type": "string"
           },
         "avatar": {
           "description": "The url of the employee's image.",
           "type": "string"
           },
         "workTitle": {
           "description": "The employee's job title.",
           "type": "string"
           },
         "workNumber": {
           "description": "The employee's work number.",
           "type": "number"
           },
        "email": {
          "description": "The employee's email.",
          "type": "string"
          },
        "backgroundImage": {
          "description": "The url of the background to use for the employee's card.",
          "type": "string"
          },
        }
     }
    
    This example only defines properties for the composite component. You can also add metadata that defines events and lifecycle listeners. The metadata lists the name of the method or event and supported parameters.
    {
      "properties": {
        ... contents omitted
      },
      "methods": {
         "flipCard" {
           "description": "Method to toggle flipping a card"
         },
         "enableFlip" {
           "description": "Enables or disables the ability to flip a card.",
           "params": [
             {
               "name": "bEnable",
               "description": "True to enable card flipping and false otherwise.",
               "type": "boolean"
              }
            ]
          },
        },
       "events": {
         "cardClick": {
           "description": "Triggered when a card is clicked and contains the value of the clicked card..",
           "bubbles": true,
           "detail": {
             "value": {
               "description": "The value of the card.",
               "type": "string"
                    }
                }
            } 
        }
    }    
    
  3. (Optional) Create the composite-name.js file in the composite component’s folder and add the ViewModel definition to it.

    The code sample below shows the ViewModel for the demo-card composite component. Comments describe the purpose, parameters, and return value of each function.

    define(['knockout'],
      function (ko) {
        function model (context) {
          var self = this;
          self.initials = null;
          self.workFormatted = null;
          var element = context.element;
    
          /**
            * Formats a 10 digit number as a phone number.
            * @param  {number} number The number to format
            * @return {number}        The formatted phone number
            */
          var formatPhoneNumber = function(number) {
            return number.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3');
          }
    ​
          // The props field on context is a Promise. Once that resolves,
          // we can access the properties that were defined in the composite metadata
          // and were initially set on the composite DOM element.
          context.props.then(function(properties) {
            if (properties.name) {
              var initials = properties.name.match(/\b\w/g);
              self.initials = (initials.shift() + initials.pop()).toUpperCase();
            }
            if (properties.workNumber)
              self.workFormatted = formatPhoneNumber(properties.workNumber);
          });
    
          /**
            * Flips a card
            * @param  {MouseEvent} event The click event
            */
          self.flipCard = function(model, event) {
            if (event.type === 'click' || (event.type === 'keypress' && event.keyCode === 13)) {
              $(element.childNodes[0]).toggleClass('flipped');
            }
          };
        }
        return model;
      }
    )
    
  4. Create the composite-name.html file in the composite component’s folder, and add the composite component’s View definition.

    The View for the demo-card composite component is shown below. Each property defined in the component’s metadata is accessed using the $props property of the View binding context.

    <div tabindex="0" role="group" class="flip-container"
      data-bind="event: {click: flipCard, keypress: flipCard}, attr: {'aria-label': $props.name + ' Press Enter for more info.'}">
      
      <div class="front-side" data-bind="attr: {style: $props.backgroundImage ? 'background-image:url(' + $props.backgroundImage + ')' : ''}">
        <!-- ko if: $props.avatar == null -->
          <div class="avatar" data-bind="text: initials"></div>
        <!-- /ko -->
    
        <!-- ko if: $props.avatar != null -->
          <img class="avatar" data-bind="attr: {src: $props.avatar, alt: $props.name}">
        <!-- /ko -->
        <h2 data-bind="text: $props.name"></h2>
      </div>
    
      <div class="back-side">
        <div class="inner-back-side">
          <h2 data-bind="text: $props.name"></h2>
          <h5 data-bind="text: $props.workTitle"></h5>
    
          <!-- ko if: $props.workNumber != null -->
            <h5>Work</h5>
            <a href="#" data-bind="text: workFormatted"></a>
          <!-- /ko -->
    
          <!-- ko if: $props.email != null -->
            <h5>Email</h5>
            <a href="#" data-bind="text: $props.email"></a>
          <!-- /ko -->
        </div>
      </div>
    </div>
    

    For accessibility, the View’s role is defined as group, with aria-label specified for the contact’s name. In general, follow the same accessibility guidelines for the composite View markup that you would anywhere else within the application.

  5. Create the loader.js RequireJS module and place it in the composite component’s folder.
    The loader.js module defines the composite dependencies and registers the composite’s tagName.
    define(['ojs/ojcore', 'text!./demo-card.html', './demo-card', 'text!./demo-card.json', 'css!./demo-card', 'ojs/ojcomposite'],
      function(oj, view, viewModel, metadata) {
        oj.Composite.register('demo-card', {
        view: {inline: view},
        viewModel: {inline: viewModel},
        metadata: {inline: JSON.parse(metadata)}
        });
      }
    );
    

    In this example, the CSS is loaded through a RequireJS plugin (css!./demo-card), and you do not need to pass it explicitly in oj.Composite.register().

  6. (Optional) Configure any custom styling that your composite component will use. You can add the styles.css file manually. If you use the Oracle JET tooling, you can also generate the styles.css file from any valid styles.scss file.
    • If you only have a few styles or are not using the Oracle JET tooling, create a styles.css file containing the desired styles and place it in the composite component’s top level folder.

      For example, the demo-card composite defines styles for the demo card’s display, width, height, margin, padding, and more.

      /* This is to prevent the flash of unstyled content before the composite properties have been setup. */
      demo-card:not(.oj-complete) {
        visibility: hidden;
      }
      
      demo-card {
        display: block;
        width: 200px;
        height: 200px;
        perspective: 800px;
        margin: 10px;
        box-sizing: border-box;
        cursor: pointer;
      }
      
      demo-card h2,
      demo-card h5,
      demo-card a, 
      demo-card .demo-card-avatar  {
        color: #fff;
        padding: 0;
      }
      
      ... remaining contents omitted
      
    • If you used the Oracle JET tooling to create your application and want to use Sass to generate your CSS:
      1. If needed, at a terminal prompt in your application’s top level directory, type the following command to add node-sass to your application: yo oraclejet:add-sass.

      2. Create styles.scss and place it in the composite component’s top level folder.

      3. Edit styles.scss with any valid SCSS syntax and save the file.

        In this example, a variable defines the demo card size:
        $demo-card-size: 200px;
        
        /* This is to prevent the flash of unstyled content before the composite properties have been setup. */
        demo-card:not(.oj-complete) {
          visibility: hidden;
        }
        
        demo-card {
          display: block;
          width: $demo-card-size;
          height: $demo-card-size;
          perspective: 800px;
          margin: 10px;
          box-sizing: border-box;
          cursor: pointer;
        }
        
        demo-card h2,
        demo-card h5,
        demo-card a,
        demo-card .demo-card-avatar  {
          color: #fff;
          padding: 0;
        }
        ... remaining contents omitted
        
      4. To compile Sass, at a terminal prompt type grunt build or grunt serve with the --sass flag and application-specific options.

        grunt build|serve [options] --sass
        

        grunt build --sass will compile your application and generate styles.css and styles.css.map files in the default platform’s folder. For a web application, the command will place the CSS in web/js/js-composites/composite-name.

        grunt serve --sass will also compile your application but will display the web application in a running browser with livereload enabled. If you save a change to styles.scss in the application’s src/js/jet-composites/composite-name folder, grunt will compile Sass again and refresh the display.

        Tip:

        For help with the grunt commands, type grunt help at a terminal prompt.