Wrap gridstack.js into Angular 2 component

2020-02-09 03:55发布

问题:

I have some Angular 1 experience, but we have a need to use gridstack.js in an Angular 2 project.

We are familiar with the gridstack-angular project, but that project is in Angular 1. I think the biggest thing I am having trouble with is the Angular 2 concepts.

Any help would be appreciated.

回答1:

Tutorials

Okay for begginers the Angular 2 Quickstart is the best.

Then that continues and moves into the Tour of Heroes. Which is also a fantastic tutorial.

Tool

For the tutorials, and quite frankly building ANY Angular 2 app, I would highly recommend using Angular-Cli. It makes building Angular 2 apps a breeze

Just take a look at the Angular-Cli's Table of Contents to see what it can do


Example


my-grid-stack.component.html

<div class="grid-stack">
    <div class="grid-stack-item"
        data-gs-x="0" data-gs-y="0"
        data-gs-width="4" data-gs-height="2">
            <div class="grid-stack-item-content"></div>
    </div>
    <div class="grid-stack-item"
        data-gs-x="4" data-gs-y="0"
        data-gs-width="4" data-gs-height="4">
            <div class="grid-stack-item-content"></div>
    </div>
</div>

my-grid-stack.component.ts (How to get JQuery in Angular 2)

import { Component, OnInit } from '@angular/core';
declare var $: any; // JQuery

@Component({
  selector: 'app-my-gridstack',
  templateUrl: './app/my-grid-stack/my-grid-stack.component.html',
  styleUrls: ['./app/my-grid-stack/my-grid-stack.component.css']
})
export class MyGridStackComponent implements OnInit {

  constructor() { }

  ngOnInit() {
      var options = {
          cell_height: 80,
          vertical_margin: 10
      };
      $('.grid-stack').gridstack(options);
  }

}

Then I would put the gridstack.js file in the src/assets/libs/gridstack folder.

Then don't forget to import in your index.html

<script src="assets/libs/gridstack/gridstack.js"></script>


回答2:

We ended up creating two directives: GridStackDirective and GridStackItemDirective -

grid-stack-directive.ts:

import { Directive, OnInit, Input, ElementRef, Renderer } from '@angular/core';
declare var $: any; // JQuery

@Directive({
    selector: '[gridStack]'
})
export class GridStackDirective implements OnInit {
    @Input() w: number;
    @Input() animate: boolean;

    constructor(
        private el: ElementRef,
        private renderer: Renderer
    ) {
        renderer.setElementAttribute(el.nativeElement, "class", "grid-stack");
    }

    ngOnInit() {
        let renderer = this.renderer;
        let nativeElement = this.el.nativeElement;
        let animate: string = this.animate ? "yes" : "no";

        renderer.setElementAttribute(nativeElement, "data-gs-width", String(this.w));
        if(animate == "yes") {
            renderer.setElementAttribute(nativeElement, "data-gs-animate", animate);
        }

        let options = {
            cellHeight: 80,
            verticalMargin: 10
        };

        // TODO: listen to an event here instead of just waiting for the time to expire
        setTimeout(function () {
            $('.grid-stack').gridstack(options);
        }, 300);
    }

}

grid-stack-item-directive.ts:

import { Directive, ElementRef, Input, Renderer, OnInit } from '@angular/core';

@Directive({
    selector: '[gridStackItem]'
})

export class GridStackItemDirective {
  @Input() x: number;
  @Input() y: number;
  @Input() w: number;
  @Input() h: number;
  @Input() minWidth: number;
  @Input() canResize: boolean;

  constructor(
    private el: ElementRef,
    private renderer: Renderer
  ) { 
    renderer.setElementAttribute(el.nativeElement, "class", "grid-stack-item");
  }

  ngOnInit(): void {
    let renderer = this.renderer;
    let nativeElement = this.el.nativeElement;
    let cannotResize: string = this.canResize ? "yes" : "no";
    let elementText: string = '<div class="grid-stack-item-content">' + nativeElement.innerHTML + '</div>';
    // TODO: Find the Angular(tm) way to do this ...
    nativeElement.innerHTML = elementText;

    renderer.setElementAttribute(nativeElement, "data-gs-x", String(this.x));
    renderer.setElementAttribute(nativeElement, "data-gs-y", String(this.y));
    renderer.setElementAttribute(nativeElement, "data-gs-width", String(this.w));
    renderer.setElementAttribute(nativeElement, "data-gs-height", String(this.h));
    if(this.minWidth) {
      renderer.setElementAttribute(nativeElement, "data-gs-min-width", String(this.minWidth));
    }
    if(cannotResize == "yes") {
      renderer.setElementAttribute(nativeElement, "data-gs-no-resize", cannotResize);
    }
  }
}

app.component.html:

<h1>My First Grid Stack Angular 2 App</h1>
<section id="demo" class="darklue">
    <div class="container">
        <div class="row">
            <div class="col-lg-12 text-center">
                <h2>Demo</h2>
                <hr class="star-light">
            </div>
        </div>
        <div gridStack w="12" animate="true">
            <div gridStackItem x="0" y="0" w="4" h="2">1</div>
            <div gridStackItem x="4" y="0" w="4" h="4">2</div>
            <div gridStackItem x="8" y="0" w="2" h="2" canResize="false" minWidth="2">
                <span class="fa fa-hand-o-up"></span> Drag me!
            </div>
            <div gridStackItem x="10" y="0" w="2" h="2">4</div>
            <div gridStackItem x="0" y="2" w="2" h="2">5</div>
            <div gridStackItem x="2" y="2" w="2" h="4">6</div>
            <div gridStackItem x="8" y="2" w="4" h="2">7</div>
            <div gridStackItem x="0" y="4" w="2" h="2">8</div>
            <div gridStackItem x="4" y="4" w="4" h="2">9</div>
            <div gridStackItem x="8" y="4" w="2" h="2">10</div>
            <div gridStackItem x="10" y="4" w="2" h="2">11</div>
        </div>
    </div>
</section>

index.html:

<html>
  <head>
    <title>Angular QuickStart</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">
    <link rel="stylesheet" href="node_modules/font-awesome/css/font-awesome.min.css">

    <link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" type="text/css">
    <link href="https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic" rel="stylesheet" type="text/css">
    <link href="https://fonts.googleapis.com/css?family=Indie+Flower" rel='stylesheet' type='text/css'>

    <!-- 1. Load libraries -->
     <!-- Polyfill(s) for older browsers -->
    <script src="node_modules/core-js/client/shim.min.js"></script>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/reflect-metadata/Reflect.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>

    <!-- 2. Configure SystemJS -->
    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>

    <!-- jquery -->
    <script src="node_modules/jquery/dist/jquery.js"></script>
    <script src="node_modules/jquery-ui-dist/jquery-ui.min.js"></script>
    <script src="node_modules/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js"></script>
    <script src="node_modules/jquery-easing/dist/jquery.easing.1.3.umd.min.js"></script>

    <!-- underscore and gridstack --> 
    <script src="node_modules/underscore/underscore-min.js"></script>
    <script src="node_modules/gridstack/dist/gridstack.js"></script>
    <link rel="stylesheet" href="node_modules/gridstack/dist/gridstack.min.css">
    <link rel="stylesheet" href="node_modules/gridstack/dist/gridstack-extra.min.css">
    <link rel="stylesheet" href="app/css/gridstack-demo.css">

    <!-- bootstrap -->
    <script src="node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
    <link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.css">

    <!-- freelancer stuff -->
    <script src="app/js/freelancer.js"></script>
    <link rel="stylesheet" href="app/css/freelancer.css">

  </head>
  <!-- 3. Display the application -->
  <body>
    <my-app>Loading...</my-app>
  </body>
</html>

We tried to copy the demo grid on the gridstack.js web page. If you're going to run this and you want it to look like theirs, you'll need to grab some .css files, .js files, etc. from their site.



回答3:

Based on Users @Etchelon and @user3758236 answers

I have created this gridstack angular 4 library module for easy usage

  • https://github.com/ramyothman/ng4-gridstack
  • https://www.npmjs.com/package/ng4-gridstack

It has a simple usage and I added an example there for dynamically generating widgets

<grid-stack class="grid-stack" [options]="options">
  <grid-stack-item [option]="widget1" class="grid-stack-item"  >
  </grid-stack-item>
  <grid-stack-item [option]="widget2" class="grid-stack-item" >
  </grid-stack-item>
</grid-stack>

Cheers :)



回答4:

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>