Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Chris Phillips
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Chris Phillips@cfchris )

Constructor vs. Property QueryList Injection In Angular 2 Beta 8

By Ben Nadel on

Yesterday, as I was trying desperately to wrap my head around the mysteries of change detection in Angular 2 Beta 8, I noticed something interesting in the documentation: queries against the rendered view could be injected into a component through two different means. One way is to define the query as meta-data on the component. The other way is to inject the query as a constructor argument. While the concrete QueryList object is the same in both cases, each approach has slightly different implications.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

A "QueryList," in Angular 2, is a live list of directive and variable bindings contained within a directive. In laymen's terms, its a collection of references to rendered items within the DOM (Document Object Model) tree. The QueryList is an unmodifiable iterator of the current references.

NOTE: "Unmodifiable" is not the same as "immutable." In fact, the QueryList is a mutable data structure for which the Angular 2 framework manages the state. This is why changes to the underlying DOM references can be observed on the same QueryList instance.

There are various types of queries that can be made against the DOM, each of which determine how many items can be returned and how deep within the DOM tree these items can be located (though I've had mixed results when playing with the query settings). In each case, the resultant QueryList can be injected into the Component using either component meta-data:

  • ng.core.Component({
  • selector: "my-component",
  • queries: {
  • itemAList: new ng.core.ViewChildren( SubComponent )
  • }
  • // ...
  • });

... or constructor parameters:

  • MyComponentController.parameters = [
  • [
  • new ng.core.Inject( ng.core.ViewChildren ),
  • new ng.core.ViewChildren( SubComponent )
  • ]
  • ];

Both of these approaches result in a QueryList instance that isn't populated until after the view is initialized. However, with the property-based injection, the actual QueryList reference itself isn't injected until after thew view is initialized whereas with the constructor-based injection, the QueryList variable is available immediately within the component constructor. This means that with constructor-based injection, you have a chance to bind to the .changes EventEmitter before the first change is announced.

To see this in action, I've put together a small demo in which we have two different sets of components: ItemA and ItemB. A query for the ItemA components is provided through component meta-data and a query for the ItemB components is provided through constructor injection:

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Constructor vs. Property QueryList Injection In Angular 2 Beta 8
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Constructor vs. Property QueryList Injection In Angular 2 Beta 8
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/8/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • // --
  • // NOTE: Not all components have to be required here since they will be
  • // implicitly required by other components.
  • requirejs(
  • [ /* Using require() for better readability. */ ],
  • function run() {
  •  
  • var App = require( "App" );
  •  
  • ng.platform.browser.bootstrap( App );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root App component.
  • define(
  • "App",
  • function registerApp() {
  •  
  • var ItemA = require( "ItemA" );
  • var ItemB = require( "ItemB" );
  •  
  • // Configure the App component definition.
  • ng.core
  • .Component({
  • selector: "my-app",
  • directives: [ ItemA, ItemB ],
  •  
  • // Here, we are asking Angular to inject a query list for the
  • // collection of ItemA components rendered within our component
  • // view. This will inject `itemAList` as a public property on
  • // the App component instance.
  • // --
  • // CAUTION: View queries are set before the ngAfterViewInit()
  • // life-cycle method is called, but after the ngOnInit() life-
  • // cycle method is called. As such, the `itemAList` property will
  • // not exist until the View is initialized.
  • queries: {
  • itemAList: new ng.core.ViewChildren( ItemA )
  • },
  •  
  • // Notice that our view has two different sets of components -
  • // ItemA and ItemB instances. Each of these will be monitored
  • // using a different live query.
  • template:
  • `
  • <item-a></item-a>
  • <item-a></item-a>
  • <br />
  • <item-b></item-b>
  • <item-b></item-b>
  • <item-b></item-b>
  •  
  • <div *ngIf="showMore">
  • <item-a></item-a>
  • <item-b></item-b>
  • </div>
  • `
  • })
  • .Class({
  • constructor: AppController,
  •  
  • // Define life-cycle methods on the prototype so that they are
  • // picked up at runtime.
  • ngOnInit: function noop() {},
  • ngAfterViewInit: function noop() {}
  • })
  • ;
  •  
  • // Here, we're asking Angular to inject a live query of the ItemB view
  • // children as a constructor argument for the component instance. Just
  • // like the live query defined in the component meta-data, this one will
  • // not contain items until the view has been initialized. However, unlike
  • // the other live query, we can at least reference this variable before
  • // the view has been initialized (where we can subscribe to the changes).
  • AppController.parameters = [
  • [
  • new ng.core.Inject( ng.core.ViewChildren ),
  • new ng.core.ViewChildren( ItemB )
  • ]
  • ];
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController( itemBList ) {
  •  
  • var vm = this;
  •  
  • // I determine if we are showing the extra components.
  • vm.showMore = false;
  •  
  • // The ViewChildren construct is a live query of the components in
  • // the view. So, in order to see how this live query changes over
  • // time, we're going to reveal more components after a short delay.
  • setTimeout(
  • function revealMore() {
  •  
  • console.info( "- - - - Reveal More Components - - - -" );
  • vm.showMore = true;
  •  
  • },
  • ( 5 * 1000 )
  • );
  •  
  • // While the itemBList was provided via constructor injection, the
  • // view still has not yet been initialized. As such, this list will
  • // be empty. But, since we have a reference to it, we can subscribe
  • // to changes (that will be triggered after the view is initialized).
  • itemBList.changes.subscribe(
  • function handleValue( queryList ) {
  •  
  • console.warn( "itemBList Change Detection (constructor)" );
  • console.log( "itemBList:", itemBList.length );
  •  
  • }
  • );
  •  
  • // Expose the public methods.
  • vm.ngAfterViewInit = ngAfterViewInit;
  • vm.ngOnInit = ngOnInit;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I get called once after the component's view has been initialized.
  • function ngAfterViewInit() {
  •  
  • // At this point, after the view has been initialized, both of the
  • // query list values are available - both itemBList, which was
  • // injected via the constructor, and itemAList, which was just
  • // injected as a property.
  • console.warn( "ngAfterViewInit()" );
  • console.log( "itemAList:", vm.itemAList.length );
  • console.log( "itemBList:", itemBList.length );
  •  
  • // Now that we finally have a reference to the itemAList, let's
  • // subscribe to the changes of the list (which updates the current
  • // instance but also provides a reference to the query list).
  • vm.itemAList.changes.subscribe(
  • function handleValue( queryList ) {
  •  
  • console.warn( "itemAList Change Detection (ngAfterViewInit)" );
  • console.log( "itemAList:", vm.itemAList.length );
  •  
  • }
  • );
  •  
  • // We've already subscribed to the changes on this list; but,
  • // let's do it again so we can see if it would pick up the initial
  • // length of the list.
  • itemBList.changes.subscribe(
  • function handleValue( queryList ) {
  •  
  • console.warn( "itemBList Change Detection (ngAfterViewInit)" );
  • console.log( "itemBList:", itemBList.length );
  •  
  • }
  • );
  •  
  • }
  •  
  •  
  • // I get called once after the component has been instantiated and
  • // the inputs have been bound (and observed once).
  • function ngOnInit() {
  •  
  • // While the itemBList was injected in the constructor, the
  • // itemAList, which was part of the component meta-data, has not
  • // been injected.
  • console.warn( "ngOnInit()" );
  • console.log( "itemAList:", ( vm.itemAList && vm.itemAList.length ) );
  • console.log( "itemBList:", itemBList.length );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a super simple Component for no other purpose than to have a
  • // component that can be rendered in the App component.
  • define(
  • "ItemA",
  • function registerItemA() {
  •  
  • // Configure the Item component definition.
  • return ng.core
  • .Component({
  • selector: "item-a",
  • template: "This is an ItemA!"
  • })
  • .Class({
  • constructor: function ItemControllerA() { /* Nothing to do. */ }
  • })
  • ;
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a super simple Component for no other purpose than to have a
  • // component that can be rendered in the App component.
  • define(
  • "ItemB",
  • function registerItemB() {
  •  
  • // Configure the Item component definition.
  • return ng.core
  • .Component({
  • selector: "item-b",
  • template: "This is an ItemB!"
  • })
  • .Class({
  • constructor: function ItemControllerB() { /* Nothing to do. */ }
  • })
  • ;
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, not only are we providing two different QueryList reference, we are also changing the number of rendered components using a setTimeout() call. This way, we can see how those QueryList references report length changes over time. We can also see when those changes are emitted.

When we run the above code, and wait until the setTimeout() executes, we get the following page output:


 
 
 

 
 QueryList injection in Angular 2 Beta 8. 
 
 
 

There are a few things to notice in this output:

  • The property-based QueryList, itemAList, wasn't defined until the ngAfterViewInit() life-cycle method was called.
  • The constructor-based QueryList, itemBList, was available within the constructor, but yielded an empty list.
  • Only the itemBList change subscription picked up on the initial population of the QueryList since it was the only one that could bind to the .changes EventEmitter before the view was initialized.
  • Both QueryList change subscriptions picked up on the subsequent change driven by the setTimeout().

To be honest, I don't have enough experience with these DOM queries to feel one or the other about the injection approach (property vs. constructor). The best choice probably depends on which type of notation you like and how convenient it would be to have an early reference to the empty QueryList. That said, it's definitely good to know that both options are available.




Reader Comments

@All,

With regard to this quote:

> There are various types of queries that can be made against the DOM,
> each of which determine how many items can be returned and how
> deep within the DOM tree these items can be located (though I've had
> mixed results when playing with the query settings).

.... I figured out why I was getting inconsistent results in some of my tests. It turns out that the form of injection method actually affects the way ViewChild is resolved against the view:

http://www.bennadel.com/blog/3042-how-injection-method-affects-viewchild-queries-in-angular-2-beta-8.htm

In retrospect, this makes 100% total sense. But, it might not be obvious at first.

Reply to this Comment

Ben, I love u :)
but honestly I think TypeScript is the future of the web, and it would be so awesome if all your posts would be in TS.
I am not sure if you have done Typed development before, but once you do, you will not believe you ever did it without typing... like having unsafe sex :)

Regards,

Sean

Checkout the Ultimate Angular 2 Boorstrap App: @ http://ng2.javascriptninja.io
Source@ https://github.com/born2net/ng2Boilerplate

Reply to this Comment

I agree, it's quite difficult to map Ben's code to TS, because the parts of code are all over the place and absolutely not straighforward. Ben, you should really try TS, you don't even need to use types and other stuff if you don't want to (remember, TS is superset of JS).

Reply to this Comment

Honestly, angularjs moving to typescript will lose a lot of developers. I think typescript is good, however the switch should be made slowly.

Converting all angular app to new syntax it's a huge work...

Reply to this Comment

@Sean, @John,

When you say that code is all over the place, can you be more specific? When I look at the ES5 code, it basically seems like the same general format. If you think about each define() call (which has nothing to do with either approach) as a single module, TypeScript is like:

* Import statements.
* Directive metadata.
* Class constructor.

... and when I have to do the same in ES5, its like:

* Require statements.
* Directive metadata.
* Constructor parameters metadata.
* Class constructor.

So, indeed, I do have one more thing - the parameter metadata - which, admittedly in TypeScript is much more "inline" with the rest of the code. But, I would say that for the most part, the ES5 and the TypeScript bits of code line up quite well.

Is it possible you're reacting more to the "module pattern" vs. "prototype" usage? I tend to use public/private methods rather than just applying everything to the class prototype. This significantly changes the structure of the code itself. But, this would look the same with TypeScript - TypeScript is really just about defining types.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.