X

Technical Articles relating to Oracle Development Tools and Frameworks

  • JET
    February 2, 2017

JET Custom Components VIII - Basic Slotting

Duncan Mills
Architect

Introduction

Not every  component can be totally self contained. There are some use cases where as well as setting configuration attributes on the tag, you want the consumer to supply whole chunks of markup to embed within the component. An example of this might be a component which shows a collection as a table with a standard toolbar. If you want to be able to allow the consumer to add their own content to the toolbar how might you do it? Well, slotting is one way. Simply put, slotting is a way of naming reserved spots within the JET Custom Component into which consumer content can be injected. This allows you to make your components more extensible without having to dream up and code for every possible form of extension. For those of you familiar with the Java EE JavaServer Faces framework, slots are analogous to named facets in JSF components.

Using a Custom Component with Slots

First of all, let's look at the process of consuming a component which has one or more slots defined, some of the JET core components also use slotting so this may be familiar. To illustrate this, we'll go back to the ccdemo-name-badge component that we've been using throughout this series and imagine that the component has defined a slot called greetingArea for the consumer to use. The idea is that the user can add whatever they like into this area, be it a text or perhaps an image, or both as shown here:

To place content in the new greetingArea slot the consumer of the component puts the required content as a child of the custom component tag and sets the slot attribute on the content root which names the slot to use.

<ccdemo-name-badge id="cc1"  
                   badge-name="{{personName}}" 
                   badge-image="[[personImageURL]]" 
                   compact-view="false">
  <div slot="greetingArea">
    <span>I &hearts;</span>
    <img src="https://blogs.oracle.com/images/oracle_jet_icon.png" 
       style="vertical-align:middle"/>
  </div>
</ccdemo-name-badge>

Futhermore, you can define multiple references to a named slot and the component will gather them up and place them into the slot in the order that they are defined, thus:

 

<ccdemo-name-badge id="cc1"  
                   badge-name="{{personName}}" 
                   badge-image="[[personImageURL]]" 
                   compact-view="false">
  <span slot="greetingArea">Hello</span>
  <div slot="greetingArea">
    <span>I &hearts;</span>
    <img src="https://blogs.oracle.com/images/oracle_jet_icon.png" 
     style="vertical-align:middle"/>
  </div>
</ccdemo-name-badge>

Results in:

Note how the HTML nodes defined with the slot="greetingArea" can be of different types.

Defining Your Slots

So we've seen now simple it is to place content into slots, what about defining them? Well that's really just as simple and involves the use of the <oj-bind-slot> tag in the custom component view along with some basic metadata for documentation purposes.
Note: In earlier versions of JET this tag was simply called <oj-slot>, that version is now deprecated in favour of <oj-bind-slot> to be more consistent with the other new <oj-bind-*> components introduced in JET 4.0

First the Metadata

As you would expect we define a little metadata to help document the slotting capabilities of the component. This uses a top level property called slots which is a peer of the properties, events and methods properties that we've already seen. The basic metadata for each slot is really just to declare it and provide some description of what it does, although you can, as ever, extend the metadata to include your own information should you desire, (see Part XI).
Here's the completed metadata json for the ccdemo-name-badge component that includes the slot definition

{
  "name" : "ccdemo-name-badge",
  "version" : "1.0.0",
  "jetVersion" : ">=2.2.0",
  "properties": {
    "badgeName": {
      "description" : "Full name to display on the badge",
      "type": "string"
    },
    "badgeImage": {
      "description" : "URL for the avatar to use for this badge",
      "type": "string"
    }
  },
  "events" : {
    "badgeSelected" : {
      "description" : "The event that consuming views can use to recognize when this badge is selected",
      "bubbles" : true,
      "cancelable" : false,
      "detail" : {
        "nameOnBadge" : {"type" : "string"}
      }
    }
  },
  "methods" : {
    "changeBackground" : {
      "description" : "A function to update the background color of the badge",
      "internalName" : "_setBackgroundColor",
      "params" : [{
        "description":"Color name or hex color code",
        "name" : "colorToSet",
        "type": "string"
      }],
      "return" : "boolean"
    }
  },
  "slots" : {
    "greetingArea":{
      "description" : "Put your customized greeting here",
    }
  }
}

X Marks the Slot

The <oj-bind-slot> tag takes an attribute called name. This name attribute is the official name of the slot that the consumer will use in the corresponding slot attribute in their injected content. This name is case sensitive.

So here's the new version of the markup for the ccdemo-name-bage.html which defines a named slot called greetingArea for injected content:

