Return to site

Web Components Multiple Slots

broken image


Jul 03, 2019 Multiple/Named Slots You can add multiple slots to a component, but if you do, all but one of them is required to have a name. If there is one without a name, it is the default slot. Here's how you create multiple slots. Angular ng-content and Content Projection: A Complete Guide - How To Use ng-content To Improve Component API Design Last Updated: 24 April 2020 localoffer Angular Core One of the Angular features that help us the most in building reusable components is Content Projection and ng-content.

Web Components Multiple Slots

Smashing Newsletter

Every week, we send out useful front-end & UX techniques. Subscribe and get the Smart Interface Design Checklists PDF delivered to your inbox.

Slots are a powerful tool for creating reusable components in Vue.js, though they aren't the simplest feature to understand. Let's take a look at how to use slots and some examples of how they can be used in your Vue applications.

With the recent release of Vue 2.6, the syntax for using slots has been made more succinct. This change to slots has gotten me re-interested in discovering the potential power of slots to provide reusability, new features, and clearer readability to our Vue-based projects. What are slots truly capable of?

If you're new to Vue or haven't seen the changes from version 2.6, read on. Probably the best resource for learning about slots is Vue's own documentation, but I'll try to give a rundown here.

What Are Slots?

Slots are a mechanism for Vue components that allows you to compose your components in a way other than the strict parent-child relationship. Slots give you an outlet to place content in new places or make components more generic. The best way to understand them is to see them in action. Let's start with a simple example:

This component has a wrapper div. Let's pretend that div is there to create a stylistic frame around its content. This component is able to be used generically to wrap a frame around any content you want. Let's see what it looks like to use it. The frame component here refers to the component we just made above.

The content that is between the opening and closing frame tags will get inserted into the frame component where the slot is, replacing the slot tags. This is the most basic way of doing it. You can also specify default content to go into a slot simply by filling it in:

So now if we use it like this instead:

The default text of 'This is the default content if nothing gets specified to go here' will show up, but if we use it as we did before, the default text will be overridden by the img tag.

Multiple/Named Slots

You can add multiple slots to a component, but if you do, all but one of them is required to have a name. If there is one without a name, it is the default slot. Here's how you create multiple slots:

We kept the same default slot, but this time we added a slot named header where you can enter a title. You use it like this:

Just like before, if we want to add content to the default slot, just put it directly inside the titled-frame component. To add content to a named slot, though, we needed to wrap the code in a template tag with a v-slot directive. You add a colon (:) after v-slot and then write the name of the slot you want the content to be passed to. Note that v-slot is new to Vue 2.6, so if you're using an older version, you'll need to read the docs about the deprecated slot syntax.

Scoped Slots

One more thing you'll need to know is that slots can pass data/functions down to their children. To demonstrate this, we'll need a completely different example component with slots, one that's even more contrived than the previous one: let's sorta copy the example from the docs by creating a component that supplies the data about the current user to its slots:

This component has a property called user with details about the user. By default, the component shows the user's last name, but note that it is using v-bind to bind the user data to the slot. With that, we can use this component to provide the user data to its descendant:

To get access to the data passed to the slot, we specify the name of the scope variable with the value of the v-slot directive.

There are a few notes to take here:

  • We specified the name of default, though we don't need to for the default slot. Instead we could just use v-slot='slotProps'.
  • You don't need to use slotProps as the name. You can call it whatever you want.
  • If you're only using a default slot, you can skip that inner template tag and put the v-slot directive directly onto the current-user tag.
  • You can use object destructuring to create direct references to the scoped slot data rather than using a single variable name. In other words, you can use v-slot='{user}' instead of v-slot='slotProps' and then you can use user directly instead of slotProps.user.

Taking those notes into account, the above example can be rewritten like this:

A couple more things to keep in mind:

  • You can bind more than one value with v-bind directives. So in the example, I could have done more than just user.
  • You can pass functions to scoped slots too. Many libraries use this to provide reusable functional components as you'll see later.
  • v-slot has an alias of #. So instead of writing v-slot:header='data', you can write #header='data'. You can also just specify #header instead of v-slot:header when you're not using scoped slots. As for default slots, you'll need to specify the name of default when you use the alias. In other words, you'll need to write #default='data' instead of #='data'.

There are a few more minor points you can learn about from the docs, but that should be enough to help you understand what we're talking about in the rest of this article.

What Can You Do With Slots?

Slots weren't built for a single purpose, or at least if they were, they've evolved way beyond that original intention to be a powerhouse tool for doing many different things.

Reusable Patterns

Components were always designed to be able to be reused, but some patterns aren't practical to enforce with a single 'normal' component because the number of props you'll need in order to customize it can be excessive or you'd need to pass large sections of content and potentially other components through the props. Slots can be used to encompass the 'outside' part of the pattern and allow other HTML and/or components to placed inside of them to customize the 'inside' part, allowing the component with slots to define the pattern and the components injected into the slots to be unique.

For our first example, let's start with something simple: a button. Imagine you and your team are using Bootstrap*. With Bootstrap, your buttons are often strapped with the base `btn` class and a class specifying the color, such as `btn-primary`. You can also add a size class, such as `btn-lg`.

