This is the second part of our brief tour of the Angular framework. In this post, we take a closer look at the primary building block of an angular application, the component.
This series is best read in sequence, as each one usually builds upon the previous ones. The target audience is somewhat experienced web developers, specially those coming from another framework like React, Vue, or Ember. However, even if you are a beginner with a decent grasp of HTML, CSS, TypeScript, HTTP and NodeJS, you should still be able to easily follow along. and at the end of the series, have a good idea of how Angular works and how to go about using it to build modern web applications.
Table of Contents
- The journey so far
- The anatomy of a component
- Interpolation
- Pipes
- A new pipe
- A new component
- Repeating UI elements
- Conditionally rendering elements
- Binding to properties, events, and attributes
- Passing data between components
- What's next
The journey so far
In the previous post, we created a new Angular workspace and then added a web application project to it. If you haven't read through that yet, please go ahead and do.
Here's a recap of the commands we used:
# Install ng (The Angular CLI)
npm install -g @angular/cli
# Create an empty workspace
ng new into-angular \
--strict --create-application="false"
# Create a web app project
ng generate application hello \
--strict --inline-template --inline-style
cd into-angular
# Run hello project in local dev server
ng serve hello --open
# Run unit tests for hello project
ng test hello
# Run end-to-end tests for hello project
ng e2e hello
# Do a production build on the hello project
ng build hello --prod
# Install serve (simple web server)
npm install -g serve
# Serve the production build locally
serve into-angular/dist/hello
Back to TOC.
The anatomy of a component
Now open the into-angular/projects/hello/src/app/app.component.ts
file. It defines AppComponent
, the component that sits at the top of the component hierarchy making up our apps' user interface.
The generator has populated this file with some content useful for Angular novices. After having a look at that, delete everything and replace it with:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div>
<h1>Hello {{ name }}!</h1>
</div>
`,
styles: ['h1 { color: darkred; }']
})
export class AppComponent {
name = 'world';
}
This is the classical "Hello World Program"- a minimal program that displays the text "Hello World!".
If the ng serve
command was already running when the above change was made, you should see the development server picking up the change to the app.component.ts
file, do a build, and hot reload the browser; this time with just the text "Hello World!".
Let's take a closer look:
The @Component decorator "decorates" the plain class AppComponent
with component metadata and identifies it as a component.
- The selector is a CSS selector expression for locating DOM nodes where this component should be attached to.
- The template is a string of HTML that represents the structure of the contents of this component.
- The
styles
is an array of CSS strings that applies styling to the DOM elements making up this component.
Having the components' code, the HTML template, and the CSS styles in a single app.component.ts
file was a nice feature. However, for more complex components, it is more convenient to move the HTML and CSS files to separate files. We can do that with this:
@Component({
selector: 'app-root',
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
In fact, this is the default behavior. It is what would have been generated if we had omitted the --inline-template
, and --inline-style
options to the ng generate application
command.
Back to TOC.
Interpolation
The HTML template in above example contains a template expression enclosed in a pair of double braces.
<h1>Hello {{ name }}!</h1>
Template expressions are side-effect free JavaScript expressions that are evaluated in the context of a component object. In this case, the context is:
name = 'world';
Since the components' name property is initialized to the value 'world'
, when the component first renders, we see that the text node inside the h1 element contains the text Hello world!
.
If we programmatically change change the value of the components' name
property, Angular will detect the change, re-evaluate the template expression, and update the DOM.
Back to TOC.
Pipes
Try changing the template expression to:
<h1>Hello {{ name | uppercase }}!</h1>
What do you see on the screen. Can you guess what's going on?
Try changing to:
<h1>Hello {{ name | uppercase | titlecase }}!</h1>
What do you see on the screen?
The "|
" character is used in template expressions to act as a "pipe" that pumps the left-side expression as a parameter to the function on the right. These can be chained as we see above to form a "pipe line" through which the value of the left most expression flows through.
- The expression
name
starts with the value "world" - The expression
name | uppercase
is equivalent touppercase("world")
, which evaluates to WORLD - And
name | uppercase | titlecase
is equivalent totitlecase(uppercase(name)
which evaluates totitlecase("WORLD")
which finally evaluates to "World"
So we can use pipes to transform our template expressions. Angular comes with a bunch of built-in pipes, let's try the date pipe to format a time string:
{{ currentTime | date: 'h:mm a on MMMM d, y' }}
Of course you need the currentTime
property set in the component:
currentTime = Date()
It is also quite easy to create our own custom pipe- it is just a simple function after all.
Back to TOC.
A new pipe
Let's create a funkycase
pipe that changes "world" to "wOrLd"
ng generate pipe funkycase
Notice that we didn't specify a project name as the hello
project is used by default.
Run the unit tests with ng test
- notice the new test that was created for our "funkycase pipe"
Try applying our new pipe:
<h1>Hello {{ name | funkycase }}!</h1>
We only get Hello !
The template expression has produced and empty result- i.e. funkycase("world")
hasn't returned a valid value. Let's open the generated into-angular/projects/hello/src/app/funkycase.pipe.ts
and fix this:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'funkycase',
})
export class FunkycasePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
if (typeof value === 'string') {
return [...value]
.map((character, index) =>
index % 2 === 0
? String(character).toLowerCase()
: String(character).toUpperCase()
)
.join('');
}
return value;
}
}
-
[Line 3-5] The
@Pipe
decorator marks ourFunkycasePipe
class as a pipe -
[Line 6-7] Our class implements
transform()
from thePipeTransform
interface. -
[Line 9] First we use the
...
syntax to spread out the string valued input into an array of strings - [Line 10-13] Then we map over this array collecting the "lowercase version if the index is even" and the "uppercase version otherwise" to a new array
- [Line 15] Finally we join this new array to form the end result
- [Line 18] If the input value is not a string just return it back
Let's add some more unit tests for this in app.component.spec.ts
it('handles a string', () => {
const pipe = new FunkycasePipe();
expect(pipe.transform('apple')).toEqual('aPpLe');
});
it('handles a number by passing it through unchanged', () => {
const pipe = new FunkycasePipe();
expect(pipe.transform(42)).toEqual(42);
});
Run ng test
to see if all is still well.
You may get an error regarding funkycase
not found from the AppComponnt unit tests. This is because the "Test Bed" used for running the AppComponent unit tests doesn't declare the FunkyTestPipe
.
We can fix this in the app.component.spec.ts
file, in the beforeEach
call, change:
+- declarations: [AppComponent, FunkycasePipe],
-- declarations: [AppComponent],
Why did you not get this error when running the app? This is because the ng generate pipe
command updated the app.module.ts
and added the FunkyTestPipe
to the declarations array in the module metadata object.
Back to TOC.
A new component
Let's create a new component:
ng g component foobar -st
This time, we've used the shorthand syntax:
-
g
is short forgenerate
-
-s
is short for--inline-styles
-
-t
is short for--inline-templates
Two files are generated:
into-angular/projects/hello/src/app/foobar/foobar.component.ts
into-angular/projects/hello/src/app/foobar/foobar.component.spec.ts
And the AppModule
definition in app.module.ts
file is updated to add the FoobarComponent
to its list of declarations.
@NgModule({
declarations: [AppComponent, FunkycasePipe, FoobarComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
What this means is, that the FoobarComponent
belongs to the AppModule
.
As we can see, we also have FunkycasePipe
in the declarations metadata field. Also, an Angular component is really a special case of what is known as a "directive". Components, pipes, and directives are known as declarables.
Every component, pipe, and directive should belong to one and only one Angular module. This is because Angular uses NgModules to group those three types of things and allow those to refer to each other.
So, since both the AppComponent
and FunkycasePipe
belong to the AppModule
, we can reference the FunkycasePipe
as funkycase
pipe from inside the HTML template of the AppComponent
.
Open the app.component.ts
file. There is no import statement for FunkycasePipe
in that file, but the HTML template has the expression {{ name | funkycase }}
. And it works!
Remove FunkycasePipe
from the AppModule's declrations
field and see what happens.
ERROR in projects/hello/src/app/app.component.ts:8:20 - error NG8004: No pipe found with name 'funkycase'.
8 <p>{{ name | funkycase }}</p>
Revert that change to fix.
Let's use our new component inside the AppComponent. Because the selector
metadata field for FoobarComponent
is set to app-foobar, we only need to append the following to the AppComponent
template:
<app-foobar></app-foobar>
Run ng test
and ng e2e
, just in case we broke anything. Everything should check out.
Back to TOC.
Repeating UI elements
Right now, our FoobarComponent
just display the text "foobar works!". Let's go work on it.
Change the template to:
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
And add the items
property:
items = ['foo', 'bar'];
The ngFor is a structural directive. It is called an structural directive because it affect the structure of the view by creating or removing elements.
Back to TOC.
Conditionally rendering elements
Another structural directive is ngIf, let's see it in action by replacing he FoobarComponent
template with:
<div>
<h3>Your options</h3>
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
<div *ngIf="selectedItem == ''">
Please type-in one of the options.
</div>
<div *ngIf="items.includes(selectedItem)">
Selected option is: <strong>{{ selectedItem }}</strong>
</div>
</div>
And adding the selectedItem
property:
selectedItem = '';
At this point, our app should look like this:
Back to TOC.
Binding our component to the DOM element
Let's add an input element where we can type-in our selection. Add this HTML to the end of the FoobarComponent template (between line 12 and 13 of previous code listing):
<div>
<input
#txt
type="text"
[value]="selectedItem"
[attr.aria-label]="ariaLabel"
[class]="customClasses"
[style]="customStyles"
/>
<button (click)="handleClick(txt.value)">Select</button>
<p *ngIf="msg != ''">{{ msg }}</p>
</div>
Also add these properties in our component:
ariaLabel = 'Option to select';
customClasses="class1 class2";
customStyles = 'width: 200px; background-color: lightblue';
We have a bunch of new concepts in this one short code snippet:
- The
#txt
syntax declares a template reference variable with the nametxt
, which we can use to refer to the corresponding DOM element, as done at line 10 above. - The
[value]="selectedItem"
is a property binding. This binds theselectedItem
property on our component with thevalue
property on the DOM element. The binding is one-way. When theselectedItem
property changes, the DOM property is updated. But when thevalue
DOM property is updated as a result of user interaction on the web page, that change does NOT automatically updates theselectedItem
property on the component. - The
[attr.aria-label]="ariaLabel"
is an attribute binding. The reason for having this type of binding is that there are some HTML element attributes that do not have corresponding DOM properties. Like. for example, thearia-label
attribute. - The
[class]="customClasses"
is a class binding. This binds thecustomClasses
property of the component, which in this example is a space seprated list of CSS classes, to the classes that are actually attached to the DOM element. - The
[style]="customStyles"
is a style binding. This binds thecustomStyles
property of the component, which contains a semicolon seprated list of CSS styles, to the DOM element. - The
(click)="handleClick(txt.value)"
is an event binding. This binds thehandleClick
method on the component with theclick
event on the DOM element. So every time the click event fires, ourhandleClick
method gets invoked. Notice how we have used the template reference variabletxt
to access the DOM element here.
Back to TOC.
Passing data between components
Like we discussed before, a component is responsible for a chunk of our applications' user interface. For better mantainability, components keep largely to themselves and has no idea what's going on outside themselves. But when we compose our application out of a hierarchy of components, we need a way to send data into a component and get data out of that component. For this, Angular provides the Input and Output decorators:
Let's create a new component to understand how this works in practice:
ng g component baz -st
Replace the generated Baz component code in baz.component.ts
with this:
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-baz',
template: `
<div>
<p>{{ msg }}</p>
<input #txt type="text" />
<button (click)="handleClick(txt.value)">Ok</button>
</div>
`,
styles: [
'div{margin: 10px; padding: 5px; width: 200px; border: 1px solid grey}',
],
})
export class BazComponent {
@Input() msg: string = 'Default prompt';
@Output() nameSelected = new EventEmitter<string>();
handleClick(val: string) {
this.nameSelected.emit(val);
}
}
And use the Baz component from inside the App component like this:
<app-baz [msg]="msgIn" (nameSelected)="onNameSelected($event)"></app-baz>
<p>
Name received from the baz component:<br />
<code>{{ nameOut }}</code>
</p>
You need to append the above HTML code to the template in the app.component.ts
file, and the below TypeScript code to the class body.
msgIn = 'Please enter your name';
nameOut = '<No Ourput from child component yet>';
onNameSelected(name: string) {
this.nameOut = name;
}
The Baz component displays a customisable message, reads user text input, and provides an Ok button that when clicked emits a custom event with what the user entered in the text box. Let's see it in action:
The displayed message is customisable by the parent component.
@Input() msg: string = 'Default prompt'
The Input decorator marks msg
as an input property.
To the parent component, msg
is "bindable" just like a DOM property:
<app-baz [msg]="msgIn"></app-baz>
The child component fires output events and the parent component binds to these events using the familiar event binding syntax.
<app-baz (nameSelected)="onNameSelected($event)"></app-baz>
The Output decorator is used on a field in the child component to declare that field as an output event.
@Output() nameSelected = new EventEmitter<string>();
When the child component has data it needs to ouput, it uses nameSelected
and emit an event with that data as the payload:
this.nameSelected.emit(val);
Back to TOC.
What's next
The code for our workspace as at the end of this post can be found under the p02
tag in the nalaka/into-angular repository.
nalaka / into-angular
Code for the dev.to series "A tour of Angular for web developers"
In the next post we will see how to create services that interact with the world outside our component and how to inject them where they are needed.
Following this, we will continue our tour by looking at routing, forms, material styling, animation, internationalization, accessibility, testing, and keeping our applications up-to date as Angular itself evovles.
Back to TOC.
Top comments (0)