Creating an item component

Components provide a way for you to organize your application. This article walks you through creating a component to handle the individual items in the list, and adding check, edit, and delete functionality. The Angular event model is covered here.

Prerequisites: Familiarity with the core HTML, CSS, and JavaScript languages, knowledge of the terminal/command line.
Objective: To learn more about components, including how events work to handle updates. To add check, edit, and delete functionality.

Creating the new component

At the command line, create a component named item with the following CLI command:

bash
ng generate component item

The ng generate component command creates a component and folder with the name you specify. Here, the folder and component name is item. You can find the item directory within the app folder.

Just as with the AppComponent, the ItemComponent is made up of the following files:

  • item.component.html for HTML
  • item.component.ts for logic
  • item.component.css for styles

You can see a reference to the HTML and CSS files in the @Component() decorator metadata in item.component.ts.

js
@Component({
  selector: 'app-item',
  templateUrl: './item.component.html',
  styleUrls: ['./item.component.css'],
})

Add HTML for the ItemComponent

The ItemComponent can take over the task of giving the user a way to check items off as done, edit them, or delete them.

Add markup for managing items by replacing the placeholder content in item.component.html with the following:

html
<div class="item">
  <input
    [id]="item.description"
    type="checkbox"
    (change)="item.done = !item.done"
    [checked]="item.done" />
  <label [for]="item.description">{{item.description}}</label>

  <div class="btn-wrapper" *ngIf="!editable">
    <button class="btn" (click)="editable = !editable">Edit</button>
    <button class="btn btn-warn" (click)="remove.emit()">Delete</button>
  </div>

  <!-- This section shows only if user clicks Edit button -->
  <div *ngIf="editable">
    <input
      class="sm-text-input"
      placeholder="edit item"
      [value]="item.description"
      #editedItem
      (keyup.enter)="saveItem(editedItem.value)" />

    <div class="btn-wrapper">
      <button class="btn" (click)="editable = !editable">Cancel</button>
      <button class="btn btn-save" (click)="saveItem(editedItem.value)">
        Save
      </button>
    </div>
  </div>
</div>

The first input is a checkbox so users can check off items when an item is complete. The double curly braces, {{}}, in the <label> for the checkbox signifies Angular's interpolation. Angular uses {{item.description}} to retrieve the description of the current item from the items array. The next section explains how components share data in detail.

The next two buttons for editing and deleting the current item are within a <div>. On this <div> is an *ngIf, a built-in Angular directive that you can use to dynamically change the structure of the DOM.

This *ngIf means that if editable is false, this <div> is in the DOM. If editable is true, Angular removes this <div> from the DOM.

html
<div class="btn-wrapper" *ngIf="!editable">
  <button class="btn" (click)="editable = !editable">Edit</button>
  <button class="btn btn-warn" (click)="remove.emit()">Delete</button>
</div>

When a user clicks the Edit button, editable becomes true, which removes this <div> and its children from the DOM. If, instead of clicking Edit, a user clicks Delete, the ItemComponent raises an event that notifies the AppComponent of the deletion.

An *ngIf is also on the next <div>, but is set to an editable value of true. In this case, if editable is true, Angular puts the <div> and its child <input> and <button> elements in the DOM.

html
<!-- This section shows only if user clicks Edit button -->
<div *ngIf="editable">
  <input
    class="sm-text-input"
    placeholder="edit item"
    [value]="item.description"
    #editedItem
    (keyup.enter)="saveItem(editedItem.value)" />

  <div class="btn-wrapper">
    <button class="btn" (click)="editable = !editable">Cancel</button>
    <button class="btn btn-save" (click)="saveItem(editedItem.value)">
      Save
    </button>
  </div>
</div>

With [value]="item.description", the value of the <input> is bound to the description of the current item. This binding makes the item's description the value of the <input>. So if the description is eat, the description is already in the <input>. This way, when the user edits the item, the value of the <input> is already eat.

The template variable, #editedItem, on the <input> means that Angular stores whatever a user types in this <input> in a variable called editedItem. The keyup event calls the saveItem() method and passes in the editedItem value if the user chooses to press enter instead of click Save.

