Home
> Angular Todo App with Firebase

Angular
Angular Todo App with Firebase

16 min read
Jonathan Gamble

jdgamble555 on Sunday, October 3, 2021 (last modified on Saturday, March 9, 2024)

Using Angular with Firebase has changing slightly since version 16. While we navigate closer to standalone components, the library is evolving. There are new ways to do the same old thing, and options depending on your needs. Here I make the same ol app, but in Angular: A Todo App.

TL;DR#

Here I show you how to setup Angular with AngularFire so that you can use Firebase with your Angular app. You can also see how to setup signals with the user for authentication, and signals with realtime subscriptions on the todos collection. The Todo App is basic, which I don't focus on, but I show you modern Angular techniques since Angular 16. You also see that deploying your app on serverless is very easy, but Angular with Firebase cannot be deployed on the Edge without using the AnalogJS Framework.

Environment Variables#

The old Angular way is to use an environment.ts file to configure your app. We now know this is bad practice. We don’t want our keys, even public keys, easily accessible on GitHub. The easiest way to allow .env file support in Angular 16 and up, is to use @ngx-env/builder. You could also consider using Webpack, but not as easy to configure.

	ng add @ngx-env/builder

Note: If you’re using AnalogJS, you can skip this step, as .env file support works out of the box with Vite.

You need to add the NG_APP_FIREBASE_CONFIG variable to your .env file. You an use your variables anywhere in your project from your .env files as long as the prefix is NG_APP_.

	NG_APP_FIREBASE_CONFIG={"apiKey":"...","authDomain":"..."...}

Note: The keys must be in double quotes as well.

	const firebaseConfig = JSON.parse(
  import.meta.env['NG_APP_FIREBASE_CONFIG']
);

You can parse the data at the top of your app.config.ts file to be used later.

AngularFire#

First you need to install AngularFire. This is necessary in Angular due to the big JavaScript wrapper called Zone.JS. This is said to be optional in future versions of Angular, and possibly removed. It creates a large overhead that is going to be unnecessary now that we have Signals.

First add the Angular Fire package.

	ng add @angular/fire

Because we use standalone components, we need to put our Firebase providers in importProvidersFrom in our app.config.ts file so that they work correctly. You app will have all of them in separate functions, but you can just delete the generated version and simplify it to look pretty.

	export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideClientHydration(),
    // Angular Providers
    importProvidersFrom(
      provideFirebaseApp(() => initializeApp(firebaseConfig)),
      provideFirestore(() => getFirestore()),
      provideAuth(() => getAuth()),
      // provideStorage(() => getStorage()),
      // provideAnalytics(() => getAnalytics()),
    )
  ]
};

User Service#

Since we can’t view our todos without a logged in user, we must first handle the user state. First we make a new service.

	ng g s services/user

We will match the User type in Firebase, but we only need a few of the variables. We must make a type in TypeScript.

	export interface userData {
  photoURL: string | null;
  uid: string;
  displayName: string | null;
  email: string | null;
};

We are basically doing a hook like pattern from other frameworks, but the Angular way. While the old way of using rxjs declaratively still works, Signals will be much cleaner and faster. If we were fetching async data directly, we would probably prefer the rxjs way.

Signals#

First we create a signal in in the service to handle the data and the loading state after we inject the Auth provider from Firebase.

	  private auth = inject(Auth);

  user$ = signal<{
    loading: boolean,
    data: userData | null
  }>({
    loading: true,
    data: null
  });

Next we subscribe onIdTokenChanged, which will watch token and user changes. If we were using an rxjs version, or we didn’t want to change the shape of the user variable, we would probably use user variable from @angular/fire/auth. Either way, this version uses Signals. To keep watch of changes and to subscribe the signal way, we put the subscription in effect(). This is the same pattern you see with hooks in other frameworks.

	effect(() => {

      // toggle loading
      this.user$().loading = true;

      // server environment
      if (!this.auth) {
        this.user$().loading = false;
        this.user$().data = null;
        return;
      }

      return onIdTokenChanged(this.auth, (_user: User | null) => {

        this.user$().loading = false;

        if (!_user) {
          this.user$().data = null;
          return;
        }

        // map data to user data type
        const { photoURL, uid, displayName, email } = _user;
        const data = { photoURL, uid, displayName, email };

        // print data in dev mode
        if (isDevMode()) {
          console.log(data);
        }

        // set store
        this.user$().data = data;
      });

    });

Login#