* I neither encourage nor discourage you from doing this, I just needed something for my example and it's pretty well known.

Let's now assume, for simplicity's sake that your app/site always uses btn-primary and btn-lg. You don't want to always have to write all three classes on your buttons, or maybe you don't trust a rookie to remember to do all three. In that case, you can create a component that automatically has all three of those classes, but how do you allow customization of the content? A prop isn't practical because a button tag is allowed to have all kinds of HTML in it, so we should use a slot.

Now we can use it everywhere with whatever content you want:

Of course, you can go with something much bigger than a button. Sticking with Bootstrap, let's look at a modal, or least the HTML part; I won't be going into functionality… yet.

Now, let's use this:

The above type of use case for slots is obviously very useful, but it can do even more.

Reusing Functionality

Vue components aren't all about the HTML and CSS. They're built with JavaScript, so they're also about functionality. Slots can be useful for creating functionality once and using it in multiple places. Let's go back to our modal example and add a function that closes the modal:

Now when you use this component, you can add a button to the footer that can close the modal. Normally, in the case of a Bootstrap modal, you could just add data-dismiss='modal' to a button, but we want to hide Bootstrap specific things away from the components that will be slotting into this modal component. So we pass them a function they can call and they are none the wiser about Bootstrap's involvement:

Renderless Components

And finally, you can take what you know about using slots to pass around reusable functionality and strip practically all of the HTML and just use the slots. That's essentially what a renderless component is: a component that provides only functionality without any HTML.

Making components truly renderless can be a little tricky because you'll need to write render functions rather than using a template in order to remove the need for a root element, but it may not always be necessary. Let's take a look at a simple example that does let us use a template first, though:

This is an odd example of a renderless component because it doesn't even have any JavaScript in it. That's mostly because we're just creating a pre-configured reusable version of a built-in renderless function: transition.

Yup, Vue has built-in renderless components. This particular example is taken from an article on reusable transitions by Cristi Jora and shows a simple way to create a renderless component that can standardize the transitions used throughout your application. Cristi's article goes into a lot more depth and shows some more advanced variations of reusable transitions, so I recommend checking it out.

For our other example, we'll create a component that handles switching what is shown during the different states of a Promise: pending, successfully resolved, and failed. It's a common pattern and while it doesn't require a lot of code, it can muddy up a lot of your components if the logic isn't pulled out for reusability.

So what is going on here? First, note that we are receiving a prop called promise that is a Promise. In the watch section we watch for changes to the promise and when it changes (or immediately on component creation thanks to the immediate property) we clear the state, and call then and catch on the promise, updating the state when it either finishes successfully or fails.

Then, in the template, we show a different slot based on the state. Note that we failed to keep it truly renderless because we needed a root element in order to use a template. We're passing data and error to the relevant slot scopes as well.

And here's an example of it being used:

We pass in somePromise to the renderless component. While we're waiting for it to finish, we're displaying 'Working on it…' thanks to the pending slot. If it succeeds we display 'Resolved:' and the resolution value. If it fails we display 'Rejected:' and the error that caused the rejection. Now we no longer need to track the state of the promise within this component because that part is pulled out into its own reusable component.

So, what can we do about that span wrapping around the slots in promised.vue? To remove it, we'll need to remove the template portion and add a render function to our component:

There isn't anything too tricky going on here. We're just using some if blocks to find the state and then returning the correct scoped slot (via this.$scopedSlots['SLOTNAME'](...)) and passing the relevant data to the slot scope. When you're not using a template, you can skip using the .vue file extension by pulling the JavaScript out of the script tag and just plunking it into a .js file. This should give you a very slight performance bump when compiling those Vue files.

This example is a stripped-down and slightly tweaked version of vue-promised, which I would recommend over using the above example because they cover over some potential pitfalls. There are plenty of other great examples of renderless components out there too. Baleada is an entire library full of renderless components that provide useful functionality like this. There's also vue-virtual-scroller for controlling the rendering of list item based on what is visible on the screen or PortalVue for 'teleporting' content to completely different parts of the DOM.

I'm Out

Vue's slots take component-based development to a whole new level, and while I've demonstrated a lot of great ways slots can be used, there are countless more out there. What great idea can you think of? What ways do you think slots could get an upgrade? If you have any, make sure to bring your ideas to the Vue team. God bless and happy coding.

(dm, il)

One of the Angular features that help us the most in building reusable components is Content Projection and ng-content.

In this post, we are going to learn how to use this feature to design components that have a very simple but still powerful API - the post is as much about component design as it's about content projection!

In order to learn content projection, let's use it build a small component (a Font Awesome Input Box). We are going to see how content projection works, when to use it and why, and how it can improve a lot the design of some of our components.

The final component that we are about to build is available here in the Angular Package Format.

Table Of Contents

