Ion-toggle (ionChange) triggered

I’m going a bit crazy, I can’t figure out why the (ionChange) event of an ion-toggle I’m using is triggered without any interaction respectively is triggered when I navigate to the page

I know without code not possible to tell, I tried to reproduce the problem with a sample app, no chance

So I just wanted to ask if someone may have faced such a problem once and maybe do remember how he/she solved it?

Thx in advance for any tips

 <ion-toggle [(ngModel)]="booleanValue" (ionChange)="crazyEvent($event)"></ion-toggle>

This has been a fairly persistent and stubborn bug dating all the way back to v2 beta. The first thing that jumps out at me is the two-way model binding. If you take that out, specifically in the box direction (from controller to template), does anything change? I realize that’s not really necessarily a viable workaround because it eliminates what may be crucial functionality, but it may at least help narrow down the issue.

Another tactic you can use to work around this class of problem is to put guards around crazyEvent so that it ignores anything where the template and controller are already in sync, a la @jasonwaters’s suggestion in here.

@rapropos you are totally right, it comes from the two-way model

after I read your lines I double checked my v4 app vs my v3 and, don’t know why, I had the good idea to change that during the migration.

v3 - no ngModel - works fine
v4 - why I changed it no idea, add ngModel - problem

well I gonna have a deeper look tomorrow, and also double check all other toggle I have modified but that’s definitely my problem, when the first binding of ngModel is done, the ionChange is called, that’s life :wink:

thx a lot for putting me on the right track :+1:

For those landing on this

In v3 I had

<ion-toggle checked={{myValue}} (ionChange)="myChange($event)"/>

 myChange($event) {
     this.myValue = !this.value;
 }

unfortunately this in my case couldn’t be used in v4 anymore because in some situation the ionChange is triggered when the model is attached which will cause an ExpressionChangedAfterItHasBeenCheckedError and and endless loop (!) because the change will be called when the value is modified

I try then in v4 to solve that like the following

 <ion-toggle [(ngModel)]="myValue" (ionChange)="myChange($event)"/>

which works great except the fact that ionChange will be triggered once or twice (yep sometime twice in my case) when the model is binded

so I guess I’ve to find a workaround, right now I’m thinking about detecting when my component is effectively loaded (afterContentChecked from angular), will test that right now … well no that doesn’t work

1 Like

ok three things

firstly, I just noticed that this behavior doesn’t happens all the time (?) if the app is cached I might not face it, if I restart my browser and server, I face it…don’t ask

secondly, like I said, I was unable to reproduce it in a simple app

finally it happens right in the middle of my wizard (too much components)

therefor I just went the workaround way which is to find a way to detect if the view is really active for your case

in my case, the problem happens inside a page inside a slide inside a component so the tricks was too pass the slide index and to check if I reached it or not (because the error was thrower on load of the components), something like

myChange($event) {
   if (this.thisSlideIsNotTheActiveOne()) {
     return;
   }
   
   // do stuffs
}

don’t judge me :wink:

I had the same problem and i fixed it by using also checked attribute.

<ion-toggle [(ngModel)]="myValue" [checked]="myValue" (ionChange)="myChange($event)"/>

This means that ionChange wont trigger when the model is initialized.

4 Likes

I have just been having exactly the same problem when using “ngModelChange” in Ionic 5, sometimes my ngModelChange event would fire when the page loaded, sometimes it wouldn’t, very strange, adding the “checked” attribute seems to have solved this as follows:

<ion-toggle [(ngModel)]="attribute.Value" [checked]="attribute.Value" ngModelChange)="attributeChange(attribute)" ></ion-toggle>

I would suggest going the exact opposite direction from the one you took. You added yet another conflicting binding, so there are now two things fighting over box and two things fighting over banana. You only want at most one of each.

[(ngModel)] covers both directions, so don’t mix it with anything.
[ngModel] and [checked] fight over box: choose only one.
(ngModel) and (ngModelChange) fight over banana: choose one or the other.

Sorry, I am a little confused by this, so what should I use instead? Can you give me a full example? I was using [(ngModel)] and (ionChange) on Ionic 3 but this didn’t work properly on Ionic 4/5 (it gave me this exact problem where the change event was getting called on initial binding) hence why I switched to using (ngModelChange) which seemed to fix the problem for everything apart from the ion-toggle element.

I have other elements that are set up like this (see example below), are you saying these are wrong as well? Just to be clear I am using [(ngModel)] for setting the value and passing back any changes to the model. The (ngModelChange) is used to catch the event of when someone changes the value, this event does not change the “value” on the model at all, it is used for updating some other varialbles (for clarity I have added the “attributeChange” event below so you can see what I mean):

<ion-input [(ngModel)]="attribute.Value" (ngModelChange)="attributeChange(attribute)"></ion-input>

attributeChange(attribute) {

    // Set attribute to dirty
    attribute.IsDirty = true;

    // Set job as edited
    this.job.Edited = true;

  }

The [(ngModel)] “banana-in-a-box” binding is effectively shorthand. Behind the scenes,

[(ngModel)]="foo"

=

[ngModel]="foo" (ngModelChange)="foo = $event"

So, when you say:

…as long as the bold part is true, you have a slightly inefficient but not overly problematic situation. I still think it’s clearer and safer to have only one binding, because it makes everything explicit. This extends to reactive form bindings like [formControl] or [formControlName] (which you might want to look into if you’re really doing things like dirty tracking in (ngModelChange) handlers, because Reactive Forms are designed with all that in mind). So you could write what you have as:

<ion-input [ngModel]="attribute.value" (ngModelChange)="attributeChange(attribute, $event)">
attributeChange(attr: Attribute, evt: string) {
  attr.value = evt;
  attr.isDirty = true;
  this.job.edited = true;
}

Thank you for the explanation, I have done some research and what you said makes sense now, there does seem to be conflicting views on whether you can/should use “banana-in-a-box” along with (ngModelChange) but I agree that I would rather keep things simple and only use one binding if possible.

I am using reactive form bindings in other parts of my app but this part is fairly unique where all the fields are dynamic, I effectively have an any[] array that is passed to the page which can contain any number of fields and they can be different types of fields from ion-input through to multiple ion-select, each with different types of data (i.e. string, int, array of ints), I have omitted a lot of this extra code from my examples above for simplicity. The “IsDirty” values are not used on the form as such but used to determine which dynamic fields to update in my API calls when saving. However, this is probably a separate conversation.

I changed my code to the following as per your suggestion but I am back to the original problem whereby intermittently the ion-toggle will fire the “attributeChange” method on the initial page load:

<ion-toggle [ngModel]="attribute.Value"  (ngModelChange)="attributeChange(attribute, $event)"></ion-toggle>

attributeChange(attribute, event: string = null) {

    if (event !== null) {
      // Set attribute value
      attribute.Value = event;
    }

    // Set attribute to dirty
    attribute.IsDirty = true;

    // Set job as edited
    this.job.Edited = true;
}

I cannot switch to the following because the “ngModelChange” will not fire without an “ngModel”:

<ion-toggle [checked]="attribute.Value" (ngModelChange)="attributeChange(attribute, $event)"></ion-toggle>

So I am not sure where to go with this, as per my earlier post the only thing that seems to resolve this is having both “ngModel” and “checked” attributes together also with “ngModelChange” but like you explained this doesn’t make any sense and is not something that should really be used.

1 Like

If you’ve read many of my posts here, you know my feelings about any in app code (I absolutely hate it for both readability and stupid-bug-enabling reasons, and have yet to run into a situation aside from “some external library uses it” where I couldn’t avoid using it), but carrying on to the topic at hand,

You’re probably already using something along these lines, but in case it’s of any use to you, here’s the design pattern I have used to implement this idiom for a markdown renderer.

