Angular/Rails CRUD Tutorial Part 1

by Jason Swett,

The idea

I’ve decided to write a very long, comprehensive, multi-part tutorial. I came up with a fictional app called Cadence, a music lesson scheduling app. I wanted to think of something that would be a fitting use case for a single-page application, and I think a calendar-based application would make sense.

The tutorial starts with Angular by itself, no Rails.

The initial version

The initial version of this application will not be very impressive. You’ll be able to create “appointments”, each of which just consists of a date and client name. When you click Save, the appointment details will be added to a list of appointments on the screen. That’s about it. But as we’ll see, just doing that much is involved enough that we wouldn’t really want to bite off more than that for starters.

Since this initial version of the app is just Angular without Rails, we’ll be using localStorage as the data store.

We won’t even worry about tests yet in this initial version. We’ll first write a version without tests, because we already have enough to deal with without bringing tests into the picture, and then we’ll make a second pass with tests.

What you’ll learn

  • How to create an Angular project using Angular CLI
  • How to build components (and nested components)
  • How to use *ngFor to display lists
  • How to use services for inter-component interaction
  • How to use localStorage with Angular

We’re going to initialize this project using Angular CLI. We’re going to install Angular CLI using NPM, so if you don’t have NPM installed yet, you’ll need to install it before proceeding. (If you’re not familiar, NPM is a package manager for JavaScript. It’s kind of analogous to RubyGems for Ruby.)

$ npm install -g @angular/cli

The Angular CLI executable is ng. We can initialize our project with the ng new command, like this:

$ ng new cadence --prefix cadence

The format of the ng new command is ng new <your project name>. In this case we’re calling our project cadence.

Let’s cd into the cadence directory since everything we’ll be doing from this point on will happen inside this directory.

$ cd cadence

Angular CLI can also serve our new Angular application with ng serve.

$ ng serve

After the server spins up, you can visit localhost:4200 and you should see a screen that looks like this:

Creating some components

In a moment we’ll be creating some components. What’s a component? I’ll try to give the simplest answer possible.

Imagine you have an application that has a datepicker thingy that you use in multiple places. Rather than pasting in the full datepicker code wherever you want the datepicker or referring to some include file everywhere you want the datepicker, you could just do something like this:

<my-datepicker></my-datepicker>

If you have an Angular component whose selector matches my-datepicker, Angular will go find the template for that component as well as the TypeScript code that defines the behavior for that component. To me this seems like a pretty natural and convenient way to invoke some behavior.

The Angular Style Guide recommends that we give our selectors application-specific prefixes. The idea is to avoid naming conflicts. So for our app called Cadence, we could put a cadence- prefix on each of our components. For example, we might have cadence-calendar or cadence-datepicker.

The people who built Angular CLI were apparently aware of this style guide recommendation because they built this kind of prefixing into Angular CLI. Angular CLI give us the ability to generate components via the ng generate component command. Each time we do this, Angular CLI will automatically give the component a cadence- prefix.

We can use the ng generate component command to generate our first component. Let’s generate a calendar component. As a reminder, this application we’re creating is a music lesson scheduling app. The calendar component will give us a place to put all our appointments.

$ ng generate component calendar

Now that the component exists let’s get it to actually show up. The selector for the CalendarComponent is cadence-calendar. (If you look at src/app/calendar/calendar.component.ts, you can see the part near the top of the file where the selector for that component is specified.)

If we put
<cadence-calendar></cadence-calendar>
in any template file, it will show us CalendarComponent. Let’s put this selector in the AppComponent template now.

<!-- src/app/app.component.html -->

<cadence-calendar></cadence-calendar>

Now that we’ve made this change we should see a screen like this:

Adding Twitter Bootstrap

I like to use Twitter Bootstrap for most of the apps I create. Let’s just get adding Bootstrap out of the way now so that everything we do from this point on will look somewhat nice.

The first step is to install the Bootstrap NPM package. (The --save means we want NPM to add the bootstrap@next package to package.json so that Bootstrap is an explicit dependency of this project and will get installed anytime anyone does an npm install.)

$ npm install --save bootstrap@next

