Last Updated Jan 2020 for JET 8
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.
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 badge-name="{{personName}}"
badge-image="[[personImageURL]]">
<div slot="greetingArea">
<span>I ♥</span>
<img src="https://blogs.oracle.com/images/oracle_jet_icon.png"
style="vertical-align:middle"/>
</div>
</ccdemo-name-badge>
Furthermore, 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 badge-name="{{personName}}"
badge-image="[[personImageURL]]">
<span slot="greetingArea">Hello</span>
<div slot="greetingArea">
<span>I ♥</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.
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 <oj-bind-*> components introduced in JET 4.0
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 attributes 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": "^8.0.0",
"displayName": "Name Badge.",
"description": "A simple re-usable name badge component",
"properties":{
"badgeName" : {
"type" : "string",
"description" : "Full name to display on the badge"
},
"badgeImage" : {
"type" : "string",
"description" : "URL for the avatar to use for this badge"
}
},
"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"
}
}
}
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 HTML view markup for the ccdemo-name-badge-view.html which defines a named slot called greetingArea for injected content:
<div class="badge-face">
<img class="badge-image"
:src="[[$properties.badgeImage]]"
:alt="[[$properties.badgeName]]">
<h2>
<oj-bind-text value="[[upperFirstName]]"></oj-bind-text>
</h2>
<h3>
<oj-bind-text value="[[$properties.badgeName]]"></oj-bind-text>
</h3>
<div class="greeting-area">
<oj-bind-slot name="greetingArea"></oj-bind-slot>
</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.
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 component 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"
:src="[[$properties.badgeImage]]"
:alt="[[$properties.badgeName]]">
<h2>
<oj-bind-text value="[[upperFirstName]]"></oj-bind-text>
</h2>
<h3>
<oj-bind-text value="[[$properties.badgeName]]"></oj-bind-text>
</h3>
<div class="greeting-area">
<oj-bind-slot name="greetingArea">
<!-- Default Content -->
<span>Hi</span>
</oj-bind-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.
As well as supporting named slots to target specific child elements to specific places within the final DOM tree, custom 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 custom 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 custom 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 badge-name="{{personName}}"
badge-image="[[personImageURL]]">
<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".
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 scope of variables referenced in this slotted content?.
For example if I utilize a statement such as this:
<ccdemo-name-badge badge-name="{{personName}}"
badge-image="[[personImageURL]]">
<p slot="greetingArea">
<oj-bind-text value="[[greetingMessage]]"></oj-bind-text>
</p>
</ccdemo-name-badge>
Then where is greetingMessage coming from? The answer is not from the custom component1. 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.
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.
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
1Actually it is possible for the component state to be used but I'll cover that in a later article
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;
}