In this post we will cover the following topics:

  • What Problem is Content Projection Trying to Solve?
  • An example of a component that would benefit from content projection
  • Component Design Problem 1 - Supporting all the HTML Properties of an HTML Input
  • Component Design Problem 2 - Integration with Angular Forms
  • Component Design Problem 3 - Capturing plain browser events of elements inside a template
  • Component Design Problem 4 - Custom third party input properties
  • The Key Problem With The Initial Design
  • Designing the Same Component Using Content Projection
  • How To apply styles to elements projected via ng-content
  • Interacting with Projected Content (inside ng-content)
  • Multi-Slot Content Projection
  • Conclusions

What Problem is Content Projection Trying to Solve?

Let's start at the beginning: in order to understand content projection, we need to first understand what set of problems is the feature trying to solve.

This is the best way to make sure that we will not misuse the feature as well! So let's implement a small component without using content projection, and see what problems we run into.

What We Are About To Build

Our Font Awesome Input Box component is designed to look and feel just like a plain HTML Input, except that it has a small Icon inside the text box.

The icon can be any of the icons available in the Font Awesome open source version, let's have a look at the component!

Encapsulating a common HTML Pattern

Adding an icon inside an input box is a very common HTML pattern that makes the input much easier to identify by the user. For example, have a look at the following text boxes:

Notice that with the presence of both the icon and the text placeholder, we hardly need to have also the field label to the left, which is especially useful on mobile.

How Does This Component Work?

As we know, normal HTML inputs cannot display an image. But this component does look like a native input, as it also has a blue focus border and supports Tab/ Shift-Tab.

So how does this work? The component is internally implemented using this very common HTML pattern:

  • Inside the component, there is a plain HTML input and a icon, wrapped in a DIV
  • we have hidden the borders of the plain HTML input, and we have added some similar looking borders to the wrapping DIV
  • we have then detected the focus and blur events in the input field, and we have used those events to add and remove a focus border to the wrapping DIV

So as you can see, we still had to use a couple of tricks to make this component look and behave as a plain HTML input!

Web components multiple slots games

Design Goals

Let's take this very common HTML pattern and make it available as an Angular component. We would like the component to be:

  • easily combinable with other Angular components and directives
  • have good integration with Angular Forms

With this in mind, let's have a look at an initial attempt of implementing this design without content projection, and see what problems we run into.

Let's first see how the component would be used:

So as we can see, the component is a custom HTML element named fa-input, that takes as input an icon name, and outputs the values of the text box.

This is a pretty natural choice for implementing this component, and it's likely the design we could come up on a first attempt.

But there are several major problems with this design, let's have a look at how the component was implemented to understand why, and then see how Content Projection will provide a clean solution for those issues.

Component API Design - An Initial Attempt

This is the initial implementation of our component:

Try to guess what is the biggest problem of this design, while we break down this implementation step-by-step.

As we can see on the template, the main idea is that the component is made of an icon and a plain HTML input.

Before going over the component class implementation, let's have a look at its styling:

Based on these styles, we can see that:

  • the HTML input inside the component had its borders and outline removed
  • but we added a similar border to the host element, creating the illusion that the component is a plain HTML input
  • the focus of the input is simulated by adding a focus css class to the host element itself

Let's go back to the component class and see how all the pieces of the puzzle are glued together:

  • as part of the public API of the component we have a icon string property that defines which icon should be shown (an envelope, a lock, etc.)
  • the component has a custom output event named value, that emits new values whenever the value of the text input changes
  • to implement the focus functionality, we are detecting the focus and blur events on the native html input, and based on that we add or remove the focus CSS class on the host element via @HostBinding

And this implementation does work! But if we start using this component in our application, we will quickly run into a series of problems. We are going to list here 4 of them, starting with:

Component Design Problem 1 - Supporting all the HTML Properties of an HTML Input

Our component is meant to be used in place of a plain HTML input, but it does not support any of its standard properties. For example, this is a plain input of type email with autocompletion turned off and a placeholder:

All these standard browser properties are not supported by our component, and these are only a few of the properties that have this problem.

There are currently 31 HTML properties listed at W3schools for inputs and this does not include all the HTML ARIA Accessibility attributes.

To support all these attributes we would have to do something like this:

So, in summary, we would have to forward all these component input properties to the internal HTML text box used inside the template.

This would be quite cumbersome but still doable. The issue is that there are other related problems with the current design of this component.

Component Design Problem 2 - Integration with Angular Forms

Another problem is, what if we would like this input to be part of an Angular Form?
In that case, we would have to also forward all the form properties such as for example formControlName to the plain input as well.

Component Design Problem 3 - Detection of plain browser events

What if we would like to detect a standard browser DOM event on that input? Such as for example the keydown event?

This could still be solved by bubbling all the events that don't bubble by default from the input up the component tree, and provide a similarly named event at the level of the component.

This would be quite cumbersome but still doable. But now, we get into a situation for which we don't have a good workaround for.

Component Design Problem 4 - Custom third party properties

While building forms, third party systems might expect certain HTML custom data- properties to be filled in, for scenarios where a full page submission occurs (instead of sending an Ajax request).

This would prove even more troublesome to solve than the situations before, because we don't know upfront the name of those properties.

At this point, we can see that there is a large variety of very common use cases that are not well supported by this design.

So what is the major problem with this component design?

