ionic-framework: PickerController doesn't show correctly

Bug Report

Ionic version:

[x] 4.x

Current behavior:

The second time the PickerController shows the options are not showing correctly.

Expected behavior:

It should show always the right options pickercontroller

Steps to reproduce:

Related code:

Other information:

Ionic info:

Ionic:

   ionic (Ionic CLI)             : 4.10.3 (/usr/local/lib/node_modules/ionic)
   Ionic Framework               : @ionic/angular 4.0.2
   @angular-devkit/build-angular : 0.13.4
   @angular-devkit/schematics    : 7.3.4
   @angular/cli                  : 7.3.4
   @ionic/angular-toolkit        : 1.4.0

System:

   NodeJS : v10.15.1 (/usr/local/bin/node)
   npm    : 6.8.0
   OS     : macOS Mojave

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 4
  • Comments: 47 (4 by maintainers)

Most upvoted comments

@michaelins

async numberPicker() {
    const picker = await this.pickerCtrl.create({
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel'
        },
        {
          text: 'Confirm',
          handler: (data) => {
            console.log(data.number);
          }
        }
      ],
      columns: [{
        name: 'number',
        options: [
          { text: 'One', value: 1 },
          { text: 'Two', value: 2 },
        ]
      }]
    });

    picker.columns[0].options.forEach(element => {
      delete element.selected;
      delete element.duration;
      delete element.transform;
    });

    picker.present();
  }

it is the problem of changed columns.options with dynamically set options, when click the picker without changing the selectedIndex twice, the columns.options will add three attribute of selected/duration/transform, just delete it. columns.options.forEach(element => { delete element.selected; delete element.duration; delete element.transform; });

I have the same issue.

It looks like the same PickerOptions object must not be reused for several pickers. I. e. if you store options in component instance, do a copy before passing to pickerController.create():

const opts = JSON.parse(JSON.stringify(this.opts));
const picker = await this.pickerController.create(opts);

Experiencing the same issue… Running ionic latest(5.x.) image

what solved the issue was declaring function removeAllAddedAttributesToArray() { data.forEach(data => { delete data.transform; delete data.duration; delete data.selected; }); }

and calling it above the const picker = await pickerController.create({

@nurizzatiabdharis Very strange, here’s how I solved it:

async openPicker() {
    this.removeAllAddedAttributesToArray();
    const pickerElement = await this.pickerCtrl.create({
      buttons: [
        {
          text: CANCEL,
          cssClass: 'cancel-btn',
          role: 'cancel'
        },
        {
          text: DONE,
          cssClass: 'done-btn',
          role: 'done',
          handler: (data) => {
            this.changeValue(data.value);
          }
        }
      ],
      columns: [{
        name: 'col',
        options: this.simpleColumns,
        selectedIndex: this.selectedIndex
      }],
      cssClass: 'ion-picker',
    });

    await pickerElement.present();
}

private removeAllAddedAttributesToArray() {
    this.simpleColumns.forEach((data) => {
      if (data.duration) {
        delete data.transform;
        delete data.duration;
        delete data.selected;
      }
    });
  }

Hi there, I think we are dealing with two separate problems here:

1- If you get all your options stacked once you reopen the picker, then the solution suggested by @MengLu2016 should solve your problem

2- If you are working with dynamic options, in my case I have 2 columns where the options in column B depends on the value selected by column A. You need to make sure that options in column B always have the same length. Otherwise, you might see stacked options if the number of the new options is greater than the number of the old options as @zwlccc said. So what I did, is to make sure that all possible options have the same length by adding disabled options by the end of the smaller one.

For example:

PossibleOptionsForColumnB = [
    [
      { value: 1, text: 'value 1' },
      { value: 2, text: 'value 2' },
      { value: 3, text: 'value 3' },
      { value: 4, text: 'value 4' },
      { value: -1, text: '', disabled: true }
    ],
    [
      { value: 1, text: 'value 1' },
      { value: 2, text: 'value 2' },
      { value: 3, text: 'value 3' },
      { value: 4, text: 'value 4' },
      { value: 5, text: 'value 5' }
    ]
    ,
    [
      { value: 1, text: 'value 1' },
      { value: 2, text: 'value 2' },
      { value: 3, text: 'value 3' },
      { value: -1, text: '', disabled: true },
      { value: -1, text: '', disabled: true }
    ]
  ];

I had the exact same problem and was able to fix it using the techniques from this thread. THANK YOU everyone! I’m doing this on picker dismissal. Changed it to work for multi-column pickers as well:

  async showPicker() {
    let opts: PickerOptions = {
      buttons: [{ text: 'Cancel', role: 'cancel' }, { text: 'Done' }],
      columns: [{ name: 'Name', options: this.options, selectedIndex: 0 }],
    };
    let picker = await this.pickerCtrl.create(opts); picker.present();
    picker.onDidDismiss().then(async data => {
      // ... program logic
      picker.columns.forEach(col => { col.options.forEach(el => { delete el.selected; delete el.duration; delete el.transform; }) })
    });
}

You can test it using the following methods: Modify the picker-column.tsx file: 1.modify this function: ` private update(y: number, duration: number, saveY: boolean) { if (!this.optsEl) { return; } let translateY = 0; let translateZ = 0; const { col, rotateFactor } = this; const selectedIndex = col.selectedIndex = this.indexForY(-y); const durationStr = (duration === 0) ? ‘’ : duration + ‘ms’; const scaleStr = ‘scale(${this.scaleFactor})’;

const children = this.optsEl.children;
const children_length = this.optsEl.children.length;
const options_length = col.options.length;
const length = children_length < options_length ? options_length : children_length;
for (let i = 0; i < length; i++) {
  const button = children[i] as HTMLElement;
  const opt = col.options[i];
  const optOffset = (i * this.optHeight) + y;
  let transform = '';

  if (rotateFactor !== 0) {
    const rotateX = optOffset * rotateFactor;
    if (Math.abs(rotateX) <= 90) {
      translateY = 0;
      translateZ = 90;
      transform = ‘rotateX(${rotateX}deg) ‘;
    } else {
      translateY = -9999;
    }

  } else {
    translateZ = 0;
    translateY = optOffset;
  }

  const selected = selectedIndex === i;
  transform += ‘translate3d(0px,${translateY}px,${translateZ}px) ‘;
  if (this.scaleFactor !== 1 && !selected) {
    transform += scaleStr;
  }

  // Update transition duration
  if (this.noAnimate) {
    if(opt){
      opt.duration = 0;
    }
    if(button){
      button.style.transitionDuration = '';
    }

  } else if (opt){
    if (duration !== opt.duration) {
      opt.duration = duration; 
      if(button){
        button.style.transitionDuration = durationStr;
      }
    }
  }

  // Update transform
  if(opt){
    if (transform !== opt.transform) {
      opt.transform = transform;
      if(button){
        button.style.transform = transform;
      }
    }
  }
  // Update selected item
  if(opt){
    if (selected !== opt.selected) {
      opt.selected = selected;
      if (selected && button) {
        button.classList.add(PICKER_OPT_SELECTED);
      } else if (button){
        button.classList.remove(PICKER_OPT_SELECTED);
      }
    }
  }
}
this.col.prevSelected = selectedIndex;

if (saveY) {
  this.y = y;
}

if (this.lastIndex !== selectedIndex) {
  // have not set a last index yet
  hapticSelectionChanged();
  this.lastIndex = selectedIndex;
}

}2.modify this function: render() { const col = this.col; const Button = ‘button’ as any; return [ col.prefix && ( <div class=“picker-prefix” style={{ width: col.prefixWidth! }}> {col.prefix} </div> ), <div class=“picker-opts” style={{ maxWidth: col.optionsWidth! }} ref={el => this.optsEl = el} > { col.options.map((o, index) => <Button type=“button” class={{ ‘picker-opt’: true, ‘picker-opt-disabled’: !!o.disabled, ‘picker-opt-selected’: o.selected }} style={{ transform: o.transform ? o.transform : ‘translate3d(0px, -9999px, 90px)’, ‘transition-duration’: o.duration ? o.duration: TRANSITION_DURATION + ‘ms’ }} opt-index={index} > {o.text} </Button> )} </div>, col.suffix && ( <div class=“picker-suffix” style={{ width: col.suffixWidth! }}> {col.suffix} </div> ) ]; }`

// just before => await picker.present();

// use:
	for (let col of picker.columns) {
		for (let opt of col.options) {
			delete opt.selected;
			delete opt.duration;
			delete opt.transform;
		}
	}

For those having issue with using IonPicker component in React. Here’s my solution:

Place sortOptions inside the render() or if using functional component then place it inside the function declaration like below:

  const sortOptions: PickerColumn[] = [
    {
      name: "sortOption",
      options: [
        { text: "Apple", value: "red" },
        { text: "Banana", value: "yellow" },
        { text: "Orange", value: "orange" },
      ],
    },
  ];

And in your IonPicker it stays the same

<IonPicker
     //...
     columns={sortOptions}
     //...
    />

It re-creates sortOptions on every render (not optimal but it is what it is until the “real” fix)

@gusterwoei FYI

@liamdebeasi hi. i am using Chrome 72.

Here are the step that I got the issue: The first time you select the controller it shows correctly. Then pick an option say “value 3” and click Ok. Then the second time you show the picker the options don’t show correctly if the selectedIndex is set.

I hope this helps