In the final part of the story about the migration to Angular, we will share the selected places of our internal documentation, helping our developers to get used to the new framework. It will be about the features of the new logic of compiling components, Change Detection and the concept of transkluda. These are actual conventions used right now when working with Angular. Well, in the end - a few links to English-language articles and videos that we recommend to colleagues.
The previous series: first, about the preparation of migration and the second, about the features of work in a hybrid mode .
One of the major changes in Angular is the logic of compiling components.
In AngularJS, we took the HTML of the first component (the highest one), stuffed it into the DOM, waited for the browser to either sparce it, or build a DOM fragment, or paste it into the DOM, get a link to the resulting DOM piece, parsili in it Angular things (directives , components), repeated for each component found.
In Angular, a compilation of templates in js / ts is used in 2 versions - JiT and AoT. At the start of the application (in JiT mode), the frame takes and compiles all (in general, all) the templates of all components from the string from HTML to JS code, which will create the desired piece of DOM. Those. Parsing and building a DOM fragment is completely independent of the browser, Angular does it itself. After that, starting from the top component, it simply starts to execute the corresponding ready-made JS code, creating DOM pieces and immediately inserting them into the right places of the tree. After insertion, additional parsing is not required, since the compiler has already disassembled everything.
AoT compilation is performed on the server / locally during the assembly, takes some time (not very much) and on output gives a heap of data for all components in the form of TS files in a separate directory. They cling to the main application, and TypeScript compilation is already taking into account the copied templates.
In AoT mode, templates are compiled into TypeScript, not JS. This gives the static typing of all templates. You skipped a variable, indicated it as private, or you don't manage it in an inappropriate way (you try to push an object into an input of a component that is waiting for a string) - you get an error when you build it. But this is all only in build mode for production, since because of the ton of added typed code, the build (compiling ts -> js) slows down a bit, and AoT mode is turned off during development (waiting for happy times of multi-threaded compilation or some magic to use AoT in developer mode without suffering).
Links to the topic: the first , second and third .
In Angular, there is no longer a global digest cycle (as well as a list of parameters that may change, and they need to be checked), native binding for events ( addEventListener
and the like) is used to the maximum, but the change detection itself has not gone anywhere.
How does Angular know that something has happened, without separate directives for events and special services for timeouts / intervals?
From the Dart language, which has a separate version of Angular, borrowed a useful concept of zones, which allows you to be aware of when the execution of some code began in a particular zone, and when it ended taking into account asynchronous calls. This all took shape in the zone.js libu, which allows you to create / operate with such zones and is actively used inside the Angular engine. To keep abreast of asynchronous events, the zone.js monkey patch (replaces with its version) the corresponding methods — addEventListener
, setTimeout
, setInterval
, Promise
methods, and the like. This also eliminated the need for separate directives for binding on events / additional services. Accordingly, at the end of any event that is caught in zone.js, Angular pulls the change detection to synchronize the model with the DOM. In dev mode, the twitch twitches 2 times - the second to make sure that nothing has changed after the first one, or give a damn (error in the console) if it has changed; in production always exactly once (no more than ten digest calls of the cycle and the corresponding error).
How does change detection pass if there is no global pool of parameters to be checked?
This is now the work of the components. Each of them may have a set of parameters that are somehow used in DOM or special binding ( HostBinding
, for example). Such parameters are stored in a separate list within the component (and are not known anywhere else).
When Angular pulls the global change detection, then each component (from top to bottom on the tree) itself checks its parameters. In this case, the change detection mechanism may be uncoupled from the component altogether, then its parameters will not be checked in principle. Or it can be configured to check only changes to its inputs ( ChangeDetectionStrategy.OnPush
), then not only will it not check anything, but it will also completely cut off the launch of checks of the components / directives nested in it. Thus, correctly scattering OnPush
, we can exclude entire branches of components from checking for each tick.
It is also possible to execute a piece of code outside or (obviously) inside the Angular zone (in which it catches events and all that). This is useful if we are clinging to a third-party libu, which chchakh all these troubles, it works on its own; and we do not want every event or timeout inside our system to detect all over the component tree. In order to tell the Angulyar later that we have updated the data (obtained from this, which was launched outside the zone he was listening to), we explicitly prescribe the execution of some action (for example, change the local property of the component) in the angular zone.
detectChanges and markForCheck
If we have a component with a detached CD or on a component (on the current or higher in the tree), ChangeDetectionStrategy.OnPush
written, and we have some changes that are not picked up automatically, that is, 2 methods of the local ChangeDetectorRef
service - detectChanges
and markForCheck
:
markForCheck
: from the current component (inclusive) and up the tree to the root component itself marks all views as requiring checking for the nearest tick of the global CD. The tick itself is not pulled at the same time, and the check of the views will occur only if someone else has ticked this tick (or it has already been pulled, but is awaiting the stabilization of changes);detectChanges
: pulls a CD only for a view of the current component, regardless of the state of the detector / input. The standard logic works for all children - if there is OnPush
, then we look at the input, if the change detector is uncoupled, then we don’t watch and ignore anything. Of the features of the work - if the component is in the process of destruction (or is considered destroyed), and its view is uncoupled, then the call to detectChanges
at this moment will throw out an error (for example, they decided to pull on timeout, and the component has already been destroyed due to the change of the route, and we pull the CD on a nonexistent view). Ugly hack - check for an already killed view (such cases should be avoided): if (!this.changeDetectorRef["destroyed"]) { this.changeDetectorRef.detectChanges(); }
When what to use:
OnPush
, and the changes came in a 'legal' way (from api, by timeout, somehow inside the zone with a tick of the global CD), we use markForCheck
;detectChanges
and don’t forget that the component could have been destroyed.ng-content
is NOT the same as ng-transclude
The concept of transcular in Angular has changed a bit (influenced by the way it is done in web components and the features of the angular engine / compiler). Externally, ng-content
looks and has the same capabilities as ng-transclude
: throw something into the component, throw it into a certain place ( select
attribute).
But there is one minor difference - there is no possibility to specify the default content inside ng-content
- and one very important one: the content thrower is responsible for initializing the content that is being pushed to ng-content
, and not the component that contains the ng-content
tag .
Explanation of an example:
@Component({ ... template: ` Visible: {{ visible }} <ng-content *ngIf="visible"></ng-content> `, }) class ProjectionComponent implements OnInit { public visible = false; public ngOnInit() { window.setTimeout(() => this.visible = true, 1000); window.setTimeout(() => this.visible = false, 2000); } } @Component({...}) class ChildComponent implements OnInit, AfterViewInit, OnDestroy { public ngOnInit() { console.log("Child component ngOnInit"); } public ngAfterViewInit() { console.log("Child component ngAfterViewInit"); } public ngOnDestroy() { console.log("Child component ngOnDestroy"); } } @Component({ ... template: ` <vim-projection> <vim-child-component></vim-child-component> </vim-projection> `, }) class ParentComponent {}
Main focus on ngIf
in ProjectionComponent
and console log:
// `Visible` `ProjectionComponent` Child component ngOnInit // Child component ngAfterViewInit // 2 // …
In the first second, despite *ngIf="false"
, the ChildComponent
was initialized (the OnInit
hook worked), and OnDestroy
did not work after 2 seconds, although it OnDestroy
like the components were hidden.
Those. regardless of what happens in the component containing ng-content
, everything that is thrown there will be initialized (called OnInit
hooks). This may be important if there are any queries in the component initialization, heavy processing and the like.
If you need to hide such content, then the one who pushes it should do:
<vim-projection> <vim-child-component *ngIf="visibleInParent"></vim-child-component> </vim-projection>
or use an interesting way to get around this restriction using a TemplateRef:
<div *ngIf="condition" class="smth1"> <ng-container *ngTemplateOutlet="contentTpl"></ng-container> </div> <div *ngIf="!condition" class="smth2"> <ng-container *ngTemplateOutlet="contentTpl"></ng-container> </div> <div *ngIf="condition2" class="smth1"> <ng-container *ngTemplateOutlet="contentWithSelectorTpl"></ng-container> </div> <div *ngIf="!condition2" class="smth2"> <ng-container *ngTemplateOutlet="contentWithSelectorTpl"></ng-container> </div> <ng-template #contentTpl><ng-content></ng-content></ng-template> <ng-template #contentWithSelectorTpl><ng-content select="[some-attribute]"></ng-content></ng-template>
Of the restrictions - to insert such ng-content
can only be in one place at a time.
Also, ng-content
does not allow to specify several identical slots in the template, over which the condition hangs when to output them.
<!-- --> <div *ngIf="smth" class="smth1"> <ng-content></ng-content> </div> <div *ngIf="!smth" class="smth2"> <ng-content></ng-content> </div> <!-- --> <div *ngIf="smth" class="smth1"> <ng-content select="[some-selector]"></ng-content> </div> <div *ngIf="!smth" class="smth2"> <ng-content select="[some-selector]"></ng-content> </div>
In Google, a lot of things on angular 2+, you can safely search for any topics that will be immediately / on the docks are not clear. The people are very active from the release (and even before it) took the frame apart and figured out how it works.
Here are some useful links:
ElementRef, TemplateRef, ViewContainerRef
ViewChildren, ContentChildren, QueryList
ng-template and microsyntax of structural directives
And finally, as always, we remind that we are always looking for cool developers (not just Angular ).
Source: https://habr.com/ru/post/348606/
All Articles