Next we have to add a few lines to .angular-cli.json. Bootstrap comes with some styles and JavaScripts and we have to tell Angular CLI about these things in order for Bootstrap to get wired up properly.

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "cadence"
  },
  "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.ts",
      "polyfills": "polyfills.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.app.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "cadence",
      "styles": [
        "../node_modules/bootstrap/dist/css/bootstrap.css",
        "styles.css"
      ],
      "scripts": [
        "../node_modules/jquery/dist/jquery.js",
        "../node_modules/tether/dist/js/tether.js",
        "../node_modules/bootstrap/dist/js/bootstrap.js"
      ],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
  ],
  "e2e": {
    "protractor": {
      "config": "./protractor.conf.js"
    }
  },
  "lint": [
    {
      "project": "src/tsconfig.app.json"
    },
    {
      "project": "src/tsconfig.spec.json"
    },
    {
      "project": "e2e/tsconfig.e2e.json"
    }
  ],
  "test": {
    "karma": {
      "config": "./karma.conf.js"
    }
  },
  "defaults": {
    "styleExt": "css",
    "component": {}
  }
}

After the very important step of restarting your server, you should see a screen like the one below. (To restart the server, kill the ng serve process and start it again.)

It’s also a good idea to put all our application’s content inside a container Let’s also add a header.

<!-- src/app/app.component.html -->

<div class="container">
  <h1>Cadence</h1>

  <cadence-calendar></cadence-calendar>
</div>

Adding NewAppointmentComponent

We haven’t yet put anything into CalendarComponent. The first thing we’ll actually put into it is another component. Angular recommends a component tree application structure where child components are nested inside of parent components. I thought it would make sense to make CalendarComponent contain a list of appointments as well as a component for creating new appointments. We’ll create that component for creating new appointments now.

First, let’s cd into the directory where CalendarComponent lives.

$ cd src/app/calendar

Now we can create NewAppointmentComponent itself.

$ ng generate component new-appointment

Finally, we’ll put the selector for NewAppointmentComponent in the component for CalendarComponent. We should see NewAppointmentComponent‘s template content (presently just “new-appointment works!”) show up on the page.

<!-- src/app/calendar/calendar.component.html -->

<p>
  calendar works!
</p>

<cadence-new-appointment></cadence-new-appointment>

Adding some form controls to the appointment form

Like I said near the beginning of this post, our appointments will only have two attributes for starters: date and client name. Let’s create two text inputs in NewAppointmentComponent‘s template, one for each of these attributes. Right now we’ll just accept arbitrary text and not worry about whether the date provided by the user is a valid date.

<!-- src/app/calendar/new-appointment/new-appointment.component.html -->

<div class="form-group">
  <label for="date">Date</label>
  <input type="text" class="form-control" name="date" id="date">
</div>

<div class="form-group">
  <label for="clientName">Client name</label>
  <input type="text" class="form-control" name="clientName" id="clientName">
</div>

<input type="submit" class="btn btn-primary" value="Save">

The form should look like this:

Make sure you’re in the calendar directory.

If you click Save, nothing happens, of course, We haven’t hooked up the Save button to anything.

The first step in getting the Save button to actually do something is to bind the form we just created to an Appointment object. We’ll bind the form to an Appointment, then when the user clicks Save, we’ll save that Appointment object (or, more precisely, we’ll save a representation of that Appointment object.)

If you’re wondering what this Appointment object is I’m talking about, it’s something that doesn’t exist yet. Let’s use Angular CLI to help us generate an Appointment class.

(We’re still in src/app/calendar, by the way.)

$ ng generate class appointment

Then we’ll open up the Appointment class definition and add date and clientName properties. Why do we have public date?: string instead of public date: string? The question mark means those arguments are optional. So when we create an empty Appointment instance, we can instantiate it by doing new Appointment() instead of having to do new Appointment('', ''), which would feel kind of dumb.

// src/app/calendar/appointment.ts

export class Appointment {
  constructor(public date?: string,
              public clientName?: string) {}
}

After we define the Appointment class we’ll pull it into NewAppointmentComponent.

// src/app/calendar/new-appointment/new-appointment.component.ts

import { Component, OnInit } from '@angular/core';
import { Appointment } from '../appointment';

@Component({
  selector: 'cadence-new-appointment',
  templateUrl: './new-appointment.component.html',
  styleUrls: ['./new-appointment.component.css']
})
export class NewAppointmentComponent implements OnInit {
  model = new Appointment();

