diff --git a/demo/angular/src/app/app.component.html b/demo/angular/src/app/app.component.html index fbf1d6e29..d76695e4a 100644 --- a/demo/angular/src/app/app.component.html +++ b/demo/angular/src/app/app.component.html @@ -35,7 +35,6 @@ - {{n.content}} @@ -46,6 +45,9 @@ + + + @@ -57,8 +59,8 @@ - + diff --git a/demo/angular/src/app/app.module.ts b/demo/angular/src/app/app.module.ts index 92d47cb0a..3447611a5 100644 --- a/demo/angular/src/app/app.module.ts +++ b/demo/angular/src/app/app.module.ts @@ -3,11 +3,12 @@ import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { GridstackItemComponent } from './gridstack-item.component'; -import { GridstackComponent } from './gridstack.component'; +import { GridstackComponent, gsCreateNgComponents, gsSaveAdditionNgInfo } from './gridstack.component'; import { AngularNgForTestComponent } from './ngFor'; import { AngularNgForCmdTestComponent } from './ngFor_cmd'; import { AngularSimpleComponent } from './simple'; import { AComponent, BComponent, CComponent } from './dummy.component'; +import { GridStack } from 'gridstack'; @NgModule({ declarations: [ @@ -34,10 +35,9 @@ import { AComponent, BComponent, CComponent } from './dummy.component'; export class AppModule { constructor() { // register all our dynamic components created in the grid - GridstackComponent.selectorToType = { - 'app-a': AComponent, - 'app-b': BComponent, - 'app-c': CComponent, - }; + GridstackComponent.addComponentToSelectorType([AComponent, BComponent, CComponent]); + // set globally our method to create the right widget type + GridStack.addRemoveCB = gsCreateNgComponents; // DONE in case switcher onShow() as well + GridStack.saveCB = gsSaveAdditionNgInfo; } } diff --git a/demo/angular/src/app/gridstack-item.component.ts b/demo/angular/src/app/gridstack-item.component.ts index e7f58ca0e..329514380 100644 --- a/demo/angular/src/app/gridstack-item.component.ts +++ b/demo/angular/src/app/gridstack-item.component.ts @@ -3,7 +3,7 @@ * Copyright (c) 2022 Alain Dumesny - see GridStack root license */ -import { ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild, ViewContainerRef, OnDestroy } from '@angular/core'; +import { Component, ElementRef, Input, ViewChild, ViewContainerRef, OnDestroy } from '@angular/core'; import { GridItemHTMLElement, GridStackNode } from 'gridstack'; /** store element to Ng Class pointer back */ @@ -28,7 +28,7 @@ export interface GridItemCompHTMLElement extends GridItemHTMLElement { styles: [` :host { display: block; } `], - changeDetection: ChangeDetectionStrategy.OnPush, + // changeDetection: ChangeDetectionStrategy.OnPush, // IFF you want to optimize and control when ChangeDetection needs to happen... }) export class GridstackItemComponent implements OnDestroy { @@ -42,8 +42,7 @@ export class GridstackItemComponent implements OnDestroy { this.el.gridstackNode.grid.update(this.el, val); } else { // store our custom element in options so we can update it and not re-create a generic div! - val.el = this.el; - this._options = val; + this._options = {...val, el: this.el}; } } /** return the latest grid options (from GS once built, otherwise initial values) */ diff --git a/demo/angular/src/app/gridstack.component.ts b/demo/angular/src/app/gridstack.component.ts index 26bb2e3bc..3869c4700 100644 --- a/demo/angular/src/app/gridstack.component.ts +++ b/demo/angular/src/app/gridstack.component.ts @@ -3,8 +3,8 @@ * Copyright (c) 2022 Alain Dumesny - see GridStack root license */ -import { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, EventEmitter, Input, - NgZone, OnDestroy, OnInit, Output, QueryList, Type, ViewChild, ViewContainerRef, createComponent, EnvironmentInjector } from '@angular/core'; +import { AfterContentInit, Component, ContentChildren, ElementRef, EventEmitter, Input, + OnDestroy, OnInit, Output, QueryList, Type, ViewChild, ViewContainerRef, reflectComponentType } from '@angular/core'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions, GridStackWidget } from 'gridstack'; @@ -17,7 +17,6 @@ export type elementCB = {event: Event, el: GridItemHTMLElement}; export type nodesCB = {event: Event, nodes: GridStackNode[]}; export type droppedCB = {event: Event, previousNode: GridStackNode, newNode: GridStackNode}; - /** extends to store Ng Component selector, instead/inAddition to content */ export interface NgGridStackWidget extends GridStackWidget { type?: string; // component type to create as content @@ -31,6 +30,7 @@ export interface GridCompHTMLElement extends GridHTMLElement { _gridComp?: GridstackComponent; } +/** selector string to runtime Type mapping */ export type SelectorToType = {[key: string]: Type}; /** @@ -49,7 +49,7 @@ export type SelectorToType = {[key: string]: Type}; styles: [` :host { display: block; } `], - changeDetection: ChangeDetectionStrategy.OnPush, + // changeDetection: ChangeDetectionStrategy.OnPush, // IFF you want to optimize and control when ChangeDetection needs to happen... }) export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy { @@ -97,8 +97,14 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy { * Unfortunately Ng doesn't provide public access to that mapping. */ public static selectorToType: SelectorToType = {}; - public static addSelector(key: string, type: Type) { - GridstackComponent.selectorToType[key] = type; + /** add a list of ng Component to be mapped to selector */ + public static addComponentToSelectorType(typeList: Array>) { + typeList.forEach(type => GridstackComponent.selectorToType[ GridstackComponent.getSelector(type) ] = type); + } + /** return the ng Component selector */ + public static getSelector(type: Type): string { + const mirror = reflectComponentType(type)!; + return mirror.selector; } private _options?: GridStackOptions; @@ -107,7 +113,8 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy { private ngUnsubscribe: Subject = new Subject(); constructor( - private readonly zone: NgZone, + // private readonly zone: NgZone, + // private readonly cd: ChangeDetectorRef, private readonly elementRef: ElementRef, ) { this.el._gridComp = this; @@ -118,19 +125,19 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy { this.loaded = !!this.options?.children?.length; this._grid = GridStack.init(this._options, this.el); delete this._options; // GS has it now + + this.checkEmpty(); } /** wait until after all DOM is ready to init gridstack children (after angular ngFor and sub-components run first) */ public ngAfterContentInit(): void { - this.zone.runOutsideAngular(() => { - // track whenever the children list changes and update the layout... - this.gridstackItems?.changes - .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe(() => this.updateAll()); - // ...and do this once at least unless we loaded children already - if (!this.loaded) this.updateAll(); - this.hookEvents(this.grid); - }); + // track whenever the children list changes and update the layout... + this.gridstackItems?.changes + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe(() => this.updateAll()); + // ...and do this once at least unless we loaded children already + if (!this.loaded) this.updateAll(); + this.hookEvents(this.grid); } public ngOnDestroy(): void { @@ -158,32 +165,35 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy { /** check if the grid is empty, if so show alternative content */ public checkEmpty() { if (!this.grid) return; - this.isEmpty = !this.grid.engine.nodes.length; + const isEmpty = !this.grid.engine.nodes.length; + if (isEmpty === this.isEmpty) return; + this.isEmpty = isEmpty; + // this.cd.detectChanges(); } /** get all known events as easy to use Outputs for convenience */ private hookEvents(grid?: GridStack) { if (!grid) return; grid - .on('added', (event: Event, nodes: GridStackNode[]) => this.zone.run(() => { this.checkEmpty(); this.addedCB.emit({event, nodes}); })) - .on('change', (event: Event, nodes: GridStackNode[]) => this.zone.run(() => this.changeCB.emit({event, nodes}))) - .on('disable', (event: Event) => this.zone.run(() => this.disableCB.emit({event}))) - .on('drag', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.dragCB.emit({event, el}))) - .on('dragstart', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.dragStartCB.emit({event, el}))) - .on('dragstop', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.dragStopCB.emit({event, el}))) - .on('dropped', (event: Event, previousNode: GridStackNode, newNode: GridStackNode) => this.zone.run(() => this.droppedCB.emit({event, previousNode, newNode}))) - .on('enable', (event: Event) => this.zone.run(() => this.enableCB.emit({event}))) - .on('removed', (event: Event, nodes: GridStackNode[]) => this.zone.run(() => { this.checkEmpty(); this.removedCB.emit({event, nodes}); })) - .on('resize', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.resizeCB.emit({event, el}))) - .on('resizestart', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.resizeStartCB.emit({event, el}))) - .on('resizestop', (event: Event, el: GridItemHTMLElement) => this.zone.run(() => this.resizeStopCB.emit({event, el}))) + .on('added', (event: Event, nodes: GridStackNode[]) => { this.checkEmpty(); this.addedCB.emit({event, nodes}); }) + .on('change', (event: Event, nodes: GridStackNode[]) => this.changeCB.emit({event, nodes})) + .on('disable', (event: Event) => this.disableCB.emit({event})) + .on('drag', (event: Event, el: GridItemHTMLElement) => this.dragCB.emit({event, el})) + .on('dragstart', (event: Event, el: GridItemHTMLElement) => this.dragStartCB.emit({event, el})) + .on('dragstop', (event: Event, el: GridItemHTMLElement) => this.dragStopCB.emit({event, el})) + .on('dropped', (event: Event, previousNode: GridStackNode, newNode: GridStackNode) => this.droppedCB.emit({event, previousNode, newNode})) + .on('enable', (event: Event) => this.enableCB.emit({event})) + .on('removed', (event: Event, nodes: GridStackNode[]) => { this.checkEmpty(); this.removedCB.emit({event, nodes}); }) + .on('resize', (event: Event, el: GridItemHTMLElement) => this.resizeCB.emit({event, el})) + .on('resizestart', (event: Event, el: GridItemHTMLElement) => this.resizeStartCB.emit({event, el})) + .on('resizestop', (event: Event, el: GridItemHTMLElement) => this.resizeStopCB.emit({event, el})) } } /** - * called by GS when a new item needs to be created, which we do as a Angular component, or deleted (skip) + * can be used when a new item needs to be created, which we do as a Angular component, or deleted (skip) **/ -function createNgComponents(host: GridCompHTMLElement | HTMLElement, w: NgGridStackWidget | GridStackOptions, add: boolean, isGrid: boolean): HTMLElement | undefined { +export function gsCreateNgComponents(host: GridCompHTMLElement | HTMLElement, w: NgGridStackWidget | GridStackOptions, add: boolean, isGrid: boolean): HTMLElement | undefined { if (add) { if (!host) return; // create the grid item dynamically - see https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html @@ -217,14 +227,10 @@ function createNgComponents(host: GridCompHTMLElement | HTMLElement, w: NgGridSt } /** - * called when saving the grid - put the extra info of type, otherwise content + * can be used when saving the grid - make sure we save the content from the field (not HTML as we get ng markups) + * and can put the extra info of type, otherwise content */ -function saveAdditionNgInfo(n: NgGridStackNode, w: NgGridStackWidget) { - // NOT needed as we get that by default in this case - // if (n.type) w.type = n.type; - // else if (n.content) w.content = n.content; +export function gsSaveAdditionNgInfo(n: NgGridStackNode, w: NgGridStackWidget) { + if (n.type) w.type = n.type; + else if (n.content) w.content = n.content; } - -// set globally our method to create the right widget type -GridStack.addRemoveCB = createNgComponents; -GridStack.saveCB = saveAdditionNgInfo; diff --git a/demo/angular/src/app/simple.ts b/demo/angular/src/app/simple.ts index 290ba6bfc..80df040de 100644 --- a/demo/angular/src/app/simple.ts +++ b/demo/angular/src/app/simple.ts @@ -17,7 +17,7 @@ // gridstack.min.css and other custom styles should be included in global styles.scss }) export class AngularSimpleComponent implements OnInit { - private items: GridStackWidget[] = [ + public items: GridStackWidget[] = [ { x: 0, y: 0, w: 9, h: 6, content: '0' }, { x: 9, y: 0, w: 3, h: 3, content: '1' }, { x: 9, y: 3, w: 3, h: 3, content: '2' }, diff --git a/src/gridstack-engine.ts b/src/gridstack-engine.ts index 60cf38fd2..6be639064 100644 --- a/src/gridstack-engine.ts +++ b/src/gridstack-engine.ts @@ -538,7 +538,7 @@ export class GridStackEngine { public removeAll(removeDOM = true): GridStackEngine { delete this._layouts; - if (this.nodes.length === 0) return this; + if (!this.nodes.length) return this; removeDOM && this.nodes.forEach(n => n._removeDOM = true); // let CB remove actual HTML (used to set _id to null, but then we loose layout info) this.removedNodes = this.nodes; this.nodes = []; diff --git a/src/gridstack.ts b/src/gridstack.ts index 398a65339..1f894ed12 100644 --- a/src/gridstack.ts +++ b/src/gridstack.ts @@ -178,12 +178,12 @@ export class GridStack { * add = false: the item will be removed from DOM (if not already done) * grid = true|false for grid vs grid-items */ - public static addRemoveCB: AddRemoveFcn; + public static addRemoveCB?: AddRemoveFcn; /** * callback during saving to application can inject extra data for each widget, on top of the grid layout properties */ - public static saveCB: SaveFcn; + public static saveCB?: SaveFcn; /** scoping so users can call GridStack.Utils.sort() for example */ public static Utils = Utils; @@ -899,11 +899,11 @@ export class GridStack { if (!removeDOM) { this.removeAll(removeDOM); this.el.classList.remove(this._styleSheetClass); + this.el.removeAttribute('gs-current-row'); } else { this.el.parentNode.removeChild(this.el); } this._removeStylesheet(); - this.el.removeAttribute('gs-current-row'); if (this.parentGridItem) delete this.parentGridItem.subGrid; delete this.parentGridItem; delete this.opts; @@ -1286,7 +1286,7 @@ export class GridStack { /** @internal */ protected _triggerAddEvent(): GridStack { if (this.engine.batchMode) return this; - if (this.engine.addedNodes && this.engine.addedNodes.length > 0) { + if (this.engine.addedNodes?.length) { if (!this._ignoreLayoutsNodeChange) { this.engine.layoutsNodesChange(this.engine.addedNodes); } @@ -1301,7 +1301,7 @@ export class GridStack { /** @internal */ public _triggerRemoveEvent(): GridStack { if (this.engine.batchMode) return this; - if (this.engine.removedNodes && this.engine.removedNodes.length > 0) { + if (this.engine.removedNodes?.length) { this._triggerEvent('removed', this.engine.removedNodes); this.engine.removedNodes = []; }