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
120 changes: 89 additions & 31 deletions demo/angular/src/app/README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,121 @@
# Angular wrapper

The Angular [wrapper component](./gridstack.component.ts) <gridstack> is a better way to use Gridstack, but alternative raw [NgFor](./ngFor.ts) or [Simple](./simple.ts) demos are also given.
The Angular [wrapper component](./gridstack.component.ts) <gridstack> is a better way to use Gridstack, but alternative raw [ngFor](./ngFor.ts) or [simple](./simple.ts) demos are also given.

## Dynamic grid items
this is the recommended way if you are going to have multiple grids (alow drag&drop between) or drag from toolbar to create items, or drag to remove items...
# Dynamic grid items
this is the recommended way if you are going to have multiple grids (alow drag&drop between) or drag from toolbar to create items, or drag to remove items, etc...

I.E. don't use Angular templating to create children as that is harder to sync.
I.E. don't use Angular templating to create grid items as that is harder to sync when gridstack will also add/remove items.

HTML
```html
<gridstack [options]="gridOptions">
</gridstack>
```
Code
```ts
import { GridStack, GridStackOptions } from 'gridstack';
import { gsCreateNgComponents } from './gridstack.component';

```javascript
import { GridStackOptions, GridStackWidget } from 'gridstack';
import { GridstackComponent, nodesCB } from './gridstack.component';
constructor() {
// use the built in component creation code
GridStack.addRemoveCB = gsCreateNgComponents;
}