The Key Problem With This Design

The key issue is that we are hiding the HTML input inside the component template.

Web Components Multiple Slots Machines

Due to that, we are creating a barrier between the external template that knows the custom properties that need to be applied to the input and the plain HTML input itself.

and that is the root cause of all the design problems listed above!

Hiding the input inside the component template causes a whole set of issues, because we need to forward properties and events in and out of the template to support many use cases.

The good news is that using content projection we will be able to support all these use cases, and much more.

Designing the Same Component Using Content Projection

Let's now redesign the API of this component. Instead of hiding the input element inside the component, let's provide it as a content element of the component itself:

Notice that we did not provide the form text field as a component input property. Instead, we have added it in the content part of the fa-input custom HTML tag.

This type of API is actually very common in several standard HTML elements, such as for example select boxes or lists:

Angular Core does allow us to do something similar in our components!

We can actually query anything in the content part of the component HTML tag and use it in the internal template as a configuration API, using the @ContentChild and @ContentChildren decorators.

But we can do more than that, we can also if necessary take anything that is inside the content section, and use it directly inside the component.

This means that we can take the HTML input that is inside the fa-input content part, and use it directly inside the Font Awesome template, by projecting it using the ng-content Angular Core Directive:

This new version of the component is still incomplete: it does not support yet the focus simulation functionality.

But it solves all the problems listed above, because we have direct access to the HTML input!

Also, this new version has accidentally created a new problem - have a look at the input box now:

Do you notice the duplicate border? It looks the styling that we had put in place to remove the border from the HTML input is not working anymore!

Also, the focus functionality is missing.

It looks like despite the several problems that we have solved by using ng-content, we still have at this point two major questions about it:

  • how to style projected content?
  • how to interact with projected content?

How To Apply styles to elements projected via ng-content

Let's start by understanding why the styles we had in place no longer work with ng-content. The current input styles look like this, and we can find them inside the fa-input.component.css file:

Here is why this does not work anymore: because these styles sit inside a file linked to the component, they will have applied at startup time an attribute that is unique to all the HTML elements inside that particular component template:

So what is this strange identifier? Let's have a look at the runtime HTML on the page for our component:

This is a simplified version of the HTML, that helps better understand what is going on:

  • we can see that each element of fa-input template, in this case, the icon tag gets applied a _ngcontent-c0 attribute, which is unique to this component
  • all the styles of the component are then scoped to only elements containing this attribute
  • which means that the styles of the component will NOT affect the projected input, because if you notice it does not have the special attribute _ngcontent-c0
  • this is normal because the input comes from another template other than the fa-input template

Styling projected content

In order to style the projected input and remove the double border, we need to change the styles to something like this:

So how do these new styles work? Let's break this down:

  • we are prefixing the style with the :host selector, meaning that the styles will be applied only inside this component
  • we are then applying the ::ng-deep modifier, which means that the style will no longer be scoped only to HTML elements of this particular component, but it will also affect any descendant elements

To see how this works in practice, this is the actual CSS at runtime:

Web Components Multiple Slots Real Money

So as we can see, the styles are still scoped to the component only, but they will leak through to any inputs inside the component at runtime: including the projected HTML input!

So this shows how to style projected content if necessary. Now let's fix the second part of the puzzle: how to interact with projected content, and simulate the focus functionality?

Interacting with Projected Content inside ng-content

To be able to simulate the focus functionality, we need the fa-input input component to know that the projected input focus was either activated or blurred.

We cannot interact with the ng-content tag, and for example define event listeners on top of it.

Instead, the best way to interact with the projected input is to start by applying a new separate directive to the input.

Let's then create a directive named inputRef, and apply it to the HTML Input:

We will take the opportunity to use that same directive to track if the input has the focus or not:

Here is what is going on in this directive:

  • we have defined a focus property, that will be true or false depending if the native Input to which the directive was applied has the focus or not
  • the native focus and blur DOM events are being detected using the @HostListener decorator

We can now use this directive and have it injected inside the Font Awesome Input component, effectively allowing us to interact with projected content!

Let's see what that would look like:

Web Components Multiple Slots Real Money

As we can see, we have simply used the @ContentChild decorator to inject the inputRef directive inside the Font Awesome Input component.

Web components multiple slots folders

Then using this directive and the boolean focus property, we have then set the CSS class named focus on the host elements using the @HostBinding decorator.

With this new implementation in place, we have now a fully functioning component, that is super-simple to use and supports implicitly all the HTML input properties, accessibility, third party properties and Angular Forms - all of that made possible by the use of content projection.

In the current implementation, we have so far been projecting the whole content of fa-input. But what if we would like to project only part of it?

Web Components Multiple Slots

Smashing Newsletter

Every week, we send out useful front-end & UX techniques. Subscribe and get the Smart Interface Design Checklists PDF delivered to your inbox.

Slots are a powerful tool for creating reusable components in Vue.js, though they aren't the simplest feature to understand. Let's take a look at how to use slots and some examples of how they can be used in your Vue applications.

