Skip to content

A Guide to (Typed) Reactive Forms in Angular - Part II (Building Dynamic Superforms)

In the first blog post of the series, we learned about Angular reactive forms and the data structures behind them. When developing real-world applications, however, you often need to leverage dynamic forms, as writing boilerplate for every form and its specific cases can be tedious and time-consuming. In certain situations, it may even be necessary to retrieve information from an API to construct the forms.

In this post, we will go over a convenient abstraction we can create to build dynamic and adaptable forms without repeating boilerplate. The trick is to create a "view model" for our data and use a service to transform that data into a reactive form. I was first introduced to this approach by my friend and former teammate Thomas Duft, and I've been using it ever since.

The approach outlined in the linked article worked great with untyped forms, but since now we can get our forms strictly typed, we'll want to upgrade it.

And here is where it gets a bit tricky. Remember how I mentioned you shouldn't predeclare your form groups earlier? If you want to recursively create a form from a config, you just have to. And it's a dynamic form, so you cannot easily type it. To solve the issue, I devised a trick inspired by a "Super Form" suggested by Bobby Galli. Assuming we will have interfaces defined for our data, using this approach, we can create dynamic type-safe forms.

First, we'll create types for our form config:

// this will be our ViewModel for configuring a FormGroup
export class FormSection<
  T extends {
    [K in keyof T]:
      | FormSection<any>
      | FormField<any>
      | (FormSection<any> | FormField<any>)[];
  } = any
> {
  public title?: string;
  public fields: T;

  constructor(section: {
    title?: string;
    fields: T;
  }) {
    this.title = section.title;
    this.fields = section.fields;
  }
}

// Let's define some editor types we'll be using in the templates later
export type FormEditor =
  | 'textInput'
  | 'passwordInput'
  | 'textarea'
  | 'checkbox'
  | 'select';

// And this will be a ViewModel for our FormControls
export class FormField<T> {
  public value: T;
  public editor: FormEditor;
  public validators: Validators;
  public label: string;
  public required: boolean;
  public options?: T[];

  constructor(field: {
    value: T;
    editor: FormEditor;
    validators: Validators;
    label: string;
    required: boolean;
    options?: T[];
  }) {
    this.value = field.value;
    this.editor = field.editor;
    this.validators = field.validators;
    this.label = field.label;
    this.required = field.required;
    this.options = field.options;
  }
}

And then we'll create some type mappings:

// We will use this type mapping to properly declare our form group
export type ControlsOf<T extends Record<string, any>> = {
  [K in keyof T]: T[K] extends Array<any>
    ? FormArray<AbstractControl<T[K][0]>>
    : T[K] extends Record<any, any>
    ? FormGroup<ControlsOf<T[K]>>
    : FormControl<T[K] | null>;
};

// We will use this type mapping to type our form config
export type ConfigOf<T> = {
  [K in keyof T]: T[K] extends (infer U)[]
    ? U extends Record<any, any>
      ? FormSection<ConfigOf<U>>[]
      : FormField<U>[]
    : T[K] extends Record<any, any>
    ? FormSection<ConfigOf<T[K]>>
    : FormField<T[K]>;
};

And now we can use our types in a service that will take care of creating nested dynamic forms:

import { Injectable } from '@angular/core';
import {
  AbstractControl,
  FormArray,
  FormControl,
  FormGroup,
} from '@angular/forms';
import { ConfigOf, ControlsOf, FormField, FormSection } from './forms.model';

@Injectable({
  providedIn: 'root',
})
export class FormsService {
  public createFormGroup<T extends Record<string, any>>(
    section: FormSection<ConfigOf<T>>
  ): FormGroup<ControlsOf<T>> {
    // we need to create an empty FormGroup first, so we can add FormControls recursively
    const group = new FormGroup({});

    Object.keys(section.fields).forEach((key: any) => {
      const field = section.fields[key];
      if (Array.isArray(field)) {
        group.addControl(key, this.createFormArray(field));
      } else {
        if (field instanceof FormSection) {
          group.addControl(key, this.createFormGroup(field));
        } else {
          group.addControl(key, new FormControl(field.value, field.validators));
        }
      }
    });

    // and we need to cast the group to the correct type before returning
    return group as unknown as FormGroup<ControlsOf<T>>;
  }