/** sample grid options and items to load... */
// sample grid options and items to load...
public gridOptions: GridStackOptions = {
margin: 5,
float: true,
children: [ // or call load() with same data
{x:0, y:0, minW:2, id:'1', content:'Item 1'},
{x:1, y:1, id:'2', content:'Item 2'},
{x:2, y:2, id:'3', content:'Item 3'},
children: [ // or call load()/addWidget() with same data
{x:0, y:0, minW:2, content:'Item 1'},
{x:1, y:1, content:'Item 2'},
{x:2, y:2, content:'Item 3'},
]
}
```

// called whenever items change size/position/etc..
# More Complete example
In this example will your actual custom angular components inside each grid item (instead of dummy html content)

HTML
```html
<gridstack [options]="gridOptions" (changeCB)="onChange($event)">
<div empty-content>message when grid is empty</div>
</gridstack>
```

Code
```ts
import { Component } from '@angular/core';
import { GridStack, GridStackOptions } from 'gridstack';
import { GridstackComponent, gsCreateNgComponents, NgGridStackWidget, nodesCB } from './gridstack.component';

// some custom components
@Component({
selector: 'app-a',
template: 'Comp A', // your real ng content goes in each component instead...
})
export class AComponent {
}

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

// .... in your module for example
constructor() {
// register all our dynamic components created in the grid
GridstackComponent.addComponentToSelectorType([AComponent, BComponent]);
// set globally our method to create the right widget type
GridStack.addRemoveCB = gsCreateNgComponents;
GridStack.saveCB = gsSaveAdditionalNgInfo;
}

// and now our content will look like instead of dummy html content
public gridOptions: NgGridStackOptions = {
margin: 5,
float: true,
minRow: 1, // make space for empty message
children: [ // or call load()/addWidget() with same data
{x:0, y:0, minW:2, type:'app-a'},
{x:1, y:1, type:'app-b'},
{x:2, y:2, content:'plain html content'},
]
}

// called whenever items change size/position/etc.. see other events
public onChange(data: nodesCB) {
console.log('change ', data.nodes.length > 1 ? data.nodes : data.nodes[0]);
}
```

# ngFor with wrapper
For simple case where you control the children creation (gridstack doesn't do create or re-parenting)

HTML
```html
<gridstack [options]="gridOptions" (changeCB)="onChange($event)">
<gridstack-item *ngFor="let n of items; trackBy: identify" [options]="n">
Item {{n.id}}
</gridstack-item>
</gridstack>
```

## ngFor with wrapper
For simple case where you control the children creation (gridstack doesn't do create or re-parenting)

Code

```javascript
import { GridStackOptions, GridStackWidget } from 'gridstack';
import { GridstackComponent, nodesCB } from './gridstack.component';
import { nodesCB } from './gridstack.component';

/** sample grid options and items to load... */
public gridOptions: GridStackOptions = {
margin: 5,
float: true,
}
public items: GridStackWidget[] = [
{x:0, y:0, minW:2, id:'1'},
{x:0, y:0, minW:2, id:'1'}, // must have unique id used for trackBy
{x:1, y:1, id:'2'},
{x:2, y:2, id:'3'},
];
Expand All @@ -65,14 +130,6 @@ public identify(index: number, w: GridStackWidget) {
return w.id; // or use index if no id is set and you only modify at the end...
}
```
HTML
```html
<gridstack [options]="gridOptions" (changeCB)="onChange($event)">
<gridstack-item *ngFor="let n of items; trackBy: identify" [options]="n">
Item {{n.id}}
</gridstack-item>
</gridstack>
```

## Demo
You can see a fuller example at [app.component.ts](https://github.com/gridstack/gridstack.js/blob/master/demo/angular/src/app/app.component.ts)
Expand All @@ -81,14 +138,15 @@ to build the demo, go to demo/angular and run `yarn` + `yarn start` and Navigate

## Caveats

- This wrapper needs v7.2+ to run as it needs the latest changes
- This wrapper needs v8.0+ to run as it needs the latest changes
- Code isn't compiled into a lib YET. You'll need to copy those files. Let me know (slack) if you are using it...
- BUG: content doesn't appear on new widget until widget is moved around (or another created that pushes it). Need to force angular detection changes...

## ngFor Caveats
## *ngFor Caveats
- This wrapper handles well ngFor loops, but if you're using a trackBy function (as I would recommend) and no element id change after an update,
you must manually update the `GridstackItemComponent.option` directly - see [modifyNgFor()](./app.component.ts#L83) example.
you must manually update the `GridstackItemComponent.option` directly - see [modifyNgFor()](./app.component.ts#L174) example.
- The original client list of items is not updated to match **content** changes made by gridstack (TBD later), but adding new item or removing (as shown in demo) will update those new items. Client could use change/added/removed events to sync that list if they wish to do so.


Would appreciate getting help doing the same for React and Vue (2 other popular frameworks)

-Alain
6 changes: 3 additions & 3 deletions demo/angular/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { GridStack, GridStackOptions, GridStackWidget } from 'gridstack';
import { GridstackComponent, NgGridStackWidget, elementCB, gsCreateNgComponents, nodesCB } from './gridstack.component';
import { GridstackComponent, NgGridStackOptions, NgGridStackWidget, elementCB, gsCreateNgComponents, nodesCB } from './gridstack.component';
import { AngularSimpleComponent } from './simple';
import { AngularNgForTestComponent } from './ngFor';
import { AngularNgForCmdTestComponent } from './ngFor_cmd';
Expand Down Expand Up @@ -54,7 +54,7 @@ export class AppComponent implements OnInit {
{x:1, y:0, w:4, h:4, subGridOpts: {children: this.sub1, id:'sub1_grid', class: 'sub1', ...this.subOptions}},
{x:5, y:0, w:3, h:4, subGridOpts: {children: this.sub2, id:'sub2_grid', class: 'sub2', ...this.subOptions}},
]
public nestedGridOptions: GridStackOptions = { // main grid options
public nestedGridOptions: NgGridStackOptions = { // main grid options
cellHeight: 50,
margin: 5,
minRow: 2, // don't collapse when empty
Expand All @@ -63,7 +63,7 @@ export class AppComponent implements OnInit {
id: 'main',
children: this.subChildren
};
private serializedData?: GridStackOptions;
private serializedData?: NgGridStackOptions;

constructor() {
// give them content and unique id to make sure we track them during changes below...
Expand Down
4 changes: 2 additions & 2 deletions demo/angular/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { GridstackItemComponent } from './gridstack-item.component';
import { GridstackComponent, gsCreateNgComponents, gsSaveAdditionNgInfo } from './gridstack.component';
import { GridstackComponent, gsCreateNgComponents, gsSaveAdditionalNgInfo } from './gridstack.component';
import { AngularNgForTestComponent } from './ngFor';
import { AngularNgForCmdTestComponent } from './ngFor_cmd';
import { AngularSimpleComponent } from './simple';
Expand Down Expand Up @@ -38,6 +38,6 @@ export class AppModule {
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;
GridStack.saveCB = gsSaveAdditionalNgInfo;
}
}
60 changes: 32 additions & 28 deletions demo/angular/src/app/gridstack.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export interface NgGridStackWidget extends GridStackWidget {
export interface NgGridStackNode extends GridStackNode {
type?: string; // component type to create as content
}
export interface NgGridStackOptions extends GridStackOptions {
children?: NgGridStackWidget[];
subGridOpts?: NgGridStackOptions;
}

/** store element to Ng Class pointer back */
export interface GridCompHTMLElement extends GridHTMLElement {
Expand Down Expand Up @@ -194,43 +198,43 @@ 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 {
if (add) {
if (!host) return;
// create the grid item 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 {
// TODO: figure out how to creat ng component inside regular Div. need to access app injectors...
// const hostElement: Element = host;
// const environmentInjector: EnvironmentInjector;
// grid = createComponent(GridstackComponent, {environmentInjector, hostElement})?.instance;
}
if (grid) grid.options = w as GridStackOptions;
return grid?.el;
// 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 {
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 type = (w as NgGridStackWidget).type;
if (!w.subGridOpts && type && GridstackComponent.selectorToType[type]) {
gridItem?.container?.createComponent(GridstackComponent.selectorToType[type]);
}

return gridItem?.el;
// 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 (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);
}

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
*/
export function gsSaveAdditionNgInfo(n: NgGridStackNode, w: NgGridStackWidget) {
export function gsSaveAdditionalNgInfo(n: NgGridStackNode, w: NgGridStackWidget) {
if (n.type) w.type = n.type;
else if (n.content) w.content = n.content;
}