ben tedder : code things

Create a reusable Address component in Angular 4

We'll be creating a shared/reusable component in Angular 4 (works in Angular 2+ as well).

The reason I needed a shared Address component was because I was tired of repeating the Pick a state dropdown, and I always wanted my city and state to sit next to each other using flex. Seemed like a good excuse to try to create a shared component.

One other thing to consider was that I needed this component in a variety of forms:

  • Registration
  • Profile Address change
  • Admin user management

Creating the component

If you're using the Angular CLI the easiest thing is just to type ng g component address. This creates the following four files:

  • address.component.html
  • address.component.scss
  • address.component.spec.ts
  • address.component.ts

Let's start with the Typescript file, as there is very little to add to this file. We're going to add three items to the AddressComponent class before the constructor:

@Input('group')

public addressForm: FormGroup;

states = ['AL','AK','AS','AZ','AR','CA','CO','CT','DE','DC','FM',
          'FL','GA','GU','HI','ID','IL','IN','IA','KS','KY','LA',
          'ME','MH','MD','MA','MI','MN','MS','MO','MT','NE','NV',
          'NH','NJ','NM','NY','NC','ND','MP','OH','OK','OR','PW',
          'PA','PR','RI','SC','SD','TN','TX','UT','VT','VI','VA',
          'WA','WV','WI','WY','AE','AA','AP'];

In order for Typescript to compile, don't forget to change the import statements at the top of the file:

import { Component, OnInit, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';

Creating the template

Now that we have our component setup, let's create a template to accept the FormGroup instance and the list of states.

<div [formGroup]="addressForm" class="address">
  <!-- Form inputs will go here -->
</div>

The reason I've chosen a div here instead of a form element is because this element will only ever live inside a form already. If you want to get to know more about the FormGroup in Angular I'd highly suggest reading the Official Angular docs on FormGroup. But let's move on to some form inputs.

<div class="form-group">
  <label>Address</label>
  <input type="text" formControlName="address" />
</div>
<div class="form-group city-state">
  <div class="city">
    <label>City</label>
    <input type="text" formControlName="city" />
  </div>
  <div class="state">
    <label>State</label>
    <select formControlName="state">
      <option *ngFor="let state of states" [ngValue]="state">{{state}}</option>
    </select>
  </div>
  <div class="form-group zip">
    <label>Zip</label>
    <input type="text" formControlName="zip" />
  </div>
</div>

There's nothing crazy about that html, but take note of a couple of things:

  • use formControlName to reference variables that will be stored in the FormGroup.
  • note the *ngFor that iterates through the list of states
  • city and state are both under the same .form-group div for styling later

Style the component

There's not much to styling this component, but I'll just use a tiny bit of flex to make sure that city and state sit next to each other and look nice. I also use a base class for a standard 8px grid. I also set the zip input to be a set width, since that will have a max of either 5 or 10 (with a hyphen) digits.

$base: 8px;

.address {
  margin-bottom: $base * 2;

  .form-group {
    margin-bottom: $base * 2;
  }

  .city-state {
    display: flex;

    .city {
      flex: 1;
      margin-right: $base * 2;
    }
    .state {
      flex: 0 0 auto;
      margin-right: $base * 2;
    }
    .zip {
      flex: 0 0 100px;
    }
  }
}

Share the component

To use this in both the internal (admin) and external (registration) portions of my app I have created a shared module. I think I've covered this in a previous post, but just in case, here's the whole file (since I only have 1 shared component at the moment):

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule, Http, XHRBackend, RequestOptions } from '@angular/http';

import { AddressComponent } from './address/address.component';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    HttpModule,
  ],
  declarations: [
    AddressComponent,
  ],
  exports: [ AddressComponent, CommonModule, FormsModule, HttpModule, ReactiveFormsModule ]
})
export class SharedModule { }

Note that FormsModule and ReactiveFormsModule are necessary for everything to work.

Inject the component

In my external.module, which handles all the things on the outside of my app, here's what I have (I've stripped irrelevant things like routing for the code sample):

import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { RegisterComponent } from './register/register.component';

@NgModule({
  imports: [ SharedModule ],
  declarations: [ RegisterComponent ]
})
export class ExternalModule {}

So now my RegisterComponent has access to anything the ShareModule has exported (ie, the AddressComponent).

Using the shared component

Now in register.component.ts I need to create a new form. I'm going to be using the FormGroup and the FormBuilder. I'll post the whole code for now and explain after:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';

@Component({
  selector: 'myapp-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {

  public registerForm: FormGroup;

  constructor(private fb: FormBuilder) {
  }

  ngOnInit() {
    this.createForm();
  }

  createForm() {
    this.registerForm = this.fb.group({
      first_name: '',
      last_name: '',
      email: '',
      password: '',
      passwordConfirmation: '',
      address: this.fb.group({
        address: '',
        city: '',
        state: '',
        zip: '',
      })
    })
  }

  onSubmit() {
    // do something with the values
    console.log(this.registerForm.value);
  }
}

So, right away you should notice that I've imported FormGroup and FormBuilder. These are great because they make it really easy to construct forms. I'm registering registerForm as a variable on the class to be assigned later.

Down in the createForm method you can see that I'm assigning a new FormGroup using the FormBuilder. You can also see that the address portion is its own FormGroup. This is so we can pass it straight into the Address Component, which is expecting a FormGroup as an @Input.

Setup the registration form HTML

<div class="register">
  <h3>Register</h3>
  <form (ngSubmit)="onSubmit()" [formGroup]="registerForm">
    <div class="form-group">
      <label for="email">Email</label>
      <input type="text" formControlName="email" />
    </div>
    <div class="form-group">
      <label for="first_name">First Name</label>
      <input type="text" formControlName="first_name" />
    </div>
    <div class="form-group">
      <label for="last_name">Last Name</label>
      <input type="text" formControlName="last_name" />
    </div>

    <myapp-address [group]="registerForm.controls.address"></myapp-address>

    <div class="form-group">
      <label for="password">Password</label>
      <input type="password" formControlName="password" />
    </div>
    <div class="form-group">
      <label for="passwordConfirmation">Confirm Password</label>
      <input type="password" formControlName="passwordConfirmation" />
    </div>
    <button type="submit" [disabled]="!registerForm.valid">Register</button>
  </form>
</div>

Hopefully that HTML is pretty self-explanatory. A few things to note:

  • Using formControlName again to add in the fields the FormGroup expects
  • Passing registerForm.controls.address to the myapp-address component's group input attribute.
  • Setting the submit button to disabled when the form is not valid.

So go ahead and spin up the form and you should see a city and state next to each other, a zip field that is a set width, and when you submit the form you should see the entire form's value logged to the console.

Wrapping up

There are a few things about this I'm curious about.

I'm not really clear on how the binding is working between the registerForm and the address FormGroup I'm passing into the shared component. It feels an awful lot like 2-way binding, which I thought Angular was moving away from.

I have not added any validators in this tutorial, but it seems quite straight forward when you read the Angular docs on the subject.

This approach seems maybe a little overkill (or maybe I didn't include enough fields), but I mostly didn't want to repeat that states array in all of the places I had user forms. I could probably move more of the user info into the component, but I like how this component functions for now.

You could also add city/state/zip validation to make it even easier for your users.

Hope this helps your project. I'm learning Angular 4 myself and posting stuff here as I go, leave comments if something looks off to you.