Thursday, February 4, 2016

Data Flows with Angular 2 and Redux

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

Simple basic Todo app in Angular 1 and Angular 2

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