In Angular project development, we usually use Input property binding and Output event binding for component communication. However, Input and Output can only be used in parent-child components. transmit information. Components form a component tree based on the calling relationship. If there are only property bindings and event bindings, then two non-direct relationship components need to communicate through each connection point itself. The middleman needs to continuously process and pass some things that it does not need to know. information (Figure 1 left). The Injectable Service provided in Angular can be provided in modules, components or instructions, and combined with injection in the constructor, can solve this problem (right in Figure 1). [Recommended related tutorials: "angular tutorial"]
Figure 1 Component communication model
The left picture only transmits information through parent-child components. Node a and node b need to pass through many nodes to communicate; if node c wants to control node b through some configuration, the nodes between them must also set additional attributes or events to transparently transmit corresponding information. The dependency injection mode node c in the picture on the right can provide a service for nodes a and b to communicate. Node a directly communicates with the service provided by node c, and node b also directly communicates with the service provided by node c. Finally, the communication is simplified, and the middle The node is not coupled to this part of the content, and has no obvious awareness of the communication that occurs between the upper and lower components.
Dependency injection (DI) is not unique to Angular. It is a means to implement the inversion of control (IOC) design pattern. The emergence of dependency injection solves the problem of over-coupling of manual instantiation, and all resources cannot be used. Management of resources by both parties, rather than being provided by resource centers or third parties that do not use resources, can bring many benefits. First, centralized management of resources makes resources configurable and easy to manage. Second, it reduces the degree of dependence between the two parties using resources, which is what we call coupling.
An analogy to the real world is that when we buy a product such as a pencil, we only need to find a store to buy a pencil-type product. We don't care where the pencil is produced or how the wood and pencil lead are bonded. We just need it to complete the writing function of a pencil. We will not have any contact with the specific pencil manufacturer or factory. As for the store, it can purchase pencils from the appropriate channels and realize the configurability of resources.
Combined with the coding scenario, more specifically, users do not need to explicitly create instances (new operations) to inject and use instances. The creation of instances is determined by providers. Resource management is through tokens. Since it does not care about the provider or the creation of instances, the user can use some local injection methods (secondary configuration of tokens) to finally achieve instance replacement and dependency injection mode. Applications and aspect programming (AOP) complement each other.
Dependency injection is one of the most important core modules of the Angular framework. Angular not only provides Service type injection, but the component tree itself is an injection dependency tree, and functions and values can also be injected. That is to say, in the Angular framework, child components can inject parent component instances through the parent component's token (usually the class name). In component library development, there are a large number of cases where interaction and communication are achieved by injecting parent components, including parameter mounting, state sharing, and even obtaining the DOM of the node where the parent component is located, etc.
To use Angular injection, you must first understand its injection resolution process. Similar to the parsing process of node_modules, when no dependencies are found, the dependencies will always bubble up to the parent layer to find dependencies. The old version of Angular (before v6) divides the injection parsing process into multi-level module injectors, multi-level component injectors and element injectors. The new version (after v9) is simplified to a two-level model. The first query chain is the static DOM level element injector, component injector, etc., collectively called element injectors, and the other query chain is the module injector. The order of parsing and the default value after parsing failure are explained more clearly in the official code comment document (provider_flag).
Figure 2 The dependency search process of the two-level injector (picture source)
means that components/instructions and providing injected content at the component/instruction level will first search for dependencies in the elements in the component view all the way to the root element. If not found, then in the element The current module, the reference (including module reference and routing lazy loading reference) of the module's parent module is searched up until the root module and platform module.
Note that the injector here has inheritance. The element injector can create and inherit the lookup function of the injector of the parent element, and the module injector is similar. After continuous inheritance, it becomes a bit like the prototype chain of js objects.
understand the order priority of dependency resolution, and we can provide content at the appropriate level. We already know that it comes in two types: module injection and element injection.
Module injector: Providers can be configured in the metadata attribute of @NgModule, and you can also use the @Injectable statement provided after v6. provideIn is declared as the module name, 'root', etc. (Actually, there are two injectors above the root module, Platform and Null. They will not be discussed here.)
Element injector: Providers, viewProviders can be configured in the metadata attribute of the component @Component, or in the @ of the directive providers in Directive metadata.
In addition, in fact, in addition to using the declared module injector, the @Injectable decorator can also be declared as an element injector. More often it will be declared as provided at root to implement a singleton. It integrates metadata through the class itself to avoid modules or components directly explicitly declaring the provider. In this way, if the class does not have any component directive service and other classes to inject it, there will be no code linked to the type declaration and it can be ignored by the compiler, thus achieving Shake the tree.
Another way to provide it is to directly give the value when declaring InjectionToken.
Here are the shorthand templates for these methods:
@NgModule({ providers: [ //Module injector] }) export class MyModule {}
@Component({ providers: [ // element injector - component], viewProviders: [ //Element Injector - Component View] }) export class MyComponent {}
@Directive({ providers: [ // element injector - directive] }) export class MyDirective {}
@Injectable({ providedIn: 'root' }) export class MyService {}
export const MY_INJECT_TOKEN = new InjectionToken<MyClass>('my-inject-token', { providedIn: 'root', factory: () => { return new MyClass(); } });
Different options for providing dependency locations will bring some differences, which ultimately affect the size of the package, the scope in which dependencies can be injected, and the life cycle of dependencies. There are different applicable solutions for different scenarios, such as singleton (root), service isolation (module), multiple editing windows (component), etc. You should choose a reasonable location to avoid inappropriate shared information or redundant code packaging. .
only provide instance injection, it will not show the flexibility of dependency injection of the Angular framework. Angular provides many flexible injection tools. useClass automatically creates new instances, useValue uses static values, useExisting can reuse existing instances, and useFactory is constructed through functions, with specified deps and specified constructor parameters. These combinations can be very versatile. . You can cut off the token token of a class and replace it with another instance you have prepared. You can create a token to save the value or instance first, and then replace it again when you need to use it later. You can even use the factory function to return it. The local information of the instance is mapped to another object or attribute value. The gameplay here will be explained through the following cases, so I won’t go into it here. The official website also has many examples for reference.
Injection in Angular can be injected within the constructor, or you can get the injector to obtain existing injected elements through the get method.
Angular supports adding decorators to mark when injecting,
, there is an article "@Self or @Optional @Host? The visual guide to Angular DI decorators." which very vividly shows that if different decorators are used between parent and child components, the examples that will eventually be hit are: What a difference.
Figure 3 Filtering results of different injected decorators
Among the host view and @Host decorators, @Host may be the most difficult to understand. Here are some specific instructions for @Host. The official explanation of the @Host decorator is
...retrieve a dependency from any injector until reaching the host element.
Host here means host. The @Host decorator will limit the scope of the query to within the host element. . What is a host element? If component B is a component used by component A's template, then component A's instance is the host element of component B's instance. The content generated by the component template is called a View. The same View may be different views for different components. If component A uses component B within its own template scope (see Figure 4), the view (red box part) formed by the template content of A is the embedded view of component A, and component B is within this view, so For B, this view is B's host view. The decorator @Host limits the search scope to the host view. If it is not found, it will not bubble up.
Figure 4 Embedded view and host view
Let's use real cases to see how dependency injection works, how to troubleshoot errors, and how to play.
The modal window component of the DevUI component library provides a service ModalService, which can pop up a modal box and can be configured as a custom component. Business students often report errors when using this component, saying that the package cannot find the custom component.
For example, the following error is reported:
Figure 5 When using ModalService, there is an error when creating a component that references EditorX. The corresponding service provider cannot be found.
Analyze how ModalService creates custom components. Lines 52 and 95 of the Open function of the ModalService source code. As you can see, if componentFactoryResolver
is not passed in, componentFactoryResolver
injected by ModalService is used. In most cases, the business will introduce DevUIModule once in the root module, but will not introduce ModalModule in the current module. That is, the current situation in Figure 6 is like this. According to Figure 6, there is no EditorXModuleService in the injector of ModalService.
Figure 6 Module service provision relationship diagram
According to the inheritance of the injector, there are four solutions:
put EditorXModule where ModalModule is declared, so that the injector can find the EditorModuleService provided by EditorXModule - this is the worst solution, itself The lazy loading implemented by loadChildren is to reduce the loading of the home page module. The result is that the content that needs to be used in the sub-page is placed in the AppModule. The large rich text module is loaded on the first load, which aggravates the FMP (First Meaningful Paint). Not to be taken.
Introduce ModalService in the module that introduces EditorXModule and uses ModalService - it is advisable. There is only one situation that is not advisable, that is, calling ModalService is another top-level public service, which still puts unnecessary modules on the upper layer for loading.
When triggering the component using ModalService, inject componentFactoryResolver
of the current module and pass it to the open function parameter of ModalService - it is advisable to introduce EditorXModule where it is actually used.
In the module used, it is advisable to manually provide a ModalService, which solves the problem of injecting search.
The four methods are actually solving the problem of EditorXModuleService in the internal chain of the injector of componentFactoryResolver
used by ModalService. By ensuring that the search chain is at two levels, this problem can be solved.
Summary of knowledge points : module injector inheritance and search scope.
Usually when we use the same template in multiple places, we will extract the common part through the template. When the DevUI Select component was developed before, the developer wanted to extract the common part and reported an error.
Figure 7 Code movement and injection error not found.
This is because the CdkVirtualScrollFor instruction needs to inject a CdkVirtualScrollViewport. However, the element injection injector inheritance system inherits the DOM of the static AST relationship, and the dynamic one is not possible. Therefore, the following query behavior occurs, and the search fails.
Figure 8 Element Injector Query Chain Search Range
Final Solution: Either 1) Keep the original code position unchanged, or 2) You need to embed the entire template to find it.
Figure 9 Embedding the whole module enables CdkVitualScrollFo to find CdkVirtualScrollViewport (Solution 2)
Summary of knowledge points : The query chain of the element injector is the DOM element ancestor of the static template.
This case comes from this blog "Angular: Nested template driven form".
We also encountered the same problem when using form validation. As shown in Figure 10, for some reasons we encapsulate the addresses of the three fields into a component for reuse.
Figure 10: Encapsulate the three address fields of the form into a subcomponent.
At this time, we will find that an error is reported. ngModelGroup
requires a ControlContainer
inside the host, which is the content provided by the ngForm directive.
Figure 11 ngModelGroup cannot find ControlContainer.
Looking at the ngModelGroup code, you can see that it only adds the restriction of the host decorator.
Figure 12 ng_model_group.ts limits the scope of injection of ControlContainer.
Here you can use viewProvider with usingExisting to add the Provider of ControlContainer to the host view of AddressComponent.
Figure 13 Using viewProviders to provide external Provider
knowledge points for nested components Summary of knowledge points: The wonderful use of viewProvider and usingExisting.
lazy loading, resulting in the inability to drag and drop each other. The internal business platform involves drag and drop across multiple modules. Due to the lazy loading of loadChildren, each module will The DragDropModule of the DevUI component library is packaged separately, and this Module provides a DragDropService. Drag-and-drop instructions are divided into Draggable instructions and Droppable instructions. The two instructions communicate through DragDropService. Originally, it was possible to communicate by introducing the same module and using the Service provided by the module. However, after lazy loading, the DragDropModule module was packaged twice, which also resulted in two isolated instances. At this time, the Draggable instruction in a lazy-loaded module cannot communicate with the Droppable instruction in another lazy-loaded module, because the DragDropService is not the same instance at this time.
Figure 14 Lazy loading of modules leads to services not being the same instance/single case.
It is obvious that our requirement here is that we need a singleton, and the method of singleton is usually providerIn: 'root'
. Then should we not use the DragDropService of the component library? It is good to provide the root domain directly at the module level. But if you think about it carefully, there are other problems here. The component library itself is provided for use by a variety of businesses. If some businesses have two corresponding drag and drop groups in two places on the page, they do not want to be linked. At this time, the singleton destroys the natural isolation based on the module.
Then it would be more reasonable to implement singleton replacement by the business side. Remember the dependency query chain we mentioned earlier. The element injector is searched first. If it is not found, the module injector is started. So the replacement idea is that we can provide element-level providers.
Figure 15 Use the extension method to obtain a new DragDropService and mark it as provided at the root level
Figure 16 You can use the same selector to superimpose repeated instructions, superimpose an additional instruction on the Draggable instruction and Droppable instruction of the component library, and replace the token of DragDropService with the DragDropGlobalService that has provided a singleton at the root.
As shown in Figures 15 and 16, we inject through elements. The handler superimposes instructions and replaces the DragDropService token with an instance of our own global singleton. At this time, where we need to use the global singleton DragDropService, we only need to introduce the module that declares and exports these two extra instructions to enable the Draggable instruction Droppable instruction of the component library to communicate across lazy-loaded modules.
Summary of knowledge points : Element injectors have higher priority than module injectors.
The theming of the DevUI component library uses CSS custom attributes (css variables) to declare the css variable value of root to achieve theme switching. If we want to display previews of different themes at the same time in one interface, we can re-declare css variables locally in the DOM element to achieve the function of local themes. When I was making a theme dither generator before, I used this method to locally apply a theme.
Figure 17 Local theme function
, but it is not enough to apply CSS variable values locally. There are some drop-down pop-up layers that are attached to the back of the body by default, which means that their attachment layer is outside the local variables, which will lead to a very embarrassing situation. problem. The drop-down box of the local theme component displays the style of the external theme.
Figure 18 The component in the local theme is attached to the external overlay drop-down box.
What should I do if the theme is incorrect? We should move the attachment point back inside the local theme dom.
It is known that the Overlay of the DatePickerPro component of the DevUI component library uses the Overlay of Angular CDK. After a round of analysis, we replaced it with injection as follows:
1) First, we inherit OverlayContainer and implement our own ElementOverlayContainer as shown below.
Figure 19 Customize the ElementOverlayContainer and replace the _createContainer logic
2) Then directly provide our new ElementOverlayContainer on the component side of the preview, and provide a new Overlay so that the new Overlay can use our OverlayContainer. Originally Overlay and OverlayContainer are provided on root, here we need to cover these two.
Figure 20 Replace OverlayContainer with a custom ElementOverlayContainer and provide a new Overlay.
Now go to preview the website, and the DOM of the pop-up layer will be successfully attached to the component-preview element.
Figure 21 The Overlay container of cdk is attached to the specified dom. The partial theme preview is successful.
There is also a custom OverlayContainerRef in the DevUI component library for some components and modal box drawer benches, which also need to be replaced accordingly. Finally, pop-up layers and other pop-up layers can be realized to perfectly support local themes.
Summary of knowledge points : A good abstraction pattern can make modules replaceable and achieve elegant aspect programming.
with the last case, I would like to talk about a less formal approach to facilitate everyone's understanding of the nature of the provider. , configuring the provider essentially means letting it help you instantiate or map to an existing instance.
We know that if cdkOverlay is used, if we want the pop-up box to follow the scroll bar and be suspended in the correct position, we need to add the cdkScrollable instruction to the scroll bar.
It’s still the same scenario as the previous example. Our entire page is loaded through routing. For simplicity, I wrote the scroll bar on the host of the component.
Figure 22 The content overflow scroll bar writes overflow:auto in the component:host.
This way we encounter a more difficult problem. The module is specified by the router definition, that is, <app-theme-picker-customize></app-theme-picker-customize>
is not explicitly called anywhere. <app-theme-picker-customize></app-theme-picker-customize>
, then how to add the cdkScrollable instruction? The solution is as follows. Some of the code is hidden here and only the core code is left.
Figure 23 Create an instance through injection and manually call the life cycle.
Here, an instance of cdkScrollable is generated through injection, and the life cycle is called synchronously during the life cycle stage of the component.
This solution is not a formal method, but it does solve the problem. It is left here as an idea and exploration for the readers to taste.
Summary of knowledge points : Dependency injection configuration provider can create instances, but please note that instances will be treated as ordinary Service classes and cannot have a complete life cycle.
please refer to this blog post: "Rendering Angular applications in Terminal"
Figure 24 Replace the RendererFactory2 renderer and other content to allow Angular to run on the terminal.
The author replaced the RendererFactory2 and other renderers so that the Angular application can run on the terminal. This is the flexibility of Angular design. Even the platform can be replaced. It is powerful and flexible. Detailed replacement details can be found in the original article and will not be expanded upon here.
Summary of knowledge points : The power of dependency injection is that the provider can configure it by itself and finally implement the replacement logic.
This article introduced the dependency injection mode of control inversion and its benefits. It also introduced how dependency injection in Angular looks for dependencies, how to configure providers, how to use limited and filtering decorators to get the desired instance, and further through N cases analyze how to combine the knowledge points of dependency injection to solve problems encountered in development and programming.
By correctly understanding the dependency search process, we can configure the provider at the exact location (Case 1 and 2), replace other instances with singletons (Case 4 and 5), and even connect across the restrictions of nested component packages. Provided examples (Case 3) or use the provided method curve to implement instruction instantiation (Case 6).
Case 5 seems to be a simple replacement, but to be able to write a code structure that can be replaced requires an in-depth understanding of the injection mode and a better and reasonable abstraction of each function. If the abstraction is not appropriate, dependencies cannot be used. The maximum effect of injection. The injection mode provides more possible space for modules to be pluggable, plug-in, and part-based, reducing coupling and increasing flexibility, so that modules can work together more elegantly and harmoniously.
The powerful dependency injection function can not only optimize component communication paths, but more importantly, it can also achieve control inversion, exposing encapsulated components to more aspects of programming, and the implementation of some business-specific logic can also become more flexible. .