Apologies if you’ve already done this, but if not, please click through the second link in my first post in the thread - @jasonwaters had an idea that I was able to use to implement <ion-toggle>s that require an intermediate action between when the user tries to slide them and when the toggling actually happens. I have a feeling that the same “event-filtering” idea might help you ignore calls to attributeChange that you don’t want to take.

Yes that makes sense, I have started converting anything that uses “any” to instead use an “interface” throughout the App, unfortunately, this is one of the parts that I haven’t got around to changing yet.

Thanks, yes something similar as follows:

    <div *ngFor="let attribute of customAttributes">
      <ion-item lines="none">
        <ion-label>{{attribute.Name}}</ion-label>
        <ion-input [(ngModel)]="attribute.Value" (ngModelChange)="attributeChange(attribute)" *ngIf="attribute.DataType == 1"></ion-input>
        <ion-textarea rows="10" [(ngModel)]="attribute.Value" (ngModelChange)="attributeChange(attribute)" *ngIf="attribute.DataType == 2" [placeholder]="'Type ' + attribute.Name + ' here...'"></ion-textarea>
        <ion-input type="number" [(ngModel)]="attribute.Value" (ngModelChange)="attributeChange(attribute)" *ngIf="attribute.DataType == 3"></ion-input>
        <ion-toggle [ngModel]="attribute.Value" (ngModelChange)="attributeChange(attribute, $event)" *ngIf="attribute.DataType == 4"></ion-toggle>
        <ion-datetime displayFormat="DD/MM/YYYY" [(ngModel)]="attribute.Value" (ngModelChange)="attributeChange(attribute)" *ngIf="attribute.DataType == 5" placeholder="Please select" max="2070"></ion-datetime>
        <ion-select [(ngModel)]="attribute.Value" (ngModelChange)="attributeChange(attribute)" interface="popover" *ngIf="attribute.DataType == 6" placeholder="Please select">
          <ion-select-option value="-1"></ion-select-option>
          <ion-select-option *ngFor="let option of attribute.Options" [value]="option.Id">{{option.Name}}</ion-select-option>
        </ion-select>
        <ion-select [(ngModel)]="attribute.ValueIntArray" (ngModelChange)="attributeChange(attribute)" multiple="true" *ngIf="attribute.DataType == 7" placeholder="Please select">
          <ion-select-option *ngFor="let option of attribute.Options" [value]="option.Id">{{option.Name}}</ion-select-option>
        </ion-select>
      </ion-item>      
    </div>

Thank you, I will give something like that a try, it is strange why this only seems to affect the ion-toggle and none of my other controls in the example above, and why it only happens sometimes.

Faced a similar bug with an ion-checkbox, and it was resolved by using (click) instead of (ionChange) to get notifications of user toggling the checkbox. Really seems like (ionChange) should only be triggered by user interaction, otherwise it is asking for crazymaking bugs.

I think rapropos was trying to tell you, you cannot combine [(ngModel)] and (ngModelChange) in the same template, the so called BANANA-in-BOX is already doing that.

The following is one of the combinations that can work in my Ionic 3.3,
The slider interaction and updates are not smooth but we have to put up with ionic/angular can of worms to see some butterflies every other day.

I think saying you cannot combine them is misleading, you can because I have done it and it does work, I think the question might be whether it is best practice or not, there are various posts available where people recommend using them together in certain scenarios (links below). Do you have a reference to an article or document which says you can’t do this or indeed that you shouldn’t do it?


The short answer is no. If it’s working and you feel good about it not breaking, go ahead.
The way these frameworks keep changing and breaking just cross your fingers you will have not to revisit code.

Unfortunately in my camp it’s been mostly trial and error getting something to work and stable.
I find it very funny and sad that I hit on these posts in my searches. Worst of all, is when I hit on my own posts two years later.
I have lost trusts in most frameworks WindowsSDK, ATL, Silverlight, WPF, AndroidSDK, ionic, angular, because they fail while working with the basic concepts. Every component will surprise you sooner than later with a weird behavior or limitation. Why should an ion-toggle or ion-range deserve so many posts? A stop at the docs should be sufficient.

