Day 22 - Alert Component Part 1 - Alert List and Alert Components
Table of Contents
- Installation
- Alert Component
- Add Close Button to Alert Component
- Alert List Component
- App Component
- Github Repositories
- Github Pages
- Resources
Component Fundamentals with JavaScript Frameworks
On day 22, I started working on the Alert Component exercise in Vue 3, Angular 20, and Svelte 5.
The Alert component uses the DaisyUI Alert component and TailwindCSS utility classes for styling. I also learned about two-way binding between components using defineModel
in Vue 3.5+. I also learned that Svelte 5 uses $bindable
to flow data from the child to the parent component. In Angular, it is model
, a writable signal, that allows input to go from parent to child and in the opposite direction.
This small exercise will be split into four parts. Part 3 and 4 are extra because I want to be able to change the alert styles and reopen the alerts.
Parts
- Part 1: DaisyUI installation, Alert List, and Alert Components
- Part 2: Dynamically Render Icon for the Alert Component
- Part 3: Add an Alert Bar to change styles
- Part 4: Update the Alert Bar to reopen closed alerts
- Part 5: Extract logic and component from Alert Bar
Let's start with Alert List and Alert Component because DaisyUI has already provided alert examples out of the box. The Alert List component is a container that iterates an alert list to display different type of alerts.
Installation
Vue 3 and SvelteKit
npm install tailwindcss@latest @tailwindcss/vite@latest daisyui@latest
Add tailwind CSS to vite
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss(), ...other plugins...],
});
Enable DaisyUI plugin in css
@import "tailwindcss";
@plugin "daisyui";
Angular 20 application
npm install daisyui@latest tailwindcss@latest @tailwindcss/postcss@latest postcss@latest --force
Configuration file
// .postcssrc.json
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
Enable DaisyUI plugin in CSS
// src/style.css
@import "tailwindcss";
@plugin "daisyui";
Copy HTML of the alerts to the AlertList component
I copied the HTML of info, success, warning, and error alerts from https://daisyui.com/components/alert/ to the AlertList component.
<script setup lang="ts"></script>
<template>
<div role="alert" class="alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>New software update available.</span>
</div>
<div role="alert" class="alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Your purchase has been confirmed!</span>
</div>
<div role="alert" class="alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Warning: Invalid email address!</span>
</div>
<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Error! Task failed successfully.</span>
</div>
</template>
It is easier for me to see the structure of the list and then refactor the template to utilize the Alert component.
We create the Alert component, and then import it into the AlertList component.
Static HTML Template of the alert
<div role="alert" class="alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>New software update available.</span>
</div>
We need to make the Alert component reusable for different types, icons and texts. Therefore, the type, icon and text must be configuable.
Alert Component
Vue 3 application
Create Props
<script setup lang="ts">
type Prop = {
type: string;
}
const { type } = defineProps<Prop>()
</script>
Prop defines the alert type
such as info, success, warning, and error.
Derive the CSS classes of the alert
<script setup lang="ts">
const alertColor = computed(() => ({
info: 'alert-info',
warning: 'alert-warning',
error: 'alert-error',
success: 'alert-success'
}[type]))
const alertClasses = computed(() => `alert ${alertColor.value}`)
</script>
The type
prop is used to derive the alertColor
and alertClasses
computed refs. The alertColor
computed ref indexes type
in the dictionary to obtain the CSS class. The alertClasses
computed ref concatenates the CSS classes and bind to the class attribute.
<div role="alert" :class="alertClasses"></div>
Conditionally render the icons by type
Use v-if
and v-else-if
directives to render the SVG icons conditionally by type.
<div role="alert" :class="alertClasses">
<svg v-if="type == 'info'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<svg v-else-if="type == 'success'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<svg v-else-if="type == 'warning'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<svg v-else-if="type == 'error'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
This solution works but it is not scalable. We will refactor this solution with icon components and dynamic rendering in part 2.
Use Slot to Display Text
<div role="alert" :class="alertClasses">
<!-- HTML to render the SVG icon -->
<span><slot /></span>
</div>
SvelteKit application
Create Props
// lib/alert.type.ts
export type AlertType = 'info' | 'success' | 'warning' | 'error';
export type AlertMessage ={
type: AlertType;
message: string;
};
<script lang="ts">
type Prop = {
alert: AlertMessage;
}
const { alert }: Prop = $props()
</script>
Derive the CSS classes of the alert
<script lang="ts">
const alertColor = $derived.by(() => ({
info: 'alert-info',
warning: 'alert-warning',
error: 'alert-error',
success: 'alert-success'
}[alert.type]))
const alertClasses = $derived(`alert ${alertColor}`)
</script>
The alert
prop is used to derive the alertColor
and alertClasses
derived runes. The alertColor
derived rune indexes alert.type
in the dictionary to obtain the CSS class. The alertClasses
derived rune concatenates the CSS classes and bind to the class attribute.
<div role="alert" class={alertClasses}></div>
Conditionally render the icons by type
Use if-else-if control flow to render the SVG icons conditionally by type.
<div role="alert" class={alertClasses}>
{#if alert.type == 'info'}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{:else if alert.type == 'success'}
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{:else if alert.type == 'warning'}
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
{:else if alert.type == 'error'}
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{/if}
</div>
Use Snippet to Render Text
Svelte 4 supports slot but Svelte 5 uses snippets
and render
tag to project content.
type Props = {
alert: AlertMessage;
alertMessage: Snippet<[string]>;
}
<div role="alert" class={alertClasses}>
<!-- HTML to render the SVG icon -->
{@render alertMessage(alert.message) }
</div>
Angular 20 application
Create Input Signals
export type AlertType = 'info' | 'success' | 'warning' | 'error';
@Component({
selector: 'app-alert',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {
type = input.required<AlertType>();
}
Derive the CSS classes of the alert
export class AlertComponent {
type = input.required<AlertType>();
alertColor = computed(() => {
return {
info: 'alert-info',
warning: 'alert-warning',
error: 'alert-error',
success: 'alert-success'
}[this.type()]
});
alertClasses = computed(() => `alert ${this.alertColor()}`);
}
The type
input signal is used to derive the alertColor
and alertClasses
computed signals. The alertColor
computed signal indexes type
in the dictionary to obtain the CSS class. The alertClasses
computed signal concatenates the CSS classes and bind to the class attribute.
@Component({
selector: 'app-alert',
template: `<div role="alert" [class]="alertClasses()"></div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {}
Conditionally render the icon by type
Use if-else-if control flow to render the SVG icons conditionally by type.
@Component({
selector: 'app-alert',
template: `
<div role="alert" [class]="alertClasses()">
@if (type() === 'info'} {
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
} @else if (type() === 'success') {
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
} @else if (type() === 'warning') {
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
} @else if (type() === 'error') {
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
}
</div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {}
Use NgContent to Render Text
Angular uses ng-content
to project content.
@Component({
selector: 'app-alert',
template: `
<div role="alert" [class]="alertClasses()">
<!-- Render SVG icons conditionally by type -->
<span><ng-content /></span>
</div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {}
Add Close Button to Alert Component
Vue 3 application
Add a Close button to the Alert
component to close the component and emit an event to the AlertList
component.
const emits = defineEmits<{
(e: 'closed', type: string): void
}>()
Define a closed
event that emits the closed type to the parent component
const closed = ref(false)
function closeAlert() {
closed.value = true
emits('closed', type)
}
<template>
<div role="alert" :class="alertClasses" v-if="!closed">
<div>
<!-- previous html code -->
<button class="btn btn-sm btn-primary" alt="Close button" @click="closeAlert">X</button>
</div>
</div>
</template>
Add a v-if
to close the <div> element when the closed
ref is true.
When the button is clicked, the closeAlert
function sets the closed
ref to true and emits the closed
event to the parent component. The argument of the event event is the alert type.
SvelteKit application
type Props = {
notifyClosed?: (type: string) => void;
}
Unlike Vue 3 and Angular, Svelte 5 does not emit events to the parent component. The parent component provides a callback prop to the child component to be invoked in an event handler.
let closed = $state(false);
function closeAlert() {
closed = true;
notifyClosed?.(alert.type)
}
{#if !closed}
<div role="alert" class={alertClasses}>
<!-- previous HTML code -->
<div>
<button class="btn btn-sm btn-primary" title="Close button" onclick={closeAlert}>X</button>
</div>
</div>
{/if}
When the button is clicked, the closeAlert
sets the closed
rune to true and invokes the notifyClosed
callback prop to perform logic in the parent component.
The argument of the callback prop is the alert type.
Angular 20 application
import { ChangeDetectionStrategy, Component, computed, input, output, signal, viewChild, ViewContainerRef } from '@angular/core';
import { AlertType } from '../alert.type';
@Component({
selector: 'app-alert',
template: `
@if (!closed()) {
<div role="alert" class="mb-[0.75rem]" [class]="alertClasses()">
<!-- previous HTML codes -->
<div>
<button class="btn btn-sm btn-primary" alt="Close button" (click)="closeAlert()">X</button>
</div>
</div>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {
/* omit other codes to keep the class succinct */
closed = signal(false);
closeNotification = output<string>();
closeAlert() {
this.closed.set(true);
this.closeNotification.emit(this.type());
}
}
The closed
signal determines whether to show or hide the <div> element.
The closeNotification
output notifies the parent component when the Alert
component is closed.
Use the @if
control flow to hide the <div> element when the closed
signal is true.
When the button is clicked, the closeAlert
method sets the closed
signal to true and emits the closeNotification
custom event to the parent component. The argument of the custom event is the alert type.
Alert List Component
Vue 3 application
Import the Alert
component and display info, success, warning and error alerts in a loop. The alerts is a prop that the App
provides.
<script setup lang="ts">
import Alert from './Alert.vue'
const props = defineProps<{
alerts: { type: string; message: string }[]
}>()
function handleClosed(type: string) {
console.log(type)
}
</script>
<template>
<h2>Alert Components (Vue ver.)</h2>
<Alert v-for="{ type, message } in alerts"
class="mb-[0.75rem]"
:key="type"
:type="type"
@closed="handleClosed">
{{ message }}
</Alert>
</template>
When the closed
event occurs, the handleClosed
function console log the alert type.
SvelteKit application
Import the Alert
component and display info, success, warning and error alerts in a loop. The alerts is a prop that the page
route provides.
<script lang="ts">
import Alert from './alert.svelte';
import type { AlertMessage } from './alert.type';
type Props = {
alerts: AlertMessage[];
}
const { alerts }: Props = $props()
function notifyClosed(type: string) {
console.log(type);
}
</script>
{#snippet alertMessage(text: string)}
<span>{text}</span>
{/snippet}
<h2>Alert Components (Svelte ver.)</h2>
{#each alerts as alert (alert.type) }
<Alert {alert} {alertMessage} {notifyClosed} />
{/each}
The alerts
rune provides the type to the Alert
component and the alertMessage
snippet renders the message.
When the notifyClosed
event occurs, the notifyClosed
function console log the alert type.
Angular 20 application
The AlertListComponent
imports the AlertComponent
and adds it to the imports
array of the @Component
decorator. The component displays info, success, warning and error alerts in a for loop.
import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
import { AlertType } from '../alert.type';
import { AlertComponent } from '../alert/alert.component';
@Component({
selector: 'app-alert-list',
imports: [AlertComponent],
template: `
@for (alert of alerts(); track alert.type) {
<app-alert [type]="alert.type" >
{{ alert.message }}
</app-alert>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertListComponent {
alerts = input.required<{ type: AlertType; message: string }[]>();
}
The alerts
is an required input signal that receives an array from the AppComponent
.
App Component
Vue 3 application
<script setup lang="ts">
import { ref } from 'vue'
import AlertList from './components/AlertList.vue'
const alerts = ref([
{
type: 'info',
message: 'New software update available.'
},
{
type: 'success',
message: 'Your purchase has been confirmed!'
},
{
type: 'warning',
message: 'Warning: Invalid email address!'
},
{
type: 'error',
message: 'Error! Task failed successfully.'
}])
</script>
The App
component creates an alerts
ref to store the alert types and messages. In the <script> tag, it imports the AlertList
component and renders it in the <template>
tags.
<template>
<main>
<AlertList :alerts="alerts" />
</main>
</template>
The alerts
is passed to the AlertList
component as a prop.
SvelteKit application
<script lang="ts">
import AlertList from '$lib/alert-list.svelte';
import type { AlertMessage } from '$lib/alert.type';
const alerts = $state<AlertMessage[]>(...same alert array to save space...)
</script>
The App
component creates an alerts
rune to store the alert types and messages. In the <script> tag, it imports the AlertList
component and renders it in the <main>
element.
<main>
<AlertList alerts={alerts} />
</main>
The alerts
is passed to the AlertList
component as a prop.
Angular 20 application
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { AlertListComponent } from './alert-list/alert-list.component';
import { AlertType } from './alert.type';
@Component({
selector: 'app-root',
imports: [AlertListComponent],
template: `
<div id="app">
<main>
<app-alert-list [alerts]="alerts()" />
</main>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
alerts = signal<{ type: AlertType, message: string }[]>(... same alert array to save space ...)
}
The AppComponent
imports the AlertListComponent
and adds it to the imports
array of the @Component
decorator. The alerts
signal is passed to the alerts
input signal of the AlertListComponent
.
We have successfully created the alert list in Vue, Svelte and Angular frameworks.
Github Repositories
- Vue 3: https://github.com/railsstudent/vue-alert-component
- 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
- DaisyUI Installation: https://daisyui.com/docs/install/
- Svelte 5 Snippet: https://svelte.dev/docs/svelte/snippet
- Svelte 5 Component Events: https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes-Component-events
Top comments (2)
That's cool , svelte is really clean love it
Keep going Connie,you are really inspiring to me
Thank you for your kind words, Ali. I want to do this comparision to show people that Angular has changed and Angular <-> Vue 3/ SvelteKit is feasible.