Making the Move from AngularJS to Angular - Part Two: Controllers to Components and HTML Syntax
To alleviate some of the same trials and tribulations we encountered in making the move from AngularJS to Angular, we thought we'd share some of the common areas most developers will encounter in this blog series.
- Part One: Understanding the Basics
- Part Two: Controllers to Components and HTML Syntax
- Part Three: Services, Observables, and Routing (Coming Soon)
- Part Four: Commonly used controls and other items worth sharing (Coming Soon)
- Part Five: Enterprise Case Study (Coming Soon)
In the previous blog, we provided some guidance on how to setup an Angular application. In this blog, we'll talk more about the heart of all Angular applications - Components.
Component Basics
Previously in AngularJS, controllers were used to along with a defined html file to build the look and feel of an application. In Angular, components are used.
For a component to be declared, an exported TypeScript class is decorated with an Angular component and its metadata. The naming pattern of a component class is {Name}Component, and the pattern of the file is {name}.component.ts.
@Component({ selector: '', template: '', styles: [] }) export class NameComponent{}
The above code snippet is not fully functional but allows me to see the core of an Angular component. The @Component decorator informs Angular that this class is an Angular Component. The objective of the metatdata object is to tell Angular how this component is to be used. Below are three of the possible metadata properties, but these tend to be the properties most used:
- The selector is a CSS selector, which identifies when this component should be used in another template.
- The templateis the HTML to be used for this constructor. If this HTML spans multiple lines, the "tick" character should be used at the beginning and end.
- The styles is an array of CSS styles to apply to the HTML in this component. There are certain selectors (:host, :host-context), which are used to reach HTML outside of this particular component. The encapsulation property can also be utilized to change the component's scope.
Also, in the above code, template and styles could be switched out with templateUrl and styleUrls as seen in code below. These properties reference .html and .css files.
@Component({ selector: 'url-component', templateUrl: './url.component.html', styleUrls: ['./url.component.css'] }) export class TemplateStylesUrlComponent { }
HTML Common Syntax
HTML Angular Syntax allows components to affect the look and feel of its own content as well as allows components to talk to each other. Below is some of the most commonly used syntax:
uses curly braces {{}} to extract the content of the object property
@Component({ selector: 'interpolation-component', template: ` <h4>Interpolation Example</h4> <p> This is an example of interpolation at {{dateTimeString}}</p> ` }) export class InterpolationComponent { dateTimeString: string = (new Date()).toString(); }
One-Way Binding allows values from a component object/property to be set as an attribute of an HTML element. Enclosing an attribute in [], designates that it is one way bound.
@Component({ selector: 'one-way-binding', template: ` <h4>One-Way Binding Example</h4> <div class="row"> <div class="col-md-12 form-group"> <label>Widget Name:</label> <input type="text" class="form-control" disabled="" [value]="widget.name" /> </div> </div> ` }) export class OneWayBindingComponent { widget: Widget = new Widget('Computer AAA'); }
specifies that changing a value within a component will change the value in the HTML as well as changing the value in the HTML will also change the value in the component. Two-way binding is designated by specifying [(ngModel)]. In this case, I am using ngModel, which is an internal Angular directive for form elements. All [(boundName)] is doing is combing both one-way binding [] and event binding (), and because of that I can use this syntax on any element as long as it supports both the ability to have a value set on boundName and a boundNameChange event.
@Component({ selector: 'two-way-binding', template: ` <h4>Two-Way Binding Example</h4> <div class="row"> <div class="col-md-12 form-group"> <label>Widget Name: <input type="text" class="form-control" [(ngModel)]="widget.name" /> <span class="form-control-static">{{widget.name}}</span> </div> </div> ` }) export class TwoWayBindingComponent { widget: Widget = new Widget(); }
Event Binding binds a particular action to an HTML element. When this bounded action occurs, the event binding syntax also specifies the action to take, such as an inline action within the HTML or a function to call within the component. The code below shows an example of event binding where a (click) event calls handleButtonClick(). $event is an object which conveys information about the event and can be used in the action being taken.
@Component({ selector: 'event-binding', template: ` <h4>Event Binding Example</h4> <div class="row"> <div class="col-md-12 form-group"> <label>Widget Name:</label> <span class="form-control-static">{{widget.name}}</span> </div> </div> <div class="row"> <div class="col-md-12"> <button type="button" class="btn btn-primary btn-xs" (click)="handleButtonClick();">Click Me</button> </div> </div> ` }) export class EventBindingComponent { widget: Widget = new Widget('Computer ABC'); handleButtonClick(): void { this.widget.name = 'Comptuer XYZ'; }; }
*ngIf allows content to be displayed conditionally
@Component({ selector: 'ngif-component', template: ` <h4>*ngIf Example</h4> <div class="row"> <div class="col-md-12 form-group"> <span class="form-control-static" *ngIf="showFirstComputer">Show First Computer</span> <span class="form-control-static" *ngIf="!showFirstComputer">Show Second Computer</span> </div> </div> <div class="row"> <div class="col-md-12"> <button type="button" class="btn btn-primary btn-xs" (click)="showFirstComputer = !showFirstComputer">Click Me</button> </div> </div> ` }) export class NgIfComponent { showFirstComputer: boolean = true; }
*ngFor provides the ability to loop over an array of objects
@Component({ selector: 'ngfor-component', template: ` <h4>*ngFor Example</h4> <div class="row" *ngFor="let widget of widgets; let i = index;"> <div class="col-md-12 form-group"> <label>Computer Name:</label> <span class="form-control-static">{{widget.name}} with Index: {{i}}</span> </div> </div> ` }) export class NgForComponent { widgets: Widget[] = [new Widget("Computer 123"), new Widget("Computer 987")]; }
CSS classes can still be dynamically set as they were in AngularJS, but in addition, individual classes and individual styles can be set as one way bounded attributes. Classes are set by specifying the class name and a boolean function: [class.className]="boolean function". Styles are set a little bit differently in that the style is specified and a value from the component is used: [styles.style]="value"
@Component({ selector: 'class-style-binding', template: ` <h4>Class & Style Binding</h4> <div class="row"> <div class="col-md-12 form-group"> <span class="form-control-static" [style.color]="textColor">Changing Text Color</span> </div> </div> <div class="row"> <div class="col-md-12"> <button type="button" class="btn btn-primary btn-xs" (click)="textColor = 'red'">Turn Text Red</button> <button type="button" class="btn btn-primary btn-xs" (click)="textColor = 'blue'">Turn Text Blue</button> </div> </div> <div class="row"> <div class="col-md-12 form-group"> <span class="form-control-static" [class.highlight-code]="turnOnHighlight">Highlight Changes</span> </div> </div> <div class="row"> <div class="col-md-12"> <button type="button" class="btn btn-primary btn-xs" (click)="turnOnHighlight = true">Turn Highlighter ON</button> <button type="button" class="btn btn-primary btn-xs" (click)="turnOnHighlight = false">Turn Highlighter OFF</button> </div> </div> ` }) export class ClassStyleBindingComponent {}
Input/Output
Angular provides @Input and @Output decorators to help ease Communication between parent components and child components.
The @Output decorator provides developers the opportunity to emit events using EventEmitter>T<. The parent listens for these events and handles them accordingly. The property decorated with the @Output decorator is the event the parent will listen to. The child will emit this event by call .emit(). A value can also be passed into the emit method as such .emit(value/object)
Parent Component
@Component({ selector: 'output-decorator', template: ` <h4>Output Decorator Example</h4> <div class="row"> <div class="col-md-12 form-group"> <label>Button Action:</label> <span class="form-control-static">{{buttonAction}}</span> </div> </div> <button (buttonClicked)="handleButtonClicked($event)"></button> ` }) export class OutputParentComponent { buttonAction: string = "No Button Pressed"; handleButtonClicked(actionTaken: string): void { this.buttonAction = actionTaken; } }
Child Component
@Component({ selector: 'buttons', template: ` <div class="row"> <div class="col-md-12 form-group"> <button type="button" class="btn btn-primary btn-xs" (click)="buttonClicked.emit('Save Clicked')">Save</button> <button type="button" class="btn btn-default btn-xs" (click)="buttonClicked.emit('Remove Clicked')">Remove</button> </div> </div> ` }) export class OutputChildComponent { @Output() buttonClicked: EventEmitter = new EventEmitter(); }
The @Input allows parent components to be able to pass values, objects, functions down into a child to use as necessary. Even though a child might have an input property declared, this does not mean the parent has to call it. Also, the value bound to an input property attribute needs to be the same as what the child component expects.
Parent Component
@Component({ selector: 'input-decorator', template: ` <h4>Input Decorator Example</h4> <widget-info [widgetDetails]="widget"></widget-info> ` }) export class InputParentComponent { widget: Widget = new Widget("Computer ABC"); }
Child Component
@Component({ selector: 'widget-info', template: ` <div class="row"> <div class="col-md-12 form-group"> <label>Widget Name:</label> <span class="form-control-static">{{widgetDetails.name}}</span> </div> </div> ` }) export class InputChildComponent { @Input() widgetDetails: Widget; }
A Child Component can manipulate the data of an input property. As with any class property, there are getter and setters.
_widgetDetails: Widget; @Input() get widgetDetails() { return this._widgetDetails; } set widgetDetails(value: any) { value.name = 'Test Name'; this._widgetDetails = value; }
The data can also be manipulated in the OnChanges Lifecycle Hook.
ngOnChanges(changes: any) { }
Data Flow
Angular follows a unidirectional data flow, which means data flows down from parent to child. Here are some of the benefits of unidirectional data flow:
- More predictable than cycles
- Has better performance because it will make a single pass and after that all the data should be settled. If a side effect occurs where data is not stable, Angular will throw an error
Change Detection
To better understand unidirectional data flow, change detection should be understood. Asynchronous actions are what causes change detection to trigger:
- Events (i.e., click, change, keypress)
- Timers (setInterval, Observable.interval)
- Fetching data from a server
Change Detection starts at the top of the Component Tree and works its way down through every leaf checking for changes. By default, Angular starts at the top of the component tree and runs change detection on every component. Because JavaScript objects are mutable, Angular runs change detection for every component for every event. By changing the detection strategy to OnPush, components will be marked as immutable, which will cause change detection to only run when input properties are changed.
changeDetection: ChangeDetectionStrategy.OnPush
Smart & Dumb Components
Smart Components: In most architectural cases, you will want a single component, which controls all of your data and event handlers. This is a called a smart component.
Dumb Components: On the other hand, all the child components of this smart component only have input and output properties. These are dummy components as they can't do anything other than accept data, display the data, and fire events.
Template Reference & ViewChild
A template reference can get a reference to an element by specifying the pound symbol followed by a reference variable name #thirdParty. Within the template, this template reference variable can be used along with any property/function on that DOM element. A template reference's limitation is in its name. It cannot be referenced inside the component class.
To get past the template reference limitation, the @ViewChild decorator is used. Inside the decorator constructor, the name of the template reference needs to be inserted. This allows one to reference the DOM object for a template element inside the component. From here, one can call functions and assign values on the child.
Interface for Child Component/Third Party
export interface IThirdParty { show(): void; hide(): void; }
Parent Component
@Component({ selector: 'templateref-viewchild', template: ` <h4>Template Reference & View Child Example</h4> <third-party #thirdParty></third-party> <div class="row"> <div class="col-md-12"> <button type="button" class="btn btn-primary btn-xs" (click)="thirdParty.show();">Show (Using Template Reference Variable)</button> <button type="button" class="btn btn-primary btn-xs" (click)="hide();">Hide (Using @ViewChild)</button> </div> </div> ` }) export class ViewChildComponent { @ViewChild('thirdParty') thirdPartyObj: IThirdParty; hide() { this.thirdPartyObj.hide(); } }
Child Component
@Component({ selector: 'third-party', template: ` <div class="row" *ngIf="showThirdParty"> <div class="col-md-12 form-group"> <p> Hello! I am a 3rd Party Object. You cannot see my HTML nor Component class. All you know is that I've exposed the show() and hide() methods. </p> </div> </div> ` }) export class ThirdPartyComponent { showThirdParty: boolean = false; show() { this.showThirdParty = true; } hide() { this.showThirdParty = false; } }
Lifecycle Hooks
In addition to component classes having constructors where services can be injected as well as parameters initialized, Angular provides lifecycle hooks. Figure 1 shows a list of all the lifecycle hooks that are able to be tied into. OnChanges, OnInit and OnDestroy are a couple of the most popular.
OnChanges:
- Every time input values change, this method gets called. Here we are able to intercept these values and make necessary changes, calls, etc.
- Receive a SimpleChanges object
- Key-Value pairs: Input Property Name - SimpleChange Object
- SimpleChange Object provides previousValue and currentValue
- Any @Input property will have its value here
OnInit:
- Called once when the component is initialized.
- Typically, where initialization calls to the API occur.
OnDestroy:
- Called once when the component is being destroyed
- On times when subscriptions do not auto destroy, this is an ideal place to put unsubscribe.
Figure-1: Angular Lifecycle Hooks provided by Angular Documentation
We'll cover more on this in the next blog of this series where we'll discuss Observables
Tell Us: What are the pros/cons of using components vs controllers? What challenges are you facing with making the switch?