When a user clicks the Cancel button, editable toggles to false, which removes the input and buttons for editing from the DOM. When editable is false, Angular puts <div> with the Edit and Delete buttons back in the DOM.

Clicking the Save button calls the saveItem() method. The saveItem() method takes the value from the #editedItem element and changes the item's description to editedItem.value string.

Prepare the AppComponent

In the next section, you will add code that relies on communication between the AppComponent and the ItemComponent.

Add the following line near the top of the app.component.ts file to import the Item:

ts
import { Item } from "./item";

Then, configure the AppComponent by adding the following to the same file's class:

js
remove(item: Item) {
  this.allItems.splice(this.allItems.indexOf(item), 1);
}

The remove() method uses the JavaScript Array.splice() method to remove one item at the indexOf the relevant item. In plain English, this means that the splice() method removes the item from the array. For more information on the splice() method, see the MDN Web Docs article on Array.prototype.splice().

Add logic to ItemComponent

To use the ItemComponent UI, you must add logic to the component such as functions, and ways for data to go in and out.

In item.component.ts, edit the JavaScript imports as follows:

js
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { Item } from "../item";

The addition of Input, Output, and EventEmitter allows ItemComponent to share data with AppComponent. By importing Item, ItemComponent can understand what an item is.

Further down item.component.ts, replace the generated ItemComponent class with the following:

js
export class ItemComponent {

  editable = false;

  @Input() item!: Item;
  @Output() remove = new EventEmitter<Item>();

  saveItem(description: string) {
    if (!description) return;
    this.editable = false;
    this.item.description = description;
  }
}

The editable property helps toggle a section of the template where a user can edit an item. editable is the same property in the HTML as in the *ngIf statement, *ngIf="editable". When you use a property in the template, you must also declare it in the class.

@Input(), @Output(), and EventEmitter facilitate communication between your two components. An @Input() serves as a doorway for data to come into the component, and an @Output() acts as a doorway for data to go out of the component. An @Output() has to be of type EventEmitter, so that a component can raise an event when there's data ready to share with another component.

Note: The ! in the class's property declaration is called a definite assignment assertion. This operator tells Typescript that the item field is always initialized and not undefined, even when TypeScript cannot tell from the constructor's definition. If this operator is not included in your code and you have strict TypeScript compilation settings, the app will fail to compile.

Use @Input() to specify that the value of a property can come from outside of the component. Use @Output() in conjunction with EventEmitter to specify that the value of a property can leave the component so that another component can receive that data.

The saveItem() method takes as an argument a description of type string. The description is the text that the user enters into the HTML <input> when editing an item in the list. This description is the same string from the <input> with the #editedItem template variable.

If the user doesn't enter a value but clicks Save, saveItem() returns nothing and does not update the description. If you didn't have this if statement, the user could click Save with nothing in the HTML <input>, and the description would become an empty string.

If a user enters text and clicks save, saveItem() sets editable to false, which causes the *ngIf in the template to remove the edit feature and render the Edit and Delete buttons again.

Though the application should compile at this point, you need to use the ItemComponent in AppComponent so you can see the new features in the browser.

Use the ItemComponent in the AppComponent

Including one component within another in the context of a parent-child relationship gives you the flexibility of using components wherever you need them.

The AppComponent serves as a shell for the application where you can include other components.

To use the ItemComponent in AppComponent, put the ItemComponent selector in the AppComponent template. Angular specifies the selector of a component in the metadata of the @Component() decorator. In this example, the selector is app-item:

js
@Component({
  selector: 'app-item',
  templateUrl: './item.component.html',
  styleUrls: ['./item.component.css']
})

To use the ItemComponent selector within the AppComponent, you add the element, <app-item>, which corresponds to the selector you defined for the component class to app.component.html. Replace the current unordered list in app.component.html with the following updated version:

html
<h2>
  {{items.length}}
  <span *ngIf="items.length === 1; else elseBlock">item</span>
  <ng-template #elseBlock>items</ng-template>
</h2>

<ul>
  <li *ngFor="let i of items">
    <app-item (remove)="remove(i)" [item]="i"></app-item>
  </li>
</ul>

The double curly brace syntax, {{}}, in the <h2> interpolates the length of the items array and displays the number.

The <span> in the <h2> uses an *ngIf and else to determine whether the <h2> should say "item" or "items". If there is only a single item in the list, the <span> containing "item" displays. Otherwise, if the length of the items array is anything other than 1, the <ng-template>, which we've named elseBlock, with the syntax #elseBlock, shows instead of the <span>. You can use Angular's <ng-template> when you don't want content to render by default. In this case, when the length of the items array is not 1, the *ngIf shows the elseBlock and not the <span>.

The <li> uses Angular's repeater directive, *ngFor, to iterate over all of the items in the items array. Angular's *ngFor like *ngIf, is another directive that helps you change the structure of the DOM while writing less code. For each item, Angular repeats the <li> and everything within it, which includes <app-item>. This means that for each item in the array, Angular creates another instance of <app-item>. For any number of items in the array, Angular would create that many <li> elements.

You can use an *ngFor on other elements, too, such as <div>, <span>, or <p>, to name a few.

The AppComponent has a remove() method for removing the item, which is bound to the remove property in the ItemComponent. The item property in the square brackets, [], binds the value of i between the AppComponent and the ItemComponent.

Now you should be able to edit and delete items from the list. When you add or delete items, the count of the items should also change. To make the list more user-friendly, add some styles to the ItemComponent.

Add styles to ItemComponent

You can use a component's style sheet to add styles specific to that component. The following CSS adds basic styles, flexbox for the buttons, and custom checkboxes.

Paste the following styles into item.component.css.

css
.item {
  padding: 0.5rem 0 0.75rem 0;
  text-align: left;
  font-size: 1.2rem;
}

.btn-wrapper {
  margin-top: 1rem;
  margin-bottom: 0.5rem;
}

.btn {
  /* menu buttons flexbox styles */
  flex-basis: 49%;
}

.btn-save {
  background-color: #000;
  color: #fff;
  border-color: #000;
}

.btn-save:hover {
  background-color: #444242;
}

.btn-save:focus {
  background-color: #fff;
  color: #000;
}

.checkbox-wrapper {
  margin: 0.5rem 0;
}

.btn-warn {
  background-color: #b90000;
  color: #fff;
  border-color: #9a0000;
}

.btn-warn:hover {
  background-color: #9a0000;
}

.btn-warn:active {
  background-color: #e30000;
  border-color: #000;
}

.sm-text-input {
  width: 100%;
  padding: 0.5rem;
  border: 2px solid #555;
  display: block;
  box-sizing: border-box;
  font-size: 1rem;
  margin: 1rem 0;
}

/* Custom checkboxes
Adapted from https://css-tricks.com/the-checkbox-hack/#custom-designed-radio-buttons-and-checkboxes */

/* Base for label styling */
[type="checkbox"]:not(:checked),
[type="checkbox"]:checked {
  position: absolute;
  left: -9999px;
}
[type="checkbox"]:not(:checked) + label,
[type="checkbox"]:checked + label {
  position: relative;
  padding-left: 1.95em;
  cursor: pointer;
}

/* checkbox aspect */
[type="checkbox"]:not(:checked) + label:before,
[type="checkbox"]:checked + label:before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 1.25em;
  height: 1.25em;
  border: 2px solid #ccc;
  background: #fff;
}

/* checked mark aspect */
[type="checkbox"]:not(:checked) + label:after,
[type="checkbox"]:checked + label:after {
  content: "\2713\0020";
  position: absolute;
  top: 0.15em;
  left: 0.22em;
  font-size: 1.3em;
  line-height: 0.8;
  color: #0d8dee;
  transition: all 0.2s;
  font-family: "Lucida Sans Unicode", "Arial Unicode MS", Arial;
}
/* checked mark aspect changes */
[type="checkbox"]:not(:checked) + label:after {
  opacity: 0;
  transform: scale(0);
}
[type="checkbox"]:checked + label:after {
  opacity: 1;
  transform: scale(1);
}

/* accessibility */
[type="checkbox"]:checked:focus + label:before,
[type="checkbox"]:not(:checked):focus + label:before {
  border: 2px dotted blue;
}

Summary

You should now have a styled Angular to-do list application that can add, edit, and remove items. The next step is to add filtering so that you can look at items that meet specific criteria.