In most of the recent frameworks, you must have came across concept called Component. Component has became integral part of JS development these days. Those are independent and reusable bits of code, which make your code looks much more cleaner and maintainable.
Here in this article, we will look into “Contextual Component” to write more flexible and expressive interfaces for your components in Ember.js framework.
Problem:
While working on one of the Ember.js application, we came across a requirement of adding a drop-down in the project. There were already two drop-down components, but they were very tightly coupled to certain styling, looks, and usage. As per the new requirement, these drop-down components were not fit to be used.
We wanted to create a generic drop-down component that should fulfill following requirements:
- It should not have business logic in it.
- Users should have the liberty to apply to their UI, styling, and action.
Possible Solution:
We find block level component should be a perfect fit for this scenario
What is the Block component?
A block component is a component that has a start-tag, end-tag, but in between, you can add the custom UI to be displayed.
/**
This is component usage
**/
{{#drop-down callbackToBeCalled=(action 'AskComponentToRunThisAtSomePoint')}}
<div>
The UI user want to write
</div>
{{/drop-down}}
In the component hbs
file you need to yield
, in order to render the content.
/**
This is drop-down.hbs file
**/
{{yield}} //This will will render passed `div`
<div>
This is component default element, which will be available everytime.
</div>
Usually, any drop-down component has the following features:
- Dropdown with the message "Choose Option" or preselected element from the list.
- On clicking the drop-down button it should show (render) a list of choices.
- On selecting the element from the list, close the drop-down and display the selected choices.
We can divide our Drop-down component into 2 parts:
-
Generic drop-down elements (This contains what every drop-down component should have):
- It should display a
button
ordiv
with some text like "Choose Option". - Show and hide the drop-down list.
- It should display a
-
User dependent elements (This contains what the user can add to the component):
- The list user wants to display along with styling and look of the list.
- How selected element of drop-down should look in the head.
- What action to be executed on selecting the list element.
We will design our component on the basis of the above generic
and user-dependent
elements.
Issue with component yield?
This component expects two elements as an input from the user, and they are the heading
of the dropdown and the list element of the dropdown.
So, if there are two inputs the component is expecting from the user, then it also has two data to yield. But how will component will know which yield
will be heading
and which will be body
?
To make the picture clear, you can see the user is providing one div
with the text "Choose Option" and another ul
tag with some list rendering logic.
{{#drop-down callbackToBeCalled=(action 'AskComponentToRunThisAtSomePoint')}}
<head>
<div> "Choose Option" </div>
</head>
<body>
<ul>
{{#each lists as |list|}}
{{list}}
{{/each}}
</ul>
</body>
{{/drop-down}}
When the user sends the head
and body
tag, our component should be able to identify it. How can we achieve this?
Contextual component comes to rescue 🦸🏻
What is Contextual Component ? In simple words, a component that can yield other components in its block. The main parent component (in our case drop-down) will provide a named block-component where users can render the UI of their choice.
We want to display head
and body
information within some blocks. So, the parent component will be passing two block-component without any body and some name.
Let's create a block-component named blank-component
where our head
and body
will be rendered.
//blank-component.hbs
{{yield}}
//blank-component.js
export default Component.extend({
tagName: ""
});
Now, we will yield this component with some name in the drop-down
component.
Note: yield
has the property to send a hash of information.
//drop-down.hbs
<div {{action "toggleDisplayItems"}}>
{{yield (hash dropDownHeader="blank-template")}}
</div>
<div class="{{if displayDropDownItems '' 'hidden'}}">
{{yield (hash
dropDownBody="blank-template"
clickItem = (action 'selectOption')
)}}
</div>
You can see we have two yield statements, one with the name dropDownHeader
and the other dropDownBody
and both of them are passing the blank-component
block component.
In short, it will provide a head
block(namespace) to display "Choose from a list"
text and the body
block to "display the list".
Final Usage:
{{#drop-down onSelect=(action "doSomethingOnSelecting") as |dropDown|}}
{{#dropDown.dropDownHeader}}
<div class="any-styling-of-your-choice">
"Choose from a list"
</div>
{{/dropDown.dropDownHeader}}
{{#dropDown.dropDownBody}}
<ul>
{{#each lists as |list|}}
<li {{action "doSomeAction" list}}>
{{list}}
</li>
{{/each}}
</ul>
{{/dropDown.dropDownBody}}
{{/drop-down}}
As you can see, the Contextual component provided the user with a namespace that gives them the freedom to render head
and display body
of their choice. The user is now not bounded to drop-down styling and action of the drop-down. User can provide custom UI-styling and actions.