Day 25 – Alert Component Part 4 – Update the Alert Bar to reopen closed alerts

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 AlertBarcomponent 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 AlertBarcomponent 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

Github Pages

Resources

Leave a Reply