  public createFormArray<T extends Record<string, any>>(
    fields: unknown[]
  ): FormArray<AbstractControl<T>> {
    const array: FormArray<AbstractControl<any>> = new FormArray(
      []
    ) as unknown as FormArray<AbstractControl<T>>;

    fields.forEach((field) => {
      if (field instanceof FormSection) {
        array.push(this.createFormGroup(field));
      } else {
        const { value, validators } = field as FormField<T>;
        array.push(new FormControl(value, validators));
      }
    });

    return array as unknown as FormArray<AbstractControl<T>>;
  }
}

And that's it. Now we can use our FormService to create forms. Let's say we have the following User model:

export type User = {
  email: string;
  name: string;
}

We can create a form for this user from config in the following way:

  const userFormConfig = new FormSection<ConfigOf<User>>({
      title: 'User Form',
      fields: {
        email: new FormField<string>({
          value: '',
          validators: [Validators.required, Validators.email],
          label: 'Email',
          editor: 'textInput',
          required: true,
        }),
        name: new FormField<string>({
          value: '',
          validators: [Validators.required],
          label: 'Name',
          editor: 'textInput',
          required: true,
        })
      }
  });

  const userForm = this.formsService.createFormGroup<User>(userFormConfig);

If we would check the type of userForm.value now, we would see that it's correctly inferred as:

Partial<{
    email: string | null;
    name: string | null;
}>

Outputting the Dynamic Forms

To display the dynamic forms, we can write a simple component that takes the FormSection or FormField as an Input() along with our FormGroup and displays the form recursively.

We can use a setter to assign either field or section property when the view model is passed into the component, so we can conveniently use them in our template. Our form component's TypeScript code will look something like this:

import { Component, Input } from '@angular/core';
import { FormField, FormSection } from '../forms.model';
import { FormArray, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.scss'],
})
export class FormComponent {
  private fieldConfig?: FormField<any>;
  private sectionConfig?: FormSection<any>;
  private arrayConfig?: (FormSection<any> | FormField<any>)[];
  private sectionFieldsArray?: [string, FormField<any>][];

  @Input()
  public set config(
    config:
      | FormField<any>
      | FormSection<any>
      | (FormSection<any> | FormField<any>)[]
  ) {
    this.fieldConfig = config instanceof FormField ? config : undefined;
    this.arrayConfig = Array.isArray(config) ? config : undefined;
    this.sectionConfig = config instanceof FormSection ? config : undefined;

    this.sectionFieldsArray = Object.entries(this.sectionConfig?.fields || {});
  }

  public get sectionFields(): [string, FormField<any>][] {
    return this.sectionFieldsArray || [];
  }

  public get field(): FormField<any> | undefined {
    return this.fieldConfig;
  }

  public get section(): FormSection<any> | undefined {
    return this.sectionConfig;
  }

  public get array(): (FormSection<any> | FormField<any>)[] | undefined {
    return this.arrayConfig;
  }

  ngAfterViewInit() {
    console.log(this.arrayConfig);
  }

  @Input()
  public key!: string;

  @Input()
  public group!: FormGroup;

  public get sectionGroup(): FormGroup {
    return this.group.get(this.key) as FormGroup;
  }

  public get formArray(): FormArray {
    return this.group.get(this.key) as FormArray;
  }
}

And our template will reference a new form component for each section field in case we have passed in a FormSection and it will have a switch case to display the correct control in case a FormField has been passed in:

<ng-container *ngIf="field">
  <label>{{ field.label }}</label>
  <div [ngSwitch]="field.editor" [formGroup]="group">
    <textarea *ngSwitchCase="'textarea'" [formControlName]="key"></textarea>
    <input
      *ngSwitchCase="'passwordInput'"
      [formControlName]="key"
      type="input"
    />
    <input *ngSwitchCase="'checkbox'" [formControlName]="key" type="checkbox" />
    <input *ngSwitchDefault [formControlName]="key" type="text" />
  </div>
