1
votes

I have the following SessionService.component.ts with a logout() method:

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

    // other session service stuff

    logout() {
        console.log('SessionService.logout');
        // do logout stuff
    }
}

The following MenuComponent.component.ts creates the menu content of the app

import { SessionService } from '../session/session.service';

@Component({
  selector: 'app-menu',
  templateUrl: './menu.component.html',
  styleUrls: ['./menu.component.css']
})
export class MenuComponent implements OnInit {
    menuItems: Array<any>;

    constructor(private sessionService: SessionService) {}

    ngOnInit() {
        menuItems = [
            // logout menu item
            {
                name: 'Logout',
                action: this.signout
            }
        ];
    }

    // method called 'signout' to make it clear what method is called when
    signout() {
        console.log('MenuComponent.signout');
        this.sessionService.logout();
    }
}

Lastly, the HTML template MenuComponent.component.html looks like this:

<mat-accordion class="example-headers-align" multi hideToggle displayMode="flat">
        <!-- App Signout -->
        <!-- THIS WORKS when (click)="signout()" is called -->
        <mat-expansion-panel class="menu-section mat-elevation-z0">
            <mat-expansion-panel-header (click)="signout()">
                <mat-panel-title fxLayout="row" fxLayoutAlign="start center">
                    <mat-icon class="menu-section-icon" svgIcon="logout"></mat-icon>
                    <span class="menu-section-title" tts="Logout"></span>
                </mat-panel-title>
            </mat-expansion-panel-header>
        </mat-expansion-panel>

        <!-- Menu Items inside ngFor loop -->
<!-- (click)="menuItem.action()" fails with the error described below, after this code block -->
        <mat-expansion-panel class="menu-section mat-elevation-z0" *ngFor="let menuItem of menuItems">
            <mat-expansion-panel-header (click)="menuItem.action()">
                <mat-panel-title fxLayout="row" fxLayoutAlign="start center">
                    <span class="menu-section-title">{{ menuItem.name }}</span>
                </mat-panel-title>
            </mat-expansion-panel-header>
</mat-accordion>

When I explicitly call the function signout() from the template, I get the expected console output which is

console.log('MenuComponent.signout');
console.log('SessionService.logout');

However, when selecting the menu item from the array, the session service is undefined and the SessionService.logout() function is never executed. I get the error

core.js:4442 ERROR TypeError: Cannot read property 'logout' of undefined at Object.signout [as action] (menu.component.ts:195) at MenuComponent_mat_expansion_panel_9_Template_mat_expansion_panel_header_click_1_listener (menu.component.html:38) at executeListenerWithErrorHandling (core.js:15225) at wrapListenerIn_markDirtyAndPreventDefault (core.js:15266) at HTMLElement. (platform-browser.js:582) at ZoneDelegate.invokeTask (zone-evergreen.js:402) at Object.onInvokeTask (core.js:27492) at ZoneDelegate.invokeTask (zone-evergreen.js:401) at Zone.runTask (zone-evergreen.js:174) at ZoneTask.invokeTask [as invoke] (zone-evergreen.js:483)

I presume it has something to do with the scope or initialisation sequence of all the components, but for the life of me I can't figure out what I need to change...

Thank you in advance :-)

2

2 Answers

2
votes

Since you calling method on menuitems, it's point to menuItems object, Use apply to point current class instance.

    <mat-expansion-panel class="menu-section mat-elevation-z0" *ngFor="let menuItem of menuItems">
        <mat-expansion-panel-header (click)="menuItem.action.apply(this)">
            <mat-panel-title fxLayout="row" fxLayoutAlign="start center">
                <span class="menu-section-title">{{ menuItem.name }}</span>
            </mat-panel-title>
        </mat-expansion-panel-header>
0
votes

When you are creating your menu items you are doing this:

ngOnInit() {
  menuItems = [{  name: 'Logout', action: this.signout }];
}

Here you are constructing a new object that has a property called name and a property called action. The action property references the function that the this.signout references and that's why whenever you call the menu menuItem.action the this is probably the new object that you've just created.

Functions/methods (without arrow functions) in javascript are not bound to the item that they are put on. They just exist in memory and the different objects/variables just keep a reference to them and all it matters is how they are called.

For example:

function test() { console.log(this.name); }
test() // will log undefined because we are calling the function without anything on the left side before the name and in the case this was the global scope the this would of been the global scope

const obj = { name: 'TEST', test }
obj.test() // will log "TEST" because we are calling the function with obj on the left side before the name so the context will be obj

const newObj = { name 'TEST 2', test: obj.test } // this is your case
newObj.test() // will log "TEST 2"

When working with classes and methods it's the same since in JS we actually don't have classes and they are only syntactic sugar:

class MyClass {
  myMethod() { console.log(this.name); }
}

underneath is just

function MyClass() {

}

// and here we are just creating a property called myMethod on the 
// prototype that is referencing this function. That doesn't mean that 
// something else can't reference the function and execute it in its own 
// context and actually it will because after all without having this 
// context changing we were not going to be able to have prototypal inheritance.

MyClass.prototype.myMethod = function() {
  console.log(this.name);
};

Of course something we will wan't to bind them in order to execute them always in the context that we want so we can use methods like bind, call and apply. Or arrow functions.

One way of fixing this is doing:

ngOnInit() {
  const action = this.signout.bind(this);
  menuItems = [{  name: 'Logout', action }];
}

or creating an signout class property that is an arrow function (which is almost the same as the first suggestion)

export class MenuComponent implements OnInit {
    signout = () => {
        console.log('MenuComponent.signout');
        this.sessionService.logout();
    }
}

there are also a few other ways but I think that these are your best options.