With the recent release of Vue 2.6, the syntax for using slots has been made more succinct. This change to slots has gotten me re-interested in discovering the potential power of slots to provide reusability, new features, and clearer readability to our Vue-based projects. What are slots truly capable of?

If you're new to Vue or haven't seen the changes from version 2.6, read on. Probably the best resource for learning about slots is Vue's own documentation, but I'll try to give a rundown here.

What Are Slots?

Slots are a mechanism for Vue components that allows you to compose your components in a way other than the strict parent-child relationship. Slots give you an outlet to place content in new places or make components more generic. The best way to understand them is to see them in action. Let's start with a simple example:

This component has a wrapper div. Let's pretend that div is there to create a stylistic frame around its content. This component is able to be used generically to wrap a frame around any content you want. Let's see what it looks like to use it. The frame component here refers to the component we just made above.

The content that is between the opening and closing frame tags will get inserted into the frame component where the slot is, replacing the slot tags. This is the most basic way of doing it. You can also specify default content to go into a slot simply by filling it in:

So now if we use it like this instead:

The default text of 'This is the default content if nothing gets specified to go here' will show up, but if we use it as we did before, the default text will be overridden by the img tag.

Multiple/Named Slots

You can add multiple slots to a component, but if you do, all but one of them is required to have a name. If there is one without a name, it is the default slot. Here's how you create multiple slots:

We kept the same default slot, but this time we added a slot named header where you can enter a title. You use it like this:

Just like before, if we want to add content to the default slot, just put it directly inside the titled-frame component. To add content to a named slot, though, we needed to wrap the code in a template tag with a v-slot directive. You add a colon (:) after v-slot and then write the name of the slot you want the content to be passed to. Note that v-slot is new to Vue 2.6, so if you're using an older version, you'll need to read the docs about the deprecated slot syntax.

Scoped Slots

One more thing you'll need to know is that slots can pass data/functions down to their children. To demonstrate this, we'll need a completely different example component with slots, one that's even more contrived than the previous one: let's sorta copy the example from the docs by creating a component that supplies the data about the current user to its slots:

This component has a property called user with details about the user. By default, the component shows the user's last name, but note that it is using v-bind to bind the user data to the slot. With that, we can use this component to provide the user data to its descendant:

To get access to the data passed to the slot, we specify the name of the scope variable with the value of the v-slot directive.

There are a few notes to take here:

  • We specified the name of default, though we don't need to for the default slot. Instead we could just use v-slot='slotProps'.
  • You don't need to use slotProps as the name. You can call it whatever you want.
  • If you're only using a default slot, you can skip that inner template tag and put the v-slot directive directly onto the current-user tag.
  • You can use object destructuring to create direct references to the scoped slot data rather than using a single variable name. In other words, you can use v-slot='{user}' instead of v-slot='slotProps' and then you can use user directly instead of slotProps.user.

Taking those notes into account, the above example can be rewritten like this:

A couple more things to keep in mind:

  • You can bind more than one value with v-bind directives. So in the example, I could have done more than just user.
  • You can pass functions to scoped slots too. Many libraries use this to provide reusable functional components as you'll see later.
  • v-slot has an alias of #. So instead of writing v-slot:header='data', you can write #header='data'. You can also just specify #header instead of v-slot:header when you're not using scoped slots. As for default slots, you'll need to specify the name of default when you use the alias. In other words, you'll need to write #default='data' instead of #='data'.

There are a few more minor points you can learn about from the docs, but that should be enough to help you understand what we're talking about in the rest of this article.

What Can You Do With Slots?

Slots weren't built for a single purpose, or at least if they were, they've evolved way beyond that original intention to be a powerhouse tool for doing many different things.

Reusable Patterns

Components were always designed to be able to be reused, but some patterns aren't practical to enforce with a single 'normal' component because the number of props you'll need in order to customize it can be excessive or you'd need to pass large sections of content and potentially other components through the props. Slots can be used to encompass the 'outside' part of the pattern and allow other HTML and/or components to placed inside of them to customize the 'inside' part, allowing the component with slots to define the pattern and the components injected into the slots to be unique.

For our first example, let's start with something simple: a button. Imagine you and your team are using Bootstrap*. With Bootstrap, your buttons are often strapped with the base `btn` class and a class specifying the color, such as `btn-primary`. You can also add a size class, such as `btn-lg`.

* I neither encourage nor discourage you from doing this, I just needed something for my example and it's pretty well known.

Let's now assume, for simplicity's sake that your app/site always uses btn-primary and btn-lg. You don't want to always have to write all three classes on your buttons, or maybe you don't trust a rookie to remember to do all three. In that case, you can create a component that automatically has all three of those classes, but how do you allow customization of the content? A prop isn't practical because a button tag is allowed to have all kinds of HTML in it, so we should use a slot.

Now we can use it everywhere with whatever content you want:

Of course, you can go with something much bigger than a button. Sticking with Bootstrap, let's look at a modal, or least the HTML part; I won't be going into functionality… yet.

Now, let's use this:

The above type of use case for slots is obviously very useful, but it can do even more.

Reusing Functionality

