Angular Input value doesn't change

Viewed 3823

I have a problem. In my Angular project I have 2 components with an two-way bound parameter. The parameter is an object id. Both components use the same service which stores the list with objects. The parent contains the list where you can select the object and the child shows the selected object details. Here is the code of the parent:

<div>
  <table>
    <tr>
      <th scope="col">Title</th>
    </tr>
    <tr *ngFor="let offer of service1.findAll();"
        [style.background-color]="offer.rowClicked ? '#ed9b82' : ''"
        (click)="highlightClickedRow(offer)">
      <td>{{offer.title}}</td>
    </tr>
  </table>
</div>

<div>
  <app-detail3 [editedOfferId]="offerSelectedId" (editedOfferChanged)="offerSelectedId=$event"></app-detail3>
</div>

with the typescript:

@Component({
  selector: 'app-overview3',
  templateUrl: './overview3.component.html',
  styleUrls: ['./overview3.component.css']
})
export class Overview3Component implements OnInit {

  public offerSelectedId: number = -1;

  constructor(public service1: OffersService) { }

  ngOnInit(): void {
  }

  public highlightClickedRow(offer :Offer) {
    let offers: Offer[] = this.service1.findAll();
    for (let i = 0; i < offers.length; i++) {
      if (offers[i] != offer) {
        offers[i].rowClicked = false;
      }
      else {
        offers[i].rowClicked = true;
        this.offerSelectedId = offers[i].id;
      }
    }
  }

}

And here is the detail component:

<div id="content" *ngIf="editedOfferId != -1">
  <div id="data-table">
    <table>
      <tr>
        <th scope="row" colspan="2">Selected offer details(id = {{service.findById(editedOfferId)!.id}})</th>
      </tr>
      <tr>
        <th scope="row">Title:</th>
        <td><input type="text" id="txtTitle" (input)="checkObjectChanged()"></td>
      </tr>
      <tr>
        <th scope="row">Description:</th>
        <td><input type="text" id="txtDescription" (input)="checkObjectChanged()" value="{{service.findById(editedOfferId)!.description}}"></td>
      </tr>
      <tr>
        <th scope="row">Status:</th>
        <td>
          <select (input)="checkObjectChanged()" id="txtStatus">
            <option *ngFor="let key of keys" [ngValue]="key" [value]="status[key]" [label]="status[key]" [selected]="service.findById(editedOfferId)!.auctionStatus === key"></option>
          </select>

        </td>
      </tr>
      <tr>
        <th scope="row">Highest Bid:</th>
        <td><input id="txtHighestBid" (input)="checkObjectChanged()" value="{{service.findById(editedOfferId)!.valueHighestBid}}"></td>
      </tr>
    </table>
  </div>
</div>
<div *ngIf="editedOfferId == -1">
  <label  id="lblNothingSelected">Nothing has been selected yet</label>
</div>

With the typescript:

@Component({
  selector: 'app-detail3',
  templateUrl: './detail3.component.html',
  styleUrls: ['./detail3.component.css']
})
export class Detail3Component implements OnInit {

  @Input() editedOfferId: number = -1;
  @Output() editedOfferChanged = new EventEmitter<number>();

  public selectedOffer: Offer = new Offer("", "", new Date(), AuctionStatus.NEW, 0);

  status = AuctionStatus
  keys: Array<number>;

  constructor(public service: OffersService) {
    this.keys = Object.keys(this.status).filter(k => !isNaN(Number(k))).map(Number);
    this.selectedOffer = service.findById(this.editedOfferId)!;
  }

  ngOnInit(): void {
  }

}

Now when I click on an object in the parent component, the details will be loaded in de detail component, but when I edit for example the title input field and then change the object in the parent, I would expect that the data of the new selected object will be loaded. This happens, but only the fields that were not edited at that moment, so when I edit the title, everything will be loaded correctly, but the value of the title will remain the same. Even tough the object has a different title, the value that I was typing stays in the input field. Why is this happening and how can I fix this?

4 Answers

First of all , you did not implement your save changes business logic of your details component , so , when you make some changes in your object , all changes you made will not save then will not be loaded in your parent component using service1. so recommend implementing a save change service request to apply changes .

to do that exactly how you want by your code logic , Use one of those solutions:

Solution 1:

Create an update method for each offer field in your OffersService.

