Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Closed
wants to merge 2 commits into from
Closed
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
125 changes: 98 additions & 27 deletions angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ I.E. don't use Angular templating to create grid items as that is harder to sync
MyComponent HTML

```html

Copy link
Member

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.

<gridstack [options]="gridOptions"></gridstack>
```

Expand All @@ -22,13 +23,13 @@ MyComponent CSS
.grid-stack {
background: #fafad2;
}

.grid-stack-item-content {
text-align: center;
background-color: #18bc9c;
}
```


Standalone MyComponent Code

```ts
Expand All @@ -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'},
]
}

Expand All @@ -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
Expand All @@ -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>
Expand All @@ -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
}
Expand All @@ -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]);
}
```
Expand All @@ -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>
```

Expand All @@ -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>();
Copy link
Member

@adumesny adumesny May 4, 2025

Choose a reason for hiding this comment

The 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)
Expand All @@ -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>;
Expand Down
26 changes: 13 additions & 13 deletions angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Member

@adumesny adumesny May 4, 2025

Choose a reason for hiding this comment

The 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"
}
}
2 changes: 2 additions & 0 deletions angular/projects/demo/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<button (click)="onShow(7)" [class.active]="show===7">Two Grids + sidebar</button>
<button (click)="onShow(8)" [class.active]="show===8">Lazy Load</button>
<button (click)="onShow(9)" [class.active]="show===9">Leak Test</button>
<button (click)="onShow(10)" [class.active]="show===10">Signal Parameters</button>
</div>

<div class="test-container">
Expand Down Expand Up @@ -118,4 +119,5 @@
<textarea #textArea cols="50" rows="50" readonly="readonly"></textarea>
</div>

<angular-signal-test *ngIf="show===10"></angular-signal-test>
</div>
2 changes: 2 additions & 0 deletions angular/projects/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AngularNgForTestComponent } from './ngFor';
import { AngularNgForCmdTestComponent } from './ngFor_cmd';
import { AngularSimpleComponent } from './simple';
import { AComponent, BComponent, CComponent, NComponent } from './dummy.component';
import { AngularSignalComponent } from './signals';

// TEST local testing
// import { GridstackModule } from './gridstack.module';
Expand All @@ -21,6 +22,7 @@ import { GridstackModule, GridstackComponent } from 'gridstack/dist/angular';
AngularNgForCmdTestComponent,
AngularNgForTestComponent,
AngularSimpleComponent,
AngularSignalComponent,
AppComponent,
AComponent,
BComponent,
Expand Down
88 changes: 88 additions & 0 deletions angular/projects/demo/src/app/signals.ts
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';
Copy link
Member

Choose a reason for hiding this comment

The 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';
Copy link
Member

Choose a reason for hiding this comment

The 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: {
Copy link
Member

Choose a reason for hiding this comment

The 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);
}
}
Loading