loading...
Cover image for Angular ElementSchemaRegistry for “dummies”

Angular ElementSchemaRegistry for “dummies”

oleksandr profile image Oleksandr Originally published at Medium on ・6 min read

How does Angular know I made an error in a component template?

Let's find out (CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, etc).

*Angular 8.2 source code is used for this article.

I bet every Angular developer met such an error message in ChromeDevTools console:

<some-custom-tag></some-custom-tag>

Usually, this means you forgot to add a component to the module — so Angular doesn’t know anything about it.

Sometimes this error is shown on native HTML tags:

<button data-someAttr=”{{someValue}}>Save</button>

You can watch these errors by running article demo project here.

Yes, we can solve it by using [addr.someAttr]=”someValue” notation, but how does Angular know that button doesn’t have such an attribute? And can we somehow omit Angular checking for our specific cases (custom elements our of Angular or some specific attributes)?

This is time to introduce ElementSchemaRegistry from ‘@angular/compiler’ package.

A more elaborate version of this article is available here .

Prolog

Angular uses two ways to check component templates to be good:

  1. Generating Type Checking Blocks — you can read more about it in Alexey Zuev article here.

  2. Angular compiler instance uses TemplateParser which runs DomElementSchemaRegistry (implementation of ElementSchemaRegistry ) methods for checking of template AST (Abstract syntax tree) nodes validity.

ElementSchemaRegistry is an abstract class that defines an interface for schema class for Angular to check its component templates (if there is such an element or if such attr exists for a specific element, etc). Here it is:

import {SchemaMetadata, SecurityContext} from '../core';

export abstract class ElementSchemaRegistry {

abstract hasProperty (tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean;

abstract hasElement (tagName: string, schemaMetas: SchemaMetadata[]): boolean;

abstract securityContext(elementName: string, propName: string, isAttribute: boolean): SecurityContext;

abstract allKnownElementNames(): string[];

abstract getMappedPropName(propName: string): string;

abstract getDefaultComponentElementName(): string;

abstract validateProperty(name: string): {error: boolean, msg?: string};

abstract validateAttribute(name: string): {error: boolean, msg?: string};

abstract normalizeAnimationStyleProperty(propName: string): string;

abstract normalizeAnimationStyleValue(camelCaseProp: string, userProvidedProp: string, val: string|number): {error: string, value: string};

}

As you can see that it has specific methods to check if some element or some attribute exists: hasElement and hasAttribute.

How does it work under-the-hood?

DomElementSchemaRegistry extends ElementSchemaRegistry

For browser elements checking we have a special class DomElementSchemaRegistry. All DOM entities and its respective attributes are defined there. Here it is:

const SCHEMA: string[] = ['[Element]|textContent,%classList,className,id,innerHTML,*beforecopy,*beforecut,*beforepaste,*copy,*cut,*paste,*search,*selectstart,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerHTML,#scrollLeft,#scrollTop,slot' + /* added manually to avoid breaking changes */      
...
' **base** ^[HTMLElement]|href,target', 

'**body**^[HTMLElement]|aLink,background,bgColor,link,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,text,vLink',
 'button^[HTMLElement]|!autofocus,!disabled,formAction,formEnctype,formMethod,!formNoValidate,formTarget,name,type,value', 

'canvas^[HTMLElement]|#height,#width', 'content^[HTMLElement]|select', 

'dl^[HTMLElement]|!compact', 'datalist^[HTMLElement]|', 

'details^[HTMLElement]|!open', 'dialog^[HTMLElement]|!open,returnValue', 

'dir^[HTMLElement]|!compact', 'div^[HTMLElement]|align', 

'embed^[HTMLElement]|align,height,name,src,type,width', 

'fieldset^[HTMLElement]|!disabled,name',
...

'span^[HTMLElement]|', 

'style ^[HTMLElement]|!disabled,media,type', 

'caption^[HTMLElement]|align', 

'th,td^[HTMLElement]|abbr,align,axis,bgColor,ch,chOff,#colSpan,headers,height,!noWrap,#rowSpan,scope,vAlign,width', 

'col,colgroup^[HTMLElement]|align,ch,chOff,#span,vAlign,width', 

'**table**^[HTMLElement]|align,bgColor,border,%caption,cellPadding,cellSpacing,frame,rules,summary,%tFoot,%tHead,width', 

' **tr** ^[HTMLElement]|align,bgColor,ch,chOff,vAlign', 

'tfoot,thead,tbody^[HTMLElement]|align,ch,chOff,vAlign', 

'template ^[HTMLElement]|', 
'textarea ^[HTMLElement]|autocapitalize,!autofocus,#cols,defaultValue,dirName,!disabled,#maxLength,#minLength,name,placeholder,!readOnly,!required,#rows,selectionDirection,#selectionEnd,#selectionStart,value,wrap', 

'title^[HTMLElement]|text', 

'track^[HTMLElement]|!default,kind,label,src,srclang', 

'ul^[HTMLElement]|!compact,type', 'unknown^[HTMLElement]|', 

'video^media|#height,poster,#width',
...

And the list is created by exporting browser IDL (interface description language) as we can see from compiler-cli/src/ngtsc/typecheck/src/dom.ts file comments (one of the places where DomElementSchemaRegistry is instantiated).

`DomElementSchemaRegistry`, a schema * maintained by the Angular team via extraction from a browser IDL.

OK, so we have a list of DOM entities and set of methods for checking where an HTML template is valid. How does Angular use it?

How is DomElementSchemaRegistry used?

a) AotCompiler (angular/packages/platform-browser-dynamic/src/compiler_factory.ts)

