Based on @user3758236's answer I developed a couple of components, instead of having just directives:
interfaces.ts:
export interface IGridConfiguration {
width: number;
height: number;
x: number;
y: number;
}
grid-stack.component.ts:
import { Component, HostBinding, OnInit, Input, OnChanges, AfterViewInit, AfterContentInit, ElementRef, Renderer, QueryList, ContentChildren } from '@angular/core';
import { GridStackItemComponent } from "./grid-stack-item.component";
import { IGridConfiguration } from "./interfaces";
declare var jQuery: any; // JQuery
declare var _: any;
@Component({
moduleId: module.id,
selector: 'grid-stack',
template: `<ng-content></ng-content>`,
styles: [":host { display: block; }"]
})
export class GridStackComponent implements OnInit, OnChanges, AfterContentInit {
@HostBinding("class") cssClass = "grid-stack";
@Input() width: number;
@Input() animate: boolean;
@Input() float: boolean;
@ContentChildren(GridStackItemComponent) items: QueryList<GridStackItemComponent>;
constructor(
private _el: ElementRef,
private _renderer: Renderer
) { }
private _jGrid: any = null;
private _grid: any = null;
ngOnInit() {
let nativeElement = this._el.nativeElement;
this._renderer.setElementAttribute(nativeElement, "data-gs-width", String(this.width));
let options = {
cellHeight: 100,
verticalMargin: 10,
animate: this.animate,
auto: false,
float: this.float
};
_.delay(() => {
const jGrid = jQuery(nativeElement).gridstack(options);
jGrid.on("change", (e: any, items: any) => {
console.log("GridStack change event! event: ", e, "items: ", items);
_.each(items, (item: any) => this.widgetChanged(item));
});
this._jGrid = jGrid;
this._grid = this._jGrid.data("gridstack");
}, 50);
}
ngOnChanges(): void { }
ngAfterContentInit(): void {
const makeWidget = (item: GridStackItemComponent) => {
const widget = this._grid.makeWidget(item.nativeElement);
item.jGridRef = this._grid;
item.jWidgetRef = widget;
};
// Initialize widgets
this.items.forEach(item => makeWidget(item));
// Also when they are rebound
this.items
.changes
.subscribe((items: QueryList<GridStackItemComponent>) => {
if (!this._grid) {
_.delay(() => this.items.notifyOnChanges(), 50);
return;
}
items.forEach(item => makeWidget(item));
});
}
private widgetChanged(change: IWidgetDragStoppedEvent): void {
var jWidget = change.el;
var gridStackItem = this.items.find(item => item.jWidgetRef !== null ? item.jWidgetRef[0] === jWidget[0] : false);
if (!gridStackItem)
return;
gridStackItem.update(change.x, change.y, change.width, change.height);
}
}
interface IWidgetDragStoppedEvent extends IGridConfiguration {
el: any[];
}
grid-stack-item.component.ts
import { Component, ComponentRef, ElementRef, Input, Output, HostBinding, Renderer } from "@angular/core";
import { EventEmitter, OnInit, OnChanges, OnDestroy, AfterViewInit, ViewChild, ViewContainerRef } from "@angular/core";
import { IGridConfiguration } from "./interfaces";
import { DynamicComponentService } from "./dynamic-component.service";
@Component({
moduleId: module.id,
selector: "grid-stack-item",
template: `
<div class="grid-stack-item-content">
<div #contentPlaceholder></div>
<ng-content *ngIf="!contentTemplate"></ng-content>
</div>`,
styleUrls: ["./grid-stack-item.component.css"]
})
export class GridStackItemComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit {
@HostBinding("class") cssClass = "grid-stack-item";
@ViewChild("contentPlaceholder", { read: ViewContainerRef }) contentPlaceholder: ViewContainerRef;
@Input() initialX: number;
@Input() initialY: number;
@Input() initialWidth: number;
@Input() initialHeight: number;
@Input() minWidth: number;
@Input() canResize: boolean;
@Input() contentTemplate: string;
contentComponentRef: ComponentRef<any> = null;
@Output() onGridConfigurationChanged = new EventEmitter<IGridConfiguration>();
private _currentX: number;
private _currentY: number;
private _currentWidth: number;
private _currentHeight: number;
jGridRef: any = null;
private _jWidgetRef: any = null;
get jWidgetRef(): any { return this._jWidgetRef; }
set jWidgetRef(val: any) {
if (!!this._jWidgetRef)
this._jWidgetRef.off("change");
this._jWidgetRef = val;
this._jWidgetRef.on("change", function () {
console.log("Change!!", arguments);
});
}
update(x: number, y: number, width: number, height: number): void {
if (x === this._currentX && y === this._currentY && width === this._currentWidth && height === this._currentHeight)
return;
this._currentX = x;
this._currentY = y;
this._currentWidth = width;
this._currentHeight = height;
this.onGridConfigurationChanged.emit({
x: x,
y: y,
width: width,
height: height
});
}
get nativeElement(): HTMLElement {
return this.el.nativeElement;
}
constructor(
private el: ElementRef,
private renderer: Renderer,
private componentService: DynamicComponentService
) { }
ngOnInit(): void {
let renderer = this.renderer;
let nativeElement = this.nativeElement;
let cannotResize: string = this.canResize ? "yes" : "no";
renderer.setElementAttribute(nativeElement, "data-gs-x", String(this.initialX));
renderer.setElementAttribute(nativeElement, "data-gs-y", String(this.initialY));
renderer.setElementAttribute(nativeElement, "data-gs-width", String(this.initialWidth));
renderer.setElementAttribute(nativeElement, "data-gs-height", String(this.initialHeight));
if (this.minWidth) {
renderer.setElementAttribute(nativeElement, "data-gs-min-width", String(this.minWidth));
}
if (cannotResize == "yes") {
renderer.setElementAttribute(nativeElement, "data-gs-no-resize", cannotResize);
}
}
ngOnChanges(): void {
// TODO: check that these properties are in the SimpleChanges
this._currentX = this.initialX;
this._currentY = this.initialY;
this._currentWidth = this.initialWidth;
this._currentHeight = this.initialHeight;
}
ngAfterViewInit(): void {
if (!!this.contentTemplate) {
this.componentService.getDynamicComponentFactory({
selector: `grid-stack-item-${Date.now()}`,
template: this.contentTemplate
})
.then(factory => {
this.contentComponentRef = this.contentPlaceholder.createComponent(factory);
})
}
}
ngOnDestroy(): void {
if (this.contentComponentRef !== null)
this.contentComponentRef.destroy();
}
}
The latter component uses a service for dynamic component creation, which u can find elsewhere on stackoverflow.
The usage is as follows:
<grid-stack width="12" animate="true" float="true">
<grid-stack-item *ngFor="let field of fields; let i = index;"
[class.selected]="field.id === selectedFieldId" (click)="fieldClicked(field.id)"
[initialX]="field.gridConfiguration.x" [initialY]="field.gridConfiguration.y"
[initialWidth]="field.gridConfiguration.width" [initialHeight]="field.gridConfiguration.height"
[contentTemplate]="getFieldTemplate(field)" (onGridConfigurationChanged)="fieldConfigurationChanged($event, field.id)">
</grid-stack-item>
</grid-stack>