Ultimate guide to edit forms with Angular & Firebase

Frank Paepens
10 min readFeb 12, 2019
Howto build an edit form & grid with Angular + Firebase + Bootstrap

Recently we were re-evaluating how we build edit forms for our projects. In order to keep myself updated with the latest technologies I took this challenge as a weekend-project. It ended up us being much more than a weekend…

In the meantime I updated this article several times, and now it is possible to create these forms for your own projects without having to write any custom code if you use our online generator: you just need copy/paste skills.

Technologies used:

  • Angular
  • Firestore (part of Firebase)
  • Bootstrap

The requirements were:

  • Basic edit form (with visual feedback for input errors)
  • A data grid to show the objects
  • Some kind of menu to navigate
  • Objects are stored in Firestore (a No-SQL back end)
  • Responsive design: the layout should adapt to different screen sizes

So this is basically CRUD functionality as we call it (Create/Read/Update/Delete).

Screenshots

The focus of this guide is mainly on architecture and less on ‘the looks’. But if you’re curious about the end result, than here are some screenshots:

Product grid
Edit form

Objectives

I had 2 objectives when making this project:

  • Try to be complete: everybody with basic programming skills should be able to reproduce this project
  • Minimize the amount of custom code: I always strive to factor out common & re-usable logic into base classes to reduce the amount of specific code. And as a bonus I will provide you in the near future with a small tool to generate the custom code for your specific project.

Before we dive into the code, let me explain part of the object model:

Class diagram

The class diagram contains the abstract classes:

  • ModelBase: a really simple base class for all the other object classes in your model. It contains the property “id”, as such every class in your model will have an id.
  • FirestoreObjectService<T extends ModelBase>: a class that contains generic code for Firestore related functionality: save, update, delete. It relies on the fact that all your model classes inherit from ModelBase (= your objects have an id)
  • BasicFormComponent<T extends ModelBase>: derive your edit form components from this class. This contains basic logic to work with both reactive & template driven forms in Angular.
  • BasicGridComponent<T extends ModelBase>: derive your object grid components from this class. (missing in diagram)

And the object model also has these concrete classes:

  • Product: This post is about products (a product edit form & a grid with these products). This class contains some product specific properties: name, description & stock. Feel free to derive your own object types: customer, invoice, …
  • ProductService: this class extends the FireStoreObjectService<T>. Thanks to the inheritance it is almost empty, but you can put more specific queries in this service if you want/need.
  • ProductComponent: derives from BasicFormComponent<T>. It contains the form to create/edit a product.
  • ProductGridComponent: derives from BasicGridComponent<T>. It contains the grid to show all existing products.

It is all about products in this example, but feel free to use your own domain objects (customer, invoice, …).

Development procedure

Introduction

From here on I will show howto build this kind of solutions.
You can also find this finished project on github. (remark: it will not run, because our Firebase settings are -of course- missing, but feel free to put yours!).
Update: I’m using the same github repository for my follow up articles. I try to update this article, but if there’s a discrepancy between this article and github, than github will contain the most recent code.

Prerequisites

Node.js

You can install it from here. Basically Node.js allows you to run Javascript code on your local PC or on a server (outside a browser).

Angular CLI

Angular CLI stands for Angular Command Line Interface. It is a command line tool for creating angular apps. You can install it with the command:

npm install -g @angular/cli

For code editing we have been using Visual Studio Code. But feel free to use the code editor of your choice…

Project setup

Create a new Angular project with routing and scss based styling:

ng new angFireBoot1 --routing=true --style=scss
cd angFireBoot1

Install all the dependencies for this project via npm:

npm install class-transformer --save
npm install bootstrap --save
npm install @ng-bootstrap/ng-bootstrap --save
npm install ngx-loading --save
npm install rxjs-compat --save
npm install firebase @angular/fire --save
npm install --save ag-grid-community ag-grid-angular

Add style sheet code to ‘/src/styles.scss’:

@import "~bootstrap/dist/css/bootstrap.min.css";
@import "~ag-grid-community/dist/styles/ag-grid.css";
@import "~ag-grid-community/dist/styles/ag-theme-balham.css";
/* to format valid/invalid inputs */
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
h1.page-header {
color: #343a40;
font-size: 2em;
margin-bottom: 10px;
}

I also like to use the icons from Font Awesome in my projects. For this I include the following link inside the <head> tag of the file ‘/src/index.html’:

<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">

