DEV Community

Connie Leung
Connie Leung

Posted on

Day 17 - Render Dynamic Content in HTML Template

Table of Contents

On day 17, I demonstrate how to render dynamic content in a component. Vue 3 projects content to slots and it displays slot props optionally. In Svelte 5, slot is replaced with snippet and the render tag renders the snippet in the template. Angular offers ng-content for content projection and ng-template creates a template fragement that can display in a ng-container.

In this blog post, there are two examples of content projection. The first example updates the text of the Add Plan button when it is hovered. The second example renders a conditional slot in the CoffeePlan component. When a coffee plan is selected, the plan displays the projected icons.

Insert Slot to Project Add Plan Button Text

  • Vue 3 application
const hover = ref(false);
Enter fullscreen mode Exit fullscreen mode
<button type="submit" 
    @mouseenter="hover = true" 
    @mouseleave="hover = false">
    <slot name="btn" :hover="hover" />
</button>
Enter fullscreen mode Exit fullscreen mode

In the AddCoffeePlan component, a named slot is inserted as the child of the button element. hover is a slot prop that returns the value of the hover ref to the PlanPicker component. The hover ref is true when mouseenter event occurs and false when mouseleave event occurs.

The PlanPicker component can use the slot prop value to project the button text.

  • SvelteKit application
<script>
    import type { Snippet } from 'svelte';

    interface Props {
        addPlanButton: Snippet<[boolean]>;
    }

    const { addPlanButton }: Props = $props();

    let hover = $state(false);
</script>
Enter fullscreen mode Exit fullscreen mode
<button type="submit"
    onmouseenter={() => (hover = true)}
    onmouseleave={() => (hover = false)}
>
    {@render addPlanButton(hover)}
</button>
Enter fullscreen mode Exit fullscreen mode

The CoffeePlan component imports the Snippet type to add typing to the addPlanButton prop. addPlanButton is a snippet that expects a boolean parameter. Moveover, hover is a rune that sets to true on mouseenter event and false on mouseleave event.

The @render tag passes the rune to the addPlanButton snippet and renders it.

  • Angular 19 application
@Component({
  ...
})
export class CoffeePlanComponent {
  hover = output<boolean>();
}
Enter fullscreen mode Exit fullscreen mode
<button type="submit" 
    (mouseenter)="hover.emit(true)"
    (mouseleave)="hover.emit(false)"
>
    <ng-content />
</button>
Enter fullscreen mode Exit fullscreen mode

I could not find the equivalent concept of slot prop in Angular. Therefore, I defined a custom hover event to emit the value to the PlanPicker component.

I also inserted a <ng-content> to the button element, so that the PlanPicker component can project the text of the Add Plan button.

Project Add Plan Button Text

  • Vue 3 application
<AddCoffeePlan>
    <template #btn="{ hover }">
        Add Plan  {{ hover ? '(+1)' : '' }}
    </template>
</AddCoffeePlan>
Enter fullscreen mode Exit fullscreen mode

The body of the <AddCoffeePlan> component has a template named #btn. The slot prop is destructured to obtain the hover property. When hover is true, the button text is Add Plan (+1), otherwise, the text is Add Plan.

  • SvelteKit application
