0

I am working on Angular project (Angular 14+) and following atomic design principles (creating minute level components like button, input, toggle etc.).

Most of the views (list, forms, headers) are based on JSON which is coming from server (telling what and where of components). Server also send function enums to execute a particular functionality. for e.g. on a button click navigate to a certain screen, or open a particular modal and take user's action and hit an API, etc. These functions (JSON based function enums) are distributed across the application (can present at any level of nesting).

So for simplicity I opted for two approaches:

  1. TypeScript class based functions (component models)
  2. Pure functions (simple import/export)

Now when writing these functions, need arises to access some of the services and third party components like Modal reference, snackbar reference, router reference or HTTP service.

For this to avoid Angular DI or other framework code, I took global reference of angular injector (injector() function) and created a global singleton TS class with set of properties for HTTP, modal, snackbar, router ( i.e. HTTPRef, modalRef, snackbarRef, routingRef) and assigned them required services or component references.

Now I am able to use them in simple classes or inside pure functions like e.g.

// pure function example:
const openModal<T>(componentRef: ComponentType<T>, componentData?: any, dialogConfig?: MatDialogConfig, closeCB?: FunctionCall){
const dialogRef = globalConfig.matDialogRef; // singelton class ref
    const openDialogRef = dialogRef.open(component,{
    data: componentData
  });
  return { dialogRef, openDialogRef };
}

Similarly other functions global dependencies, it helped me to call in TS normal classes (component Models).

Creation of these global Ref is as follows:

AppModule ---> InitializerModule --> calling TS class methods

e.g.

<!-- InitializerModule -->
// all imports
export let NGInjectorInstance: Injector; ----- [A]
// some more code
export class InitializerModule {
  constructor(private injector: Injector) {
    NGInjectorInstance = this.injector; // injector reference
    CLIENTS_INIT(); // calling classes init functions
    GLOBAL_INIT(); // same here, will use global injector inside these classes to assign references 
  }
}

Classes:

GLOBAL_INIT() ---> /src/models/config/index.ts

// index.ts 
import { default as GlobalConfig } from "./global.config"; // singelton class
export const globalConfig = GlobalConfig;

export default () => {
    globalConfig.initSnackBarDeps();
    globalConfig.initMatDialogDeps();
    globalConfig.initRouterDeps();
    globalConfig.initHTTPDeps();
    globalConfig.initStoreDeps();
}


public initMatDialogDeps(){
        this.matDialogRef =  NGInjectorInstance.get<MatDialog>(MatDialog);
}

I read here that injecting service to a standalone class is an anti-pattern and here that injecting service in a class is considered anti-pattern. So even if it's how should I validate it how much I can deviate/bend to follow this approach (I also agree how much is very subjective and require more context). Is it something to be discarded as a whole or there is still room to follow this?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Monis
  • 165
  • 2
  • 12

1 Answers1

-1

Why are you trying to avoid DI when you need to use it?

DI is probably about the most widely used pattern around at the moment due to it's numerous benefits. Global static stuff has been out the window for years now as it makes testing a pain in the backside.

You don't have to use DI, certainly, and you don't have to use Angular's DI, for that matter. If you just have the habit of injecting your dependencies, it doesn't matter who fulfils those, so you can always do it yourself; then that class can be used within a component or via other means, and it can then be suitably mocked and tested, etc.

In your first link, it specifically talks about the framework fulfilling the creation of components and services and filling the dependencies, but you can do that yourself if you're creating the instances.

Somewhere along the way, as you've found out, you need the services that come from somewhere and make use of them. And then, to top it off, you need to use those in your components at the end of the day - another place you have the ability to access those services.

Right now, you're bending over backwards to avoid... something, and making it harder for yourself.

One option, to simplify things, is that you could group it all up and have a FunctionProvider service.

This service would have all the necessaries injected into it, and it can return the finished product (function) for consumers to use. Consumers would only ever need to inject this FunctionProvider without worrying about everything else it entails under the hood (because DI sorts that for you).

And you can then pass that into your 'standalone' classes for them to use - the provider was supplied by Angular DI into your component, but your component must surely then be taking ownership of creating and using your standalone classes - so it can pass it along for it to make use of.

Basic version might look like:

@Injectable()
export class FunctionProvider {
    constructor(private readonly http: HttpClient,
                private readonly matDialog: MatDialog) {}

    public get(fnType: FunctionTypeEnum): any {
        switch (fnType) {
            case FunctionTypeEnum.OpenDialog:
                return this.openDialogFn();
        }
    }

    private openDialogFn(): any {
        return (component, data) => {
            return this.matDialog.open(component, {data});
        };
    }
}

Class might get fairly large at some point, you'd probably move each function provider to their own helper class or something - and your choice how you provide the services (stick with constructor, or pass in to the method itself)...

export class OpenDialogFnProvider {
    constructor(private readonly matDialog: MatDialog) {}

    public get(): (component, data) => Observable<Bleh> {
        return (component, data) => this.matDialog.open(component, {data});
    }
}

So your switch entry becomes you creating the class:

case FunctionTypeEnum.OpenDialog:
    return new OpenDialogFnProvider(this.matDialog).get();

And then using it:

export class SomeComponent {
    constructor(private readonly fnProvider: FunctionProvider) {}

    public onClick(): void {
        const fn = this.fnProvider.get(FunctionTypeEnum.OpenDialog);
        fn(SomeDialogComponent, {title: 'test'});
    }
}

After all of this, regardless, you have a function that you can pass around however you'd like for anything to use. Component gets it and then passes it into a pure class that maybe does some processing - functions can be passed around just like any other variable, so I'm not sure you are saving any effort or generally making life any easier for anyone by trying to avoid using the available mechanisms.

Maybe I just haven't understood your question properly and what you're trying to do.

Krenom
  • 1,894
  • 1
  • 13
  • 20