mgechev / aspect.js

JavaScript library for aspect-oriented programming using modern syntax.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to use aspects with dependencies in angular2 projects?

andrashatvani opened this issue · comments

Use case:

  • The logger aspect has post log messages to a REST services
  • When using plain XMLHttpRequest, then the following error will be shown:

Error: This test module uses the component RestartWidgetComponent which is using a "templateUrl", but they were never compiled. Please call "TestBed.compileComponents" before your test. in node_modules/systemjs/dist/system.src.js line 1555 > eval (line 762)

  • Therefore angular2's http service is desired to be used
  • When injecting http, then the following error occurs:

ERROR: 'Unhandled Promise rejection:', 'this.httpService is undefined'

The aspects are not instantiated, and therefore cannot use the dependency injection mechanism of Angular. Unfortunately, since the Angular DI got generated JiT or AoT there's no easy way to get access to the injector and use it inside of the aspects.

As a dirty workaround in my projects I inject the injector in the woven services and access it inside of the aspects by calling: meta.context.injector.

It's not clear but I don't see a better way to access the injector by using meta-programming and no build-time weaving, at this point.

Where do you get the context from?

It should be meta.method.context, excuse me.

Here's link to the MethodMetadata https://github.com/mgechev/aspect.js#methodmetadata.

Thanks!

I've found another way where you only have to set up the aspect once and can leave all woven classes untouched. The dependency of the aspect must then statically be set in the AppModule.

@Injectable()
export class LoggerAspect {
    public static loggerService:LoggerService;

    @beforeMethod({
        classNamePattern: /^(HttpService|ErrorComponent)/,
        methodNamePattern: /^(get|post|put|delete|handleError)/
    })
    static invokeBeforeMethod(meta:Metadata):void {
        let message:string = `method: ${meta.method.name}, args: ${meta.method.args.join(', ')}`;
        let payload:LogEntry = new LogEntry(LogEntry.Level.INFO, meta.className, message, new Date());
        LoggerAspect.loggerService.log(SystemConfiguration.Endpoint.LOGGING, JSON.stringify(payload));
    }
}

export class AppModule {
    constructor(private loggerService:LoggerService) {
        LoggerAspect.loggerService = loggerService;
    }
}

Yes, this works too. The only issue is that you're coupling the target with the aspect which is against the paradigm's philosophy.

In case this coupling is not a problem in your case, you can go this way; both approaches work and unfortunately, both approaches are not very clean.

Do you think we can close this issue?

No, you missed the point: httpService:HttpService is not a target, but simply a dependency which will be used to send the log messages to a backend service. I adapted the code to make it clean and self-explanatory. Do you think it would be of advantage for others to add this sample to the wiki as this seems to be an issue which probably many other face, too?

Yes, I missed your pointcut, I thought that the weaving target is AppModule.

Looks good! A simple example which shows how aspect.js can be combined with Angular in the README seems most helpful, and visible.

Great! Will you extend the README or should I make a PR?

Yes, PR sounds great!

commented

I've attempted to do the same, but am running into a problem when adding the @afterMethod decorator. If I add this decorator, the beforeMethod is invoked twice.

app.module.ts:33 BEFORE: AppComponent.ngOnInit()
app.module.ts:33 BEFORE: AppComponent.ngOnInit()
app.module.ts:33 BEFORE: AppComponent.isLoggedIn(Can't retrieve the CurrentUser, false)
app.module.ts:33 BEFORE: AppComponent.isLoggedIn(Can't retrieve the CurrentUser, false)
app.module.ts:41 AFTER: AppComponent.isLoggedIn returns undefined
app.module.ts:41 AFTER: AppComponent.ngOnInit returns undefined
(etc)

Here is my AppModule.ts:

@Injectable()
export class LoggerAspect {
    @beforeMethod({
        classNamePattern: /^.*/,
        methodNamePattern: /^.*/
    })
    static invokeBeforeMethod(meta:Metadata):void {
        console.log(`BEFORE: ${meta.className}.${meta.method.name}(${meta.method.args.join(', ')})`);
    }

    @afterMethod({
        classNamePattern: /^.*/,
        methodNamePattern: /^.*/
    })
    static invokeAfterMethod(meta:Metadata):void {
        console.log(`AFTER: ${meta.className}.${meta.method.name} returns ${meta.method.result}`);
    }
}

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        HttpModule,
        routing
    ],
    providers: [
        UserLoginService
    ]
    bootstrap: [AppComponent]
})
export class AppModule {
}

Here is my App.Component.ts:

import {Component, OnInit} from "@angular/core";
import {AwsUtil} from "./service/aws.service";
import {UserLoginService, CognitoUtil, LoggedInCallback} from "./service/cognito.service";
import { Wove } from "aspect.js";

@Wove()
@Component({
    selector: 'app-root',
    templateUrl: 'template/app.html'
})
export class AppComponent implements OnInit, LoggedInCallback {

    constructor(public awsUtil:AwsUtil, public userService:UserLoginService, public cognito:CognitoUtil) {
    }

    ngOnInit() {
        this.userService.isAuthenticated(this);
    }

    isLoggedIn(message: string, isLoggedIn: boolean) {
        let mythis = this;
        this.cognito.getIdToken({
            callback() {

            },
            callbackWithParam(token: any) {
                // Include the passed-in callback here as well so that it's executed downstream
                mythis.awsUtil.initAwsService(null, isLoggedIn, token);
            }
        });
    }
}

Any ideas? I've tried using the same AspectLogger class inside the demo/index.ts file in your project and it works as expected (not called twice). Thinking this may be an issue with Angular2.

I had this issue with my LoggerAspect after introducing a second method. I worked around this issue by removing the static keyword of one of them.

commented

@andrashatvani : that worked. Thanks. :)

I had a issue with my aspect where i am trying to call my servlet from @afterMethod through http call, but _http:Http is not getting initialized in aspect and getting the error,
TypeError: cannot read property 'get' of undefined

@mgechev , @andrashatvani Please help how can i call servlet from @afterMethod under LoggerAspect.