</ng-container>

<ng-container *ngIf="section">
  <div>
    <h3 *ngIf="section?.title">{{ section.title }}</h3>
    <app-form
      *ngFor="let sectionField of sectionFields"
      [config]="sectionField[1]"
      [key]="sectionField[0]"
      [group]="key ? sectionGroup : group"
    ></app-form>
  </div>
</ng-container>

<ng-container *ngIf="array">
  <div [formGroup]="group">
    <div [formArrayName]="key">
      <div *ngFor="let item of array; let i = index">
        <app-form
          [config]="item"
          [key]="i.toString()"
          [group]="sectionGroup"
        ></app-form>
      </div>
    </div>
  </div>
</ng-container>

That way, we can display the whole form just by referencing one component, such as

 <app-form [config]="formViewModel" [group]="form"></app-form>

Check out an example on StackBlitz.

In the next (and last) post of the series, we will learn about building custom Form Controls.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

A Guide to Custom Angular Attribute Directives cover image

A Guide to Custom Angular Attribute Directives

When working inside of Angular applications you may have noticed special attributes such as NgClass, NgStyle and NgModel. These are special attributes that you can add to elements and components that are known as attribute directives. In this article, I will cover how these attributes are created and show a couple of examples. What are Attribute Directives? Angular directives are special constructs that allow modification of HTML elements and components. Attribute directives are also applied through attributes, hence the name. There exist other types of directives such as structural directives as well, but we’re just going to focus on attribute directives. If you’ve used Angular before then you have almost certainly used a couple of the attribute directives I mentioned earlier before. You are not limited to just the built-in directives though. Angular allows you to create your own! Creating Attribute Directives Directives can be created using code generation via the ng CLI tool. ` ng generate directive ` This will create a file to house your directive and also an accompanying test file as well. The contents of the directive are very barebones to start with. Let’s take a look. ` import { Directive } from '@angular/core'; @Directive({ selector: '[appExample]', }) export class ExampleDirective { constructor() {} } ` You will see here that directives are created using a @Directive decorator. The selector in this case is the name of the attribute as it is intended to be used in your templates. The square brackets around the name make it an attribute selector, which is what we want for a custom attribute directive. I would also recommend that a prefix is always used for directive names to minimize the risk of conflicts. It should also go without saying to avoid using the ng prefix for custom directives to avoid confusion. Now, let’s go over the lifecycle of a directive. The constructor is called with a reference to the ElementRef that the directive was bound to. You can do any initialization here if needed. This element reference is dependency injected, and will be available outside the constructor as well. You can also set up @HostListener handlers if you need to add functionality that runs in response to user interaction with the element or component, and @Input properties if you need to pass data to the directive. Click Away Directive One useful directive that doesn’t come standard is a click away directive. This is one that I have used before in my projects, and is very easy to understand. This directive uses host listeners to listen for user input, and determine whether the element that directive is attached to should be visible or not after the click event occurs. ` @Directive({ selector: '[appClickAway]', }) export class ClickAwayDirective { @Output() onClickAway: EventEmitter = new EventEmitter(); constructor(private elementRef: ElementRef) {} @HostListener('document:click', ['$event']) onClick(event: PointerEvent): void { if (!this.elementRef.nativeElement.contains(event.target)) { this.onClickAway.emit(event); } } } ` There are a few new things in this directive we’ll briefly go over. The first thing is the event emitter output onClickAway. A generic directive isn’t going to know how to handle click away behavior by itself as this will change based on your use case when using the directive. To solve this issue, we make the directive emit an event that the user of the directive can listen for. The other part is the click handler. We use @HostListener to attach a click handler so we can run our click away logic whenever clicks are done. The one interesting thing about this directive is that it listens to all click events since we’ve specified ‘document’ in the first parameter. The reason for this is because we care about listening for clicking anything that isn’t the element or component that the directive is attached to. If we didn’t do this, then the event handler would only fire when clicking on the component the directive is attached to, which defeats the purpose of a click away handler. Once we’ve determined the element was not clicked, we emit the aforementioned event. Using this directive makes it trivial to implement click away functionality for both modals and context menus alike. If we have a custom dialog component we could hook it up like this: ` Dialog Box This is a paragraph with content! ` If you want to see this directive in action, then you can find it in our blog demos repo here. Drag and Drop Directive Another useful directive is one that assists with drag and drop operations. The following directive makes elements draggable, and executes a function with a reference to the location where the element was dragged to. ` @Directive({ selector: '[appDragDrop]', }) export class DragDropDirective implements OnInit, OnDestroy { @Output() onDragDrop: EventEmitter = new EventEmitter(); mouseDown$ = new Subject(); mouseUp$ = new Subject(); destroy$ = new Subject(); constructor(private elementRef: ElementRef) {} ngOnInit(): void { this.mouseDown$ .pipe(takeUntil(this.destroy$)) .pipe(exhaustMap(() => this.mouseUp$.pipe(take(1)))) .subscribe((event) => { if ( event.target && event.target instanceof Element && !this.elementRef.nativeElement.contains(event.target) ) { this.onDragDrop.emit(event); } }); } ngOnDestroy(): void { this.destroy$.next(null); this.destroy$.complete(); } @HostListener('mousedown', ['$event']) onMouseDown(event: MouseEvent): void { this.mouseDown$.next(event); } @HostListener('document:mouseup', ['$event']) onMouseUp(event: MouseEvent): void { this.mouseUp$.next(event); } } ` Just like the previous directive example an event emitter is used so the user of the directive can associate custom functionality with it. RxJs is also utilized for the drag and drop detection. This directive uses the exhaustMap function to create an observable that emits both after a mouse down, and finally a mouse up is done. With that observable, we can subscribe to it and call the drag and drop callback so long as the element that’s dragged on isn’t the component itself. Note how the mouse down event is local to the component while the mouse up event is attached to the document. For mouse down, this is done since we only want the start of the dragging to be initiated from clicking the component itself. The mouse up must listen to the document since the dragging has to end on something that isn’t the component that we’re dragging. Just like the previous directive, we simply need to reference the attribute and register an event handler. ` Drag me over something! ` Conclusion In this article, we have learned how to write our own custom attribute directives and demonstrated a couple of practical examples of directives you might use or encounter in the real world. I hope you found this introduction to directives useful, and that it helps you with writing your own directives in the future! You can find the examples shown here in our blog demos repository if you want to use them yourself....

