Skip to content

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 <directive_name_here>

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<PointerEvent> = 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:

<div class="shadow">
  <div class="dialog" appClickAway (onClickAway)="clickAway($event)">
    <h2>Dialog Box</h2>
    <p>This is a paragraph with content!</p>
  </div>
</div>

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<MouseEvent> = new EventEmitter();

  mouseDown$ = new Subject<MouseEvent>();
  mouseUp$ = new Subject<MouseEvent>();
  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.

<div class="drag-drop-demo" appDragDrop (onDragDrop)="drop($event)">
  Drag me over something!
</div>

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.

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

Angular 17: Continuing the Renaissance cover image

Angular 17: Continuing the Renaissance

Angular 17: A New Era November 8th marked a significant milestone in the world of Angular with the release of Angular 17. This wasn't just any ordinary update; it was a leap forward, signifying a new chapter for the popular framework. But what made this release truly stand out was the unveiling of Angular's revamped website, complete with a fresh brand identity and a new logo. This significant transformation represents the evolving nature of Angular, aligning with the modern demands of web development. To commemorate this launch, we also hosted a release afterparty, where we went deep into its new features with Minko Gechev from the Angular core team, and Google Developer Experts (GDEs) Brandon Roberts, Deborah Kurata, and Enea Jahollari. But what exactly are these notable new features in the latest version? Let's dive in and explore. The Angular Renaissance Angular has been undergoing a significant revival, often referred to as Angular's renaissance, a term coined by Sarah Drasner, the Director of Engineering at Google, earlier this year. This revival has been particularly evident in its recent versions. The Angular team has worked hard to introduce many new improvements, focusing on signal-based reactivity, hydration, server-side rendering, standalone components, and migrating to esbuild and Vite for a better and faster developer experience. This latest release, in particular, marks many of these features as production-ready. Standalone Components About a year ago, Angular began a journey toward modernity with the introduction of standalone components. This move significantly enhanced the developer experience, making Angular more contemporary and user-friendly. In Angular's context, a standalone component is a self-sufficient, reusable code unit that combines logic, data, and user interface elements. What sets these components apart is their independence from Angular's NgModule system, meaning they do not rely on it for configuration or dependencies. By setting a standalone: true` flag, you no longer need to embed your component in an NgModule and you can bootstrap directly off that component: `typescript // ./app/app.component.ts @Component({ selector: 'app', template: 'hello', standalone: true }) export class AppComponent {} // ./main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent).catch(e => console.error(e)); ` Compared to the NgModules way of adding components, as shown below, you can immediately see how standalone components make things much simpler. `ts // ./app/app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], }) export class AppComponent { title = 'CodeSandbox'; } // ./app/app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } // .main.ts import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ` In this latest release, the Angular CLI now defaults to generating standalone components, directives, and pipes. This default setting underscores the shift towards a standalone-centric development approach in Angular. New Syntax for Enhanced Control Flow Angular 17 introduces a new syntax for control flow, replacing traditional structural directives like ngIf` or `ngFor`, which have been part of Angular since version 2. This new syntax is designed for fine-grained change detection and eventual zone-less operation when Angular completely migrates to signals. It's more streamlined and performance-efficient, making handling conditional or list content in templates easier. The @if` block replaces `*ngIf` for expressing conditional parts of the UI. `ts @if (a > b) { {{a}} is greater than {{b}} } @else if (b > a) { {{a}} is less than {{b}} } @else { {{a}} is equal to {{b}} } ` The @switch` block replaces `ngSwitch`, offering benefits such as not requiring a container element to hold the condition expression or each conditional template. It also supports template type-checking, including type narrowing within each branch. ```ts @switch (condition) { @case (caseA) { Case A. } @case (caseB) { Case B. } @default { Default case. } } ``` The @for` block replaces `*ngFor` for iteration and presents several differences compared to its structural directive predecessor, `ngFor`. For example, the tracking expression (calculating keys corresponding to object identities) is mandatory but offers better ergonomics. Additionally, it supports `@empty` blocks. `ts @for (item of items; track item.id) { {{ item.name }} } ` Defer Block for Lazy Loading Angular 17 introduces the @defer` block, a dramatically improving lazy loading of content within Angular applications. Within the `@defer` block framework, several sub-blocks are designed to elegantly manage different phases of the deferred loading process. The main content within the `@defer` block is the segment designated for lazy loading. Initially, this content is not rendered, becoming visible only when specific triggers are activated or conditions are met, and after the required dependencies have been loaded. By default, the trigger for a `@defer` block is the browser reaching an idle state. For instance, take the following block: it delays the loading of the calendar-imp` component until it comes into the viewport. Until that happens, a placeholder is shown. This placeholder displays a loading message when the `calendar-imp` component begins to load, and an error message if, for some reason, the component fails to load. `ts @defer (on viewport) { } @placeholder { Calendar placeholder } @loading { Loading calendar } @error { Error loading calendar } ` The on` keyword supports a wide a variety of other conditions, such as: - idle` (when the browser has reached an idle state) - interaction` (when the user interacts with a specified element) - hover` (when the mouse has hovered over a trigger area) - timer(x)` (triggers after a specified duration) - immediate` (triggers the deferred load immediately) The second option of configuring when deferring happens is by using the when` keyword. For example: `ts @defer (when isVisible) { } ` Server-Side Rendering (SSR) Angular 17 has made server-side rendering (SSR) much more straightforward. Now, a --ssr` option is included in the `ng new` command, removing the need for additional setup or configurations. When creating a new project with the `ng new` command, the CLI inquires if SSR should be enabled. As of version 17, the default response is set to 'No'. However, for version 18 and beyond, the plan is to enable SSR by default in newly generated applications. If you prefer to start with SSR right away, you can do so by initializing your project with the `--ssr` flag: `shell ng new --ssr ` For adding SSR to an already existing project, utilize the ng add` command of the Angular CLI: `shell ng add @angular/ssr ` Hydration In Angular 17, the process of hydration, which is essential for reviving a server-side rendered application on the client-side, has reached a stable, production-ready status. Hydration involves reusing the DOM structures rendered on the server, preserving the application's state, and transferring data retrieved from the server, among other crucial tasks. This functionality is automatically activated when server-side rendering (SSR) is used. It offers a more efficient approach than the previous method, where the server-rendered tree was completely replaced, often causing visible UI flickers. Such re-rendering can adversely affect Core Web Vitals, including Largest Contentful Paint (LCP), leading to layout shifts. By enabling hydration, Angular 17 allows for the reuse of the existing DOM, effectively preventing these flickers. Support for View Transitions The new View Transitions API, supported by some browsers, is now integrated into the Angular router. This feature, which must be activated using the withViewTransitions` function, allows for CSS-based animations during route transitions, adding a layer of visual appeal to applications. To use it, first you need to import withViewTransitions`: `ts import { provideRouter, withViewTransitions } from '@angular/router'; ` Then, you need to add it to the provideRouter` configuration: `ts bootstrapApplication(AppComponent, { providers: [ provideRouter(routes, withViewTransitions()) ] }) ` Other Notable Changes - Angular 17 has stabilized signals, initially introduced in Angular 16, providing a new method for state management in Angular apps. - Angular 17 no longer supports Node 16. The minimal Node version required is now 18.13. - TypeScript version 5.2 is the least supported version starting from this release of Angular. - The @Component` decorator now supports a `styleUrl` attribute. This allows for specifying a single stylesheet path as a string, simplifying the process of linking a component to a specific style sheet. Previously, even for a single stylesheet, an array was required under `styleUrls`. Conclusion With the launch of Angular 17, the Angular Renaissance is now in full swing. This release has garnered such positive feedback that developers are showing renewed interest in the framework and are looking forward to leveraging it in upcoming projects. However, it's important to note that it might take some time for IDEs to adapt to the new templating syntax fully. While this transition is underway, rest assured that you can still write perfectly valid code using the old templating syntax, as all the changes in Angular 17 are backward compatible. Looking ahead, the future of Angular appears brighter than ever, and we can't wait to see what the next release has in store!...