<AddCoffeePlan>
    {#snippet addPlanButton(hover: boolean)}
       Add Plan {hover ? '(+1)' : ''}
    {/snippet}  
</AddCoffeePlan>
Enter fullscreen mode Exit fullscreen mode

The addPlanButton snippet is declared inside the AddCoffeePlan component; therefore, it is the implicit prop of the component. Similarly, the button text is Add Plan (+1) when the hover parameter is true. Otherwise, the text is Add Plan.

  • Angular 19 application
<app-add-coffee-plan (hover)="hover.set($event)">
   {{ addPlanText() }}
</app-add-coffee-plan>
Enter fullscreen mode Exit fullscreen mode
export class PlanPickerComponent {
  hover = signal(false);

  addPlanText = computed(() => `Add Plan ${this.hover() ? '(+1)' : ''}`);
}
Enter fullscreen mode Exit fullscreen mode

The hover custom event emits a boolean value that sets the hover signal directly.

Then, the addPlanText computed derives the button text based on the value of the hover signal.

In the body of the <app-add-coffee-plan> component, the addPlanText getter is invoked and the text is projected to the Add Plan button.

Project Conditional Content

When a coffee plan is selected and the plan name starts with 'The', coffee and coffee maker icons are projected to it. When the selected plan does not start with 'The', tea and burger icons are projected. Unselected plan does not display any icon.

  • Install Icon libraries

  • Vue 3

npm install --save-dev @iconify/vue
Enter fullscreen mode Exit fullscreen mode
  • Svelte 5
npm install --save-dev @iconify/svelte
Enter fullscreen mode Exit fullscreen mode
  • Angular
npm install @ng-icons/core @ng-icons/material-icons
Enter fullscreen mode Exit fullscreen mode
  • Create Templates to Render Different Icons

  • Vue 3 Application

   import { Icon } from '@iconify/vue';
Enter fullscreen mode Exit fullscreen mode
<CoffeePlan v-for="plan in plans">
      <template #coffee v-if="isSelected(plan) && plan.startsWith('The')">
        <div class="coffee">
          <Icon class="icon" v-for="icon in ['ic:outline-coffee', 'ic:outline-coffee-maker']" 
            :key="icon" :icon="icon" />
        </div>
      </template>

      <template #beverage v-if="isSelected(plan) && !plan.startsWith('The')">
        <div class="beverage">
          <Icon class="icon" v-for="icon in ['ic:outline-emoji-food-beverage', 'ic:outline-fastfood']" :key="icon" :icon="icon" />
        </div>        
      </template>
</CoffeePlan>
Enter fullscreen mode Exit fullscreen mode

Two named templates are created in the PlanPicker component. The coffee template is rendered when the selected plan starts with 'The'. The template displays the coffee and coffee maker icons. Plan that does not start with 'The', displays the beverage template. The beverage template renders the tea and burger icons.

The icon size is set to 48px in the coffee template. The beverage template has smaller blue icons, 42px.

  • SvelteKit Application
import Icon from "@iconify/svelte";
Enter fullscreen mode Exit fullscreen mode
{#snippet selectedPlanIcons()}
        <div class="coffee">
            {#each ['ic:outline-coffee', 'ic:outline-coffee-maker'] as name (name)}
                <Icon icon={name} width="48" height="48" />
            {/each} 
        </div>
{/snippet}

{#snippet selectedPlanBeverageIcons()}
    <div class="beverage">
    {#each ['ic:outline-emoji-food-beverage', 'ic:outline-fastfood'] as name (name)}
        <Icon icon={name} width="42" height="42" color="blue" />
    {/each}
    </div>
{/snippet}
Enter fullscreen mode Exit fullscreen mode

Similarly, the selectedPlanIcons snippet renders the coffee and coffee maker icons and the selectedPlanBeverageIcons snippet renders the tea and burger icons.

{#each plans as plan (plan)}
     {#if isSelected(plan)}
      <CoffeePlan 
        selectedPlanBeverageIcons={!plan.startsWith('The') ? selectedPlanBeverageIcons : undefined}
        selectedPlanIcons={plan.startsWith('The') ? selectedPlanIcons : undefined} />
      {:else}
      <CoffeePlan name={plan} {selectedPlan} selected={isSelected(plan)} />
      {/if}
{/each}
Enter fullscreen mode Exit fullscreen mode

The snippets are passed to the CoffeePlan component as props. When the selected plan does not start with 'The', the selectedPlanBeverageIcons prop receives the selectedPlanBeverageIcons template. Otherwise, the prop is undefined. When the name starts with 'The', the selectedPlanIcons prop receives the selectedPlanIcons template. Otherwise, the prop is undefined.

  • Angular Application

Angular's NgTemplateOutlet is more suitable for this use case. The PlanPicker component creates two template fragments, coffee and beverage, that render different icons. These fragments are passed to the CoffeePlan component as signal inputs.

import { NgIcon, provideIcons } from '@ng-icons/core';
import {
  matCoffeeMakerOutline,
  matCoffeeOutline,
  matEmojiFoodBeverageOutline,
  matFastfoodOutline,
} from '@ng-icons/material-icons/outline';

@Component({
  selector: 'app-plan-picker',
  imports: [NgIcon],
  viewProviders: [
    provideIcons({ matCoffeeMakerOutline, matCoffeeOutline, matEmojiFoodBeverageOutline, matFastfoodOutline }),
  ],
}) 
export class PlanPickerComponent {}
Enter fullscreen mode Exit fullscreen mode

The PlanPicker component imports the classes and SVG icons from the ng-icons and ng-icons/material-icons libraries.

  <ng-template #coffee>
    <div class="coffee">
      @for (iconName of ['matCoffeeOutline', 'matCoffeeMakerOutline']; track iconName) {
        <ng-icon class="icon" [name]="iconName" />
      }
    </div>
  </ng-template>
Enter fullscreen mode Exit fullscreen mode
.coffee {
  flex: display;
  align-items: center;

  > .icon {
    width: 48px;
    height: 48px;
    color: brown;
  }
}
Enter fullscreen mode Exit fullscreen mode

This template fragment has a template variable, coffee. It renders coffee and coffee maker icons and use simple CSS classes to adjust their dimensions and color.

  <ng-template #beverage>
    <div class="beverage">
      @for (iconName of ['matEmojiFoodBeverageOutline', 'matFastfoodOutline']; track iconName) {
        <ng-icon class="icon" [name]="iconName" />
      }
    </div>
  </ng-template>
Enter fullscreen mode Exit fullscreen mode
.beverage {
  display: flex; 
  flex-direction: column; 
  padding: 0.25rem;

  > .icon {
    width: 42px;
    height: 42px;
    color: green;
  }
}
Enter fullscreen mode Exit fullscreen mode

The beverage template fragment renders tea and burger icons and uses CSS classes to make them blue and 42px big.

@for (plan of plans(); track plan) {
    @let coffeeTemplate = isSelected && plan.startsWith('The') ? coffee : undefined;
    @let beverageTemplate = isSelected && !plan.startsWith('The') ? beverage : undefined;
    <app-coffee-plan
      [coffeeTemplate]="coffeeTemplate"
      [beverageTemplate]="beverageTemplate"
    />
}
Enter fullscreen mode Exit fullscreen mode

The coffeeTemplate input receives the coffee template fragment when the selected plan name begins with 'The'. The beverageTemplate input receives the beverage template fragment for selected plan that does not match the 'The' requirement.

Update CoffeePlan Component to Render Snippets

  • Vue 3 Application
<div class="plan" @click="selectPlan" :class="{ 'active-plan': selected }">
   <template v-if="$slots.coffee">
       <slot name="coffee" />
   </template>

   <div class="description">
       <span class="title"> {{ name }} </span>
   </div>

   <template v-if="$slots.beverage">
      <slot name="beverage" />
   </template>
</div>
Enter fullscreen mode Exit fullscreen mode

When $slots.coffee is true, the PlanPicker component projects the coffee template to the coffee slot. The icons are displayed on the left side of the description.

When $slots.beverage is true, the PlanPicker component projects the beverage template to the beverage slot. The icons are displayed on the right side of the description.

  • Svelte 5 Application
interface Props {
    selectedPlanIcons?: Snippet;
    selectedPlanBeverageIcons?: Snippet;
}

let {
    name = 'Default Plan',
    selectedPlan,
    selected,
    selectedPlanIcons,
    selectedPlanBeverageIcons
}: Props = $props();
Enter fullscreen mode Exit fullscreen mode

In the CoffeePlan component, add optional selectedPlanIcons and selectedPlanBeverageIcons Snippet to the Props interface.

Then, selectedPlanIcons and selectedPlanBeverageIcons are destructured from the component props.

<div>
    {@render selectedPlanIcons?.()}
    <div class="description">
        <span class="title"> {name} </span>
    </div>
    {@render selectedPlanBeverageIcons?.()}
</div>
Enter fullscreen mode Exit fullscreen mode

The render tag renders the optional selectedPlanIcons snippet on the left side of the description. The optional selectedPlanBeverageIcons snippet is rendered on the right side of the description.

  • Angular Application
@Component({
  selector: 'app-coffee-plan',
  imports: [NgTemplateOutlet],  
})
export class CoffeePlanComponent {
    coffeeTemplate = input<TemplateRef<any> | undefined>(undefined);
    beverageTemplate = input<TemplateRef<any> | undefined>(undefined);
}
Enter fullscreen mode Exit fullscreen mode

The CoffeePlan component imports NgTemplateOutlet to use the dirctive. coffeeTemplate and beverageTemplate are optional template references that refer to the fragments in the PlanPicker component.

<div class="plan" (click)="selectPlan()" [class]="{ 'active-plan': selected() }">
      <ng-container [ngTemplateOutlet]="coffee()" />
      <div class="description">
        <span class="title"> {{ name() }} </span>
      </div>
      <ng-container [ngTemplateOutlet]="beverage()" />
</div>
Enter fullscreen mode Exit fullscreen mode

ngTemplateOutlet is an input of the NgTemplateOutlet directive. It accepts a template reference and displays the content of the ng-template.

The first ng-container embeds the coffee template and the second ng-container embeds the beverage template. The coffee icons are rendered on the left side of the description and the beverage icons are rendered on the right side.

When ngTemplateOutlet input is undefined, nothing is rendered.

Conclusions

We have successfully performed content projection and rendered conditional slots in the CoffeePlan component. Vue 3 uses slots to display reusable templates. Svelte 5 introduces snippet and render to achieve the same thing. Angular provides ngContent for projection and ngTemplate to create template fragments that embed dynamic content in a ngContainer.

Resources

Github Repositories:

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.