Vue components aren't all about the HTML and CSS. They're built with JavaScript, so they're also about functionality. Slots can be useful for creating functionality once and using it in multiple places. Let's go back to our modal example and add a function that closes the modal:

Now when you use this component, you can add a button to the footer that can close the modal. Normally, in the case of a Bootstrap modal, you could just add data-dismiss='modal' to a button, but we want to hide Bootstrap specific things away from the components that will be slotting into this modal component. So we pass them a function they can call and they are none the wiser about Bootstrap's involvement:

Renderless Components

And finally, you can take what you know about using slots to pass around reusable functionality and strip practically all of the HTML and just use the slots. That's essentially what a renderless component is: a component that provides only functionality without any HTML.

Making components truly renderless can be a little tricky because you'll need to write render functions rather than using a template in order to remove the need for a root element, but it may not always be necessary. Let's take a look at a simple example that does let us use a template first, though:

This is an odd example of a renderless component because it doesn't even have any JavaScript in it. That's mostly because we're just creating a pre-configured reusable version of a built-in renderless function: transition.

Yup, Vue has built-in renderless components. This particular example is taken from an article on reusable transitions by Cristi Jora and shows a simple way to create a renderless component that can standardize the transitions used throughout your application. Cristi's article goes into a lot more depth and shows some more advanced variations of reusable transitions, so I recommend checking it out.

For our other example, we'll create a component that handles switching what is shown during the different states of a Promise: pending, successfully resolved, and failed. It's a common pattern and while it doesn't require a lot of code, it can muddy up a lot of your components if the logic isn't pulled out for reusability.

So what is going on here? First, note that we are receiving a prop called promise that is a Promise. In the watch section we watch for changes to the promise and when it changes (or immediately on component creation thanks to the immediate property) we clear the state, and call then and catch on the promise, updating the state when it either finishes successfully or fails.

Then, in the template, we show a different slot based on the state. Note that we failed to keep it truly renderless because we needed a root element in order to use a template. We're passing data and error to the relevant slot scopes as well.

And here's an example of it being used:

We pass in somePromise to the renderless component. While we're waiting for it to finish, we're displaying 'Working on it…' thanks to the pending slot. If it succeeds we display 'Resolved:' and the resolution value. If it fails we display 'Rejected:' and the error that caused the rejection. Now we no longer need to track the state of the promise within this component because that part is pulled out into its own reusable component.

So, what can we do about that span wrapping around the slots in promised.vue? To remove it, we'll need to remove the template portion and add a render function to our component:

There isn't anything too tricky going on here. We're just using some if blocks to find the state and then returning the correct scoped slot (via this.$scopedSlots['SLOTNAME'](...)) and passing the relevant data to the slot scope. When you're not using a template, you can skip using the .vue file extension by pulling the JavaScript out of the script tag and just plunking it into a .js file. This should give you a very slight performance bump when compiling those Vue files.

This example is a stripped-down and slightly tweaked version of vue-promised, which I would recommend over using the above example because they cover over some potential pitfalls. There are plenty of other great examples of renderless components out there too. Baleada is an entire library full of renderless components that provide useful functionality like this. There's also vue-virtual-scroller for controlling the rendering of list item based on what is visible on the screen or PortalVue for 'teleporting' content to completely different parts of the DOM.

I'm Out

Vue's slots take component-based development to a whole new level, and while I've demonstrated a lot of great ways slots can be used, there are countless more out there. What great idea can you think of? What ways do you think slots could get an upgrade? If you have any, make sure to bring your ideas to the Vue team. God bless and happy coding.

(dm, il)

One of the Angular features that help us the most in building reusable components is Content Projection and ng-content.

In this post, we are going to learn how to use this feature to design components that have a very simple but still powerful API - the post is as much about component design as it's about content projection!

In order to learn content projection, let's use it build a small component (a Font Awesome Input Box). We are going to see how content projection works, when to use it and why, and how it can improve a lot the design of some of our components.

The final component that we are about to build is available here in the Angular Package Format.

Table Of Contents

In this post we will cover the following topics:

  • What Problem is Content Projection Trying to Solve?
  • An example of a component that would benefit from content projection
  • Component Design Problem 1 - Supporting all the HTML Properties of an HTML Input
  • Component Design Problem 2 - Integration with Angular Forms
  • Component Design Problem 3 - Capturing plain browser events of elements inside a template
  • Component Design Problem 4 - Custom third party input properties
  • The Key Problem With The Initial Design
  • Designing the Same Component Using Content Projection
  • How To apply styles to elements projected via ng-content
  • Interacting with Projected Content (inside ng-content)
  • Multi-Slot Content Projection
  • Conclusions

What Problem is Content Projection Trying to Solve?

Let's start at the beginning: in order to understand content projection, we need to first understand what set of problems is the feature trying to solve.

This is the best way to make sure that we will not misuse the feature as well! So let's implement a small component without using content projection, and see what problems we run into.

What We Are About To Build

Our Font Awesome Input Box component is designed to look and feel just like a plain HTML Input, except that it has a small Icon inside the text box.

The icon can be any of the icons available in the Font Awesome open source version, let's have a look at the component!

