Creating custom Angular components

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.

Introducing the component's view encapsulation

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.

Implementing the component's controllers

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:

  • We bind to the change event of the checkbox using (change)="statement".
  • We bind to the property of the 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:

Implementing the component's controllers

Figure 6

Note

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>

Handling user actions

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

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 8Tooling 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.

Using inputs and outputs

By refactoring our todo application, we will demonstrate how we can take advantage of the directives' inputs and outputs:

Using 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.

Determining of the inputs and outputs

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:

Determining of the inputs and outputs

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:
    • Inputs: a placeholder for the textbox and a label for the submit button.
    • Outputs: the content of the textbox once the submit button is clicked.

  • TodoList: This is responsible for rendering the individual to-do items. It has the following inputs and outputs:
    • Inputs: a list of to-do items.
    • Outputs: the completion status of a to-do item.

Now, let's begin with the implementation!

Defining the inputs and outputs

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.

Note

Note that all the outputs of all the components need to be instances of EventEmitter.

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.

Passing inputs and consuming the outputs

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.

Event bubbling

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.

Renaming the inputs and outputs

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:

  • We can rename the method.
  • We can rename the property.

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

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> 

An alternative syntax to define inputs and outputs

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset