Skip to the content.

Angular Integration

This guide shows how to integrate Firebase ORM with Angular applications, covering both standalone components and traditional NgModule-based applications.

Quick Setup

1. Installation

npm install @arbel/firebase-orm firebase rxfire moment --save

2. Angular Configuration

Add to your angular.json build options:

{
  "projects": {
    "your-app": {
      "architect": {
        "build": {
          "options": {
            "allowedCommonJsDependencies": [
              "@arbel/firebase-orm",
              "moment"
            ]
          }
        }
      }
    }
  }
}

3. TypeScript Configuration

Update your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strictPropertyInitialization": false
  }
}

Firebase Setup Service

Create a Firebase service to initialize the connection:

// src/app/services/firebase.service.ts
import { Injectable } from '@angular/core';
import { initializeApp, FirebaseApp } from 'firebase/app';
import { getFirestore, Firestore } from 'firebase/firestore';
import { FirestoreOrmRepository } from '@arbel/firebase-orm';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class FirebaseService {
  private app: FirebaseApp;
  private firestore: Firestore;

  constructor() {
    this.initializeFirebase();
  }

  private initializeFirebase() {
    this.app = initializeApp(environment.firebase);
    this.firestore = getFirestore(this.app);
    
    // Initialize Firebase ORM
    FirestoreOrmRepository.initGlobalConnection(this.firestore);
  }

  getFirestore(): Firestore {
    return this.firestore;
  }
}

Environment Configuration

Add Firebase config to your environment files:

// src/environments/environment.ts
export const environment = {
  production: false,
  firebase: {
    apiKey: "your-api-key",
    authDomain: "your-project.firebaseapp.com",
    projectId: "your-project-id",
    storageBucket: "your-project.appspot.com",
    messagingSenderId: "123456789",
    appId: "your-app-id"
  }
};

Model Definition

Create your models in a dedicated folder:

// src/app/models/user.model.ts
import { BaseModel, Model, Field, HasMany } from '@arbel/firebase-orm';
import { Post } from './post.model';

@Model({
  reference_path: 'users',
  path_id: 'user_id'
})
export class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @Field({ is_required: true })
  public email!: string;

  @Field({ field_name: 'created_at' })
  public createdAt?: string;

  @HasMany({ model: Post, foreignKey: 'author_id' })
  public posts?: Post[];

  // Angular-specific helper method
  getDisplayName(): string {
    return this.name || this.email;
  }
}

Data Services

Create Angular services to encapsulate data operations:

// src/app/services/user.service.ts
import { Injectable } from '@angular/core';
import { Observable, from, BehaviorSubject } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { User } from '../models/user.model';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private usersSubject = new BehaviorSubject<User[]>([]);
  public users$ = this.usersSubject.asObservable();

  constructor(private firebaseService: FirebaseService) {}

  // Get all users
  getUsers(): Observable<User[]> {
    return from(User.getAll()).pipe(
      map(users => {
        this.usersSubject.next(users);
        return users;
      }),
      catchError(error => {
        console.error('Error fetching users:', error);
        throw error;
      })
    );
  }

  // Get user by ID
  getUser(id: string): Observable<User> {
    return from(this.loadUser(id));
  }

  private async loadUser(id: string): Promise<User> {
    const user = new User();
    await user.load(id);
    return user;
  }

  // Create new user
  createUser(userData: Partial<User>): Observable<User> {
    return from(this.saveUser(userData));
  }

  private async saveUser(userData: Partial<User>): Promise<User> {
    const user = new User();
    user.initFromData(userData);
    user.createdAt = new Date().toISOString();
    await user.save();
    return user;
  }

  // Update user
  updateUser(id: string, updates: Partial<User>): Observable<User> {
    return from(this.updateUserAsync(id, updates));
  }

  private async updateUserAsync(id: string, updates: Partial<User>): Promise<User> {
    const user = new User();
    await user.load(id);
    user.initFromData(updates);
    await user.save();
    return user;
  }

  // Delete user
  deleteUser(id: string): Observable<void> {
    return from(this.deleteUserAsync(id));
  }

  private async deleteUserAsync(id: string): Promise<void> {
    const user = new User();
    await user.load(id);
    await user.destroy();
  }

  // Real-time subscription
  subscribeToUsers(): () => void {
    return User.onList((user: User) => {
      const currentUsers = this.usersSubject.value;
      const index = currentUsers.findIndex(u => u.getId() === user.getId());
      
      if (index >= 0) {
        currentUsers[index] = user;
      } else {
        currentUsers.push(user);
      }
      
      this.usersSubject.next([...currentUsers]);
    });
  }
}