==================================
Rants-Start.
I hate semicolons in C (during college compiler courses),
I hate semicolons in C# and every other language.
C, C++, C#, JAVA, JavaScript, TypeScript, CSS, XML,…
While I am editing, I take special joy deleting semicolons in my ionic apps :slight_smile:
If the compiler wants semicolons let it shove them where it wants to.

I hate stupid case sensitive languages and OS (security? yeah, right!)
Linux will eventually succumb, it started supporting case-INSenSitive filesystem, just last year.
English has only 26 characters let’s not invent more, “B” and “b” are just glyphs not separate characters.

Don’t feel you have to use any particular Casing Conventions, it’s only meant for those control freak in your enterprise team.
The most stupid of all is cAmElCaSe
Case conventions should be something I configure in my editor don’t try to force it on me.
When something is case sensitive I purposely use ALLCAPS just to see if I can blow some of its bolts off.

Rants-Stop.

Angular #11234.

IMHO, [(ngModel)]="foo" (ngModelChange)="onFooChange($event)" is an Angular cousin of the (in)famous a[i] = i++ mistake in C.

Things are a bit different in Angular-land than they were in C-land back then, because there is really only one reference Angular implementation, so the holy grail of portability that was so crucial to C programmers is no longer as holy, but I still think it’s super-important not to depend on arbitrary unguaranteed aspects of framework internals, and the order of execution of multiple event bindings is one of those things in my book.

So the way I would state the rule is that if you are going to have multiple banana bindings on an element (even implicit ones), they must operate correctly regardless of the order in which they are called. onFooChange in the above example cannot rely on this.foo. Especially considering readability and maintainability, I would strongly encourage simply limiting oneself to one event binding.

The way I am using it they are independent of each other (i.e. it doesn’t matter what order they run), I am only using it on a multiple ion-select (code example below), the banana-in-a-box is used to set the actual selected values and ngModelChange is used only to set another couple of attributes (one of which is on a parent object).

<ion-select [(ngModel)]="attribute.ValueIntArray" (ngModelChange)="multiSelectAttributeChange(attribute)" multiple="true" >
 <ion-select-option *ngFor="let option of attribute.Options" [value]="option.Id">{{option.Name}}</ion-select-option>
</ion-select>
multiSelectAttributeChange(attribute) {

    // Set attribute to dirty
    attribute.IsDirty = true;

    // Set job as edited
    this.job.Edited = true;
  }

As discussed in previous posts I ended up switching others out to using [ngModel] then using (ngModelChange) to set the value but with a multilple ion-select I wasn’t sure how to do this without possibly splitting the “value” and setting it that way, all of which seemed like a lot of extra code for no benefit. Unless you know a way of doing this that I have missed. Just to give and example this is what I am doing on a normal ion-select (i.e. not multiple) which works fine but this does not work on a multiple select:

<ion-select [ngModel]="attribute.Value" (ngModelChange)="attributeChange($event, attribute)" interface="popover" >
 <ion-select-option *ngFor="let option of attribute.Options" [value]="option.IdString">{{option.Name}}</ion-select-option>
</ion-select>
attributeChange(event: any, attribute) {

      // Set value
      attribute.Value = event;

      // Set attribute to dirty
      attribute.IsDirty = true;

      // Set job as edited
      this.job.Edited = true;
    }
}

I did this and it’s still triggering the IonChange, can you provide the full code?

Here’s mine:

.html

 <ion-toggle
            color="bard-primary"
            [(ngModel)]="isToggleBtnChecked"
            [checked]="isToggleBtnChecked"
            (ionChange)="onToggleBtnChange($event)"
          >
          </ion-toggle>

.ts

onToggleBtnChange(event): void {
    const isChecked = this.isToggleBtnChecked;
    console.log(isChecked);
    if (isChecked) {
      this.presentAlert();
    } 
  }

If I set this.isToggleBtnChecked to true in the ngOnInit hook, the alert triggers, which means the ionChange is triggering as well…