ionic-team / ionic-framework

A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.
https://ionicframework.com
MIT License
50.91k stars 13.52k forks source link

4.0.0-beta.12 error in render when mixing ionItemReorder and delete item #15836

Closed jepiqueau closed 5 years ago

jepiqueau commented 5 years ago

Bug Report

Ionic Info Run ionic info from a terminal/cmd prompt and paste the output below.

insert the output from ionic info here

Ionic:

ionic (Ionic CLI) : 4.0.3

System:

NodeJS : v10.9.0 npm : 6.4.1 OS : macOS High Sierra

@stencil/core13.2 @ionic/core4.0.0-beta12 Describe the Bug when coupling the ionItemReorder and the delete item from a list element the display information is wrong

Related Code

npm init stencil ionic-pwa
import { Component, Prop, State, Listen } from '@stencil/core';

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css'
})
export class AppHome {
  @Prop({ connect: 'ion-alert-controller' }) alertCtrl: HTMLIonAlertControllerElement;
  @State() toggle: boolean = false;
  list:Array<string>;
  alert: HTMLIonAlertElement;

  @Listen('ionItemReorder')
  itemReorderHandler(event: CustomEvent) {
        let itemToMove:string  = this.list.splice(event.detail.from,1)[0];
        this.list.splice(event.detail.to,0,itemToMove);
        console.log('in itemReorderHandler this.list ',this.list)

  }  
  componentWillLoad() {
    this.list = [
      "deer",
      "elephant",
      "tiger",
      "lion",
      "eagle"
    ];
  }
  async presentAlert(options: any): Promise<void> {
      this.alert = await this.alertCtrl.create(options);
      await this.alert.present();
      return;
  }
  async dismissAlert(): Promise<void> {
      await this.alert.dismiss();
      return;
  }
  async _deleteCancel() {
    await this.dismissAlert();
  }
  async _deleteConfirm(index:number) {
        this.list.splice(index,1)
        await this.dismissAlert();
        this.toggle = !this.toggle;
  }

  async _handleItemClick(button:string,index:number) {
      switch (button) {
          case 'delete' : {
              let options:any = {   
                  header: 'Confirm Delete!',
                  buttons: [
                      {
                          text: 'Cancel',
                          role: 'cancel',
                          cssClass: 'alertcancel',
                          handler: () => {
                              this._deleteCancel();
                          }
                      }, {
                          text: 'Start',
                          cssClass: 'alertdanger',
                          handler:() => {
                              this._deleteConfirm(index);
                          }
                      }
                  ]
              }
              await this.presentAlert(options);
              break;
          }
      } 
  }

  render() {
    console.log('in render this.list ',this.list)
    const reorderList = this.list.map((item,index) => {
      return [
          <ion-item>
            <ion-label>{item}</ion-label>
              <ion-reorder slot="end"></ion-reorder>
              <ion-icon name="trash" slot="end" color="danger" onClick={() => this._handleItemClick('delete',index)}></ion-icon>
          </ion-item>
      ];
    });
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Home</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content padding>
        <p>
          Welcome to the PWA Toolkit. You can use this starter to build entire
          apps with web components using Stencil and ionic/core! Check out the
          README for everything that comes in this starter out of the box and
          check out our docs on <a href="https://stenciljs.com">stenciljs.com</a> to get started.
        </p>

        <ion-button href="/profile/ionic" expand="block">Profile page</ion-button>
        <ion-list id="reorder-list" inset lines="inset">
          <ion-reorder-group disabled={false}>
              {reorderList}
              </ion-reorder-group>
          </ion-list>
      </ion-content>
    ];
  }
}
npm start

Additional Context

jepiqueau commented 5 years ago

It seems that the ion-reorder-group works directly in the DOM so not going through the render process meaning that you must not toggle for the render. So the workaround is to write the delete function directly on the DOM by removing the child of the ion-reoder-group element, so i modified the app-home as follows


import { Component, Element, Prop, State, Listen } from '@stencil/core';

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css'
})
export class AppHome {

  @Element() el: Element;
  @Prop({ connect: 'ion-alert-controller' }) alertCtrl: HTMLIonAlertControllerElement;
  @State() toggle: boolean = false;
  list:Array<string>;
  alert: HTMLIonAlertElement;
  reorderGrpEL: HTMLIonReorderGroupElement;