Component Implementation

Standalone Component (Angular 14+)

// src/app/components/user-list/user-list.component.ts
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { UserService } from '../../services/user.service';
import { User } from '../../models/user.model';

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [CommonModule, FormsModule, ReactiveFormsModule],
  template: `

    <div class="user-list-container">
      <h2>Users</h2>
      
      <!-- Add User Form -->
      <form [formGroup]="userForm" (ngSubmit)="addUser()" class="add-user-form">
        <input formControlName="name" placeholder="Name" required>
        <input formControlName="email" type="email" placeholder="Email" required>
        <button type="submit" [disabled]="userForm.invalid">Add User</button>
      </form>

      <!-- Users List -->
      <div class="users-grid">
        <div *ngFor="let user of users$ | async; trackBy: trackByUserId" 
             class="user-card">
          <h3>{{ user.getDisplayName() }}</h3>
          <p>Email: {{ user.email }}</p>
          <p>Created: {{ user.createdAt | date }}</p>
          
          <div class="user-actions">
            <button (click)="editUser(user)">Edit</button>
            <button (click)="deleteUser(user.getId())" class="danger">Delete</button>
            <button (click)="loadUserPosts(user)">Load Posts</button>
          </div>

          <!-- Show posts if loaded -->
          <div *ngIf="user.posts" class="user-posts">
            <h4>Posts ({{ user.posts.length }})</h4>
            <div *ngFor="let post of user.posts" class="post-item">
              {{ post.title }}
            </div>
          </div>
        </div>
      </div>
    </div>

  `,
  styles: [`
    .user-list-container { padding: 20px; }
    .add-user-form { 
      display: flex; 
      gap: 10px; 
      margin-bottom: 20px; 
    }
    .add-user-form input { 
      padding: 8px; 
      border: 1px solid #ddd; 
      border-radius: 4px; 
    }
    .users-grid { 
      display: grid; 
      grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 
      gap: 20px; 
    }
    .user-card { 
      border: 1px solid #ddd; 
      padding: 15px; 
      border-radius: 8px; 
    }
    .user-actions { 
      display: flex; 
      gap: 5px; 
      margin-top: 10px; 
    }
    .user-actions button { 
      padding: 5px 10px; 
      border: none; 
      border-radius: 4px; 
      cursor: pointer; 
    }
    .danger { background-color: #ff6b6b; color: white; }
    .user-posts { 
      margin-top: 10px; 
      padding-top: 10px; 
      border-top: 1px solid #eee; 
    }
    .post-item { 
      padding: 5px; 
      background: #f5f5f5; 
      margin: 2px 0; 
      border-radius: 3px; 
    }
  `]
})
export class UserListComponent implements OnInit, OnDestroy {
  private userService = inject(UserService);
  private fb = inject(FormBuilder);
  
  users$!: Observable<User[]>;
  userForm!: FormGroup;
  private subscription?: Subscription;
  private unsubscribe?: () => void;

  ngOnInit() {
    this.initializeForm();
    this.loadUsers();
    this.setupRealtimeSubscription();
  }

  ngOnDestroy() {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
    if (this.unsubscribe) {
      this.unsubscribe();
    }
  }

  private initializeForm() {
    this.userForm = this.fb.group({
      name: [''],
      email: ['']
    });
  }

  private loadUsers() {
    this.users$ = this.userService.users$;
    this.userService.getUsers().subscribe();
  }

  private setupRealtimeSubscription() {
    this.unsubscribe = this.userService.subscribeToUsers();
  }

  addUser() {
    if (this.userForm.valid) {
      const userData = this.userForm.value;
      this.userService.createUser(userData).subscribe({
        next: (user) => {
          console.log('User created:', user);
          this.userForm.reset();
        },
        error: (error) => {
          console.error('Error creating user:', error);
        }
      });
    }
  }

  editUser(user: User) {
    const newName = prompt('Enter new name:', user.name);
    if (newName && newName !== user.name) {
      this.userService.updateUser(user.getId(), { name: newName }).subscribe({
        next: (updatedUser) => {
          console.log('User updated:', updatedUser);
        },
        error: (error) => {
          console.error('Error updating user:', error);
        }
      });
    }
  }