Add the Firestore configuration to ‘environments.ts’ and to ‘/src/environments.prod.ts’:

export const environment = {
production: false,
firebase: {
apiKey: "...",
authDomain: "...",
databaseURL: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "..."
}

};

You can get these configuration settings from the Firebase console:

Create or select your project in the Firebase console. Than you will find in the project settings the different app types that you support (iOS, Android, </> = WebApp). Select </>, and than you will see these settings. Just be sure that you copy all the key/value pairs to the correct location inside the environment TypeScript files.

Add import statements to the top of ‘/src/app/app.module.ts’:

import { environment } from '../environments/environment';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { AgGridModule } from 'ag-grid-angular';
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { NgxLoadingModule } from 'ngx-loading';

Add the items in bold to the imports array of the @NgModule decorator of the same file ‘/src/app/app.module.ts’:

@NgModule({
...,
imports: [
...,
NgbModule,
FormsModule,

ReactiveFormsModule,
AgGridModule.withComponents([]),
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule,
NgxLoadingModule.forRoot({})

],
...
})

Notice that we imported 2 different forms modules:

  • FormsModule: to work with template driven forms
  • ReactiveFormsModule: to work with reactive forms

These are 2 different ways of building forms in Angular, we support both (you can choose at component level).

Some generic infrastructure

Let’s create all the abstract classes that we already mentioned in the class diagram before (and some other helper interfaces/classes). Create the folder ‘general’ (inside ‘/src’) and copy over all the files from this directory in github:

  • i-session.service.ts
  • basic-form.component.ts
  • basic-grid.component.ts
  • date-helper.ts (needed when you work with date selections)
  • ngb-date-format.ts
  • firestore-object.service.ts
  • model-base.ts
  • where-filter.ts

Session service

Most applications have a session service to keep track of session state, such as the current users, preferences, etc… . So let’s generate the session service:

ng generate service services/session

We’ll use it currently for 2 purposes:

  • showSpinner: if true, a waiting spinner will be shown
  • menus: we’ll keep track of the app menu’s in this service (in some cases we need to update the selected menu item,

Copy over the code from github, also make sure to add ‘implements ISessionService’ next to the class name!

Model definitions

First create the folder ‘/src/app/model’.

Up to this point we created general stuff not related to any specific business object (just copy/paste work). As from here we start to create more specific code. But don’t worry, I made it easy: you can generate all this custom code with our online Angular Generator tool. It will generate most of the custom code needed to get your own code working!

We will model a ‘Product’ as an example, but feel free to replace it with your domain object.

So now we can create our specific model classes by deriving from ModelBase (a class that we previously created inside the general folder). For this example we create a model class in ‘/src/app/model/product.ts’:

import { ModelBase } from "../../general/model-base";export class Product extends ModelBase {
name: string = ''
description: string = ''
stock: number = 0
}

Also create an ‘index.ts’ inside the same folder with the content (than you can easily import all your domain objects with 1 import statement):

export * from "./product"

Create product components & service

Generate Angular components & services:

ng generate service services/product
ng generate component product
ng generate component product-grid

Routes

Add routes to the routes array in ‘/src/app/app-routing.module.ts’:

const routes: Routes = [
{ path: 'product/:id', component: ProductComponent },
{ path: 'product-grid', component: ProductGridComponent }

];

You will also need to put the correct import statements at the top of this file. A trick in Visual Studio Code is to remove the word ‘ProductComponent’ and to retype it manually, than Visual Studio Code will automatically add the import statements for you.

The main file

The visual entry point for your app is located in the file ‘/src/app/app.component.html’. Copy the content of this file (from github) into your project.

Now update the sibling Typescript file ‘/src/app/app.component.ts’ and also copy content from github.

Product service

Let’s first setup the product service in ‘/src/app/services/product.service.ts’:

import { Injectable } from '@angular/core';
import { FirestoreObjectService } from '../../general/firestore-object.service';
import * as Model from '../model';
import { AngularFirestore } from '@angular/fire/firestore';
@Injectable({
providedIn: 'root'
})
export class ProductService extends FirestoreObjectService<Model.Product> {constructor(firestore: AngularFirestore) {
super(Model.Product, firestore, 'product')
}
}

This service can stay very minimal thanks to the generic Firestore Object Service that we inherit from. Via the constructor we initiate this service (see super(…)) and we tell it:

  • that it will work with objects of type ‘Model.Product’
  • that these objects are stored in the path ‘product’ inside the Firestore db

Product edit form

The files (.html & .ts) for the product edit form are located in the folder ‘/src/app/product/’

Update the html file with the code below, or with the code that is generated with our builder:

<h1 class="page-header">Product</h1><form [formGroup]="formGroup">
<div class="form-group">
<label for="name">Name</label>
<input type="text" required class="form-control" id="name" placeholder="A name" formControlName="name">
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea class="form-control" id="description" formControlName="description"></textarea>
</div>
<div class="form-group">
<label for="stock">De stock</label>
<input type="number" class="form-control" id="stock" placeholder="The stock" formControlName="stock">
</div>
<button class="btn btn-primary float-right" (click)="save()" [disabled]="formGroup.pristine || formGroup.status != 'VALID'">Save</button><button *ngIf="object && object.id" class="btn btn-primary float-right mr-2" (click)="delete()">Delete</button>
</form>

And the typescript file with (or use builder):

(I left out the imports for clarity, see github)@Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.scss'],
providers: [
{ provide: NgbDateParserFormatter, useClass: NgbDateCustomParserFormatter }
]
})
export class ProductComponent extends BasicFormComponent<Model.Product> implements OnInit {constructor(productSvc: ProductService, sessionSvc: SessionService, router: Router, route: ActivatedRoute, fb: FormBuilder) {
super(AngularFormMode.Reactive, Model.Product, productSvc, sessionSvc, router, route, fb)
}
ngOnInit() {
this.processParameters()
}
/** will be called automatically if formMode AngularFormMode.Reactive (see above super(AngularFormMode.___) */

createFormGroup() {
this.formGroup = this.fb.group({
name: ['', Validators.required],
description: [''],
stock: ['']
})
}
}

