diff --git a/package.json b/package.json index ab9630385..e095fafe9 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@angular/platform-browser": "^7.2.4", "@angular/platform-browser-dynamic": "^7.2.4", "@angular/router": "^7.2.4", + "@ng-select/ng-select": "^2.15.3", "@types/flatpickr": "^3.1.2", "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.6", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index e69b35a10..aa1a527fa 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -5,6 +5,7 @@ import { GridClientSideComponent } from './examples/grid-clientside.component'; import { GridColspanComponent } from './examples/grid-colspan.component'; import { GridDraggableGroupingComponent } from './examples/grid-draggrouping.component'; import { GridEditorComponent } from './examples/grid-editor.component'; +import { GridEditorAngularComponent } from './examples/grid-editor-angular.component'; import { GridFormatterComponent } from './examples/grid-formatter.component'; import { GridFrozenComponent } from './examples/grid-frozen.component'; import { GridGroupingComponent } from './examples/grid-grouping.component'; @@ -31,6 +32,7 @@ const routes: Routes = [ { path: 'basic', component: GridBasicComponent }, { path: 'colspan', component: GridColspanComponent }, { path: 'editor', component: GridEditorComponent }, + { path: 'editor-angular', component: GridEditorAngularComponent }, { path: 'formatter', component: GridFormatterComponent }, { path: 'frozen', component: GridFrozenComponent }, { path: 'headerbutton', component: GridHeaderButtonComponent }, diff --git a/src/app/app.component.html b/src/app/app.component.html index 435bdf1de..5e436a0c4 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -91,6 +91,9 @@
  • 21- Row Detail View
  • +
  • + 22- Editors Angular Components +
  • diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ed0a8a47a..1335896b9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -4,16 +4,19 @@ import { FormsModule } from '@angular/forms'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { Injector, APP_INITIALIZER, NgModule } from '@angular/core'; import { LOCATION_INITIALIZED } from '@angular/common'; +import { NgSelectModule } from '@ng-select/ng-select'; import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { AppComponent } from './app.component'; +import { EditorNgSelectComponent } from './examples/editor-ng-select.component'; import { GridAddItemComponent } from './examples/grid-additem.component'; import { GridBasicComponent } from './examples/grid-basic.component'; import { GridClientSideComponent } from './examples/grid-clientside.component'; import { GridColspanComponent } from './examples/grid-colspan.component'; import { GridDraggableGroupingComponent } from './examples/grid-draggrouping.component'; import { GridEditorComponent } from './examples/grid-editor.component'; +import { GridEditorAngularComponent } from './examples/grid-editor-angular.component'; import { GridFormatterComponent } from './examples/grid-formatter.component'; import { GridFrozenComponent } from './examples/grid-frozen.component'; import { GridGraphqlComponent } from './examples/grid-graphql.component'; @@ -68,12 +71,14 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj @NgModule({ declarations: [ AppComponent, + EditorNgSelectComponent, GridAddItemComponent, GridBasicComponent, GridClientSideComponent, GridColspanComponent, GridDraggableGroupingComponent, GridEditorComponent, + GridEditorAngularComponent, GridFormatterComponent, GridFrozenComponent, GridGraphqlComponent, @@ -100,6 +105,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj BrowserModule, FormsModule, HttpClientModule, + NgSelectModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, @@ -119,6 +125,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj ], entryComponents: [ // dynamically created components + EditorNgSelectComponent, RowDetailPreloadComponent, RowDetailViewComponent, ], diff --git a/src/app/examples/custom-angularComponentEditor.ts b/src/app/examples/custom-angularComponentEditor.ts new file mode 100644 index 000000000..5bb683b47 --- /dev/null +++ b/src/app/examples/custom-angularComponentEditor.ts @@ -0,0 +1,157 @@ +import { ComponentRef } from '@angular/core'; +import { + AngularUtilService, + Column, + Editor, + EditorValidator, + EditorValidatorOutput, +} from './../modules/angular-slickgrid'; + +/* + * An example of a 'detached' editor. + * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. + */ +export class CustomAngularComponentEditor implements Editor { + /** Angular Component Reference */ + componentRef: ComponentRef; + + /** default item Id */ + defaultId: string; + + /** default item object */ + defaultItem: any; + + constructor(private args: any) { + this.init(); + } + + /** Angular Util Service */ + get angularUtilService(): AngularUtilService { + return this.columnDef && this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor.params.angularUtilService; + } + + /** Get the Collection */ + get collection(): any[] { + return this.columnDef && this.columnDef && this.columnDef.internalColumnEditor.collection || []; + } + + /** Get Column Definition object */ + get columnDef(): Column { + return this.args && this.args.column || {}; + } + + /** Get Column Editor object */ + get columnEditor(): any { + return this.columnDef && this.columnDef.internalColumnEditor || {}; + } + + get hasAutoCommitEdit() { + return this.args.grid.getOptions().autoCommitEdit; + } + + /** Get the Validator function, can be passed in Editor property or Column Definition */ + get validator(): EditorValidator { + return this.columnEditor.validator || this.columnDef.validator; + } + + init() { + if (!this.columnEditor || !this.columnEditor.params.component) { + throw new Error(`[Angular-Slickgrid] For the Editors.angularComponent to work properly, you need to provide your component to the "component" property and make sure to add it to your "entryComponents" array. + Example: this.columnDefs = [{ id: 'title', field: 'title', editor: { component: MyComponent, model: Editors.angularComponent, collection: [...] },`); + } + if (this.columnEditor && this.columnEditor.params.component) { + this.componentRef = this.columnEditor.params.angularUtilService.createAngularComponentAppendToDom(this.columnEditor.params.component, this.args.container); + Object.assign(this.componentRef.instance, { collection: this.collection }); + + this.componentRef.instance.onModelChanged.subscribe((item) => { + this.save(); + }); + } + } + + save() { + const validation = this.validate(); + if (validation && validation.valid) { + if (this.hasAutoCommitEdit) { + this.args.grid.getEditorLock().commitCurrentEdit(); + } else { + this.args.commitChanges(); + } + } + } + + cancel() { + this.componentRef.instance.selectedId = this.defaultId; + this.componentRef.instance.selectedItem = this.defaultItem; + if (this.args && this.args.cancelChanges) { + this.args.cancelChanges(); + } + } + + hide() { + // optional, implement a hide method on your Angular Component + if (this.componentRef && this.componentRef.instance && typeof this.componentRef.instance.hide === 'function') { + this.componentRef.instance.hide(); + } + } + + show() { + // optional, implement a show method on your Angular Component + if (this.componentRef && this.componentRef.instance && typeof this.componentRef.instance.show === 'function') { + this.componentRef.instance.show(); + } + } + + destroy() { + // destroy the Angular Component + if (this.componentRef && this.componentRef.destroy) { + this.componentRef.destroy(); + } + } + + focus() { + // optional, implement a focus method on your Angular Component + if (this.componentRef && this.componentRef.instance && typeof this.componentRef.instance.focus === 'function') { + this.componentRef.instance.focus(); + } + } + + applyValue(item: any, state: any) { + item[this.columnDef.field] = state; + } + + getValue() { + return this.componentRef.instance.selectedId; + } + + loadValue(item: any) { + const itemObject = item && item[this.columnDef.field]; + this.componentRef.instance.selectedId = itemObject && itemObject.id || ''; + this.componentRef.instance.selectedItem = itemObject && itemObject; + } + + serializeValue(): any { + return this.componentRef.instance.selectedItem; + } + + isValueChanged() { + return (!(this.componentRef.instance.selectedId === '' && this.defaultId == null)) && (this.componentRef.instance.selectedId !== this.defaultId); + } + + validate(): EditorValidatorOutput { + if (this.validator) { + const value = this.componentRef.instance.selectedId; + const validationResults = this.validator(value, this.args); + if (!validationResults.valid) { + return validationResults; + } + } + + // by default the editor is always valid + // if user want it to be required, he would have to provide his own validator + return { + valid: true, + msg: null + }; + } +} diff --git a/src/app/examples/editor-ng-select.component.ts b/src/app/examples/editor-ng-select.component.ts new file mode 100644 index 000000000..ceb99c357 --- /dev/null +++ b/src/app/examples/editor-ng-select.component.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Component({ + template: ` + + + {{ item?.name }} + + ` +}) +export class EditorNgSelectComponent { + selectedId: string; + selectedItem: any; + collection; // this will be filled by the collection of your column definition + onModelChanged = new Subject(); // object + + onChange(item: any) { + this.selectedItem = item; + this.onModelChanged.next(item); + } + + focus() { + // do a focus + } +} diff --git a/src/app/examples/grid-editor-angular.component.html b/src/app/examples/grid-editor-angular.component.html new file mode 100644 index 000000000..c1a858906 --- /dev/null +++ b/src/app/examples/grid-editor-angular.component.html @@ -0,0 +1,49 @@ +
    +

    {{title}}

    +
    +
    + +
    + + + + + +
    + + + + +
    +
    + + +
    +
    + Updated Item: {{updatedObject | json}} +
    +
    + Updated Item: {{alertWarning}} +
    +
    + +
    + + +
    +
    diff --git a/src/app/examples/grid-editor-angular.component.ts b/src/app/examples/grid-editor-angular.component.ts new file mode 100644 index 000000000..b4f414470 --- /dev/null +++ b/src/app/examples/grid-editor-angular.component.ts @@ -0,0 +1,286 @@ +import { Component, Injectable, OnInit } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { TranslateService } from '@ngx-translate/core'; +import { + AngularGridInstance, + AngularUtilService, + Column, + Editors, + FieldType, + Filters, + Formatters, + GridOption, + OnEventArgs, + OperatorType, +} from './../modules/angular-slickgrid'; +import { EditorNgSelectComponent } from './editor-ng-select.component'; +import { CustomAngularComponentEditor } from './custom-angularComponentEditor'; + +// using external non-typed js libraries +declare var Slick: any; + +const NB_ITEMS = 100; + +@Component({ + templateUrl: './grid-editor-angular.component.html' +}) +@Injectable() +export class GridEditorAngularComponent implements OnInit { + title = 'Example 22: Editors with Angular Components'; + subTitle = ` + Grid with Inline Editors and onCellClick actions (Wiki docs). + + `; + + private _commandQueue = []; + angularGrid: AngularGridInstance; + columnDefinitions: Column[]; + gridOptions: GridOption; + dataset: any[]; + gridObj: any; + isAutoEdit = true; + alertWarning: any; + updatedObject: any; + selectedLanguage = 'en'; + assignees = [ + { id: '1', name: 'John' }, + { id: '2', name: 'Pierre' }, + { id: '3', name: 'Paul' }, + ]; + + constructor(private angularUtilService: AngularUtilService, private http: HttpClient, private translate: TranslateService) {} + + ngOnInit(): void { + this.prepareGrid(); + } + + angularGridReady(angularGrid: AngularGridInstance) { + this.angularGrid = angularGrid; + this.gridObj = angularGrid.slickGrid; + } + + prepareGrid() { + this.columnDefinitions = [ + { + id: 'title', + name: 'Title', + field: 'title', + minWidth: 100, + filterable: true, + sortable: true, + type: FieldType.string, + editor: { + model: Editors.longText + }, + onCellChange: (e: Event, args: OnEventArgs) => { + console.log(args); + this.alertWarning = `Updated Title: ${args.dataContext.title}`; + } + }, { + id: 'assignee', + name: 'Assignee', + field: 'assignee', + minWidth: 100, + filterable: true, + sortable: true, + type: FieldType.string, + formatter: Formatters.complexObject, + params: { + complexField: 'assignee.name' + }, + exportWithFormatter: true, + editor: { + model: CustomAngularComponentEditor, + collection: this.assignees, + params: { + angularUtilService: this.angularUtilService, + component: EditorNgSelectComponent, + } + }, + onCellChange: (e: Event, args: OnEventArgs) => { + console.log(args); + this.alertWarning = `Updated Title: ${args.dataContext.title}`; + } + }, { + id: 'duration', + name: 'Duration (days)', + field: 'duration', + minWidth: 100, + filterable: true, + sortable: true, + type: FieldType.number, + filter: { model: Filters.slider, params: { hideSliderNumber: false } }, + editor: { + model: Editors.slider, + minValue: 0, + maxValue: 100, + } + }, { + id: 'complete', + name: '% Complete', + field: 'percentComplete', + minWidth: 100, + filterable: true, + formatter: Formatters.multiple, + type: FieldType.number, + editor: { + model: Editors.singleSelect, + + // We can also add HTML text to be rendered (any bad script will be sanitized) but we have to opt-in, else it will be sanitized + enableRenderHtml: true, + collection: Array.from(Array(101).keys()).map(k => ({ value: k, label: k, symbol: '' })), + customStructure: { + value: 'value', + label: 'label', + labelSuffix: 'symbol' + }, + + // collection: Array.from(Array(101).keys()).map(k => ({ value: k, label: k, labelSuffix: '%' })), + collectionSortBy: { + property: 'label', + sortDesc: true + }, + collectionFilterBy: { + property: 'value', + value: 0, + operator: OperatorType.notEqual + }, + elementOptions: { + maxHeight: 400 + } + }, + params: { + formatters: [Formatters.collectionEditor, Formatters.percentCompleteBar], + } + }, { + id: 'start', + name: 'Start', + field: 'start', + minWidth: 100, + filterable: true, + filter: { model: Filters.compoundDate }, + formatter: Formatters.dateIso, + exportWithFormatter: true, + sortable: true, + type: FieldType.date, + editor: { + model: Editors.date + }, + }, { + id: 'finish', + name: 'Finish', + field: 'finish', + minWidth: 100, + filterable: true, + sortable: true, + filter: { model: Filters.compoundDate }, + formatter: Formatters.dateIso, + exportWithFormatter: true, + type: FieldType.date, + editor: { + model: Editors.date + }, + } + ]; + + this.gridOptions = { + asyncEditorLoading: false, + autoEdit: this.isAutoEdit, + autoCommitEdit: false, + autoResize: { + containerId: 'demo-container', + sidePadding: 15 + }, + rowHeight: 45, // increase row height so that the ng-select fits in the cell + editable: true, + enableCellNavigation: true, + enableColumnPicker: true, + enableExcelCopyBuffer: true, + enableFiltering: true, + editCommandHandler: (item, column, editCommand) => { + this._commandQueue.push(editCommand); + editCommand.execute(); + }, + i18n: this.translate + }; + + this.dataset = this.mockData(NB_ITEMS); + } + + mockData(itemCount, startingIndex = 0) { + // mock a dataset + const tempDataset = []; + for (let i = startingIndex; i < (startingIndex + itemCount); i++) { + const randomYear = 2000 + Math.floor(Math.random() * 10); + const randomMonth = Math.floor(Math.random() * 11); + const randomDay = Math.floor((Math.random() * 29)); + const randomPercent = Math.round(Math.random() * 100); + + tempDataset.push({ + id: i, + title: 'Task ' + i, + assignee: i % 3 ? this.assignees[2] : i % 2 ? this.assignees[1] : this.assignees[0], + duration: Math.round(Math.random() * 100) + '', + percentComplete: randomPercent, + percentCompleteNumber: randomPercent, + start: new Date(randomYear, randomMonth, randomDay), + finish: new Date(randomYear, (randomMonth + 1), randomDay), + effortDriven: (i % 5 === 0), + }); + } + return tempDataset; + } + + onCellChanged(e, args) { + this.updatedObject = args.item; + } + + onCellClicked(e, args) { + const metadata = this.angularGrid.gridService.getColumnFromEventArguments(args); + console.log(metadata); + + if (metadata.columnDef.id === 'edit') { + this.alertWarning = `open a modal window to edit: ${metadata.dataContext.title}`; + + // highlight the row, to customize the color, you can change the SASS variable $row-highlight-background-color + this.angularGrid.gridService.highlightRow(args.row, 1500); + + // you could also select the row, when using "enableCellNavigation: true", it automatically selects the row + // this.angularGrid.gridService.setSelectedRow(args.row); + } else if (metadata.columnDef.id === 'delete') { + if (confirm('Are you sure?')) { + this.angularGrid.gridService.deleteDataGridItemById(metadata.dataContext.id); + } + } + } + + onCellValidation(e, args) { + alert(args.validationResults.msg); + } + + changeAutoCommit() { + this.gridOptions.autoCommitEdit = !this.gridOptions.autoCommitEdit; + this.gridObj.setOptions({ + autoCommitEdit: this.gridOptions.autoCommitEdit + }); + return true; + } + + setAutoEdit(isAutoEdit) { + this.isAutoEdit = isAutoEdit; + this.gridObj.setOptions({ autoEdit: isAutoEdit }); // change the grid option dynamically + return true; + } + + undo() { + const command = this._commandQueue.pop(); + if (command && Slick.GlobalEditorLock.cancelCurrentEdit()) { + command.undo(); + this.gridObj.gotoCell(command.row, command.cell, false); + } + } +} diff --git a/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts b/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts index 9f435db03..ebc97fcbd 100644 --- a/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts +++ b/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts @@ -316,7 +316,7 @@ export class RowDetailViewExtension implements Extension { private renderPreloadView() { const containerElements = document.getElementsByClassName(`${PRELOAD_CONTAINER_PREFIX}`); if (containerElements && containerElements.length) { - this.angularUtilService.appendAngularComponentToDom(this._preloadComponent, containerElements[0]); + this.angularUtilService.createAngularComponentAppendToDom(this._preloadComponent, containerElements[0]); } } @@ -324,7 +324,7 @@ export class RowDetailViewExtension implements Extension { private renderViewModel(item: any) { const containerElements = document.getElementsByClassName(`${ROW_DETAIL_CONTAINER_PREFIX}${item.id}`); if (containerElements && containerElements.length) { - const compRef = this.angularUtilService.appendAngularComponentToDom(this._viewComponent, containerElements[0]); + const compRef = this.angularUtilService.createAngularComponentAppendToDom(this._viewComponent, containerElements[0]); Object.assign(compRef.instance, { model: item }); const viewObj = this._views.find((obj) => obj.id === item.id); diff --git a/src/app/modules/angular-slickgrid/formatters/complexObjectFormatter.ts b/src/app/modules/angular-slickgrid/formatters/complexObjectFormatter.ts index 324b9b2d7..9298e2bc4 100644 --- a/src/app/modules/angular-slickgrid/formatters/complexObjectFormatter.ts +++ b/src/app/modules/angular-slickgrid/formatters/complexObjectFormatter.ts @@ -6,10 +6,12 @@ export const complexObjectFormatter: Formatter = (row: number, cell: number, val return ''; } + const columnParams = columnDef.params || {}; + const complexField = columnParams && columnParams.complexField || columnDef.field; + if (columnDef.labelKey) { - return dataContext[columnDef.field] && dataContext[columnDef.field][columnDef.labelKey]; + return dataContext[complexField] && dataContext[complexField][columnDef.labelKey]; } - const complexField = columnDef.field || ''; return complexField.split('.').reduce((obj, i) => (obj ? obj[i] : ''), dataContext); }; diff --git a/src/app/modules/angular-slickgrid/formatters/index.ts b/src/app/modules/angular-slickgrid/formatters/index.ts index 1e2b6e37c..4dc618b04 100644 --- a/src/app/modules/angular-slickgrid/formatters/index.ts +++ b/src/app/modules/angular-slickgrid/formatters/index.ts @@ -59,7 +59,13 @@ export const Formatters = { /** When value is filled (true), it will display a Font-Awesome icon (fa-check) */ checkmark: checkmarkFormatter, - /** Takes a complex data object and return the data under that property (for example: "user.firstName" will return the first name "John") */ + /** + * Takes a complex data object and return the data under that property (for example: "user.firstName" will return the first name "John") + * You can pass the complex structure in the "field" or the "params: { complexField: string }" properties. + * For example:: + * this.columnDefs = [{ id: 'username', field: 'user.firstName', ... }] + * OR this.columnDefs = [{ id: 'username', field: 'user', params: { complexField: 'user.firstName' }, ... }] + */ complexObject: complexObjectFormatter, /** diff --git a/src/app/modules/angular-slickgrid/services/angularUtilService.ts b/src/app/modules/angular-slickgrid/services/angularUtilService.ts index 3edaa44fe..1b23f170e 100644 --- a/src/app/modules/angular-slickgrid/services/angularUtilService.ts +++ b/src/app/modules/angular-slickgrid/services/angularUtilService.ts @@ -1,4 +1,4 @@ -import { ApplicationRef, ComponentFactoryResolver, EmbeddedViewRef, Injectable, Injector } from '@angular/core'; +import { ApplicationRef, ComponentFactoryResolver, ComponentRef, EmbeddedViewRef, Injectable, Injector } from '@angular/core'; @Injectable() export class AngularUtilService { @@ -9,7 +9,7 @@ export class AngularUtilService { ) { } // ref https://hackernoon.com/angular-pro-tip-how-to-dynamically-create-components-in-body-ba200cc289e6 - appendAngularComponentToDom(component: any, targetElement?: HTMLElement | Element) { + createAngularComponentAppendToDom(component: any, targetElement?: HTMLElement | Element): ComponentRef { // Create a component reference from the component const componentRef = this.compFactoryResolver .resolveComponentFactory(component) diff --git a/src/styles.scss b/src/styles.scss index 756f526ce..d4d37de33 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -20,3 +20,5 @@ .faded:hover { opacity: 0.5; } + +@import "~@ng-select/ng-select/themes/default.theme.css";