Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Lindsey Redinger
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Lindsey Redinger

Building A Better Mental Model For Meta-Data In Angular 2 Beta 3

By
Published in

Up until now, I've gotten my Angular 2 demoes to work; but, I've never really stepped back and thought about what the decorators are doing, how they work, or when I do and do not need to attach meta-data to a particular class. In this exploration, all I'm trying to do is build a better mental model of the meta-data and when and where I need to apply it.

Run this demo in my JavaScript Demos project on GitHub.

For the most part, my demoes have dealt almost exclusively with Components and Directives. And this got me thinking about Services - do they need meta-data? Do I need to use the .Class() decorator for non-directive classes? As it turns out, no. And, after a little more exploration, I discovered that the value returned by the decorators is actually a reference to the given class constructor.

To dig into these concepts, I put together a small demo that includes a Component (AppComponent) that requires a Service (GlobalCounter). I've wired up the AppComponent in the same way that I've done for all my previous demos. But, for the GlobaCounter service, I've omitted the decorators altogether.

NOTE: Services require less meta-data than components - I do not mean to imply that you can or should get rid of decorators for components.

Things to notice in the AppComponent:

  • I'm using the .Component() and .Class() decorators.
  • I'm applying the .parameters property to the return value of the decorators.
  • I'm using the decorator return value as the DI (dependency-injection) token.
  • I'm .info() logging the equality of the constructor to the decorator return value.

Things to notice in the GlobalCounter:

  • I'm not using the .Class() decorator.
  • I'm applying the .parameters property directly to the service constructor.
  • I'm using the service constructor as the DI token.

Ok, let's look at the code:

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

	<title>
		Building A Better Mental Model For Meta-Data In Angular 2 Beta 3
	</title>

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

	<h1>
		Building A Better Mental Model For Meta-Data In Angular 2 Beta 3
	</h1>

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

	<!-- Load demo scripts. -->
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/3/es6-shim.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/3/Rx.umd.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/3/angular2-polyfills.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/3/angular2-all.umd.js"></script>
	<!-- AlmondJS - minimal implementation of RequireJS. -->
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/3/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(
			[ "AppComponent", "GlobalCounter" ],
			function run( AppComponent, GlobalCounter ) {

				ng.platform.browser.bootstrap(
					AppComponent,
					[
						GlobalCounter,

						// Setup the OPTIONAL initial value for the global counter.
						ng.core.provide(
							"INITIAL_COUNT",
							{
								useValue: 123
							}
						)
					]
				);

			}
		);


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


		// I provide the root application component.
		define(
			"AppComponent",
			function registerAppComponent() {

				var GlobalCounter = require( "GlobalCounter" );

				// Configure the App component definition using the .Component() and
				// .Class() meta-data decorators.
				var AppComponent = ng.core
					.Component({
						selector: "my-app",
						template:
						`
							<a (click)="increment()">You have clicked {{ counter }} times</a>
						`
					})
					.Class({
						constructor: AppController
					})
				;

				// Tell the Dependency-Injector which argument types are needed by the
				// controller. Notice that the parameters definition is being added to
				// the result of the meta-data decorators, NOT the controller.
				AppComponent.parameters = [ new ng.core.Inject( GlobalCounter ) ];

				// .... HOWEVER .... what we discover is that the value returned by the
				// meta-data decorator is actually the Controller itself. So, in reality,
				// we are adding the parameters definition to the constructor function,
				// not some special meta-data object.
				console.info(
					"App Component === App Controller :",
					( AppComponent === AppController )
				);

				// NOTE: Because ( AppComponent === AppController ) it actually doesn't
				// matter which one I return - they are the same reference.
				return( AppComponent );


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

					var vm = this;

					// I hold the current global counter value.
					vm.counter = globalCounter.get();

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


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


					// I increment the counter.
					function increment() {

						vm.counter = globalCounter.incrementAndGet();

					}

				}

			}
		);


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


		// I provide a global counter that can be shared across the application.
		define(
			"GlobalCounter",
			function registerGlobalCounter() {

				// Tell the Dependency-Injector which arguments types are needed by the
				// constructor. Notice that the parameters definition is being added
				// directly to the service constructor itself (as a static property).
				GlobalCounter.parameters = [
					[ new ng.core.Inject( "INITIAL_COUNT" ), new ng.core.Optional() ]
				];

				return( GlobalCounter );


				// I initialize the global counter using the optional default value.
				function GlobalCounter( defaultValue ) {

					// I hold the global counter value.
					var counter = ( defaultValue || 0 );

					// Return the public API.
					return({
						get: get,
						incrementAndGet: incrementAndGet
					});


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


					// I return the current global counter value.
					function get() {

						return( counter );

					}


					// I increment the global counter and return the resultant value.
					function incrementAndGet() {

						return( ++counter );

					}

				}

			}
		);

	</script>

</body>
</html>

When we run this code, we can see that the GlobalCounter received the appropriate constructor arguments. And, we can see that the return value of the decorators is actually a reference back to the target constructor:

Building a better mental model of decorators and meta-data in Angular 2.

This is really great. I think I understand the Angular 2 meta-data much better now. And, I think I'll be able to simplify some of my demoes going forward. Maybe only by a line or two of code; but, the important thing is that I now have an increased awareness of what is going on and how decorators and meta-data and classes fit together in Angular 2.

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

Reader Comments

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