ghiscoding/aurelia-slickgrid

Getting an error when I try to open another row detail after using pagination button

Closed this issue · 3 comments

Hey there. Sorry to bother you with this however I thought I would ask you this as I am struggling to understand why this error is happening. The error occurs when I have a row detail panel open and I then click on the pagination button at the bottom of the grid to go to the next page without closing the detail panel first. The next page comes up perfectly. However if I now click on a plus "+" button to open up a detail panel no panel opens. Instead I get the following error.

Uncaught Error: Invalid id
at DataView.deleteItem (slick.dataview.js:413)
at collapseDetailView (slick.rowdetailview.js:451)
at collapseAll (slick.rowdetailview.js:434)
at expandDetailView (slick.rowdetailview.js:469)
at handleAccordionShowHide (slick.rowdetailview.js:549)
at toggleRowSelection (slick.rowdetailview.js:426)
at SlickGrid.handleClick (slick.rowdetailview.js:245)
at Event.notify (slick.core.js:189)
at trigger (slick.grid.js:2527)
at HTMLDivElement.handleClick (slick.grid.js:4436)

Here is the location in Slickgrid:

dataview

It seems I need to somehow close the panel before proceeding to the next page.. am I right with this and if so how would I close the panel before I go to the next page..

For extra context here is my viewmodel: (By the way I use an embedded array for the detail panel for each client so I don't need to go off to the server again - not sure if that's relevant)

import { bindable, PLATFORM, autoinject } from "aurelia-framework";
import { Router } from "aurelia-router";
import { HttpClient, json } from 'aurelia-fetch-client';
import { Subscription } from 'aurelia-event-aggregator';
import {
	AureliaGridInstance,
	Column,
	FieldType,
	Formatter,
	Formatters,
	Filters,
	GridOdataService,
	GridOption,
	GridStateChange,
	GridState,
	ExtensionName,
	Statistic,
} from 'aurelia-slickgrid';

import { MessagePayload } from '../../../../services/messages/messages'
import { AuthService } from '../../../../services/auth/auth-service'

const defaultPageSize = 10;

// create my custom Formatter with the Formatter type
const myCustomCheckmarkFormatter: Formatter = (row, cell, value, columnDef, dataContext) => {
	// you can return a string of a object (of type FormatterResultObject), the 2 types are shown below
	return value ? `<i class="fa fa-fire red" aria-hidden="true"></i>` : { text: '<i class="fa fa-snowflake-o" aria-hidden="true"></i>', addClasses: 'lightblue', toolTip: 'Freezing' };
};

// create custom Formatter for display name.
const displayNameFormatter: Formatter = (row, cell, value, columnDef, dataContext) => {
	return `<b>` + dataContext.clientLastName + `</b>` + ", " + dataContext.clientFirstName;
};

 // create my custom Formatter for jobs.
//const jobsFormatter: Formatter = (row, cell, value, columnDef, dataContext) => {
//	var x = dataContext.jobs.length;
//	return x;
//};

@autoinject()
export class ClientList {
	title = 'Client List';
	subTitle = `
	Click on a client to examine and or edit.
	<br/>
	  `;
	@bindable detailViewRowCount = 7;
	aureliaGrid: AureliaGridInstance;
	gridOptions: GridOption;
	columnDefinitions: Column[];
	dataset: any[];
	statistics: Statistic;
	subscriptions: Subscription[];

	odataQuery = '';
	processing = false;
	status = { text: '', class: '' };

	constructor(
		private http: HttpClient,
		private authService: AuthService,
		private router: Router) {
		// define the grid options & columns and then create the grid itself
		this.defineGrid();
	}

	detached() {
		this.saveCurrentGridState()
	}

	// you can save it to Local Storage of DB in this call
	saveCurrentGridState() {
		const gridState: GridState = this.aureliaGrid.gridStateService.getCurrentGridState();
		console.log('Leaving page with current grid state', gridState);
	}

	aureliaGridReady(aureliaGrid: AureliaGridInstance) {
		this.aureliaGrid = aureliaGrid;
	}

	get rowDetailInstance(): any {
		return this.aureliaGrid && this.aureliaGrid.extensionService.getSlickgridAddonInstance(ExtensionName.rowDetailView) || {};
	}

