masaj salonu masaj salonları
Home » Advertising » Developing a Web Application Using Angular (Part 4)

Developing a Web Application Using Angular (Part 4)

In the previous article, we moved one step closer to our working web application by building our service provider layer that acts as an abstraction of our order management web service. With this foundation set, we are ready to complete the last layer in our web application: the User Interface (UI) layer.

Implementing the UI Layer

The first step in implementing our UI layer is to reflect upon the UI design we created in Part 1. In this design, we created two main components: (1) an order list component and (2) a save order component. While the latter component is abstracted into a single component, we are actually designing it to perform two separate tasks: (1) create a new order and (2) edit an existing order. This double-duty derives from the fact that components for creating a new order and editing an existing order look identical, with a few important caveats:

  1. Different actions executed when the save button is clicked (one component creates a new order while the other commits changes to an existing order).
  2. The title of the components (Create Order in the case of the create order component and Edit Order in the case of the edit order component).
  3. The editing component requires the ID of the order to edit while the creation component uses a blank order that the user fills in.
  4. The text of the save button (Create in the case of the create order component and Update in the case of the edit order component).

This is a common issue in many web applications, but the Angular framework provides a mechanism for solving it with minimal repetition. Angular allows us to create a single template that acts as the look of a component, while at the same time allowing us to create multiple component classes that use this single template. We can also create an abstract component class that can contain the functionality that is common between the create order and edit order components. Combining these two mechanisms, we end up with the following components and templates for our UI layer:

Image title

Although we could have skipped the creation of an abstract component and had our edit and create order components use the common template, by creating an abstract component, we ensure that the behavior expected by the common template is present in both of the components. This abstract class also provides a convenient location for the common functionality (such as error checking) used by both components to be located.

With our UI design and component design complete, we can now move to bringing the UI to life. Since the templates are depended on by the components, we will first create the templates and then move onto creating the components. Before we do so, though, we will need to add support for Bootstrap, a Hypertext Markup Language (HTML), Cascading Stylesheets (CSS), and JavaScript UI framework that will use in our templates.

Adding Bootstrap Support

In order to add Bootstrap support, we use a Content Delivery Network (CDN) to import the CSS and JavaScript support to our application. By using a CDN, we do not need to download or include any files in our application. Instead, we simply import the files hosted by the CDN. To do this, we simply add the following lines to the head (between the head tags) in our index.html file:

link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"
script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"/script
script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"/script

With these lines added, we can now use Bootstrap CSS classes throughout our templates.

Creating the Templates

A template in Angular is simply an HTML file that dictates the visual structure of a component. The template for our SaveOrderComponent (used by EditOrderComponent and CreateOrderComponent) is as follows:

div
    h2{{getTitle()}}/h2
    form class="form-horizontal" *ngIf="order !== undefined"
        div class="form-group" [ngClass]="{'has-error': !isDescriptionValid()}"
            label for="description" class="col-sm-2 control-label"Description/label
            div class="col-sm-10"
                input type="text" class="form-control" id="description" name="description" [(ngModel)]="order.description" placeholder="Description"
                p *ngIf="!isDescriptionValid()" class="help-block"Description may not be blank/p
            /div
        /div
        div class="form-group" [ngClass]="{'has-error': !isCostValid()}"
            label for="cost" class="col-sm-2 control-label"Cost/label
            div class="col-sm-10"
                div class="input-group"
                    span class="input-group-addon"$/span
                    input type="number" class="form-control cost" id="cost" name="cost" step="0.01" min="0.00" [(ngModel)]="order.cost" placeholder="Cost"
                /div
                p *ngIf="!isCostValid()" class="help-block"Cost may not be blank/p
            /div
        /div
        div class="form-group"
            div class="col-sm-offset-2 col-sm-10"
            div class="checkbox"
                label
                    input type="checkbox" [checked]="order.isComplete" (change)="order.toggleComplete()" Completed
                /label
            /div
            /div
        /div
        div class="form-group"
            div class="col-sm-offset-2 col-sm-10"
                button type="submit" class="btn btn-primary" (click)="onSave()" [disabled]="!canSubmit()"{{getSaveButtonText()}}/button
                button type="submit" class="btn btn-default" (click)="onCancel()"Cancel/button
            /div
        /div
    /form
