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 or div with some text like "Choose Option".
    • Show and hide the drop-down list.
  • 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.

References: