20 May 2024
7 min

Debugging techniques – Global utils

Global utils

Debugging is an essential skill for software engineers. Some say that it’s twice as hard as writing the code in the first place.
Hence, it’s important for us to leverage all the available tools and techniques to simplify this task.
In this section we’ll explore some that you will find most useful during your day-to-day work with Angular applications.

We’ll start with the utils that Angular publishes under the global ng namespace. The API can be accessed in the console like this:

Image alt

Most of those methods are self-explanatory and are defined in the publishDefaultGlobalUtils function:

export function publishDefaultGlobalUtils() {
  publishGlobalUtil('ɵsetProfiler', setProfiler);
  publishGlobalUtil('getDirectiveMetadata', getDirectiveMetadata);
  publishGlobalUtil('getComponent', getComponent);
  publishGlobalUtil('getContext', getContext);
  publishGlobalUtil('getListeners', getListeners);
  publishGlobalUtil('getOwningComponent', getOwningComponent);
  publishGlobalUtil('getHostElement', getHostElement);
  publishGlobalUtil('getInjector', getInjector);
  publishGlobalUtil('getRootComponents', getRootComponents);
  publishGlobalUtil('getDirectives', getDirectives);
  publishGlobalUtil('applyChanges', applyChanges);
}

You can see that most of them are defined inside the
discovery utils

First we’ll take a look at those which take a DOM node as an argument.
The following methods are used to retrieve a class instances corresponding to a DOM node:

  • getComponent
  • getDirectives
  • getRootComponents
  • getListeners
  • getOwningComponentLet’s dive in.

Starting with a DOM node

Given a DOM node, you could easily retrieve the component instance that owns the DOM element.
There are 3 methods that can resolve a component instance from a DOM element:

  • getComponent
  • getOwningComponent
  • getRootComponents

Let’s assume we have the following setup:

@Component({
  selector: 'app-root',
  template: `
    <div>i'm just a regular div</div>
    <child-cmp></child-cmp>
  `,
})
export class AppComponent {}

@Component({
  selector: 'child-cmp',
  template: `<div>I am a child component</div>`,
})
export class ChildComponent {}

If you start with a DOM element that is a host element of a component, we could use
getComponent
method:

const childComponentElement = document.querySelector('child-cmp');
const appComponentInstance = ng.getComponent(el.nativeElement);

We could check that it matches the instance of the component by exposing the reference to the instance
inside the constructor of the ChildComponent:

@Component({...})
export class ChildComponent {
  constructor() {
    window.childComponentInstance = this;
  }
}

and running a simple check later:

const childComponentElement = document.querySelector('child-cmp');
const appComponentInstance = ng.getComponent(el.nativeElement);

appComponentInstance === window.childComponentInstance; // true

If you have a reference to a DOM element that belongs to the template (view) of the component, we could use
getOwningComponent
to resolve the component instance whose view contains the DOM element.
This means that if pass it the DOM element corresponding to the ChildComponent,
we’ll get the reference to the parent AppComponent instance:

const instance = ng.getOwningComponent(document.querySelector('child-cmp'));
instance.constructor.name; // AppComponent

This works for any DOM element, not just a component’s host element:

const instance = ng.getOwningComponent(document.querySelector('app-root div'));
instance.constructor.name; // AppComponent

The last method getRootComponents
lets you retrieve a root component of a components tree
(change-detection tree).
Most applications have just one (apart from modals rendered into portals), so calling this method on any node will yield an array of just one top-most component:

const printName = (rootComponent) => rootComponent.constructor.name;

printName(ng.getRootComponents(document.querySelector('app-root div'))[0]); // AppComponent
printName(ng.getRootComponents(document.querySelector('child-cmp'))[0]); // AppComponent
printName(ng.getRootComponents(document.querySelector('child-cmp div'))[0]); // AppComponent

Retrieving directives

To get the directive instances attached to a DOM node use
getDirectives
utility function. With the following example:

@Directive({
  selector: '[custom]',
})
export class CustomDirective {}

@Component({
  selector: 'child-cmp',
  template: `<div custom>I am a child component</div>`,
})
export class ChildComponent {}

the function call will return an instance of the CustomDirective class:

const directives = ng.getDirectives(document.querySelector('[custom]'));
console.log(directives[0].constructor); // class CustomDirective {}

This method does not include component instances, which means calling getDirectives on child-cmp element will return an empty array.

Retrieving event listeners

In the similar fashion you could extract a list of event listeners associated with a DOM element.
If we modify the directive from the previous example to attach an event listener like this:

@Directive({
  selector: '[custom]',
})
export class CustomDirective {
  @HostListener('click') logMe() {}
}

We could use the
getListeners
method to retrieve the listeners attached by the directive:

ng.getListeners(document.querySelector('[custom]'));

This returns an array of event listeners wrapped
inside Angular’s callback:

Image alt

The list though doesn’t include event listeners added outside the Angular context, e.g. directly on DOM through addEventListener method.

Starting with a class instance

You can also go backwards and retrieve a corresponding DOM node if you have an instance of a component or a directive.
Here’s an example that illustrates that:

const hostElement = document.querySelector('child-cmp');
const instance = ng.getComponent(hostElement);
ng.getHostElement(instance) === hostElement; // true

As you can see in the
sources,
it retrieves the relevant context and reads a native DOM node from it:

export function getHostElement(componentOrDirective: {}): Element {
  return getLContext(componentOrDirective)!.native as unknown as Element;
}

The context is a broad topic so let’s talk about it next.

Retrieving context