/div

This template contains standard HTML, with a few important exceptions. The first of these exceptions is the inclusion of data within double braces ({{ and }}). Any code within double braces executes with respect to the component that uses the template. For example, the snippet {{getTitle()}} means that our template expects that the component that uses it has a getTitle() method, the result of which is displayed in place of the double braces expression. This means that when the template is shown to the user in his or her browser, the resolved HTML will be h2Whatever Title is Returned/h2 (assuming that getTitle() returns Whatever Title is Returned), rather than h2{{getTitle()}}/h2.

The next exception is the use of special HTML attributes (specifically called directives in Angular) that are preceded by an asterisk (*), surrounded by square brackets ([ and ]), surrounded by parenthesis (( and )), or surrounded by both parenthesis and square brackets. In particular, there are a few expressions that require further explanation:

  • *ngIf: If the contents of this attribute resolve to true, the corresponding HTML element is displayed. The contents of this attribute are treated as executable TypeScript code. Therefore, the expression *ngIf="order !== undefined" means that the form element is displayed if the order attribute of the component using this template is not invalid.
  • [ngClass]: Appends the corresponding HTML classes if each entry resolves to true. For example, the expression [ngClass]="{'has-error': !isDescriptionValid()}" means that the corresponding HTML elements that include this expression are marked with the HTML class has-error if !isDescriptionValid() resolves to true. More conditional classes can be added by adding more entries to the JSON object, where each key equals the name of the class to add and each corresponding value represents the condition that must be true for the class to be added.
  • [(ngModel)]: Bidirectionally binds the value of the expression to the contents of the input. For example, the expression [(ngModel)]="order.description" binds the current value of the description field of the order field in the component using the template to the input element. Thus, when the user changes the value in the input element, the value of order.description changes to match the input field. Since this binding is bidirectional, if the value of order.description changes (say, by the component), the value of the input field is updated accordingly. Therefore, if the value of the input field changes, order.description is updated to match the new input element value. Vice-versa, if the value of order.description changes, the value of the input element is changed to match the new value of order.description.
  • [checked]: Sets the initial value of a checkbox to the result of the expression. For example, the expression [checked]="order.isComplete" means that the associated checkbox will be true if order.isComplete is true and false if order.isComplete is false.
  • (changed): Called when the value of the associated HTML element changes. For example, the expression (change)="order.toggleComplete()" associated with a checkbox means that each time the checkbox is checked or unchecked, the method toggleComplete() method is called on the order field associated with the component using the template.
  • (click): Called when the associated HTML element is clicked. Just as with (changed), the contents of the attribute are executed when the associated HTML element is clicked.
  • [disabled]: Disables the associated HTML element when the contents of the attribute resolve to true. If the contents resolve to false, the HTML is enabled. This value may dynamically change (if the contents of the attribute are false, and if at some point in the future the contents of the attribute resolve to true, the associated HTML element is changed to enabled).

As we will see shortly, the Angular directive used in a template is closely related to the component that uses it. For example, a directive such as *ngIf="order !== undefined" expects that the component that uses the template has a field named order.

In order to add some visual enhancements, we have also included some CSS for the template:

h2 {
    border-bottom: 1px solid 
    padding-bottom: 15px;
    margin-bottom: 35px;
}

input.cost {
    width: 80px;
}

The template for the list of order components is listed below and is very similar in its usage of HTML elements as the save order template, but it does have some noticeable differences.