  constructor() { }

  ngOnInit() {
  }

  save() {
    console.log(this.model);
  }
}

We’ve created a model property on NewAppointmentComponent and now we’ll bind model.date to the Date input and model.clientName to the Client name input. If you’re interested in learning more about data binding I’d recommend this article.

We’re also adding a form and putting an event on the form. When the form gets submitted, the save() function on NewAppointmentComponent gets invoked. For now we’re just logging the model. I don’t want to try to do anything fancier than that yet because I want to be sure that this step works first.

<!-- src/app/calendar/new-appointment/new-appointment.component.html -->

<form (submit)="save()">

  <div class="form-group">
    <label for="date">Date</label>
    <input type="text"
           class="form-control"
           name="date"
           id="date"
           [(ngModel)]="model.date">
  </div>

  <div class="form-group">
    <label for="clientName">Client name</label>
    <input type="text"
           class="form-control"
           name="clientName"
           id="clientName"
           [(ngModel)]="model.clientName">
  </div>

  <input type="submit" class="btn btn-primary" value="Save">
</form>

If we try to reload the page now we’ll get an error in the console that says, “Can’t bind to ‘ngModel’ since it isn’t a known property of ‘input’.” This is because ngModel needs FormsModule in order to work but we haven’t imported FormsModule.

// src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { CalendarComponent } from './calendar/calendar.component';
import { NewAppointmentComponent } from './calendar/new-appointment/new-appointment.component';