	defineGrid() {
		this.columnDefinitions = [
			{ id: 'clientNo', name: 'Client No.', field: 'clientNo', cssClass: "sg-column-centered", maxWidth: 80, sortable: true, type: FieldType.string, filterable: true, filter: { model: Filters.compoundInput } },
			{
				id: 'active', name: 'Active', field: 'active', cssClass: "sg-column-centered",
				maxWidth: 80,
				sortable: true,
				formatter: Formatters.multiple, params: { formatters: [Formatters.complexObject, Formatters.checkmark] },
				filterable: true, filter: { collection: ['', 'True', 'False'], model: Filters.singleSelect }
			},
			{
				id: 'clientLastName', name: 'Contact Name', field: 'clientLastName',
				sortable: true,
				formatter: displayNameFormatter,
				type: FieldType.string,
				filterable: true, filter: { model: Filters.compoundInput }
			},
			{
				id: 'company', name: 'Company', field: 'company', cssClass: "sg-column-centered",
				maxWidth: 80,
				sortable: true,
				formatter: Formatters.checkmark,
				filterable: true, filter: { collection: ['', 'True', 'False'], model: Filters.singleSelect }
			},
			{ id: 'companyName', name: 'Company Name', field: 'companyName', sortable: true, type: FieldType.string, filterable: true, filter: { model: Filters.compoundInput } },
			{
				id: 'isWarrantyCompany', name: 'Warranty Company', field: 'isWarrantyCompany', cssClass: "sg-column-centered",
				maxWidth: 80,
				sortable: true,
				formatter: Formatters.checkmark,
				filterable: true, filter: { collection: ['', 'True', 'False'], model: Filters.singleSelect }
			},
			//{ id: 'displayName', name: 'Contact Name', field: 'displayName', sortable: true, type: FieldType.string, filterable: true, filter: { model: Filters.compoundInput } },
			{ id: 'mobilePhone', name: 'Mobile Ph.', field: 'mobilePhone', maxWidth: 100, cssClass: "sg-column-centered", sortable: true, type: FieldType.string, filterable: true, filter: { model: Filters.compoundInput } },
			{
				id: 'jobs', name: 'Jobs', field: 'jobs',
				maxWidth: 50,
				//formatter: jobsFormatter,
				cssClass: "sg-column-centered"
			},
		];

		this.gridOptions = {
			autoHeight: true,
			enableAutoResize: true,
			autoResize: {
				containerId: 'sl-container',
				sidePadding: 15
			},
			enableCellNavigation: true,
			enableFiltering: true,
			enableRowDetailView: true,
			enableRowSelection: true,
			rowSelectionOptions: {
				selectActiveRow: true
			},
			rowDetailView: {

				// We can load the "process" asynchronously in 3 different ways (aurelia-http-client, aurelia-fetch-client OR even Promise)
				process: (item) => this.simulateServerAsyncCall(item),
				// process: (item) => this.http.get(`api/item/${item.id}`),

				// load only once and reuse the same item detail without calling process method
				loadOnce: false,

				// limit expanded row to only 1 at a time
				singleRowExpand: true,

				// false by default, clicking anywhere on the row will open the detail view
				// when set to false, only the "+" icon would open the row detail
				// if you use editor or cell navigation you would want this flag set to false (default)
				useRowClick: false,

				// how many grid rows do we want to use for the row detail panel (this is only set once and will be used for all row detail)
				// also note that the detail view adds an extra 1 row for padding purposes
				// so if you choose 4 panelRows, the display will in fact use 5 rows
				panelRows: this.detailViewRowCount,

				// you can override the logic for showing (or not) the expand icon
				// for example, display the expand icon only on every 2nd row
				// expandableOverride: (row: number, dataContext: any, grid: any) => (dataContext.id % 2 === 1),

				// Preload View Template
				preloadView: PLATFORM.moduleName('./client-preload.html'),

				// ViewModel Template to load when row detail data is ready
				viewModel: PLATFORM.moduleName('./client-detail-view'),
			},

			//enableRowSelection: true,
			pagination: {
				pageSizes: [10, 20, 50, 100, 500],
				pageSize: defaultPageSize,
				totalItems: 0
			},
			presets: {
				// you can also type operator as string, e.g.: operator: 'EQ'

				sorters: [
					// direction can be written as 'asc' (uppercase or lowercase) and/or use the SortDirection type
					{ columnId: 'clientNo', direction: 'desc' },
					{ columnId: 'clientLastName', direction: 'asc' },

				],
				pagination: { pageNumber: 1, pageSize: 10 }
			},
			backendServiceApi: {
				service: new GridOdataService(),
				preProcess: () => this.displaySpinner(true),
				process: (query) => this.getCustomerApiCall(query),

				postProcess: (response) => {
					console.log("RESPONSE: ", response);
					this.displaySpinner(false);
					this.getCustomerCallback(response);
				}
			}
		};
	}

	handleRowSelection(event, args) {
		console.log("HANDLE ROW SELECTION: ", event, args.rows[0]);
		const route = 'clients/detail/' + args.rows[0];
		this.router.navigate(route)
	}

	simulateServerAsyncCall(item: any) {
		// random set of names to use for more item detail

		// fill the template on async delay
		return new Promise((resolve) => {
			const itemDetail = item;
			console.log("ITEM DETAIL: ", itemDetail);

			resolve(item);
		});
	}

	changeDetailViewRowCount() {
		const options = this.rowDetailInstance.getOptions();
		if (options && options.panelRows) {
			options.panelRows = this.detailViewRowCount; // change number of rows dynamically
			this.rowDetailInstance.setOptions(options);
		}
	}

	closeAllRowDetail() {
		this.rowDetailInstance.collapseAll();
	}

	displaySpinner(isProcessing) {
		//console.log("SPINNER");
		this.processing = isProcessing;
		this.status = (isProcessing)
			? { text: 'processing...', class: 'alert alert-danger' }
			: { text: 'done', class: 'alert alert-success' };
	}

	getCustomerCallback(data) {
		// totalItems property needs to be filled for pagination to work correctly
		// however we need to force Aurelia to do a dirty check, doing a clone object will do just that
		console.log("...AND HERE!", data);

		this.gridOptions.pagination.totalItems = data.totalRecordCount;

		console.log("this.gridOptions.pagination.totalItems", this.gridOptions.pagination.totalItems)

		this.gridOptions.pagination.totalItems = data['totalRecordCount'];
		this.gridOptions = { ...{}, ...this.gridOptions };

		console.log("data['records']: ", data['records']);

		// once pagination totalItems is filled, we can update the dataset
		this.dataset = data.records;
	}

	getCustomerApiCall(odataQuery) {
		// in your case, you will call your WebAPI function (wich needs to return a Promise)
		// for the demo purpose, we will call a mock WebAPI function
		const headers = this.authService.header();
		this.odataQuery = odataQuery;

		return this.http.fetch(`/api/Client/Index/${odataQuery}`, {
			method: "GET",
			headers
		}).then(response => response.json());
	}

	gridStateChanged(gridStateChanges: GridStateChange) {
		console.log('Client sample, Grid State changed:: ', gridStateChanges);
	}
}

Are you using OData with Pagination? I never tried it with Row Detail but I'm quite sure that you'll have to close all the row detail before changing page, that is probably the error. I think it tries to close a row id that doesn't exist anymore because you changed page, you'll need to close all row detail before changing page.

So you'll have to subscribe to the asg-on-pagination-changed.delegate="paginationChanged($event.detail)" event and then call the collapseAll() method from the core lib rowdetail plugin, you can see how to do that here. Hopefully that is going to be early enough to close all the rows, if that is not early enough then I might need to create a new event to trigger before changing the page action.

If that is not too personal, you should take some print screen of what you're building up (I'd be interested to see). Seems like a project with lots of stuff to put in.

I reckon your on the money... I'll be trying it tonight (aus time) and seeing if it works.. I believe it will..

Ok for those that read this the suggestion worked..

All i did was to add this asg-on-pagination-changed.delegate="paginationChanged($event.detail)" to the template:

<div id="sl-container">
	<aurelia-slickgrid class="col-sm-8"
		column-definitions.bind="columnDefinitions"
		grid-options.bind="gridOptions"
		dataset.bind="dataset"
		asg-on-aurelia-grid-created.delegate="aureliaGridReady($event.detail)"
		asg-on-grid-state-changed.delegate="gridStateChanged($event)"
		asg-on-pagination-changed.delegate="paginationChanged($event.detail)"
		sg-on-selected-rows-changed.delegate="handleRowSelection($event.detail.eventData, $event.detail.args)">
	</aurelia-slickgrid>
</div>

and then in the viewmodel I've added the function that collapses all.

    paginationChanged() {
        if (this.aureliaGrid && this.aureliaGrid.extensionService) {
            const rowDetailInstance = this.aureliaGrid.extensionService.getSlickgridAddonInstance(ExtensionName.rowDetailView);
            rowDetailInstance.collapseAll();
        }

..and it worked.. :) Thanks