Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions angular/projects/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AngularNgForTestComponent } from './ngFor';
import { AngularNgForCmdTestComponent } from './ngFor_cmd';

// NOTE: local testing of file
// import { GridstackComponent, NgGridStackOptions, NgGridStackWidget, elementCB, gsCreateNgComponents, nodesCB } from '../../../../../dist/angular';
// import { GridstackComponent, NgGridStackOptions, NgGridStackWidget, elementCB, gsCreateNgComponents, nodesCB } from './gridstack.component';
import { GridstackComponent, NgGridStackOptions, NgGridStackWidget, elementCB, gsCreateNgComponents, nodesCB } from 'gridstack/dist/angular';

// unique ids sets for each item for correct ngFor updating
Expand All @@ -25,7 +25,7 @@ export class AppComponent implements OnInit {
@ViewChild('textArea', {static: true}) textEl?: ElementRef<HTMLTextAreaElement>;

// which sample to show
public show = 6;
public show = 5;

/** sample grid options and items to load... */
public items: GridStackWidget[] = [
Expand All @@ -38,9 +38,9 @@ export class AppComponent implements OnInit {
float: true,
minRow: 1,
}
public gridOptionsFull: GridStackOptions = {
public gridOptionsFull: NgGridStackOptions = {
...this.gridOptions,
children: this.items,
children: [{x:0, y:0, selector:'app-a'}, {x:1, y:0, selector:'app-b'}, {x:2, y:0, content:'plain html'}],
}

// nested grid options
Expand All @@ -50,7 +50,7 @@ export class AppComponent implements OnInit {
acceptWidgets: true, // will accept .grid-stack-item by default
margin: 5,
};
private sub1: NgGridStackWidget[] = [ {x:0, y:0, type:'app-a'}, {x:1, y:0, type:'app-b'}, {x:2, y:0, type:'app-c'}, {x:3, y:0}, {x:0, y:1}, {x:1, y:1}];
private sub1: NgGridStackWidget[] = [ {x:0, y:0, selector:'app-a'}, {x:1, y:0, selector:'app-b'}, {x:2, y:0, selector:'app-c'}, {x:3, y:0}, {x:0, y:1}, {x:1, y:1}];
private sub2: NgGridStackWidget[] = [ {x:0, y:0}, {x:0, y:1, w:2}];
private subChildren: NgGridStackWidget[] = [
{x:0, y:0, content: 'regular item'},
Expand All @@ -71,7 +71,7 @@ export class AppComponent implements OnInit {
constructor() {
// give them content and unique id to make sure we track them during changes below...
[...this.items, ...this.subChildren, ...this.sub1, ...this.sub2].forEach((w: NgGridStackWidget) => {
if (!w.type && !w.subGridOpts) w.content = `item ${ids}`;
if (!w.selector && !w.subGridOpts) w.content = `item ${ids}`;
w.id = String(ids++);
});
}
Expand Down
3 changes: 2 additions & 1 deletion angular/projects/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { AngularSimpleComponent } from './simple';
import { AComponent, BComponent, CComponent } from './dummy.component';

// local testing
// import { GridstackModule, GridstackComponent } from '../../../../../dist/angular';
// import { GridstackModule } from './gridstack.module';
// import { GridstackComponent } from './gridstack.component';
import { GridstackModule, GridstackComponent } from 'gridstack/dist/angular';

@NgModule({
Expand Down
16 changes: 11 additions & 5 deletions angular/projects/demo/src/app/dummy.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,31 @@

// dummy testing component that will be grid items content

import { Component } from '@angular/core';
import { Component, OnDestroy, Input } from '@angular/core';
import { BaseWidget, NgCompInputs } from 'gridstack/dist/angular';

@Component({
selector: 'app-a',
template: 'Comp A',
template: 'Comp A {{text}}',
})
export class AComponent {
export class AComponent extends BaseWidget implements OnDestroy {
@Input() text: string = 'foo'; // test custom input data
public override serialize(): NgCompInputs | undefined { return this.text ? {text: this.text} : undefined; }
ngOnDestroy() {
console.log('Comp A destroyed'); // test to make sure cleanup happens
}
}

@Component({
selector: 'app-b',
template: 'Comp B',
})
export class BComponent {
export class BComponent extends BaseWidget {
}

@Component({
selector: 'app-c',
template: 'Comp C',
})
export class CComponent {
export class CComponent extends BaseWidget {
}
28 changes: 28 additions & 0 deletions angular/projects/lib/src/lib/base-widgets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* gridstack-item.component.ts 8.1.2-dev
* Copyright (c) 2022 Alain Dumesny - see GridStack root license
*/

/**
* Base interface that all widgets need to implement in order for GridstackItemComponent to correctly save/load/delete/..
*/

import { Injectable } from '@angular/core';
import { NgCompInputs, NgGridStackWidget } from './gridstack.component';

@Injectable()
export abstract class BaseWidget {
/**
* REDEFINE to return an object representing the data needed to re-create yourself, other than `selector` already handled.
* This should map directly to the @Input() fields of this objects on create, so a simple apply can be used on read
*/
public serialize(): NgCompInputs | undefined { return; }

/**
* REDEFINE this if your widget needs to read from saved data and transform it to create itself - you do this for
* things that are not mapped directly into @Input() fields for example.
*/
public deserialize(w: NgGridStackWidget) {
if (w.input) Object.assign(this, w.input);
}
}
10 changes: 9 additions & 1 deletion angular/projects/lib/src/lib/gridstack-item.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* Copyright (c) 2022 Alain Dumesny - see GridStack root license
*/

import { Component, ElementRef, Input, ViewChild, ViewContainerRef, OnDestroy } from '@angular/core';
import { Component, ElementRef, Input, ViewChild, ViewContainerRef, OnDestroy, ComponentRef } from '@angular/core';
import { GridItemHTMLElement, GridStackNode } from 'gridstack';
import { BaseWidget } from './base-widgets';

/** store element to Ng Class pointer back */
export interface GridItemCompHTMLElement extends GridItemHTMLElement {
Expand Down Expand Up @@ -35,6 +36,12 @@ export class GridstackItemComponent implements OnDestroy {
/** container to append items dynamically */
@ViewChild('container', { read: ViewContainerRef, static: true}) public container?: ViewContainerRef;

/** ComponentRef of ourself - used by dynamic object to correctly get removed */
public ref: ComponentRef<GridstackItemComponent> | undefined;

/** child component so we can save/restore additional data to be saved along */
public childWidget: BaseWidget | undefined;

/** list of options for creating/updating this item */
@Input() public set options(val: GridStackNode) {
if (this.el.gridstackNode?.grid) {
Expand Down Expand Up @@ -65,6 +72,7 @@ export class GridstackItemComponent implements OnDestroy {
}

public ngOnDestroy(): void {
delete this.ref;
delete this.el._gridItemComp;
}
}
117 changes: 82 additions & 35 deletions angular/projects/lib/src/lib/gridstack.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,29 @@
*/

import { AfterContentInit, Component, ContentChildren, ElementRef, EventEmitter, Input,
OnDestroy, OnInit, Output, QueryList, Type, ViewChild, ViewContainerRef, reflectComponentType } from '@angular/core';
OnDestroy, OnInit, Output, QueryList, Type, ViewChild, ViewContainerRef, reflectComponentType, ComponentRef } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridStackOptions, GridStackWidget } from 'gridstack';

import { GridItemCompHTMLElement, GridstackItemComponent } from './gridstack-item.component';
import { BaseWidget } from './base-widgets';

/** events handlers emitters signature for different events */
export type eventCB = {event: Event};
export type elementCB = {event: Event, el: GridItemHTMLElement};
export type nodesCB = {event: Event, nodes: GridStackNode[]};
export type droppedCB = {event: Event, previousNode: GridStackNode, newNode: GridStackNode};

export type NgCompInputs = {[key: string]: any};

/** extends to store Ng Component selector, instead/inAddition to content */
export interface NgGridStackWidget extends GridStackWidget {
type?: string; // component type to create as content
selector?: string; // component type to create as content
input?: NgCompInputs; // serialized data for the component input fields
}
export interface NgGridStackNode extends GridStackNode {
type?: string; // component type to create as content
selector?: string; // component type to create as content
}
export interface NgGridStackOptions extends GridStackOptions {
children?: NgGridStackWidget[];
Expand Down Expand Up @@ -96,6 +100,9 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
/** return the GridStack class */
public get grid(): GridStack | undefined { return this._grid; }

/** ComponentRef of ourself - used by dynamic object to correctly get removed */
public ref: ComponentRef<GridstackComponent> | undefined;

/**
* stores the selector -> Type mapping, so we can create items dynamically from a string.
* Unfortunately Ng doesn't provide public access to that mapping.
Expand All @@ -107,8 +114,7 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
}
/** return the ng Component selector */
public static getSelector(type: Type<Object>): string {
const mirror = reflectComponentType(type)!;
return mirror.selector;
return reflectComponentType(type)!.selector;
}

private _options?: GridStackOptions;
Expand Down Expand Up @@ -145,6 +151,7 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
}

public ngOnDestroy(): void {
delete this.ref;
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
this.grid?.destroy();
Expand Down Expand Up @@ -197,44 +204,84 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
/**
* can be used when a new item needs to be created, which we do as a Angular component, or deleted (skip)
**/
export function gsCreateNgComponents(host: GridCompHTMLElement | HTMLElement, w: NgGridStackWidget | GridStackOptions, add: boolean, isGrid: boolean): HTMLElement | undefined {
// only care about creating ng components here...
if (!add || !host) return;

// create the component dynamically - see https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html
if (isGrid) {
let grid: GridstackComponent | undefined;
const gridItemComp = (host.parentElement as GridItemCompHTMLElement)?._gridItemComp;
if (gridItemComp) {
grid = gridItemComp.container?.createComponent(GridstackComponent)?.instance;
} else {
export function gsCreateNgComponents(host: GridCompHTMLElement | HTMLElement, w: NgGridStackWidget | GridStackNode, add: boolean, isGrid: boolean): HTMLElement | undefined {
if (add) {
//
// create the component dynamically - see https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html
//
if (!host) return;
if (isGrid) {
const container = (host.parentElement as GridItemCompHTMLElement)?._gridItemComp?.container;
// TODO: figure out how to create ng component inside regular Div. need to access app injectors...
// const hostElement: Element = host;
// const environmentInjector: EnvironmentInjector;
// grid = createComponent(GridstackComponent, {environmentInjector, hostElement})?.instance;
// if (!container) {
// const hostElement: Element = host;
// const environmentInjector: EnvironmentInjector;
// grid = createComponent(GridstackComponent, {environmentInjector, hostElement})?.instance;
// }
const gridRef = container?.createComponent(GridstackComponent);
const grid = gridRef?.instance;
if (!grid) return;
grid.ref = gridRef;
grid.options = w as GridStackOptions;
return grid.el;
} else {
const gridComp = (host as GridCompHTMLElement)._gridComp;
const gridItemRef = gridComp?.container?.createComponent(GridstackItemComponent);
const gridItem = gridItemRef?.instance;
if (!gridItem) return;
gridItem.ref = gridItemRef

// IFF we're not a subGrid, define what type of component to create as child, OR you can do it GridstackItemComponent template, but this is more generic
const selector = (w as NgGridStackWidget).selector;
const type = selector ? GridstackComponent.selectorToType[selector] : undefined;
if (!w.subGridOpts && type) {
const childWidget = gridItem.container?.createComponent(type)?.instance as BaseWidget;
if (typeof childWidget?.serialize === 'function' && typeof childWidget?.deserialize === 'function') {
// proper BaseWidget subclass, save it and load additional data
gridItem.childWidget = childWidget;
childWidget.deserialize(w);
}
}

return gridItem.el;
}
if (grid) grid.options = w as GridStackOptions;
return grid?.el;
} else {
const gridComp = (host as GridCompHTMLElement)._gridComp;
const gridItem = gridComp?.container?.createComponent(GridstackItemComponent)?.instance;

// IFF we're not a subGrid, define what type of component to create as child, OR you can do it GridstackItemComponent template, but this is more generic
const selector = (w as NgGridStackWidget).type;
const type = selector ? GridstackComponent.selectorToType[selector] : undefined;
if (!w.subGridOpts && type) {
gridItem?.container?.createComponent(type);
//
// REMOVE - have to call ComponentRef:destroy() for dynamic objects to correctly remove themselves
// Note: this will destroy all children dynamic components as well: gridItem -> childWidget
//
const n = w as GridStackNode;
if (isGrid) {
const grid = (n.el as GridCompHTMLElement)?._gridComp;
if (grid?.ref) grid.ref.destroy();
else grid?.ngOnDestroy();
} else {
const gridItem = (n.el as GridItemCompHTMLElement)?._gridItemComp;
if (gridItem?.ref) gridItem.ref.destroy();
else gridItem?.ngOnDestroy();
}

return gridItem?.el;
}
return;
}

/**
* 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
* called for each item in the grid - check if additional information needs to be saved.
* Note: since this is options minus gridstack private members using Utils.removeInternalForSave(),
* this typically doesn't need to do anything. However your custom Component @Input() are now supported
* using BaseWidget.serialize()
*/
export function gsSaveAdditionalNgInfo(n: NgGridStackNode, w: NgGridStackWidget) {
if (n.type) w.type = n.type;
else if (n.content) w.content = n.content;
const gridItem = (n.el as GridItemCompHTMLElement)?._gridItemComp;
if (gridItem) {
const input = gridItem.childWidget?.serialize();
if (input) {
w.input = input;
}
return;
}
// else check if Grid
const grid = (n.el as GridCompHTMLElement)?._gridComp;
if (grid) {
//.... save any custom data
}
}
1 change: 1 addition & 0 deletions angular/projects/lib/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@

export * from './lib/gridstack-item.component';
export * from './lib/gridstack.component';
export * from './lib/base-widgets';
export * from './lib/gridstack.module';
2 changes: 2 additions & 0 deletions doc/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Change log

## 8.1.2-dev TBD
* feat: `makeWidget()` now take optional `GridStackWidget` for sizing
* fix: make sure `GridStack.saveCB` is call in `removeWidget()`
* feat: angular wrapper: serialize custom data support, and making sure destroy() is called on ng components

## 8.1.2 (2023-5-22)
* [#2323](https://github.com/gridstack/gridstack.js/issues/2323) module for Angular wrapper
Expand Down