  deleteUser(userId: string) {
    if (confirm('Are you sure you want to delete this user?')) {
      this.userService.deleteUser(userId).subscribe({
        next: () => {
          console.log('User deleted');
        },
        error: (error) => {
          console.error('Error deleting user:', error);
        }
      });
    }
  }

  async loadUserPosts(user: User) {
    try {
      user.posts = await user.loadHasMany('posts');
    } catch (error) {
      console.error('Error loading user posts:', error);
    }
  }

  trackByUserId(index: number, user: User): string {
    return user.getId();
  }
}

App Bootstrap

Standalone Bootstrap (Angular 14+)

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { FirebaseService } from './app/services/firebase.service';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    FirebaseService, // Ensure Firebase is initialized
    // ... other providers
  ]
}).catch(err => console.error(err));

Traditional NgModule Bootstrap

// src/app/app.module.ts
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { FirebaseService } from './services/firebase.service';
import { UserService } from './services/user.service';

export function initializeApp(firebaseService: FirebaseService) {
  return () => Promise.resolve(); // Firebase is initialized in constructor
}

@NgModule({
  declarations: [
    AppComponent,
    // ... other components
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    // ... other modules
  ],
  providers: [
    FirebaseService,
    UserService,
    {
      provide: APP_INITIALIZER,
      useFactory: initializeApp,
      deps: [FirebaseService],
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Error Handling

Create an error handling service:

// src/app/services/error-handling.service.ts
import { Injectable } from '@angular/core';
import { ToastrService } from 'ngx-toastr'; // Optional: for toast notifications

@Injectable({
  providedIn: 'root'
})
export class ErrorHandlingService {

  constructor(private toastr?: ToastrService) {}

  handleError(error: any, context: string = '') {
    console.error(`Error in ${context}:`, error);
    
    let message = 'An unexpected error occurred';
    
    if (error?.message) {
      if (error.message.includes('permission-denied')) {
        message = 'You do not have permission to perform this action';
      } else if (error.message.includes('not-found')) {
        message = 'The requested resource was not found';
      } else if (error.message.includes('network-request-failed')) {
        message = 'Network error. Please check your connection';
      }
    }

    // Show toast notification if available
    if (this.toastr) {
      this.toastr.error(message, 'Error');
    } else {
      alert(message);
    }
  }
}

Testing

Unit Testing with Jasmine/Karma

// src/app/services/user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { UserService } from './user.service';
import { FirebaseService } from './firebase.service';

describe('UserService', () => {
  let service: UserService;
  let firebaseServiceSpy: jasmine.SpyObj<FirebaseService>;

  beforeEach(() => {
    const spy = jasmine.createSpyObj('FirebaseService', ['getFirestore']);

    TestBed.configureTestingModule({
      providers: [
        UserService,
        { provide: FirebaseService, useValue: spy }
      ]
    });
    
    service = TestBed.inject(UserService);
    firebaseServiceSpy = TestBed.inject(FirebaseService) as jasmine.SpyObj<FirebaseService>;
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  // Add more tests for your service methods
});

Best Practices

1. Lazy Loading with Models

// src/app/features/users/users.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild([
      { path: '', loadComponent: () => import('./user-list.component').then(m => m.UserListComponent) }
    ])
  ]
})
export class UsersModule { }

2. State Management Integration

// With NgRx or Akita
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { User } from '../models/user.model';

interface UserState {
  users: User[];
  loading: boolean;
  error: string | null;
}

@Injectable()
export class UserStore extends ComponentStore<UserState> {
  constructor() {
    super({
      users: [],
      loading: false,
      error: null
    });
  }

  // Selectors
  readonly users$ = this.select(state => state.users);
  readonly loading$ = this.select(state => state.loading);
  readonly error$ = this.select(state => state.error);

  // Updaters
  readonly setUsers = this.updater((state, users: User[]) => ({
    ...state,
    users
  }));

  readonly setLoading = this.updater((state, loading: boolean) => ({
    ...state,
    loading
  }));
}

Common Issues

1. Build Warnings

If you get warnings about CommonJS dependencies, add them to allowedCommonJsDependencies in angular.json.

2. Decorator Issues

Ensure TypeScript configuration includes experimental decorators support.

3. Real-time Subscriptions

Always unsubscribe from real-time listeners in ngOnDestroy to prevent memory leaks.

Next Steps