diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index aa1a527fa..5bde19c96 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,25 +1,25 @@ +import { HomeComponent } from './examples/home.component'; import { GridAddItemComponent } from './examples/grid-additem.component'; -import { GridMenuComponent } from './examples/grid-menu.component'; +import { GridAngularComponent } from './examples/grid-angular.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'; import { GridGroupingComponent } from './examples/grid-grouping.component'; import { GridHeaderButtonComponent } from './examples/grid-headerbutton.component'; import { GridHeaderMenuComponent } from './examples/grid-headermenu.component'; import { GridLocalizationComponent } from './examples/grid-localization.component'; +import { GridMenuComponent } from './examples/grid-menu.component'; import { GridOdataComponent } from './examples/grid-odata.component'; -import { GridGraphqlComponent } from './examples/grid-graphql.component'; import { GridRemoteComponent } from './examples/grid-remote.component'; import { GridRowDetailComponent } from './examples/grid-rowdetail.component'; import { GridRowMoveComponent } from './examples/grid-rowmove.component'; import { GridRowSelectionComponent } from './examples/grid-rowselection.component'; import { GridStateComponent } from './examples/grid-state.component'; -import { HomeComponent } from './examples/home.component'; import { SwtCommonGridTestComponent } from './examples/swt-common-grid-test.component'; import { NgModule } from '@angular/core'; @@ -28,11 +28,11 @@ import { TranslateModule } from '@ngx-translate/core'; const routes: Routes = [ { path: 'home', component: HomeComponent }, + { path: 'angular-components', component: GridAngularComponent }, { path: 'additem', component: GridAddItemComponent }, { 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 5e436a0c4..c6daf9ab5 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- Editors Angular Components
  • diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1335896b9..9f743d8f4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -9,6 +9,7 @@ import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-transla 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 { GridAddItemComponent } from './examples/grid-additem.component'; import { GridBasicComponent } from './examples/grid-basic.component'; @@ -16,7 +17,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 { GridAngularComponent } from './examples/grid-angular.component'; import { GridFormatterComponent } from './examples/grid-formatter.component'; import { GridFrozenComponent } from './examples/grid-frozen.component'; import { GridGraphqlComponent } from './examples/grid-graphql.component'; @@ -71,14 +72,15 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj @NgModule({ declarations: [ AppComponent, + CustomTitleFormatterComponent, EditorNgSelectComponent, GridAddItemComponent, + GridAngularComponent, GridBasicComponent, GridClientSideComponent, GridColspanComponent, GridDraggableGroupingComponent, GridEditorComponent, - GridEditorAngularComponent, GridFormatterComponent, GridFrozenComponent, GridGraphqlComponent, @@ -125,6 +127,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj ], entryComponents: [ // dynamically created components + CustomTitleFormatterComponent, EditorNgSelectComponent, RowDetailPreloadComponent, RowDetailViewComponent, diff --git a/src/app/examples/custom-angularComponentEditor.ts b/src/app/examples/custom-angularComponentEditor.ts index 5bb683b47..e40d34418 100644 --- a/src/app/examples/custom-angularComponentEditor.ts +++ b/src/app/examples/custom-angularComponentEditor.ts @@ -5,6 +5,7 @@ import { Editor, EditorValidator, EditorValidatorOutput, + GridOption, } from './../modules/angular-slickgrid'; /* @@ -21,13 +22,21 @@ export class CustomAngularComponentEditor implements Editor { /** default item object */ defaultItem: any; + /** SlickGrid grid object */ + grid: any; + constructor(private args: any) { + this.grid = args && args.grid; this.init(); } - /** Angular Util Service */ + /** Angular Util Service (could be inside the Grid Options Params or the Editor Params ) */ get angularUtilService(): AngularUtilService { - return this.columnDef && this.columnDef && this.columnDef.internalColumnEditor && this.columnDef.internalColumnEditor.params.angularUtilService; + let angularUtilService = this.gridOptions && this.gridOptions.params && this.gridOptions.params.angularUtilService; + if (!angularUtilService || !(angularUtilService instanceof AngularUtilService)) { + angularUtilService = this.columnEditor && this.columnEditor.params && this.columnEditor.params.angularUtilService; + } + return angularUtilService; } /** Get the Collection */ @@ -45,8 +54,13 @@ export class CustomAngularComponentEditor implements Editor { return this.columnDef && this.columnDef.internalColumnEditor || {}; } + /** Getter for the Grid Options pulled through the Grid Object */ + get gridOptions(): GridOption { + return (this.grid && this.grid.getOptions) ? this.grid.getOptions() : {}; + } + get hasAutoCommitEdit() { - return this.args.grid.getOptions().autoCommitEdit; + return this.gridOptions.autoCommitEdit; } /** Get the Validator function, can be passed in Editor property or Column Definition */ @@ -55,12 +69,15 @@ export class CustomAngularComponentEditor implements Editor { } init() { - if (!this.columnEditor || !this.columnEditor.params.component) { + 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. - Example: this.columnDefs = [{ id: 'title', field: 'title', editor: { component: MyComponent, model: Editors.angularComponent, collection: [...] },`); + 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 }]; + OR this.columnDefs = [{ id: 'title', field: 'title', editor: { model: CustomAngularComponentEditor, collection: [...] }]; this.gridOptions = { params: { angularUtilService: this.angularUtilService }}`); } if (this.columnEditor && this.columnEditor.params.component) { - this.componentRef = this.columnEditor.params.angularUtilService.createAngularComponentAppendToDom(this.columnEditor.params.component, this.args.container); + const componentOutput = this.angularUtilService.createAngularComponentAppendToDom(this.columnEditor.params.component, this.args.container); + this.componentRef = componentOutput && componentOutput.componentRef; Object.assign(this.componentRef.instance, { collection: this.collection }); this.componentRef.instance.onModelChanged.subscribe((item) => { diff --git a/src/app/examples/custom-titleFormatter.component.ts b/src/app/examples/custom-titleFormatter.component.ts new file mode 100644 index 000000000..db318fd64 --- /dev/null +++ b/src/app/examples/custom-titleFormatter.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + template: `{{item?.assignee?.name}}` +}) +export class CustomTitleFormatterComponent { + item: any; +} diff --git a/src/app/examples/grid-editor-angular.component.html b/src/app/examples/grid-angular.component.html similarity index 100% rename from src/app/examples/grid-editor-angular.component.html rename to src/app/examples/grid-angular.component.html diff --git a/src/app/examples/grid-editor-angular.component.ts b/src/app/examples/grid-angular.component.ts similarity index 73% rename from src/app/examples/grid-editor-angular.component.ts rename to src/app/examples/grid-angular.component.ts index b4f414470..883ffe8ee 100644 --- a/src/app/examples/grid-editor-angular.component.ts +++ b/src/app/examples/grid-angular.component.ts @@ -1,5 +1,4 @@ -import { Component, Injectable, OnInit } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { Component, Injectable, OnInit, EmbeddedViewRef } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AngularGridInstance, @@ -15,24 +14,29 @@ import { } from './../modules/angular-slickgrid'; import { EditorNgSelectComponent } from './editor-ng-select.component'; import { CustomAngularComponentEditor } from './custom-angularComponentEditor'; +import { CustomTitleFormatterComponent } from './custom-titleFormatter.component'; // using external non-typed js libraries declare var Slick: any; +declare var $: any; const NB_ITEMS = 100; @Component({ - templateUrl: './grid-editor-angular.component.html' + templateUrl: './grid-angular.component.html' }) @Injectable() -export class GridEditorAngularComponent implements OnInit { - title = 'Example 22: Editors with Angular Components'; +export class GridAngularComponent implements OnInit { + title = 'Example 22: Multiple Angular Components'; subTitle = ` - Grid with Inline Editors and onCellClick actions (Wiki docs). + Grid with usage of Angular Components as Editor & AsyncPostRender (similar to Formatter). `; @@ -52,7 +56,7 @@ export class GridEditorAngularComponent implements OnInit { { id: '3', name: 'Paul' }, ]; - constructor(private angularUtilService: AngularUtilService, private http: HttpClient, private translate: TranslateService) {} + constructor(private angularUtilService: AngularUtilService, private translate: TranslateService) {} ngOnInit(): void { this.prepareGrid(); @@ -90,14 +94,13 @@ export class GridEditorAngularComponent implements OnInit { type: FieldType.string, formatter: Formatters.complexObject, params: { - complexField: 'assignee.name' + complexField: 'assignee.name', }, exportWithFormatter: true, editor: { model: CustomAngularComponentEditor, collection: this.assignees, params: { - angularUtilService: this.angularUtilService, component: EditorNgSelectComponent, } }, @@ -106,19 +109,26 @@ export class GridEditorAngularComponent implements OnInit { this.alertWarning = `Updated Title: ${args.dataContext.title}`; } }, { - id: 'duration', - name: 'Duration (days)', - field: 'duration', + id: 'assignee2', + name: 'Assignee with Angular Component', + field: 'assignee', minWidth: 100, filterable: true, sortable: true, - type: FieldType.number, - filter: { model: Filters.slider, params: { hideSliderNumber: false } }, - editor: { - model: Editors.slider, - minValue: 0, - maxValue: 100, - } + type: FieldType.string, + + // loading formatter, text to display while Post Render gets processed + formatter: () => '...', + + // to load an Angular Component, you cannot use a Formatter since Angular needs at least 1 cycle to render everything + // you can use a PostRenderer but you will visually see the data appearing, + // which is why it's still better to use regular Formatter (with jQuery if need be) instead of Angular Component + asyncPostRender: this.renderAngularComponent.bind(this), + params: { + component: CustomTitleFormatterComponent, + angularUtilService: this.angularUtilService, + }, + exportWithFormatter: true, }, { id: 'complete', name: '% Complete', @@ -201,11 +211,16 @@ export class GridEditorAngularComponent implements OnInit { enableColumnPicker: true, enableExcelCopyBuffer: true, enableFiltering: true, + enableAsyncPostRender: true, // for the Angular PostRenderer, don't forget to enable it + asyncPostRenderDelay: 0, // also make sure to remove any delay to render it editCommandHandler: (item, column, editCommand) => { this._commandQueue.push(editCommand); editCommand.execute(); }, - i18n: this.translate + i18n: this.translate, + params: { + angularUtilService: this.angularUtilService // provide the service to all at once (Editor, Filter, AsyncPostRender) + } }; this.dataset = this.mockData(NB_ITEMS); @@ -283,4 +298,14 @@ export class GridEditorAngularComponent implements OnInit { this.gridObj.gotoCell(command.row, command.cell, false); } } + + renderAngularComponent(cellNode: HTMLElement, row: number, dataContext: any, colDef: Column) { + if (colDef.params.component) { + const componentOutput = this.angularUtilService.createAngularComponent(colDef.params.component); + Object.assign(componentOutput.componentRef.instance, { item: dataContext }); + + // use a delay to make sure Angular ran at least a full cycle and it finished rendering the Component + setTimeout(() => $(cellNode).empty().html(componentOutput.domElement)); + } + } } diff --git a/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts b/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts index ebc97fcbd..ea51e1f3e 100644 --- a/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts +++ b/src/app/modules/angular-slickgrid/extensions/rowDetailViewExtension.ts @@ -324,12 +324,14 @@ 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.createAngularComponentAppendToDom(this._viewComponent, containerElements[0]); - Object.assign(compRef.instance, { model: item }); + const componentOutput = this.angularUtilService.createAngularComponentAppendToDom(this._viewComponent, containerElements[0]); + if (componentOutput && componentOutput.componentRef && componentOutput.componentRef.instance) { + Object.assign(componentOutput.componentRef.instance, { model: item }); - const viewObj = this._views.find((obj) => obj.id === item.id); - if (viewObj) { - viewObj.componentRef = compRef; + const viewObj = this._views.find((obj) => obj.id === item.id); + if (viewObj) { + viewObj.componentRef = componentOutput.componentRef; + } } } } diff --git a/src/app/modules/angular-slickgrid/models/angularComponentOutput.interface.ts b/src/app/modules/angular-slickgrid/models/angularComponentOutput.interface.ts new file mode 100644 index 000000000..7a225a636 --- /dev/null +++ b/src/app/modules/angular-slickgrid/models/angularComponentOutput.interface.ts @@ -0,0 +1,6 @@ +import { ComponentRef } from '@angular/core'; + +export interface AngularComponentOutput { + componentRef: ComponentRef; + domElement: HTMLElement; +} diff --git a/src/app/modules/angular-slickgrid/models/index.ts b/src/app/modules/angular-slickgrid/models/index.ts index 285c508a2..f99cd7c0f 100644 --- a/src/app/modules/angular-slickgrid/models/index.ts +++ b/src/app/modules/angular-slickgrid/models/index.ts @@ -1,4 +1,5 @@ export * from './aggregator.interface'; +export * from './angularComponentOutput.interface'; export * from './angularGridInstance.interface'; export * from './autoResizeOption.interface'; export * from './backendService.interface'; diff --git a/src/app/modules/angular-slickgrid/services/angularUtilService.ts b/src/app/modules/angular-slickgrid/services/angularUtilService.ts index 1b23f170e..e787ba099 100644 --- a/src/app/modules/angular-slickgrid/services/angularUtilService.ts +++ b/src/app/modules/angular-slickgrid/services/angularUtilService.ts @@ -1,3 +1,4 @@ +import { AngularComponentOutput } from './../models/angularComponentOutput.interface'; import { ApplicationRef, ComponentFactoryResolver, ComponentRef, EmbeddedViewRef, Injectable, Injector } from '@angular/core'; @Injectable() @@ -9,7 +10,7 @@ export class AngularUtilService { ) { } // ref https://hackernoon.com/angular-pro-tip-how-to-dynamically-create-components-in-body-ba200cc289e6 - createAngularComponentAppendToDom(component: any, targetElement?: HTMLElement | Element): ComponentRef { + createAngularComponent(component: any): AngularComponentOutput { // Create a component reference from the component const componentRef = this.compFactoryResolver .resolveComponentFactory(component) @@ -19,16 +20,26 @@ export class AngularUtilService { this.appRef.attachView(componentRef.hostView); // Get DOM element from component - const domElem = (componentRef.hostView as EmbeddedViewRef) - .rootNodes[0] as HTMLElement; + let domElem; + const viewRef = (componentRef.hostView as EmbeddedViewRef); + if (viewRef && Array.isArray(viewRef.rootNodes) && viewRef.rootNodes[0]) { + domElem = viewRef.rootNodes[0] as HTMLElement; + } + + return { componentRef, domElement: domElem }; + } + + // ref https://hackernoon.com/angular-pro-tip-how-to-dynamically-create-components-in-body-ba200cc289e6 + createAngularComponentAppendToDom(component: any, targetElement?: HTMLElement | Element): AngularComponentOutput { + const componentOutput = this.createAngularComponent(component); // Append DOM element to the HTML element specified if (targetElement && targetElement.appendChild) { - targetElement.appendChild(domElem); + targetElement.appendChild(componentOutput.domElement); } else { - document.body.appendChild(domElem); // when no target provided, we'll simply add it to the HTML Body + document.body.appendChild(componentOutput.domElement); // when no target provided, we'll simply add it to the HTML Body } - return componentRef; + return componentOutput; } }