Lesson Monday

After the last lesson, we can successfully display data from our Firebase database into our application. That's fantastic! However, we'd also like to be able to create new records in our application and save them into Firebase.

Admin Route

First, we'll need to create an area that allows us to add new Albums to our store. For now, this will simply be an "admin" route. (We won't worry about authenticating user accounts though!) Let's create a new admin route now, using the same process we learned in the Managing and Navigating Multiple Routes lesson.

First, we'll make an AdminComponent to manage the display and behavior present in this route:

$ ng g component admin

Next, we'll import it into our router file and define a new route object:

src/app/app.routing.ts
...
import { AdminComponent }   from './admin/admin.component';

const appRoutes: Routes = [
  ...
  {
    path: 'admin',
    component: AdminComponent
  }
 ];
...

Next, we'll make sure there's a routerLink available for users to navigate to the new Admin route. We won't worry about authenticating any users right now, so we'll just place a small link in a footer at the bottom of our AppComponent template:

src/app/app.component.html
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand" routerLink="">Epicodus Tunes</a>
    </div>
    <ul class="nav navbar-nav navbar-right">
      <li><a routerLink="about">About</a></li>
      <li><a routerLink="marketplace">Marketplace</a></li>
    </ul>
  </div>
</nav>

<div class="container">
  <h1>{{title}}</h1>
  <router-outlet></router-outlet>
</div>

<footer class="footer">
  <div class="container">
    <a class="text-muted" routerLink="admin">Admin</a>
  </div>
</footer>

If we'd like, we can also add a CSS rule to style our new footer:

src/styles.css
.footer {
  position: fixed;
  bottom: 0;
  width: 100%;
  height: 60px;
  background-color: #f5f5f5;
  padding: 15px;
}

Here, we fix it to the bottom of our page. We place this rule in the global styles.css folder, instead of one of the .css files generated when we create a component, because we want our footer (and therefore its styles) to appear application-wide.

If we click our new footer link, we should see the "admin works" text, automatically placed in our admin-component.html file by Angular CLI, appear on the page.

Form

Next, our AdminComponent will need a form to create new objects. We'll use template reference variables to quickly retrieve the content placed in the fields. This should also be review from last week.

src/app/admin/admin.component.html
<h3>Add New Album to Inventory</h3>
<div>
  <div>
    <label>Album Title:</label>
    <br>
    <input #newTitle>
  </div>

  <div>
    <label>Album Artist:</label>
    <br>
    <input #newArtist>
  </div>

  <div>
    <label>Album Description:</label>
    <br>
    <textarea #newDescription></textarea>
  </div>
</div>

We can optionally place our form in a Bootstrap panel with a heading to make it look a bit nicer, too:

src/app/admin/admin.component.html
<div class="panel panel-default">
  <div class="panel-heading">
    <h3 class="panel-title">Add New Album to Inventory</h3>
  </div>
  <div class="panel-body">
    <div>
      <div>
        <label>Album Title:</label>
        <br>
        <input #newTitle>
      </div>

      <div>
        <label>Album Artist:</label>
        <br>
        <input #newArtist>
      </div>

      <div>
        <label>Album Description:</label>
        <br>
        <textarea #newDescription></textarea>
      </div>
    </div>
  </div>
</div>

Next, let's add a button to our form that will trigger a method to create a new Album object with the information provided by the user.

src/app/admin/admin.component.html
...
      <div>
        <label>Album Description:</label>
        <br>
        <textarea #newDescription></textarea>
      </div>
      <button (click)="submitForm(newTitle.value, newArtist.value, newDescription.value)">Add</button>
...

Here, we've added an event binding to trigger a submitForm() method when clicked. It provides the form input as parameters.

Let's also make sure to empty each text field after the form is submitted:

src/app/admin/admin.component.html
...
      <button (click)="submitForm(newTitle.value, newArtist.value, newDescription.value); newTitle.value=''; newArtist.value=''; newDescription.value=''">Add</button>
...

Next, we'll define submitForm() in the AdminComponent:

src/app/admin/admin.component.ts
import { Album } from '../album.model';

...

export class AdminComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

  submitForm(title: string, artist: string, description: string) {
    var newAlbum: Album = new Album(title, artist, description);
    console.log(newAlbum);
  }

}

Here, we're simply creating a new Album with the form input, and logging it to the console. You'll also notice we don't include an id property as a parameter. Nor does our form collect an id for the Album from the user. This is because Firebase will now assign each new Album an id automatically. We no longer have to do this in our application. So, we'll remove the id property from the Album model now, too:

