Getting inside Angular's ElementSchemaRegistry mechanism
In this article I'll explore the mechanics of template checking in Angular. We'll become familiar with ElementSchemaRegistry, its implementations like NO_ERRORS_SCHEMA and its usages.

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

Usually, this means you forgot to add a component to the module — so Angular doesn’t know anything about it. Sometimes this error is appears for native HTML tags:

You can see these errors by running article's demo project here.
Although we can easily solve the problem by using [attr.someAttr]="someValue"
notation, the question is how does Angular know that a button doesn’t have such attribute? And can we somehow omit Angular checking for our specific cases, e.g. custom elements our of Angular or some specific attributes? It's time to introduce ElementSchemaRegistry from ‘@angular/compiler’ package.
Some background
Angular uses two ways to check component templates to be good:
- Generating Type Checking Blocks — you can read more about it in Alexey Zuev article here.
- Angular's compiler instance uses
TemplateParser
which runsDomElementSchemaRegistry
(an implementation ofElementSchemaRegistry
) methods for checking 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 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 */
...
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 DomElementSchemaRegistry works
DomElementSchemaRegistry
is used with both AOT and JIT compilers, let's explore the details.
AOT Compiler
- Typescript
ts.createProgram
starter needs compiler instance from compiler factory function - compiler_factory (let's review AOT) creates
schemaParser
and feed it tonew TemplateParser(…)
statement and thentemplateParser
instance are fed to compiler instance constructor (see 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};
}
Сompiler instance has two (actually more but we review these two) methods _compileComponent
and _createTypeCheckBlock
. These methods call _parseTemplate
where templateParserInstance.parse
is performed. 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 !);
}
JIT compiler
AOT compiler creates an instance for TemplateParser
explicitly (you remember that parser then uses schemaRegistry
, which is an 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:
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. The question is what to do with this knowledge? Let's explore some use cases.
Use cases for DomElementSchemaRegistry
Let's start with CUSTOM_ERRORS_SCHEMA when we have custom elements in Angular application.
CUSTOM_ERRORS_SCHEMA
If you ever tried to use custom elements in Angular (you can read about it here) — you already know that to prevent template errors we need to add CUSTOM_ERRORS_SCHEMA
to schemas:[]
property of NgModule
decorator object:
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core
...
@NgModule({
declarations: [AppComponent],
schemas: [ CUSTOM_ELEMENTS_SCHEMA ],
...
})
export class AppModule {}
Why? Because Angular doesn't know anything about custom elements tags and treats them as some error in a template. You may wonder how does adding CUSTOM_ELEMENTS_SCHEMA
prevent Angular from throwing an error? Let's see.
You remember that Template parser uses DomElementSchemaRegistry
instance through _schemaRegistry
to check for the template node's validity. _schemaRegistry
has two methods _schemaRegistry.hasElement
and _schemaRegistry.hasProperty
. Let's take a look at them in dom_element_schema_registry.ts
file:
hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean {
if (schemaMetas.some((schema) => schema.name === NO_ERRORS_SCHEMA.name)) { return true; }
if (tagName.indexOf('-') > -1) {
if (isNgContainer(tagName) || isNgContent(tagName)) {
return false;
}
if (schemaMetas.some((schema) => schema.name === CUSTOM_ELEMENTS_SCHEMA.name)) { return true;}
}
...
}
hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean {
if (schemaMetas.some((schema) => schema.name === NO_ERRORS_SCHEMA.name)) { return true; }
if (tagName.indexOf('-') > -1) {
if (isNgContainer(tagName) || isNgContent(tagName)) { return true; }
if (schemaMetas.some((schema) => schema.name === CUSTOM_ELEMENTS_SCHEMA.name)) { return true; }
}
...
}
Pay attention to that part:
if (tagName.indexOf('-') > -1) {
if (isNgContainer(tagName) || isNgContent(tagName)) {
return false;
}
if (schemaMetas.some((schema) => schema.name === CUSTOM_ELEMENTS_SCHEMA.name)) { return true;}
}
This means that if a template tag has dash (-) in it, this tag is not ng-content
and not ng-container
, and CUSTOM_ELEMENTS_SCHEMA
was added to ngModule
schemas (schemaMetas variable) — then treat it as valid. In other words, Angular just searches for a dash in name and if found it treats it as a custom element.
You can try the code in 2_CUSTOM_ELEMENTS_SCHEMA
branch of the article demo project.
Using NO_ERRORS_SCHEMA for unit testing
Looking at Angular code above you can also understand how NO_ERRORS_SCHEMA
works:
if (schemaMetas.some((schema) => schema.name === NO_ERRORS_SCHEMA.name)) { return true; }
If we added NO_ERRORS_SCHEMA to ngModule decorator schemas prop — Angular doesn't do any checks at all.
It may be useful if you test your component but doesn't want to include child component into TestBed
module configuration.
// my-parent.component.ts
@Component({
selector: "app-my-parent",
template: `
<div>
<child></child> <!-- Don't want to instantiate child -->
</div>
<button (click)="doSomething()">Start</button>
`,
styleUrls: ["./my-parent.component.scss"],
})
export class MyParentComponent {
constructor() {
}
doSomething() {
// some code
}
}
// my-parent.spec.ts
...
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MyParentComponent],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents()
}))
Without NO_ERRORS_SCHEMA
Angular will throw that it doesn't know anything about <child></child>
. But if you don't want to test <child>
then now you know how to skip checking for it. By the way, someone might see it as a bad practice, so it's up for you to decide. You can check the code here in the 3_NO_ERRORS_SCHEMA branch
.
Redefining ElementSchemaRegistry
with a custom class to add exclusion rules
JIT Compiler version
It may be needed if you use not only Angular on your web site so some custom elements (or attributes) should be added to exclusions.
I found a nice example of this case in StackOverflow issue:
There are some custom elements and attributes in component template (in this example they are used by third-party non-Angular code):<foo></foo>
<div data-bar="{{ bar }}"></div>
They cause a compiler error:Template parse errors:
'foo' is not a known element:
How can foo element and data-bar attribute be added to compiler schema?
And the perfect answer from Alexey Zuev:
You can try to override DomElementSchemaRegistry
like this:
//main.ts
import { DomElementSchemaRegistry, ElementSchemaRegistry } from '@angular/compiler'
import { SchemaMetadata } from '@angular/core';
const MY_DOM_ELEMENT_SCHEMA = [
'foo'
];
const MY_CUSTOM_PROPERTIES_SCHEMA = {
'div': {
'bar': 'string'
}
};
export class CustomDomElementSchemaRegistry extends DomElementSchemaRegistry {
constructor() {
super();
}
hasElement(tagName: string, schemaMetas: SchemaMetadata[]): boolean {
return MY_DOM_ELEMENT_SCHEMA.indexOf(tagName) > -1 ||
super.hasElement(tagName, schemaMetas);
}
hasProperty(tagName: string, propName: string, schemaMetas: SchemaMetadata[]): boolean {
const elementProperties = MY_CUSTOM_PROPERTIES_SCHEMA[tagName.toLowerCase()];
return (elementProperties && elementProperties[propName]) ||
super.hasProperty(tagName, propName, schemaMetas);
}
}
platformBrowserDynamic().bootstrapModule(AppModule, {
providers: [{ provide: ElementSchemaRegistry, useClass: CustomDomElementSchemaRegistry, deps: [] }]
});
Now Angular doesn't show template error for <foo>
element.
But this example works in JIT compilation only — check the code at 4_Customizing_element_schema
branch of article demo project. Can we somehow make it work in AOT build?
AOT Compiler version
You remember that JIT Compiler uses COMPILER_PROVIDERS:
{ provide: ElementSchemaRegistry, useExisting: DomElementSchemaRegistry},
So it was easy to substitute ElementSchemaRegistry
with some other custom class like we did in previous example. But it is not possible to do so for AOT Compiler since in AOT compiler factory schemaRegistry
is instantiated explicitly:
const elementSchemaRegistry = new DomElementSchemaRegistry();
To make previous example work in AOT build we have to somehow re-defined DomElementSchemaRegistry.hasProperty (or hasElement)
methods (thanks to Alexey Zuev for providing such approach):
Almost everything can be overwritten in JavaScript.
— Alexey Zuev (@yurzui) February 4, 2020
One option is to create `ng.js` file and run it like
node ng serve --aot
or
node ng build --prod#Angular pic.twitter.com/cvpM07JDyW
Here's the full code example:

