Accessing Children

March 11, 2024

3,359 words

Post contents

In our "Passing Children" chapter, we talked about how you can pass components and elements as children to another component:

<FileTableContainer>	<FileTableHeader />	<FileTableBody /></FileTableContainer>

We also touched on the ability to access the following:

But only when the respective HTML elements or components are inside the parent's template itself:

// Inside FileTableContainer<FileTableHeader /><FileTableBody />

What if, instead, there was a way to access the children passed to an element through the slotted area one by one?

Well, is there a way to do that?

Yes.

Let's start with a simple example: Counting how many children a component has.

Counting a Component's Children

Let's count how many elements and components are being passed to our component:

React has a built-in helper called Children that will help you access data passed as a child to a component. Using this Children helper, we can use the toArray method to create an array from children that we can then do anything we might otherwise do with a typical array.

This comes in handy when trying to access the length count.

import { Children } from "react";const ParentList = ({ children }) => {	const childArr = Children.toArray(children);	console.log(childArr); // This is an array of ReactNode - more on that in the next sentence	return (		<>			<p>There are {childArr.length} number of items in this array</p>			<ul>{children}</ul>		</>	);};const App = () => {	return (		<ParentList>			<li>Item 1</li>			<li>Item 2</li>			<li>Item 3</li>		</ParentList>	);};

Here, childArr is an array of type ReactNode. A ReactNode is created by React's createElement method.

Remember that JSX transforms to call React's createElement node under-the-hood.

This means that the following JSX:

const el = <div />;

Becomes the following code after compilation:

const el = createElement("div");

There also exists a Children.count method that we could use as an alternative if we wanted.

const ParentList = ({ children }) => {	const childrenLength = Children.count(children);	return (		<>			<p>There are {childrenLength} number of items in this array</p>			<ul>{children}</ul>		</>	);};

To get the count of the children elements within a component in Angular requires some pre-requisite knowledge. Let's go through each step until we find ourselves at the solution.

ContentChild to Access a Single Child

In our "Dynamic HTML" chapter, we talked about how you're able to assign a "variable template variable" using a # syntax:

<div #templVar></div>

In our previous example, we used them to conditionally render content using ngIf, but these template tags aren't simply useful in ngIf usage. We can also use them in a myriad of programmatic queries, such as ContentChild.

ContentChild is a way to query the projected content within ng-content from JavaScript.

import {	Component,	AfterContentInit,	ContentChild,	ElementRef,} from "@angular/core";@Component({	selector: "parent-list",	standalone: true,	template: `<ng-content></ng-content>`,})class ParentListComponent implements AfterContentInit {	@ContentChild("childItem") child!: ElementRef<HTMLElement>;	// This cannot be replaced with an `OnInit`, otherwise `children` is empty. We'll explain soon.	ngAfterContentInit() {		console.log(this.child.nativeElement); // This is an HTMLElement	}}@Component({	selector: "app-root",	standalone: true,	imports: [ParentListComponent],	template: `		<parent-list>			<p #childItem>Hello, world!</p>		</parent-list>	`,})class AppComponent {}

Here, we're querying for the template tag childItem within the project content by using the ContentChild decorator.

ContentChild then returns a TypeScript generic type of ElementRef.

ElementRef is a type that has a single property called nativeElement containing the HTMLElement in question.

ngAfterContentInit Detects Child Initialization

If you were looking at the last code sample and wondered:

"What is ngAfterContentInit and why are we using it in place of ngOnInit?"

Then you're asking the right questions!

See, if we replace our usage of ngAfterContentInit with a ngOnInit, then we get undefined in place of this.child:

@Component({	selector: "parent-list",	standalone: true,	template: ` <ng-content></ng-content> `,})class ParentListComponent implements OnInit {	@ContentChild("childItem") child!: ElementRef<HTMLElement>;	ngOnInit() {		console.log(this.child); // This is `undefined`	}}

This is because while ngOnInit runs after the component has rendered, it has not yet received any values within ng-content; this is where ngAfterContentInit comes into play. This lifecycle method runs once ng-content has received the values, which we can then use as a sign that ContentChild has finished its query.

This can be solved by either:

Handle Multiple Children with ContentChildren

While ContentChild is useful for querying against a single item being projected, what if we wanted to query against multiple items being projected?

This is where ContentChildren comes into play:

import {	Component,	AfterContentInit,	ContentChildren,	QueryList,} from "@angular/core";@Component({	selector: "parent-list",	standalone: true,	template: `		<p>There are {{ children.length }} number of items in this array</p>		<ul>			<ng-content></ng-content>		</ul>	`,})class ParentListComponent implements AfterContentInit {	@ContentChildren("listItem") children!: QueryList<HTMLElement>;	ngAfterContentInit() {		console.log(this.children);	}}@Component({	selector: "app-root",	standalone: true,	imports: [ParentListComponent],	template: `		<parent-list>			<li #listItem>Item 1</li>			<li #listItem>Item 2</li>			<li #listItem>Item 3</li>		</parent-list>	`,})class AppComponent {}

ContentChildren returns an array-like QueryList generic type. You can then access the properties of children inside of the template itself, like what we're doing with children.length.

Unlike React and Angular, Vue's APIs don't allow us to count a child's list items easily. There are a lot of nuances as to why this is the case, but we'll do our best to explain that when we rewrite Vue from scratch in the third book of the series.

Instead, you'll want to pass the list from the parent component to the list display to show the value you intend:

<!-- ParentList --><script setup>const props = defineProps(["list"]);</script><template>	<p>There are {{ props.list.length }} number of items in this array</p>	<ul>		<slot></slot>	</ul></template>
<!-- App.vue --><script setup>import ParentList from "./ParentList.vue";const list = [1, 2, 3];</script><template>	<ParentList :list="list">		<li v-for="i in list">Item {{ i }}</li>	</ParentList></template>

Rendering Children in a Loop

Now that we're familiar with accessing children let's use the same APIs introduced before to take the following component template:

<ParentList>	<span>One</span>	<span>Two</span>	<span>Three</span></ParentList>

To render the following HTML:

<ul>	<li><span>One</span></li>	<li><span>Two</span></li>	<li><span>Three</span></li></ul>
const ParentList = ({ children }) => {	const childArr = Children.toArray(children);	console.log(childArr); // This is an array of ReactNode - more on that in the next sentence	return (		<>			<p>There are {childArr.length} number of items in this array</p>			<ul>				{children.map((child) => {					return <li>{child}</li>;				})}			</ul>		</>	);};const App = () => {	return (		<ParentList>			<span style={{ color: "red" }}>Red</span>			<span style={{ color: "green" }}>Green</span>			<span style={{ color: "blue" }}>Blue</span>		</ParentList>	);};

Since Angular's ContentChildren gives us an HTMLElement reference when using our template variables on HTMLElements, we're not able to wrap those elements easily.

Instead, let's change our elements to ng-templates and render them in an ngFor, similarly to what we did in our "Directives" chapter:

@Component({	selector: "parent-list",	standalone: true,	imports: [NgFor, NgTemplateOutlet],	template: `		<p>There are {{ children.length }} number of items in this array</p>		<ul>			<li *ngFor="let child of children">				<ng-template [ngTemplateOutlet]="child" />			</li>		</ul>	`,})class ParentListComponent {	@ContentChildren("listItem") children!: QueryList<TemplateRef<any>>;}@Component({	standalone: true,	imports: [ParentListComponent],	selector: "app-root",	template: `		<parent-list>			<ng-template #listItem>				<span style="color: red">Red</span>			</ng-template>			<ng-template #listItem>				<span style="color: green">Green</span>			</ng-template>			<ng-template #listItem>				<span style="color: blue">Blue</span>			</ng-template>		</parent-list>	`,})class AppComponent {}

Just as before, Vue's APIs have a limitation when accessing direct children. Let's explore why in the third book in our book series.

Adding Children Dynamically

Now that we have a list of items being transformed by our component let's add the functionality to add another item to the list:

const ParentList = ({ children }) => {	const childArr = Children.toArray(children);	console.log(childArr);	return (		<>			<p>There are {childArr.length} number of items in this array</p>			<ul>				{children.map((child) => {					return <li>{child}</li>;				})}			</ul>		</>	);};const App = () => {	const [list, setList] = useState([1, 42, 13]);	const addOne = () => {		// `Math` is built into the browser		const randomNum = Math.floor(Math.random() * 100);		setList([...list, randomNum]);	};	return (		<>			<ParentList>				{list.map((item, i) => (					<span key={i}>						{i} {item}					</span>				))}			</ParentList>			<button onClick={addOne}>Add</button>		</>	);};
@Component({	selector: "parent-list",	standalone: true,	imports: [NgFor, NgTemplateOutlet],	template: `		<p>There are {{ children.length }} number of items in this array</p>		<ul>			<li *ngFor="let child of children">				<ng-template [ngTemplateOutlet]="child" />			</li>		</ul>	`,})class ParentListComponent {	@ContentChildren("listItem") children!: QueryList<TemplateRef<any>>;}@Component({	standalone: true,	imports: [ParentListComponent, NgFor],	selector: "app-root",	template: `		<parent-list>			<ng-template *ngFor="let item of list; let i = index" #listItem>				<span>{{ i }} {{ item }}</span>			</ng-template>		</parent-list>		<button (click)="addOne()">Add</button>	`,})class AppComponent {	list = [1, 42, 13];	addOne() {		const randomNum = Math.floor(Math.random() * 100);		this.list.push(randomNum);	}}

While Vue can't render children in a list, it has many more capabilities to showcase. Read on, dear reader.

Here, we can see that whenever a random number is added to the list, our list item counter still increments properly.

Passing Values to Projected Content

While counting the number of items in a list is novel, it's not a very practical use of accessing projected content in JavaScript.

Instead, let's see if there's a way that we can pass values to our projected content. For example, let's try to change the background color of each li item if the index is even or odd.

By now, we should be familiar with the children property in React. Now get ready to forget everything you know about it:

const AddTwo = ({ children }) => {	return 2 + children;};// This will display "7"const App = () => {	return <AddTwo children={5} />;};

WHAT?!

Yup — as it turns out, you can pass any JavaScript value to React's children prop. It even works when you write it out like this:

const AddTwo = ({ children }) => {	return 2 + children;};const App = () => {	return <AddTwo>{5}</AddTwo>;};

Knowing this, what happens if we pass a function as children?:

const AddTwo = ({ children }) => {	return 2 + children();};// This will display "7"const App = () => {	return <AddTwo>{() => 5}</AddTwo>;	// OR <AddTwo children={() => 5} />};

Sure enough, it works!

Now, let's combine this knowledge with the ability to use JSX wherever a value might go:

const ShowMessage = ({ children }) => {	return children();};const App = () => {	return <ShowMessage>{() => <p>Hello, world!</p>}</ShowMessage>;};

Finally, we can combine this with the ability to pass values to a function:

const ShowMessage = ({ children }) => {	return children("Hello, world!");};const App = () => {	return <ShowMessage>{(message) => <p>{message}</p>}</ShowMessage>;};

Confused about how this last one is working? It might be a good time to review your knowledge on how functions are able to pass to one another and call each other.

Displaying the List in React

Now that we've seen the capabilities of a child function, let's use it to render a list with alternating backgrounds:

const ParentList = ({ children, list }) => {	return (		<>			<ul>				{list.map((item, i) => {					return (						<Fragment key={item}>							{children({								backgroundColor: i % 2 ? "grey" : "",								item,								i,							})}						</Fragment>					);				})}			</ul>		</>	);};const App = () => {	const [list, setList] = useState([1, 42, 13]);	const addOne = () => {		const randomNum = Math.floor(Math.random() * 100);		setList([...list, randomNum]);	};	return (		<>			<ParentList list={list}>				{({ backgroundColor, i, item }) => (					<li style={{ backgroundColor: backgroundColor }}>						{i} {item}					</li>				)}			</ParentList>			<button onClick={addOne}>Add</button>		</>	);};

Let's use the ability to pass values to an ngTemplate using context to provide the background color to the passed template to our ParentList component:

@Component({	selector: "parent-list",	standalone: true,	template: `		<p>There are {{ children.length }} number of items in this array</p>		<ul>			<ng-template				*ngFor="let template of children; let i = index"				[ngTemplateOutlet]="template"				[ngTemplateOutletContext]="{ backgroundColor: i % 2 ? 'grey' : '' }"			></ng-template>		</ul>	`,})class ParentListComponent {	@ContentChildren("listItem", { read: TemplateRef }) children: QueryList<		TemplateRef<any>	>;}@Component({	selector: "app-root",	standalone: true,	imports: [ParentListComponent],	template: `		<parent-list>			<ng-template				#listItem				*ngFor="let item of list; let i = index"				let-backgroundColor="backgroundColor"			>				<li [style]="{ backgroundColor }">{{ i }} {{ item }}</li>			</ng-template>		</parent-list>		<button (click)="addOne()">Add</button>	`,})class AppComponent {	list = [1, 42, 13];	addOne() {		const randomNum = Math.floor(Math.random() * 100);		this.list.push(randomNum);	}}

Vue ca... Wait! Vue can do this one!

Let's take our code from the start of this chapter and refactor it so that we don't have to have our v-for inside of the App.vue. Instead, let's move it into ParentList.vue and pass properties to the <slot> element.

<!-- ParentList.vue --><script setup>const props = defineProps(["list"]);</script><template>	<p>There are {{ props.list.length }} number of items in this array</p>	<ul id="parentList">		<slot			v-for="(item, i) in props.list"			:item="item"			:i="i"			:backgroundColor="i % 2 ? 'grey' : ''"		></slot>	</ul></template>
<!-- App.vue --><script setup>import { ref } from "vue";import ParentList from "./ParentList.vue";const list = ref([1, 2, 3]);function addOne() {	const randomNum = Math.floor(Math.random() * 100);	list.value.push(randomNum);}</script><template>	<ParentList :list="list">		<!-- Think of this as "template is receiving an object		 we'll call props" from "ParentList" -->		<template v-slot="props">			<li :style="'background-color:' + props.backgroundColor">				{{ props.i }} {{ props.item }}			</li>		</template>	</ParentList>	<button @click="addOne()">Add</button></template>

This v-slot is similar to how you might pass properties to a component, but instead, we're passing data directly to a template to be rendered by v-slot.

You can object destructure the v-slot usage to gain access to the property names without having to repeat props each time:

<template v-slot="{item, i}">	<li>{{i}} {{item}}</li></template>

Challenge

Let's write a table component! Something like this:

Heading OneHeading Two
Some val 1Some val 2
Some val 3Some val 4
Some val 5Some val 6

However, instead of having to write out the HTML ourselves, let's try to make this table easy to use for our development team.

Let's pass:

  • An array of object data
  • A table header template that receives the length of object data
  • A table body template that receives the value for each row of data

This way, we don't need any loops in our App component.

const Table = ({ data, header, children }) => {	const headerContents = header({ length: data.length });	const body = data.map((value, rowI) => children({ value, rowI }));	return (		<table>			<thead>{headerContents}</thead>			<tbody>{body}</tbody>		</table>	);};const data = [	{		name: "Corbin",		age: 24,	},	{		name: "Joely",		age: 28,	},	{		name: "Frank",		age: 33,	},];function App() {	return (		<Table			data={data}			header={({ length }) => (				<tr>					<th>{length} items</th>				</tr>			)}		>			{({ rowI, value }) => (				<tr>					<td						style={							rowI % 2								? { background: "lightgrey" }								: { background: "lightblue" }						}					>						{value.name}					</td>				</tr>			)}		</Table>	);}
@Component({	selector: "table-comp",	standalone: true,	imports: [NgFor, NgTemplateOutlet],	template: `		<table>			<thead>				<ng-template					[ngTemplateOutlet]="header"					[ngTemplateOutletContext]="{ length: data.length }"				/>			</thead>			<tbody>				<ng-template					*ngFor="let item of data; let index = index"					[ngTemplateOutlet]="body"					[ngTemplateOutletContext]="{ rowI: index, value: item }"				/>			</tbody>		</table>	`,})class TableComponent {	@ContentChild("header", { read: TemplateRef }) header!: TemplateRef<any>;	@ContentChild("body", { read: TemplateRef }) body!: TemplateRef<any>;	@Input() data!: any[];}@Component({	selector: "app-root",	standalone: true,	imports: [TableComponent],	template: `		<table-comp [data]="data">			<ng-template #header let-length="length">				<tr>					<th>{{ length }} items</th>				</tr>			</ng-template>			<ng-template #body let-rowI="rowI" let-value="value">				<tr>					<td						[style]="							rowI % 2 ? 'background: lightgrey' : 'background: lightblue'						"					>						{{ value.name }}					</td>				</tr>			</ng-template>		</table-comp>	`,})class AppComponent {	data = [		{			name: "Corbin",			age: 24,		},		{			name: "Joely",			age: 28,		},		{			name: "Frank",			age: 33,		},	];}
<!-- App.vue --><script setup>import Table from "./Table.vue";const data = [	{		name: "Corbin",		age: 24,	},	{		name: "Joely",		age: 28,	},	{		name: "Frank",		age: 33,	},];</script><template>	<Table :data="data">		<template #header="{ length }">			<tr>				<th>{{ length }} items</th>			</tr>		</template>		<template v-slot="{ rowI, value }">			<tr>				<td					:style="rowI % 2 ? 'background: lightgrey' : 'background: lightblue'"				>					{{ value.name }}				</td>			</tr>		</template>	</Table></template>
<!-- Table.vue --><script setup>const props = defineProps(["data"]);</script><template>	<table>		<thead>			<slot name="header" :length="props.data.length"></slot>		</thead>		<tbody>			<slot				v-for="(item, i) in props.data"				:key="i"				:rowI="i"				:value="props.data[i]"			/>		</tbody>	</table></template>
Previous articleDirectives

Subscribe to our newsletter!

Subscribe to our newsletter to get updates on new content we create, events we have coming up, and more! We'll make sure not to spam you and provide good insights to the content we have.