A Guide to (Typed) Reactive Forms in Angular - Part III (Creating Custom Form Controls) cover image

A Guide to (Typed) Reactive Forms in Angular - Part III (Creating Custom Form Controls)

So far in the series, we have learned the basics of Angular Reactive forms and created some neat logic to construct and display dynamic forms. But our work is still not done yet. Whether we just want to make our controls look good and enhance them with some markup, or whether we need a more complex control than a simple textarea, input or checkbox, we'll either need to use a component library such as Angular Material Components or get familiar with the ControlValueAccessor` interface. Angular Material, by the way, uses ControlValueAccessor` in its components and I recommend looking into the source code if you want to learn some advanced use cases (I have borrowed a lot of their ideas in the past). In this post, however, we will build a basic custom control from scratch. A common requirement for a component that cannot be satisfied by using standard HTML markup I came across in many projects is having a searchable combobox**. So let's build one. We will start by creating a new Angular component and we can do that with a handy ng cli command: ` ng generate component form-fields/combobox ` Then we'll implement displaying data passed in the form of our FormField` class we have defined earlier in a list and allowing for filtering and selecting the options: `TypeScript // combobox.component.ts import { Component, ElementRef, Input, ViewChild } from '@angular/core'; import { FormField } from '../../forms.model'; @Component({ selector: 'app-combobox', templateUrl: './combobox.component.html', styleUrls: ['./combobox.component.scss'], }) export class ComboboxComponent { private filteredOptions?: (string | number)[]; // a simple way to generate a "unique" id for each component // in production, you should rather use a library like uuid public id = String(Date.now() + Math.random()); @ViewChild('input') public input?: ElementRef; public selectedOption = ''; public listboxOpen = false; @Input() public formFieldConfig!: FormField; public get options(): (string | number)[] { return this.filteredOptions || this.formFieldConfig.options || []; } public get label(): string { return this.formFieldConfig.label; } public toggleListbox(): void { this.listboxOpen = !this.listboxOpen; if (this.listboxOpen) { this.input?.nativeElement.focus(); } } public closeListbox(event: FocusEvent): void { // timeout is needed to prevent the list box from closing when clicking on an option setTimeout(() => { this.listboxOpen = false; }, 150); } public filterOptions(filter: string): void { this.filteredOptions = this.formFieldConfig.options?.filter((option) => { return option.toString().toLowerCase().includes(filter.toLowerCase()); }); } public selectOption(option: string | number): void { this.selectedOption = option.toString(); this.listboxOpen = false; } } ` `HTML {{ label }} &#9660; {{ option }} ` > Note: For the sake of brevity, we will not be implementing keyboard navigation and aria labels. I strongly suggest referring to W3C WAI patterns to get guidelines on the markup and behavior of an accessible combo box. While our component now looks and behaves like a combo box, it's not a form control yet and is not connected with the Angular forms API. That's where the aforementioned ControlValueAccessor` comes into play along with the `NG_VALUE_ACCESSOR` provider. Let's import them first, update the `@Component` decorator to provide the value accessor, and declare that our component is going to implement the interface: `TypeScript import { ControlValueAccessor, NGVALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'app-combobox', templateUrl: './combobox.component.html', styleUrls: ['./combobox.component.scss'], providers: [ { // provide the value accessor provide: NGVALUE_ACCESSOR, // for our combobox component useExisting: ComboboxComponent, // and we don't want to override previously provided value accessors // we want to provide an additional one under the same "NGVALUE_ACCESSOR" token instead multi: true, }, ], }) export class ComboboxComponent implements ControlValueAccessor { ` Now, the component should complain about a few missing methods that we need to satisfy the ControlValueAccessor` interface: - A writeValue` method that is called whenever the form control value is updated from the forms API (e.g. with `patchValue()`). - A registerOnChange` method, which registers a callback function for when the value is changed from the UI. - A registerOnTouched` method that registers a callback function that marks the control when it's been interacted with by the user (typically called in a `blur` handler). - An optional setDisabledState` method that is called when we change the form control `disabled` state- Our (pretty standard) implementation will look like the following: `TypeScript private onChanged!: Function; private onTouched!: Function; public disabled = false; // This will write the value to the view if the form control is updated from outside. public writeValue(value: any) { this.value = value; } // Register a callback function that is called when the control's value changes in the UI. public registerOnChange(onChanged: Function) { this.onChanged = onChanged; } // Register a callback function that is called by the forms API on initialization to update the form model on blur. public registerOnTouched(onTouched: Function) { this.onTouched = onTouched; } public setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } public setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } ` We don't have to update the template a lot, but we can add [disabled]="disabled"` attribute on our button and input to disable the interactive UI elements if the provided form control was disabled. The rest of the work can be done in the component's TypeScript code. We'll call `this.onTouched()` in our `closeListbox` method, and create a `value` setter that updates our internal value and also notifies the model about the value change: `TypeScript public set value(val: string | number) { this.selectedOption = val.toString(); this.onChanged && this.onChanged(this.selectedOption); this.onTouched && this.onTouched(); } ` You can check out the full implementation on StackBlitz. Conclusion In this series, we've explored the powerful features of Angular reactive forms, including creating and managing dynamic typed forms. We also demonstrated how to use the ControlValueAccessor interface to create custom form controls, such as a searchable combo box. This knowledge will enable you to design complex and dynamic forms in your Angular applications. While the examples provided here are basic, they serve as a solid foundation for building more advanced form controls and implementing validation, accessibility, and other features that are essential for a seamless user experience. By mastering Angular reactive forms and custom form controls, you'll be able to create versatile and maintainable forms in your web applications. If you want to further explore the topic and prefer a form of a video, you can check out an episode of JavaScript Marathon by my amazing colleague Chris. Happy coding!...

