Introduction
Not every Composite 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 Composite Component into which consumer content can be injected. This allows you to make your Composite 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 Composite Component with Slots
First of all, let's look at the process of consuming a Composite Component which has one or more slots defined. 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 Composite 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 ♥</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 Composite 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 ♥</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 Composite 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-slot> tag in the Composite Component view along with some basic metadata for documentation purposes.
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 Composite Component that includes the slot definition
{
"name" : "ccdemo-name-badge",
"version" : "1.0.0",
"jetVersion" : ">=2.2.0",
"properties": {
"badge-name": {
"description" : "Full name to display on the badge",
"type": "string"
},
"badge-image": {
"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-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['badge-image'], alt: $props['badge-name']}"/>
<h2 data-bind="text: upperFirstName"/>
<h3 data-bind="text: $props['badge-name']"/>
<div class="greeting-area">
<oj-slot name="greetingArea"/>
</div>
</div>
You can define as many named slots like this within your Composite Component view template as you desire. You should not define more than one <oj-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 Composite 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" data-bind="attr:{src: $props['badge-image'], alt: $props['badge-name']}"/>
<h2 data-bind="text: upperFirstName"/>
<h3 data-bind="text: $props['badge-name']"/>
<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-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 Composite 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 Composite 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 of Composite Components, component consumers are able to inject their own markup into the midst of the Composite. 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">
<span slot="greetingArea" data-bind="text:greetingMessage"/>
</ccdemo-name-badge>
Then where is
greetingMessage coming from? The answer is not from the Composite Component. Any binding expressions that you use within slotted content are associated with the viewModel of the view that contains the reference to the Composite Component. Effectively, although it appears that slotted content is a child of the Composite 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 Composite Component lifecycle and how looped content is handled.
Composite Component Article Series Index
- Introduction
- Your First Composite Component - A Tutorial
- Composite Conventions and Standards
- Attributes, Properties and Data
- Events
- Methods
- The Lifecycle
- Slotting Part 1
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;
}