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
stylesis 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
namestarts with the value "world" - The expression
name | uppercaseis equivalent touppercase("world"), which evaluates to WORLD - And
name | uppercase | titlecaseis 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
@Pipedecorator marks ourFunkycasePipeclass as a pipe -
[Line 6-7] Our class implements
transform()from thePipeTransforminterface. -
[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:
-
gis short forgenerate -
-sis short for--inline-styles -
-tis short for--inline-templates
Two files are generated:
into-angular/projects/hello/src/app/foobar/foobar.component.tsinto-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
#txtsyntax 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 theselectedItemproperty on our component with thevalueproperty on the DOM element. The binding is one-way. When theselectedItemproperty changes, the DOM property is updated. But when thevalueDOM property is updated as a result of user interaction on the web page, that change does NOT automatically updates theselectedItemproperty 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-labelattribute. - The
[class]="customClasses"is a class binding. This binds thecustomClassesproperty 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 thecustomStylesproperty 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 thehandleClickmethod on the component with theclickevent on the DOM element. So every time the click event fires, ourhandleClickmethod gets invoked. Notice how we have used the template reference variabletxtto 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)