@NgModule({
  declarations: [
    AppComponent,
    CalendarComponent,
    NewAppointmentComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

You can see in the screenshot below that I’ve filled out the form and clicked Save. In the console appears the Appointment object that corresponds to the values I entered in the form.

Saving data to localStorage

If you’ve never used localStorage before, don’t worry. It’s pretty simple to use and you don’t have to do anything special to use localStorage with Angular.

Below we’re creating array called appointments which, for now, only has one element: the Appointment object we’re saving from the form. Then we’re saving that array to localStorage.

// src/app/calendar/new-appointment/new-appointment.component.ts

import { Component, OnInit } from '@angular/core';
import { Appointment } from '../appointment';

@Component({
  selector: 'cadence-new-appointment',
  templateUrl: './new-appointment.component.html',
  styleUrls: ['./new-appointment.component.css']
})
export class NewAppointmentComponent implements OnInit {
  model = new Appointment();

  constructor() { }

  ngOnInit() {
  }

  save() {
    let appointments = [this.model];
    localStorage.setItem('appointments', JSON.stringify(appointments));
  }
}

If you save an appointment now, then open up the Application tab in the Chrome developer toolbar and go to Local Storage, you should be be able to see the appointment you just saved.

Showing the saved appointments in CalendarComponent

To get the saved appointments out of localStorage and into CalendarComponent, we can just do JSON.parse(localStorage.getItem('appointments')).

// src/app/calendar/calendar.component.ts

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

@Component({
  selector: 'cadence-calendar',
  templateUrl: './calendar.component.html',
  styleUrls: ['./calendar.component.css']
})
export class CalendarComponent implements OnInit {
  appointments = [];

  constructor() { }

  ngOnInit() {
    this.appointments = JSON.parse(localStorage.getItem('appointments'));
  }

}

We can show the appointments on the page using an *ngFor loop.

<!-- src/app/calendar/calendar.component.html -->

<div *ngFor="let appointment of appointments">
  {{ appointment.date }}
  {{ appointment.clientName }}
</div>

<cadence-new-appointment></cadence-new-appointment>

Here’s what the page looks like with the appointments added. Not very nice-looking yet but we’ll improve the appearance shortly.

Preserving previously-saved appointments

Until now, NewAppointmentComponent saves an array of appointments that actually just consists of one element, the appointment that’s being saved right now. What it should really do is save an array of appointments consisting of all the existing appointments plus the appointment that’s being saved now. Let’s fix this.

// src/app/calendar/new-appointment/new-appointment.component.ts

import { Component, OnInit } from '@angular/core';
import { Appointment } from '../appointment';

@Component({
  selector: 'cadence-new-appointment',
  templateUrl: './new-appointment.component.html',
  styleUrls: ['./new-appointment.component.css']
})
export class NewAppointmentComponent implements OnInit {
  model = new Appointment();

  constructor() { }

  ngOnInit() {
  }

  save() {
    let appointments = JSON.parse(localStorage.getItem('appointments'));
    appointments.push(this.model);

    localStorage.setItem('appointments', JSON.stringify(appointments));
  }
}

Refactoring the appointment code into a service

The appropriate place to put business logic in Angular is in services. Components shouldn’t do very much. Components should only call services that do things. If there’s some saving of records going on, the component shouldn’t be aware of how that saving happens.

Let’s move our appointment-saving and appointment-retrieving code out of the components and into a service.

We’ll use Angular CLI to generate a service. Before you do so, make sure you’re in the calendar directory.

$ ng generate service appointment

This command will generate a service called AppointmentService. Let’s create a function called save() which accepts an appointment. In the body of our function goes the code we’ve been using inside NewAppointmentComponent so far to save an appointment.

// src/app/calendar/appointment.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class AppointmentService {

  constructor() { }

  save(appointment) {
    let appointments = JSON.parse(localStorage.getItem('appointments'));
    appointments.push(appointment);

    localStorage.setItem('appointments', JSON.stringify(appointments));
  }
}

Then NewAppointmentComponent can just call the service’s save() function.

// src/app/calendar/new-appointment/new-appointment.component.ts

import { Component, OnInit } from '@angular/core';
import { Appointment } from '../appointment';
import { AppointmentService } from '../appointment.service';

@Component({
  selector: 'cadence-new-appointment',
  templateUrl: './new-appointment.component.html',
  styleUrls: ['./new-appointment.component.css']
})
export class NewAppointmentComponent implements OnInit {
  model = new Appointment();

  constructor(private appointmentService: AppointmentService) { }

  ngOnInit() {
  }

  save() {
    this.appointmentService.save(this.model);
  }
}

We’ll need to add AppointmentService as a provider in order for it to be able to be injected into components.

// src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { CalendarComponent } from './calendar/calendar.component';
import { NewAppointmentComponent } from './calendar/new-appointment/new-appointment.component';
import { AppointmentService } from './calendar/appointment.service';

@NgModule({
  declarations: [
    AppComponent,
    CalendarComponent,
    NewAppointmentComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [AppointmentService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Similar to how we moved the saving code from the component to the service, we can move the retrieval code as well. Let’s create a getList() function in AppointmentService.

// src/app/calendar/appointment.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class AppointmentService {

  constructor() { }

  getList() {
    return JSON.parse(localStorage.getItem('appointments'));
  }

  save(appointment) {
    let appointments = JSON.parse(localStorage.getItem('appointments'));
    appointments.push(appointment);

    localStorage.setItem('appointments', JSON.stringify(appointments));
  }
}

Then CalendarComponent can call getList().

// src/app/calendar/calendar.component.ts

import { Component, OnInit } from '@angular/core';
import { AppointmentService } from './appointment.service';

@Component({
  selector: 'cadence-calendar',
  templateUrl: './calendar.component.html',
  styleUrls: ['./calendar.component.css']
})
export class CalendarComponent implements OnInit {
  appointments = [];

  constructor(private appointmentService: AppointmentService) { }

  ngOnInit() {
    this.appointments = this.appointmentService.getList();
  }
}

Now that the getList() function exists, we can remove some duplication between the getList() function and the save() function by just calling getList() to get the list of appointments.

// src/app/calendar/appointment.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class AppointmentService {

  constructor() { }

  getList() {
    return JSON.parse(localStorage.getItem('appointments'));
  }

  save(appointment) {
    let appointments = this.getList();
    appointments.push(appointment);

    localStorage.setItem('appointments', JSON.stringify(appointments));
  }
}

Getting the components to talk to each other

At this point, saving appointments works and getting appointments back out works but we have to refresh the page in order for our appointments to show up. This is of course not what we ultimately want. We want the saved appointments to show up on the page immediately.

We need to get NewAppointmentComponent and CalendarComponent to talk to each other. The Angular docs describe a method of inter-component communication where component A sends some data to a service, then component B gets that data out of the same service. At a high level, this is a simple enough concept to understand.

Where things start to get somewhat complicated is the fact that the example from the Angular docs use observables to achieve this communication. I’d recommend you take a look at my blog post Observables Made Simple before proceeding. It took me a long time to grasp observables. I think that post will help you understand them a little more quickly than I first did.

The way we’re going to use observables in this case is the following: when an appointment is saved, AppointmentService will emit that appointment. When the appointment is emitted, CalendarComponent will become aware of it because CalendarComponent has subscribed to the observable that emits appointments.

The first step is to add the observable to AppointmentService.

// src/app/calendar/appointment.service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class AppointmentService {

  private appointmentCreatedSource = new Subject<string>();

  appointmentCreated$ = this.appointmentCreatedSource.asObservable();

  constructor() { }

  getList() {
    return JSON.parse(localStorage.getItem('appointments'));
  }

  save(appointment) {
    let appointments = this.getList();
    appointments.push(appointment);

    localStorage.setItem('appointments', JSON.stringify(appointments));
    this.appointmentCreatedSource.next(appointment);
  }
}

Then modify CalendarComponent so it subscribes to the appointment-emitting service and reloads the appointment list when it knows a new appointment has been saved.

// src/app/calendar/calendar.component.ts

import { Component, OnInit } from '@angular/core';
import { AppointmentService } from './appointment.service';

@Component({
  selector: 'cadence-calendar',
  templateUrl: './calendar.component.html',
  styleUrls: ['./calendar.component.css']
})
export class CalendarComponent implements OnInit {
  appointments = [];

  constructor(private appointmentService: AppointmentService) {
    appointmentService.appointmentCreated$.subscribe(appointment => {
      this.loadAppointments();
    });
  }

  loadAppointments() {
    this.appointments = this.appointmentService.getList();
  }

  ngOnInit() {
    this.loadAppointments();
  }
}

Now when you save an appointment it will show up immediately.

Making it look nicer

It would be nice if the appointment form actually cleared itself when we save an appointment. We can do this by calling reset() on the form when the submit button is clicked.

<!-- src/app/calendar/new-appointment/new-appointment.component.html -->

<form #appointmentForm="ngForm" (submit)="save(); appointmentForm.reset()">

  <div class="form-group">
    <label for="date">Date</label>
    <input type="text"
           class="form-control"
           name="date"
           id="date"
           [(ngModel)]="model.date">
  </div>

  <div class="form-group">
    <label for="clientName">Client name</label>
    <input type="text"
           class="form-control"
           name="clientName"
           id="clientName"
           [(ngModel)]="model.clientName">
  </div>

  <input type="submit" class="btn btn-primary" value="Save">
</form>

A little padding would also help.

/* src/styles.css */

body {
  padding-top: 30px;
}

Lastly, we’ll give the appointment form a containing gray box. First we’ll give it a class name.

<!-- src/app/calendar/new-appointment/new-appointment.component.html -->

<div class="new-appointment-form-container">
  <form #appointmentForm="ngForm" (submit)="save(); appointmentForm.reset()">

    <div class="form-group">
      <label for="date">Date</label>
      <input type="text"
             class="form-control"
             name="date"
             id="date"
             [(ngModel)]="model.date">
    </div>

    <div class="form-group">
      <label for="clientName">Client name</label>
      <input type="text"
             class="form-control"
             name="clientName"
             id="clientName"
             [(ngModel)]="model.clientName">
    </div>

    <input type="submit" class="btn btn-primary" value="Save">
  </form>
</div>

Then we’ll add the styles.

/* src/app/calendar/new-appointment/new-appointment.component.css */

.new-appointment-form-container {
  background-color: #DDD;
  padding: 30px;
}

Then we’ll change the markup of the calendar page to make it look a little nicer.

<!-- src/app/calendar/calendar.component.html -->

<div class="row">
  <div class="col-lg-6">
    <cadence-new-appointment></cadence-new-appointment>
  </div>

  <div class="col-lg-6">
    <div *ngFor="let appointment of appointments">
      <hr>
      {{ appointment.date }}
      {{ appointment.clientName }}
    </div>
  </div>
</div>

Your application should now roughly match the screenshot below.

One thought on “Angular/Rails CRUD Tutorial Part 1

  1. manjul

    You are awesome, i did not get any tutorial with detailed understanding can u guide me with more angular concepts ?

    for example you have skipped, what does it mean by this “#appointmentForm” in this

    Reply

Leave a Reply

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