Home > Mobile >  Angular manually set @Input value is still overwritten with the same value in ngOnChanges
Angular manually set @Input value is still overwritten with the same value in ngOnChanges

Time:09-28

I have a parent component employees.component.ts and a child component employee.component.ts. The parent's template is divided into two parts - 20% of the width is a PrimeNG tree component (its nodes represent different employees) and the rest is for the child. The child's template is a form where you can change the selected employee's data or add a new employee. When selecting a node from the tree, the id of the employee is sent from the parent to the child as an input. The input change in the child component triggers ngOnChanges in which a HTTP request is made for the employee's data.

Now when I try to save a new employee, I set that input to undefined to indicate that currently an empty form is shown so that a new employee can be created. After saving the new employee, I get the created employee from the response. I set the input equal to the new employee's id and fill the form with its data.

Also after creating the new employee, I emit an output to the parent to inform it, that a new employee node must be added to the tree to represent this new employee and select it as the form is filled with the employee's data. The problem is that when the parent sets the node as selected, the id of the employee is sent as an input to the child (which currently is undefined after the saving process) which again triggers ngOnChanges and makes an HTTP request, but as I got the data already from the POST request's response, I do not want to make this new request. To avoid it, I set the input manually right after I get back the response. However, even though now the input's value is correctly the id of the employee that is selected in the tree, the ngOnChanges is still triggered. When I log the changes: SimpleChanges object to the console, I see that the input's previous value is shown as undefined and its current value is the correct id.

How come the previous value is shown as undefined even though I had set it before anything else (the output to the parent)? What can I do so that the ngOnChanges is not triggered?

CodePudding user response:

Overriding an @Input property inside a component is an anti-pattern, because it leads to all kinds of difficult to trace, spaghetti, information flows trough your application.

To get around this issue, you could add an intermediary class member of type BehaviorSubject, such as in the (simplified) example below.

TS

@Component({ .. })
export class MyComponent implements OnChanges {
  /** single source of truth of your employeeId */
  private readonly employeeId_ = new BehaviorSubject<string>("");

  /** expose observable, that can be used in html with simple async pipe for efficient change detection */
  public readonly employeeId$ = this.employeeId_.asObservable();

  /** primary input, will detect changes from upstream parent */
  @Input() public employeeId(newValue: string) {
    this.setEmployeeIdValue(newValue);
  }

  /** lifecycle hook that detects changes from upstream and propagates them internally */
  ngOnChanges(changes: SimpleChanges) {
    if (changes.employeeId) {
      this.employeeId_.next(changes.employeeId.currentValue);
    }
  }

  /** method to synchronously evaluate latest value */
  public getEmployeeIdValue(): string {
    return this.employeeId_.getValue();
  }

  /** call from your html or from this component's code to handle your business/ui logic */
  public setEmployeeIdValue(newValue: string) {
    this.employeeId_.next(newValue);
  }
}

HTML

<div>
  {{ employeeId$ | async }}
</div>

Read more about BehaviorSubject in the RxJS docs.

CodePudding user response:

You have two questions:

  1. Why is previous value undefined
  2. How not to trigger call in your ngOnChanges, right after new employee is created

Why is previous value undefined

The behavior you are describing is correct.

When creating new employee, there is no employee ID (undefined), then after filling the form you do a call to create new employee and receive employee's ID back in the response.

If you emit new ID to parent component, which will set it to your child's @Input, then ngOnChanges will be called and you will correctly have previousValue set to undefined and currentValue set to new ID, this is how SimpleChange is supposed to work.

How not to trigger call in your ngOnChanges, right after new employee is created

To avoid making a call for an employee you've just created you can add additional check in ngOnChanges. If the ID you received from parent is the same as the ID of last created employee, just skip the load call:

@Component({
    selector: 'app-employee',
    templateUrl: './app-employee.component.html',
    styleUrls: ['./app-employee.component.scss']
})
export class EmployeeComponent implements OnChanges {
    @Input() 
    selectedEmployeeId: string;

    @Output()
    createdEmployee = new EventEmitter<string>();

    private createdEmployeeId: string;

    constructor(private http: HttpClient) {}

    ngOnChanges(changes: SimpleChanges): void {
        if (typeof changes.selectedEmployeeId.currentValue === 'undefined') {
            // there is no ID, cannot load employee data
            return;
        }

        if (changes.selectedEmployeeId.currentValue === this.createdEmployeeId) {
            // skip the call and reset createdEmployeeId to null
            this.createdEmployeeId = null;
        } else {
            // do a call to load selectedEmployeeId data
            this.http
                .get(...)
                .subscribe(...);
        }
    }

    createEmployee(): void {
        this.http
            .post(...)
            .subscribe(
                employeeId => {
                    this.createdEmployeeId = employeeId;
                    this.createdEmployee.next(employeeId);
                }
            )
    }
}
  • Related