Introducing BookService: A New Service Class

When working through this Angularization series, it becomes clear that relying on @input and @output decorators for data sharing has its limits. Services offer a more scalable approach by centralizing data storage and making it accessible throughout the application.

Continuing from the project developed in the Introduction to Angular Services post, we'll introduce an input field that lets users append strings to a book list. This demonstrates a practical method for passing data with Angular Services.

The input lives in OneComponent, the list itself is managed by a service, and the output is rendered in AppComponent. Below is the final result.

Passing data with angular services app

As was the case in the Introduction to Angular Services, styling is largely omitted to keep the focus on the logic. The complete codebase is available on GitHub.

To start, we generate a new class within a fresh file, book.service.ts, placed in src/app. The class is composed of three parts:

  1. favBooks: A private array that holds book titles as objects.
  2. getBooksList: A method that returns the favBooks array.
  3. createBook: A method that verifies a title isn't empty, constructs a book object, and pushes it onto favBooks.
// book.service.ts

import { Injectable } from '@angular/core';
import { Book } from './models';

@Injectable({ providedIn: 'root' })
export class BookService {
  private favBooks: Book[] = [
    { title: 'Principles' },
    { title: 'The Story of Success' },
    { title: 'Extreme Economies' },
  ];

  getBooksList() {
    return this.favBooks;
  }

  createBook(bookTitle: string) {
    // simple check, title must be at least 1 char
    if (bookTitle.length !== 0) {
      const bookObj = { title: bookTitle };
      this.favBooks.push(bookObj);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that a Book type has been defined in src/models.ts.

Harnessing User Input

Next, OneComponent is modified to include a text input along with an "Add Title" button. Clicking the button triggers an onAddBook method in one.component.ts. This handler delegates to BookService's createBook method, supplying the user's input string.

Template Markup

// one.component.html

<div>
  <input 
    type="text" 
    placeholder="Write a title" 
    #titleInput />
  <button (click)="onAddBook()">Add Title</button>  
</div>
Enter fullscreen mode Exit fullscreen mode

The template uses #titleInput, a template reference variable that grants direct access to the DOM element. This approach is generally discouraged (as detailed in the Angular docs on ElementRef), but it provides the simplest path to capture input and keeps the spotlight on the service. For more robust solutions, consider ngModel or Angular reactive/ template-driven forms.

Class Logic

The one.component.ts file leverages ViewChild and ElementRef to access the input value. While not the recommended practice for data handling, it provides a simple mechanism until we cover Angular forms in a future post.

The key line is this.titleInputReference.nativeElement.value, which extracts the typed text from the input element.

// one.component.ts

import { Component, ElementRef, ViewChild } from '@angular/core';
import { BookService } from '../book.service';

@Component({
  selector: 'app-one',
  templateUrl: './one.component.html',
  styleUrls: ['./one.component.css'],
})
export class OneComponent {
  @ViewChild('titleInput')
  titleInputReference!: ElementRef;

  constructor(private bookService: BookService) {}

  onAddBook() {
    this.bookService.createBook(
      this.titleInputReference.nativeElement.value
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

In onAddBook(), the service's createBook method receives the input value—the string entered by the user.

Rendering Data in a Separate Component

To display the book list, AppComponent first injects the service via its constructor and then invokes it during ngOnInit() to retrieve the data.

// app.component.ts

import { Component, OnInit } from '@angular/core';
import { BookService } from './book.service';
import { Book } from './models';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  title = 'Passing Data with Angular Services';
  booksList: Book[] | undefined;

  constructor(private bookService: BookService) {}

  ngOnInit(): void {
    this.booksList = this.bookService.getBooksList();
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, the AppComponent template is updated to iterate over and render the list of books.

// app.component.html

<div>
  <h1>{{ title }}</h1>
  <hr />
  <div>
    <app-one></app-one>
    <app-two></app-two>
  </div>
  <hr />
  <div *ngIf="booksList" class="wrapper">
    <div *ngFor="let book of booksList" class="book">
      {{ book.title }}
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

This architecture enables data transfer between components by way of a shared service. This service is not limited to a single pair; it can supply information to any component within the application and accept updates from anywhere in the codebase.