This is how the DOM constructs nodes as parents and children. Notice how the <li> is distinctly below the <ul> tag rather than a syntax like:
<!-- This isn't correct HTML to do what we want --><div ul=" li='One' li='Two' li='Three' "/>
While the above looks strange and counter-intuitive, let's look at how we define the same list if each element is a dedicated component using the methods we've created thus far:
<!-- Container.vue --><script setup>import List from "./List.vue";</script><template> <div> <List /> </div></template>
This is fairly similar to that strange fake nested HTML syntax. The alternative component usage syntax that matches closer to the DOM might otherwise look like this:
<Component> <OtherComponent /></Component>
This mismatch occurs because if we look at how our components are defined, we're building out our previous components deeply rather than broadly.
This is the difference between building apps with HTML alone and building them with a frontend framework; while the DOM is typically thought of as one-dimensional, there are really two dimensions that are exposed more thoroughly by the framework's ability to construct this tree in a more fine-grained manner.
Let's move the component tree back to being breadth first by using a feature that may sound familiar: Passing children.
Passing Basic Children
Before we explore passing children with our frameworks, let's first think of a potential use case for when we want to do this.
For example, say you want the button to have a "pressed" effect whenever you click on it. Then, when you click on it for a second time, it unclicks. This might look something like the following:
Here, we're passing text as a string property to assign text. But oh no! What if we wanted to add a span inside of the button to add bolded text? After all, if you pass Hello, <span>world</span>!, it wouldn't render the span, but instead render the <span> as text.
Instead, let's allow the parent of our ToggleButton to pass in a template that's then rendered into the component.
React
Angular
Vue
In React, JSX that's passed as a child to a component can be accessed through a special children component property name:
// "children" is a preserved property name by React. It reflects passed child nodesconst ToggleButton = ({ children }) => { const [pressed, setPressed] = useState(false); return ( <button onClick={() => setPressed(!pressed)} style={{ backgroundColor: pressed ? "black" : "white", color: pressed ? "white" : "black", }} type="button" aria-pressed={pressed} > {/* We then utilize this special property name as any */} {/* other JSX variable to display its contents */} {children} </button> );};const ToggleButtonList = () => { return ( <> <ToggleButton> Hello <span style={{ fontWeight: "bold" }}>world</span>! </ToggleButton> <ToggleButton>Hello other friends!</ToggleButton> </> );};
An embedded webpage:React Passing Basic Children - StackBlitz
Because slot is a built-in component of Vue, we do not need to import it from the vue package.
Here, we're able to pass a span and other elements directly to our ToggleButton component as children.
Using Other Framework Features with Component Children
However, because these templates have the full power of the frameworks at their disposal, these children have superpowers! Let's add a for loop into our children's template to say hello to all of our friends:
While passing one set of elements is useful in its own right, many components require there to be more than one "slot" of data you can pass.
For example, take this dropdown component:
Let's build this dropdown component
These tend to be useful for FAQ pages, hidden content, and more!
This dropdown component has two potential places where passing elements would be beneficial:
<Dropdown> <DropdownHeader>Let's build this dropdown component</DropdownHeader> <DropdownBody> These tend to be useful for FAQ pages, hidden contents, and more! </DropdownBody></Dropdown>
Let's build this component with an API similar to the above using "named children."
React
Angular
Vue
Something worth reminding is that JSX constructs a value, just like a number or string, that you can then store to a variable.
const table = <p>Test</p>;
This can be passed to a function, like console.log, or anything any other JavaScript value can do.
console.log(<p>Test</p>); // ReactElement
Because of this behavior, to pass more than one JSX value to a component, we can use function parameters and pass them that way.
ng-content allows you to pass a select property to have specific children projected in dedicated locations. This select property takes CSS selector query values. Knowing this, we can pass the attribute query for header by wrapping the attribute name in square brackets like so:
Once ng-content finds related elements that match the select query, they will be content projected into the appropriate locations. If not matched by a ng-content[select], they will be projected to a non select enabled ng-content.
Similar to how Angular's ng-content[select] query works, Vue allows you to pass a name to the slot component to a projected and named content.
<!-- App.vue --><script setup>import { ref } from "vue";import Dropdown from "./Dropdown.vue";const expanded = ref(false);</script><template> <Dropdown :expanded="expanded" @toggle="expanded = !expanded"> <template v-slot:header>Let's build this dropdown component</template> These tend to be useful for FAQ pages, hidden contents, and more! </Dropdown></template>
Here, we can see that slot is querying for a header template slot. This query is then satisfied by App's template for the heading template element.
v-slot also has a shorthand of #, similar to how v-bind has a shorthand of :. Using this shorthand, we can modify our App component to look like:
<!-- App.vue --><script setup>import { ref } from "vue";import Dropdown from "./Dropdown.vue";const expanded = ref(false);</script><template> <Dropdown :expanded="expanded" @toggle="expanded = !expanded"> <template #header>Let's build this dropdown component</template> These tend to be useful for FAQ pages, hidden contents, and more! </Dropdown></template>
An embedded webpage:Vue Named Children - StackBlitz
A simple version of this dropdown component is actually built into the browser as <details> and <summary> HTML tags. Building our own is an experiment intended mostly for learning. For production environment, it's highly suggested to use those built-in elements instead.
Using Passed Children to Build a Table
Now that we're familiar with how to pass a child to a component, let's apply it to one of the components we've been building for our file hosting application: our files "list."
While this does constitute a list of files, there are actually two dimensions of data: Down and right. This makes this "list" really more of a "table". As such, it's actually a bit of a misconception to use the Unordered List (<ul>) and List Item (<li>) elements for this specific UI element.
Instead, let's build out an HTML table element. A normal HTML table might look something like this:
Angular is unlike the other frameworks covered in this book. For example, let's say we port the table components from the other frameworks one-to-one into Angular.
Here, you'll notice that the <tbody> isn't under <table>; it's under a <file-table-body> in between those two elements. Similarly, <tr> isn't under <tbody>, it's under <file-item>.
These changes to the markup are not allowed or understood by the HTML specification, which is why our table is formatted so weirdly.
Rendering this component will yield the following DOM elements:
<list-item></list-item>
This list-item selector-named element is known as a component's "host element." This host element preservation is different from how React and Vue work, which don't have the concept of in-DOM host nodes.
At first, this might seem like a roadblock to implementing our list-item component properly. However, there are two features that we can use to fix this problem:
Now that we have an explicit FileTable component, let's see if we're able to style it a bit more with a replacement FileTableContainer component, which uses passing children to style the underlying table element.
Let's make this chapter's challenge a continuation of the table that we just built in the last section. See, our previous table only had the files themselves, not the header. Let's change that by adding in a second set of children we can pass, like so: