akveo / nebular

:boom: Customizable Angular UI Library based on Eva Design System :new_moon_with_face::sparkles:Dark Mode
https://akveo.github.io/nebular
MIT License
8.06k stars 1.51k forks source link

NbRangepickerComponent Error -> Uncaught (in promise): TypeError: Cannot read property 'detach' of undefined #1095

Closed RamEduard closed 5 years ago

RamEduard commented 5 years ago

Issue type

I'm submitting a ... (check one with "x")

Issue description

Current behavior:

When change my page component in my admin throws the error:

Error Uncaught (in promise): TypeError: Cannot read property 'detach' of undefined
TypeError: Cannot read property 'detach' of undefined
at NbRangepickerComponent.push../node_modules/@nebular/theme/components/datepicker/datepicker.component.js.NbBasePicker.hide (datepicker.component.js:152)
    at NbRangepickerComponent.push../node_modules/@nebular/theme/components/datepicker/datepicker.component.js.NbBasePicker.ngOnDestroy (datepicker.component.js:128)

Expected behavior:

When I try to change the page do not throw the exception. It can be a validation in the component or I don't know, something allows prevent this exception.

Steps to reproduce:

  1. Create a component (father). It will contain 2 child components, both with rangepicker component.
  2. Create other component, it will allow the routing page necessary to replicate the error.
  3. When the component with rangepickers is rendered, it render normally. But the second component (page) try to render (PUM) throws the exception.

Related code:

metrics.component.ts

import { Component } from '@angular/core';

import { TRANSACTIONS, USERS } from '../../../environments/environment';

@Component({
  selector: 'ngx-metrics',
  templateUrl: './metrics.component.html',
})
export class MetricsComponent {

  public conceptOptions: {label: string, value: string}[];

  public roleOptions: {label: string, value: string}[];

  public statusOptions: {label: string, value: string}[];

  constructor() {
    // Definir las opciones del Select de Concepts
    this.conceptOptions = TRANSACTIONS.concepts.map((concept: string) => {
      const option = {label: '', value: concept};

      switch (concept) {
        case 'package':
          option.label = 'Plan';
          break;
        case 'personal':
          option.label = 'Evento Personal';
          break;
        case 'grupal':
          option.label = 'Evento Grupal';
          break;
      }

      return option;
    });

    // Definir las opciones del Select de Roles
    this.roleOptions = USERS.roles.map((role: string) => {
      const option = {label: '', value: role};

      switch (role) {
        case 'Admin':
          option.label = 'Administrador';
          break;
        case 'Coach':
          option.label = 'Entrenador';
          break;
        case 'Fitness':
          option.label = 'Fitness';
          break;
      }

      return option;
    });

    // Definir las opciones del Select de Estatus
    this.statusOptions = USERS.statuses.map((status: string) => {
      const option = {label: '', value: status};

      switch (status) {
        case 'active':
          option.label = 'Activo';
          break;
        case 'blocked':
          option.label = 'Bloqueado';
          break;
        case 'deleted':
          option.label = 'Eliminado';
          break;
        case 'inactive':
          option.label = 'Inactivo';
          break;
        case 'pending':
          option.label = 'Pendiente';
          break;
      }

      return option;
    });
  }

}

metrics.component.html

<div class="metrics-container">
  <div class="row">
    <div class="col-md-6">
      <nb-card size="large">
        <nb-card-header>Métricas de Transacciones</nb-card-header>
        <nb-card-body>
          <div class="metrics-tab-container">
            <ngx-metrics-transactions
              [conceptOptions]="conceptOptions"
              [roleOptions]="roleOptions">
            </ngx-metrics-transactions>
          </div>
        </nb-card-body>
      </nb-card>
    </div>
    <div class="col-md-6">
      <nb-card size="large">
        <nb-card-header>Métricas de Usuarios</nb-card-header>
        <nb-card-body>
          <div class="metrics-tab-container">
            <ngx-metrics-users
              [roleOptions]="roleOptions"
              [statusOptions]="statusOptions">
            </ngx-metrics-users>
          </div>
        </nb-card-body>
      </nb-card>
    </div>
  </div>
</div>

transactions/transactions.component.ts

import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { NbRadioComponent } from '@nebular/theme/components/radio/radio.component';
import { NbRangepickerComponent } from '@nebular/theme/components/datepicker/datepicker.component';
import { NbSelectComponent } from '@nebular/theme/components/select/select.component';

// Models
import { MetricsRestResponse } from '../../../../@models/rest.response';

// Services
import { MetricsRestService } from '../../../../services/metrics.rest.service';

@Component({
  selector: 'ngx-metrics-transactions',
  templateUrl: './transactions.component.html',
})
export class TransactionsComponent implements OnInit {

  @Input() conceptOptions: {label: string, value: string}[];
  @Input() roleOptions: {label: string, value: string}[];

  @ViewChild('radioIsDeleted') radioIsDeleted: NbRadioComponent;
  @ViewChild('radioIsReleased') radioIsReleased: NbRadioComponent;
  @ViewChild('selectConcept') selectConcept: NbSelectComponent<string>;
  @ViewChild('selectRole') selectRole: NbSelectComponent<string>;
  @ViewChild('rangepicker') rangepicker: NbRangepickerComponent<any>;

  summary: {title: string, value: number}[];

  public city: string;

  public conceptSelected: string;

  public dateFrom: Date;

  public dateTo: Date;

  public isDeleted: boolean;

  public isReleased: boolean;

  public roleSelected: string;

  constructor(
    private metricsService: MetricsRestService,
  ) { }

  ngOnInit() {
    this.loadMetrics();

    // Cuando cambia el valor del Radio de Transaccion Eliminada
    this.radioIsDeleted.valueChange.subscribe((val: boolean) => {
      this.isDeleted = val;
      this.loadMetrics()
    });

    // Cuando cambia el valor del Radio de Transaccion Liberada
    this.radioIsReleased.valueChange.subscribe((val: boolean) => {
      this.isReleased = val;
      this.loadMetrics()
    });

    // Select role change
    this.selectConcept.selectedChange.subscribe(() => this.loadMetrics());

    // Select role change
    // this.selectRole.selectedChange.subscribe(() => this.loadMetrics());

    // Range Picker change
    this.rangepicker.rangeChange.subscribe((val: {start: Date, end: Date}) => {
      // Actualizar dateFrom
      this.dateFrom = (val.start) ? val.start : null;

      // Actualizar dateTo
      this.dateTo = (val.end) ? val.end : null;

      this.loadMetrics();
    });
  }

  private loadMetrics(): void {
    console.info('TransactionsComponent.loadMetrics');

    this.metricsService.bankMovements(
      this.dateFrom, this.dateTo, this.conceptSelected, this.city,
      this.isReleased, this.isDeleted,
    )
      .subscribe((metricsResp: MetricsRestResponse) => {
        console.info('TransactionsComponent.loadMetrics.response', metricsResp.data);

        this.summary = [
          { title: 'Total de Usuarios', value: metricsResp.data.completeTotal },
          { title: 'Total filtrado', value: metricsResp.data.total },
        ];
      });
  }

  public blurInputCity(): void {
    if (this.city === '') this.city = undefined;

    this.loadMetrics();
  }

}

transactions/transactions.component.html

<div class="metrics-transactions-container">
  <ngx-metrics-summary [summary]="summary"></ngx-metrics-summary>
  <div class="form-filters container">
    <div class="row">
      <div class="col-sm-6">
        <div class="form-group">
          <label for="inputDateFromTo">Fecha (Desde - Hasta)</label>
          <input class="form-control" nbInput id="inputDateFromTo" placeholder="Desde - Hasta" [nbDatepicker]="rangepicker">
          <nb-rangepicker #rangepicker></nb-rangepicker>
        </div>
      </div>
      <div class="col-sm-6">
        <div class="form-group">
          <label for="inputCity">Ciudad</label>
          <input type="text" nbInput class="form-control" id="inputCity" placeholder="Ciudad" [(ngModel)]="city" (blur)="blurInputCity()">
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-6">
        <div class="form-group">
          <label>Concepto</label>
          <nb-select #selectConcept placeholder="Selecciona concepto" name="select-concept" [(ngModel)]="conceptSelected">
            <nb-option>Ninguno</nb-option>
            <nb-option *ngFor="let option of conceptOptions" value="{{option.value}}">
              {{ option.label }}
            </nb-option>
          </nb-select>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-6">
        <div class="form-group">
          <label>¿Transacción Liberada?</label>
          <nb-radio-group #radioIsReleased name="radio-released">
            <nb-radio>Ninguno</nb-radio>
            <nb-radio [value]="false">No</nb-radio>
            <nb-radio [value]="true">Si</nb-radio>
          </nb-radio-group>
        </div>
      </div>
      <div class="col-md-6">
        <label>¿Transacción Eliminada?</label>
        <nb-radio-group #radioIsDeleted name="radio-deleted">
          <nb-radio>Ninguno</nb-radio>
          <nb-radio [value]="false">No</nb-radio>
          <nb-radio [value]="true">Si</nb-radio>
        </nb-radio-group>
      </div>
    </div>
  </div>
</div>

users/users.component.ts

import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { NbRadioComponent } from '@nebular/theme/components/radio/radio.component';
import { NbRangepickerComponent } from '@nebular/theme/components/datepicker/datepicker.component';
import { NbSelectComponent } from '@nebular/theme/components/select/select.component';

// Models
import { MetricsRestResponse } from '../../../../@models/rest.response';

// Services
import { MetricsRestService } from '../../../../services/metrics.rest.service';

@Component({
  selector: 'ngx-metrics-users',
  templateUrl: './users.component.html',
})
export class UsersComponent implements OnInit {

  @Input() roleOptions: {label: string, value: string}[];
  @Input() statusOptions: {label: string, value: string}[];

  @ViewChild('radioIsDeleted') radioIsDeleted: NbRadioComponent;
  @ViewChild('selectRole') selectRole: NbSelectComponent<string>;
  @ViewChild('selectStatus') selectStatus: NbSelectComponent<string>;
  @ViewChild('rangepicker') rangepicker: NbRangepickerComponent<any>;

  summary: {title: string, value: number}[];

  public city: string;

  public dateFrom: Date;

  public dateTo: Date;

  public isDeleted: boolean;

  public roleSelected: string;

  public statusSelected: string;

  constructor(
    private metricsService: MetricsRestService,
  ) {}

  ngOnInit() {
    // Data inicial
    this.loadMetrics();

    // Cuando cambia el valor del Radio de Transaccion Eliminada
    this.radioIsDeleted.valueChange.subscribe((val: boolean) => {
      this.isDeleted = val;
      this.loadMetrics()
    });

    // Select role change
    this.selectRole.selectedChange.subscribe(() => this.loadMetrics());

    // Select status change
    this.selectStatus.selectedChange.subscribe(() => this.loadMetrics());

    // Range Picker change
    this.rangepicker.rangeChange.subscribe((val: {start: Date, end: Date}) => {
      // Actualizar dateFrom
      this.dateFrom = (val.start) ? val.start : null;

      // Actualizar dateTo
      this.dateTo = (val.end) ? val.end : null;

      this.loadMetrics();
    });
  }

  private loadMetrics(): void {
    console.info('UsersComponent.loadMetrics');

    this.metricsService.users(
      this.dateFrom, this.dateTo, this.roleSelected, this.statusSelected,
      this.city, this.isDeleted,
    )
      .subscribe((metricsResp: MetricsRestResponse) => {
        console.info('UsersComponent.loadMetrics.response', metricsResp.data);

        this.summary = [
          { title: 'Total de Usuarios', value: metricsResp.data.completeTotal },
          { title: 'Total filtrado', value: metricsResp.data.total },
        ];
      });
  }

  public blurInputCity(): void {
    if (this.city === '') this.city = undefined;

    this.loadMetrics();
  }

  public changeCheckboxIsDeleted(): void {
    if (!this.isDeleted) this.isDeleted = undefined;

    this.loadMetrics();
  }

}

users/users.component.html

<div class="metrics-users-container">
  <ngx-metrics-summary [summary]="summary"></ngx-metrics-summary>

  <div class="form-filters container">
    <div class="row">
      <div class="col-sm-6">
        <div class="form-group">
          <label for="inputDateFromTo">Fecha (Desde - Hasta)</label>
          <input class="form-control" nbInput id="inputDateFromTo" placeholder="Desde - Hasta" [nbDatepicker]="rangepicker">
          <nb-rangepicker #rangepicker></nb-rangepicker>
        </div>
      </div>
      <div class="col-sm-6">
        <div class="form-group">
          <label for="inputCity">Ciudad</label>
          <input type="text" nbInput class="form-control" id="inputCity" placeholder="Ciudad" [(ngModel)]="city" (blur)="blurInputCity()">
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-6">
        <div class="form-group">
          <label>Rol</label>
          <nb-select #selectRole placeholder="Selecciona rol" name="select-role" [(ngModel)]="roleSelected">
            <nb-option>Ninguno</nb-option>
            <nb-option *ngFor="let option of roleOptions" value="{{option.value}}">
              {{ option.label }}
            </nb-option>
          </nb-select>
        </div>
      </div>
      <div class="col-sm-6">
        <div class="form-group">
          <label>Estatus</label>
          <nb-select #selectStatus placeholder="Selecciona estatus" name="select-status" [(ngModel)]="statusSelected">
             <nb-option>Ninguno</nb-option>
             <nb-option *ngFor="let option of statusOptions" value="{{option.value}}">
               {{ option.label }}
             </nb-option>
          </nb-select>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-md-6">
        <label>¿Transacción Eliminada?</label>
        <nb-radio-group #radioIsDeleted name="radio-deleted">
          <nb-radio>Ninguno</nb-radio>
          <nb-radio [value]="false">No</nb-radio>
          <nb-radio [value]="true">Si</nb-radio>
        </nb-radio-group>
      </div>
    </div>
  </div>
</div>

Other information:

npm, node, OS, Browser

Node v8.12.0, npm 6.4.1
OS: Linux (Debian 9.5)
Browser: Chrome

Angular, Nebular

package.json

{
  "name": "connectfy-dashboard",
  "version": "0.9.0",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://gitlab.com/somostechdev/connectfy-dashboard"
  },
  "bugs": {
    "url": "https://gitlab.com/somostechdev/connectfy-dashboard/issues"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "IE 11"
  ],
  "scripts": {
    "ng": "ng",
    "conventional-changelog": "conventional-changelog",
    "build:dev": "cp ./config/dev.ts ./src/environments/environment.prod.ts && npm run build -- --prod --aot",
    "build:prod": "cp ./config/prod.ts ./src/environments/environment.prod.ts && npm run build -- --prod --aot",
    "build:test": "cp ./config/test.ts ./src/environments/environment.prod.ts && npm run build -- --prod --aot",
    "run:dev": "cp ./config/dev.ts ./src/environments/environment.ts && ng serve",
    "run:prod": "cp ./config/prod.ts ./src/environments/environment.ts && ng serve",
    "run:test": "cp ./config/test.ts ./src/environments/environment.ts && ng serve",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "test:coverage": "rimraf coverage && npm run test -- --code-coverage",
    "lint": "ng lint",
    "lint:fix": "ng lint ngx-admin-demo --fix",
    "lint:styles": "stylelint ./src/**/*.scss",
    "lint:ci": "npm run lint && npm run lint:styles",
    "pree2e": "webdriver-manager update --standalone false --gecko false",
    "e2e": "ng e2e",
    "docs": "compodoc -p src/tsconfig.app.json -d docs",
    "docs:serve": "compodoc -p src/tsconfig.app.json -d docs -s",
    "prepush": "npm run lint:ci",
    "release:changelog": "npm run conventional-changelog -- -p angular -i CHANGELOG.md -s"
  },
  "dependencies": {
    "@agm/core": "^1.0.0-beta.5",
    "@angular/animations": "^7.0.3",
    "@angular/cdk": "^7.0.3",
    "@angular/common": "^7.0.3",
    "@angular/compiler": "^7.0.3",
    "@angular/core": "^7.0.3",
    "@angular/forms": "^7.0.3",
    "@angular/http": "^7.0.3",
    "@angular/platform-browser": "^7.0.3",
    "@angular/platform-browser-dynamic": "^7.0.3",
    "@angular/router": "^7.0.3",
    "@asymmetrik/ngx-leaflet": "3.0.1",
    "@nebular/auth": "^3.0.0",
    "@nebular/bootstrap": "^3.0.0",
    "@nebular/security": "^3.0.0",
    "@nebular/theme": "^3.0.0",
    "@ng-bootstrap/ng-bootstrap": "^4.0.0",
    "angular-froala-wysiwyg": "^2.8.5",
    "angular2-toaster": "^6.1.0",
    "bootstrap": "4.0.0",
    "browser-image-compression": "0.0.3",
    "classlist.js": "1.1.20150312",
    "core-js": "2.5.1",
    "echarts": "^4.0.2",
    "eva-icons": "^1.1.0",
    "intl": "1.2.5",
    "ionicons": "2.0.1",
    "leaflet": "1.2.0",
    "nebular-icons": "1.0.8",
    "jquery": "^3.3.1",
    "moment": "^2.22.2",
    "ng2-ckeditor": "^1.2.2",
    "ng2-completer": "^2.0.8",
    "ng2-smart-table": "^1.3.5",
    "ngx-bootstrap": "^3.0.1",
    "ngx-editor": "^3.3.0",
    "normalize.css": "6.0.0",
    "pace-js": "1.0.2",
    "roboto-fontface": "0.8.0",
    "rxjs": "6.3.0",
    "rxjs-compat": "6.3.0",
    "socicon": "3.0.5",
    "typeface-exo": "0.0.22",
    "web-animations-js": "2.2.5",
    "zone.js": "^0.8.26"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^0.11.3",
    "@angular/cli": "^7.0.3",
    "@angular/compiler-cli": "^7.0.3",
    "@angular/language-service": "7.0.0",
    "@compodoc/compodoc": "^1.1.7",
    "@fortawesome/fontawesome-free": "^5.2.0",
    "@types/googlemaps": "^3.30.4",
    "@types/jasmine": "2.5.54",
    "@types/jasminewd2": "2.0.3",
    "@types/leaflet": "1.2.3",
    "@types/node": "6.0.90",
    "codelyzer": "^4.5.0",
    "conventional-changelog-cli": "1.3.4",
    "husky": "0.13.3",
    "jasmine-core": "2.6.4",
    "jasmine-spec-reporter": "4.1.1",
    "karma": "^3.1.3",
    "karma-chrome-launcher": "2.1.1",
    "karma-cli": "1.0.1",
    "karma-coverage-istanbul-reporter": "1.3.0",
    "karma-jasmine": "1.1.0",
    "karma-jasmine-html-reporter": "0.2.2",
    "npm-run-all": "4.0.2",
    "protractor": "^5.4.1",
    "rimraf": "2.6.1",
    "stylelint": "7.13.0",
    "ts-node": "3.2.2",
    "tslint": "5.7.0",
    "tslint-language-service": "0.9.6",
    "typescript": "3.1.3"
  }
}
QuantalDingo commented 5 years ago

Just add next code in node_modules/@nebular/components/datepicker/datepicker.component.js:

NbBasePicker.prototype.hide = function () { // check if ref exists in this if(this.ref) { this.ref.detach(); } // save current value if picker was rendered if (this.picker) { this.queue = this.value; this.pickerRef.destroy(); this.pickerRef = null; this.container = null; } };

NbBasePicker.prototype.ngOnDestroy = function () { this.alive = false; this.hide(); // check if ref exists in this if(this.ref) { this.ref.dispose(); } };

Tibing commented 5 years ago

Resolved by #1116