div class="orders"
    div class="search"
        div class="input-group"
            input type="text" class="form-control" placeholder="Search" aria-describedby="basic-addon1" [(ngModel)]="searchDescription"
            span class="input-group-addon" id="basic-addon1"
                span class="glyphicon glyphicon-search"/span
            /span
        /div
    /div
    table class="table table-sm"
        thead
            tr
                th class="actions"span class="glyphicon glyphicon-menu-hamburger" aria-hidden="true"/span/th
                th class="description"Description/th
                th class="cost"Cost/th
                th class="status"Status/th
            /tr
        /thead
        tbody
            tr *ngFor="let order of orders | orderDescriptionFilter:searchDescription" class="order" scope="row" [ngClass]="{'complete': order.isComplete}"
                td class="actions"
                    span class="glyphicon glyphicon-pencil edit" (click)="editOrder(order)"/span
                    span class="glyphicon glyphicon-remove delete" (click)="deleteOrder(order)"/span/td
                td class="description"{{order.description}}/td
                td class="cost"{{order.costString}}/td
                td class="status"
                    span *ngIf="order.isIncomplete" class="label label-default"Incomplete/span
                    span *ngIf="order.isComplete" class="label label-success"Complete/span
                /td
            /tr

            !-- No orders are available --
            tr *ngIf="orders.length === 0"
                td colspan="4" class="no-orders-message"No orders currently available/td
            /tr
        /tbody
    /table
    div class="general-actions"
        button type="button" class="btn btn-primary" (click)="createOrder()"span class="glyphicon glyphicon-plus"/span Create/button
    /div
/div

The major difference in these two templates is the inclusion of the *ngFor directive. The *ngFor directive repeats the associated HTML element for each element in the associated expression. For now, we will focus on the contents of the expression before the pipe character (|): let order of orders. The let keyword simply defines a variable in TypeScript, but the expression order of orders means that we are iterating through the orders field of the component using this template and assigning each iterated value to order.

For example, if we had a list of strings called myStrings where the value of the list was ["string 1", "string 2"], the expression let currentString of myStrings would result in currentString having a value of string 1 for the first iteration and a value of string 2 for the second iteration. If this expression (of the form *ngFor="let currentString of myStrings") where associated with an HTML element, there would be two elements produced. Calling {{currentString}} inside the first element would result in string 1 and calling {{currentString}} inside the second element would result in string 2. For example, the following template:

ul
    li *ngFor="let currentString of myStrings"{{currentString}}/li
/ul

would produce the equivalent of the following HTML:

ul
    listring 1/li
    listring 2/li
/ul

In our case, we are iterating over a list of orders, so we can consequently call methods or access fields of the order variable in each iteration (for example, order.description). Just as with the save order template, we also include some CSS to make the template more visually appealing:

.search {
    float: right;
    margin-bottom: 15px;
}

.search .input-group {
    width: 250px;
}

.orders .actions {
    width: 70px;
    text-align: center;
}

.orders .actions .edit,
.orders .actions .delete {
    color: 
}

.orders .actions .edit {
    margin-right: 8px;
}

.orders .actions .edit:hover {
    color: 
    cursor: pointer;
}

.orders .actions .delete:hover {
    color: 
    cursor: pointer;
}

.orders .status {
    width: 70px;
    text-align: center;
}

.orders .cost {
    width: 100px;
}

.general-actions {
    border-top: 1px solid 
    padding-top: 12px;
    text-align: right;
}

.no-orders-message {
    text-align: center;
}

With our templates implemented, we are ready to move onto implementing our four components.

Creating the Components

Since our OrdersComponent (that displays the list of existing orders) has the fewest number of interactions and intricacies, we will start with implementing this component:

@Component({
    templateUrl: './orders.component.html',
    styleUrls: ['./orders.component.css'],
    selector: 'orders'
})
export class OrdersComponent implements OnInit {

    orders: Order[] = [];
    searchDescription: string;

    constructor(private orderService: OrderService, private router: Router) { }

    ngOnInit() {
        this.retrieveOrders();
    }

    private retrieveOrders() {
        this.orderService.getOrders()
            .then(orders = this.orders = orders);
    }

    public deleteOrder(order: Order) {
        this.orderService.deleteOrder(order)
            .then(() = this.retrieveOrders());
    }

    public editOrder(order: Order) {
        this.router.navigate(['/orders', order.id, 'edit']);
    }

    public createOrder() {
        this.router.navigate(['/orders/create']);
    }
}

