SAPUI5 Web Components in Angular

July 19, 2022

Web components are basically custom HTML tags that are defined in JavaScript code and are reusable. They offer the great advantage that they can be used in any frontend framework or technology. Angular provides support for web components and custom elements. But sometimes we wish for deeper integration with the high-level APIs that Angular can provide us. In this post, we will show how we can use Angular Directives to extend SAPUI5 Web Components to work seamlessly with Angular Forms.

blue and pink gradient

What are SAPUI5 web components?

SAPUI5 is an established framework for the development of web-based applications in the enterprise sector. Many companies appreciate its easy integration into the SAP system landscape and the uniform Fiori design concept according to the Fiori Guidelines. A central component of SAPUI5 applications are UI5 controls. These are used to display and control the user interface. They enable a uniform design and a consistent user experience. Examples are simple controls like buttons or inputs but also more complex elements like layouts. SAPUI5 is characterized by comprehensive libraries of UI controls. This means that developers usually do not have to develop new controls.

However, if one does not want to use SAP technologies or SAPUI5 and wants to develop the web applications using other frameworks such as Angular or React, there is a possibility with UI5 Web Components to use UI5 controls independently of the framework used. This means that almost all the features that SAPUI5 offers can be used.

Web components in Angular

Although web components can be easily used in Angular Forms, they do not come with the helpful features such as custom validation and form state management that the Angular Forms API provides.

Using web components in Angular is quite simple. The first step is to tell Angular that we are using custom element tags in our application.

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

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

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

By adding CUSTOM_ELEMENTS_SCHEMA to AppModule, we are making sure that Angular will not throw errors on custom tag elements that do not match the registered Angular components. Now we can import the web components that we want to use. In our example, we will import the Date Picker web component from SAPUI5. To do this, we first need to install the SAPUI5 Web Components. They are provided as ES6 modules via several NPM packages. A list of the most commonly used packages can be found here. After adding the NPM package @ui5/webcomponents, we can import the Date Picker web component.

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

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

import '@ui5/webcomponents/dist/DatePicker';

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

Now we can use the basic Date Picker as follows:

<ui5-date-picker id="myDatepicker1"></ui5-date-picker>

But how can we now achieve our goal of having SAPUI5 Web Components work smoothly with Angular Forms?

Web components communicate mainly through properties and custom events, just like inputs and outputs in Angular. Initially, when we use this web component with an Angular Form, we have no way to use it as a Custom Form Control to leverage custom validation or other Angular Forms APIs. But Angular Directives come to the rescue. Directives allow us to apply the Custom Form Control API to our web components.

Angular Directives

Angular directives are used to attach custom behavior to elements in the DOM; this includes custom element tags. With a directive, we can seamlessly connect our web component - in our case, the SAPUI5 date picker - to the Angular Forms API.

Implementing an abstract class

To get closer to our goal, we first implement an abstract class called AbstractControlValueAccessor. The methods or functionalities can be inherited/derived from the abstract class. Thus, we follow the DRY principle - Don't Repeat Yourself with our abstract class, so that we avoid duplicate code.

With this abstract class, we use an empty @Directive decorator, that is, a directive with no declarations.
We do this because our extended components have their own directive declarations. We will see this in a moment when we implement the directive for the date picker.

After importing the directive from @angular/core, we can use it with the @Directive decorator.

The AbstractControlValueAccessor implements the ControlValueAccessor. A ControlValueAccessor is an interface of Angular. With this interface, Angular will understand when the value of the component changes or is updated and can return this to the Angular form.

We need these four methods provided by ControlValueAccessor:

  • writeValue writes a value to the input
  • registerOnChange to tell Angular when the value of the input changes
  • registerOnTouched to tell Angular when the input has been touched
  • setDisabledState disables the input
import { Directive, ElementRef, Renderer2 } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';

@Directive()
export abstract class AbstractControlValueAccessor
  implements ControlValueAccessor
{
  protected onChange = (_: any) => {};
  protected onTouched = () => {};

  constructor(
    protected _renderer: Renderer2,
    protected _elementRef: ElementRef
  ) {}

  protected setProperty(key: string, value: any): void {
    this._renderer.setProperty(this._elementRef.nativeElement, key, value);
  }

  registerOnChange(fn: (_: any) => {}): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.setProperty('disabled', isDisabled);
  }

  abstract writeValue(obj: any): void;
}