  1. Typescript ts.createProgram starter needs compiler instance from compiler factory function
  2. compiler_factory (lets review AOT) creates schemaParser and feed it to new TemplateParser(…) statement and then templateParser instance are fed to compiler instance constructor (here)
export function createAotCompiler (

compilerHost: AotCompilerHost, options: AotCompilerOptions,

errorCollector?: (error: any, type?: any) =>

void): {compiler: AotCompiler, reflector: StaticReflector} {

...

const elementSchemaRegistry = new DomElementSchemaRegistry();

const tmplParser = new TemplateParser(

config, staticReflector, expressionParser, **elementSchemaRegistry** , htmlParser, console, []);

...

const compiler = new AotCompiler (

config, options, compilerHost, staticReflector, resolver, **tmplParser** ,

new StyleCompiler(urlResolver), viewCompiler, typeCheckCompiler,

new NgModuleCompiler(staticReflector),

new InjectableCompiler(staticReflector, !!options.enableIvy), new TypeScriptEmitter(),

summaryResolver, symbolResolver);

return {compiler, reflector: staticReflector};

}
  1. a) Сompiler instance has two(actually more but we review these two) methods _compileComponent and _createTypeCheckBlock. b) These methods call _parseTemplate where templateParserInstance.parse is performed.

c) And it starts template parsing process (btw visitor pattern is used to iterate over AST nodes) where our DomElementSchemaRegistry instance and TemplateParseVisitor instance are used to check whether AST node (which represents template element) is valid.

For example here and here you can see code that represents messages at the beginning of the article:

private _assertElementExists(matchElement: boolean, element: html.Element) {

const elName = element.name.replace(/^:xhtml:/, '');

if (!matchElement && !this._schemaRegistry.hasElement(elName, this._schemas)) {

let errorMsg = `'${elName}' is not a known element:\n`;

errorMsg +=

`1. If '${elName}' is an Angular component, then verify that it is part of this module.\n`;

if (elName.indexOf('-') > -1) {

errorMsg +=

`2. If '${elName}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.`;

} else {

errorMsg +=

`2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`;

}

this._reportError(errorMsg, element.sourceSpan !);

}

b) JIT compiler (angular/packages/platform-browser-dynamic/src/compiler_factory.ts)

AOT compiler creates an instance for TemplateParser explicitly (you remember that parser then uses schemaRegistry (instance of DomElementSchemaRegistry)).

//angular/angular/blob/8.2.x/packages/compiler/src/aot/compiler_factory.ts

...
const elementSchemaRegistry = new DomElementSchemaRegistry();

const tmplParser = new TemplateParser(config, staticReflector, expressionParser, elementSchemaRegistry, htmlParser, console, []);
...

For instantiating JIT Compiler Angular uses Injector to create respective dependencies instances to its compiler_factory.

// angular/angular/blob/8.2.x/packages/platform-browser-dynamic/src/compiler_factory.ts

export const COMPILER_PROVIDERS = <StaticProvider[]>[
...
{ provide: DomElementSchemaRegistry, deps: []},  
{ provide: ElementSchemaRegistry, useExisting: DomElementSchemaRegistry},
...
]

This means that for JIT build we can substitute DomElementSchemaRegistry with some custom ElementSchema class (we will talk about it later).

Now we know how it works. And? What to do with this knowledge?

Use cases

Continue reading...


I am preparing my future video-course with advanced techniques of mastering Angular/RxJS. Want to get a notification when it is done? Leave your email here (and get free video-course on RxJS unit testing): http://eepurl.com/gHF0av

Like this article? Follow me on Twitter!

Discussion

pic
Editor guide