A Tale of Form Autofill, LitElement and the Shadow DOM cover image

A Tale of Form Autofill, LitElement and the Shadow DOM

Many web applications utilize forms in places be it for logging in, making payments, or editing a user profile. As a user of web applications, you have probably noticed that the browser is able to autofill in certain fields when a form appears so that you don't have to do it yourself. If you've ever written an application in Lit though, you may have noticed that this doesn't always work as expected. The Problem I was working on a frontend project utilizing Lit and had to implement a login form. In essence these aren’t very complicated on the frontend side of life. You just need to define a form, put some input elements inside of it with the correct type attributes assigned to it, then you hook the form up to your backend, API, or whatever you need to call to authenticate by adding a submit handler. However, there was an issue. The autocomplete doesn’t appear to be working as expected. Only the username field was being filled, but not the password. When this happened, I made sure to check documentation sites such as MDN and looked at examples. But I couldn’t find any differences between theirs and mine. At some point, I prepared a minimal reproducible example without Lit, and I was able to get the form working fine, so it had to do something with my usage of Lit. After doing a little bit of research and some testing, I found out this happened because Lit relies very heavily on something known as the Shadow DOM. I don’t believe the Shadow DOM is necessarily supposed to break this functionality. But for most major browsers, it doesn’t play nice with autocomplete for the time being. I experienced slightly different behavior in all browsers, and the autocomplete even worked under Shadow DOM with Firefox in the Lit app I was working on. The solution I ended up settling on was ensuring the form was contained inside of the Light DOM instead of the Shadow DOM, whilst also allowing the Shadow DOM to continue to be used in places where autofillable forms are not present. In this article I will show you how to implement this solution, and how to deal with any problems that might arise from it. Shadow DOM vs. Light DOM The Shadow DOM is a feature that provides a way to encapsulate your components and prevent unrelated code and components from affecting them in undesired ways. Specifically, it allows for a way to prevent outside CSS from affecting your components and vice versa by scoping them to a specific shadow root. When it comes to the Light DOM, even if you’ve never heard of the term, you’ve probably used it. If you’ve ever worked on any website before, and interacted with the standard DOM tree, that is the Light DOM. The Light DOM, and any Shadow DOMs under it for that matter, can contain Shadow DOMs inside of them attached to elements. When you add a Lit component to a page, a shadow root will get attached to it that will contain its subelements, and prevent CSS from outside of that DOM from affecting it. Using Light DOM with Certain Web Components By default, Lit attaches a shadow root to all custom elements that extend from LitElement. However, web components don’t actually require a shadow root to function. We can do away with the shadow root by overriding the createRenderRoot method, and returning the web component itself: ` createRenderRoot(): ShadowRoot | this { return this; } ` Although we can just put this method in any element we want exposed into the Light DOM. We can also make a new component called LightElement that overrides this method that we can extend from instead of LitElement on our own components. This will be useful later when we tackle another problem. Uh oh, where did my CSS styling and slots go? The issue with not using a shadow root is Lit has no way to encapsulate your component stylesheets anymore. As a result, your light components will now inherit styles from the root that they are contained in. For example, if your components are directly in the body of the page, then they will inherit all global styles on the page. Similarly when your light components are inside of a shadow root, they will inherit any styles attached to that shadow root. To resolve this issue, one could simply add style tags to the HTML template returned in the render()` method, and accept that other stylesheets in the same root could affect your components. You can use naming conventions such as BEM for your CSS classes to mitigate this for the most part. Although this does work and is a very pragmatic solution, this solution does pollute the DOM with multiple duplicate stylesheets if more than one instance of your component is added to the DOM. Now, with the CSS problem solved, you can now have a functional Lit web component with form autofill for passwords and other autofillable data! You can view an example using this solution here. A Better Approach using Adopted Stylesheets For a login page where only one instance of the component is in the DOM tree at any given point, the aforementioned solution is not a problem at all. However, this can become a problem if whatever element you need to use the Light DOM with is used in lots of places or repeated many times on a page. An example of this would be a custom input element in a table that contains hundreds of rows. This can potentially cause performance issues, and also pollute the CSS inspector in your devtools resulting in a suboptimal experience both for users and yourself. The better, though still imperfect, way to work around this problem is to use the adopted stylesheets feature to attach stylesheets related to the web component to the root it is connected in, and reuse that same stylesheet across all instances of the node. Below is a function that tracks stylesheets using an id and injects them in the root node of the passed in element. Do note that, with this approach, it is still possible for your component’s styles to leak to other components within the same root. And like I advised earlier, you will need to take that into consideration when writing your styles. ` export function injectSharedStylesheet( element: Element, id: string, content: string ) { const root = element.getRootNode() as DocumentOrShadowRoot; if (root.adoptedStyleSheets != null) { evictDisconnectedRoots(); const rootNodes = documentStylesheets[id] ?? []; if (rootNodes.find(value => value === root)) { return; } let sharedStylesheet = sharedStylesheets[id]; if (sharedStylesheet == null) { sharedStylesheet = new CSSStyleSheet(); sharedStylesheet.replaceSync(content); sharedStylesheets[id] = sharedStylesheet; } root.adoptedStyleSheets.push(sharedStylesheet); if (documentStylesheets[id] != null) { documentStylesheets[id].push(root); } else { documentStylesheets[id] = [root]; } } else { // FALLBACK: Inject manually into the document if adoptedStyleSheets // is not supported. const target = root === document ? document.head : root; if (target?.querySelector(#${id}`)) { return; } const styleElement = document.createElement('style'); styleElement.id = id; styleElement.appendChild(document.createTextNode(content)); target.appendChild(styleElement); } } ` This solution works for most browsers, and a fallback is included for Safari as it doesn’t support adoptedStylesheets at the time of writing this article. For Safari we inject de-duplicated style elements at the root. This accomplishes the same result effectively. Let’s go over the evictDisconnectedRoots function that was called inside of the injection function. We need to ensure we clean up global state since the injection function relies on it to keep duplication to a minimum. Our global state holds references to document nodes and shadow roots that may no longer exist in the DOM. We want these to get cleaned up so as to not leak memory. Thankfully, this is easy to iterate through and check because of the isConnected property on nodes. ` function evictDisconnectedRoots() { Object.entries(documentStylesheets).forEach(([id, roots]) => { documentStylesheets[id] = roots.filter(root => root.isConnected); }); } ` Now we need to get our Lit component to use our new style injection function. This can be done by modifying our LightElement component, and having it iterate over its statically defined stylesheets and inject them. Since our injection function contains the de-duplication logic itself, we don’t need to concern ourselves with that here. ` import { CSSResult, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { injectSharedStylesheet } from './style-injector.js'; export interface SharedStylesheet { id: string; content: CSSResult; } @customElement('light-element') export class LightElement extends LitElement { static sharedStyles: SharedStylesheet[] = []; connectedCallback() { const { sharedStyles } = this.constructor as any; if (sharedStyles) { sharedStyles.forEach((stylesheet: SharedStylesheet) => { injectSharedStylesheet( this, stylesheet.id, stylesheet.content.toString() ); }); } super.connectedCallback(); } createRenderRoot(): ShadowRoot | this { return this; } } ` With all that you should be able to get an autocompletable form just like the previous example. The full example using the adopted stylesheets approach can be found here. Conclusion I hope this article was helpful for helping you figure out how to implement autofillable forms in Lit. Both examples can be viewed in our blog demos repository. The example using basic style tags can be found here, and the one using adopted stylesheets can be found here....

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 🙂...