  @Listen('ionItemReorder')
  async itemReorderHandler(event: CustomEvent) {
        let itemToMove:string  = this.list.splice(event.detail.from,1)[0];
        this.list.splice(event.detail.to,0,itemToMove);
  }  
  componentWillLoad() {
    this.list = [
      "deer",
      "elephant",
      "tiger",
      "lion",
      "eagle"
    ];

  }
  componentDidLoad() {
    this.reorderGrpEL = this.el.querySelector('#reorder_group');
  }
  async presentAlert(options: any): Promise<void> {
      this.alert = await this.alertCtrl.create(options);
      await this.alert.present();
      return;
  }
  async dismissAlert(): Promise<void> {
      await this.alert.dismiss();
      return;
  }
  async _deleteCancel() {
    await this.dismissAlert();
  }
  async _deleteConfirm(index:number) {
    let children:NodeListOf<HTMLIonItemElement> = this.reorderGrpEL.querySelectorAll('ion-item');    
    this.reorderGrpEL.removeChild(children[index]);
    this.list.splice(index,1)
    await this.dismissAlert();
//    this.toggle = !this.toggle;
  }

  async _handleItemClick(button:string,index:number) {
      switch (button) {
          case 'delete' : {
              let options:any = {   
                  header: 'Confirm Delete!',
                  buttons: [
                      {
                          text: 'Cancel',
                          role: 'cancel',
                          cssClass: 'alertcancel',
                          handler: () => {
                              this._deleteCancel();
                          }
                      }, {
                          text: 'Start',
                          cssClass: 'alertdanger',
                          handler:() => {
                              this._deleteConfirm(index);
                          }
                      }
                  ]
              }
              await this.presentAlert(options);
              break;
          }
      } 
  }

  render() {
    const reorderList = this.list.map((item,index) => {
      return [
          <ion-item>
            <ion-label>{item}</ion-label>
              <ion-reorder slot="end"></ion-reorder>
              <ion-icon name="trash" slot="end" color="danger" onClick={() => this._handleItemClick('delete',index)}></ion-icon>
          </ion-item>
      ];
    });
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Home</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content padding>
        <p>
          Welcome to the PWA Toolkit. You can use this starter to build entire
          apps with web components using Stencil and ionic/core! Check out the
          README for everything that comes in this starter out of the box and
          check out our docs on <a href="https://stenciljs.com">stenciljs.com</a> to get started.
        </p>

        <ion-button href="/profile/ionic" expand="block">Profile page</ion-button>
        <ion-list id="reorder-list" inset lines="inset">
          <ion-reorder-group id="reorder_group" disabled={false}>
              {reorderList}
              </ion-reorder-group>
          </ion-list>
      </ion-content>
    ];
  }
}

