Here is why you will not find components inside Angular
Component is just a directive with a template? Or is it?
Since I started using Angular, I have been confused by the difference between components and directives, especially for those from the angular.js world, because angular.js only has directives, although we often use them as components. If you search the Internet for an explanation of this question, many will say something like this:
Components are just directives with a content defined in a template…
Angular components are a range of directives. Directives, components always have…
Components are high-order directives with templates and serve as…
All of this seems to be true. When I look at the source code for the view factory generated by the Angular compiler to compile components, I find no component definition. If you look, you’ll only find directives.
Note: Use angular-cli ng new for a new project, run ng serve, You can view the **.ngfactory.js file generated after compiling the component in the ng:// field of the Chrome Dev Tools Source Tab.
I can’t find a reason for this on the web, because you need to be familiar with the inner workings of Angular to know why. If you’ve been wondering for a long time, this article is for you. Let’s explore and prepare.
Essentially, this article explains how Components and directives are defined inside Angular and introduces a new view node definition, directive definition.
Note: View nodes also include element nodes and text nodes. See the Angular DOM update mechanism.
view
If you’ve read my previous articles, especially translating the Angular DOM update mechanism, you probably know that an Angular application is a tree of views. Each view is generated by a view factory, and each view contains a different view node with a specific function. In the article I just mentioned (which is important for understanding this article), I introduced the two simplest node types — element node definitions and text node definitions. The element node definition is used to create all DOM element nodes, and the text node definition is used to create all DOM text nodes.
So if you write a template like this:
<div><h1>Hello {{name}}</h1></div>
Copy the code
Angular Compiler will compile the template and generate two element nodes, the div and H1 DOM elements, and a text node, the Hello {{name}} DOM text. These are important nodes, because without them, you can’t see anything on the screen. But the component composition pattern tells us that components can be nested, so there must be another view node to embed components. To figure out what these special nodes are, you first need to understand what components are made of. In essence, components are essentially DOM elements with specific behaviors that are implemented in component classes. Let’s start with the DOM element.
Custom DOM elements
You probably know that you can create a new HTML tag in HTML. For example, if you don’t use frames, you can simply insert a new tag into HTML:
<a-comp></a-comp>
Copy the code
Then query the DOM node and check the type, and you’ll see that it’s a perfectly legal DOM element (note: you can try this in an HTML file, or even write
a Component
const element = document.querySelector('a-comp');
element.nodeType === Node.ELEMENT_NODE; // true
Copy the code
Browsers create a-comp elements using the HTMLUnknownElement interface, which in turn inherits the HTMLElement interface, but does not need to implement any properties or methods. You can decorate it with CSS, or you can add event listeners to it to listen for common events such as the Click event. So as I said, a-comp is a perfectly legal DOM element.
You can then enhance the element by turning it into a custom DOM element. You need to create a separate class for it and register it using the JS API:
class AComponent extendsHTMLElement {... }window.customElements.define('a-comp', AComponent);
Copy the code
Isn’t that similar to what you’ve been doing?
Yes, this is very similar to defining a component in Angular. In fact, the Angular framework strictly follows Web component standards but simplifies things for us, so we don’t have to create our own shadow root and mount it to the host element. In Chrome Dev Tools, click Settings in the upper right corner and then click Preferences -> Elements. After opening Show user Agent Shadow root, you can see many DOM Elements under Shadow root in the Elements panel. However, components we create in Angular are not registered as custom elements; they are handled by Angular in a specific way. If you’re curious about how to create Components without a framework, you can check out Custom Elements V1: Reusable Web Components.
Now that you know it, you can create any HTML tag and use it in your template. So, if we use this tag in the Angular component template, the framework will create the element definition for the tag (note: this is generated by the Angular Compiler) :
function View_AppComponent_0(_l) {
return jit_viewDef2(0, [
jit_elementDef3(0.null.null.1.'a-comp'[],...). ] )}Copy the code
However, you need to add the schemas to the Module or component decorator property: [CUSTOM_ELEMENTS_SCHEMA] to tell Angular that you are using custom elements, otherwise Angular Compiler will throw an error. So if you need to use a component, you have to in the module. The declarations or module. EntryComponents or component. EntryComponents to register the components) :
'a-comp' is not a known element:
1. If 'c-comp' is an Angular component, then. 2. If'c-comp' is a Web Component then add...
Copy the code
So, we have DOM elements but no classes attached to them. Does Angular have any other special classes besides components? Of course there are — instructions. Let’s see what the instructions are.
Order to define
You probably know that each instruction has a selector that is used to mount a particular DOM element. Most directives use property selectors, but some also select Element selectors. In fact, Angular form directives use element selector forms to attach specific behavior to HTML form elements.
So, let’s create an empty directive class, attach it to the custom element, and see what the view definition looks like:
@Directive({selector: 'a-comp'})
export class ADirective {}
Copy the code
Then check the generated view factory:
function View_AppComponent_0(_l) {
return jit_viewDef2(0, [
jit_elementDef3(0.null.null.1.'a-comp'[],...). , jit_directiveDef4(16384.null.0, jit_ADirective5, [],...) ] .null.null);
}
Copy the code
Now Angular Compiler adds the newly generated directive definition jit_directiveDef4 node to the second parameter array of the view definition function, after the element definition node jit_elementDef3. Also set the childCount of the element definition to 1, because all instructions attached to an element are treated as children of that element.
The directive definition is a simple node definition that is generated by the directiveDef function with the following argument list (note: Angular v5.x is different now) :
Name | Description |
---|---|
matchedQueries | used when querying child nodes |
childCount | specifies how many children the current element have |
ctor | reference to the component or directive constructor |
deps | an array of constructor dependencies |
props | an array of input property bindings |
outputs | an array of output property bindings |
In this article we are only interested in the cTOR argument, which is simply a reference to the ADirective class we defined. When Angular creates a directive object, it instantiates a directive class and stores it in the Provider data property of the view node.
So we see that a component is really just an element definition plus an instruction definition, but is that all? You probably know Angular isn’t always that simple.
Components display
From the above, we can simulate creating a component by creating a custom element and the instructions attached to that element. Let’s define a real component and compare the view factory class compiled by that component to our experimental view factory class above:
@Component({
selector: 'a-comp',
template: '<span>I am A component</span>'
})
export class AComponent {}
Copy the code
Are you ready? Here is the generated view factory class:
function View_AppComponent_0() {
return jit_viewDef2(0, [
jit_elementDef3(0.null.null.1.'a-comp', [],... jit_View_AComponent_04, jit__object_Object_5), jit_directiveDef6(49152.null.0, jit_AComponent7, [], ...)
Copy the code
Ok, so now we’ve just verified the above. In this example, Angular uses two view nodes to represent components — element node definitions and directive node definitions. However, when using a real component, the parameter lists defined by the two nodes are somewhat different. Let’s see what the differences are.
The node type
Node type (NodeFlags) is the first argument to all node-defined functions (note: the argument list is slightly different in Angular V5.*, as in directiveDef, NodeFlags is the second argument). It is actually the NodeFlags bitmask, which contains a set of specific node information, most of which is used by the framework during the change-detection loop. And different node types use different numbers: 16384 indicates the simple instruction node type (note: it is only an instruction, see TypeDirective). 49152 indicates the Component directive node type. To better understand how these flag bits are set by the compiler, let’s first convert to binary:
16384 = 100000000000000 // 15th bit set
49152 = 1100000000000000 // 15th and 16th bit set
Copy the code
If you’re curious about how these transformations work, check out my article The Simple Math Behind Decimal-Binary Conversion Algorithms. So the Angular compiler sets the 15-th bit to 1 for simple directives:
TypeDirective = 1 << 14
Copy the code
For component nodes, the 15-th and 16-th bits are set to 1:
TypeDirective = 1 << 14
Component = 1 << 15
Copy the code
Now you see why these numbers are different. For directives, the generated nodes are marked as TypeDirective nodes; For Component directives, the generated nodes are marked as Component nodes as well as TypeDirective nodes.
The view defines the parser
Because A-comp is a component, for the following simple template:
<span>I am A component</span>
Copy the code
The compiler compiles it, generating a factory function with a view definition and a view node:
function View_AComponent_0(_l) {
return jit_viewDef1(0, [
jit_elementDef2(0.null.null.1.'span'[],...). , jit_textDef3(null['I am A component'])
Copy the code
Angular is a view tree, so the parent view needs to have a reference to its child views, which are stored in the element node. In this example, the A-comp view is stored in the host element node generated for < A-comp >
. You can also look at the _host. ngFactory.js file, which represents the factory of the host element
Note: This paragraph is somewhat obscure because it involves a large number of source code functions. The author describes the detailed process of creating a view, down to many function calls. In summary, just remember one thing: The view parser gets the ViewDefinition by resolving the ViewDefinitionFactory. Don’t worry about the details.
So once you have the view, how do you draw it? See below.
Component renderer type
Angular determines which DOM renderer to use based on the ViewEncapsulation mode defined in the component decorator:
- Emulated Encapsulation Renderer
- Shadow Renderer
- Default Renderer
The above component renderer is created using DomRendererFactory2. The componentRendererType parameter is passed in the element definition, in this case the jit__object_Object_5. The last parameter to jit_elementDef3(), which is the object in the code above, is a basic descriptor for the renderer that determines which renderer component to use. Of these, the most important are the view encapsulation mode and the style applied to the component:
{
styles:[["h1[_ngcontent-%COMP%] {color: green}"]],
encapsulation:0
}
Copy the code
If you define style for a component, the compiler will automatically set the component encapsulation mode to ViewEncapsulation. The Emulated, or you can explicitly set inside the component decorator encapsulation attribute. If not set any style, and also did not explicitly set encapsulation attribute, the descriptor is set to ViewEncapsulation. The Emulated, and neglected to take effect, the use of this descriptor components will use the parent component of component renderer.
The child instruction
Now, the final question is what happens if we apply a directive to a component template like the following:
<a-comp adir></a-comp>
Copy the code
We already know that when generating factory functions for AComponent, the compiler creates element definitions for the A-comp element and directive definitions for the AComponent class. But since the compiler generates instruction definition nodes for each instruction, the factory function of the above template looks like this. Angular v5.* generates a separate * _host. ngfactory.js file for the
function View_AppComponent_0() {
return jit_viewDef2(0, [
jit_elementDef3(0.null.null.2.'a-comp', [],... jit_View_AComponent_04, jit__object_Object_5), jit_directiveDef6(49152.null.0, jit_AComponent7, [], ...)
jit_directiveDef6(16384.null.0, jit_ADirective8, [], ...)
Copy the code
The above code is all familiar, except for the addition of one more instruction definition and an increase in the number of subcomponents to 2.
That’s all!
Note: This article focuses on how components (views) are defined inside Angular with directive nodes and element nodes.