How to Create Standalone Components in Angular cover image

How to Create Standalone Components in Angular

Angular has become one of the most popular frameworks to build web applications today. One of the key features of the framework is its component-based architecture, which allows best practices like modularity and reusability. Each Angular component consists of a template, a TypeScript class and metadata. In this blog post, we will dive deeper into standalone components, and we will explore the anatomy of an application based on them. For the demo application, we will create a card-like component which can be used to render blog posts in a web application. Prerequisites You'll need to have installed the following tools in your local environment: The latest LTS version of Node.js is recommended. Either NPM or Yarn as a package manager. The Angular CLI tool(Command-line interface for Angular). Initialize the Project Let's create a project from scratch using the Angular CLI tool: `bash ng new demo-angular-standalone-components --routing --prefix corp --style css --skip-tests ` This command will initialize a base project using some configuration options: `--routing`. It will create a routing module. `--prefix corp`. It defines a prefix to be applied to the selectors for created components(`corp` in this case). The default value is `app`. `--style css`. The file extension for the styling files. `--skip-tests`. Disable the generation of testing files for the new project. If you pay attention to the generated files and directories, you'll see an initial project structure including the main application module and component: `txt |- src/ |- app/ |- app.module.ts |- app-routing.module.ts |- app.component.ts ` Creating Standalone Components First, let's create the custom button to be used as part of the Card component later. Run the following command on your terminal: `bash ng generate component button --inline-template --standalone ` It will create the files for the component. The --standalone` option will generate the component as _standalone_. Let's update the button.component.ts` file using the content below. `ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'corp-button', standalone: true, imports: [CommonModule], template: , styleUrls: ['./button.component.css'] }) export class ButtonComponent { } ` Pay attention to this component since it's marked as standalone: true`. Starting with Angular v15: components, directives, and pipes can be marked as standalone by using the flag standalone`. When a class is marked as standalone_, it does not need to be declared as part of an `NgModule`. Otherwise, the Angular compiler will report an error. Also, imports` can be used to reference the dependencies. > The imports property specifies the standalone component's template dependencies — those directives, components, and pipes that can be used within its template. Standalone components can import other standalone components, directives, and pipes as well as existing NgModules. Next, let's create the following components: card-title`, `card-content`, `card-actions` and `card`. This can be done at once using the next commands. `bash ng generate component card-title --inline-template --standalone ng generate component card-content --inline-template --standalone ng generate component card-actions --inline-template --standalone ng generate component card --inline-template --standalone ` On card-title.component.ts` file, update the content as follows: `ts //card-title.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'corp-card-title', standalone: true, imports: [CommonModule], template: , styleUrls: ['./card-title.component.css'] }) export class CardTitleComponent { } ` Next, update the card-content.component.ts` file: `ts //card-content.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'corp-card-content', standalone: true, imports: [CommonModule], template: , styleUrls: ['./card-content.component.css'] }) export class CardContentComponent { } ` The card-actions.component.ts` file should have the content below: `ts // card-actions.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'corp-card-actions', standalone: true, imports: [CommonModule], template: , styleUrls: ['./card-actions.component.css'] }) export class CardActionsComponent { } ` Finally, the card.component.ts` file should be defined as follows: `ts //card.component.ts import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'corp-card', standalone: true, imports: [CommonModule], template: , styleUrls: ['./card.component.css'] }) export class CardComponent { } ` Using Standalone Components Once all Standalone components are created, we can use them without the need to define an NgModule`. Let's update the app.component.ts` file as follows: `ts // app.component.ts import { Component } from '@angular/core'; import { ButtonComponent } from './button/button.component'; import { CardComponent } from './card/card.component'; import { CardTitleComponent } from './card-title/card-title.component'; import { CardContentComponent } from './card-content/card-content.component'; import { CardActionsComponent } from './card-actions/card-actions.component'; @Component({ selector: 'corp-root', standalone: true, imports: [ ButtonComponent, CardComponent, CardTitleComponent, CardContentComponent, CardActionsComponent ], templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'demo-angular-standalone-components'; } ` Again, this Angular component is set as standalone: true` and the `imports` section specifies the other components created as dependencies. Then, we can update the `app.component.html` file and use all the standalone components. `html Latest Posts Introduction to Angular Angular is a component-based framework for building scalable web applications. View Introduction to TypeScript TypeScript is a strongly typed programming language that builds on JavaScript, providing better tooling at any scale. View ` This may not work yet, since we need to configure the Angular application and get rid of the autogenerated module defined under the app.module.ts` file. Bootstrapping the Application Angular provides a new API to use a standalone component as the application's root component. Let's update the main.ts` file with the following content: `ts // main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent); ` As you can see, we have removed the previous bootstrapModule` call and the `bootstrapApplication` function will render the standalone component as the application's root component. You can find more information about it here. Live Demo and Source Code Want to play around with this code? Just open the Stackblitz editor or the preview mode in fullscreen. Conclusion We’re now ready to use and configure the Angular components as standalone and provide a simplified way to build Angular applications from scratch. Do not forget that you can mark directives and pipes as standalone: true`. If you love Angular as much as we do, you can start building today using one of our starter.dev kits or by looking at their related showcases for more examples....

