Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Brett Davis
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Brett Davis ( @CFCoderMonkey )

Querying For Multiple ViewChild Directives On A Single Element In Angular 2 Beta 17

By on

Yesterday, I took a look at exporting component and directive references in Angular 2. But, as I was playing around with that code, I realized that it was more complicated than it needed to be. Instead of exporting and querying for references, I could instead query for multiple ViewChild types on a single element in the component view. This approach has less dependencies and I thought was worth a quick follow-up post.

Run this demo in my JavaScript Demos project on GitHub.

As we saw yesterday, when you query the View (shadow DOM) for references (ie, #var attributes), the directives that you're targeting have to export an explicit reference name (or rely on the implicit reference exposed only by Component directives). If, however, we query for "types," rather than references, the target directives don't have to do anything. And, what's more, we can query for multiple directive types on the same element within the view.

To demonstrate, I've refactored my demo from yesterday to use injected ViewChild queries that are type-driven, not reference-driven. In the following code, notice that the root component view only houses one element with no exposed references. And yet, I'm querying for three different types:

<!doctype html>
<html>
<head>
	<meta charset="utf-8" />

	<title>
		Querying For Multiple ViewChild Directives On A Single Element In Angular 2 Beta 17
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css"></lin>
</head>
<body>

	<h1>
		Querying For Multiple ViewChild Directives On A Single Element In Angular 2 Beta 17
	</h1>

	<my-app>
		Loading...
	</my-app>

	<!-- Load demo scripts. -->
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/es6-shim.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/Rx.umd.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/angular2-polyfills.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/angular2-all.umd.js"></script>
	<!-- AlmondJS - minimal implementation of RequireJS. -->
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/almond.js"></script>
	<script type="text/javascript">

		// Defer bootstrapping until all of the components have been declared.
		requirejs(
			[ /* Using require() for better readability. */ ],
			function run() {

				ng.platform.browser.bootstrap( require( "App" ) );

			}
		);


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I provide the root application component.
		define(
			"App",
			function registerApp() {

				// Configure the App component definition.
				ng.core
					.Component({
						selector: "my-app",
						directives: [
							require( "ComponentA" ),
							require( "DirectiveB" ),
							require( "DirectiveC" )
						],

						// In this version of the demo, rather than querying for "refs",
						// we're querying for instances of the given directive types.
						// --
						// NOTE: All three of these directives select on "my-component".
						queries: {
							qTestA: new ng.core.ViewChild( require( "ComponentA" ) ),
							qTestB: new ng.core.ViewChild( require( "DirectiveB" ) ),
							qTestC: new ng.core.ViewChild( require( "DirectiveC" ) )
						},
						template:
						`
							<my-component></my-component>
						`
					})
					.Class({
						constructor: AppController,

						// Define the life-cycle methods on the prototype so that
						// Angular will pick them up at runtime.
						ngAfterViewInit: function noop() {}
					})
				;

				return( AppController );


				// I control the App component.
				function AppController() {

					var vm = this;

					// Expose the public methods.
					vm.ngAfterViewInit = ngAfterViewInit;


					// ---
					// PUBLIC METHODS.
					// ---


					// I get called once after the view is initialized / checked for the
					// first time. At this point, all of the DOM-queries have been linked.
					function ngAfterViewInit() {

						console.group( "Injected ViewChild Queries" );
						console.log( "qTestA:", vm.qTestA );
						console.log( "qTestB:", vm.qTestB );
						console.log( "qTestC:", vm.qTestC );
						console.groupEnd();

					}

				}

			}
		);


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I provide a component that selects on "my-component".
		define(
			"ComponentA",
			function registerComponentA() {

				// Configure the ComponentA component definition.
				return ng.core
					.Component({
						selector: "my-component",
						template:
						`
							<div>
								Such component! Much Directive!
							</div>
						`
					})
					.Class({
						constructor: function ComponentAController() {

							this.name = "ComA";

						}
					})
				;

			}
		);


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I provide a directive that selects on "my-component".
		define(
			"DirectiveB",
			function registerDirectiveB() {

				// Configure the DirectiveB directive definition.
				return ng.core
					.Directive({
						selector: "my-component"
					})
					.Class({
						constructor: function DirectiveBController() {

							this.name = "DirB";

						}
					})
				;

			}
		);


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I provide a directive that selects on "my-component".
		define(
			"DirectiveC",
			function registerDirectiveC() {

				// Configure the DirectiveC directive definition.
				return ng.core
					.Directive({
						selector: "my-component"
					})
					.Class({
						constructor: function DirectiveCController() {

							this.name = "DirC";

						}
					})
				;

			}
		);

	</script>

</body>
</html>

As you can see, the component and directives in this demo aren't exporting anything - they're just existing. And yet, when our root component queries for the ViewChild'ren based on type, we're able to locate and extract all three of our directive controller instances on the same "my-component" element:

ViewChild query for multiple directive instances in Anuglar 2 Beta 17.

The reference-based queries in Angular 2 Beta 17 makes a lot of sense if you actually need to reference the directive instances within the View. But, if all you need to do is inject the references into the controller, I think the type-based queries make the most sense. Plus, this approach requires less coordination between the directives as we're not dependent on the target directive to export reference names.

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

Great article!
I would like to point out an extended approach though:

You write in the comment above the queries that

// NOTE: All three of these directives select on "my-component".

This is true in your example since there are no other components in the template. It would be different if you e.g. used the directive routerLink on lets say

<some-component routerLink="someroute1">
<some-component>
<some-component routerLink="someroute3">

and were to query both the component itself and its corresponding routerlink or the elementRef and maybe you don't even have any control over <some-component>, but still you want to get a reference to <some-component> along with its corresponding routerLink.

In your approach, the ViewChildren queries would return a list of two and three elements qhen querying for 'some-component' and 'routerLink', respectively. It would be cumbersome to find the element in the second query corresponding to an element in the first query.

However, you can easily achieve this by writing a custom directive that selects for e.g. <some-component> and use the dependency injection to inject all the references you want, and finally simply query for your custom directive the same way you explained in your article:

@Directive({
'selector': 'some-component'
})
class MyDirective {
constructor(
public elementRef: ElementRef,
@Self() @Inject(RouterLink) public routerLink: RouterLink,
@Self() @Inject(SomeComponent) public someComponent: SomeComponent
) {

}
}

@Component({
selector: 'my-app',
template: `
<some-component routerLink="someroute1">
<some-component>
<some-component routerLink="someroute3">
`
})
class MyAppComponent {

@ViewChildren(MyDirective) myDirectiveList: QueryList<MyDirective>;

ngAfterViewInit()
this.myDirectiveList.forEach(myDirective => {
console.log(myDirective.elementRef)
console.log(myDirective.routerLink)
console.log(myDirective.someComponent)
})

}

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel