From 0779250652e1ff792407dffd9d69439307e1dba1 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Fri, 15 Feb 2019 20:19:02 -0500 Subject: [PATCH 1/4] refactor(extension): extract angular function into it's own service - the method "appendAngularComponentToDom" can be used for much more than just the RowDetailViewExtension, so let's extract it into it's own service --- src/app/examples/rowdetail-view.component.ts | 10 ++++- .../components/angular-slickgrid.component.ts | 12 +++--- .../extensions/rowDetailViewExtension.ts | 38 ++++--------------- .../modules/angular-slickgrid.module.ts | 2 + .../services/angularUtilService.ts | 34 +++++++++++++++++ .../services/extension.service.ts | 3 -- .../angular-slickgrid/services/index.ts | 1 + 7 files changed, 61 insertions(+), 39 deletions(-) create mode 100644 src/app/modules/angular-slickgrid/services/angularUtilService.ts diff --git a/src/app/examples/rowdetail-view.component.ts b/src/app/examples/rowdetail-view.component.ts index 6d5086e56..ef4149465 100644 --- a/src/app/examples/rowdetail-view.component.ts +++ b/src/app/examples/rowdetail-view.component.ts @@ -4,7 +4,15 @@ import { Component } from '@angular/core'; templateUrl: './rowdetail-view.component.html' }) export class RowDetailViewComponent { - model: { duration: Date; percentComplete: number; reporter: string; start: Date; finish: Date; effortDriven: boolean; assignee: string; title: string; }; + model: { + duration: Date; + percentComplete: number; + reporter: string; + start: Date; + finish: Date; + effortDriven: boolean; + assignee: string; title: string; + }; constructor() {} 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 e2aaf4823..759678c1a 100644 --- a/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts +++ b/src/app/modules/angular-slickgrid/components/angular-slickgrid.component.ts @@ -27,6 +27,7 @@ import { SlickgridConfig } from '../slickgrid-config'; import { isObservable, Observable, Subscription } from 'rxjs'; // Services +import { AngularUtilService } from './../services/angularUtilService'; import { ExportService } from './../services/export.service'; import { ExtensionService } from '../services/extension.service'; import { ExtensionUtility } from '../extensions/extensionUtility'; @@ -67,6 +68,7 @@ const slickgridEventPrefix = 'sg'; templateUrl: './angular-slickgrid.component.html', providers: [ // make everything transient (non-singleton) + AngularUtilService, AutoTooltipExtension, CellExternalCopyManagerExtension, CheckboxSelectorExtension, @@ -236,7 +238,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn if (column.editor && column.editor.collectionAsync) { this.loadEditorCollectionAsync(column); } - return { ...column, editor: column.editor && column.editor.model, internalColumnEditor: { ...column.editor }}; + return { ...column, editor: column.editor && column.editor.model, internalColumnEditor: { ...column.editor }}; }); // save reference for all columns before they optionally become hidden/visible @@ -324,17 +326,17 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn // return all available Services (non-singleton) backendService: this.gridOptions && this.gridOptions.backendServiceApi && this.gridOptions.backendServiceApi.service, exportService: this.exportService, + extensionService: this.extensionService, filterService: this.filterService, gridEventService: this.gridEventService, gridStateService: this.gridStateService, gridService: this.gridService, groupingService: this.groupingAndColspanService, - extensionService: this.extensionService, + resizerService: this.resizer, + sortService: this.sortService, /** @deprecated please use "extensionService" instead */ pluginService: this.extensionService, - resizerService: this.resizer, - sortService: this.sortService, }); } @@ -348,7 +350,7 @@ export class AngularSlickgridComponent implements AfterViewInit, OnDestroy, OnIn // a timeout must be set or this could come into conflict when slickgrid // tries to commit the edit when going from one editor to another on the grid // through the click event. If the timeout was not here it would - // try to commit/destroy the twice, which would throw a jquery + // try to commit/destroy the editor twice, which would throw a jquery // error about the element not being in the DOM setTimeout(() => { // make sure the target is the active editor so we do not diff --git a/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts b/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts index e640f15d8..9f435db03 100644 --- a/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts +++ b/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts @@ -1,6 +1,7 @@ -import { ComponentFactoryResolver, ComponentRef, Injectable, Type, ViewChild, ViewContainerRef, ElementRef, Injector, ApplicationRef, EmbeddedViewRef } from '@angular/core'; +import { ApplicationRef, ComponentRef, Injectable, Type, ViewContainerRef } from '@angular/core'; import { Column, CurrentFilter, Extension, ExtensionName, GridOption } from '../models/index'; import { ExtensionUtility } from './extensionUtility'; +import { AngularUtilService } from '../services/angularUtilService'; import { FilterService } from '../services/filter.service'; import { SharedService } from '../services/shared.service'; import { unsubscribeAllObservables } from '../services/utilities'; @@ -32,12 +33,11 @@ export class RowDetailViewExtension implements Extension { private _userProcessFn: (item: any) => Promise; constructor( - private compFactoryResolver: ComponentFactoryResolver, + private angularUtilService: AngularUtilService, + private appRef: ApplicationRef, private extensionUtility: ExtensionUtility, private filterService: FilterService, private sharedService: SharedService, - private appRef: ApplicationRef, - private injector: Injector, ) { } dispose() { @@ -137,7 +137,7 @@ export class RowDetailViewExtension implements Extension { this._eventHandler.subscribe(this._extension.onAfterRowDetailToggle, (e: any, args: { grid: any; item: any; expandedRows: any[]; }) => { // display preload template & re-render all the other Detail Views after toggling // the preload View will eventually go away once the data gets loaded after the "onAsyncEndUpdate" event - this.renderPreloadView(args); + this.renderPreloadView(); this.renderAllViewComponents(); if (this.sharedService.gridOptions.rowDetailView && typeof this.sharedService.gridOptions.rowDetailView.onAfterRowDetailToggle === 'function') { @@ -313,10 +313,10 @@ export class RowDetailViewExtension implements Extension { } /** Render (or rerender) the View Component (Row Detail) */ - private renderPreloadView(args: any) { + private renderPreloadView() { const containerElements = document.getElementsByClassName(`${PRELOAD_CONTAINER_PREFIX}`); if (containerElements && containerElements.length) { - this.appendComponentToHtmlElement(this._preloadComponent, containerElements[0]); + this.angularUtilService.appendAngularComponentToDom(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.appendComponentToHtmlElement(this._viewComponent, containerElements[0]); + const compRef = this.angularUtilService.appendAngularComponentToDom(this._viewComponent, containerElements[0]); Object.assign(compRef.instance, { model: item }); const viewObj = this._views.find((obj) => obj.id === item.id); @@ -333,26 +333,4 @@ export class RowDetailViewExtension implements Extension { } } } - - // ref https://hackernoon.com/angular-pro-tip-how-to-dynamically-create-components-in-body-ba200cc289e6 - private appendComponentToHtmlElement(component: any, appendToElement: HTMLElement | Element) { - // Create a component reference from the component - const componentRef = this.compFactoryResolver - .resolveComponentFactory(component) - .create(this.injector); - - // Attach component to the appRef so that it's inside the ng component tree - this.appRef.attachView(componentRef.hostView); - - // Get DOM element from component - const domElem = (componentRef.hostView as EmbeddedViewRef) - .rootNodes[0] as HTMLElement; - - // Append DOM element to the HTML element specified - if (appendToElement) { - appendToElement.appendChild(domElem); - } - - return componentRef; - } } diff --git a/src/app/modules/angular-slickgrid/modules/angular-slickgrid.module.ts b/src/app/modules/angular-slickgrid/modules/angular-slickgrid.module.ts index 751e3f4dc..561fb81fb 100644 --- a/src/app/modules/angular-slickgrid/modules/angular-slickgrid.module.ts +++ b/src/app/modules/angular-slickgrid/modules/angular-slickgrid.module.ts @@ -1,3 +1,4 @@ +import { AngularUtilService } from './../services/angularUtilService'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; @@ -31,6 +32,7 @@ export class AngularSlickgridModule { ngModule: AngularSlickgridModule, providers: [ { provide: 'config', useValue: config }, + AngularUtilService, CollectionService, FilterFactory, GraphqlService, diff --git a/src/app/modules/angular-slickgrid/services/angularUtilService.ts b/src/app/modules/angular-slickgrid/services/angularUtilService.ts new file mode 100644 index 000000000..3edaa44fe --- /dev/null +++ b/src/app/modules/angular-slickgrid/services/angularUtilService.ts @@ -0,0 +1,34 @@ +import { ApplicationRef, ComponentFactoryResolver, EmbeddedViewRef, Injectable, Injector } from '@angular/core'; + +@Injectable() +export class AngularUtilService { + constructor( + private compFactoryResolver: ComponentFactoryResolver, + private appRef: ApplicationRef, + private injector: Injector, + ) { } + + // ref https://hackernoon.com/angular-pro-tip-how-to-dynamically-create-components-in-body-ba200cc289e6 + appendAngularComponentToDom(component: any, targetElement?: HTMLElement | Element) { + // Create a component reference from the component + const componentRef = this.compFactoryResolver + .resolveComponentFactory(component) + .create(this.injector); + + // Attach component to the appRef so that it's inside the ng component tree + this.appRef.attachView(componentRef.hostView); + + // Get DOM element from component + const domElem = (componentRef.hostView as EmbeddedViewRef) + .rootNodes[0] as HTMLElement; + + // Append DOM element to the HTML element specified + if (targetElement && targetElement.appendChild) { + targetElement.appendChild(domElem); + } else { + document.body.appendChild(domElem); // when no target provided, we'll simply add it to the HTML Body + } + + return componentRef; + } +} diff --git a/src/app/modules/angular-slickgrid/services/extension.service.ts b/src/app/modules/angular-slickgrid/services/extension.service.ts index bb3912250..ba033a592 100644 --- a/src/app/modules/angular-slickgrid/services/extension.service.ts +++ b/src/app/modules/angular-slickgrid/services/extension.service.ts @@ -27,9 +27,6 @@ import { } from '../extensions/index'; import { SharedService } from './shared.service'; -// using external non-typed js libraries -declare var Slick: any; - @Injectable() export class ExtensionService { extensionList: ExtensionModel[] = []; diff --git a/src/app/modules/angular-slickgrid/services/index.ts b/src/app/modules/angular-slickgrid/services/index.ts index 9a4851ecb..65641f825 100644 --- a/src/app/modules/angular-slickgrid/services/index.ts +++ b/src/app/modules/angular-slickgrid/services/index.ts @@ -1,3 +1,4 @@ +export * from './angularUtilService'; export * from './backend-utilities'; export * from './collection.service'; export * from './export.service'; From 0b719c88fa81f626936ceac89b5dc36344766cd2 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Fri, 15 Feb 2019 20:22:11 -0500 Subject: [PATCH 2/4] poc(editor): proof of concept for rendering angular component as editor - still unsure how to pass the chosen value back to the regular editor though, but the rendering works --- package.json | 1 + src/app/app-routing.module.ts | 2 + src/app/app.component.html | 3 + src/app/app.module.ts | 7 + .../examples/custom-angularComponentEditor.ts | 141 ++++++ .../examples/editor-ng-select.component.ts | 20 + .../grid-editor-angular.component.html | 53 +++ .../examples/grid-editor-angular.component.ts | 422 ++++++++++++++++++ src/styles.scss | 2 + 9 files changed, 651 insertions(+) create mode 100644 src/app/examples/custom-angularComponentEditor.ts create mode 100644 src/app/examples/editor-ng-select.component.ts create mode 100644 src/app/examples/grid-editor-angular.component.html create mode 100644 src/app/examples/grid-editor-angular.component.ts 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..64cc6613b --- /dev/null +++ b/src/app/examples/custom-angularComponentEditor.ts @@ -0,0 +1,141 @@ +import { + AngularUtilService, + Column, + CollectionOption, + Editor, + EditorValidator, + EditorValidatorOutput, + GridOption, +} from './../modules/angular-slickgrid'; + +// using external non-typed js libraries +declare var $: any; + +/* + * 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 { + /** Grid options */ + gridOptions: GridOption; + + $input: any; + defaultValue: any; + + constructor(private args: any) { + this.gridOptions = this.args.grid.getOptions() as GridOption; + const gridOptions = this.gridOptions || this.args.column.params || {}; + + 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 || []; + } + + /** Getter for the Collection Options */ + get collectionOptions(): CollectionOption { + return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor.collectionOptions; + } + + /** 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) { + const compRef = this.columnEditor.params.angularUtilService.appendAngularComponentToDom(this.columnEditor.params.component, this.args.container); + Object.assign(compRef.instance, { collection: this.collection }); + } + } + + save() { + + } + + cancel() { + + } + + hide() { + + } + + show() { + + } + + position(position: any) { + + } + + destroy() { + $('#ngSelectContainer').appendTo('#editorsContainer'); + } + + focus() { + + } + + applyValue(item: any, state: any) { + + } + + getValue() { + return this.$input.val(); + } + + loadValue(item: any) { + + } + + serializeValue(): any { + + } + + isValueChanged(): boolean { + return false; + } + + validate(): EditorValidatorOutput { + if (this.validator) { + const value = this.$input && this.$input.val && this.$input.val(); + 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 a required checkbox, 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..6e2802e3d --- /dev/null +++ b/src/app/examples/editor-ng-select.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + template: ` + + + {{ item?.name }} + + ` +}) +export class EditorNgSelectComponent { + selectedPersonId: string; + collection; // this will be filled by the collection of your column definition +} 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..054c90e4c --- /dev/null +++ b/src/app/examples/grid-editor-angular.component.html @@ -0,0 +1,53 @@ +
    +

    {{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..387ba83f9 --- /dev/null +++ b/src/app/examples/grid-editor-angular.component.ts @@ -0,0 +1,422 @@ +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 { Subject } from 'rxjs'; +import { EditorNgSelectComponent } from './editor-ng-select.component'; +import { CustomAngularComponentEditor } from './custom-angularComponentEditor'; + +// using external non-typed js libraries +declare var Slick: any; +declare var $: any; + +const NB_ITEMS = 100; +const URL_SAMPLE_COLLECTION_DATA = 'assets/data/collection_100_numbers.json'; + +// create a custom Formatter to show the Task + value +const taskFormatter = (row, cell, value, columnDef, dataContext) => { + if (value && Array.isArray(value)) { + const taskValues = value.map((val) => `Task ${val}`); + const values = taskValues.join(', '); + return `${values}`; + } + return ''; +}; + +@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). +
      +
    • Support of Angular Component as Custom Editor
    • +
    • Example shown below uses ng-select +
    + `; + + private _commandQueue = []; + angularGrid: AngularGridInstance; + columnDefinitions: Column[]; + gridOptions: GridOption; + dataset: any[]; + gridObj: any; + isAutoEdit = true; + alertWarning: any; + updatedObject: any; + selectedLanguage = 'en'; + + 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: CustomAngularComponentEditor, + collection: [ + { id: '1', name: 'Jean' }, + { id: '2', name: 'Pierre' }, + { id: '3', name: 'Paul' }, + ], + 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, + // params: { hideSliderNumber: true }, + }, + /* + editor: { + // default is 0 decimals, if no decimals is passed it will accept 0 or more decimals + // however if you pass the "decimalPlaces", it will validate with that maximum + model: Editors.float, + minValue: 0, + maxValue: 365, + // the default validation error message is in English but you can override it by using "errorMessage" + // errorMessage: this.i18n.tr('INVALID_FLOAT', { maxDecimal: 2 }), + params: { decimalPlaces: 2 }, + }, + */ + }, { + 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, + 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, + type: FieldType.date, + editor: { + model: Editors.date + }, + }, { + id: 'effort-driven', + name: 'Effort Driven', + field: 'effortDriven', + minWidth: 70, + filterable: true, + type: FieldType.boolean, + filter: { + model: Filters.singleSelect, + collection: [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }], + }, + formatter: Formatters.checkmark, + editor: { + model: Editors.checkbox, + }, + }, { + id: 'prerequisites', + name: 'Prerequisites', + field: 'prerequisites', + minWidth: 100, + filterable: true, + formatter: taskFormatter, + sortable: true, + type: FieldType.string, + editor: { + collectionAsync: this.http.get<{ value: string; label: string; }[]>(URL_SAMPLE_COLLECTION_DATA), + // OR a regular collection load + // collection: Array.from(Array(100).keys()).map(k => ({ value: k, prefix: 'Task', label: k })), + collectionSortBy: { + property: 'label', + sortDesc: true + }, + customStructure: { + label: 'label', + value: 'value', + labelPrefix: 'prefix', + }, + collectionOptions: { + separatorBetweenTextLabels: ' ' + }, + model: Editors.multipleSelect, + }, + filter: { + collectionAsync: this.http.get<{ value: string; label: string; }[]>(URL_SAMPLE_COLLECTION_DATA), + // OR a regular collection load + // collection: Array.from(Array(100).keys()).map(k => ({ value: k, prefix: 'Task', label: k })), + collectionSortBy: { + property: 'label', + sortDesc: true + }, + customStructure: { + label: 'label', + value: 'value', + labelPrefix: 'prefix', + }, + collectionOptions: { + separatorBetweenTextLabels: ' ' + }, + model: Filters.multipleSelect, + operator: OperatorType.inContains, + } + } + ]; + + this.gridOptions = { + asyncEditorLoading: false, + autoEdit: this.isAutoEdit, + autoCommitEdit: false, + autoResize: { + containerId: 'demo-container', + sidePadding: 15 + }, + rowHeight: 45, + 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); + } + + /** Add a new row to the grid and refresh the Filter collection. + * Note that because Filter elements are always displayed on the screen, we need to tell the Filter, + * we do this via a Subject .next(), that it's collection got changed + * as for the Editor, there's nothing to do since the element is not shown and it will have latest collection next time it shows up + */ + addItem() { + const lastRowIndex = this.dataset.length; + const newRows = this.mockData(1, lastRowIndex); + + // wrap into a timer to simulate a backend async call + setTimeout(() => { + const requisiteColumnDef = this.columnDefinitions.find((column: Column) => column.id === 'prerequisites'); + if (requisiteColumnDef) { + const filterCollectionAsync = requisiteColumnDef.filter.collectionAsync; + const editorCollection = requisiteColumnDef.editor.collection; + + if (Array.isArray(editorCollection)) { + // add the new row to the grid + this.angularGrid.gridService.addItemToDatagrid(newRows[0]); + + // then refresh the Editor "collection", we have 2 ways of doing it + + // Push to the Editor "collection" + editorCollection.push({ value: lastRowIndex, label: lastRowIndex, prefix: 'Task' }); + + // or replace entire "collection" + // durationColumnDef.editor.collection = [...collection, ...[{ value: lastRowIndex, label: lastRowIndex }]]; + + // for the Filter only, we have a trigger an RxJS/Subject change with the new collection + // we do this because Filter(s) are shown at all time, while on Editor it's unnecessary since they are only shown when opening them + if (filterCollectionAsync instanceof Subject) { + filterCollectionAsync.next(editorCollection); + } + } + } + }, 250); + } + + /** + * Delete last inserted row. + * Note that because Filter elements are always displayed on the screen, we need to tell the Filter, + * we do this via a Subject .next(), that it's collection got changed + * as for the Editor, there's nothing to do since the element is not shown and it will have latest collection next time it shows up + */ + deleteItem() { + const requisiteColumnDef = this.columnDefinitions.find((column: Column) => column.id === 'prerequisites'); + if (requisiteColumnDef) { + const filterCollectionAsync = requisiteColumnDef.filter.collectionAsync; + const filterCollection = requisiteColumnDef.filter.collection; + + if (Array.isArray(filterCollection)) { + // sort collection in descending order and take out last collection option + const selectCollectionObj = this.sortCollectionDescending(filterCollection).pop(); + + // then we will delete that item from the grid + this.angularGrid.gridService.deleteDataGridItemById(selectCollectionObj.value); + + // for the Filter only, we have a trigger an RxJS/Subject change with the new collection + // we do this because Filter(s) are shown at all time, while on Editor it's unnecessary since they are only shown when opening them + if (filterCollectionAsync instanceof Subject) { + filterCollectionAsync.next(filterCollection); + } + } + } + } + + sortCollectionDescending(collection) { + return collection.sort((item1, item2) => item1.value - item2.value); + } + + 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, + 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), + prerequisites: (i % 2 === 0) && i !== 0 && i < 12 ? [i, i - 1] : [], + countryOfOrigin: (i % 2) ? { code: 'CA', name: 'Canada' } : { code: 'US', name: 'United States' }, + countryOfOriginName: (i % 2) ? 'Canada' : 'United States', + cityOfOrigin: (i % 2) ? 'Vancouver, BC, Canada' : 'Boston, MA, United States', + }); + } + 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/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"; From c1c379b9331f920e7de98709170c83fae37964a5 Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Mon, 18 Feb 2019 15:01:16 -0500 Subject: [PATCH 3/4] feat(editor): finalize Custom Editor with Angular Component, closes #77 --- .../examples/custom-angularComponentEditor.ts | 90 +++++---- .../examples/editor-ng-select.component.ts | 20 +- .../grid-editor-angular.component.html | 6 - .../examples/grid-editor-angular.component.ts | 187 +++--------------- .../extensions/rowDetailViewExtension.ts | 4 +- .../formatters/complexObjectFormatter.ts | 6 +- .../angular-slickgrid/formatters/index.ts | 8 +- .../services/angularUtilService.ts | 4 +- 8 files changed, 114 insertions(+), 211 deletions(-) diff --git a/src/app/examples/custom-angularComponentEditor.ts b/src/app/examples/custom-angularComponentEditor.ts index 64cc6613b..5bb683b47 100644 --- a/src/app/examples/custom-angularComponentEditor.ts +++ b/src/app/examples/custom-angularComponentEditor.ts @@ -1,31 +1,27 @@ +import { ComponentRef } from '@angular/core'; import { AngularUtilService, Column, - CollectionOption, Editor, EditorValidator, EditorValidatorOutput, - GridOption, } from './../modules/angular-slickgrid'; -// using external non-typed js libraries -declare var $: any; - /* * 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 { - /** Grid options */ - gridOptions: GridOption; + /** Angular Component Reference */ + componentRef: ComponentRef; - $input: any; - defaultValue: any; + /** default item Id */ + defaultId: string; - constructor(private args: any) { - this.gridOptions = this.args.grid.getOptions() as GridOption; - const gridOptions = this.gridOptions || this.args.column.params || {}; + /** default item object */ + defaultItem: any; + constructor(private args: any) { this.init(); } @@ -39,11 +35,6 @@ export class CustomAngularComponentEditor implements Editor { return this.columnDef && this.columnDef && this.columnDef.internalColumnEditor.collection || []; } - /** Getter for the Collection Options */ - get collectionOptions(): CollectionOption { - return this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor.collectionOptions; - } - /** Get Column Definition object */ get columnDef(): Column { return this.args && this.args.column || {}; @@ -69,62 +60,87 @@ export class CustomAngularComponentEditor implements Editor { Example: this.columnDefs = [{ id: 'title', field: 'title', editor: { component: MyComponent, model: Editors.angularComponent, collection: [...] },`); } if (this.columnEditor && this.columnEditor.params.component) { - const compRef = this.columnEditor.params.angularUtilService.appendAngularComponentToDom(this.columnEditor.params.component, this.args.container); - Object.assign(compRef.instance, { collection: this.collection }); + 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() { - - } - - position(position: any) { - + // 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() { - $('#ngSelectContainer').appendTo('#editorsContainer'); + // 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.$input.val(); + 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(): boolean { - return false; + isValueChanged() { + return (!(this.componentRef.instance.selectedId === '' && this.defaultId == null)) && (this.componentRef.instance.selectedId !== this.defaultId); } validate(): EditorValidatorOutput { if (this.validator) { - const value = this.$input && this.$input.val && this.$input.val(); + const value = this.componentRef.instance.selectedId; const validationResults = this.validator(value, this.args); if (!validationResults.valid) { return validationResults; @@ -132,7 +148,7 @@ export class CustomAngularComponentEditor implements Editor { } // by default the editor is always valid - // if user want it to be a required checkbox, he would have to provide his own validator + // 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 index 6e2802e3d..ceb99c357 100644 --- a/src/app/examples/editor-ng-select.component.ts +++ b/src/app/examples/editor-ng-select.component.ts @@ -1,13 +1,16 @@ import { Component } from '@angular/core'; +import { Subject } from 'rxjs'; @Component({ template: ` {{ item?.name }} @@ -15,6 +18,17 @@ import { Component } from '@angular/core'; ` }) export class EditorNgSelectComponent { - selectedPersonId: string; + 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 index 054c90e4c..6b4424330 100644 --- a/src/app/examples/grid-editor-angular.component.html +++ b/src/app/examples/grid-editor-angular.component.html @@ -24,12 +24,6 @@

    {{title}}

    -
    - - - - -
    diff --git a/src/app/examples/grid-editor-angular.component.ts b/src/app/examples/grid-editor-angular.component.ts index 387ba83f9..f02745ccc 100644 --- a/src/app/examples/grid-editor-angular.component.ts +++ b/src/app/examples/grid-editor-angular.component.ts @@ -19,10 +19,8 @@ import { CustomAngularComponentEditor } from './custom-angularComponentEditor'; // using external non-typed js libraries declare var Slick: any; -declare var $: any; const NB_ITEMS = 100; -const URL_SAMPLE_COLLECTION_DATA = 'assets/data/collection_100_numbers.json'; // create a custom Formatter to show the Task + value const taskFormatter = (row, cell, value, columnDef, dataContext) => { @@ -43,8 +41,8 @@ export class GridEditorAngularComponent implements OnInit { subTitle = ` Grid with Inline Editors and onCellClick actions (Wiki docs).
      -
    • Support of Angular Component as Custom Editor
    • -
    • Example shown below uses ng-select +
    • Support of Angular Component as Custom Editor (click on "Assignee" column)
    • +
    • The column "Assignee" shown below uses ng-select as a custom editor with Angular Component
    `; @@ -58,6 +56,11 @@ export class GridEditorAngularComponent implements OnInit { 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) {} @@ -80,13 +83,28 @@ export class GridEditorAngularComponent implements OnInit { 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' + }, editor: { model: CustomAngularComponentEditor, - collection: [ - { id: '1', name: 'Jean' }, - { id: '2', name: 'Pierre' }, - { id: '3', name: 'Paul' }, - ], + collection: this.assignees, params: { angularUtilService: this.angularUtilService, component: EditorNgSelectComponent, @@ -109,20 +127,7 @@ export class GridEditorAngularComponent implements OnInit { model: Editors.slider, minValue: 0, maxValue: 100, - // params: { hideSliderNumber: true }, - }, - /* - editor: { - // default is 0 decimals, if no decimals is passed it will accept 0 or more decimals - // however if you pass the "decimalPlaces", it will validate with that maximum - model: Editors.float, - minValue: 0, - maxValue: 365, - // the default validation error message is in English but you can override it by using "errorMessage" - // errorMessage: this.i18n.tr('INVALID_FLOAT', { maxDecimal: 2 }), - params: { decimalPlaces: 2 }, - }, - */ + } }, { id: 'complete', name: '% Complete', @@ -186,67 +191,6 @@ export class GridEditorAngularComponent implements OnInit { editor: { model: Editors.date }, - }, { - id: 'effort-driven', - name: 'Effort Driven', - field: 'effortDriven', - minWidth: 70, - filterable: true, - type: FieldType.boolean, - filter: { - model: Filters.singleSelect, - collection: [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }], - }, - formatter: Formatters.checkmark, - editor: { - model: Editors.checkbox, - }, - }, { - id: 'prerequisites', - name: 'Prerequisites', - field: 'prerequisites', - minWidth: 100, - filterable: true, - formatter: taskFormatter, - sortable: true, - type: FieldType.string, - editor: { - collectionAsync: this.http.get<{ value: string; label: string; }[]>(URL_SAMPLE_COLLECTION_DATA), - // OR a regular collection load - // collection: Array.from(Array(100).keys()).map(k => ({ value: k, prefix: 'Task', label: k })), - collectionSortBy: { - property: 'label', - sortDesc: true - }, - customStructure: { - label: 'label', - value: 'value', - labelPrefix: 'prefix', - }, - collectionOptions: { - separatorBetweenTextLabels: ' ' - }, - model: Editors.multipleSelect, - }, - filter: { - collectionAsync: this.http.get<{ value: string; label: string; }[]>(URL_SAMPLE_COLLECTION_DATA), - // OR a regular collection load - // collection: Array.from(Array(100).keys()).map(k => ({ value: k, prefix: 'Task', label: k })), - collectionSortBy: { - property: 'label', - sortDesc: true - }, - customStructure: { - label: 'label', - value: 'value', - labelPrefix: 'prefix', - }, - collectionOptions: { - separatorBetweenTextLabels: ' ' - }, - model: Filters.multipleSelect, - operator: OperatorType.inContains, - } } ]; @@ -274,76 +218,6 @@ export class GridEditorAngularComponent implements OnInit { this.dataset = this.mockData(NB_ITEMS); } - /** Add a new row to the grid and refresh the Filter collection. - * Note that because Filter elements are always displayed on the screen, we need to tell the Filter, - * we do this via a Subject .next(), that it's collection got changed - * as for the Editor, there's nothing to do since the element is not shown and it will have latest collection next time it shows up - */ - addItem() { - const lastRowIndex = this.dataset.length; - const newRows = this.mockData(1, lastRowIndex); - - // wrap into a timer to simulate a backend async call - setTimeout(() => { - const requisiteColumnDef = this.columnDefinitions.find((column: Column) => column.id === 'prerequisites'); - if (requisiteColumnDef) { - const filterCollectionAsync = requisiteColumnDef.filter.collectionAsync; - const editorCollection = requisiteColumnDef.editor.collection; - - if (Array.isArray(editorCollection)) { - // add the new row to the grid - this.angularGrid.gridService.addItemToDatagrid(newRows[0]); - - // then refresh the Editor "collection", we have 2 ways of doing it - - // Push to the Editor "collection" - editorCollection.push({ value: lastRowIndex, label: lastRowIndex, prefix: 'Task' }); - - // or replace entire "collection" - // durationColumnDef.editor.collection = [...collection, ...[{ value: lastRowIndex, label: lastRowIndex }]]; - - // for the Filter only, we have a trigger an RxJS/Subject change with the new collection - // we do this because Filter(s) are shown at all time, while on Editor it's unnecessary since they are only shown when opening them - if (filterCollectionAsync instanceof Subject) { - filterCollectionAsync.next(editorCollection); - } - } - } - }, 250); - } - - /** - * Delete last inserted row. - * Note that because Filter elements are always displayed on the screen, we need to tell the Filter, - * we do this via a Subject .next(), that it's collection got changed - * as for the Editor, there's nothing to do since the element is not shown and it will have latest collection next time it shows up - */ - deleteItem() { - const requisiteColumnDef = this.columnDefinitions.find((column: Column) => column.id === 'prerequisites'); - if (requisiteColumnDef) { - const filterCollectionAsync = requisiteColumnDef.filter.collectionAsync; - const filterCollection = requisiteColumnDef.filter.collection; - - if (Array.isArray(filterCollection)) { - // sort collection in descending order and take out last collection option - const selectCollectionObj = this.sortCollectionDescending(filterCollection).pop(); - - // then we will delete that item from the grid - this.angularGrid.gridService.deleteDataGridItemById(selectCollectionObj.value); - - // for the Filter only, we have a trigger an RxJS/Subject change with the new collection - // we do this because Filter(s) are shown at all time, while on Editor it's unnecessary since they are only shown when opening them - if (filterCollectionAsync instanceof Subject) { - filterCollectionAsync.next(filterCollection); - } - } - } - } - - sortCollectionDescending(collection) { - return collection.sort((item1, item2) => item1.value - item2.value); - } - mockData(itemCount, startingIndex = 0) { // mock a dataset const tempDataset = []; @@ -356,16 +230,13 @@ export class GridEditorAngularComponent implements OnInit { 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), - prerequisites: (i % 2 === 0) && i !== 0 && i < 12 ? [i, i - 1] : [], - countryOfOrigin: (i % 2) ? { code: 'CA', name: 'Canada' } : { code: 'US', name: 'United States' }, - countryOfOriginName: (i % 2) ? 'Canada' : 'United States', - cityOfOrigin: (i % 2) ? 'Vancouver, BC, Canada' : 'Boston, MA, United States', }); } return tempDataset; 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..d6c129b8b 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, EmbeddedViewRef, Injectable, Injector, ComponentRef } 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) From 527045fc263b45f1fa7fe765be26dde716bccbbe Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Mon, 18 Feb 2019 15:20:57 -0500 Subject: [PATCH 4/4] refactor(editor): cleanup some code and fix export with formatter --- .../examples/grid-editor-angular.component.html | 2 ++ .../examples/grid-editor-angular.component.ts | 17 +++++------------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/app/examples/grid-editor-angular.component.html b/src/app/examples/grid-editor-angular.component.html index 6b4424330..c1a858906 100644 --- a/src/app/examples/grid-editor-angular.component.html +++ b/src/app/examples/grid-editor-angular.component.html @@ -1,6 +1,7 @@

    {{title}}

    +
    @@ -26,6 +27,7 @@

    {{title}}

    +
    Updated Item: {{updatedObject | json}} diff --git a/src/app/examples/grid-editor-angular.component.ts b/src/app/examples/grid-editor-angular.component.ts index f02745ccc..b4f414470 100644 --- a/src/app/examples/grid-editor-angular.component.ts +++ b/src/app/examples/grid-editor-angular.component.ts @@ -13,7 +13,6 @@ import { OnEventArgs, OperatorType, } from './../modules/angular-slickgrid'; -import { Subject } from 'rxjs'; import { EditorNgSelectComponent } from './editor-ng-select.component'; import { CustomAngularComponentEditor } from './custom-angularComponentEditor'; @@ -22,16 +21,6 @@ declare var Slick: any; const NB_ITEMS = 100; -// create a custom Formatter to show the Task + value -const taskFormatter = (row, cell, value, columnDef, dataContext) => { - if (value && Array.isArray(value)) { - const taskValues = value.map((val) => `Task ${val}`); - const values = taskValues.join(', '); - return `${values}`; - } - return ''; -}; - @Component({ templateUrl: './grid-editor-angular.component.html' }) @@ -43,6 +32,7 @@ export class GridEditorAngularComponent implements OnInit {
    • Support of Angular Component as Custom Editor (click on "Assignee" column)
    • The column "Assignee" shown below uses ng-select as a custom editor with Angular Component +
    • Increased rowHeight to 45 so that the "ng-select" fits in the cell. Ideally it would be better to override the ng-select component styling to change it's max height
    `; @@ -102,6 +92,7 @@ export class GridEditorAngularComponent implements OnInit { params: { complexField: 'assignee.name' }, + exportWithFormatter: true, editor: { model: CustomAngularComponentEditor, collection: this.assignees, @@ -173,6 +164,7 @@ export class GridEditorAngularComponent implements OnInit { filterable: true, filter: { model: Filters.compoundDate }, formatter: Formatters.dateIso, + exportWithFormatter: true, sortable: true, type: FieldType.date, editor: { @@ -187,6 +179,7 @@ export class GridEditorAngularComponent implements OnInit { sortable: true, filter: { model: Filters.compoundDate }, formatter: Formatters.dateIso, + exportWithFormatter: true, type: FieldType.date, editor: { model: Editors.date @@ -202,7 +195,7 @@ export class GridEditorAngularComponent implements OnInit { containerId: 'demo-container', sidePadding: 15 }, - rowHeight: 45, + rowHeight: 45, // increase row height so that the ng-select fits in the cell editable: true, enableCellNavigation: true, enableColumnPicker: true,