Encapsulating a common HTML Pattern

Adding an icon inside an input box is a very common HTML pattern that makes the input much easier to identify by the user. For example, have a look at the following text boxes:

Notice that with the presence of both the icon and the text placeholder, we hardly need to have also the field label to the left, which is especially useful on mobile.

How Does This Component Work?

As we know, normal HTML inputs cannot display an image. But this component does look like a native input, as it also has a blue focus border and supports Tab/ Shift-Tab.

So how does this work? The component is internally implemented using this very common HTML pattern:

  • Inside the component, there is a plain HTML input and a icon, wrapped in a DIV
  • we have hidden the borders of the plain HTML input, and we have added some similar looking borders to the wrapping DIV
  • we have then detected the focus and blur events in the input field, and we have used those events to add and remove a focus border to the wrapping DIV

So as you can see, we still had to use a couple of tricks to make this component look and behave as a plain HTML input!

Design Goals

Let's take this very common HTML pattern and make it available as an Angular component. We would like the component to be:

  • easily combinable with other Angular components and directives
  • have good integration with Angular Forms

With this in mind, let's have a look at an initial attempt of implementing this design without content projection, and see what problems we run into.

Let's first see how the component would be used:

So as we can see, the component is a custom HTML element named fa-input, that takes as input an icon name, and outputs the values of the text box.

This is a pretty natural choice for implementing this component, and it's likely the design we could come up on a first attempt.

But there are several major problems with this design, let's have a look at how the component was implemented to understand why, and then see how Content Projection will provide a clean solution for those issues.

Component API Design - An Initial Attempt

This is the initial implementation of our component:

Try to guess what is the biggest problem of this design, while we break down this implementation step-by-step.

As we can see on the template, the main idea is that the component is made of an icon and a plain HTML input.

Before going over the component class implementation, let's have a look at its styling:

Based on these styles, we can see that:

  • the HTML input inside the component had its borders and outline removed
  • but we added a similar border to the host element, creating the illusion that the component is a plain HTML input
  • the focus of the input is simulated by adding a focus css class to the host element itself

Let's go back to the component class and see how all the pieces of the puzzle are glued together:

  • as part of the public API of the component we have a icon string property that defines which icon should be shown (an envelope, a lock, etc.)
  • the component has a custom output event named value, that emits new values whenever the value of the text input changes
  • to implement the focus functionality, we are detecting the focus and blur events on the native html input, and based on that we add or remove the focus CSS class on the host element via @HostBinding

And this implementation does work! But if we start using this component in our application, we will quickly run into a series of problems. We are going to list here 4 of them, starting with:

Component Design Problem 1 - Supporting all the HTML Properties of an HTML Input

Our component is meant to be used in place of a plain HTML input, but it does not support any of its standard properties. For example, this is a plain input of type email with autocompletion turned off and a placeholder:

All these standard browser properties are not supported by our component, and these are only a few of the properties that have this problem.

There are currently 31 HTML properties listed at W3schools for inputs and this does not include all the HTML ARIA Accessibility attributes.

To support all these attributes we would have to do something like this:

So, in summary, we would have to forward all these component input properties to the internal HTML text box used inside the template.

This would be quite cumbersome but still doable. The issue is that there are other related problems with the current design of this component.

Component Design Problem 2 - Integration with Angular Forms

Another problem is, what if we would like this input to be part of an Angular Form?
In that case, we would have to also forward all the form properties such as for example formControlName to the plain input as well.

Component Design Problem 3 - Detection of plain browser events

What if we would like to detect a standard browser DOM event on that input? Such as for example the keydown event?

This could still be solved by bubbling all the events that don't bubble by default from the input up the component tree, and provide a similarly named event at the level of the component.

This would be quite cumbersome but still doable. But now, we get into a situation for which we don't have a good workaround for.

Component Design Problem 4 - Custom third party properties

While building forms, third party systems might expect certain HTML custom data- properties to be filled in, for scenarios where a full page submission occurs (instead of sending an Ajax request).

This would prove even more troublesome to solve than the situations before, because we don't know upfront the name of those properties.

At this point, we can see that there is a large variety of very common use cases that are not well supported by this design.

So what is the major problem with this component design?

The Key Problem With This Design

The key issue is that we are hiding the HTML input inside the component template.

Web Components Multiple Slots Machines

Due to that, we are creating a barrier between the external template that knows the custom properties that need to be applied to the input and the plain HTML input itself.

and that is the root cause of all the design problems listed above!

Hiding the input inside the component template causes a whole set of issues, because we need to forward properties and events in and out of the template to support many use cases.

The good news is that using content projection we will be able to support all these use cases, and much more.

Designing the Same Component Using Content Projection

Let's now redesign the API of this component. Instead of hiding the input element inside the component, let's provide it as a content element of the component itself:

Notice that we did not provide the form text field as a component input property. Instead, we have added it in the content part of the fa-input custom HTML tag.

This type of API is actually very common in several standard HTML elements, such as for example select boxes or lists:

Angular Core does allow us to do something similar in our components!

We can actually query anything in the content part of the component HTML tag and use it in the internal template as a configuration API, using the @ContentChild and @ContentChildren decorators.

