Making a modal dialog using yieldable named blocks
15 October 2020
Have you ever wished you could pass more than one block when invoking components using the angle-bracket syntax? I certainly have.
There are ways around it but all the ones I tried had shortcomings. So I was glad to see the Yieldable Named Blocks RFC accepted and was even happier to learn that a polyfill add-on, ember-named-blocks-polyfill already implements the RFC.
What are named blocks?
So what are named blocks? With angle-bracket invocation, components can yield from their templates which invokes the passed block.
Imagine we have the following modal dialog component:
Let's invoke it as follows:
That gives us the following rendered content:
1<div class="modal-dialog"> 2 <div class="header"> 3 <h3>Are you sure?</h3> 4 </div> 5 <div class="body"> 6 <button class="cancel" type="button">Oh, no, I misclicked</button> 7 <button class="confirm" type="button">Yes, I'm sure</button> 8 </div> 9 <div class="footer"> 10 This will irrevocably delete your account. 11 </div> 12</div>
So far so good. However, what if we want to also customize the header or footer, beyond passing in simple strings for those values? I might need to have an h2
in the header or have a fancy footer, with non-trivial markup.
One solution is to pass in (contextual) components and check in the component's template if they are. The main drawback of this approach is that you have to create components for such simple tasks (like passing in an h2
) which adds clutter to the project.
This is where named blocks can provide a cleaner solution.
Using named blocks
Since I used a modal dialog example in a previous blog post, let's practice learning by doing and convert it to use named blocks.
We start by creating a modal dialog component:
1$ ember g component modal-dialog
The modal dialog currently looks like the following:
Since we didn't write this to be a general solution for modal dialogs, there are use-case specific parts which need to be made customizable. This includes the title of the dialog, the confirm and the cancel actions, at least. A higher-level way to think about this is that we want to allow overwriting parts of the component itself: the header, the body, and the buttons. Let's bring over the template bits, keeping this in mind:
There's a lot to unpack, so let's go one by one. The great thing about modern Ember is that we know at a glance which properties are passed when invoking the component since those use the @
prefix: the named arguments of the component.
Let's start with the overlay:
@onOverlayClick
is the action that needs to be called when the overlay is clicked. To keep the template code more readable, we provide a default action, called noop
. It doesn't do anything, but because we can't conditionally add an event handler with on
, we'd have to duplicate the whole div
otherwise: once with the on
and once without it.
The noop
is just a helper with the following content:
Let's look at the next snippet from the template:
The {{yield to="header"}}
and {{yield to="body"}}
are the slots which can be filled in when calling the component (we'll soon see how).
The next template block shows another feature of named blocks:
With has-block
, we can check whether a block was passed for a specific name which enables us to provide a default implementation for the block.
In this case, the else
branch has a confirm and cancel button which can be customized by passing @buttonText
, @onConfirm
, and @onCancel
.
We know now that when calling the component can pass in a header
and body
block for the header and body content respectively. We can also pass in a buttons-panel
block. If we don't, two buttons will be rendered by default and we need to provide @buttonText
, @onConfirm
, and @onCancel
to make them work.
Our ModalDialog
is all set: let's start using it. Going back to the songs
template, let's replace the huge template chunk we had there with the following:
The <:header>
syntax is how you provide a named block: you write the name of the block after the leading :
.
Fortunately, the flow of deleting a song still works after extracting the component.
What (else) can you do with named blocks?
We've seen one example of using named blocks, a modal dialog. Named blocks are useful anywhere where it makes sense to provide several "slots" that the user of the component can customize. This can include tables, layouts, or button panels.
Can you think of some examples where named blocks are great to use? Or in cases, when they are not? Other questions or comments? Let me know on the discussion forum for the post.
Share on Twitter