SOLID Angular architecture with 'stupid' components

Viewed 635

Our team is currently trying out an Angular architecture and I want some advices from other people too.

The "basic" Angular architecture would be something like that

Angular Architecture

The component has a view, it's logic and it's dependencies.

But we want to do a more solid approach which says "A class, function or method should only have one responsability". So we thought that we could let the component class manage only the view and export it's logic to a strongly linked service. Our architecture now looks more like that

Our Angular Architecture

But something feels weird about using a strongly linked service to export the logic of the component, we discussed about it in our team and the principle of an Angular service should be to performs business logic like calling an API and should be usable anywhere, not strongly linked to something like a component.

So that's why I'm here, we would like to have advices from the dev community about our architecture and be sure that we are not going in an unstable / wrong direction (please tell us if you think so and why)

Here is an example of what we did

We have a component that should display a list of products. We exported the logic to two services :

  • ProductListItemService that manage an item of the list
  • ProductsListService that manage the list itself

You can imagine that this will be difficult to use it elsewhere unless we need the same list in another view

We used the Inversion of Control from SOLID and injected our services with TokenInjection that allows us to use interfaces

The component do absolutly nothing but calling it's logic services and handling the view (show a loader, change a variable). Here you go with the code

// IMPORTS ARE HERE

@Component({
    selector: 'app-products-list',
    templateUrl: './products-list.component.html',
    styleUrls: ['./products-list.component.css'],
    providers: [{
        provide: BASE_PRODUCT_LIST_ITEM_SERVICE_TOKEN, // This is a token used to allows us to inject an interface
        useClass: ProductListItemService,
    },
    {
        provide: PRODUCTS_LIST_SERVICE_TOKEN, // This is a token used to allows us to inject an interface
        useClass: ProductsListService,
    },
    {
        provide: MODAL_SERVICE_TOKEN, // This is a token used to allows us to inject an interface
        useClass: ModalService
    },
    ]
})

export class ProductsListComponent implements OnInit {

    // CLASS MEMBERS ARE HERE

    constructor(
        @Inject(BASE_APP_CONFIG_PROVIDER_TOKEN) private appConfig: BaseAppConfigProvider,
        @Inject(BASE_PRODUCT_LIST_ITEM_SERVICE_TOKEN) private _productListItemService: BaseProductListItemService,
        @Inject(BASE_PRODUCTS_LIST_SERVICE_TOKEN) private _productsListService: BaseProductsListService
    ) {
    }

    async ngOnInit() {
        await this.initProducts();
        // Subscribe on a promise from the service that manage the rows
        // When the service upsert a row, it notify that and the component can perform action(s)
        this._productListItemService.onUpdated.subscribe(async () => {
            await this.initProducts();
        });
    }

    /**
     * Get the list of TD products linked to the btob account
     **/
    public async initProducts() {
        this.loading = true;
        // Call the service that manage the list to populate the view
        this.products = await this._productsListService.loadProducts(this.btobAccount.Id);
        this.loading = false;
    }

    /**
     * Open a dialog to confirm the activation or deactivation of a product
     * @param event
     * @param productId
     * @param newStatus
     */
    public openUpdateProductStatusModal(event: MouseEvent, productId: number, newStatus: boolean) {
        // Call service that manage the rows
        this._productListItemService.openUpdateStatusModal(event, this.products, productId, newStatus);
    }

    /**
     * Open the product modal
     * @param state The state to open the modal in. Either insert or update
     * @param product The product to update if the state is update
     */
    public openProductModal(state: Crud, product?: Product) {
        // Call service that manage the rows
        this._productListItemService.openUpsertModal(state, this.btobAccount.Id, product);
    }

    /**
     * Open the product params modal
     * @param product
     */
    public openProductParamsModal(product?: Product): void {
        // Call service that manage the rows
        this._productListItemService.openProductParamsModal(product);
    }

    /**
     * Check if a product is expired
     * @param product The product to check
     */
    public isExpiredProduct(product: Product): boolean {
        // Call service that manage the rows
        return this._productListItemService.isExpired(product);
    }

    public openCommentModal(id: number, name: string, comment: string): void {
        // Call service that manage the rows
        this._productListItemService.openUpdateCommentModal(id, name, comment);
    }
}
0 Answers
Related