But we can do more than that, we can also if necessary take anything that is inside the content section, and use it directly inside the component.

This means that we can take the HTML input that is inside the fa-input content part, and use it directly inside the Font Awesome template, by projecting it using the ng-content Angular Core Directive:

This new version of the component is still incomplete: it does not support yet the focus simulation functionality.

But it solves all the problems listed above, because we have direct access to the HTML input!

Also, this new version has accidentally created a new problem - have a look at the input box now:

Do you notice the duplicate border? It looks the styling that we had put in place to remove the border from the HTML input is not working anymore!

Also, the focus functionality is missing.

It looks like despite the several problems that we have solved by using ng-content, we still have at this point two major questions about it:

  • how to style projected content?
  • how to interact with projected content?

How To Apply styles to elements projected via ng-content

Let's start by understanding why the styles we had in place no longer work with ng-content. The current input styles look like this, and we can find them inside the fa-input.component.css file:

Here is why this does not work anymore: because these styles sit inside a file linked to the component, they will have applied at startup time an attribute that is unique to all the HTML elements inside that particular component template:

So what is this strange identifier? Let's have a look at the runtime HTML on the page for our component:

This is a simplified version of the HTML, that helps better understand what is going on:

  • we can see that each element of fa-input template, in this case, the icon tag gets applied a _ngcontent-c0 attribute, which is unique to this component
  • all the styles of the component are then scoped to only elements containing this attribute
  • which means that the styles of the component will NOT affect the projected input, because if you notice it does not have the special attribute _ngcontent-c0
  • this is normal because the input comes from another template other than the fa-input template

Styling projected content

In order to style the projected input and remove the double border, we need to change the styles to something like this:

So how do these new styles work? Let's break this down:

  • we are prefixing the style with the :host selector, meaning that the styles will be applied only inside this component
  • we are then applying the ::ng-deep modifier, which means that the style will no longer be scoped only to HTML elements of this particular component, but it will also affect any descendant elements

To see how this works in practice, this is the actual CSS at runtime:

Web Components Multiple Slots Real Money

So as we can see, the styles are still scoped to the component only, but they will leak through to any inputs inside the component at runtime: including the projected HTML input!

So this shows how to style projected content if necessary. Now let's fix the second part of the puzzle: how to interact with projected content, and simulate the focus functionality?

Interacting with Projected Content inside ng-content

To be able to simulate the focus functionality, we need the fa-input input component to know that the projected input focus was either activated or blurred.

We cannot interact with the ng-content tag, and for example define event listeners on top of it.

Instead, the best way to interact with the projected input is to start by applying a new separate directive to the input.

Let's then create a directive named inputRef, and apply it to the HTML Input:

We will take the opportunity to use that same directive to track if the input has the focus or not:

Here is what is going on in this directive:

  • we have defined a focus property, that will be true or false depending if the native Input to which the directive was applied has the focus or not
  • the native focus and blur DOM events are being detected using the @HostListener decorator

We can now use this directive and have it injected inside the Font Awesome Input component, effectively allowing us to interact with projected content!

Let's see what that would look like:

Web Components Multiple Slots Real Money

As we can see, we have simply used the @ContentChild decorator to inject the inputRef directive inside the Font Awesome Input component.

Then using this directive and the boolean focus property, we have then set the CSS class named focus on the host elements using the @HostBinding decorator.

With this new implementation in place, we have now a fully functioning component, that is super-simple to use and supports implicitly all the HTML input properties, accessibility, third party properties and Angular Forms - all of that made possible by the use of content projection.

In the current implementation, we have so far been projecting the whole content of fa-input. But what if we would like to project only part of it?

Multi-Slot Content Projection

Let's now say that we would like to project not only the HTML input itself, but also the icon inside the input. This is also possible using content projection.

In the content part of the the fa-input tag we can put multiple types of content, for example:

We can then consume the different types of content available inside the fa-input tag, by using the select property of ng-content:

These two selectors are looking for a particular element type (an input or an icon tag), but we could also look for an element with a given CSS class, and combine multiple selectors.

For example, this selector would look for inputs with a given CSS class named test-class:

It's also possible to capture content that does not match any selector. For example, this would also inject the input element into the template:

In this context, the tag without any selector would fetch all the content that did not match any of the other selectors:

In this case, that would mean all the content which is not an icon tag, which would be the HTML Input.

As we have seen, it's as important to know how the ng-content core directive works, as to know the typical scenarios and use cases on which we would like to use it.

The ng-content core directive allows for component designs where certain internal details of the template are not hidden inside the component but instead are provided as an input, which in certain cases really simplifies the design.

I hope you enjoyed the post. I invite you to subscribe to our newsletter to get timely Angular News, Free courses and PDFs:

And if you would like to know about more advanced Angular Core features, we recommend checking the Angular Core Deep Dive course, where content projection is covered in much more detail.

If you are just getting started learning Angular, have a look at the Angular for Beginners Course:

Other posts on Angular

If you enjoyed this post, have also a look also at other popular posts that you might find interesting:





broken image