Now, let's build a simple to-do application in order to demonstrate the syntax to define components further.
Our to-do items will have the following format:
interface Todo { completed: boolean; label: string; }
Let's start by importing everything we will need:
import {Component, NgModule, ViewEncapsulation} from '@angular/core'; //...
Now, let's declare the component and the metadata associated with it:
@Component({ selector: 'todo-app', templateUrl: './app.html', styles: [ `ul li { list-style: none; } .completed { text-decoration: line-through; }` ], encapsulation: ViewEncapsulation.Emulated })
Here, we specify that the selector of the Todo
component will be the todo-app
element. Later, we add the template URL, which points to the app.html
file. After that, we use the styles
property; this is the first time we encounter it. As we can guess from its name, it is used to set the styles of the component.
As we know, Angular is inspired from Web Components, whose core feature is the shadow DOM. The shadow DOM allows us to encapsulate the styles of our Web Components without allowing them to leak outside the component's scope; Angular provides this feature. If we want Angular's renderer to use the shadow DOM, we can use ViewEncapsulation.Native
. However, at the time of writing this book, the shadow DOM was not supported by all browsers; if we want to have the same level of encapsulation without using the shadow DOM, we can use ViewEncapsulation.Emulated
.
If we don't want to have any encapsulation at all, we can use ViewEncapsulation.None
. By default, the renderer uses encapsulation of the type Emulated
.
Now, let's continue with the implementation of the application:
// ch4/ts/todo-app/app.ts class TodoCtrl { todos: Todo[] = [{ label: 'Buy milk', completed: false }, { label: 'Save the world', completed: false }]; name: string = 'John'; addTodo(label) { ... } removeTodo(idx) { ... } toggleCompletion(idx) { ... } }
Here is part of the implementation of the controller associated with the template of the Todo
application. Inside the class declaration, we initialized the todos
property to an array with two todo
items.
Now, let's update the template and render these items. Here's how this is done:
<ul> <li *ngFor="let todo of todos; let index = index" [class.completed]="todo.completed"> <input type="checkbox" [checked]="todo.completed" (change)="toggleCompletion(index)"> {{todo.label}} </li> </ul>
In the preceding template, we iterate over all the todo
items inside the todos
property of the controller. For each todo
item, we create a checkbox that can toggle
the item's completion status; we also render the todo
item's label with the interpolation directive. Here, we can note a syntax that was explained earlier:
(change)="statement"
.todo
item using [checked]="expr"
.In order to have a line across the completed todo
items, we bind to the class.completed
property of the element. Since we want to apply the completed
class to all the completed to-do items, we use [class.completed]="todo.completed"
. This way, we declare that we want to apply the completed
class depending on the value of the todo.completed
expression. Here is how our application looks now:
Figure 6
Similar to the class binding syntax, Angular allows us to bind to the element's styles and attributes. For instance, we can bind to the td
element's colspan
attribute using the following line of code:
<td [attr.colspan]="colspanCount"></td>
In the same way, we can bind to any style
property using this line of code: <div [style.backgroundImage]="expression"></td>
So far, so good! Now, let's implement the toggleCompletion
method. This method accepts the index of the to-do item as an argument:
toggleCompletion(idx) { let todo = this.todos[idx]; todo.completed = !todo.completed; }
In toggleCompletion
, we simply toggle the completed
boolean value associated with the current to-do item, which is specified by the index passed as an argument to the method.
Now, let's add a text input to add the new to-do items:
<p> Add a new todo: <input #newtodo type="text"> <button (click)="addTodo(newtodo.value); newtodo.value = ''"> Add </button> </p>
The input here defines a new variable called newtodo
. Now, we can reference the input element using the newtodo
identifier inside the template. Once the user clicks on the button, the addTodo
method defined in the controller will be invoked with the value of the newtodo
input as an argument. Inside the statement that is passed to the (click)
attribute, we also reset the value of the newtodo
input by setting it to the empty string.
Note that directly manipulating DOM elements is not considered as the best practice since it will prevent our component from running properly outside the browser environment. We will explain how we can migrate this application to Web Workers in Chapter 8, Tooling and Development Experience.
Now, let's define the addTodo
method:
addTodo(label) { this.todos.push({ label, completed: false }); }
Inside it, we create a new to-do item using the object literal syntax.
The only thing left out of our application is to implement removal of the existing to-do items. Since it is quite similar to the functionality used to toggle the completion of the to-do items, I'll leave its implementation as a simple exercise for the reader.
By refactoring our todo
application, we will demonstrate how we can take advantage of the directives' inputs and outputs:
Figure 7
We can think of the inputs as properties (or even arguments) that the given directive accepts. The outputs could be considered as events that it triggers. When we use a directive provided by a third-party library, mostly, we care about its inputs and outputs because they define its API.
Inputs refer to values that parameterize the directive's behavior and/or view. On the other hand, outputs refer to events that the directive fires when something special happens.
Now, let's divide our monolithic to-do application into separate components that communicate with each other. In the following screenshot, you can see the individual components, which when composed together, implement the functionality of the application:
Figure 8
The outer rectangle represents the entire Todo
application. The first nested rectangle contains the component that is responsible for entering labels of the new to-do items, and the one below it lists the individual items stored in the root component.
Having said this, we can define these three components as follows:
TodoApp
: This is responsible for maintaining the list of to-do items (adding new items and toggling the completion status).InputBox
: This is responsible for entering the label of the new to-do item. It has the following inputs and outputs:
TodoList
: This is responsible for rendering the individual to-do items. It has the following inputs and outputs:
Now, let's begin with the implementation!
Let's use a bottom-up approach, and start with the InputBox
component. Before that, we need a couple of imports from Angular's @angular/core
package:
import { Component, Input, Output, EventEmitter } from '@angular/core';
In the preceding code, we import the @Component
, @Input
, and @Output
decorators and the EventEmitter
class. As their names state, @Input
and @Output
are used for declaring the directive's inputs and outputs. EventEmitter
is a generic class (that is, accepting a type parameter), which when combined with the @Output
decorator helps us emit outputs.
As the next step, let's take a look at the InputBox
component's declaration:
// ch4/ts/inputs-outputs/app.ts @Component({ selector: 'text-input', template: ` <input #todoInput [placeholder]="inputPlaceholder"> <button (click)="emitText(todoInput.value); todoInput.value = '';"> {{buttonLabel}} </button> ` }) class InputBox {...}
Note that in the template, we declare a text input and keep a reference to it using the todoInput
identifier, and set its placeholder property to the value that we got from the evaluation of the inputPlaceholder
expression. The value of the expression is the value of the inputPlaceholder
property defined in the component's controller. This is the first input that we need to declare:
class InputBox { @Input() inputPlaceholder: string; ... }
Similarly, we declare the other input of the buttonLabel
component, which we use as a value of the label of the button:
class InputBox { @Input() inputPlaceholder: string; @Input() buttonLabel: string; ... }
In the preceding template, we bind the click event of the button to this emitText(todoInput.value); todoInput.value = '';
statement. The emitText
method should be defined in the component's controller; once it is invoked, it should emit the value of the text input. Here is how we can implement this behavior:
class InputBox { ... @Output() inputText = new EventEmitter<string>(); emitText(text: string) { this.inputText.emit(text); } }
Initially, we declare an output called inputText
. As its value, we set a new instance of the type EventEmitter<string>
that we create.
Inside the emitText
method, we invoke the emit
method of the inputText
and as its argument we pass the value of the text input.
Now, let's define the TodoList
component in the same fashion:
@Component(...) class TodoList { @Input() todos: Todo[]; @Output() toggle = new EventEmitter<Todo>(); toggleCompletion(index: number) { let todo = this.todos[index]; this.toggle.emit(todo); } }
Since the value of the object literal passed to the @Component
decorator is not essential for the purpose of this section, we omitted it. The complete implementation of this example can be found at ch4/ts/inputs-outputs/app.ts
. Let's take a look at the body of the TodoList
class. Similarly, to the InputBox
component, we define the todos
input. We also define the toggle
output by declaring the toggle
property, setting its value to a new instance of the type EventEmitter<Todo>
and decorating it with the @Output
decorator.
Now, let's combine the components we defined in the preceding section and implement our complete application.
The last component we need to take a look at is TodoApp
:
@Component({ selector: 'todo-app', template: ` <h1>Hello {{name}}!</h1> <p> Add a new todo: <input-box inputPlaceholder="New todo..." buttonLabel="Add" (inputText)="addTodo($event)"> </input-box> </p> <p>Here's the list of pending todo items:</p> <todo-list [todos]="todos" (toggle)="toggleCompletion($event)"></todo-list> ` }) class TodoApp {...}
Initially, we define the TodoApp
class and decorate it with the @Component
decorator. Note that in order to use the InputBox
and TodoList
components, we will need to include them in the declarations
property of the decorator of the module, which declares TodoApp
. The magic of how these components collaborate together happens in the template:
<input-box inputPlaceholder="New todo..." buttonLabel="Add" (inputText)="addTodo($event)"> </input-box>
First, we use the InputBox
component and pass values to the inputs inputPlaceholder
and buttonLabel
. Note that just like we saw earlier, if we want to pass an expression as a value to any of these inputs, we will need to surround them with brackets (that is, [inputPlaceholder]="expression"
). In this case, the expression will be evaluated in the context of the component that owns the template, and the result will be passed as an input to the component that owns the given property.
Right after we pass the value for the buttonLabel
input, we consume the inputText
output by setting the value of the (inputText)
attribute to the addTodo($event)
expression. The value of
$event
will equal the value we passed to the
emit
method of the
inputText
object inside the
emitText
method of
InputBox
(in case we bind to a native event, the value of the event object will be the native event object itself).
In the same way, we pass the input of the TodoList
component and handle its toggle output. Now, let's define the logic behind the TodoApp
component:
class TodoApp { todos: Todo[] = []; name: string = 'John'; addTodo(label: string) { this.todos.push({ label, completed: false }); } toggleCompletion(todo: Todo) { todo.completed = !todo.completed; } }
In the addTodo
method, we simply push a new to-do item to the todos
array. The implementation of toggleCompletion
is even simpler: we toggle the value of the completed flag that is passed as an argument to the to-do item. Now, we are familiar with the basics of the components' inputs and outputs.
In Angular, we have the same bubbling behavior we're used to in the DOM. For instance, let's suppose we have the following template:
<input-box inputPlaceholder="New todo..." buttonLabel="Add" (click)="handleClick($event)" (inputText)="addTodo($event)"> </input-box>
The declaration of input-box
looks like this:
<input #todoInput [placeholder]="inputPlaceholder"> <button (click)="emitText(todoInput.value); todoInput.value = '';"> {{buttonLabel}} </button>
Once the user clicks on the button defined within the template of the input-box
component, the handleClick($event)
expression will be evaluated.
Further, the target
property of the first argument of handleClick
will be the button itself, but the currentTarget
property will be the input-box
element. The event will bubble the same way if we're not using Angular. At some point, it will reach the document unless a handler along the way doesn't stop its propagation.
In contrast, if we have a custom @Output
, the event will not bubble and instead of a DOM event, the value of the $event
variable will be the value that we pass to the emit method of the output.
Now, we will explore how we can rename the directives' inputs and outputs. Let's suppose that we have the following definition of the TodoList
component:
class TodoList { ... @Output() toggle = new EventEmitter<Todo>(); toggle(index: number) { ... } }
The output of the component is called toggle
; the method that handles changes in the checkboxes responsible for toggling completion of the individual to-do items is also called toggle
. This code will not be compiled, as in the TodoList
controller we have two identifiers named in the same way. We have two options here:
If we rename the property, this will change the name of the component's output as well. So, the following line of code will no longer work:
<todo-list [toggle]="foobar($event)"...></todo-list>
What we can do instead is rename the toggle
property and explicitly set the name of the output using the @Output
decorator:
class TodoList { ... @Output('toggle') toggleEvent = new EventEmitter<Todo>(); toggle(index: number) { ... } }
This way, we will be able to trigger the toggle
output using the toggleEvent
property.
Note that such renames could be confusing and are not considered as best practices. For a complete set of best practices, visit https:// angular.io/styleguide .
Similarly, we can rename the component's inputs using the following code snippet:
class TodoList { @Input('todos') todoList: Todo[]; @Output('toggle') toggleEvent = new EventEmitter<Todo>(); toggle(index: number) { ... } }
Now, it doesn't matter that we renamed the input and output properties of TodoList
; it still has the same public interface:
<todo-list [todos]="todos" (toggle)="toggleCompletion($event)"> </todo-list>
The @Input
and @Output
decorators are syntax sugar for easier declaration of the directive's inputs and outputs. The original syntax for this purpose is as follows:
@Directive({ outputs: ['outputName: outputAlias'], inputs: ['inputName: inputAlias'] }) class Dir { outputName = new EventEmitter(); }
Using @Input
and @Output
, the preceding syntax is equivalent to this:
@Directive(...) class Dir { @Output('outputAlias') outputName = new EventEmitter<any>(); @Input('inputAlias') inputName: any; }
Although both have the same semantics, according to the best practices, we should use the latter one, because it is easier to read and understand.