Notice that:

  • the ProductComponent class inherits from the BasicFormComponent class (that you created before in the ‘general’ folder).
  • we specified to use the Angular Reactive form mode when calling the constructor of BasicFormComponent (see the call super( AngularFormMode.Reactive …))
  • Since we use the reactive mode, we need a createFormGroup() method (the online generator will also create this for you)

Product grid

The files (.html & .ts) for the product grid are located in the folder ‘/src/app/product-grid/’

Update the html file with (see github for latest version, or use our generator):

<h1 class="page-header">Product Grid</h1><ag-grid-angular #agGrid class="ag-theme-balham d-none d-sm-block" [rowData]="objectCol" [columnDefs]="columnDefs" rowSelection="single">
</ag-grid-angular>

<button class="btn btn-primary btn-raised text-white float-right mt-3" (click)="editSelectedRow()">Select</button>

And the typescript file with (the columnDefs can also be generated):

(I left out the imports for clarity, see github)@Component({
selector: 'product-grid',
templateUrl: './product-grid.component.html',
styleUrls: ['./product-grid.component.scss']
})
export class ProductGridComponent extends BasicGridComponent<Model.Product> implements OnInit {
constructor(router: Router, productSvc: ProductService) {let columnDefs = [
{ headerName: 'Name', field: 'name', sortable: true, filter: true },
{ headerName: 'Description', field: 'description', sortable: true, filter: true },
{ headerName: 'Stock', field: 'stock', sortable: true, filter: true }]
super(columnDefs, router, productSvc, '/product')
}
ngOnInit() {
this.getData()
}
}

Problems you might encounter

ERROR in ./src/styles.scss

This is a problem that we encounter regularly in the last weeks. You can fix this by running:

npm rebuild node-sass

Some links

Building this solution wouldn’t have been possible without:

Conclusion

In this guide, we demonstrated how to build a basic Web App to show & edit products (or any other object). We used:

  • Angular: as our Web application framework
  • Firestore (part of Firebase): the back-end to store our product data
  • Bootstrap: to skin (style) the Web App

I tried to minimize the amount of custom code needed to get any object/form working. And the custom code that is needed for your own projects can be generated by our online tool that you can find here.

Follow-up articles

Edit forms with Angular & Firebase part II: form with sub-forms in tabbed interface: sometimes you need to create more complex forms with tabs, here I show how you can achieve this, also with code generation.

Ultimate Angular Forms: Part III: This post was about very basic input controls, in part 3 I show some other controls (checkbox, radio, dropdown selects, date, …). Again with the help of the code builder!

Or, you can check the form builder page on our agency website.

--

--

Frank Paepens

Partner at Appdnd: agency specialized in App Design & Development. Interests: technology, startups, travel & watersports.