Ionic - Can't change DOM

Hi All,

When I change an HTMLElement in the DOM, the change is not reflected.

I have an html img element:

<img class="selected" id="icon-image-{{item.id}}" src="{{item.icon}}" height="75" width="75" />

and some sccs:

img.selected {
    -webkit-filter: grayscale(100%); /* Chrome, Safari, Opera */
    filter: grayscale(100%);
}

When this loads, it applies the selected class, and makes the image grey as expected.

However, when I run the following typescrpt the image just stays grey:

let imgEl: HTMLElement = document.getElementById('icon-image-' + subCategoryModel.id);
imgEl.className = "";

When I log the className, it shows selected is there before, and has been removed after.

console.log(imgEl.classList);

Any ideas why, even though I remove a className, the style is not reflected?

When I look at the source code, it is not updating the DOM, but imgEl.classList is being changed. Do you know why?

Thanks

Something strange, if I change the className in the DOM manually (in Firebug for example), it reflects the change. However, as soon as I click the image, it updates to what the original value was. It is as if the DOM is being refreshed.

If this is the case, then how is it possible to update the DOM dynamically? Because the DOM refresh just overwrites all changes.

Html

  <ion-row *ngFor="let trio of getTriples()">
    <ion-col *ngFor="let item of trio" (click)="itemTapped(item)">
      <center>
      <div class="row responsive-md">
        <img class="selected" id="icon-image-{{item.id}}" src="{{item.icon}}" height="75" width="75" />
      </div>
      </center>
    </ion-col>
  </ion-row>

Typescript

  itemTapped(subCategoryModel: SubCategoryModel) {
    let idx: number = this.isInArray(this.employeeModel.subCategories, subCategoryModel);
    let imgEl: HTMLElement = document.getElementById('icon-image-' + subCategoryModel.id);
    if (idx >= 0) {
      this.employeeModel.subCategories.splice(idx, 1);
      imgEl.setAttribute("class", "");
    } else {
      this.employeeModel.subCategories.push(subCategoryModel);
      imgEl.setAttribute("class", "selected");
    }
    console.log('icon-image-'+subCategoryModel.id+' ('+imgEl.id+'): ' + document.getElementById('icon-image-' + subCategoryModel.id).className);
  }

Is this a scope issue, am I updating the wrong document? What’s the answer, I’m confused. Appreciate any help. I do a simplified version in jsfiddle and it works, so I think this maybe has something to do with Ionic2?

I did try this with no success:

changeDetection: ChangeDetectionStrategy.OnPush

Also tried:

setTimeout( () => {
        imgEl.setAttribute("class", "");
        },500);

And also tried returning the function as a Promise.

How do I do the following in Ionic 2?

$scope.$apply()

Here is something that says in Angular you can’t manipulate the DOM. What are you supposed to do then if you want to make style changes dynamically?

It says that one should change the scope. How do you do that? Does that mean you need duplicate objects? If so, is that not bad practice?

What is the best solution?

This discussion suggests, to use NgZone? It also talks about additional plugins.

I try the following with no success:

   this.zone.run(() => {
     imgEl.setAttribute("class", "");
   });

I have also tried ChangeDetectorRef detectChanges().

Any advise appreciated.

Probably same problem here. :disappointed_relieved:

Hi Moscardini, it may be similar. However, I did try NgZone with no success. Not sure if it will work for you, but look at my previous comment.

From what I have been reading it looks like Angular doesn’t allow DOM manipulation. Seems like a serious limitation, and I can’t seem to find a solution as yet.

Here is a similar issue too.

Let give it a try.
If works, tell me something :relaxed:
I’m trying it too now.

Hi Moscardini, looks like he suggests to use NgZone.

this.ngZone.run(() => {...});

I’ve tried that with no success, but hope it works for you.

Hi,

tl;dr - I think [class.active] is what you’re after

If you’re just looking to add a class on a condition then I use something like the following in Ionic 2 -

item.html

<ion-item class="item-stable" (click)="toggleItem(item)" [class.active]="itemShown === item"> {{item.name}} </ion-item>

item.ts

...
this.itemShown = null;
isItemShown(item) {
    return this.itemShown === item;
}
toggleItem(item) {
  if (this.isItemShown(item)) {
      this.itemShown = null;
    } else {
      this.itemShown = item;
    }
}
...

To breakdown what’s happening here -

  1. A list of items is created on the DOM, initially no item is ‘active’ (itemShown is null)
  2. If an item is clicked the toggleItem() function is called with the item object as a parameter
  3. The toggleItem() function checks if the item is currently active, if not it sets itemShown to the item, if currently active it sets itemShown to null
  4. The [class.item]=“itemShown === item” sets the active class on the DOM element if the itemShown variable is set to that DOM elements item

I’m not sure if this is what you’re looking for or if my explanation has made it any clearer. Let me know if you need more info.

1 Like

ngZone work like a charm for me!
Maybe you are doing something not right, that’s my code.
You can check old code in my topic.

import { Component, NgZone } from '@angular/core';
import { NavController, Alert, reorderArray} from 'ionic-angular';

declare var SpeechRecognition: any;

@Component({
  templateUrl: 'build/pages/notes/notes.html',
})
export class NotesPage {
  _zone: any;

  recognition: any;
  listaSpesa: any = [];

  constructor(private nav: NavController, _zone: NgZone) {
        this._zone = _zone;
    
        this.recognition = new SpeechRecognition();
        this.recognition.lang = 'it-IT';
        this.recognition.onresult = (event => {
          if (event.results.length > 0) {          
              console.log('--> risultati: ', event.results[0][0].transcript);
              this._zone.run(() => this.listaSpesa.push({alimento: event.results[0][0].transcript}));
          }  
          console.log('--> SpeechRecognition: listening END'); 
        });
  }

    addNoteMic() {
        console.log('--> SpeechRecognition: listening');
        this.recognition.start();
    }
1 Like

Hi Moscardini,

I’m glad it works for you.

I have tried it,

this._zone.run(() => { imgEl.setAttribute("class", ""); });

with no success unfortunately. Unless you van see what I am doing incorrectly?

jjmax has suggested a toggle function above which I am going to try too.

2 Likes

Hi jjmax,

I understand that you are showing a toggle switch, but I don’t understand how you get it to update the className on the HTMLElement (img) to the item-selected class?

I have the following code:

scss

img.item-selected {
    -webkit-filter: grayscale(100%); /* Chrome, Safari, Opera */
    filter: grayscale(100%);
}

img.item-stable {
    
}

html

  <ion-row *ngFor="let trio of getTriples()">
    <ion-col *ngFor="let item of trio">
      <center>
        <div class="row responsive-md">
          <img class="item-stable" (click)="toggleItem(item)" [class.active]="itemShown === item" id="icon-image-{{item.id}}" src="{{item.icon}}" height="75" width="75" />
        </div>
      </center>
    </ion-col>
  </ion-row>

ts

isItemShown(item) {
  return this.itemShown === item;
}
toggleItem(item) {
  if (this.isItemShown(item)) {
    this.itemShown = null;
  } else {
    this.itemShown = item;
  }
  console.log('toggleItem: '+item+': '+this.itemShown);
}

quick fix I think :slight_smile:

Just change [class.active] to [class.item-selected]

The bit after [class. is the class that will be added to your element

1 Like

Also, have you created a variable in the ts file?

It would be something like this:

itemShown: any;

.....
(in constructor)
this.itemShown = itemShown;

Thank you jjmax, that works!! I was missing the[class.item-selected].

Thank you for your help, I was really struggling.

:grin:

No problem, it had me a few days ago too