Aurelia supports the <slot>
element provided via the HTML Web Components specification, which allows you to define placeholders in your HTML that can be replaced.
A lot of examples around seem to wrap a slot with a DIV, perhaps a class. The issue with this approach is if you have a styled DIV wrapping a slot and that slot is empty, your DIV will still be affecting the space around it. This is where the ability to check if a user has provided a slot or not helps.
This post was prompted by a StackOverflow question I answered here. It dawned on me this isn’t a straightforward solution and other people might find it valuable.
Take the following fictional example of a modal custom element. We have a header, body and footer. The body contains a slot without a name, but the header and footer are named slots.
<template>
<div class="modal__header">
<h1>Header:</h1>
<slot name="modalHeader"></slot>
</div>
<div class="modal__body">
<h1>Body:</h1>
<slot></slot>
</div>
<div class="modal__footer">
<h1>Footer:</h1>
<slot name="modalFooter"></slot>
</div>
</template>
The issue here as I mentioned earlier is a) styles on wrapping DIV’s affecting the layout even if the slot is empty and b) if the element wrapping your slot contains other HTML with your slot, it will show up even if no slot content is provided (as can be seen above).
What we want to do is conditionally check if a slot has user-provided content and show or hide it accordingly.
Accessing the controller instance
Aurelia creates an au
property on every tracked Aurelia node in the page, containing its view factory information, bindings and more. By accessing the controller and then the view
property of our controller, we can access our slots.
export class ModalCustomElement {
static inject = [Element];
constructor(element) {
this.element = element;
}
attached() {
this.$slots = this.element.au.controller.view.slots;
console.log(this.$slots);
}
}
Inside of the view-model for our fictional modal custom element, we inject the custom element instance and then we create a class property called $slots
where we pass in the view controller slots property containing our slots.
In our example, the $slots
property we created should contain three slots. Because the $slots
property is an object which is keyed by slot name, in our view we can determine if the user has provided slot data by checking the children
property.
This then gives us some HTML that looks like this:
<template>
<div class="modal__header" show.bind="$slots.modalHeader.children.length">
<h1>Header:</h1>
<slot name="modalHeader"></slot>
</div>
<div class="modal__body">
<h1>Body:</h1>
<slot></slot>
</div>
<div class="modal__footer" show.bind="$slots.modalFooter.children.length">
<h1>Footer:</h1>
<slot name="modalFooter"></slot>
</div>
</template>
For each slot, we check the children
property which is an array. We know if the length is zero, the user hasn’t provided any data for this slot. Default data inside of a slot will not be counted in the children
array, that lives elsewhere in the fallback slot property.
We are also accessing the slot via its name as it lives in the object. For the default slot with no provided name, its name internally is: __au-default-slot-key__
which can also be checked using a show.bind
.
Why if.bind
and not show.bind
?
You’ll notice we are using show.bind
and not if.bind
above. You can’t actually use if.bind
because it removes the slot from the controller and therefore, we can no longer check for it. When using show
the element remains but is hidden and our slot still exists.
Working example
If the above went over your head, the example I posted in the SO question is here where you can see DIV’s being shown/hidden depending on whether or not the user provides slot content.
This is great!
Speaking of slots, would you explain how to use them dynamically — the docs say that you can’t use them w/binding, so how would I have a set of slottable bits and use them dynamically?
Hey Dwayne, nice work resolving this issue.
I’ just tried it with typescript, but the property au doesn’t exist in the element. Do you know how to make this work using typescript?