Component Fundamentals with JavaScript Frameworks
On day 25, I extend the Alert Bar component to display buttons of closed alerts. Then, users can either reopen all the closed alerts or a specific type of alert.
Track Closed Alerts
Vue 3 application
<script setup lang="ts">
import { ref } from 'vue'
const closedNotifications = defineModel<string[]>('closedNotifications', { default: [] })
</script>
In the AlertBar
component, use the defineModel
to create a closedNotifications
ref of type string[]
. The default value of the closedNotifications
is an empty list.
For each type in this ref, the button is displayed and can be used to open the alert. When the ref is not empty, the Open all alerts
button is also displayed.
SvelteKit application
<script lang="ts">
type Props = {
... other prop ...
closedNotifications: string[];
}
let {
... other bindables ...
closedNotifications = $bindable(),
}: Props = $props();
</script>
Add closedNotifications
callback prop to the Props
type. Then, destructure closedNotifications
from the $props()
macro and use the $bindable
macro to establish the two-way binding.
Angular 20 application
import { ChangeDetectionStrategy, Component, input, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-alert-bar',
imports: [FormsModule],
template: `...inline template...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertBarComponent {
... other models ...
closedNotifications = model<string[]>([]);
}
In the AlertBarComponent
, create a closedNotifications
model signal of type string[]
. The default value of the closedNotifications
is an empty string array.
User Events to Track Closed Alerts
Vue 3 application
In the AlertBar
component, two user events can occur that manipulates the closedNotifications
ref. When users click the "Open all alerts" button, the closedNotifications
ref should be an empty array. When users click a button to reopen a specific type of alert, that type must be removed from the closedNotifications
ref.
<script setup lang="ts">
const closedNotifications = defineModel<string[]>('closedNotifications', { default: [] })
function removeNotification(type: string) {
closedNotifications.value = closedNotifications.value.filter((t) => t !== type)
}
function clearAllNotifications() {
closedNotifications.value = []
}
function hasClosedNotifications() {
return closedNotifications.value.length > 0
}
</script>
The removeNotification
function removes the specific type
from the closedNotifications
ref.
The clearAllNotifications
function removes all the types from the closedNotification
ref.
The hasClosedNotifications
function determines whether or not the closedNotifications
ref is empty.
<template>
<div>
<p class="mb-[0.75rem]">
<button v-for="type in closedNotifications"
:key="type" @click="removeNotification(type)"
>
<OpenIcon />{{ capitalize(type) }}
</button>
<button
v-if="hasClosedNotifications()"
@click="clearAllNotifications">
Open all alerts
</button>
</p>
</div>
</template>
The v-for
directive iterates the closedNotifications
ref to display buttons for each closed notification. When the click
event occurs, the removeNotification
function removes the type
from the ref.
The v-if
diretive displays the Open all alerts
button when the closedNotifications
ref is not an empty array. When the click
event occurs, the clearAllNotifications
function removes all types from the ref.
SvelteKit application
In the AlertBar
component.
<script lang="ts">
type Props = {
... omitted due to brevity ...
closedNotifications: string[];
}
let {
... omitted due to brevity ...
closedNotifications = $bindable(),
}: Props = $props();
function removeNotification(type: string) {
closedNotifications = closedNotifications.filter((t) => t !== type)
}
function clearAllNotifications() {
closedNotifications = []
}
function hasClosedNotifications() {
return closedNotifications.length > 0
}
</script>
The removeNotification
function removes the specific type
from the closedNotifications
callback prop.
The clearAllNotifications
function removes all the types from the closedNotifications
callback prop.
The hasClosedNotifications
function determines whether or not the closedNotifications
callback prop is empty.
<div>
<p class="mb-[0.75rem]">
{#each closedNotifications as type (type)}
<button
class={getBtnClass(type) + ' mr-[0.5rem] btn'}
onclick={() => removeNotification(type)}
>
<OpenIcon />{ capitalize(type) }
</button>
{/each}
{#if hasClosedNotifications()}
<button
class="btn btn-primary"
onclick={clearAllNotifications}>
Open all alerts
</button>
{/if}
</p>
</div>
In Svelte, event is an attribute that begins with 'on', so the attribute name of a click event is onclick
.
onclick={() => removeNotification(type)}
executes the removeNotification
function.
onclick={clearAllNotifications}
executes the clearAllNotifications
function. It is a shortcut because the function does not require any argument.
The #each
iterates the closedNotifications
callback prop to display buttons for each closed notification. When the click
event occurs, the removeNotification
function is fired.
The #if
displays the Open all alerts
button when the closedNotifications
callback prop is not an empty array. When the click
event occurs, the clearAllNotifications
function is fired.
Angular 20 application
In the AlertBarComponent
import { ChangeDetectionStrategy, Component, input, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { capitalize } from '../capitalize';
import { OpenIconComponent } from '../icons/icon.component';
@Component({
selector: 'app-alert-bar',
imports: [FormsModule, OpenIconComponent],
template: `...inline template...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertBarComponent {
closedNotifications = model<string[]>([]);
removeNotification(type: string) {
this.closedNotifications.update((prev) => prev.filter((t) => t !== type));
}
clearAllNotifications() {
this.closedNotifications.set([])
}
hasCloseButtonChanged() {
return this.closedNotifications().length > 0;
}
}
The removeNotification
method calls the update
method of the closedNotifications
model signal to remove a specific type
.
The clearAllNotifications
method calls the set
method of the closedNotifications
model signal to remove all the types.
The hasClosedNotifications
method determines whether or not the closedNotifications
model signal is empty.
<div>
<p class="mb-[0.75rem]">
@for (type of closedNotifications(); track type) {
<button (click)="removeNotification(type)">
<app-open-icon />{{ capitalize(type) }}
</button>
}
@if (hasClosedNotifications()) {
<button (click)="closedNotifications.set([])">
Open all alerts
</button>
}
</p>
</div>
In Angular, event is emitted and the banana syntax, (event)
, is used to emit an event to a parent.
(click)="removeNotification(type)"
executes the removeNotification
method.
(click)="clearAllNotifications"
executes the clearAllNotifications
method.
The @each
iterates the closedNotifications
model signal to display buttons for each closed notification. When the click
event occurs, the removeNotification
method is fired.
The @if
displays the Open all alerts
button when the closedNotifications
model signal is not an empty array. When the click
event occurs, the clearAllNotifications
method is fired.
Two-way binding with the AlertList Component
Vue 3 application
<script setup lang="ts">
import type { AlertType } from '@/types/alert-type';
import { computed, ref } from 'vue';
import Alert from './Alert.vue';
import AlertBar from './AlertBar.vue';
const closedNotifications = ref<string[]>([])
const alerts = computed(() => props.alerts.filter((alert) =>
!closedNotifications.value.includes(alert.type))
)
function handleClosed(type: string) {
closedNotifications.value.push(type)
}
</script>
The closedNotifications
ref binds to the closedNotifications
ref of the AlertBar
component.
The alerts
computed ref is an array of opened alerts. When the AlertBar
component removes any item from the closedNotification
, the alert
ref recomputes the alerts to displayed.
The handleClosed
function adds the type of the closed alert to the closedNotifications
ref. After this function is executed, the alerts
ref recomputes to get a new value.
<template>
<AlertBar
v-model:closedNotifications="closedNotifications"
/>
</template>
The v-model
directive binds the closedNotifications
ref to the closedNotifications
ref of the AlertBar
component. When closedNotifications
is updated in the AlertBar
component, closedNotifications
is also updated in the AlertList
component.
<template>
<Alert v-for="{ type, message } in alerts"
:key="type"
:type="type"
@closed="handleClosed">
{{ message }}
</Alert>
</template>
The v-for
directive iterates the alerts
computed ref to display the opened alerts.
SvelteKit application
<script lang="ts">
import AlertBar from './alert-bar.svelte';
import Alert from './alert.svelte';
let closedNotifications = $state<string[]>([]);
let filteredNotifications = $derived.by(() =>
alerts.filter(alert => !closedNotifications.includes(alert.type))
);
function notifyClosed(type: string) {
console.log(`Alert of type ${type} closed`);
closedNotifications.push(type);
}
</script>
The closedNotifications
rune binds to the closedNotifications
rune of the AlertBar
component.
The filteredNotifications
derived rune is an array of opened alerts. When the AlertBar
component removes any item from the closedNotifications
, the filteredNotifications
derived rune recomputes the alerts to be displayed.
The handleClosed
function adds the type of the closed alert to the closedNotifications
rune. After this function is executed, the filteredNotifications
derived rune recomputes to get a new value.
<AlertBar
bind:closedNotifications={closedNotifications}
/>
{#each filteredNotifications as alert (alert.type) }
<Alert {notifyClosed} />
{/each}
bind:closedNotifications={closedNotifications}
means the AlertList
component listens to what the closedNotifications
prop of the AlertBar
component has to say.
#each
iterates the filteredNotifications
derived rune to display the opened alerts.
Angular 20 application
import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
import { AlertType } from '../alert.type';
import { AlertComponent } from '../alert/alert.component';
import { AlertBarComponent } from '../alert-bar/alert-bar.component';
@Component({
selector: 'app-alert-list',
imports: [AlertComponent, AlertBarComponent],
template: `...inline template...`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertListComponent {
closedNotifications = signal<string[]>([]);
filteredAlerts = computed(() =>
this.alerts().filter(alert => !this.closedNotifications().includes(alert.type))
);
handleCloseNotification(type: string) {
this.closedNotifications.update((prev) => ([...prev, type ]));
}
}
The closedNotifications
signal binds to the closedNotifications
model signal of the AlertBar
component.
The filteredAlerts
computed signal is an array of opened alerts. When the AlertBar
component removes any item from the closedNotifications
, the filteredAlerts
computed signal recomputes the alerts to be displayed.
The handleCloseNotification
method adds the type of the closed alert to the closedNotifications
signal. After the method is executed, the filteredAlerts
recomputes to get a new value.
<app-alert-bar
[(closedNotifications)]="closedNotifications"
/>
@for (alert of filteredAlerts(); track alert.type) {
<app-alert [type]="alert.type"
[alertConfig]="alertConfig()"(closeNotification)="handleCloseNotification($event)">
{{ alert.message }}
</app-alert>
}
[(closedNotifications)]="closedNotifications"
means the
/>AlertList
component listens to what the closedNotifications
model signal of the AlertBar
component has to say.
@for
iterates the filteredAlerts
computed signal to display the opened alerts.
Now, users can clicks button in the AlertBar
component to open the closed alerts.
Github Repositories
- Vue 3: https://github.com/railsstudent/vue-alert-component/blob/main/src/components/AlertBar.vue
- Svelte 5: https://github.com/railsstudent/svelte-alert-component
- Angular 20: https://github.com/railsstudent/angular-alert-component
Github Pages
- Vue 3: https://railsstudent.github.io/vue-alert-component
- Svelte 5: https://railsstudent.github.io/svelte-alert-component
- Angular 20: https://railsstudent.github.io/angular-alert-component
Resources
- Vue multiple v-model bindings: https://vuejs.org/guide/components/v-model#multiple-v-model-bindings
- Svelte \$bindable: https://svelte.dev/docs/svelte/\$bindable
- Angular model: https://angular.dev/api/core/model
Top comments (0)