This article is part of a web development series from Microsoft. Thank you for supporting the partners who make SitePoint possible.
This is the third part in the Angular 2 series. You can read part two here.
Code for this article can be found on Github here. Browse to the src/data-flows folder to find the applicable source. This application running on Node in Azure can be found here.
Data Binding in Angular 2
[author_more]
Data binding can be a touchy topic. Prior to Angular 1 it was reserved largely for compiled and server rendered languages or UI frameworks such as Silverlight and Flex. When Angular 1 broke onto the scene we were amazed at the "magic" that data binding provides in the UI.
If you've been reading up on Angular 2 you've probably heard things like "immutability" and "one-way data flow". But how exactly do these relate to Angular? Do they work with NgModel? How can we manage the state of our application when everything flows in one direction.
This article covers first how to treat Angular 2 data bindings similarly to Angular 1. Then we will talk about converting to a one-way data flow and will make the needed modifications to do so.
Simple Angular 1 App
We will build the most basic Todo
app I can think of. No styling, just an input allowing the user to type a Todo
and hit Enter to submit. Then a list of Todos with the ability to mark them Done.
The final product looks like this
Browser screenshots in Microsoft Edge
Building this in Angular 1 should be relatively straight-forward. We have a simple HTML form with an ng-submit
(so our form handles the Enter
key), a controller method that turns the text we entered into a new Todo
, and a way to toggle done vs not done Todos.
The markup looks something like this:
<h2>Angular 1 Todo App</h2>
<div ng-controller="AddTodo as addTodo">
<form ng-submit="addTodo.addTodo(todo)">
<input ng-model="todo" placeholder="Create a Todo"/>
</form>
</div>
<div ng-controller="TodoList as todoList">
<ul>
<li ng-repeat="todo in todoList.todos"
ng-click="todoList.toggleTodo(todo)"
ng-class="{completed: todo.completed}"
>{{todo.title}}</li>
</ul>
</div>
There is a simple service that manages the list of Todos that are rendered. The service also handles adding and toggling the completed state of each Todo:
function TodoService () {
return {
// Default a couple Todos for initial rendering
todos: [
{title: "Read the todo list", completed: true},
{title: "Look at the code", completed: false}
],
add: function (todo) {
if (!todo.length) {
return;
}
this.todos.push({title: todo, completed: false});
},
toggle: function (todo) {
todo.completed = !todo.completed;
}
}
}
When a user types into the <input>
they will be adding to the “todo” model property. Hitting Enter will submit the form and run addTodo()
on the AddTodo
controller, which looks like this:
functionAddTodoController (TodoService) {
this.todoService = TodoService;
}
AddTodoController.prototype.addTodo = function (todo) {
this.todoService.add(todo);
}
And finally the list of Todos is rendered in the <ul>
tag and each Todo can run the toggleTodo()
method. The TodoList
controller looks like this:
functionTodoListController (TodoService) {
this.todoService = TodoService;
this.todos = this.todoService.todos;
}
TodoListController.prototype.toggleTodo = function (todo) {
this.todoService.toggle(todo);
}
Angular 1 Summary
While this extremely basic Angular 1 application might not be something you would write for production (I would recommend using directives and/or a router to tie views to controllers rather than using ng-controller
), it provides a simple example using basic data binding and dirty checking to render the list of Todos. It’s a good place to start since it touches on the common data flow scenarios in Angular 1.
Converted to Angular 2
In Angular 2 we can build an application in the same way as Angular 1. We can use two-way data binding (really it’s just two one-way data bindings, but the end result is two-way data binding) and a service to manage our data. We can rely on dirty checking to re-render the UI when changes happen to our data.
So what would be the advantage of using Angular 2? Well, in looking at the following code samples we’ll see a cleaner separation of concerns. Using components we build the application as a tree from the top down, and the functionality in each component can be easily reasoned about.
Here is the base component for our application:
// TodoApp (app.ts)
import {Component, DoCheck} from 'angular2/core';
import {AddTodo, TodoList, Header} from './components';
import {TodoService} from './services/todos';
@Component({
selector: 'TodoApp',
template: '
<Header></Header>
<AddTodo></AddTodo>
<TodoList></TodoList>
',
directives: [AddTodo, TodoList, Header],
// Add the TodoService so all children components
// have access to the same TodoService instance.
providers: [TodoService]
})
export class TodoApp {}
This is a very basic component, but the key part of note is the providers
property being used in the Component decorator. TodoService
isn’t being used directly by this base component, but adding it to the providers
list means that any child components will receive the same instance of the TodoService
(creating a singleton for any child components).
Notice in the AddTodo
component how we inject TodoService
, but it isn’t included in a providers property. Angular 2 will look up the chain of injectors until it finds TodoService
at the top level and inject that one:
// TodoService (services/todos.ts)
import {Injectable} from 'angular2/core';
import {TodoModel} from '../models/todo';
@Injectable()
export class TodoService {
todos: TodoModel[];
constructor () {
this.todos = [
{title: "Read the todo list", completed: true},
{title: "Look at the code", completed: false}
];
}
add (todo: String): void {
if (!todo.length) {
return;
}
this.todos.push({title: todo, completed: false});
}
toggle (todo: TodoModel): void {
todo.completed = !todo.completed;
}
}
Now the TodoList
gets a reference to the same TodoService
instance and uses it to render the list of Todos:
// TodoList (components/todoList.ts)
import {Component, Input} from 'angular2/core';
import {Todo} from './todo';
import {TodoService} from '../services/todos';
@Component({
selector: 'TodoList',
template: '
<ul class="list-group">
<Todo *ngFor="#todo of todos" [todo]="todo"></Todo>
</ul>
',
directives: [Todo]
})
export class TodoList {
todos;
constructor (todoService: TodoService) {
this.todos = todoService.todos;
}
}
And finally the Todo component itself renders the Todo text. It also wires up a click
event to toggle the state of the Todo:
// Todo (components/todo.ts)
import {Component, Input} from 'angular2/core';
import {TodoService} from '../services/todos';
@Component({
selector: 'Todo',
template: '
<li [ngClass]="{completed: todo.completed}"
(click)="toggle(todo)"
class="list-group-item">
{{todo.title}}
</li>
'
})
export class Todo {
@Input() todo;
constructor (public todoService: TodoService) {}
toggle (todo) {
this.todoService.toggle(todo);
}
}
Angular 2 Summary
While the implementation is different, the data flows and concepts between the Angular 1 and Angular 2 implementations are identical.
A service manages the list of Todos being rendered. The same service also manages adding and toggling of Todos. Both use a singleton instance of the TodoService so all components of the application are always referring to the same set of data. We mutate the array of Todos (and the individual Todos in the array) and rely on dirty checking to re-render.
Overall this is not a bad way to build an application. But as your applications grow, dependencies between components and the data they render can become cumbersome. It can make reasoning about your application difficult, and relying on data mutation and dirty checking can make testing harder.
Next we will look at a third way to build this simple application: With a one-way data flow.
Continue reading %Data Flows with Angular 2 and Redux%
by Jason Aden via SitePoint
No comments:
Post a Comment