Day 16 - Select a Coffee Plan with Component Event
Connie Leung

Connie Leung @railsstudent

About: Google Developer Expert for Angular 🅰️ 🇭🇰| 🅰Angular Architecture| 🅽NestJS| 🆅VueJs | 🆂 Svelte | 🆃TypeScript| Blogger| YouTube Content Creator| Software Architect at Diginex

Location:
Hong Kong
Joined:
Dec 15, 2021

Day 16 - Select a Coffee Plan with Component Event

Publish Date: Aug 10
3 0

Table of Contents

On day 16, I put a border around a coffee plan when it was selected. Other coffee plans were inactive and lost the border. The CoffeePlan component emitted the active name to the PlanPicker component so that it could notify other coffee plans to remove the border.

Define Component Event to Emit Active Plan

  • Vue 3 application
const emit = defineEmits<{
  (e: 'selectedPlan', name: string): void
}>()

function selectPlan() {
  emit('selectedPlan', props.name)
}
Enter fullscreen mode Exit fullscreen mode

The CoffeePlan component defines a custom event that emits the name to the PlanPicker component.

The selectedPlan function uses the selectedPlan event to emit the plan name to the parent component. The PlanPicker component receives the active plan and can notify other plans that they are no longer active.

  • SvelteKit application
<script lang="ts">
    interface Props {
        name: string;
        selectedPlan: (name: string) => void;
    }

    let { name = 'Default Plan', selectedPlan }: Props = $props();

    const handleSelectPlan = () => selectedPlan(name);
</script>
Enter fullscreen mode Exit fullscreen mode

The CoffeePlan component extracts the selectedPlan function from $props(). The PlanPicker component must provide the prop callback for the CoffeePlan component to call in the handleSelectPlan function.

The handleSelection invokes the selectedPlan function with the plan name.

  • Angular 19 application
@Component({
  ...
})
export class CoffeePlanComponent {
  name = input('Default Plan');
  selectedPlan = output<string>();

  selectPlan() {
    this.selectedPlan.emit(this.name());
  }
}
Enter fullscreen mode Exit fullscreen mode

The CoffeePlanComponent declares a selectedPlan output that emits the coffee name to the PlanPickerComponent. The name is a signal input of type string.

When the selectPlan method is invoked, the selectedPlan outputs the result of the name getter to the PlanPickerComponent.

Add Selected Prop in the CoffeePlan Component

  • Vue 3 application
<script setup lang="ts">
const props = defineProps({
  selected: {
    type: Boolean,
    default: false,
  },
})
</script>
Enter fullscreen mode Exit fullscreen mode

Add selected to the CoffeePlan's props. When selected is true, the plan is active and it has a border. When selected is false, the border is removed.

<template>
  <div class="plan" @click="selectPlan" :class="{ 'active-plan': selected }">
    <div class="description">
      <span class="title"> {{ name }} </span>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

The div element is bound to the active-plan class dynamically. When selected is true, the CSS class is enabled. Otherwise, the class is removed.

  • SvelteKit application
<script lang="ts">
    interface Props {
        name: string;
        selectedPlan: (name: string) => void;
        selected: boolean;
    }

    let { name = 'Default Plan', selectedPlan, selected }: Props = $props();

        const handleSelectPlan = () => selectedPlan(name);
</script>
Enter fullscreen mode Exit fullscreen mode

Similarly, the selected flag is extracted from $props. Moreover, the Props interface has a selected property of type boolean.

<div onclick={handleSelectPlan} class={['plan', selected && 'active-plan']}>
    <div class="description">
        <span class="title"> {name} </span>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

When selected is true, active-plan is part of the class list. Otherwise, the class list does not have the class

  • Angular 19 application
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';

@Component({
  selector: 'app-coffee-plan',
  template: `
    <div class="plan" (click)="selectPlan()" [class]="{ 'active-plan': selected() }">
      <div class="description">
        <span class="title"> {{ name() }} </span>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CoffeePlanComponent {
  name = input('Default Plan');
  selected = input(false);

  selectedPlan = output<string>();

  selectPlan() {
    this.selectedPlan.emit(this.name());
  }
}
Enter fullscreen mode Exit fullscreen mode

selected is a signal input that is initialized to false.

The class attribute of the div element is bound to an object. active-plan class is enabled when the getter function of selected returns true. Otherwise, the class is removed from the element and border is not seen.

PlanPicker notifies other CoffeePlan to unselect

  • Vue 3 application
// PlanPicker

<script setup lang="ts">
const selectedPlan = ref('')
function handleSelectPlan(name: string) {
  selectedPlan.value = name
}

function isSelected(plan: string) {
  return selectedPlan.value === plan
}
</script>

<template>
  <div class="plans">
    <CoffeePlan
      v-for="plan in plans"
      :key="plan"
      :name="plan"
      :selected="isSelected(plan)"
      @selectedPlan="handleSelectPlan"
    >
    </CoffeePlan>
  </div>  
</template>
Enter fullscreen mode Exit fullscreen mode

When CoffeePlan emits the selectedPlan event, the handleSelectedPlan updates the selectedPlan ref. The isSelected function determines whether or not a coffee plan is selected. Then, the function assigns the result to the selected input of the CoffeePlan component. When the plan is selected, the CSS adds a border to it. Otherwise, the coffee plan does not display the border.

  • SvelteKit application
<script lang="ts">
    let selectedCoffeePlan = $state('');
    const selectedPlan = (name: string) => (selectedCoffeePlan = name);
    const isSelected = (plan: string) => selectedCoffeePlan === plan;
</script>

<div class="plans">
{#each plans as plan (plan)}
    <CoffeePlan name={plan} {selectedPlan} selected={isSelected(plan)} />
{/each}
</div>
Enter fullscreen mode Exit fullscreen mode

When CoffeePlan emits the selectedPlan event, the selectedPlan updates the selectedCoffeePlan rune. The isSelected function determines whether or not a coffee plan is selected. Then, the function assigns the result to the selected input of the CoffeePlan component. When the plan is selected, the CSS adds a border to it. Otherwise, the coffee plan does not display the border.

  • Angular application

@Component({
  selector: 'app-plan-picker',
})
export class PlanPickerComponent {
  selectedPlan = signal('');

  handleSelectPlan(name: string) {
    this.selectedPlan.set(name);
  }

  isPlanSelected(planName: string): boolean {
    return this.selectedPlan() === planName;
  }
}
Enter fullscreen mode Exit fullscreen mode
<div class="plans">
    @for (plan of plans(); track plan) {
        <app-coffee-plan
            [name]="plan"                           
            (selectedPlan)="handleSelectPlan($event)"
            [selected]="isPlanSelected(plan)"
        />
      }
</div>
Enter fullscreen mode Exit fullscreen mode

When PlanPickerComponent emits the selectedPlan event, the handleSelectPlan method updates the selectedPlan signal. The isPlanSelected method determines whether or not a coffee plan is selected. Then, the method assigns the result to the selected input of the CoffeePlan component. When the plan is selected, the CSS adds a border to it. Otherwise, the coffee plan does not display the border.

Conclusions

We have successfully added new prop and component event to communicate between the CoffeePlan component and thePlanPicker component. The PlanPicker derives the value of theselected and passes it to other CoffeePlan components to enable or disable CSS class dynamically.

Resources

Github Repositories:

Comments 0 total

    Add comment