1

Currently trying to figure out why this happens, but if I have an optionnal function binding (&?) on a NG1 component then NG2 will put a EventEmitter on it :/

This is not what I expect and breaks some code we do when these are normally null/undefined.

Here is a example of what we have :

class MyController {
  public onInit: () => string;
  public onAction: () => void;

  public resultFromNg2: string;
  public onActionHasValue: boolean;
  public typeOfOnAction: string;

  public $onInit() {
    this.resultFromNg2 = this.onInit();
    this.onActionHasValue = this.onAction != null;
    this.typeOfOnAction = typeof this.onAction;
  }
}

export const ng1DemoComponent = {
  selector: "ng1-demo",
  template: `
    <div>Hello, {{ $ctrl.username }}!</div>
    <div>resultFromNg2: {{ $ctrl.resultFromNg2 }}</div>
    <div>onActionHasValue: {{ $ctrl.onActionHasValue }} - typeof: {{ $ctrl.typeOfOnAction }}</div>
  `,
  bindings: {
    username: "<",
    onInit: "&?",
    onAction: "&?"
  },
  controller: MyController
};

@Directive({
  selector: ng1DemoComponent.selector
})
export class Ng1DemoComponentFacade extends UpgradeComponent {
  @Input() username: string;
  @Input() onInit: () => string;
  @Input() onAction: () => void;

  constructor(elementRef: ElementRef, injector: Injector) {
    super(ng1DemoComponent.selector, elementRef, injector);
  }
}

Testable environment : https://stackblitz.com/edit/angular-hybrid-upgrade-dpjv4p?file=ng1%2Fng1-demo.component.ts

In this code, I expect to have onAction set to undefined so I can assign my other property properly, but currently it's not working :/

I also tried removing the ?, so from &? to &, but now it's NG1 who gives probably angular.noop ..

Any ideas on how I can make this work without rewriting stuff ?

Thanks a lot of any help given !

Note: We want to keep a part of our codebase in NG1, because it's not feasable for us to convert all of it right now with the codebase we have.

1 Answers1

1

Angular's UpgradeComponent constructor calls this:

private initializeOutputs() {
  // Initialize the outputs for `=` and `&` bindings
  this.bindings.twoWayBoundProperties.concat(this.bindings.expressionBoundProperties)
      .forEach(propName => {
        const outputName = this.bindings.propertyToOutputMap[propName];
        (this as any)[outputName] = new EventEmitter(); // <--- sets it to an EventEmitter
      });
}

which sets each & binding defined on your bindings object to an EventEmitter even if (unfortunately for you) the binding is optional and not currently assigned.

One thing you can do, though, is use the $onChanges() lifecycle hook in your NG1 component controller to help you identify which bindings have been assigned and which have not been.

This hook fires before $onInit() and gets called with an object that holds the changes of all one-way bindings with the currentValue and the previousValue.

This means that the object has a property only for each binding that's been assigned a value. So in your case, that "changes" object will have username and onInit properties but will not have an onAction property.

There are different ways you can leverage that, of course, but one simple approach would be like so:

Create an assignedBindings member on your controller and, in $onChanges(), assign it the changes object that gets passed in:

// ng1DemoComponent MyController

assignedBindings;

public $onChanges(changes) {
  this.assignedBindings = changes;
}

So this.assignedBindings will look like this:

{
  onInit: { previousValue: undefined, /* ... */ }, // <-- SimpleChange instance
  username: { previousValue: undefined, /* ... */ } // <-- SimpleChange instance
}

Then update $onInit() to check any of the bound properties that you would only want to be set to EventEmitters if they were actually assigned, and set the properties to null if they're not assigned:

// ng1DemoComponent MyController

public $onInit() {
  this.resultFromNg2 = this.onInit();

  if (!this.assignedBindings.onInit) { // <--- verify onInit assigned
    this.onInit = null;
  }

  if (!this.assignedBindings.onAction) { // <--- verify onAction assigned
    this.onAction = null;
  }

  this.onActionHasValue = this.onAction != null;
  this.typeOfOnAction = typeof this.onAction;
}

You might need to go further and check the currentValue of each of those SimpleChange instances if they could change over time, but you get the idea.

Here's a fork of your StackBlitz showing this approach. It might be a bit hacky but it seems to work.

Thanks for creating that StackBlitz, by the way—that helped tremendously in trying to look into this.

MikeJ
  • 2,179
  • 13
  • 16
  • Thanks for the elaborated answer on this ! The thing I don't understand through is that they put an `new EventEmitter` for every `&` bindings, however my `onInit` binding that has a return value works which normaly would not work with a `EventEmitter` so I'm kinda intrigued. – MLefebvreICO Apr 12 '21 at 15:07