<div class="badge-face">
  <img class="badge-image" 
      data-bind="attr:{src: $props.badgeImage, alt: $props.badgeName}"/>
  <h2 data-bind="text: upperFirstName"/>
  <h3 data-bind="text: $props.badgeName"/>
  <div class="greeting-area">
    <oj-bind-slot name="greetingArea"/>
  </div>
</div>

You can define as many named slots like this within your component view template as you desire. You should not define more than one <oj-bind-slot> with the same name attribute, however. Only one of the duplicates would be used and the others ignored.

Defaulting Slot Contents

In some cases you might want to supply some default content for a particular slot, just in case the user does not supply their own content. To do this, all you need to do is to add the required elements to your vomponent view template HTML as children of the relevant slot. For example, we might decide to default in a standard greeting of "Hi" into the greetingArea slot if the consumer does not specify something themselves.

<div class="badge-face">
  <img class="badge-image" 
      data-bind="attr:{src: $props.badgeImage, alt: $props.badgeName}"/>
  <h2 data-bind="text: upperFirstName"/>
  <h3 data-bind="text: $props.badgeName"/>
  <div class="greeting-area">
    <oj-slot name="greetingArea">
      <!-- Default Content -->
      <span>Hi</span>
    </oj-slot>
  </div>
</div>

If the consumer does not specify anything for the slot then this default will be used. However, if they supply any content for the slot then any default value is ignored and not displayed.
Slots are not in any way compulsory and should the user not provide any content and the slot itself has no default content, then the slot is just ignored.

Default Slot

As well as supporting named slots to target specific child elements to specific places within the final DOM tree, Composite Components also support the concept of a default slot. The default slot is just defined using a plain <oj-bind-slot> tag with no specified name attribute. Then, any content assigned as a child of the Composite Component that does not specify a slot="…" attribute, will be assigned to this default slot. As many nodes as are required can be placed into the default slot, and again they will appear in the order in which they were defined. If you do not define a default slot, then any children of the Composite Component that do not include the slot attribute will be ignored because there is nowhere to put them.

Note that if you define a child node of the component with an invalid slot name, then it will also not appear. Importantly you should note that slot names are case sensitive, thus content defined as:

<ccdemo-name-badge id="cc1"  
                   badge-name="{{personName}}" 
                   badge-image="[[personImageURL]]" 
                   compact-view="false">
  <span slot="GreetingArea">Hello</span>
</ccdemo-name-badge>

Would not display the "Hello" content because the example custom component slot name uses a lowercase leading "g" not the uppercase used by slot="GreetingArea".

Scoping of Slotted Content

We've seen that using the slotting capability , component consumers are able to inject their own markup into the midst of the component. An important question to ask, however, is what's the viewModel scope?.

For example if I utilize a statement such as this:

<ccdemo-name-badge id="cc1"  
                    badge-name="{{personName}}" 
                    badge-image="[[personImageURL]]" 
                    compact-view="false">
  <oj-bind-text slot="greetingArea" value="[[greetingMessage]]"/>
</ccdemo-name-badge>

Then where is greetingMessage coming from? The answer is not from the Custom Component. Any binding expressions that you use within slotted content are associated with the viewModel of the view that contains the reference to the component. Effectively, although it appears that slotted content is a child of the Custom Component, the reality is that it is its peer. When you think about it, this makes a lot of sense. As a consumer of a component you are not privy to the internal implementation of that component and its viewModel should be hidden from you.

What's Next?

In the next article, I'll be looking at some of the more advanced aspects of slotting, specifically how it integrates into the JET Custom Component lifecycle and how looped content is handled.


JET Custom Component Series

If you've just arrived at 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


Appendix

Updated ccdemo-name-badge.css File

Now the component supports slotting the CSS has changed a little to define the new greeting-area class and to increase the height of the component as a whole. Here's the full version of the CSS in case you want to update your copy of the code if you have been following along using the sample created in Part II of the series:

    ccdemo-name-badge:not(.oj-complete){
        visibility: hidden;
    }
    ccdemo-name-badge{
        display : block;
        width : 200px;
        height: 260px;
        margin : 10px;
        padding: 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;
    }
    ccdemo-name-badge .greeting-area {
        max-height: 60px;
        min-height: 0px;
        border-style: solid;
        border-width: 1px;
        border-radius: 5px;
        background-color: #DCE3E4;
        padding:5px;
        margin : 10px;
        text-align: center;
        overflow : hidden;
    }

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha