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 HTMLitem.component.ts
for logicitem.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 theitem
field is always initialized and notundefined
, 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;
}