src/app/album.model.ts
export class Album {
  constructor (public title: string, public artist: string, public description: string) {      }
}

We'll also need to temporarily comment out the contents of getAlbumById() in our service, since it also references the id property we've just removed:

src/app/album.service.ts
...
  getAlbumById(albumId: number){
    // for (var i = 0; i <= ALBUMS.length - 1; i++) {
    //   if (ALBUMS[i].id === albumId) {
    //     return ALBUMS[i];
    //   }
    // }
  }
...

The contents of the goToDetailPage() method in the MarketplaceComponent also references the id property of Album objects, so we'll comment it out too, to avoid compiler errors:

src/app/marketplace/marketplace.component.ts
...
  goToDetailPage(clickedAlbum: Album) {
    // this.router.navigate(['albums', clickedAlbum.id]);
  };
...

Now, we should be able to enter information into our form, and see it logged to the console. Excellent, we're successfully creating a new Album object. Everything up until this point should be review. Next, let's explore how to take this new object, and persist it in our Firebase database.

Saving Data to Firebase

When we created new objects with forms last week, we passed them to a parent component to store and display. We could pass our new Album object from the child AdminComponent to the parent AppComponent with an @Output if we wanted.

However, as discussed previously, our AlbumService will continue managing our application's relationship with Firebase. And, a service can be injected into any component. It can even be injected into multiple components! So, we can send them to Firebase right here in AdminComponent.

Injecting a Service

First we'll need the AlbumService here in our AdminComponent:

src/app/admin/admin.component.ts
import { Component } from '@angular/core';
import { AlbumService } from '../album.service';
import { Album } from '../album.model';
...

Next, we'll add the AlbumService to the constructor:

src/app/admin/admin.component.ts
...
  constructor(private albumService: AlbumService) { }
...

Finally, we'll create a providers array in the AdminComponent's annotation, and register AlbumService:

src/app/admin/admin.component.ts
...
@Component({
  selector: 'app-admin',
  templateUrl: './admin.component.html',
  styleUrls: ['./admin.component.css'],
  providers: [AlbumService]
})
...

Now, when injecting the AlbumService previously, we also added code to the component's ngOnInit() method. Any code in this method ran automatically as soon as the component was instantiated.

However, we don't need to update the ngOnInit() method here in the AdminComponent quite yet, because we don't need it to interact with our Service or Firebase as soon as it's initialized. We only need to do so when the form is submitted.

Creating a New Firebase Entry

Next, we'll need a method in the AlbumService to manage the creation of new database entries. The submitForm() method in the AdminComponent will call this method to invoke the Service to save a new Album to Firebase.

src/app/album.service.ts
import { Injectable } from '@angular/core';
import { Album } from '../album.model';
import { AngularFireDatabase, FirebaseListObservable } from 'angularfire2/database';

@Injectable()
export class AlbumService {
  albums: FirebaseListObservable<any[]>;

  constructor(private database: AngularFireDatabase) {
    this.albums = database.list('albums');
  }

  getAlbums(){
    return this.albums;
  }

  addAlbum(newAlbum: Album) {
    this.albums.push(newAlbum);
  }

  getAlbumById(albumId: number){
    // for (var i = 0; i <= ALBUMS.length - 1; i++) {
    //   if (ALBUMS[i].id === albumId) {
    //     return ALBUMS[i];
    //   }
    // }
  }

}
  • Here, the addAlbum() method refers to the this.albums defined in the service's constructor. this.albums refers to the specific area of our database where our list of Albums is stored.

  • Because this.albums is a FirebaseListObservable<any[]>, as declared at the top of the file, it has many of the same properties and capabilities of any other list or array. We can simply call push() on it to add our new album to the list.

Next, let's update the submitForm() method in our AdminComponent to call the service's new addAlbum() method:

src/app/admin/admin.component.ts
...
  submitForm(title: string, artist: string, description: string) {
    var newAlbum: Album = new Album(title, artist, description);
    this.albumService.addAlbum(newAlbum);
  }

...

Now, we should be able to submit our form ....

submit-new-form

And see a new entry immediately appear in our Firebase database! (If you're not able to do this, try restarting your server!)

new-firebase-entry

Additionally, our new album is now listed in the MarketplaceComponent:

new-entry-in-list

However, we can't navigate to the new album's detail view, because we had to temporarily comment out the contents of the goToDetailPage() method in the MarketplaceComponent to avoid compiler errors.

In the next lesson, we'll address this error when we discuss how to manage Firebase ids, including returning only specific objects from the database. This will resolve our errors, and ensure we're able to view all albums' detail pages.