The @Component decorator denotes that our class is a component that can be used by the Angular framework, which allows it to be used to serve up visual components on the UI. The @Component decorator also allows the class to be used for injection, and thus have other objects injected. In this particular case, we are injecting the OrderService and Router (provided by the Angular framework) through constructor injector; we will later use the OrderService to retrieve the list of all orders, while the Router object will be used for navigating to other components. We will revisit both shortly.

The templateUrl parameter specifies the location of the template file, which we previously created (in this case, we have placed the template file in the same directory as the OrdersComponent). The styleUrls similarly specifies the locations of the stylesheet files that correspond to the component. Note that this parameter value is a list and even if only one value is supplied, the list brackets must be included. The final parameter, selector, allows the component to be referenced in an HTML template file. For the purpose of our components (since we will not use them within template files), it suffices to supply unique selector values for each of our components. For more information, see the Angular Component documentation.

Our component also implements the OnInit interface, which notifies the Angular framework to call the ngOnInit method when Angular has completed initialization, including the injection of all injectable objects and the creation of all required components. This method provides us with an optimal location to initialize the list of orders in our OrdersComponent. Although it might be tempting to initialize this list in the constructor, we are not guaranteed the component and service are initialized in the constructor, and therefore, performing calls on the injected service may cause difficult-to-track errors.

As established in Part 1 of this series, we are required to refresh the list of orders when a deletion occurs. For that reason, when we complete the deletion of an order through the OrderService, we retrieve the list of orders from the OrderService again. If we started to see a performance issue with this retrieval (i.e. retrieving the list becomes a temporally expensive operation due to the number orders), we can simply remove the order from our local list of orders. Such a performance optimization should only be made when we start to see a severe performance issue or the implemented operation does not satisfy our performance requirements. For the purpose of demonstration, we will simply retrieve the list again from the OrderService

The remaining two methods, editOrder and createOrder, simply navigate to other components using the injected Router object. At this point, we will leave this explanation incomplete until we explore the component routing protocol in later sections.

With the OrdersComponent complete, we can now proceed to implement the remaining components. Since the SaveOrderComponent is required by the EditOrderComponent and CreateOrderComponnent, we will start there:

export abstract class SaveOrderComponent implements OnInit {

    protected order: Order;

    public constructor(private router: Router) {}

    protected abstract loadOrder(): PromiseOrder;
    protected abstract getTitle(): string;
    protected abstract onSave();
    protected abstract getSaveButtonText(): string;

    ngOnInit() {
        this.loadOrder().then(order = this.order = order);
    }

    public isDescriptionValid(): boolean {
        return this.order !== undefined  
            this.order.description !== undefined  
            this.order.description !== "";
    }

    public isCostValid(): boolean {
        return this.order !== undefined 
            this.order.cost !== undefined 
            this.order.cost = 0.0;
    }

    public canSubmit(): boolean {
        return this.isDescriptionValid()  this.isCostValid();
    }

    public onCancel() {
        this.navigateToOrders();
    }

    protected navigateToOrders() {
        this.router.navigate(['/orders']);
    }
}

Note that we do not adorn this component with the @Component decorator, as we do not intend for it to be displayed on the UI, but rather, be extended by the EditOrderComponent and CreateOrderComponnent classes. The remainder of the logic is divided between error checking methods (such as isDescriptionValid(), isCostValid(), canSubmit() methods) and button-click logic (onCancel() method). The latter logic simply navigates to the list of order components; we will revisit this navigation logic when we discuss the routing logic for components.

With the Abstract Base Class (ABC) complete, we can now move onto implementing our CreateOrderComponent:

@Component({
    templateUrl: './save-order.component.html',
    styleUrls: ['./save-order.component.css'],
    selector: 'create-order'
})
export class CreateOrderComponent extends SaveOrderComponent {

    public constructor(
        private orderService: OrderService,
        router: Router) {
            super(router);
    }

    protected loadOrder(): PromiseOrder {
        const order = new Order();
        order.cost = 0.0;
        return Promise.resolve(order);
    }

    protected getTitle(): string {
        return "Create Order";
    }

    protected onSave() {
        this.orderService.createOrder(this.order).then(order = this.navigateToOrders());
    }

