In the last few days, I was in Rome by a customer, who is porting a vb6 application to Angular. During the code review, he asks me to add a dashboard with resizable widgets, that can be positioned on the page with the classical drag & drop approach.
As a first step we create a new project with the angular-cli, enter in the project folder, install the gridstack dependency, generate two new components named dashboard and widget, and finally open Visual Studio Code on the folder:
- ng new dashboard
- cd dashboard
- npm install -save jquery jqueryui lodash gridstack
- ng g component dashboard
- ng g component widget
- code .
If you prefer you can link jquery, jquery-ui, lodash, and gridstack from a CDN, according to your requirements, but if you have downloaded these libraries you have to add their paths in the angular-cli.json:
According to the documentation, we can create a simple dashboard by creating a container with a ‘grid-stack’ class, that will contain the widget container identified by ‘grid-stack-item’ class, and some custom data-* attribute for the position (data-gs-x and data-gs-y), the width (data-gs-width) and the height (data-gs-height). Our dashboard.component.html will be as follow:
The widget content is identified by the ‘grid-stack-item-content’ class, that we can use to stylize the aspect of the widget in the dashboard.component.css file:
Gridstack is a jQuery plugin, then we need to use jQuery in our code to init the plugin. The problem with jQuery is that you have to do assumption on the HTML structure to select the appropriate element of the DOM, in this case we need to add the following code row:
In Angular, the separation between the component logic (the .ts file) and the HTML structure (the .html file) is very important and, usually, we encapsulate all the calls that need to know the dom in an Angular Directive, especially because we need to have the DOM ready when executing the jQuery call. But, in this case, the component executes only this call, because all the work will be done by the plugin. This one may be the only case where we can do some assumptions on the HTML structure, and dirty the component logic with the jQuery call.
If we don’t like it, we can use the @ViewChild decorator on an ElementRef property and add a #gridStackContainer on the right div:
At this point, the question is: when? When is the DOM ready for the jQuery call from the component?. At this purpose, the Angular component has some hooks that are invoked in various phases of the component lifecycle. More information about this topic can be found in the official documentation (https://angular.io/guide/lifecycle-hooks). In our case, we need the AfterViewInit hook, that is called after Angular initialize the component view (and also his potential child views):
Obviously, Typescript doesn’t know the jQuery $ symbol, then we have to declare a const to pass the Typescript transpilation, as you can see after the import statement:
declare const $: any;
To end this first step, we have to change the app.component.html as follows:
Running our application with the ng serve -o command, we can see the result:
Ok, it’s time to abstract our components to make them reusable. We start moving the widget structure in the widget component, exposing the widget properties as component input properties. The widget.component.ts will be as follows:
The widget.component.html could be as follow:
But as the compiler tells us, we can’t bind unknown attribute to out property, and the data-* are too generic to be known by angular. We can solve the problem using the [attr.] binding:
Note the ng-content element: it permits us to project the content in our component:
The span Widget 1 element will be then projected in the widget component, exactly at ng-content position. This is a very cool feature and we can use it also in the dashboard.component.html:
So, our app.component.html will become as follows:
But, when we run the application, the result is not as we would expect:
Why? If we show the actual DOM structure, we can see the problem clearly:
Between the dashboard and the widget, Angular places an element with the name of the selector of the component and this create problems with several jQuery plugins, like gridstack, because it searches the direct child with the ‘grid-stack-item’ class and data-* attributes, but it founds the app-widget element instead of our div element. How can we solve the problem? Our widget cannot be necessarily a div, so we can move the grid-stack-item class and the data-* properties on the element using the host component property or, better, the @HostBinding() decorator:
Thanks to HostBinding decorator we can add the bound element on our component selector, and we will solve our problem:
Perfect, but what happens if the widgets come from a service? How does our implementation change? It seems a simple question because we have only to simulate a service call and cycling on its results. Ok, try to do this. If we add two files to the dashboard component, a dashboard.model.ts to create a type for the Widget response:
And a dashboard.service.ts to simulate the service call by creating and returning a Subject and by using a setTimeout to delay the data retrieving of one second:
If we inject the service in our dashboard.component.ts, we can call the service and store the result in a specified array as follow:
The dashboard.component.html changes, according to the new source of the widgets, as follows:
When we run the application, however, we realize that something doesn’t work and the console doesn’t show errors.
The problem is that when we call $(this.gridStackContainer.nativeElement).gridstack(); the dashboard doesn’t contain the widgets yet (we have delayed the widget retrieving of 1 second). And even if we move the call in the subscribe, we are not yet ready for the call because the rendering of the args association to the array requires some time. To solve the problem, we can use another component hook, named AfterViewChecked, that is called each time Angular checks the component’s views and child views:
Perfect! No? A suspect must always come in your mind when you work with jQuery plugins: what happens if the data change after the first time and your jQuery code is recalled? Typically it stops to work fine and it also happens in our casehellip; If we add a second setTimeout to our service as follows:
After 5 seconds, our application will show correctly the widgets, but they are not draggable and resizable.
If you have some experience with jQuery, you surely know that the solution is to recall the jQuery call, but first, we need to destroy the previous grid first, to make our code correct. According to the gridstack documentation, the code changes as follows:
Now it works well! You can find the code on my GitHub page, I created a branch for every step (step1, step2, and step3) and merged the step3 in the master at this address:
I hope this post could be useful for you, not only for the dashboard implementation, but also to integrate other jQuery plugins.
See you soon.