Finally, we need to add our login methods to the service. This app uses Login With Google to keep it simple.

	  login() {
    signInWithPopup(this.auth, new GoogleAuthProvider());
  }

  logout() {
    signOut(this.auth);
  }

Todo Service#

Generate the service.

	ng g s services/todos

This service follows the same pattern as our user service, except that we require the user auth to subscribe to the todos collection by injecting the user service.

	  user = inject(UserService).user$;
  db = getFirestore();

  todos = signal<{
    data: TodoItem[],
    loading: boolean
  }>({
    data: [],
    loading: true
  });

We put the realtime query in our effect() function as well, and it will automatically unsubscribe when the component is destroyed by returning the snapshot.

	    effect(() => {

      const userData = this.user().data;

      if (!userData) {
        this.todos().loading = false;
        this.todos().data = [];
        return;
      }

      return onSnapshot(

        // query realtime todo list
        query(
          collection(this.db, 'todos') as CollectionReference<TodoItem[]>,
          where('uid', '==', userData.uid),
          orderBy('created')
        ), (q) => {

          // toggle loading
          this.todos().loading = false;

          // get data, map to todo type
          const data = snapToData(q);

          /**
           * Note: Will get triggered 2x on add 
           * 1 - for optimistic update
           * 2 - update real date from server date
          */

          // print data in dev mode
          if (isDevMode()) {
            console.log(data);
          }

          // add to store
          this.todos().data = data;
        });

    });

If there is no user, we don’t subscribe the database. This is important as the where clause requires a user id. We also need to transform the snap shot data, so I added a helper function above the service. You can put it in an external file if you plan to reuse it.

	export const snapToData = (
  q: QuerySnapshot<DocumentData, DocumentData>
) => {

  // creates todo data from snapshot
  if (q.empty) {
    return [];
  }
  return q.docs.map((doc) => {
    const data = doc.data();
    return {
      ...data,
      created: new Date(data['created']?.toMillis()),
      id: doc.id
    }
  }) as TodoItem[];
}

This just keeps everything clean.

Todo CRUD#

My example app doesn’t use Angular Reactive Forms, but you would probably want to if you have anything but simplicity.

	  addTodo = (e: SubmitEvent) => {

    e.preventDefault();

    if (!this.user().data) {
      throw 'No user!';
    }

    const uid = this.user().data?.uid;

    // get and reset form
    const target = e.target as HTMLFormElement;
    const form = new FormData(target);
    const { task } = Object.fromEntries(form);
  
    if (typeof task !== 'string') {
        return;
    }
  
    // reset form
    target.reset();
  
    addDoc(collection(this.db, 'todos'), {
        uid,
        text: task,
        complete: false,
        created: serverTimestamp()
    });
  }

You must prevent the todo from being added if there is no user, but this should be impossible as you shouldn’t be able to view the form without a logged in user. However, errors can happen.

The update and delete functions are as expected.

	  updateTodo = (id: string, complete: boolean) => {
    updateDoc(doc(this.db, 'todos', id), { complete });
  }
  
  deleteTodo = (id: string) => {
    deleteDoc(doc(this.db, 'todos', id));
  }

Components#

I’m not going to go over the entire Angular app as this post is more about Firebase, but you need to know that you can use the user service in any component by injecting it.

	us = inject(UserService);

Or if you need just the user signal:

	user = inject(UserService).user$;

You call it in your template with:

	us.user$().data

And of course you do the same thing with todos.

	todos = inject(TodosService).todos;

Just make sure to put the todos in a component where a user is logged in.

Deployment#

Deploying an Angular app is not easy, but deploying an Angular Fire app is even more brutal. Google wants you to use Firebase Functions of course. You can use firebase init hosting or ng deploy and follow the instructions. You may need to set prerender, to false in angular.json, and comment out the port in server.ts, as this is a known issue.

	// const port = process.env['PORT'] || 4000;
const port = 4000;

See State of Angular SSR Deployment for other serverless options and more information.

Edge Deployment#

There is no efficient way to deploy Angular on the Edge without using AnalogJS. For the moment, that is the only way to get Angular and Firebase to work together. You also cannot use firebase-admin at all on Edge servers, but you could theoretically call a Firebase Cloud Function to handle the login from the server.

In any case, remember to add your firebase config information to your production server.

Repo: GitHub

Analog Repo: GitHub

Serverless Demo: Vercel Demo

Edge Demo: Netlify Demo


Related Posts

🔥 Authentication

🔥 Counting Documents

🔥 Data Modeling

🔥 Framework Setup

🔥Under Construction

© 2024 Code.Build