    protected getSaveButtonText(): string {
        return 'Create';
    }
}

This component class is fairly simple due in large part to the ABC we previously created. This class simply provides implementations for the abstract methods in the ABC, supporting the logic specifically for creating an order. Note that we create a blank Order (and initialize its cost to 0.0 so that it is not undefined) that will eventually be filled in by the user and then sent to the OrderService to be used in creating a new order through the web service.

Likewise, the implementation of EditOrderComponent is very similar, but it does have one major difference that must be discussed further:

@Component({
    templateUrl: './save-order.component.html',
    styleUrls: ['./save-order.component.css'],
    selector: 'edit-order'
})
export class EditOrderComponent extends SaveOrderComponent {

    public constructor(
        private orderService: OrderService, 
        private route: ActivatedRoute,
        router: Router) {
            super(router);
    }

    protected loadOrder(): PromiseOrder {
        return new PromiseOrder(resolver = {
            this.route.paramMap
                .switchMap(params = this.orderService.getOrder(+params.get('id')))
                .subscribe(order = resolver(order))
        });
    }

    protected onSave() {
        this.orderService.updateOrder(this.order).then(order = this.navigateToOrders());
    }

    protected getTitle(): string {
        return "Edit Order";
    }

    protected getSaveButtonText() {
        return 'Update';
    }
}

Since this component is responsible for editing an existing order, we must know the ID of the order being edited. This ID will be supplied in, and later extracted from, the URL associated with the component (we will revisit this once we discuss routing). Once we know the ID, we can load the existing order and the user can make edits to this order. To understand this logic, we have to look further into the loadOrder() method:

protected loadOrder(): PromiseOrder {
    return new PromiseOrder(resolver = {
        this.route.paramMap
            .switchMap(params = this.orderService.getOrder(+params.get('id')))
            .subscribe(order = resolver(order))
    });
}

Since the retrieval of the existing order is performed asynchronously, we will have our method return a promise. In this particular case, we are using the Promise constructor that includes a resolver function that can be called to complete the promise when the desired value is obtained. Note that this resolver function is called on line 5 and returns the Order that was retrieved. This Order is then passed to any callback function that is registered using the then() method.

To extract the ID from the URL, we use the injected ActivatedRoute and extract the ID from its associated parameters. This ActivatedRoute object represents the URL components that are currently active and provide helper methods and structures that allow us to extract useful information about the current URL. For example, when we later associated our EditOrderComponent with the URL /order/{id}/edit, we can use the ActivatedRoute to extract the ID (i.e. if the URL is /order/1/edit, we can find that the ID is 1). The parameters for our route is stored in the paramMap and can be extracted using +params.get('id').

Before moving onto routing our application, we must address one last topic pertaining to our components: Our search filter.

Implementing the Search Filter

When we previously discussed the orders list template, we deferred an explanation of the second portion of the *ngFor directive for simplicity. If we revisit this snippet, we see that we have included a pipe character after the list of orders with the name of a search filter:

*ngFor="let order of orders | orderDescriptionFilter:searchDescription"

The orderDescriptionFilter specifies the name of a pipe that we can use to filter the orders presented to the user, while the searchDescription represents the field that is tied to the current value of the search input element (as the user types into this search element, the value of the field is updated to match the input value; this binding was established using the [(ngModel)] directive). To implement this pipe, we simply create a new class and adorn it with the @Pipe decorator:

@Pipe({
    name: 'orderDescriptionFilter',
    pure: false
})
export class OrderDescriptionPipe implements PipeTransform {

    transform(orders: Order[], description: string): any {

        if (description === "" || description === undefined) {
            return orders;
        }
        else {
            return orders.filter(order = order.description.toLowerCase().startsWith(description.toLowerCase()));
        }
    }
}

The name parameter within specifies the unique name of the pipe (the same name that we used to reference the pipe in our template file), while the pure parameter specifies whether the pipe is pure or impure. For our purposes, we will specify the pipe as impure (pipe purity is an involved topic; for more information, see the official Angular Pipe documentation).

