diff --git a/src/app/app.component.html b/src/app/app.component.html index c6daf9ab5..897e2759c 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -92,7 +92,7 @@ 21- Row Detail View
  • - 22- Editors Angular Components + 22- Use of Angular Components
  • diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9f743d8f4..2df0de62c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,6 +11,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { AppComponent } from './app.component'; import { CustomTitleFormatterComponent } from './examples/custom-titleFormatter.component'; import { EditorNgSelectComponent } from './examples/editor-ng-select.component'; +import { FilterNgSelectComponent } from './examples/filter-ng-select.component'; import { GridAddItemComponent } from './examples/grid-additem.component'; import { GridBasicComponent } from './examples/grid-basic.component'; import { GridClientSideComponent } from './examples/grid-clientside.component'; @@ -74,6 +75,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj AppComponent, CustomTitleFormatterComponent, EditorNgSelectComponent, + FilterNgSelectComponent, GridAddItemComponent, GridAngularComponent, GridBasicComponent, @@ -129,6 +131,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj // dynamically created components CustomTitleFormatterComponent, EditorNgSelectComponent, + FilterNgSelectComponent, RowDetailPreloadComponent, RowDetailViewComponent, ], diff --git a/src/app/examples/custom-angularComponentEditor.ts b/src/app/examples/custom-angularComponentEditor.ts index e40d34418..4909d1207 100644 --- a/src/app/examples/custom-angularComponentEditor.ts +++ b/src/app/examples/custom-angularComponentEditor.ts @@ -1,4 +1,5 @@ import { ComponentRef } from '@angular/core'; +import { Subscription } from 'rxjs'; import { AngularUtilService, Column, @@ -13,6 +14,8 @@ import { * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. */ export class CustomAngularComponentEditor implements Editor { + changeSubscriber: Subscription; + /** Angular Component Reference */ componentRef: ComponentRef; @@ -41,7 +44,7 @@ export class CustomAngularComponentEditor implements Editor { /** Get the Collection */ get collection(): any[] { - return this.columnDef && this.columnDef && this.columnDef.internalColumnEditor.collection || []; + return this.columnDef && this.columnDef.internalColumnEditor.collection || []; } /** Get Column Definition object */ @@ -70,17 +73,21 @@ export class CustomAngularComponentEditor implements Editor { init() { if (!this.columnEditor || !this.columnEditor.params.component || !(this.angularUtilService instanceof AngularUtilService)) { - 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. + throw new Error(`[Angular-Slickgrid] For Editor with Angular Component to work properly, you need to provide your component to the "component" property and make sure to add it to your "entryComponents" array. You also need to provide the "AngularUtilService" via the Editor Params OR the Grid Options Params - Example: this.columnDefs = [{ id: 'title', field: 'title', editor: { model: CustomAngularComponentEditor, collection: [...] }, params: { component: MyComponent, angularUtilService: this.angularUtilService }]; + Example: this.columnDefs = [{ id: 'title', field: 'title', editor: { model: CustomAngularComponentEditor, collection: [...], params: { component: MyComponent, angularUtilService: this.angularUtilService }}]; OR this.columnDefs = [{ id: 'title', field: 'title', editor: { model: CustomAngularComponentEditor, collection: [...] }]; this.gridOptions = { params: { angularUtilService: this.angularUtilService }}`); } if (this.columnEditor && this.columnEditor.params.component) { const componentOutput = this.angularUtilService.createAngularComponentAppendToDom(this.columnEditor.params.component, this.args.container); this.componentRef = componentOutput && componentOutput.componentRef; + + // here we override the collection object of the Angular Component + // but technically you can pass any values you wish to your Component Object.assign(this.componentRef.instance, { collection: this.collection }); - this.componentRef.instance.onModelChanged.subscribe((item) => { + // when our model (item object) changes, we'll call a save of the slickgrid editor + this.changeSubscriber = this.componentRef.instance.onItemChanged.subscribe((item) => { this.save(); }); } @@ -105,29 +112,30 @@ export class CustomAngularComponentEditor implements Editor { } } + /** optional, implement a hide method on your Angular Component */ 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(); } } + /** optional, implement a show method on your Angular Component */ 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 the Angular Component & Subscription */ destroy() { - // destroy the Angular Component if (this.componentRef && this.componentRef.destroy) { this.componentRef.destroy(); + this.changeSubscriber.unsubscribe(); } } + /** optional, implement a focus method on your Angular Component */ 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(); } diff --git a/src/app/examples/custom-angularComponentFilter.ts b/src/app/examples/custom-angularComponentFilter.ts new file mode 100644 index 000000000..9e3aa1a8e --- /dev/null +++ b/src/app/examples/custom-angularComponentFilter.ts @@ -0,0 +1,118 @@ +import { ComponentRef } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { + AngularUtilService, + Column, + ColumnFilter, + Filter, + FilterArguments, + FilterCallback, + GridOption, + OperatorType, + OperatorString, + SearchTerm, +} from './../modules/angular-slickgrid'; + +// using external non-typed js libraries +declare var $: any; + +export class CustomAngularComponentFilter implements Filter { + changeSubscriber: Subscription; + + /** Angular Component Reference */ + componentRef: ComponentRef; + + grid: any; + searchTerms: SearchTerm[]; + columnDef: Column; + callback: FilterCallback; + operator: OperatorType | OperatorString = OperatorType.equal; + + constructor() {} + + /** Angular Util Service (could be inside the Grid Options Params or the Filter Params ) */ + get angularUtilService(): AngularUtilService { + let angularUtilService = this.gridOptions && this.gridOptions.params && this.gridOptions.params.angularUtilService; + if (!angularUtilService || !(angularUtilService instanceof AngularUtilService)) { + angularUtilService = this.columnFilter && this.columnFilter.params && this.columnFilter.params.angularUtilService; + } + return angularUtilService; + } + + /** Get the Collection */ + get collection(): any[] { + return this.columnFilter && this.columnFilter.collection || []; + } + + /** Getter for the Column Filter */ + get columnFilter(): ColumnFilter { + return this.columnDef && this.columnDef.filter || {}; + } + + /** Getter for the Grid Options pulled through the Grid Object */ + get gridOptions(): GridOption { + return (this.grid && this.grid.getOptions) ? this.grid.getOptions() : {}; + } + + /** + * Initialize the Filter + */ + init(args: FilterArguments) { + this.grid = args.grid; + this.callback = args.callback; + this.columnDef = args.columnDef; + this.searchTerms = args.searchTerms || []; + + if (!this.columnFilter || !this.columnFilter.params.component || !(this.angularUtilService instanceof AngularUtilService)) { + throw new Error(`[Angular-Slickgrid] For Filter with Angular Component to work properly, you need to provide your component to the "component" property and make sure to add it to your "entryComponents" array. + You also need to provide the "AngularUtilService" via the Filter Params OR the Grid Options Params + Example: this.columnDefs = [{ id: 'title', field: 'title', filter: { model: CustomAngularComponentFilter, collection: [...], params: { component: MyComponent, angularUtilService: this.angularUtilService }}]; + OR this.columnDefs = [{ id: 'title', field: 'title', filter: { model: CustomAngularComponentFilter, collection: [...] }]; this.gridOptions = { params: { angularUtilService: this.angularUtilService }}`); + } + + if (this.columnFilter && this.columnFilter.params.component) { + // use a delay to make sure Angular ran at least a full cycle and it finished rendering the Component before hooking onto it + // else we get the infamous error "ExpressionChangedAfterItHasBeenCheckedError" + setTimeout(() => { + const $headerElm = this.grid.getHeaderRowColumn(this.columnDef.id); + $($headerElm).empty(); + const componentOuput = this.angularUtilService.createAngularComponentAppendToDom(this.columnFilter.params.component, $headerElm); + this.componentRef = componentOuput.componentRef; + + // here we override the collection object of the Angular Component + // but technically you can pass any values you wish to your Component + Object.assign(componentOuput.componentRef.instance, { collection: this.collection }); + + this.changeSubscriber = componentOuput.componentRef.instance.onItemChanged.subscribe((item) => { + this.callback(undefined, { columnDef: this.columnDef, operator: this.operator, searchTerms: [item.id] }); + }); + }); + } + } + + /** + * Clear the filter value + */ + clear() { + if (this.componentRef && this.componentRef.instance && this.componentRef.instance.hasOwnProperty('selectedId')) { + this.componentRef.instance.selectedId = 0; + } + } + + /** destroy the Angular Component & Subscription */ + destroy() { + if (this.componentRef && this.componentRef.destroy) { + this.componentRef.destroy(); + this.changeSubscriber.unsubscribe(); + } + } + + /** + * Set value(s) on the DOM element + */ + setValues(values) { + if (this.componentRef && this.componentRef.instance && this.componentRef.instance.hasOwnProperty('selectedId')) { + this.componentRef.instance.selectedId = values; + } + } +} diff --git a/src/app/examples/custom-inputFilter.ts b/src/app/examples/custom-inputFilter.ts index fcbce4eaf..5ddf47799 100644 --- a/src/app/examples/custom-inputFilter.ts +++ b/src/app/examples/custom-inputFilter.ts @@ -24,7 +24,7 @@ export class CustomInputFilter implements Filter { constructor() {} - /** Getter for the Filter Operator */ + /** Getter for the Column Filter */ get columnFilter(): ColumnFilter { return this.columnDef && this.columnDef.filter || {}; } diff --git a/src/app/examples/editor-ng-select.component.ts b/src/app/examples/editor-ng-select.component.ts index ceb99c357..18e697157 100644 --- a/src/app/examples/editor-ng-select.component.ts +++ b/src/app/examples/editor-ng-select.component.ts @@ -21,11 +21,11 @@ export class EditorNgSelectComponent { selectedId: string; selectedItem: any; collection; // this will be filled by the collection of your column definition - onModelChanged = new Subject(); // object + onItemChanged = new Subject(); // object onChange(item: any) { this.selectedItem = item; - this.onModelChanged.next(item); + this.onItemChanged.next(item); } focus() { diff --git a/src/app/examples/filter-ng-select.component.ts b/src/app/examples/filter-ng-select.component.ts new file mode 100644 index 000000000..2618fd002 --- /dev/null +++ b/src/app/examples/filter-ng-select.component.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { Subject } from 'rxjs'; + +// the appendTo="body" (necessary for SlickGrid filter) requires the body to be position relative like so +// +@Component({ + template: ` + + + {{ item?.name }} + + ` +}) +export class FilterNgSelectComponent { + selectedId: string; + selectedItem: any; + collection; // this will be filled by the collection of your column definition + onItemChanged = new Subject(); // object + + onChange(item: any) { + this.selectedItem = item; + this.onItemChanged.next(item); + } +} diff --git a/src/app/examples/grid-angular.component.html b/src/app/examples/grid-angular.component.html index c1a858906..b191aafab 100644 --- a/src/app/examples/grid-angular.component.html +++ b/src/app/examples/grid-angular.component.html @@ -4,7 +4,7 @@

    {{title}}


    - +
    diff --git a/src/app/examples/grid-angular.component.scss b/src/app/examples/grid-angular.component.scss new file mode 100644 index 000000000..9e5990c28 --- /dev/null +++ b/src/app/examples/grid-angular.component.scss @@ -0,0 +1,16 @@ +h3 { + font-style: normal; + color: #3d3d3d; +} +.subtitle { + font-size: 18px; +} +.ng-select.custom { + border:0px; + max-height: 27px; + border-radius: 0; +} +.ng-input { + padding-top: -4px !important; + padding-left: 4px !important; +} diff --git a/src/app/examples/grid-angular.component.ts b/src/app/examples/grid-angular.component.ts index 883ffe8ee..fa7b8be0b 100644 --- a/src/app/examples/grid-angular.component.ts +++ b/src/app/examples/grid-angular.component.ts @@ -1,4 +1,5 @@ -import { Component, Injectable, OnInit, EmbeddedViewRef } from '@angular/core'; +import { CustomAngularComponentFilter } from './custom-angularComponentFilter'; +import { Component, Injectable, OnInit, ViewEncapsulation } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AngularGridInstance, @@ -15,6 +16,7 @@ import { import { EditorNgSelectComponent } from './editor-ng-select.component'; import { CustomAngularComponentEditor } from './custom-angularComponentEditor'; import { CustomTitleFormatterComponent } from './custom-titleFormatter.component'; +import { FilterNgSelectComponent } from './filter-ng-select.component'; // using external non-typed js libraries declare var Slick: any; @@ -23,20 +25,31 @@ declare var $: any; const NB_ITEMS = 100; @Component({ - templateUrl: './grid-angular.component.html' + templateUrl: './grid-angular.component.html', + styleUrls: ['./grid-angular.component.scss'], + encapsulation: ViewEncapsulation.None }) @Injectable() export class GridAngularComponent implements OnInit { - title = 'Example 22: Multiple Angular Components'; + title = 'Example 22: Use of Angular Components'; subTitle = ` +

    Filters, Editors, AsyncPostRender with Angular Components

    Grid with usage of Angular Components as Editor & AsyncPostRender (similar to Formatter).
    • Support of Angular Component as Custom Editor (click on any "Assignee" name cell)
      • That column uses ng-select as a custom editor as an Angular Component -
      • Increased Grid Options "rowHeight" to 45 so that the "ng-select" fits in the cell. Ideally it would be better to override the ng-select css styling to change it's max height
      • +
      • Increased Grid Options "rowHeight" & "headerRowHeight" to 45 so that the "ng-select" fits in the cell. Ideally it would be better to override the ng-select css styling to change it's max height
      +
    • Support of Angular Component as Custom Filter ("Assignee" columns), which also uses "ng-select"
    • The 2nd "Assignee" column (showing in bold text) uses "asyncPostRender" with an Angular Component
    • +
        +
      • Why can't we use Angular Component as Customer Formatter and why do I see a slight delay in loading the data?
      • +
      • It's totally normal since SlickGrid Formatters only accept strings (synchronously), + so we cannot use that (Angular requires at least 1 full cycle to render the element), so we are left with SlickGrid "asyncPostRender" and + it works but as the name suggest it's async users might see noticeable delay in loading the data +
      • +
    `; @@ -51,6 +64,7 @@ export class GridAngularComponent implements OnInit { updatedObject: any; selectedLanguage = 'en'; assignees = [ + { id: '', name: '' }, { id: '1', name: 'John' }, { id: '2', name: 'Pierre' }, { id: '3', name: 'Paul' }, @@ -91,10 +105,18 @@ export class GridAngularComponent implements OnInit { minWidth: 100, filterable: true, sortable: true, - type: FieldType.string, + filter: { + model: new CustomAngularComponentFilter(), // create a new instance to make each Filter independent from each other + collection: this.assignees, + params: { + component: FilterNgSelectComponent, + } + }, + queryFieldFilter: 'assignee.id', // for a complex object it's important to tell the Filter which field to query and our CustomAngularComponentFilter returns the "id" property + queryFieldSorter: 'assignee.name', formatter: Formatters.complexObject, params: { - complexField: 'assignee.name', + complexFieldLabel: 'assignee.name', }, exportWithFormatter: true, editor: { @@ -115,7 +137,15 @@ export class GridAngularComponent implements OnInit { minWidth: 100, filterable: true, sortable: true, - type: FieldType.string, + filter: { + model: new CustomAngularComponentFilter(), // create a new instance to make each Filter independent from each other + collection: this.assignees, + params: { + component: FilterNgSelectComponent, + } + }, + queryFieldFilter: 'assignee.id', // for a complex object it's important to tell the Filter which field to query and our CustomAngularComponentFilter returns the "id" property + queryFieldSorter: 'assignee.name', // loading formatter, text to display while Post Render gets processed formatter: () => '...', @@ -127,8 +157,9 @@ export class GridAngularComponent implements OnInit { params: { component: CustomTitleFormatterComponent, angularUtilService: this.angularUtilService, + complexFieldLabel: 'assignee.name' // for the exportCustomFormatter }, - exportWithFormatter: true, + exportCustomFormatter: Formatters.complexObject, }, { id: 'complete', name: '% Complete', @@ -148,21 +179,15 @@ export class GridAngularComponent implements OnInit { 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 } }, + filter: { + model: Filters.slider, + operator: '>=', + params: { hideSliderNumber: false } + }, params: { formatters: [Formatters.collectionEditor, Formatters.percentCompleteBar], } @@ -205,6 +230,7 @@ export class GridAngularComponent implements OnInit { containerId: 'demo-container', sidePadding: 15 }, + headerRowHeight: 45, rowHeight: 45, // increase row height so that the ng-select fits in the cell editable: true, enableCellNavigation: true, @@ -238,7 +264,7 @@ export class GridAngularComponent implements OnInit { tempDataset.push({ id: i, title: 'Task ' + i, - assignee: i % 3 ? this.assignees[2] : i % 2 ? this.assignees[1] : this.assignees[0], + assignee: i % 3 ? this.assignees[3] : i % 2 ? this.assignees[2] : this.assignees[1], duration: Math.round(Math.random() * 100) + '', percentComplete: randomPercent, percentCompleteNumber: randomPercent, diff --git a/src/app/examples/grid-editor.component.html b/src/app/examples/grid-editor.component.html index dd8466ebd..4af2b6734 100644 --- a/src/app/examples/grid-editor.component.html +++ b/src/app/examples/grid-editor.component.html @@ -3,7 +3,7 @@

    {{title}}

    - +