From de52614b23ef1099fd1108699c8afc939ea5ab8b Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Tue, 8 Oct 2019 17:05:23 -0400 Subject: [PATCH 1/2] feat(export): add delimiter/listSeparator override to use with GraphQL --- .../components/angular-slickgrid.component.ts | 9 +++- .../models/exportOption.interface.ts | 3 ++ .../models/graphqlResult.interface.ts | 8 ++-- .../models/graphqlServiceOption.interface.ts | 10 ++++- .../services/__tests__/export.service.spec.ts | 29 +++++++++++++ .../__tests__/graphql.service.spec.ts | 42 +++++++++++++++---- .../services/export.service.ts | 32 +++++++------- .../services/graphql.service.ts | 13 +++++- 8 files changed, 114 insertions(+), 32 deletions(-) diff --git a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts index aaf6e67dd..e1ec31006 100644 --- a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts +++ b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts @@ -398,10 +398,17 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn if (backendApi) { // internalPostProcess only works (for now) with a GraphQL Service, so make sure it is that type if (backendApi && backendApi.service instanceof GraphqlService) { - backendApi.internalPostProcess = (processResult: any) => { + backendApi.internalPostProcess = (processResult: GraphqlResult) => { const datasetName = (backendApi && backendApi.service && typeof backendApi.service.getDatasetName === 'function') ? backendApi.service.getDatasetName() : ''; if (processResult && processResult.data && processResult.data[datasetName]) { this._dataset = processResult.data[datasetName].nodes; + if (processResult.data[datasetName].listSeparator) { + // if the "listSeparator" is available in the GraphQL result, we'll override the ExportOptions Delimiter with this new info + if (!this.gridOptions.exportOptions) { + this.gridOptions.exportOptions = {}; + } + this.gridOptions.exportOptions.delimiterOverride = processResult.data[datasetName].listSeparator.toString(); + } this.refreshGridData(this._dataset, processResult.data[datasetName].totalCount); } else { this._dataset = []; diff --git a/src/app/modules/angular-slickgrid/models/exportOption.interface.ts b/src/app/modules/angular-slickgrid/models/exportOption.interface.ts index af9ce6c74..f70733801 100644 --- a/src/app/modules/angular-slickgrid/models/exportOption.interface.ts +++ b/src/app/modules/angular-slickgrid/models/exportOption.interface.ts @@ -4,6 +4,9 @@ export interface ExportOption { /** export delimiter, can be (comma, tab, ... or even custom string). */ delimiter?: DelimiterType | string; + /** Allows you to override for the export delimiter, useful when adding the "listSeparator" to the GraphQL query */ + delimiterOverride?: DelimiterType | string; + /** Defaults to false, which leads to all Formatters of the grid being evaluated on export. You can also override a column by changing the propery on the column itself */ exportWithFormatter?: boolean; diff --git a/src/app/modules/angular-slickgrid/models/graphqlResult.interface.ts b/src/app/modules/angular-slickgrid/models/graphqlResult.interface.ts index 298494a70..1e1e601f5 100644 --- a/src/app/modules/angular-slickgrid/models/graphqlResult.interface.ts +++ b/src/app/modules/angular-slickgrid/models/graphqlResult.interface.ts @@ -1,14 +1,16 @@ +import { DelimiterType } from './delimiterType.enum'; import { Metrics } from './metrics.interface'; import { Statistic } from './statistic.interface'; export interface GraphqlResult { data: { [datasetName: string]: { - nodes: any[], + nodes: any[]; pageInfo: { hasNextPage: boolean; - }, - totalCount: number + }; + listSeparator?: DelimiterType; + totalCount: number; } }; diff --git a/src/app/modules/angular-slickgrid/models/graphqlServiceOption.interface.ts b/src/app/modules/angular-slickgrid/models/graphqlServiceOption.interface.ts index 1cd0efc79..496670201 100644 --- a/src/app/modules/angular-slickgrid/models/graphqlServiceOption.interface.ts +++ b/src/app/modules/angular-slickgrid/models/graphqlServiceOption.interface.ts @@ -7,11 +7,17 @@ import { GraphqlPaginationOption } from './graphqlPaginationOption.interface'; export interface GraphqlServiceOption extends BackendServiceOption { /** - * When using Translation, we probably want to add locale in the query for the filterBy/orderBy to work - * ex.: users(first: 10, offset: 0, locale: "en-CA", filterBy: [{field: name, operator: EQ, value:"John"}]) { + * When using Translation, we probably want to add locale as a query parameter for the filterBy/orderBy to work + * ex.: users(first: 10, offset: 0, locale: "en-CA", filterBy: [{field: name, operator: EQ, value:"John"}]) { } */ addLocaleIntoQuery?: boolean; + /** + * Add the Current User List Separator to the result query (in English the separator is comma ","). + * This is useful to set the "delimiter" property when using Export CSV, for example French uses semicolon ";" as a delimiter/separator + */ + addListSeparator?: boolean; + /** What is the dataset, this is required for the GraphQL query to be built */ datasetName?: string; diff --git a/src/app/modules/angular-slickgrid/services/__tests__/export.service.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/export.service.spec.ts index af7ded41c..d63413eb3 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/export.service.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/export.service.spec.ts @@ -250,6 +250,10 @@ describe('ExportService', () => { describe('startDownloadFile call after all private methods ran ', () => { let mockCollection: any[]; + beforeEach(() => { + mockGridOptions.exportOptions = { delimiterOverride: '' }; + }); + it(`should have the Order exported correctly with multiple formatters which have 1 of them returning an object with a text property (instead of simple string)`, (done) => { mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }]; jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); @@ -274,6 +278,31 @@ describe('ExportService', () => { }); }); + it(`should have the Order exported correctly with multiple formatters and use a different delimiter when "delimiterOverride" is provided`, (done) => { + mockGridOptions.exportOptions = { delimiterOverride: DelimiterType.doubleSemicolon }; + mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }]; + jest.spyOn(dataViewStub, 'getLength').mockReturnValue(mockCollection.length); + jest.spyOn(dataViewStub, 'getItem').mockReturnValue(null).mockReturnValueOnce(mockCollection[0]); + const spyOnAfter = jest.spyOn(service.onGridAfterExportToFile, 'next'); + const spyUrlCreate = jest.spyOn(URL, 'createObjectURL'); + const spyDownload = jest.spyOn(service, 'startDownloadFile'); + + const optionExpectation = { filename: 'export.csv', format: 'csv', useUtf8WithBom: false }; + const contentExpectation = + `"User Id";;"FirstName";;"LastName";;"Position";;"Order" + ="1E06";;"John";;"Z";;"SALES_REP";;"10"`; + + service.init(gridStub, dataViewStub); + service.exportToFile(mockExportCsvOptions); + + setTimeout(() => { + expect(spyOnAfter).toHaveBeenCalledWith(optionExpectation); + expect(spyUrlCreate).toHaveBeenCalledWith(mockCsvBlob); + expect(spyDownload).toHaveBeenCalledWith({ ...optionExpectation, content: removeMultipleSpaces(contentExpectation) }); + done(); + }); + }); + it(`should have the UserId escape with equal sign showing as prefix, to avoid Excel casting the value 1E06 to 1 exponential 6, when "exportCsvForceToKeepAsString" is enable in its column definition`, (done) => { mockCollection = [{ id: 0, userId: '1E06', firstName: 'John', lastName: 'Z', position: 'SALES_REP', order: 10 }]; diff --git a/src/app/modules/angular-slickgrid/services/__tests__/graphql.service.spec.ts b/src/app/modules/angular-slickgrid/services/__tests__/graphql.service.spec.ts index cf8b36677..485db6b7a 100644 --- a/src/app/modules/angular-slickgrid/services/__tests__/graphql.service.spec.ts +++ b/src/app/modules/angular-slickgrid/services/__tests__/graphql.service.spec.ts @@ -243,6 +243,16 @@ describe('GraphqlService', () => { expect(removeSpaces(query)).toBe(removeSpaces(expectation)); }); + it('should include "listSeparator" in the query string when option "addListSeparator" is enabled', () => { + const expectation = `query{ users(first:10, offset:0){ listSeparator, totalCount, nodes{ id, field1, field2 }}}`; + const columns = [{ id: 'field1', field: 'field1', width: 100 }, { id: 'field2', field: 'field2', width: 100 }]; + + service.init({ datasetName: 'users', columnDefinitions: columns, addListSeparator: true }, paginationOptions, gridStub); + const query = service.buildQuery(); + + expect(removeSpaces(query)).toBe(removeSpaces(expectation)); + }); + it('should include default locale "en" in the query string when option "addLocaleIntoQuery" is enabled and i18n is not defined', () => { const expectation = `query{ users(first:10, offset:0, locale: "en"){ totalCount, nodes{ id, field1, field2 }}}`; const columns = [{ id: 'field1', field: 'field1', width: 100 }, { id: 'field2', field: 'field2', width: 100 }]; @@ -415,6 +425,7 @@ describe('GraphqlService', () => { }); it('should return a query with the new filter', () => { + serviceOptions.addListSeparator = false; const expectation = `query{users(first:10, offset:0, filterBy:[{field:gender, operator:EQ, value:"female"}]) { totalCount,nodes{ id,field1,field2 } }}`; const querySpy = jest.spyOn(service, 'buildQuery'); const resetSpy = jest.spyOn(service, 'resetPaginationOptions'); @@ -441,9 +452,10 @@ describe('GraphqlService', () => { }); it('should return a query with a new filter when previous filters exists', () => { + serviceOptions.addListSeparator = true; const expectation = `query{users(first:10, offset:0, filterBy:[{field:gender, operator:EQ, value:"female"}, {field:firstName, operator:StartsWith, value:"John"}]) - { totalCount,nodes{ id,field1,field2 } }}`; + { listSeparator, totalCount,nodes{ id,field1,field2 } }}`; const querySpy = jest.spyOn(service, 'buildQuery'); const resetSpy = jest.spyOn(service, 'resetPaginationOptions'); const mockColumn = { id: 'gender', field: 'gender' } as Column; @@ -476,6 +488,7 @@ describe('GraphqlService', () => { describe('processOnPaginationChanged method', () => { it('should return a query with the new pagination', () => { + serviceOptions.addListSeparator = false; const expectation = `query{users(first:20, offset:40) { totalCount,nodes { id, field1, field2 }}}`; const querySpy = jest.spyOn(service, 'buildQuery'); @@ -489,7 +502,8 @@ describe('GraphqlService', () => { }); it('should return a query with the new pagination and use pagination size options that was passed to service options when it is not provided as argument to "processOnPaginationChanged"', () => { - const expectation = `query{users(first:10, offset:20) { totalCount,nodes { id, field1, field2 }}}`; + serviceOptions.addListSeparator = true; + const expectation = `query{users(first:10, offset:20) { listSeparator, totalCount,nodes { id, field1, field2 }}}`; const querySpy = jest.spyOn(service, 'buildQuery'); service.init(serviceOptions, paginationOptions, gridStub); @@ -519,6 +533,7 @@ describe('GraphqlService', () => { describe('processOnSortChanged method', () => { it('should return a query with the new sorting when using single sort', () => { + serviceOptions.addListSeparator = false; const expectation = `query{ users(first:10, offset:0, orderBy:[{field:gender, direction: DESC}]) { totalCount,nodes{ id,field1,field2 } }}`; const querySpy = jest.spyOn(service, 'buildQuery'); const mockColumn = { id: 'gender', field: 'gender' } as Column; @@ -532,9 +547,10 @@ describe('GraphqlService', () => { }); it('should return a query with the multiple new sorting when using multiColumnSort', () => { + serviceOptions.addListSeparator = true; const expectation = `query{ users(first:10, offset:0, orderBy:[{field:gender, direction: DESC}, {field:firstName, direction: ASC}]) { - totalCount,nodes{ id,field1,field2 } }}`; + listSeparator, totalCount,nodes{ id,field1,field2 } }}`; const querySpy = jest.spyOn(service, 'buildQuery'); const mockColumn = { id: 'gender', field: 'gender' } as Column; const mockColumnName = { id: 'firstName', field: 'firstName' } as Column; @@ -553,6 +569,7 @@ describe('GraphqlService', () => { describe('updateFilters method', () => { beforeEach(() => { serviceOptions.columnDefinitions = [{ id: 'company', field: 'company' }, { id: 'gender', field: 'gender' }, { id: 'name', field: 'name' }]; + serviceOptions.addListSeparator = false; }); it('should throw an error when filter columnId is not found to be part of the column definitions', () => { @@ -569,7 +586,9 @@ describe('GraphqlService', () => { }); it('should return a query with the new filter when filters are passed as a filter trigger by a filter event and is of type ColumnFilters', () => { - const expectation = `query{users(first:10, offset:0, filterBy:[{field:gender, operator:EQ, value:"female"}]) { totalCount,nodes{ id,company,gender,name } }}`; + serviceOptions.addListSeparator = true; + const expectation = `query{users(first:10, offset:0, filterBy:[{field:gender, operator:EQ, value:"female"}]) { + listSeparator, totalCount,nodes{ id,company,gender,name } }}`; const mockColumn = { id: 'gender', field: 'gender' } as Column; const mockColumnFilters = { gender: { columnId: 'gender', columnDef: mockColumn, searchTerms: ['female'], operator: 'EQ' }, @@ -913,7 +932,8 @@ describe('GraphqlService', () => { }); it('should return a query without any sorting after clearFilters was called', () => { - const expectation = `query{ users(first:10,offset:0) { totalCount,nodes{id, company, gender,name} }}`; + serviceOptions.addListSeparator = true; + const expectation = `query{ users(first:10,offset:0) { listSeparator, totalCount, nodes {id, company, gender,name} }}`; const mockColumn = { id: 'gender', field: 'gender' } as Column; const mockColumnFilters = { gender: { columnId: 'gender', columnDef: mockColumn, searchTerms: ['female'], operator: 'EQ' }, @@ -933,10 +953,13 @@ describe('GraphqlService', () => { describe('presets', () => { beforeEach(() => { serviceOptions.columnDefinitions = [{ id: 'company', field: 'company' }, { id: 'gender', field: 'gender' }, { id: 'duration', field: 'duration' }, { id: 'startDate', field: 'startDate' }]; + serviceOptions.addListSeparator = false; }); it('should return a query with search having a range of exclusive numbers when the search value contains 2 (..) to represent a range of numbers', () => { - const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:GT, value:"2"}, {field:duration, operator:LT, value:"33"}]) { totalCount,nodes{ id,company,gender,duration,startDate } }}`; + serviceOptions.addListSeparator = true; + const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:GT, value:"2"}, {field:duration, operator:LT, value:"33"}]) { + listSeparator, totalCount,nodes{ id,company,gender,duration,startDate } }}`; const presetFilters = [ { columnId: 'duration', searchTerms: ['2..33'] }, ] as CurrentFilter[]; @@ -1044,12 +1067,14 @@ describe('GraphqlService', () => { describe('updateSorters method', () => { beforeEach(() => { serviceOptions.columnDefinitions = [{ id: 'company', field: 'company' }, { id: 'gender', field: 'gender' }, { id: 'name', field: 'name' }]; + serviceOptions.addListSeparator = false; }); it('should return a query with the multiple new sorting when using multiColumnSort', () => { + serviceOptions.addListSeparator = true; const expectation = `query{ users(first:10, offset:0, orderBy:[{field:gender, direction: DESC}, {field:firstName, direction: ASC}]) { - totalCount,nodes{ id, company, gender, name } }}`; + listSeparator, totalCount,nodes{ id, company, gender, name } }}`; const mockColumnSort = [ { columnId: 'gender', sortCol: { id: 'gender', field: 'gender' }, sortAsc: false }, { columnId: 'firstName', sortCol: { id: 'firstName', field: 'firstName' }, sortAsc: true } @@ -1136,8 +1161,9 @@ describe('GraphqlService', () => { }); it('should return a query without any sorting after clearSorters was called', () => { + serviceOptions.addListSeparator = true; const expectation = `query { users(first:10, offset:0) { - totalCount, nodes { id,company,gender,name }}}`; + listSeparator, totalCount, nodes { id,company,gender,name }}}`; const mockColumnSort = [ { columnId: 'gender', sortCol: { id: 'gender', field: 'gender' }, sortAsc: false }, { columnId: 'firstName', sortCol: { id: 'firstName', field: 'firstName' }, sortAsc: true } diff --git a/src/app/modules/angular-slickgrid/services/export.service.ts b/src/app/modules/angular-slickgrid/services/export.service.ts index adebcac5e..4733f5213 100644 --- a/src/app/modules/angular-slickgrid/services/export.service.ts +++ b/src/app/modules/angular-slickgrid/services/export.service.ts @@ -1,5 +1,8 @@ import { Injectable, Optional } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { TextEncoder } from 'text-encoding-utf-8'; +import { Subject } from 'rxjs'; + import { Column, ExportOption, @@ -10,8 +13,6 @@ import { } from './../models/index'; import { Constants } from './../constants'; import { addWhiteSpaces, htmlEntityDecode, sanitizeHtmlToText, titleCase } from './../services/utilities'; -import { TextEncoder } from 'text-encoding-utf-8'; -import { Subject } from 'rxjs'; // using external non-typed js libraries declare let $: any; @@ -23,6 +24,8 @@ export interface ExportColumnHeader { @Injectable() export class ExportService { + private _delimiter = ','; + private _fileFormat = FileType.csv; private _lineCarriageReturn = '\n'; private _dataView: any; private _grid: any; @@ -77,6 +80,8 @@ export class ExportService { return new Promise((resolve, reject) => { this.onGridBeforeExportToFile.next(true); this._exportOptions = $.extend(true, {}, this._gridOptions.exportOptions, options); + this._delimiter = this._exportOptions.delimiterOverride || this._exportOptions.delimiter || ''; + this._fileFormat = this._exportOptions.format || FileType.csv; // get the CSV output from the grid data const dataOutput = this.getDataOutput(); @@ -86,8 +91,8 @@ export class ExportService { setTimeout(() => { try { const downloadOptions = { - filename: `${this._exportOptions.filename}.${this._exportOptions.format}`, - format: this._exportOptions.format, + filename: `${this._exportOptions.filename}.${this._fileFormat}`, + format: this._fileFormat, useUtf8WithBom: this._exportOptions.hasOwnProperty('useUtf8WithBom') ? this._exportOptions.useUtf8WithBom : true }; @@ -165,8 +170,6 @@ export class ExportService { private getDataOutput(): string { const columns = this._grid.getColumns() || []; - const delimiter = this._exportOptions.delimiter || ''; - const format = this._exportOptions.format || ''; // Group By text, it could be set in the export options or from translation or if nothing is found then use the English constant text let groupByColumnHeader = this._exportOptions.groupingColumnHeaderTitle; @@ -177,7 +180,7 @@ export class ExportService { } // a CSV needs double quotes wrapper, the other types do not need any wrapper - this._exportQuoteWrapper = (format === FileType.csv) ? '"' : ''; + this._exportQuoteWrapper = (this._fileFormat === FileType.csv) ? '"' : ''; // data variable which will hold all the fields data of a row let outputDataString = ''; @@ -187,7 +190,7 @@ export class ExportService { const grouping = this._dataView.getGrouping(); if (grouping && Array.isArray(grouping) && grouping.length > 0) { this._hasGroupedItems = true; - outputDataString += (format === FileType.csv) ? `"${groupByColumnHeader}"${delimiter}` : `${groupByColumnHeader}${delimiter}`; + outputDataString += (this._fileFormat === FileType.csv) ? `"${groupByColumnHeader}"${this._delimiter}` : `${groupByColumnHeader}${this._delimiter}`; } else { this._hasGroupedItems = false; } @@ -199,7 +202,7 @@ export class ExportService { const outputHeaderTitles = this._columnHeaders.map((header) => { return this._exportQuoteWrapper + header.title + this._exportQuoteWrapper; }); - outputDataString += (outputHeaderTitles.join(delimiter) + this._lineCarriageReturn); + outputDataString += (outputHeaderTitles.join(this._delimiter) + this._lineCarriageReturn); } // Populate the rest of the Grid Data @@ -276,8 +279,6 @@ export class ExportService { private readRegularRowData(columns: Column[], row: number, itemObj: any) { let idx = 0; const rowOutputStrings = []; - const delimiter = this._exportOptions.delimiter; - const format = this._exportOptions.format; const exportQuoteWrapper = this._exportQuoteWrapper; for (let col = 0, ln = columns.length; col < ln; col++) { @@ -291,7 +292,7 @@ export class ExportService { // if we are grouping and are on 1st column index, we need to skip this column since it will be used later by the grouping text:: Group by [columnX] if (this._hasGroupedItems && idx === 0) { - const emptyValue = format === FileType.csv ? `""` : ''; + const emptyValue = this._fileFormat === FileType.csv ? `""` : ''; rowOutputStrings.push(emptyValue); } @@ -341,7 +342,7 @@ export class ExportService { } // when CSV we also need to escape double quotes twice, so " becomes "" - if (format === FileType.csv && itemData) { + if (this._fileFormat === FileType.csv && itemData) { itemData = itemData.toString().replace(/"/gi, `""`); } @@ -353,7 +354,7 @@ export class ExportService { idx++; } - return rowOutputStrings.join(delimiter); + return rowOutputStrings.join(this._delimiter); } /** @@ -363,11 +364,10 @@ export class ExportService { private readGroupedTitleRow(itemObj: any) { let groupName = sanitizeHtmlToText(itemObj.title); const exportQuoteWrapper = this._exportQuoteWrapper; - const format = this._exportOptions.format; groupName = addWhiteSpaces(5 * itemObj.level) + groupName; - if (format === FileType.csv) { + if (this._fileFormat === FileType.csv) { // when CSV we also need to escape double quotes twice, so " becomes "" groupName = groupName.toString().replace(/"/gi, `""`); } diff --git a/src/app/modules/angular-slickgrid/services/graphql.service.ts b/src/app/modules/angular-slickgrid/services/graphql.service.ts index 9693d1246..29319c25d 100644 --- a/src/app/modules/angular-slickgrid/services/graphql.service.ts +++ b/src/app/modules/angular-slickgrid/services/graphql.service.ts @@ -101,19 +101,28 @@ export class GraphqlService implements BackendService { } const filters = this.buildFilterQuery(columnIds); + let graphqlFields = []; if (this.options.isWithCursor) { // ...pageInfo { hasNextPage, endCursor }, edges { cursor, node { _filters_ } } const pageInfoQb = new QueryBuilder('pageInfo'); pageInfoQb.find('hasNextPage', 'endCursor'); dataQb.find(['cursor', { node: filters }]); - datasetQb.find(['totalCount', pageInfoQb, dataQb]); + graphqlFields = ['totalCount', pageInfoQb, dataQb]; } else { // ...nodes { _filters_ } dataQb.find(filters); - datasetQb.find(['totalCount', dataQb]); + graphqlFields = ['totalCount', dataQb]; } + // are we adding the current user listSeparator to the fields query result? + if (this.options.addListSeparator) { + graphqlFields.unshift('listSeparator'); + } + + // properties to be returned by the query + datasetQb.find(graphqlFields); + // add dataset filters, could be Pagination and SortingFilters and/or FieldFilters let datasetFilters: GraphqlDatasetFilter = {}; From ba6082cb68a7552c7688dda510df42a528455a26 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Tue, 8 Oct 2019 17:08:36 -0400 Subject: [PATCH 2/2] feat(example): add Bootstrap Dropdown Action demo, closes #304 --- src/app/app.module.ts | 3 + .../custom-actionFormatter.component.ts | 15 +++++ src/app/examples/grid-angular.component.html | 1 + src/app/examples/grid-angular.component.ts | 56 ++++++++++++++++++- 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/app/examples/custom-actionFormatter.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index c192170cd..d1d168f1a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -10,6 +10,7 @@ import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-transla import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { AppComponent } from './app.component'; +import { CustomActionFormatterComponent } from './examples/custom-actionFormatter.component'; import { CustomTitleFormatterComponent } from './examples/custom-titleFormatter.component'; import { EditorNgSelectComponent } from './examples/editor-ng-select.component'; import { FilterNgSelectComponent } from './examples/filter-ng-select.component'; @@ -77,6 +78,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj @NgModule({ declarations: [ AppComponent, + CustomActionFormatterComponent, CustomTitleFormatterComponent, EditorNgSelectComponent, FilterNgSelectComponent, @@ -137,6 +139,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj ], entryComponents: [ // dynamically created components + CustomActionFormatterComponent, CustomTitleFormatterComponent, EditorNgSelectComponent, FilterNgSelectComponent, diff --git a/src/app/examples/custom-actionFormatter.component.ts b/src/app/examples/custom-actionFormatter.component.ts new file mode 100644 index 000000000..7a45e704c --- /dev/null +++ b/src/app/examples/custom-actionFormatter.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + template: `` +}) +export class CustomActionFormatterComponent { + parent: any; +} diff --git a/src/app/examples/grid-angular.component.html b/src/app/examples/grid-angular.component.html index 0081a630b..000831873 100644 --- a/src/app/examples/grid-angular.component.html +++ b/src/app/examples/grid-angular.component.html @@ -48,6 +48,7 @@

{{title}}

(sgOnCellChange)="onCellChanged($event.detail.eventData, $event.detail.args)" (sgOnClick)="onCellClicked($event.detail.eventData, $event.detail.args)" (sgOnValidationError)="onCellValidation($event.detail.eventData, $event.detail.args)" + (sgOnActiveCellChanged)="onActiveCellChanged($event.detail.eventData, $event.detail.args)" [columnDefinitions]="columnDefinitions" [gridOptions]="gridOptions" [dataset]="dataset"> diff --git a/src/app/examples/grid-angular.component.ts b/src/app/examples/grid-angular.component.ts index 48dc7f383..d0e632c0e 100644 --- a/src/app/examples/grid-angular.component.ts +++ b/src/app/examples/grid-angular.component.ts @@ -7,11 +7,13 @@ import { Editors, FieldType, Filters, + Formatter, Formatters, GridOption, OnEventArgs, } from './../modules/angular-slickgrid'; import { EditorNgSelectComponent } from './editor-ng-select.component'; +import { CustomActionFormatterComponent } from './custom-actionFormatter.component'; import { CustomAngularComponentEditor } from './custom-angularComponentEditor'; import { CustomAngularComponentFilter } from './custom-angularComponentFilter'; import { CustomTitleFormatterComponent } from './custom-titleFormatter.component'; @@ -23,6 +25,17 @@ declare var $: any; const NB_ITEMS = 100; +const customActionFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid: any) => { + // use the same button text "Action" as the "CustomActionFormatterComponent" button text + // we basically recreate a dropdown on top of this one here which is just an empty one to show something in the grid + return ``; +}; + @Component({ templateUrl: './grid-angular.component.html', styleUrls: ['./grid-angular.component.scss'], @@ -218,7 +231,8 @@ export class GridAngularComponent implements OnInit { editor: { model: Editors.date }, - } + }, + { id: 'action', name: 'Action', field: 'id', formatter: customActionFormatter, width: 70 } ]; this.gridOptions = { @@ -329,8 +343,46 @@ export class GridAngularComponent implements OnInit { const componentOutput = this.angularUtilService.createAngularComponent(colDef.params.component); Object.assign(componentOutput.componentRef.instance, { item: dataContext }); - // use a delay to make sure Angular ran at least a full cycle and it finished rendering the Component + // use a delay to make sure Angular ran at least a full cycle and make sure it finished rendering the Component setTimeout(() => $(cellNode).empty().html(componentOutput.domElement)); } } + + /* Create an Action Dropdown Menu */ + deleteCell(rowNumber: number) { + const item = this.angularGrid.dataView.getItem(rowNumber); + this.angularGrid.gridService.deleteItemById(item.id); + } + + onActiveCellChanged(event, args) { + if (args.cell !== 6) { + return; // don't do anything unless it's the Action column which is at position 6 in this grid + } + + $('#myDrop').remove(); // make sure to remove previous Action dropdown, you don't want to have 100 after a 100 clicks... + const cell = args.cell; + const row = args.row; + + // hide the dropdown we created as a Formatter, we'll redisplay it later + const cellPos = $(`#myDrop-r${row}-c${cell}`).offset(); + + const componentOutput = this.angularUtilService.createAngularComponent(CustomActionFormatterComponent); + + // pass "this" and the row number to the Component instance (CustomActionFormatter) so that we can call "parent.deleteCell(row)" with (click) + Object.assign(componentOutput.componentRef.instance, { parent: this, row: args.row }); + + // use a delay to make sure Angular ran at least a full cycle and make sure it finished rendering the Component before using it + setTimeout(() => { + const elm = $(componentOutput.domElement); + elm.appendTo('body'); + elm.css('position', 'absolute'); + elm.css('top', cellPos.top + 5); + elm.css('left', cellPos.left); + $('#myDrop').addClass('open'); + + $('#myDrop').on('hidden.bs.dropdown', () => { + $(`#myDrop-r${row}-c${cell}`).show(); + }); + }); + } }