651.288.7000 info@intertech.com

Note: This post is part of an eleven-part series on Angular development. You can find the series overview here.

Overview

Overall, there are three main tools to use in Angular development, Services, Components, and Directives.  By far the most used throughout an application is the Component – however that does not mean all Components are created equal.  In the same manner that the class is the basis of all object oriented applications, common design patterns set guidelines and expectations for different types of classes: models, services, repositories, factories, etc.  Each type has a place and an expectation of how it should function in order to keep the structure of an application predictable, maintainable, and testable.  The same mentality can be applied to the Angular Component.  This article focuses on two main types of components, Smart and Dumb components, and how they can be used to build a more scalable, testable, and maintainable application.

A common mistake new Angular developers make is to treat a Component as a page in its entirety.  The Angular router directly maps routes to pages, therefore it is a simple jump mentally to make that Component responsible for all of the content within that page.  While this is generally a good place to start when developing, careful attention should be paid to the Component to continually refactor and break it into smaller chunks.  This helps maintain a key Angular design principal: Components should have a single responsibility.  Due to the way the Angular build process works, there is zero overhead during runtime with creating an additional component.  This can be used to our advantage to decouple the application components into smaller, more easily maintained and tested parts.  Enter the smart and dumb component structure. 

What Are They?

So what are smart and dumb components?  You may hear many different names to refer to them (application, container, or controller vs presentation or pure components), but the idea is the same.  You want components that are responsible for loading and manipulating data to not be intermingled with the presentation and display of that data. The general guideline is that smart components act as your controller, binding service layer data to your dumb components through Inputs and Outputs.  Smart components have very little display logic, generally only rough framework or items tightly linked to the service layer (e.g. display a spinner while loading, then replace with a data table once data is loaded).  The dumb components are responsible for the layout and interaction with the data (e.g. the data table in the previous example).

Example

To follow along with the articles there a two repositories created: a starting point and final solution.

Let’s take an example large component and break it down into these parts.  Say we have a shopping site with a search page and a list of products matching the search.  We also want to pre-populate the search using query params to allow sharing of search results via URL.  The initial component may look something like the code below.  Note I am using the Bootstrap library to provide some standard styling and display components.   

search.component.ts
@Component({
    selector: 'app-search',
    template: `
        <div class="row">
            <div class="col">
                <form [formGroup]="searchForm" [ngClass]="{'was-validated': attemptSearch}">
                    <div class="form-group">
                        <label for="searchTerm">Search</label>
                        <input type="text" class="form-control" id="searchTerm" formControlName="searchTerm" required />
                    </div>

                    <button class="btn btn-primary" type="submit" (click)="search()">Search</button>
                </form>
            </div>
            <div class="col">
                <ul class="list-group" *ngIf="items">
                    <li class="list-group-item" *ngFor="let item of items">
                        <h2>{{item.name}}</h2>
                        <p>{{item.description}}</p>
                    </li>
                </ul>
                <div *ngIf="searching" class="spinner-border"></div>
            </div>
        </div>`
})
export class SearchComponent implements OnInit {
    public items: ItemInterface[];
    public searching = false;
    public searchForm: FormGroup;
    public attemptSearch: boolean;

    constructor(private formBuilder: FormBuilder, private searchService: SearchService, private route: ActivatedRoute) { }

    ngOnInit() {
        this.attemptSearch = false;
        this.route.queryParams.subscribe(params => {
            this.buildForm(params as SearchInterface);
        });
    }

    private buildForm(init: SearchInterface) {
        this.searchForm = this.formBuilder.group({
            searchTerm: [ init.searchTerm, Validators.required ]
        });
    }

    search() {
        this.attemptSearch = true;
        if (this.searchForm.valid) {
            this.searching = true;
            this.searchService.search(this.searchForm.value).subscribe(data => {
                this.items = data;
                this.searching = false;
            });
        }
    }
}

As you can see this component can start to get fairly large.  This also hasn’t implemented much in the way of display, form validation, or other design elements.  We have a fair amount of coupling between our items, especially from running a search with the query parameters.  Let’s see how we can start to break this apart.

Items

First off, let’s start with the display of the items.  We can extract the display logic of the items into it’s own set of components:

search-results.component.ts
@Component({
    selector: 'app-search-results',
    template: `
            <ul class="list-group" *ngFor="let item of items">
                <li class="list-group-item">
                    <app-item [item]="item"></app-item>
                </li>
            </ul>`
})
export class SearchResultsComponent {
    @Input() items: ItemInterface[];
}
item.component.ts
@Component({
    selector: 'app-item',
    template: `
        <h2>{{item.name}}</h2>
        <p>{{item.description}}</p>`
})
export class ItemComponent {
    @Input() item: ItemInterface;
}

You can see here we have created two components, one responsible for the display of the individual item and one for handling a list of items.  Take a moment to think about how this design becomes more re-usable (after all, we’ll probably display a item summary somewhere else), and easier to test.  As the item grows and becomes more complex, it’s far simpler to test with an Input than mocking a server response to test corner cases.