As you can see, we injected Renderer2 and ElementRef in the constructor of our abstract class.

"The Renderer2 class is an abstraction provided by Angular in the form of a service that allows to manipulate elements of your app without having to touch the DOM directly."

In our case, we use the setProperty method of Renderer2 to set a property to elements that have the directive. With the ElementRef we get access to the underlying native element that our directive is attached to.

Implementing the Ui5DatePickerValueAccessorDirective

Now for the exciting part! We will implement the directive Ui5DatePickerValueAccessorDirective for the SAPUI5 DatePicker Web Component.

import { Directive, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { AbstractControlValueAccessor } from './abstract-control-value-accessor.directive';
import formatISO from 'date-fns/formatISO';

@Directive({
  selector:
    'ui5-date-picker[formControlName],ui5-date-picker[formControl],ui5-date-picker[ngModel]',
  host: {
    '(change)': 'onDateChange()',
    '(input)': 'onTouched()',
  },
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => Ui5DatePickerValueAccessorDirective),
      multi: true,
    },
  ],
})
export class Ui5DatePickerValueAccessorDirective extends AbstractControlValueAccessor {
  
  onDateChange() {
    const value: Date = this._elementRef.nativeElement.dateValue;
    this.onChange(value);
  }

  writeValue(obj: any): void {
    if (obj instanceof Date) {
      obj = formatISO(obj, {
        representation: 'date',
      });
    }
    this.setProperty('value', obj);
  }
}

This directive extends the AbstractControlValueAccessor so that its properties and methods can be used in our new directive.
Now we need to provide the NG_VALUE_ACCESSOR to inform Angular that the ControlValueAccessor interface implemented by our AbstractControlValueAccessor can be used. Unlike the AbstractControlValueAccessor, our new directive has a selector, or the combined selectors 'ui5-date-picker[formControlName],ui5-date-picker[formControl],ui5-date-picker[ngModel]'. This selector selects all HTML elements that have an element tag named ui5-date-picker and attributes named formControlName, formControl or ngModel.

The host property is used to bind properties, attributes and events to that particular class, using a set of key-value pairs. SAP's ui5-date-picker maps the change and input events to the corresponding methods of ControlValueAccessor.

The ui5-date-picker provides an input field with an associated calendar.
It consists of the date input field and the date picker.

Our method OnDateChange updates the dateValue, a property of the SAPUI5 DatePicker, and calls the method onChange from our AbstractControlValueAccessor, which gets the dateValue passed.

As you learned earlier, writeValue writes a new value to the input. In our case, a new value property with a suitably formatted date is written to the date input field. It is important that the date entered matches the date format used. Here we use formatISO from the date-fns package to format the date.

Since our directive connects our web component to the Angular Forms API, we can now use ngModel, Reactive Form Controls and Custom Validation.

And that's it! We achieved our goal! Now we can use a web component and get all the advantages from the Angular Forms API. Here you can see an example with ngModel, but you can also use formControlor formControlName:

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

@Component({
  selector: 'app-ui5-date-picker-works-with-angular-forms',
  template: `
    <ui5-date-picker [(ngModel)]="date"></ui5-date-picker>
    
    <p>Date Value double binding: {{ date }}</p>`,
})
export class AppComponent {
  date = new Date('2022-07-19');
}

Technical outlook

Of course, the SAPUI5 date picker has other properties/attributes than the dateValue that are needed to configure the date picker, like placeholder, minDate and maxDate. We can write a directive for this as well, which would save us the trouble of looking up all the properties again and again in the SAPUI5 documentation. This directive would also have to match the ui5-date-picker. There we could define each property/attribute as @Input and add a @HostBinding to pass it directly to the component.

Conclusion

The presented example shows how we get the best of both worlds when using web components in Angular: web component reusability and framework-level integration.

If you want to learn more about the SAPUI5 Web Components, you can read the [documentation](https://sap.github. io/ui5-webcomponents/). In the Angular documentation, you can get a deeper look into the ControlValueAccessor interface of Angular.