DEV Community

Connie Leung
Connie Leung

Posted on

Day 22 - Alert Component Part 1 - Alert List and Alert Components

Day 22 - Alert Component Part 1 - Alert List and Alert Components

Table of Contents

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
Enter fullscreen mode Exit fullscreen mode

Add tailwind CSS to vite

import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tailwindcss(), ...other plugins...],
});
Enter fullscreen mode Exit fullscreen mode

Enable DaisyUI plugin in css

@import "tailwindcss";
@plugin "daisyui";
Enter fullscreen mode Exit fullscreen mode

Angular 20 application

npm install daisyui@latest tailwindcss@latest @tailwindcss/postcss@latest postcss@latest --force
Enter fullscreen mode Exit fullscreen mode

Configuration file

// .postcssrc.json
{
  "plugins": {
    "@tailwindcss/postcss": {}
  }
}
Enter fullscreen mode Exit fullscreen mode

Enable DaisyUI plugin in CSS

// src/style.css

@import "tailwindcss";
@plugin "daisyui";
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

SvelteKit application

Create Props

// lib/alert.type.ts
export type AlertType = 'info' | 'success' | 'warning' | 'error';

export type AlertMessage ={
    type: AlertType;
    message: string;
};
Enter fullscreen mode Exit fullscreen mode
<script lang="ts">
    type Prop = {
        alert: AlertMessage; 
    }

    const { alert }: Prop = $props()
</script>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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]>;
}
Enter fullscreen mode Exit fullscreen mode
<div role="alert" class={alertClasses}>
    <!-- HTML to render the SVG icon -->
    {@render alertMessage(alert.message) }
</div>
Enter fullscreen mode Exit fullscreen mode

Angular 20 application

Create Input Signals

export type AlertType = 'info' | 'success' | 'warning' | 'error';
Enter fullscreen mode Exit fullscreen mode
@Component({
  selector: 'app-alert',
  template: '',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AlertComponent {
  type = input.required<AlertType>();
}
Enter fullscreen mode Exit fullscreen mode

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()}`);
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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
}>()
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
{#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}
Enter fullscreen mode Exit fullscreen mode

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
{#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}
Enter fullscreen mode Exit fullscreen mode

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 }[]>();
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 ...)
}
Enter fullscreen mode Exit fullscreen mode

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

Github Pages

Resources

Top comments (2)

Collapse
 
arigatouz profile image
Ali Gamal

That's cool , svelte is really clean love it
Keep going Connie,you are really inspiring to me

Collapse
 
railsstudent profile image
Connie Leung • Edited

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.