Angular

Please note, that Angular has 2 modes using components:

  • standalone
  • module based

In Angular 19, which is expected to be released in November 2024, the standalone: true flag will become the default for all components, directives, and pipes.

Standalone components were introduced in Angular 14 as a developer preview in June 2022.

Angular 18 (Standalone Components)

Install dap-design-system inside project folder.

npm i dap-design-system

In angular.json add light.theme.css (just an example, some parts omitted).

"angular-project-name": {
    "architect": {
        "build": {
            "options": {
                "styles": [
                    "src/styles.scss",
                    "dap-design-system/dist/light.theme.css"
                ],
            }
        }
    }
}

In main.ts add import 'dap-design-system/dist/dds.js';

import 'dap-design-system/dist/dds.js';
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));
Adding an Input Component to AppComponent

In app.component.ts add:

  • CUSTOM_ELEMENTS_SCHEMA import and schema
  • also add onDDSInputChanged function.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  imports: [RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {
  title = 'angular-project';

  onDDSInputChanged(event: CustomEvent) {
    console.log(event.detail.value);
  }
}

In app.component.html add a form and dap-ds-input with (dds-change)="onDDSInputChanged($event)"

<h1>App Component</h1>
<form id="input-form">
  <dap-ds-input
    (dds-change)="onDDSInputChanged($event)"
    id="text-input"
    name="text-input"
    required
    label="Required"
    description="Description"
    helperText="Validate for required"
    tooltip="Tooltip content"
    feedbackType="negative" value=""></dap-ds-input>
</form>
Angular 15 (Module Based Components)

Install dap-design-system inside project folder.

npm i dap-design-system

In angular.json add light.theme.css (just an example, some parts omitted).

"angular-project-name": {
    "architect": {
        "build": {
            "options": {
                "styles": [
                    "src/styles.scss",
                    "dap-design-system/dist/light.theme.css"
                ],
            }
        }
    }
}

In main.ts add import 'dap-design-system/dist/dds.js';

import 'dap-design-system/dist/dds.js';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';


platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

In app.module.ts add CUSTOM_ELEMENTS_SCHEMA.

import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule { }
Adding an Input Component to AppComponent

In app.component.ts add custom event function: onDDSInputChanged

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'angular-15';

  onDDSInputChanged(event: CustomEvent) {
    console.log(event.detail.value);
  }
}

In app.component.html add dap-ds-input

<h1>App Component</h1>
<form id="input-form">
  <dap-ds-input
    (dds-change)="onDDSInputChanged($event)"
    id="text-input"
    name="text-input"
    required
    label="Required"
    description="Description"
    helperText="Validate for required"
    tooltip="Tooltip content"
    feedbackType="negative" value=""></dap-ds-input>
</form>
<router-outlet></router-outlet>
Using Web Components with Angular Forms (ControlValueAccessor)

Web components offer the benefit of using components that work anywhere, in any front end framework or technology. While web components work with Angular's template API, they don't have access to Angular's helpful form APIs such as custom validation and form state management. Using Angular Directives with ControlValueAccessor, we can level up our web components to work seamlessly with Angular Forms.

Basic Web Component Usage

Web components primarily communicate via properties and custom events, just like Angular Input and Outputs. However, when used with Angular forms, they don't work as well as custom Angular form controls.

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h2>Web Component</h2>
    <p>{{count}}</p>
    <dap-ds-input 
      [value]="count" 
      (dds-change)="count = $event.detail.value">
    </dap-ds-input>
  `
})
export class AppComponent {
  count = 5;
}
Creating a ControlValueAccessor Directive

To integrate web components with Angular Forms, we can create a directive that implements ControlValueAccessor. This allows us to use the same custom element selector as our web component and attach Angular-specific API logic.

import {
  Directive,
  forwardRef,
  HostBinding,
  Input,
  Output,
  EventEmitter,
  ElementRef,
  HostListener
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Directive({
  selector: 'dap-ds-input',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => DapInputDirective),
      multi: true
    }
  ]
})
export class DapInputDirective implements ControlValueAccessor {
  onChange: any = () => {};
  onTouched: any = () => {};

  private _value: string;

  get value() {
    return this._value;
  }

  set value(val) {
    if (val !== this._value) {
      this._value = val;
      this.onChange(this._value);
      this.onTouched();
      this.elementRef.nativeElement.value = val;
    }
  }

  constructor(private elementRef: ElementRef) { }

  @HostListener('dds-change', ['$event.detail.value'])
  listenForValueChange(value) {
    this.value = value;
  }

  writeValue(value) {
    if (value) {
      this.value = value;
    }
  }

  registerOnChange(fn) {
    this.onChange = fn;
  }

  registerOnTouched(fn) {
    this.onTouched = fn;
  }
}
Using with Reactive Forms

Once the directive is implemented, you can use the web component with Angular's Reactive Forms API:

import { Component } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'my-app',
  template: `
    <h2>Angular Reactive Forms with Web Component</h2>
    <p>Value: {{inputControl.value}}</p>
    <p>Valid: {{inputControl.valid}}</p>
    <dap-ds-input [formControl]="inputControl"></dap-ds-input>
  `
})
export class AppComponent {
  inputControl = new FormControl('', Validators.required);
}
Using with Template-Driven Forms

You can also use the web component with Angular's Template-Driven Forms using ngModel:

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h2>Angular Template Forms with Web Component</h2>
    <p>Value: {{inputValue}}</p>
    <dap-ds-input [(ngModel)]="inputValue"></dap-ds-input>
  `
})
export class AppComponent {
  inputValue = '';
}
Module Configuration

For module-based Angular applications, make sure to include the directive in your module declarations:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { DapInputDirective } from './dap-input.directive';

@NgModule({
  imports: [BrowserModule, ReactiveFormsModule, FormsModule],
  declarations: [AppComponent, DapInputDirective],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
Benefits of ControlValueAccessor Integration

By implementing ControlValueAccessor, your web components gain:

  • Full Angular Forms API support: Access to validation, form state management, and reactive forms
  • Custom validation: Apply Angular validators to web component values
  • Form state tracking: Automatic tracking of touched, dirty, and valid states
  • Reactive forms integration: Use with FormControl, FormGroup, and FormArray
  • Template-driven forms: Support for ngModel and two-way binding
  • Framework-level integration: Maintain the excellent developer experience of Angular's form APIs

This approach gives you the best of both worlds: the reusability of web components and the powerful form management capabilities of Angular.

Reference: Using Web Components in Angular Forms by Cory Rylan