Component Inputs Provide Both Property And Attribute Bindings In AngularJS 2 Beta 1
As I read through the AngularJS 2 Beta 1 Quick Start and Developer Guide, it was a little like drinking from the firehose - a lot has changed; not the least of which is the syntax used to define the component templates. Now, with AngularJS 2, the template syntax actually offers some insight into the binding mechanism. After reading through the guides, however, it was unclear to me as to how components could actually use attribute interpolation. What I didn't understand at the time was that component inputs automatically provide both property and attribute bindings.
Run this demo in my JavaScript Demos project on GitHub.
When passing data from a parent component into a child component, there are two different syntaxes available:
- [name]="expression" - Evaluates the expression using the expression context (ie, the component) and binds to the given property.
- name="string" - Calculates the attribute value using standard attribute interpolation.
On the receiving side, however, within the component that exposes the input binding, I didn't understand how to express this difference in binding mechanism. And, as it turns out, I don't have to. An input, exposed by a component, will respond to both property and attribute bindings. The only difference is in how the value is calculated in the calling context before it is updated within the target component.
To see this in action, I've created an Echo component that takes a "message" property and echoes that message back out into the DOM (Document Object Model). Then, I consume two instances of this component, one that uses property-based bindings and one that uses attribute interpolation:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Component Inputs Provide Both Property And Attribute Bindings In AngularJS 2 Beta 1
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Component Inputs Provide Both Property And Attribute Bindings In AngularJS 2 Beta 1
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-all.umd.min.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/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" ],
function run( AppComponent ) {
ng.platform.browser.bootstrap( AppComponent );
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the root application component.
define(
"AppComponent",
[ "Echo" ],
function registerAppComponent( Echo ) {
// Configure the App component definition.
var AppComponent = ng.core
.Component({
selector: "my-app",
directives: [ Echo ],
// Notice that we using two instances of the ECHO component in
// the template. Both of them are setting the "message" input;
// however, one is binding using property syntax while the other
// is binding using attribute interpolation syntax.
template:
`
<p>
<a href="#foo" (click)="incrementCounter()">Increment Counter</a>
</p>
<!-- PROPERTY Binding. -->
<echo [message]="( messageAsExpression + ' (' + counter + ')' )"></echo>
<!-- ATTRIBUTE Binding (always converts to a string). -->
<echo message="{{ messageAsString }} ({{ counter }})"></echo>
`
})
.Class({
constructor: AppController
})
;
return( AppComponent );
// I control the App component.
function AppController() {
var vm = this;
// In order to trigger subsequent changes to the input bindings, the
// passed-in value actually has to change. We're using the counter to
// keep the bound-inputs dynamic.
vm.counter = 1;
// Both the values in the view-model are strings; the difference lies
// in how we are passing the values into the Echo component instances.
vm.messageAsExpression = "Such expressions!";
vm.messageAsString = "Much interpolation!"
// Expose the public methods.
vm.incrementCounter = incrementCounter;
// ---
// PUBLIC METHODS.
// ---
// I increment the counter,
function incrementCounter() {
console.log( "- - - - - - - - - - - - - -" );
vm.counter++;
};
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a component that exposes a [message] input and echoes it back out
// in the page rendering
define(
"Echo",
function registerEcho() {
// Configure the echo component definition.
var EchoComponent = ng.core
.Component({
selector: "echo",
inputs: [ "message" ], // <-- Input binding that we're testing.
template:
`
<p>
{{ message }}
</p>
`
})
.Class({
constructor: EchoController,
// Defining the life-cycle methods on the prototype so that
// AngularJS will pick them up at invocation time.
ngOnChanges: function noop() {}
})
;
return( EchoController );
// I control the echo component.
function EchoController() {
var vm = this;
// Expose the public methods.
vm.ngOnChanges = ngOnChanges;
// ---
// PUBLIC METHODS.
// ---
// The onChanges event is the first event triggered in the component
// life-cycle. It is fired after the inputs are first bound to the
// component instance. It is then fired whenever the inputs change.
function ngOnChanges( event ) {
console.log( "ngOnChanges[ message ]:", vm.message );
// console.log( "--> Current:", event.message.currentValue );
// console.log( "--> Previous:", event.message.previousValue );
}
}
}
);
</script>
</body>
</html>
As you can see, I'm passing the message into the Echo components using property binding and attribute interpolation, respectively:
- [message]="( messageAsExpression + ' (' + counter + ')' )"
- message="{{ messageAsString }} ({{ counter }})"
Both of these approaches update the exposed property of the Echo component. And, when we run the above code, we get the following output:
After creating this demo, I went back and re-read the Developer Guide. And, of course, this information was already there - I just missed it the first time:
Interpolation is actually a convenient alternative for Property Binding in many cases. In fact, Angular translates those Interpolations into the corresponding Property Bindings before rendering the view.... There is no technical reason to prefer one form to the other. We lean toward readability which tends to favor interpolation. We suggest establishing coding style rules for the organization and choosing the form that both conforms to the rules and feels most natural for the task at hand.
There it was, hiding in plain sight. Like I said, it was like drinking from the firehose. I suppose I was just a little overwhelmed.
One thing I should note, however, is that attribute interpolation, by its nature, only ever results in a string value. With property-based binding, you can pass-in a string, an array, a class instance, or whatever you like. But, with attribute interpolation, the passed-in value is always a computed string value.
I apologize for writing about something that is clearly in the documentation. But, I missed it on my first pass; so, maybe it can serve as reminder to anyone else that may have missed it in their learnings.
Want to use code from this post? Check out the license.
Reader Comments
Not sure, but I think your reference to "attribute bindings" might be wrong or misleading. In the same doc you linked to it seems to say you need to use [attr.xxxx] to bind to an attribute vs. a property.
But I must admit I still haven't fully got my head around attributes vs. props, though (independently from Angular...). Every now and again I think I get it then 10 mins later I've forgotten. I remember it was a big thing in jQuery too.
@Fergus,
It is quite possible that I am using the wrong terminology. I wasn't entirely sure how to refer to these things; especially when Angular is using attribute values to update property values. So, it all sort of goes into the same place.
I've found it very useful to always imagine a feature, in this case interpolation, without the syntactic sugar. Syntactic sugars obviously increase readability and decrease boilerplate, but at the expense of hiding the real syntax which is more clear and understandable.
<div>Hello {{name}}</div>
is just a syntactic sugar for
<div [textContent]="interpolate(['Hello'], [name])"></div>
Also from the CheatSheat:
<div title="Hello {{ponyName}}">
is just syntactic sugar to:
<div [title]="'Hello' + ponyName">
@Esfand,
I vaguely remember seeing the [textContent] talked about in the dev guide, but I didn't remember the interpolate() method. There's sooo much to get in my brain. But, I agree - knowing the non-sugar approach is great for building the mental model!
> knowing the non-sugar approach is great for building the mental model!
Exactly! Real good way to put it.
> There's sooo much to get in my brain.
:) Tell me about it.
I've found this blog post good for a brief and concise list of sugars.
http://victorsavkin.com/post/119943127151/angular-2-template-syntax
Interestingly enough, direct access to the properties, methods, and events of DOM nodes makes the knowledge of DOM API more important. TypeScript helps to some extent for remembering hundreds of properties of various DOM nodes such as [textContent], [hidden], [focus()], etc.
@Esfand,
Groovy link! And, I'm pretty sure Victor Savkin is on the core team, so he knows his stuff!