Content Projection in Front-end JavaScript Frameworks cover image

Content Projection in Front-end JavaScript Frameworks

Content Projection in Front-end Frameworks Initially, I wanted to write a nice comprehensive guide on ng-content` and content projection in Angular. But then I found out that my colleague Linda already wrote a wonderful article that covers the topic very well, so I thought about it a little and realized that pretty much every front-end framework has some implementation of the concept. So I decided to write a short article about content projection in general and show how it is implemented in some of the most popular front-end frameworks. What is Content Projection? Some of you may also remember it by the term "transclusion", a bit of a tongue-twister from the Angular.js days. Content projection is a way to take markup (HTML, components...) and slot it right into another component. It's like the Swiss Army knife in our coding toolbox, helping us create reusable components that can fit anywhere. Let's imagine a card component - something you see on pretty much every website these days. It can have a header, a body, maybe some image, and a footer. Now, what you put inside that card can change based on what you need. Maybe today it's a user profile, tomorrow it's a product description, and the day after, it's a blog post. The layout stays the same, but the content? That's entirely up to you, and that's the magic of content projection. Using this approach can help us reduce redundancy in our code and keep it DRY. It also ensures a more consistent user experience. It is, however, important to remember that content projection is not a silver bullet. It can be overused, and it can make your code harder to read and maintain. So, as with everything, use it wisely. How Does it Work Content projection is essentially about two main players: the receiving component (where the content is projected into) and the projecting component (which provides the content to be projected). The receiving component has specific placeholders, often denoted as slots or named slots in frameworks like Vue.js or Web Components. These slots serve as 'parking spaces' for the content from the projecting component. In general, we distinguish two types of content projection: single-slot and multi-slot. Single-slot content projection is when you have only one placeholder for the content. Multi-slot content projection is when you have multiple placeholders for the content. The placeholders can be named or unnamed. Named placeholders are used when you want to project different content into different placeholders. When you're defining the receiving component, you usually specify these slots without defining what will go into them. It's akin to laying out an empty stage for a play, with specific spots designated for props, but not specifying what those props will be. This gives you the flexibility to later decide and alter what content will 'perform' in those spaces. Now, when it comes to the projecting component, that's where the magic happens. Here, you'll take advantage of the slots that the receiving component provides, filling them with the specific content you want to project. For example, you might have a card component with a 'header' slot. When you use this card component elsewhere in your application, you can decide what gets projected into that 'header' slot. It could be a title for one instance of the card and an image for another. Now, let's see the concepts different frameworks use to allow content projection, starting with the Web Components way. The Web Components Way (Slots) As I mentioned before, a very popular way to implement content projection is with slots`. The "slot" approach is not only used in Web Components, but also in Vue.js, Svelte, or Qwik. In Web Components, slots are defined using the ` tag. Let's illustrate how slots` are used in Web Components with an example of a card component with three slots: header, body, and footer. The content that will be projected into these slots will be defined when the component is used. The component will look like this: `html :host { display: block; border: 1px solid #ddd; border-radius: 4px; padding: 20px; margin-bottom: 20px; } ::slotted([slot="header"]) { font-weight: bold; margin-bottom: 10px; } ::slotted([slot="body"]) { margin-bottom: 10px; } ::slotted([slot="footer"]) { text-align: right; color: gray; } class CardComponent extends HTMLElement { constructor() { super(); const template = document.getElementById("card-template"); const templateContent = template.content; this.attachShadow({ mode: "open" }).appendChild( templateContent.cloneNode(true) ); } } customElements.define("card-component", CardComponent); ` This receiving component could be used in a projecting component like this: `html Card Title This is the main content of the card. Click me ` You can check out this example on StackBlitz. Vue.js As mentioned above, Vue.js and other frameworks also use the slot` concept. Let's see how it's implemented in Vue.js. First, the receiving component: `html Default body content here export default { name: "CardComponent", }; .card { border: 1px solid #ddd; border-radius: 4px; padding: 20px; margin-bottom: 20px; } .card-header { font-weight: bold; margin-bottom: 10px; } .card-body { margin-bottom: 10px; } .card-footer { text-align: right; color: gray; } ` And then, the projecting component: `html Card Title This is the main content of the card. Click me import CardComponent from "./CardComponent.vue"; export default { components: { CardComponent, }, }; ` Pretty much the only difference for you as a developer is that you use the v-slot` directive instead of the `slot` attribute. Similarly, Qwik for example uses a `q:slot` attribute. You can check out the Vue.js example on StackBlitz too! The Angular Way (ng-content) In Angular, content projection is implemented using the ` tag. It is a tag that is used to mark the place where the content will be projected. It can be used in two ways: - As a tag with a select` attribute, which is used to select the content that will be projected. The value of the `select` attribute is a CSS selector that is used to select the content that will be projected. If the `select` attribute is not present, all content will be projected. - As a tag without a select` attribute, which will project all content. Let's have look at an example of a receiving card component in Angular: `TypeScript import { Component } from '@angular/core'; @Component({ selector: 'app-card', standalone: true, template: , styles: [ .card { border: 1px solid #ddd; border-radius: 4px; padding: 20px; margin-bottom: 20px; }, .card-header { font-weight: bold; margin-bottom: 10px; }, .card-body { margin-bottom: 10px; }, .card-footer { text-align: right; color: gray; }, ], }) export class CardComponent {} ` And then, the projecting component can be used like this: `html Card Title This is the main content of the card. Click me ` You can check out this example on StackBlitz. As you can see, the Angular way is not very different from the Web Components way. It uses the ` tag instead of the `` tag and a `select` directive instead of the `name` (or `q-name`) attribute, but the principles are very much the same. The React Way (children) So far, we covered the "standard" way of content projection. In React, however, it is not so straightforward as it does not have a concept of "slots" or "content projection". We can still achieve something similar using the children` prop. It is a prop that is used to pass children to a component. It can be used in two ways: - As a prop with a function as a value, which is used to select the content that will be projected. The function will be called for each child and it should return a React element. If the function returns null` or `undefined`, the child will not be rendered. This way allows us to have "multiple content projection". - As a prop without a function as a value, which will project all content. Let's see an example of a receiving card component in React: `jsx import as React from "react"; const styles = { card: { border: "1px solid #ddd", borderRadius: "4px", padding: "20px", marginBottom: "20px", }, header: { fontWeight: "bold", marginBottom: "10px", }, body: { marginBottom: "10px", }, footer: { textAlign: "right", color: "gray", }, }; const CardComponent = ({ header, children, footer }) => { return ( {header} {children} {footer} ); }; export default CardComponent; ` And then, we can project content like this: `jsx import React from "react"; import CardComponent from "./CardComponent"; const App = () => { return ( Card Title} footer={Click me} > This is the main content of the card. ); }; export default App; ` In this example, anything you pass as a header prop will be rendered in the header of the card component, and anything you pass as a footer prop will be rendered in the footer. Any children of the CardComponent will be rendered in the body. Here's the StackBlitz to play with! Conclusion As we've seen, content projection is a powerful and handy concept widely adopted by front-end frameworks to enable reusability, component composition, and dynamic content management. The implementation may vary between different libraries, such as Angular's `, Vue's `slot`s, or React's `children` prop. Nevertheless, the underlying principle remains the same: providing a way to inject custom content into predefined placeholders within a component, thereby extending its flexibility and reusability. We've taken a look at how this is achieved in Angular, Vue.js, Web Components, and even in React. But remember, just like with any tool or skill, it's important to think things through before you start implementing this in your app or library. It might be tempting to start throwing this into every component you build, but it's always worth taking a step back and considering whether it's the right tool for the job. Always aim for code that's not just flexible, but also easy to read, maintain, and efficient to run....

Testing a Fastify app with the NodeJS test runner cover image

Testing a Fastify app with the NodeJS test runner

Introduction Node.js has shipped a built-in test runner for a couple of major versions. Since its release I haven’t heard much about it so I decided to try it out on a simple Fastify API server application that I was working on. It turns out, it’s pretty good! It’s also really nice to start testing a node application without dealing with the hassle of installing some additional dependencies and managing more configurations. Since it’s got my stamp of approval, why not write a post about it? In this post, we will hit the highlights of the testing API and write some basic but real-life tests for an API server. This server will be built with Fastify, a plugin-centric API framework. They have some good documentation on testing that should make this pretty easy. We’ll also add a SQL driver for the plugin we will test. Setup Let's set up our simple API server by creating a new project, adding our dependencies, and creating some files. Ensure you’re running node v20 or greater (Test runner is a stable API as of the 20 major releases) Overview `index.js` - node entry that initializes our Fastify app and listens for incoming http requests on port 3001 `app.js` - this file exports a function that creates and returns our Fastify application instance `sql-plugin.js` - a Fastify plugin that sets up and connects to a SQL driver and makes it available on our app instance Application Code A simple first test For our first test we will just test our servers index route. If you recall from the app.js` code above, our index route returns a 501 response for “not implemented”. In this test, we're using the createApp` function to create a new instance of our Fastify app, and then using the `inject` method from the Fastify API to make a request to the `/` route. We import our test utilities directly from the node. Notice we can pass async functions to our test to use async/await. Node’s assert API has been around for a long time, this is what we are using to make our test assertions. To run this test, we can use the following command: By default the Node.js test runner uses the TAP reporter. You can configure it using other reporters or even create your own custom reporters for it to use. Testing our SQL plugin Next, let's take a look at how to test our Fastify Postgres plugin. This one is a bit more involved and gives us an opportunity to use more of the test runner features. In this example, we are using a feature called Subtests. This simply means when nested tests inside of a top-level test. In our top-level test call, we get a test parameter t` that we call methods on in our nested test structure. In this example, we use `t.beforeEach` to create a new Fastify app instance for each test, and call the `test` method to register our nested tests. Along with `beforeEach` the other methods you might expect are also available: `afterEach`, `before`, `after`. Since we don’t want to connect to our Postgres database in our tests, we are using the available Mocking API to mock out the client. This was the API that I was most excited to see included in the Node Test Runner. After the basics, you almost always need to mock some functions, methods, or libraries in your tests. After trying this feature, it works easily and as expected, I was confident that I could get pretty far testing with the new Node.js core API’s. Since my plugin only uses the end method of the Postgres driver, it’s the only method I provide a mock function for. Our second test confirms that it gets called when our Fastify server is shutting down. Additional features A lot of other features that are common in other popular testing frameworks are also available. Test styles and methods Along with our basic test` based tests we used for our Fastify plugins - `test` also includes `skip`, `todo`, and `only` methods. They are for what you would expect based on the names, skipping or only running certain tests, and work-in-progress tests. If you prefer, you also have the option of using the describe` → `it` test syntax. They both come with the same methods as `test` and I think it really comes down to a matter of personal preference. Test coverage This might be the deal breaker for some since this feature is still experimental. As popular as test coverage reporting is, I expect this API to be finalized and become stable in an upcoming version. Since this isn’t something that’s being shipped for the end user though, I say go for it. What’s the worst that could happen really? Other CLI flags —watch` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--watch —test-name-pattern` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--test-name-pattern TypeScript support You can use a loader like you would for a regular node application to execute TypeScript files. Some popular examples are tsx` and `ts-node`. In practice, I found that this currently doesn’t work well since the test runner only looks for JS file types. After digging in I found that they added support to locate your test files via a glob string but it won’t be available until the next major version release. Conclusion The built-in test runner is a lot more comprehensive than I expected it to be. I was able to easily write some real-world tests for my application. If you don’t mind some of the features like coverage reporting being experimental, you can get pretty far without installing any additional dependencies. The biggest deal breaker on many projects at this point, in my opinion, is the lack of straightforward TypeScript support. This is the test command that I ended up with in my application: I’ll be honest, I stole this from a GitHub issue thread and I don’t know exactly how it works (but it does). If TypeScript is a requirement, maybe stick with Jest or Vitest for now 🙂...