class OffersService {
    ...
    updateTitle(offerId:number,newTitle:string){ ... }
    updateDescription(offerId,newDesc:string){ ... }
    ....
}

In your edit component use onFocuseOut event ( angular focus Out event depend to your angular version ) to each control field , example for title:

<tr>
    <th scope="row">Title:</th>
    <td><input type="text" id="txtTitle" (focusout)="service1.updateTitle(editedOfferId,$event.target.value)"></td>
</tr>

please do the same thing for each other fields and make sure your update.. methods save offer values as well .

Solution 2:

  • create a service method in service1 that saves all changes of a specific Offer object. example:

    class OffersService { ... updateOfferById(id:number,newOffer:Offer) { ... } ... }

make submit changes button in your details component .html , and in its handler implement change detection and call :

service1.updateOfferById(editedOfferId,changedOffer).subscribe( ... );

the difference between the two methods , the first apply changes without using user action to do like submit change button, the input focusOut event do that for each field instead of saving all changes at once .

Instead of using the offerSelectedId in your Overview3Component, change your variable to store the entire object reference.

So instead of public offerSelectedId: number = -1;, use something like public offerSelected: Offer = new Offer(...)

Then set the selected item (this.offerSelected = offers[i];) and pass the reference (offerSelected) into the your child component Detail3Component.

@Component({
  selector: 'app-overview3',
  templateUrl: './overview3.component.html',
  styleUrls: ['./overview3.component.css']
})
export class Overview3Component implements OnInit {

  public offerSelected: Offer = new Offer("", "", new Date(), AuctionStatus.NEW, 0);

  allOffers: Offer[] = [];

  constructor(public service1: OffersService) { }

  ngOnInit(): void {
      this.allOffers = this.service1.findAll();
  }

  public highlightClickedRow(offer :Offer) {
    for (let i = 0; i < this.allOffers.length; i++) {
      if (offers[i] != offer) {
        offers[i].rowClicked = false;
      }
      else {
        offers[i].rowClicked = true;
        this.offerSelected = offers[i]; // set with entire object 
      }
    }
  }

}

Remember to make the required changes in the child component as well, changing from just the object ID @Input() editedOfferId: number = -1; to the object @Input() selectedOffer: Offer;.

@Component({
  selector: 'app-detail3',
  templateUrl: './detail3.component.html',
  styleUrls: ['./detail3.component.css']
})
export class Detail3Component implements OnInit {

  // NOTE: This object would be binded back to the source
  @Input() selectedOffer: Offer = new Offer("", "", new Date(), AuctionStatus.NEW, 0);

  constructor() {
  }

  ngOnInit(): void {
  }

}

Passing the entire object reference like this will allow you to change the values and have them reflected in the parent, in this case the allOffers array.

There should be no need for the EventEmitter @Output() editedOfferChanged = new EventEmitter<number>(); unless you want to know if they changed something.

Just note that this will only update the data in the current state and will not be saved on refresh unless you implement some saving logic. Also this is by no means the best solution but it is a solution.

You've not implemented save changes logic in the detail3 component and values are not linked to any object or variable. It can be done in multiple ways. You should pass an offer object to the child component instead of an id so that you don't have to find selected offer from all the offers. Bind selected offer object to your forms using ngModel or use reactive forms.

Since data passed to child component is object and any change in object will be reflected to main array (you can prevent that by using spread operator when passing value to child component if you don't want these change to be reflected to main array and fire an event to manually to save data on change).

I would prefer reactive forms and submit/save button to save change. I've implemented an example in stackblitz.

"the value of the title will remain the same"

<tr>
        <th scope="row">Title:</th>
        <td><input type="text" id="txtTitle" (input)="checkObjectChanged()"></td>
      </tr>

Your child component don't assign value for title.

value="{{service.findById(editedOfferId)!.title}}"

It seem the parent component doesn't reload the list offers after child component already updated it.

I suggest you change the function:

(editedOfferChanged)="offerSelectedId=$event"

To

(editedOfferChanged)="onEditedOffer($event)"

For easily debug and do few tasks at parent component after offer was updated at child component.

For Example

onEditedOffer(event){
    console.log('onEditedOffer',event;
    offerSelectedId = event;
    //do something to update list or single offer.
    //i choose easy way "reload list offers"
    offers: Offer[] = this.service1.findAll();
}
Related