DomElementSchemaRegistry.hasElement
Well, how does it work?
We copy original DomElementSchemaRegistry.hasElement
implementation and then expand it with exclusion code (but after that call original method as well). To check if it works — clone this repo and run node ng serve --aot
or node ng build --aot
. Note that ng
in that command is not Angular CLI binary ng
but our ng.js
file.
I implemented the code in ng-new.js
file at 4_Customizing_element_schema
branch of article demo project. Just clone the project, switch to the branch and run: node ng-new build --prod
. No errors! But if you try ng build — prod
with standard Angular CLI command build will fail with template errors.
Imperfections
What I noticed that even if we add an exclusion for custom attributes, like this:
<div bar="{{title}}">Angular removed bar attribute from that div silently</div>
Angular will remove bar
attribute ?. If we want to keep it — you have to use Angular attr
notation, however in this case there's no need to create custom exclusion rule:
<div [attr.bar]="title">Angular keep bar attribute if we use [attr.bar]="" notation</div>
For custom attributes the only case that works with providing schema exclusion is non-dynamic attributes:
<div bar="NonDymanicValue">Angular removed bar attribute from that div silently</div>
Projects that use customized ElementSchema
I know two projects where customized elementSchemaRegistry
is used.
Angular Terminal Platform
How to run an Angular application in a terminal window? Easy :)

