-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
angular: update BaseWidget.deserialize to set values on model() signals #3025
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ I.E. don't use Angular templating to create grid items as that is harder to sync | |
MyComponent HTML | ||
|
||
```html | ||
|
||
<gridstack [options]="gridOptions"></gridstack> | ||
``` | ||
|
||
|
@@ -22,13 +23,13 @@ MyComponent CSS | |
.grid-stack { | ||
background: #fafad2; | ||
} | ||
|
||
.grid-stack-item-content { | ||
text-align: center; | ||
background-color: #18bc9c; | ||
} | ||
``` | ||
|
||
|
||
Standalone MyComponent Code | ||
|
||
```ts | ||
|
@@ -41,15 +42,15 @@ import { GridstackComponent, GridstackItemComponent } from 'gridstack/dist/angul | |
GridstackItemComponent | ||
] | ||
... | ||
}) | ||
}) | ||
export class MyComponent { | ||
// sample grid options + items to load... | ||
public gridOptions: GridStackOptions = { | ||
margin: 5, | ||
children: [ // or call load(children) or addWidget(children[0]) with same data | ||
{x:0, y:0, minW:2, content:'Item 1'}, | ||
{x:1, y:0, content:'Item 2'}, | ||
{x:0, y:1, content:'Item 3'}, | ||
{x: 0, y: 0, minW: 2, content: 'Item 1'}, | ||
{x: 1, y: 0, content: 'Item 2'}, | ||
{x: 0, y: 1, content: 'Item 3'}, | ||
] | ||
} | ||
|
||
|
@@ -64,9 +65,12 @@ import { GridstackModule } from 'gridstack/dist/angular'; | |
@NgModule({ | ||
imports: [GridstackModule, ...] | ||
... | ||
bootstrap: [AppComponent] | ||
bootstrap: | ||
[AppComponent] | ||
}) | ||
export class AppModule { } | ||
|
||
export class AppModule { | ||
} | ||
``` | ||
|
||
# More Complete example | ||
|
@@ -76,6 +80,7 @@ In this example (build on previous one) will use your actual custom angular comp | |
HTML | ||
|
||
```html | ||
|
||
<gridstack [options]="gridOptions" (changeCB)="onChange($event)"> | ||
<div empty-content>message when grid is empty</div> | ||
</gridstack> | ||
|
@@ -95,7 +100,8 @@ import { GridstackComponent, gsCreateNgComponents, NgGridStackWidget, nodesCB, B | |
}) | ||
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; } | ||
public override serialize(): NgCompInputs | undefined { return this.text ? {text: this.text} : undefined; } | ||
|
||
ngOnDestroy() { | ||
console.log('Comp A destroyed'); // test to make sure cleanup happens | ||
} | ||
|
@@ -109,25 +115,32 @@ export class BComponent extends BaseWidget { | |
} | ||
|
||
// ...in your module (classic), OR your ng19 app.config provideEnvironmentInitializer call this: | ||
constructor() { | ||
constructor() | ||
{ | ||
// register all our dynamic components types created by the grid | ||
GridstackComponent.addComponentToSelectorType([AComponent, BComponent]) ; | ||
GridstackComponent.addComponentToSelectorType([AComponent, BComponent]); | ||
} | ||
|
||
// now our content will use Components instead of dummy html content | ||
public gridOptions: NgGridStackOptions = { | ||
public | ||
gridOptions: NgGridStackOptions = { | ||
margin: 5, | ||
minRow: 1, // make space for empty message | ||
children: [ // or call load()/addWidget() with same data | ||
{x:0, y:0, minW:2, selector:'app-a'}, | ||
{x:1, y:0, minW:2, selector:'app-a', input: { text: 'bar' }}, // custom input that works using BaseWidget.deserialize() Object.assign(this, w.input) | ||
{x:2, y:0, selector:'app-b'}, | ||
{x:3, y:0, content:'plain html'}, | ||
{x: 0, y: 0, minW: 2, selector: 'app-a'}, | ||
{x: 1, y: 0, minW: 2, selector: 'app-a', input: {text: 'bar'}}, // custom input that works using BaseWidget.deserialize() Object.assign(this, w.input) | ||
{x: 2, y: 0, selector: 'app-b'}, | ||
{x: 3, y: 0, content: 'plain html'}, | ||
] | ||
} | ||
|
||
// called whenever items change size/position/etc.. see other events | ||
public onChange(data: nodesCB) { | ||
public | ||
onChange(data | ||
: | ||
nodesCB | ||
) | ||
{ | ||
console.log('change ', data.nodes.length > 1 ? data.nodes : data.nodes[0]); | ||
} | ||
``` | ||
|
@@ -139,13 +152,14 @@ For simple case where you control the children creation (gridstack doesn't do cr | |
HTML | ||
|
||
```html | ||
|
||
<gridstack [options]="gridOptions" (changeCB)="onChange($event)"> | ||
<!-- Angular 17+ --> | ||
@for (n of items; track n.id) { | ||
<gridstack-item [options]="n">Item {{n.id}}</gridstack-item> | ||
@for (n of items; track n.id) { | ||
<gridstack-item [options]="n">Item {{n.id}}</gridstack-item> | ||
} | ||
<!-- Angular 16 --> | ||
<gridstack-item *ngFor="let n of items; trackBy: identify" [options]="n"> Item {{n.id}} </gridstack-item> | ||
<gridstack-item *ngFor="let n of items; trackBy: identify" [options]="n"> Item {{n.id}}</gridstack-item> | ||
</gridstack> | ||
``` | ||
|
||
|
@@ -156,24 +170,79 @@ import { GridStackOptions, GridStackWidget } from 'gridstack'; | |
import { nodesCB } from 'gridstack/dist/angular'; | ||
|
||
/** sample grid options and items to load... */ | ||
public gridOptions: GridStackOptions = { margin: 5 } | ||
public items: GridStackWidget[] = [ | ||
{x:0, y:0, minW:2, id:'1'}, // must have unique id used for trackBy | ||
{x:1, y:0, id:'2'}, | ||
{x:0, y:1, id:'3'}, | ||
public | ||
gridOptions: GridStackOptions = {margin: 5} | ||
public | ||
items: GridStackWidget[] = [ | ||
{x: 0, y: 0, minW: 2, id: '1'}, // must have unique id used for trackBy | ||
{x: 1, y: 0, id: '2'}, | ||
{x: 0, y: 1, id: '3'}, | ||
]; | ||
|
||
// called whenever items change size/position/etc.. | ||
public onChange(data: nodesCB) { | ||
public | ||
onChange(data | ||
: | ||
nodesCB | ||
) | ||
{ | ||
console.log('change ', data.nodes.length > 1 ? data.nodes : data.nodes[0]); | ||
} | ||
|
||
// ngFor unique node id to have correct match between our items used and GS | ||
public identify(index: number, w: GridStackWidget) { | ||
public | ||
identify(index | ||
: | ||
number, w | ||
: | ||
GridStackWidget | ||
) | ||
{ | ||
return w.id; // or use index if no id is set and you only modify at the end... | ||
} | ||
``` | ||
|
||
# Supplying initial values when adding widgets (Angular+17) | ||
|
||
When you dynamically add a widget via `grid.addWidget()`, you pass its initial inputs | ||
in the `input` payload. Since `input<T>()` signals are read‑only, you should define | ||
corresponding `model<T>()` fields in your component to receive and update those values. | ||
|
||
```typescript | ||
this.gridComp()?.grid?.addWidget({ | ||
autoPosition: true, | ||
w: 2, | ||
h: 4, | ||
selector: 'angular-signal-based', | ||
input: { | ||
title: 'Item #' + id + ' (signal)', | ||
x: 'Item #' + id + ' (signal)', | ||
id | ||
}, | ||
id: String(this.ids), | ||
} as NgGridStackWidget); | ||
``` | ||
Here, each key in input must match a `model<T>()`declaration in your Component, ensuring | ||
the widget starts with the correct state. | ||
|
||
```typescript | ||
// Component expecting input as signals | ||
export class SignalBasedComponent extends BaseWidget { | ||
title: ModelSignal<string | undefined> = model<string>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see the benefit to using readonly signal and have to write all that woraround code, when @input() works just fine with latest Angular and you can still have set/get if you want ot be notified of changes... |
||
description: ModelSignal<string | undefined> = model<string>(); | ||
id: ModelSignal<number> = model<number>(0); | ||
|
||
computedValue: Signal<"#FFF" | "#000"> = computed(() => { | ||
return this.id() % 2 ? '#FFF' : '#000'; | ||
}) | ||
} | ||
``` | ||
In old Angular version you can still using `@Input()` | ||
|
||
```typescript | ||
@Input() title: string; | ||
``` | ||
|
||
## Demo | ||
|
||
You can see a fuller example at [app.component.ts](projects/demo/src/app/app.component.ts) | ||
|
@@ -190,8 +259,10 @@ Code started shipping with v8.1.2+ in `dist/angular` for people to use directly | |
|
||
NOTE: if you are on Angular 13 or below: copy the wrapper code over (or patch it - see main page example) and change `createComponent()` calls to use old API instead: | ||
NOTE2: now that we're using standalone, you will also need to remove `standalone: true` and `imports` on each component so you will to copy those locally (or use <11.1.2 version) | ||
|
||
```ts | ||
protected resolver: ComponentFactoryResolver, | ||
protected | ||
resolver: ComponentFactoryResolver, | ||
... | ||
const factory = this.resolver.resolveComponentFactory(GridItemComponent); | ||
const gridItemRef = grid.container.createComponent(factory) as ComponentRef<GridItemComponent>; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,31 +10,31 @@ | |
}, | ||
"private": true, | ||
"dependencies": { | ||
"@angular/animations": "^14", | ||
"@angular/common": "^14", | ||
"@angular/compiler": "^14", | ||
"@angular/core": "^14", | ||
"@angular/forms": "^14", | ||
"@angular/platform-browser": "^14", | ||
"@angular/platform-browser-dynamic": "^14", | ||
"@angular/router": "^14", | ||
"@angular/animations": "^17", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no. I intentionally DO NOT use latest rev so Ang14-18+ can use the published bits. please revert. |
||
"@angular/common": "^17", | ||
"@angular/compiler": "^17", | ||
"@angular/core": "^17", | ||
"@angular/forms": "^17", | ||
"@angular/platform-browser": "^17", | ||
"@angular/platform-browser-dynamic": "^17", | ||
"@angular/router": "^17", | ||
"gridstack": "^12.1.1", | ||
"rxjs": "~7.5.0", | ||
"tslib": "^2.3.0", | ||
"zone.js": "~0.11.4" | ||
}, | ||
"devDependencies": { | ||
"@angular-devkit/build-angular": "^14", | ||
"@angular/cli": "^14", | ||
"@angular/compiler-cli": "^14", | ||
"@angular-devkit/build-angular": "^17", | ||
"@angular/cli": "^17", | ||
"@angular/compiler-cli": "^17", | ||
"@types/jasmine": "~4.0.0", | ||
"jasmine-core": "~4.3.0", | ||
"karma": "~6.4.0", | ||
"karma-chrome-launcher": "~3.1.0", | ||
"karma-coverage": "~2.2.0", | ||
"karma-jasmine": "~5.1.0", | ||
"karma-jasmine-html-reporter": "~2.0.0", | ||
"ng-packagr": "^14", | ||
"typescript": "~4.7.2" | ||
"ng-packagr": "^17", | ||
"typescript": "~5.2.0" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/** | ||
* Simplest Angular Example using GridStack API directly | ||
*/ | ||
import { Component, computed, model, ModelSignal, Signal, viewChild } from '@angular/core'; | ||
|
||
import { GridStack } from 'gridstack'; | ||
import { GridstackComponent, NgGridStackOptions, NgGridStackWidget } from 'gridstack/dist/angular'; | ||
import { BaseWidget } from '../../../lib/src'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. likely wrong import |
||
|
||
@Component({ | ||
selector: 'angular-signal-based', | ||
standalone: true, | ||
template: ` | ||
<div style="color:{{ computedValue() }};"> | ||
<h3>Title:{{ title() }}</h3> | ||
<p>Description {{ description() }}</p> | ||
</div>`, | ||
}) | ||
export class SignalBasedComponent extends BaseWidget { | ||
title: ModelSignal<string | undefined> = model<string>(); | ||
description: ModelSignal<string | undefined> = model<string>(); | ||
id: ModelSignal<number> = model<number>(0); | ||
|
||
computedValue: Signal<"#FFF" | "#000"> = computed(() => { | ||
return this.id() % 2 ? '#FFF' : '#000'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make 2 shade of grey (B vs W won't render text) |
||
}) | ||
} | ||
|
||
@Component({ | ||
selector: 'angular-signal-test', | ||
template: ` | ||
<p><b>SIMPLEST</b>: angular example using GridStack API directly, so not really using any angular construct per say | ||
other than waiting for DOM rendering</p> | ||
<button (click)="add()">add item</button> | ||
<gridstack [options]="gridOptions"></gridstack> | ||
`, | ||
// gridstack.min.css and other custom styles should be included in global styles.scss | ||
}) | ||
export class AngularSignalComponent { | ||
private gridComp = viewChild(GridstackComponent); | ||
|
||
private ids = 0; | ||
private grid!: GridStack; | ||
|
||
gridOptions: NgGridStackOptions = { | ||
margin: 5, | ||
minRow: 1, | ||
float: true, | ||
column: 12, | ||
columnOpts: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not necessarry for signal demo. simplify. |
||
columnMax: 12, | ||
breakpoints: [ | ||
{ | ||
w: 800, | ||
c: 6, | ||
layout: 'none', | ||
}, | ||
{ | ||
w: 500, | ||
c: 3, | ||
layout: 'none', | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
constructor() { | ||
GridstackComponent.addComponentToSelectorType([ | ||
SignalBasedComponent, | ||
]); | ||
} | ||
|
||
public add() { | ||
const id = ++this.ids; | ||
this.gridComp()?.grid?.addWidget({ | ||
autoPosition: true, | ||
w: 2, | ||
h: 1, | ||
selector: 'angular-signal-based', | ||
input: { | ||
title: `Item #${id} (signal)`, | ||
description: `Description ${id*2}`, | ||
id | ||
}, | ||
id: String(this.ids), | ||
} as NgGridStackWidget); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revert all your formatting changes please.