Dojo NG is being built in the open, one section at a time — follow along on Heptapod.

FRAMEWORKS

Angular

Angular binds properties and events on custom elements with its usual syntax. Add CUSTOM_ELEMENTS_SCHEMA so the template compiler accepts the dj-* tags, and add ngDefaultControl to use the text inputs with Angular forms.

Install and register

npm install @dojo-ng/select @dojo-ng/chip @dojo-ng/text-input @dojo-ng/checkbox

Each package registers its tag when you import it. Import the packages once, in main.ts:

// main.ts: import the components once, before bootstrapping
import "@dojo-ng/select";
import "@dojo-ng/chip";
import "@dojo-ng/text-input";
import "@dojo-ng/checkbox";

import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";

bootstrapApplication(AppComponent);

An example

Each component that uses dj-* tags needs CUSTOM_ELEMENTS_SCHEMA in its schemas. Without it, the template compiler reports the tags as unknown elements.

import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from "@angular/core";

@Component({
  selector: "app-root",
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <dj-select
      label="Plan"
      [options]="plans"
      [value]="plan()"
      (change)="plan.set($any($event.target).value)"
    ></dj-select>

    <dj-checkbox
      [checked]="agree()"
      (change)="agree.set($any($event.target).checked)"
    >I agree</dj-checkbox>

    @for (tag of tags(); track tag) {
      <dj-chip closeable (dj-close)="remove(tag)">{{ tag }}</dj-chip>
    }
  `,
})
export class AppComponent {
  plans = [
    { value: "free", label: "Free" },
    { value: "pro", label: "Pro" },
  ];
  plan = signal("free");
  agree = signal(false);
  tags = signal(["design", "docs"]);

  remove(tag: string) {
    this.tags.update((tags) => tags.filter((t) => t !== tag));
  }
}

Properties and attributes

  • [name]="value" sets the property, so arrays and objects arrive as they are, such as [options]="plans". This works even when the tag renders before its package is imported.
  • A plain attribute, such as label="Plan" or closeable, works as in HTML.
  • Use [attr.name] only when you need an attribute, such as an aria-* value.

Events

  • Listen for a Dojo NG event by its exact name, with the dash: (dj-close), (dj-submit), (dj-select).
  • Standard events work as usual: (input), (change), and (click).
  • The event data is in $event.detail. $event.target is typed as a plain EventTarget, so the example uses $any() to read value and checked.

ngModel and reactive forms

Angular's forms attach only to the native inputs they know. Add ngDefaultControl to a Dojo NG text input, and [(ngModel)], [formControl], and formControlName work with it:

import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms";

@Component({
  selector: "app-profile",
  imports: [FormsModule, ReactiveFormsModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <dj-text-input label="Name" ngDefaultControl [(ngModel)]="name"></dj-text-input>
    <dj-text-input label="Email" ngDefaultControl [formControl]="email"></dj-text-input>
  `,
})
export class ProfileComponent {
  name = "";
  email = new FormControl("");
}
  • ngDefaultControl works with controls that have a value property and emit input: the text inputs and the text area.
  • For a checkbox, switch, or select, bind [checked] or [value] and update it in (change), as in the example above.
  • Validation through Angular forms comes from your validators. The browser's own checks, such as required on a Dojo NG control, apply to a native <form>. See Forms & validation.
Next: Svelte Properties, events, and state with Svelte 5.