user1969903
user1969903

Reputation: 962

Where to write UI logic in an Angular app?

Assume I have an Angular app with two views. The first view shows a preview of some object, let's say a car, and the other view shows detailed information of that car. The car model would be something similar to:

export class Car {
    model: string;
    type: CarTypeEnum;
    ...;
}

Let's say I want both views to show me an Icon representing the car's type. The logic would be:

switch(someCar.type) {
    case CarTypeEnum.HATCHBACK: return icons.hotHatch;
    case CarTypeEnum.SEDAN: return icons.sedan;
    case CarTypeEnum.SUV: return icons.suv;
    case CarTypeEnum.COUPE: return icons.coupe;
    case CarTypeEnum.VAN: return icons.van;
    case CarTypeEnum.WAGON: return icons.wagon;
}

Where should this logic for getting the icon based on the car type go?

export class Car {
    model: string;
    type: CarTypeEnum;
    ...;

    get typeIcon() { 
        // switch goes here
    }
}

This feels somewhat right, given that I'm using this in two separate views, but it also feels like I'm polluting the model.

<div *ngIf="car.type == carType.HATCHBACK" class="hatchback-icon-class"> ... </div>
<div *ngIf="car.type == carType.COUPE" class="coupe-icon-class"> ... </div>
...

Upvotes: 1

Views: 519

Answers (2)

Brian Burton
Brian Burton

Reputation: 3842

My preferred strategy is to use services. You can either create it as a singleton so that when you load a car it's available for all components, or you can load it individually so that each component can load a different car.

Here's an example of what that looks like.

/services/car.service.ts

The service that loads a car from your data source and provides a standardized interface to all of your components

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

// If you want this to behave as a singleton, add {providedIn: 'root'} to @Injectable
@Injectable()
export class CarService {
    private _car = new BehaviorSubject<any>(null);
    private _carSnapshot;
    
    // Method called by your components to load a specific car
    load(carId: string): Promise<any> {
        return this.getCarInfoFromWherever(carId);
    }

    // Returns an observable of the currently loaded car
    car$(): Observable {
        return this._car.asObservable();
    }

    // Method to retrieve the car data from whatever datasource you're using
    getCarInfoFromWherever(carId: string): Promise<any> {
        return new Promise(async (resolve: any) => {
            // Retrieve the car information from wherever it is such as a database.
            const carInfo = await DbGetCarInfo(carId);

            // Set an object for easy access to current vehicle
            this._carSnapshot = carInfo;

            // Update your observable
            this._car.next(carInfo);
        });
    }

    // Example of abstraction to retrieve car attributes
    getType(): string {
      if (this._carSnapshot)
          return this._carSnapshot['type'];

      return null;
    }
}

/components/main/main.component.ts

A component that wants to display a Ford Pinto

import { Component } from '@angular/core';
import { distinctUntilChanged } from 'rxjs/operators';

@Component({
    selector: 'app-main',
    templateUrl: './main.component.html',
    styleUrls: ['./main.component.scss']
})
export class MainComponent {
    private _subscription;

    constructor(
        // Inject the CarService into our component
        public carSvc: CarService
    ) {
        // Tell CarService which car to load
        this.carSvc.load('FordPinto').then();
    }

    ngOnInit(): void {
        // Subscribe to the car service observable
        this._subscription = this.carSvc.car$()
            .pipe(distinctUntilChanged())
            .subscribe((car: any) => {
                // The car has been loaded, changed, do something with the data.
                console.log("Car Type:", this.carSvc.getType());
            });
    }

    // Unsubscribe from the CarService observable
    ngOnDestroy(): void {
        this._subscription.unsubscribe();
    }
}

/components/dashboard/dashboard.component.ts

A component that wants to display a Ferrari Testarossa

import { Component, OnInit } from '@angular/core';
import { distinctUntilChanged } from 'rxjs/operators';

@Component({
    selector: 'app-dashboard',
    // Here's an example of using the observable in a template
    template: `<div>{{carSvc.car$() | json}}`,
    styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit, O {

    constructor(
        public carSvc: CarService
    ) {
        this.carSvc.load('FerrariTestarossa').then();
    }

    ngOnInit(): void {
        this.carSvc.car$()
            .pipe(distinctUntilChanged())
            .subscribe((car: any) => {
                // Do something with the car information
            });
    }

    // Unsubscribe from the CarService observable
    ngOnDestroy(): void {
        this._subscription.unsubscribe();
    }
}

In this example two separate components load up to separate cars.

Upvotes: 0

Dani P.
Dani P.

Reputation: 1138

If you are going to use that icon all along the app, I would recommend creating a component with an input binding (as you say in your last paragraph).

Angular really encourages you to make presentational components that are easy to test and reuse, following KISS and SOLID principles.

More info in this article: https://indepth.dev/posts/1066/presentational-components-with-angular

Upvotes: 1

Related Questions