`
``
You must comment the this.toggle = !this.toggle; in the deleteConfirm function otherwise it doesn't work
So this is a restriction meaning that after reordering and deleting you should never go through the render function
corysmc commented 5 years ago

I've seen this similar issue. I've reordered my items via redux. On reorder the data is reordered correctly (logged inside redux, and even rendered outside the ion-reorder-group). However, the ion-reorder-group isn't re-rendered to the correct order. The way I've worked around this for now, is by forcing a re-render by changing a different state variable.

jepiqueau commented 5 years ago

trying to add the "add item" feature was a bit challenging but i finally got the code working the app-home.ts file was updated as follows

import { Component, Element, Prop, State, Listen } from '@stencil/core';

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css'
})
export class AppHome {

  @Element() el: Element;
  @Prop({ connect: 'ion-alert-controller' }) alertCtrl: HTMLIonAlertControllerElement;
  @State() toggle: boolean = false;
  list:Array<string>;
  alert: HTMLIonAlertElement;
  reorderGrpEL: HTMLIonReorderGroupElement;
  validateIcon: HTMLIonIconElement;
  addedValue: string;
  hidInputs:Array<any>;
  ionCardHeader: HTMLIonCardElement;

  @Listen('ionItemReorder')
  async itemReorderHandler(event: CustomEvent) {
        let itemToMove:string  = this.list.splice(event.detail.from,1)[0];
        this.list.splice(event.detail.to,0,itemToMove);
        console.log('in itemReorderHandlert this.list ',this.list);
      } 
  @Listen('ionChange')
  async inputChangeHandler($event: CustomEvent) {
      this.addedValue = $event.detail.value;
      if(this.addedValue.length > 1) {
        this.validateIcon.classList.remove('not-visible');
      } else {
        this.validateIcon.classList.add('not-visible');
      }
  }

  componentWillLoad() {
    this.list = [
      "deer",
      "elephant",
      "tiger",
      "lion",
      "eagle"
    ];

  }
  componentDidLoad() {
    const ionCard: HTMLIonCardElement = this.el.querySelector("#reorder-card");
    this.reorderGrpEL = ionCard.querySelector('#reorder_group');
    this.ionCardHeader = this.el.querySelector('#header-card');
    this.validateIcon = this.ionCardHeader.querySelector('.icon-validate');
  }
  async getNames(childs: NodeListOf<HTMLIonItemElement>): Promise<Array<string>> {
    let retArray: Array<string> = [];
    for(let i:number=0;i < childs.length;i++) {
      retArray = [...retArray,childs[i].textContent];
      if (i === childs.length -1 ) return Promise.resolve(retArray);
    }
  }

  async presentAlert(options: any): Promise<void> {
      this.alert = await this.alertCtrl.create(options);
      await this.alert.present();
      return;
  }
  async dismissAlert(): Promise<void> {
      await this.alert.dismiss();
      return;
  }
  async _deleteCancel() {
    await this.dismissAlert();
  }
  async _deleteConfirm(item:string) {
    let children:NodeListOf<HTMLIonItemElement> = this.reorderGrpEL.querySelectorAll('ion-item');    
    const arrayName:Array<string> = await this.getNames(children);
    const index = arrayName.indexOf(item);
    this.reorderGrpEL.removeChild(children[index]);
    this.list.splice(index,1)
    console.log('in deleteFromList this.list ',this.list);
    await this.dismissAlert();
//    this.toggle = !this.toggle;
  }

  async _handleItemClick(item:string) {
    let options:any = {   
        header: 'Confirm Delete!',
        buttons: [
            {
                text: 'Cancel',
                role: 'cancel',
                cssClass: 'alertcancel',
                handler: () => {
                    this._deleteCancel();
                }
            }, {
                text: 'Start',
                cssClass: 'alertdanger',
                handler:() => {
                    this._deleteConfirm(item);
                }
            }
        ]
    }
    await this.presentAlert(options);
  }
  _handleClick(button:string) {
    switch (button) {
      case 'button' : {
        const div:HTMLDivElement = document.createElement('div');
        div.setAttribute("id","div-add");
        div.innerHTML = '<ion-textarea class="textarea-input" autofocus="true" rows="1" placeholder="Enter any value ..."></ion-textarea>';
        const headerCard: HTMLIonCardElement = this.el.querySelector('#header-card');
        headerCard.appendChild(div);
        break; 
      }
      case 'icon' : {
        const value:string = this.addedValue;
        const ionItem: HTMLIonItemElement = document.createElement('ion-item');
        let innerHtml:string = '<ion-label>'+value +'</ion-label>';
        innerHtml += '<ion-reorder slot="end"></ion-reorder>';
        innerHtml += '<ion-icon id="ion-icon" name="trash" slot="end" color="danger"></ion-icon>';
        ionItem.innerHTML = innerHtml;
        this.reorderGrpEL.appendChild(ionItem);
        const ionIcon:HTMLIonIconElement = ionItem.querySelector('#ion-icon');
        ionIcon.onclick = () => {
          this._handleItemClick(value)};  
        this.list = [...this.list,value];
        console.log("in addToList this.list ",this.list);
        break;
      }
    }
  }
  render() {
    const reorderList = this.list.map((item) => {
      return [
          <ion-item>
              <ion-label>{item}</ion-label>
              <ion-reorder slot="end"></ion-reorder>
              <ion-icon id="ion-icon" name="trash" slot="end" color="danger" onClick={() => this._handleItemClick(item)}></ion-icon>
          </ion-item>
      ];
    });
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Home</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content padding>

        <ion-card id="header-card">
          <ion-button class="button-button" ion-button icon-only color="light" onClick={ () => this._handleClick('button')}>
              <ion-icon class="button-icon" name="add"></ion-icon>
          </ion-button>
          <ion-icon class="icon-validate not-visible" name="checkmark-circle-outline" slot="end" onClick={() => this._handleClick('icon')}></ion-icon>
        </ion-card>
        <ion-card id="reorder-card">
          <ion-card-header class="reorder-card-header" color="light">Element List</ion-card-header>
          <ion-list id="reorder-list" inset lines="inset">
            <ion-reorder-group id="reorder_group" disabled={false}>
              {reorderList}
            </ion-reorder-group>
          </ion-list>
        </ion-card>
      </ion-content>
    ];
  }
}

and make it a bit nicer with a bit of css in the app-home.css

#header-card {
   height:75px; 
}
.reorder-card-header {
    text-align:center;
    font-size: 20px;
    font-weight: bold;
}
#div-add {
    position: relative;
    z-index:999;
    width: 160px;
    left:90px;
    top:-20px;
    background-color:#f3f3f3;
    border-style: solid;
    border-width: 2px;
    border-color:grey;
    box-shadow : 0.5em 0.5em 0.75em -0.15em darkgrey;
}
.textarea-input{
    --padding-top:5px;
    --padding-bottom:5px;
}
.button-button {
    position: relative;
    top: 15px;
    left: 10px;
}
.icon-validate {
    position: relative;
    width: 25px;
    height: 25px;
    top: 28px;
    left:220px;
}
.not-visible {
    visibility:hidden;
}

button.alert-button.alertdanger {
    color: red !important;
}

So the question now: Is this the right way ? we directly write element dynamically in the DOM without going except the first time through the render function

if the answer is yes, we can close the issue

ionitron-bot[bot] commented 5 years ago

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.