And here is a link to schema-registry.ts used in a project. Nothing special — we just validate everything:
// projects/platform-terminal/src/lib/schema-registry.ts
export class TerminalElementSchemaRegistry extends ElementSchemaRegistry {
hasProperty(_tagName: string, _propName: string): boolean {
return true;
}
hasElement(_tagName: string, _schemaMetas: SchemaMetadata[]): boolean {
return true;
}
...
// projects/platform-terminal/src/lib/platform.ts
...
{ provide: ElementSchemaRegistry, useClass: TerminalElementSchemaRegistry, deps: [] },
You should know what this means after reading the article, right? ?
Applications Node-GUI
A project that allows running Angular applications as desktop applications (like Electron app.)

The author uses NodeguiElementSchemaRegistry
that also allows everything by returning true
on every hasElement
and hasProperty
request. And like in Angular terminal project — we re-define ElementSchemaRegistry
in COMPILER_OPTIONS
in angular-nodegui/src/lib/platform-dynamic.ts
file:
provide: COMPILER_OPTIONS,
useValue: {
providers: [{
provide: ElementSchemaRegistry,
useClass: NodeguiElementSchemaRegistry,
deps: []
}
...
Conclusion
So let's wrap it up:
- Angular uses
ElementSchemaRegistry
and its implementationDomElementSchemaRegistry
to check if attributes and elements in the component's template are valid. - An instance of
DomElementSchemaRegistry
is used inTemplateParser
which is used in the compiler to visit all template AST Nodes (JIT and AOT). - We can create our custom rules for validation. It is easy for JIT compiler (
platformBrowserDynamic
) and a bit tricky but also possibly for AOT build. - We can switch off validation by proving
CUSTOM_ELEMENTS_SCHEMA
andNO_ERRORS_SCHEMA
inngModule
config objectschemas
property.
You can find all the code I used in the article here.