The body of the pipe contains a single method, which consumes a list of Order resource objects, along with the search string (the description to match against) and produces a filtered list of Order resource objects whose descriptions start with the search string. In order to check the quality of our description values, we force both the match string (description argument) and the description of the Order resource to lower case, which results in a case-insensitive starts-with filtering. If the description string is undefined or blank, we simply return the unfiltered list of Order objects that was passed into the method.

If we return to our template file, we can see that the declaration of our filter matches the signature of our transform method, where the list of Order resources is fed to the transform method as the first argument and the searchDescription field is fed in as the second argument. We will see later that as the user types into the search field, the value of the searchDescription field will change, which will cause the filter to re-evaluate the list of Order resources and only return Order resource objects whose description starts with the search string (case insensitive).

Wiring the Components Together

While we now have all our components implemented, we still need to inform the Angular framework that our components exist. To do so, we need to register them within the @NgModule decorator. Although we ignored a large portion of the @NgModule decorator earlier in this series, it is time to revisit this decorator:

@NgModule({
    declarations: [
        AppComponent,
        OrdersComponent,
        EditOrderComponent,
        CreateOrderComponent,
        OrderDescriptionPipe
    ],
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        AppRoutingModule
    ],
    providers: [
        OrderService
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}

If we focus on the declarations section of the decorator, we see that we have included all of the components we previously created, along with the AppComponent, which constitutes the root component of our web application. The purpose of this component is very simple: declare the template file that should be used to display the application:

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {}

We also include some simple CSS styles to change the appearance of the application component template (we will define this template in the following section):

.center-container {
    margin: auto;
    width: 800px;
    margin-top: 200px;
}

.title {
    margin-bottom: 65px;
}

Apart from declaring our components in the declarations section, we also must include the BrowserModule and FormsModule in the imports section of the decorator (for more information on the FormsModule, see the Tour of Heroes tutorial). With the correct wiring of our application, we can now route our components and implement navigation between our components.

Routing the Components

The final task we must complete before using our web application is to establish the routes that Angular will use to get from one component to another. The concept of routes can be difficult to understand in the abstract, so we will start with the concrete configuration and work our way back to a more abstract understanding:

const routes: Routes = [
    { path: '', redirectTo: '/orders', pathMatch: 'full' },
    { path: 'orders',  component: OrdersComponent },
    { path: 'orders/:id/edit', component: EditOrderComponent },
    { path: 'orders/create', component: CreateOrderComponent }
];

@NgModule({
    imports: [ RouterModule.forRoot(routes) ],
    exports: [ RouterModule ]
})
export class AppRoutingModule {}

The main purpose of our routes is to associate specific URLs with specific components. For example, in line 3 of the configuration above, we map the URL order to our OrdersComponent; thus, if the URL of our web application is http://localhost:4200, then navigating to http://localhost:4200/orders in the browser will display our OrderComponent (more precisely, the template associated with the OrdersComponent).

The first route we establish (line 2) stands apart from the others since it acts as the default URL and redirects to the /orders URL. Thus, if a user goes to http://localhost:4200 in the browser, he or she will immediately be redirected to http://localhost:4200/orders (for more information on the pathMatch parameter, see the Tour of Heroes tutorial).

Also of special note is line 4, which includes a URL parameter, :id, in its path declaration. This parameter, denoted by the preceding colon, allows for a parameter in the URL to be mapped to a key. For example, going to the http://localhost:4200/orders/3/edit displays the EditOrderComponent and maps the value 3 to the key id (this is how we were able to extract the ID of the edited Order resource through the ActivatedRoute in our implementation of the EditOrderComponent).

The last remaining task is to provide a location for our routes to be displayed. To do this, we add the router-outlet/router-outlet tags to the main app.component.html file:

div class="center-container"
    h1 class="title"Order Management System/h1
    router-outlet/router-outlet
/div

With the outlet established, we now have a location for our routed components to be displayed and are ready to run our web application. We will explore this, as well as some testing and inspection strategies for web applications, in the next and final article in this series.

Leave a Reply

Your email address will not be published. Required fields are marked *

*
*

cover letter