0

I'm relatively new to Angular so please forgive my may be trivial question.

I've also googled a lot, found a lot of similar questions, but none really addressing my problem.

Situation

Let's assume the following component's template:

<div class="wrapper">
    <!-- Stuff -->
    .
    .
    .
    <div *ngFor="let group of groups">
        <button #btn (click)="onClick(btn, group)">Change group title</button>
        <div class="group-title">
            {{group.title | translate}}
        </div>
        .    
        .    
        .    
        <div *ngFor="let item of group.subGroup">
            {{item.title}}
            .    
            .    
            .    
        </div>
    </div>
<div/>

Now I want the user to be able to trigger a series of actions/events that eventually change the title of a single Group (single item in ngFor).

I.e.

onClick(btnRef, group) {
    .    
    .    
    .
    anAsyncronousEvent.subscribe(
        success => group.title = success.BrandNewTitle;
    )    
}

Desired outcome

The DOM should display the new title of the affected group, but it doesn't. I've also tried without the 'translate' pipe, but in vain.

By googling around, I seem to understand that a change in DOM is detected by Angular only by looking at object reference used to generate the ngFor items, so that if I change only some internal property of the Object/Item, a ChangeEvent is not triggered.

Possibile solutions

One idea could be to make a deep copy of the Group Object and replace the corresponding item in the array behind the ngFor, but that would cripple the whole mess of making Angular refresh DOM only for what is really changed. Another idea could be to use markForCheck() but here I have two choices:

1) Use markForCheck() in the clickHandler but, again, this would trigger re-rendering for all groups in the component.

2) I could somehow create a subcomponent only for the group title but it seems to me kind of an overkill, and I'm also not sure it would work.

So, how do I force Angular to refresh ONLY the group title?

Thank you all in advance

EDIT

Ok. Probably the code posted above is a bit oversimplified.

This one here is closer to my real world app and is running on stackblitz here, where it reproduces the problem I'm trying to solve.

// Template
<div class="wrapper">
    <div *ngFor="let group of groups">
        <button #btn (click)="onClick(btn, group)">Pop away item</button>
        <div class="group-title">
            Group info: count = {{group.info | format}}
        </div>
        <div *ngFor="let item of group.items">
            &nbsp;&nbsp;&nbsp;&nbsp;Item title: "{{item.title}}"
        </div>
    </div>
</div>

//Component
onClick(btn, group: any) {
  setTimeout(() => { // In the real App this is an Observable
    let i = this.groups.indexOf(group);
    group.items.pop();
    group.info.count--;
  }, 1000);
}

I'm expecting the items count to be show decreased after button click, but it is not.

  • 1
    Can you provide a minimal repro of this on stackblitz? – yurzui Jun 01 '18 at 07:19
  • 1
    @yurzui I'm working at it (stackblitz). As soon as it's ready I'll post the link –  Jun 01 '18 at 08:38
  • *but that would cripple the whole mess of making Angular refresh DOM only for what is really changed* .. this would not be the case if you use `trackBy` – Vikas Jun 01 '18 at 08:43
  • @Vikas Well, not exactly because trackBy is meant for make angular track changes not by reference to the object but by other means. In my case, however, I *do want* Angular track the reference. Suppose I generate a new object with the same title (which in my case is allowed). What should do Angular if I tell it to track by the title? It wouldn't refresh, right? –  Jun 01 '18 at 08:48
  • `ngFor` by default tracks list items using object identity if you generate new object with same title angular will pick up the changes and it would re-create DOM but with `trackBy` it would just update the DOM rather than destroying and re-creating it – Vikas Jun 01 '18 at 09:02
  • well waiting for your stackblitz – Vikas Jun 01 '18 at 09:09
  • @Vikas Ok, Vikas, I'll give a try. –  Jun 01 '18 at 09:36
  • @yurzui stackblitz here: https://angular-8rufrr.stackblitz.io –  Jun 01 '18 at 10:05
  • Pure pipes are only triggered when the input value changes. See https://stackoverflow.com/questions/39757603/unraveling-angular-2-book-chapter-1-example-5/39761822 and https://stackoverflow.com/questions/41869301/how-to-re-trigger-all-pure-pipes-on-all-component-tree-in-angular-2 – yurzui Jun 01 '18 at 10:12
  • @yurzui Ok, Yurzui, then what should I do? Actually I'm looking for a way to force/simulate an "input change" as you describe it. That's why I've thought of markForCheck(), but how? –  Jun 01 '18 at 10:16
  • https://stackblitz.com/edit/angular-ksygr2?file=src/app/groupsComponent/groups.component.ts Here i just added new parameter to the pipe `: group.info.count` so that when you change count then pipe will be reevaluated. markForCheck has nothing to do with your issue. – yurzui Jun 01 '18 at 10:19
  • @yurzui Thanks, Yurzui, it works. My pipe is a bit more complex, I had to make some other adjustment to my code but you did put me on the right path. Please, post your comment as answer and I'll mark it as the accepted one. Thanks again. –  Jun 01 '18 at 11:38

1 Answers1

0

As per your requirement if you don't want to update the entire groups[] but only want to change a particular group title in the DOM, one thing what you can do is pass the index of the group to the click event and use that to generate the ids dynamically so that u can access that id and change the HTML value when the event is triggered. Something like below

<div *ngFor="let group of groups; let i = index">
    <button #btn (click)="onClick(btn, group, i)">Change group title</button>
    <div class="group-title" id="group{{i}}">
        {{group.title | translate}}
    </div>
    .    
    .    
    .    
    <div *ngFor="let item of group.subGroup">
        {{item.title}}
        .    
        .    
        .    
    </div>
</div>

Here you can make use of that index"i" to update your groups[] and then since you have access to with unique id you can change the value in that .

onClick(btnRef, group, index) {
.    
.    
.
anAsyncronousEvent.subscribe(
    success => {
         document.getElementById(`group${index}`)
             .text(success.BrandNewTitle);
      }
    )    
  }

Alternatively you can use a component where you can pass each group data, in that case you can explicitly reload only that group-component

Avinash
  • 1,245
  • 11
  • 19
  • Thanks, Avinash. It could be a solution, but Angular is discouraging direct access to DOM. Furthermore, I'm trying to leverage template variables and pipes at the top of their potential. Your idea would be a workaround. –  Jun 01 '18 at 08:41