The
getContext
method takes a DOM element and returns the instance of the component whose view
owns this DOM element. In this way the getContext method is similar to the getOwningComponent method we saw above.

However, when it’s applied to a DOM element that’s part of the embedded view created through a view container (e.g. *ngIf or *templateOutlet),
it retrieves the context of the embedded view that the element is part of.

Here’s an example to illustrate that:

@Component({
  selector: 'app-root',
  template: `
    <div>Just a plain div</div>
    <child-cmp></child-cmp>

    <ng-container *ngTemplateOutlet="t; context: embedCtx"></ng-container>
    <ng-template #t let-name="name">
      <div class="embed">Inside the template: {{ name }}</div>
    </ng-template>
  `,
})
export class AppComponent {
  embedCtx = {
    name: 'Ace',
  };
}

@Component({
  selector: 'child-cmp',
  template: ` <div>I am a child component</div> `,
})
export class ChildComponent {}

So, when we pass the div or child-cmp node inside the getContext method, we’ll get the instance of the AppComponent:

Image alt

But when we query the DOM element inside the ng-template,
we’ll get the context object that was passed to the ngTemplateOutlet directive:

Image alt

It’s the same object that you can access on the instance of the AppComponent.
Here’s how we can test this:

const appComponentInstance = ng.getComponent(
  document.querySelector('app-root')
).embedCtx;
const domElementInsideEmbeddedView = ng.getContext(
  document.querySelector('.embed')
);

appComponentInstance === domElementInsideEmbeddedView; // true

What’s a bit unexpected is that querying a DOM element with a directive applied to it still returns the owning component,
not a directive. It means that if we apply a directive to the div element like this:

@Component({
  selector: 'app-root',
  template: ` <div custom>i'm just a plain div with a directive</div> `,
})
export class AppComponent {}

@Directive({
  selector: '[custom]',
})
export class CustomDirective {}

Query this div element and pass it to the getContext method, we’ll get the reference to the AppComponent instance:

const instance = ng.getContext(document.querySelector('[custom]'));
instance.constructor.name; // 'AppComponent'

To get a directive instance, we need to use getDirectives method we explored above.

Retrieving injector

If we want to retrieve an Injector associated with an element, component or directive instance,
we need to use the getInjector method. For example, given the following setup:

@Component({
  selector: 'app-root',
  template: ` <child-cmp></child-cmp> `,
})
export class AppComponent {}

@Component({
  selector: 'child-cmp',
  template: `<div>I am a child component</div>`,
})
export class ChildComponent {}

To retrieve the injector associated with the ChildComponent,
we query the child-cmp element and pass it to the utility method:

const injector = ng.getInjector(document.querySelector('child-cmp'));

The function returns a NodeInjector instance that wraps around a TNode and LView :

export function getInjector(elementOrDir: Element|{}): Injector {
  const context = getLContext(elementOrDir)!;
  const lView = context ? context.lView : null;
  if (lView === null) return Injector.NULL;

  const tNode = lView[TVIEW].data[context.nodeIndex] as TElementNode;
  return new NodeInjector(tNode, lView);
}

The LView is a pointer to the wrapping parent LView, in our case it’s the view that describes app-root component.
The tNode is the node that describes ChildComponent in the TView created for the AppComponent.

We could also get an equivalent injector through the component’s constructor:

@Component({
  selector: 'child-cmp',
  template: `<div>I am a child component</div>`,
})
export class ChildComponent {
  constructor(injector: Injector) {
    window.inj = injector;
  }
}

It’s a different instance, but contains exactly the same tokens:

const injector = ng.getInjector(document.querySelector('child-cmp'));
inj._tNode === injector._tNode; // true
inj._lView === injector._lView; // true

Triggering change detection

Angular also gives us a way to run change detection from console using applyChanges method. Under the hood it basically marks a component for check (to account for OnPush components) and synchronously performs change detection on the root component of the application:

export function applyChanges(component: {}): void {
  ngDevMode && assertDefined(component, 'component');
  markViewDirty(getComponentViewByInstance(component));
  getRootComponents(component).forEach((rootComponent) =>
    detectChanges(rootComponent)
  );
}

You can think of it as an equivalent of the application wide change detection executed through the tick method on the ApplicationRef.

Let’s observe the effect of running applyChanges by adding a logger function inside the template:

@Component({
  selector: 'child-cmp',
  template: `<div>log()</div>`,
})
export class ChildComponent {
  log() {
    console.log(`Change detection has been executed`);
  }
}

When we run it, we see the logged statement in the console:

Image alt

There’s no debug method to trigger a local change detection.
To do that, you’ll need to retrieve the ChangeDetectorRef from the injector. Getting injector is easy,
but you’ll need the token to get the associated change detector.
Since Angular no longer publishes these tokens at ng.coreTokens, you’ll need to expose the token yourself.
Perhaps like this:

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

(window as any).coreNgTokens = {
  'ChangeDetectorRef': ChangeDetectorRef
}

bootstrapApplication(AppComponent, {}).catch((err) =>
  console.error(err)
);

And then you can obtain the cdRef like this:

const childCmpInjector = ng.getInjector(document.querySelector('child-cmp'));
const cdRef = childCmpInjector.get(coreNgTokens.ChangeDetectorRef);
cdRef.detectChanges();

That’s it for now. We haven’t explored the setProfiler, we’ll do that in a separate section.
Another method we haven’t discussed is
getDirectiveMetadata.
This method is rarely used and when provided with an instance of a directive/component will return the debug (partial) metadata for that component/directive.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.