This is also a good example to point out that the smart vs. dumb components don’t need to be a 1 to 1 relationship.  If we imagine a shopping cart feature added to this application, the SearchResultsComponent is the ideal place to add in the related service dependency.  It transforms to a smart wrapper component that is aware of the application as a whole and binds together interaction between features.  In this case, selection or actions within the item can be translated to service level actions such as adding or removing from a shopping cart.

Search

The next candidate for extraction is the search form.  Search forms can have many complicated internal interactions with field validations, conditional form fields, validation messages, etc.  But ultimately, forms are easily extractable into a black box with a default options input, and a form submission output.  Let’s see how we can accomplish this:

search-form.component.ts
@Component({
    selector: 'app-search-form',
    template: `
        <form [formGroup]="searchForm" [ngClass]="{'was-validated': attemptSearch}">
            <div class="form-group">
                <label for="searchTerm">Search</label>
                <input type="text" class="form-control" id="searchTerm" formControlName="searchTerm" required />
            </div>

            <button class="btn btn-primary" type="submit" (click)="search()">Search</button>
        </form>`
})
export class SearchFormComponent implements OnInit {
    @Input() default: SearchInterface;
    @Output() search: EventEmitter<SearchInterface> = new EventEmitter<SearchInterface>();
    public searchForm: FormGroup;
    public attemptSearch: boolean;

    constructor(private formBuilder: FormBuilder) { }

    ngOnInit() {
        this.attemptSearch = false;
        this.searchForm = this.formBuilder.group({
            searchTerm: [ this.default.searchTerm, Validators.required ]
        });
    }

    searchClick() {
        this.attemptSearch = true;
        if (this.searchForm.valid) {
            this.search.emit(this.searchForm.value);
        }
    }
}

Here we have pulled out the search form and provide an Input for default values, and an Output that emits an event containing the search criteria upon a valid search request.  Again, this becomes far more testable and maintainable.  It’s isolated from other functionality, so adding validation and other search fields becomes straightforward and contained within the functionality of the form.  We also get the ability to perform far easier and accurate testing through the Input and Output.  We can directly manipulate the form and test if the search event is emitted, no need for mocking interaction with other services or components.

Bringing It Together

Now we can update our initial component to bring all these components together:

search.component.ts
@Component({
    selector: 'app-search',
    template: `
        <div class="row">
            <div class="col">
                <app-search-form [default]="init" (search)="search($event)"></app-search-form>
            </div>

            <div class="col">
                <div *ngIf="items">
                    <app-search-results [items]="items"></app-search-results>
                <div>

                <div *ngIf="searching" class="spinner-border"></div>
            </div>
        </div>`
})
export class SearchComponent implements OnInit {
    public init: SearchInterface;
    public items: ItemInterface[];
    public searching = false;

    constructor(private searchService: SearchService, private route: ActivatedRoute) { }

    ngOnInit() {
        this.route.queryParams.subscribe(params => {
            this.init = params as SearchInterface;
        });
    }

    search(criteria: SearchInterface) {
        this.searching = true;
        this.searchService.search(criteria).subscribe(data => {
            this.items = data;
            this.searching = false;
        });
    }
}

As you can see, this component has maintained nearly all of the dependencies and is responsible for understanding where it is in the application.  This is acceptable because we will never re-use this component, it’s responsibility is to be a router endpoint and handle the logic at that specific location.  It also binds the service layer to the actions taken within the application, but has no concern for the implementation of that action.  It doesn’t matter if a selection is made with a button click, a drag-and-drop, gestures, or any other input method.  The dumb components are responsible for translating user actions into an interface that is easy to predict and understand – and also very importantly is already validated.  The smart component is responsible for reacting to those actions however they are made, and triggering actions throughout the application.

A Note on Layout

Also of note is how small the template has become for this component. This is a very common side effect of this design pattern.  The smart component is responsible for binding items together and is only responsible for very general layout.  In a Bootstrap-like grid system, this is generally the best location to put the grid elements for a page as a whole.  This allows for a much easier to see overall application layout.  Is the page a stacked design with the search on top of the results?  Is is a pinned side bar and scrolling results?  This is not to say the dumb components have no layout responsibility – they are responsible for how that component appears in many situations.  That is to say they should be a responsive design to appear in any of the previously mentioned situations.  By having the form component reactive to switch layout of the inputs, it’s easy for the smart component to place it in any of the  various locations and with different set widths (e.g. full with on top, or a narrow pinned side bar).  This separation of responsibility helps maintain a complete plug and play type of architecture and greatly reduces the effort of any redesign. 

Conclusion

There are many more individual situations that can arise, but this design mentality is simple to remember benchmark that will put you on a path to build far simpler to understand, test, and maintain applications.

Related Articles In This Series

Coming next week….Angular Development #7 – Provider Scopes – Component Level Services

 

About Intertech

Founded in 1991, Intertech delivers software development consulting to Fortune 500, Government, and Leading Technology institutions, along with real-world based corporate education services. Whether you are a company looking to partner with a team of technology leaders who provide solutions, mentor staff and add true business value, or a developer interested in working for a company that invests in its employees, we’d like to meet you. Learn more about us.