0

I'm using Angular 4 Reactive Forms, with "Ionic(3) native" UI components, and techniques from various "nested reactive forms" blogs/tutorials to create a dynamic, multi-section data entry app.

I wanted a reusable segmented control component that takes in a title & an array of options (based on a dropdown selection) so I don't have to create multiple nearly identical sub-form components.

It all works great the 1st time, but appeared to break after a subsequent selection if the ion-segment changes its number of buttons or their label values (if 2 selections happen to have identical options, the segment buttons all still work fine).

Example: The initial set of 3 options had "adult & "unknown" ... enter image description here

After changing the dropdown & the passed-in array of segment options, the common choices can still be selected, but I cannot set "calf" or "yearling" as Active (although in the component code & formGroup model it does get set). If I first select "goat", which only has 1 "unknown" option, that's the only one I can select.

enter image description here

"Calf" only turns light/disabled instead of "Active". This is what I need to fix.

It properly updates to show the correct number of buttons with the correct labels, and it properly updates the formGroup model even when it appears broken, but the "Active" state only works for the 1st selection. I've tried using (click) and (ionSelect) to call the method on the button, but no difference.

Basically everything seems to work except for the Ionic styling & CSS classes on subsequent changes to the @Input array of button options.

MY QUESTION: Where/how can I tell the Ionic <ion-segment> to only use the latest values & # of segments?? The examples in tutorials or Ionic docs use static lists and template-driven forms with [(ngModel)] data-binding, which I can't use w/a reactive form. Is this only possible with template-driven forms??

mc01
  • 3,750
  • 19
  • 24
  • I _think_ I understand what you are asking but can you add some code please? It's hard to provide a solution like that :-) – David Sep 06 '17 at 09:08
  • Hi @David - yeah, it was 1:30am when I posted initially :) Added code & some screenshots. Probably something very simple about Ionic or Angular that I just don't know I need to do to refresh a view's content. Not too familiar with either. – mc01 Sep 06 '17 at 16:01
  • Thanks, thats a lot of code! So my best guess for this is that Angulars change detection is playing you a trick here. According to you the model is updated but not the view - seems like Angular did not update the DOM tree according to your changes. Fortunately we can do this using ChangeDetectorRef (https://angular.io/api/core/ChangeDetectorRef). Inject it in your constructor, and use the `.detectChanges()` method to run change detection manually after you made a change which should be reflected by the view! – David Sep 06 '17 at 16:33
  • Thanks - I looked at `ChangeDetectionStrategy` & `ChangeDetectorRef` before but didn't seem to have any effect. Just injected & tried `this.ref.detectChanges()`, `this.ref.markForCheck()`, and `this.ref.reattach()` in `ngOnChanges()`, OnInit, and in the `.subscribe()` closure for the formControls. None worked. CDStrategy is 'Default'. Using `this.ref.detach()` made it so I couldn't select anything at all, so that had an effect, but in the wrong direction :) Same issue now w/another component where data updates, but UI doesn't. When/where should I update the DOM with Ionic components? Thanks! – mc01 Sep 06 '17 at 17:48
  • Usually you have one class-level ng-model for an ionic-component and everytime new data arrives (in your case through @Input) you update this model. In your case the fitting place would be `ngOnChanges` as this lifecycle hook always runs when an @Input parameter changes. – David Sep 06 '17 at 17:54
  • Hmm ... that's the method I ended up using for that reason, but it's not triggering the UI to update in the parent component. All the data & changes do reflect in the model & component properties though. Is there a "re-render the template" method I can call directly? Is that what `.detectChanges()` is supposed to do? At this point I think this control needs to become another `ion-select` so I can move on. Thanks for trying! – mc01 Sep 06 '17 at 18:04
  • Yes thats what `.detectChanges()` does, it tells Angular to check if theres something that should be rendered on the DOM. I think you should not use two components in a parent/child relation for this use-case. One component with both UI-elements is more fitting here because there is so much "communication" between them adn communication through input-properties and output-events is not laid out for that. – David Sep 06 '17 at 21:26
  • Turns out I have the exact same problem using only 1 component. I cannot set the Active segment if the buttons are changed dynamically in *ngFor. Only works with a static, predetermined list of options, which is kind of dumb. Using `.detectChanges()` in the button click event does nothing. – mc01 Sep 06 '17 at 22:43
  • Very wired! But I'm glad you were able to hack it together yourself finally! – David Sep 07 '17 at 10:48

1 Answers1

2

So after waaay too many hours wasted, turns out it's some problem w/how Ionic applies CSS classes to components that use a structural directive ... None of the lifecycle or ChangeDetection methods worked because it was already up to date! Grrr ....

For future reference: If you use an *ngFor or *ngIf structural directive to generate your <ion-segment-button> elements, and you change either:

  1. the number of buttons in the segment or
  2. their values ...

The result is that it appears as if you cannot select updated segment buttons ...

But you can - it all works fine except for applying the segment-activated CSS class to the selected button and removing it from all the others.

If your updated segment data source has identical values & number of buttons, no problem. But, the segment-activated class won't get applied to any buttons that have a higher index or different value than previous ones.

I finally hacked together this ugly approach to fix the problem, and it'll have to do since I've wasted so much time on it already ...

<ion-segment formArrayName="segmentArray">
    <ion-segment-button *ngFor="let option of options; let i=index;" [value]="i" (tap)="setOption(i, $event)">{{option}}</ion-segment-button>
</ion-segment>

public setOption(index, event) {
    if (this.options[index] != null) {
      this.selectedSegment = this.options[index]; 

      //note you have to use "tap" or "click" - if you bind to "ionSelected" you don't get the "target" property
      let segments = event.target.parentNode.children;
      let len = segments.length;
      for (let i=0; i < len; i++) {
        segments[i].classList.remove('segment-activated');
      }
      event.target.classList.add('segment-activated');
}

It's ugly & ol'skool, but it gets the job done and I have no more time to make it fancy ...

mc01
  • 3,750
  • 19
  • 24