Angular custom directives not working with external javascript plugin - javascript

I am creating a custom angular directive that will use the Image Tilt Effect plugin.
<tilt></tilt>
the template looks like this.
<li class="grid__item">
<div class="grid__img grid__img--example-1" >
<img src="img/3.jpg" class="tilt-effect" alt="grid01" data-tilt-options='{ "opacity" : 0.3, "extraImgs" : 3, "movement": { "perspective" : 1200, "translateX" : -5, "translateY" : -5, "rotateX" : -5, "rotateY" : -5 } }' />
</div>
The problem I have is this script that I am loading at the bottom of the page doesnt seem to be on when the custom directive is injected into the page.
<script src="js/tiltfx.js"></script>
If I try and move the script into the html fragment for the custom directive I get an Error: $compile:tplrt

I've created a wrapper directive for this the Image Tilt Effect plugin (fiddle).
When you have a DOM plugin you need to use in angular, don't use auto initialization, such as the data-tilt-options of this plugin, because they are hard to predict, and may cause weird behavior, memory leaks, etc... This plugin has a manual init method - new TiltFx(element, options), so we can use that.
Another problem is that this plugin must wait for angular to finish rendering, before it should be initialized. The simple fix is just using setTimeout or $timeout (if we need to update the digest cycle), to put the init at the end of the JS queue. We can also use mutation observers to do that, but that's out of this answer's scope.
One troubling aspect of using a DOM plugin in angular, is memory leaks. Plugins should have some sort of a cleaning mechanism. This plugin doesn't. So, you'll have to check for memory leaks, and if there are stray event handlers remove them, when the wrapped element is removed.
Directive code:
appModule.directive('tilt', function tilt() {
var ddo = {
template: '<div class="grid__img"><img class="tilt-effect" ng-src="{{imgSrc}}" alt="The image" />',
restrict: 'E',
replace: true,
scope: {
imgSrc: '#', // the image src
tiltOptions: '=?' // the tilt options object - optional
},
link: function (scope, $el) {
var img = $el[0].querySelector('img.tilt-effect'); // find the img element
var tilt;
setTimeout(function () { // wait 'till directive is rendered
tilt = new TiltFx(img, scope.tiltOptions); // apply tilt on image with options (if any)
});
$el.on('$destroy', function() {
// use tilt variable to perform cleanup on wrapper and img if needed
tilt = null;
});
}
};
return ddo;
});
Usage:
<div ng-app="tilt">
<tilt img-src="http://cdn.cutestpaw.com/wp-content/uploads/2013/12/Most-Famous-Felines-034.jpg" tilt-options='{ "opacity" : 0.3, "extraImgs" : 3, "movement": { "perspective" : 1500, "translateX" : -5, "translateY" : -5, "rotateX" : -5, "rotateY" : -5 } }'></tilt>
<tilt img-src="http://www.cats.org.uk/uploads/images/pages/photo_latest14.jpg"></tilt>
</div>
Don't forget that this plugin requires the container to have fixed width and height, for example:
.grid__img {
width: 400px;
height: 400px;
}

Change
<script src="js/tiltfx.js"></script>
to
<script ng-src="js/tiltfx.js"></script>

Related

Basic Grid Layout not Initializing or Running

I'm trying to use cytoscape.js to display a graph on my page and I'm having a hard time just getting a basic instance to display properly.
Code Breakdown:
I get the graph elements via an AJAX call, pass the elements into the cytoscape constructor, and display the instance in a Bootstrap modal.
Here's my JavaScript:
var cy;
$.ajax({
url : "getGraphElements",
data : {
str : variableToGetCorrectGraphData
},
success : function(data) {
var elementsJson = JSON.parse(data.elements);
console.log(elementsJson);
cy = cytoscape({
container : document.getElementById('cy'),
wheelSensitivity : 0.25,
elements : elementsJson,
style : [
{
selector: 'node',
style:
{
'background-color' : '#666',
label : 'data(id)'
}
},
{
selector: 'edge',
style:
{
'width' : 3,
'line-color' : '#737373',
'target-arrow-color' : '#737373',
'target-arrow-shape' : 'triangle',
'curve-style' : 'bezier'
}
}
],
layout : {
name: 'grid',
fit: true, // whether to fit the viewport to the graph
padding: 0, // padding used on fit
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
avoidOverlapPadding: 20, // extra spacing around nodes when avoidOverlap: true
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
condense: false, // uses all available space on false, uses minimal space on true
sort: function(a,b) { // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
return a.degree() - b.degree();
},
animate: false, // whether to transition the node positions
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
}
});
$('#cyModal').modal('show');
}
});
});
Here's my Bootstrap Modal :
<div class="modal fade bd-example-modal-lg" id="cyModal" tabindex="-1" role="dialog" aria-labelledby="cyModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span> </button>
<h4 class="modal-title" id="cyModalLabel">Graph View</h4>
</div>
<div class="modal-body">
<div id="cy" style="height : 750px"></div>
</div>
</div>
</div>
</div>
And here's a sample of the JSON returned by the AJAX (although I'm almost certain this is not incorrect because all the elements appear to be present in the constructed graph) :
{ "nodes" : [{ "data" : { "id" : "12293"} }...], "edges" : [{ "data" : { "id" : "24607-26336", "source" : "24607", "target" : "26336" } }...] }
My Problem is that all the nodes appear stacked in the top left corner when the graph finishes initializing. I believe this is because the instance is using the null layout. A quick peek in the inspector console shows that cy.layout.name = 'layout'.
I just can't get it to initialize with the grid layout like I want it to. I've tried taking out the layout in the constructor, and using cy.layout({name : 'grid',}).run();. I've tried using both, one after the other. I've even tried putting cy.layout({name : 'grid',}).run(); in a while loop until cy.layout.name == 'grid' - that just caused the page to freeze. I've tried changing basically every option in both the Cytoscape initializer and the Layout initializer - no dice. What I find weird is that when I execute cy.layout({name : 'grid',}).run(); in the inspector console, the layout sets up properly...
If anyone has any ideas I'd greatly appreciate it!
Ok, OP here. I think my problem is with Bootstrap.
I started by noticing that I call $('#cyModal').modal('show'); after I've finished initializing the Cytoscape instance. I realized that before I call $('#cyModal').modal('show'); the div container for my Cytoscape instance has no size and is invisible. So I tried calling $('#cyModal').modal('show'); before initializing Cytoscape, but that still didn't work. Furthermore, I noticed that the modal wasn't actually showing until after Cytoscape initialized.
Apparently, for this sort of thing, I need to wait for the Bootstrap modal to fire the 'shown' event before setting my Cytoscape layout to be sure that the div is visible and has a size. So I put my layout setter in a listener for shown.bs.modal:
$('#cyModal').on('shown.bs.modal', function (e) {
cy.layout({
name: 'grid',
fit: true, // whether to fit the viewport to the graph
padding: 0, // padding used on fit
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
avoidOverlapPadding: 20, // extra spacing around nodes when avoidOverlap: true
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
condense: false, // uses all available space on false, uses minimal space on true
sort: function(a,b) { // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
return a.degree() - b.degree();
},
animate: false, // whether to transition the node positions
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
}).run();
});
This works for me, but it feels a bit like a hack - why can't I just do it like I originally wanted to? If that's just the way the cookie crumbles with Cytoscape.js and Bootstrap - c'est la vie; but I'll take any other suggestions if anyone has a more 'natural feeling' solution.

AngularJS + Jquery Gantt - multiple charts

I am working on a web application using angularJs and I need to display gantt charts. I use the jquery-gantt plugin and everything works just fine when only one chart is displayed but if I want to display two or more, they seem to have the same source even if it is not the case.
<chronogramme source="$ctrl.firstSource"></chronogramme>
<chronogramme source="$ctrl.secondSource"></chronogramme>
($ctrl is my alias for my controller, defined with "controllerAs")
For instance, when I update "secondSource", the first chart is updated too and shows the same things as the second one.
Is it even possible to have multiple charts ?
Thanks
UPDATE :
The problem must be in here, as the selector for the jquery gantt affects every single chart, I will look into it (put this here if it can helps others)
"chronogramme" directive :
app.directive("chronogramme", function(){
return {
restrict: 'E',
replace: true,
scope: {
source: '=source',
minScale: '=?minScale',
scale: '=?scale',
onItemClick: '=onItemClick',
onTitleClick: '=onTitleClick'
},
template: '<div class="gantt"></div>',
link: function(scope, element, attrs){
if (!scope.minScale) { scope.minScale = 'months'; }
if (!scope.scale) { scope.scale = 'months'; }
scope.$watch('source', function(){
$(".gantt").gantt({
source: scope.source,
navigate: "scroll",
minScale: scope.minScale,
scale: scope.scale,
itemsPerPage: 20,
waitText: "Chargement...",
months: ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"],
dow: ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"],
scrollToToday: true,
onItemClick: function(data) {
scope.onItemClick(data);
},
onTitleClick: function(data) {
scope.onTitleClick(data);
}
});
});
}
};
});
The directive is using a class selector to invoke the plugin:
template: '<div class="gantt"></div>',
link: function(scope, element, attrs){
if (!scope.minScale) { scope.minScale = 'months'; }
if (!scope.scale) { scope.scale = 'months'; }
scope.$watch('source', function(){
̶$̶(̶"̶.̶g̶a̶n̶t̶t̶"̶)̶.̶g̶a̶n̶t̶t̶(̶{̶
element.gantt({
Instead of invoking the plugin on all of the elements with the class gantt, invoke it on the element specific to the directive.
Also the directive replace property has been deprecated. It could cause problems if the directive is used with ng-repeat.
Using Angular Gantt clearly solves the issue. You can check the code for gantt directive inside the library it's much complex and you don't need to invest time on creating one in your application using just jquery version of the library. So with using the gantt directive you can have multiple charts on same view. (As of course it's created with isolated scope). Here's example of two charts on same view with different data set: https://plnkr.co/edit/lyBH7ZnZ5PgThqe46YKk?p=preview

Rendering gridstack in Meteor

I am using gridstack.js (https://github.com/troolee/gridstack.js) in Meteor.
I have the grid
<template name="grid">
<div class="grid-stack" data-gs-width="12" data-gs-animate="yes">
{{#each tiles}}
{{> gridItem}}
{{/each}}
</div>
</template>
and I apply gridstack to my grid element with
Template.grid.onRendered(function() {
$('.grid-stack').gridstack();
});
It works as it should, but if I first go to another route and then back to the route with the grid, the gridstack features are not "active" any longer (but the console doesn't say anything wrong). If I refresh, the gridstack will again work as it should. So the problem occurs only when I come back to this page without refreshing the entire page.
I have tried changing onRendered to onCreated, and suddently the grid gets the exact same behaviour when I refresh, i.e. the gridstack doesn't work neither when refreshing the page nor when I first go to another page and then back to the page with the grid.
So I guess I should still use onRendered, but it seems Meteor doesn't render the same way when I 'browse' between pages. Should I probably deinitialize the library when I leave the page, so it can initialize correctly again when the template is rerendered?
Edit
I have tried
Template.grid.onRendered(function() {
var $el = $('.grid-stack');
// destroy if already applied
if ($el.data('gridstack')) {
$el.data('gridstack').destroy();
}
// apply gridstak to grid element
var grid = $el.gridstack();
});
and also
Template.grid.onDestroyed(function() {
$('.grid-stack').data('gridstack').destroy();
});
but neither works.
But it says
TypeError: $(...).data(...).destroy is not a function
I've faced the same problem while I was implementing Gridstack on AngularJS. The Page change breaks the Gridstack. Here is how I solved the issue in my controller:
.controller('HomeCtrl', ['$scope', 'analyticsService', '$modal', '$timeout', function($scope, analyticsService, $modal, $timeout) {
$scope.homeWidgets = [
{sizeX: 6, sizeY: 5, row: 0, col:0, type: 'ndl-action-items'},
{sizeX: 6, sizeY: 5, row: 0, col:7, type: 'ndl-stage-shift-graph'},
{sizeX: 6, sizeY: 5, row: 1, col:0, type: 'ndl-nodules-in-system'},
{sizeX: 6, sizeY: 5, row: 1, col:7, type: 'ndl-case-volume'}
];
$scope.loadGrid = function() {
if ($scope.homeGrid && $scope.homeGrid.destroy) {
$scope.homeGrid.destroy();
}
var options = {
cell_height: 70,
vertical_margin: 10
};
// console.log("init grid");
$scope.homeGrid = $('.grid-stack').gridstack(options);
// console.log($scope.homeGrid);
};
$scope.$on('$viewContentLoaded', function(event) {
$timeout($scope.loadGrid, 300);
// event.preventDefault();
/* Act on the event */
// console.log('loaded');
// $scope.loadGrid();
});
}]);
So basically when my view reloads, I destroy the grid object if it exists and then recreate it. I had to use timeout so that the DOM gets loaded before the grid is initialized.

how to use angular-gridster and highcharts-ng directives together in angularjs

I am using angularjs-gridster (https://github.com/ManifestWebDesign/angular-gridster) with higharts-ng directive (https://github.com/pablojim/highcharts-ng/blob/master/README.md)
I am trying to generate these highcharts inside the grid cells. My problem is that the highcharts are occupying their default width and height (600px * 400px) even when i place my graph drawer function in a $timeout service. Here's the code:
HTML:
<div class="graph-list" gridster="gridsterOpts">
<ul>
<li gridster-item="graph.grid" class="graph-set" ng-repeat="graph in graphs | orderBy: 'number'">
<highchart id="{{'graph' + graph.number}}" class="graph" config="graph.config"></highchart>
</li>
</ul>
</div>
JS:
// inside the graph-list div controller
$scope.gridsterOpts = {
colums: 4,
rowHeight: 240,
margins: [10,10],
outerMargin: false,
draggable: {
enabled: false // whether dragging items is supported
}
};
$scope.graphs = {}; //
$scope.somefunction(){ /* this function populates the graphs object */ };
function drawGraphs(){ /* this function populates the graph.config object by looping through all the graph objects */ }
$timeout(function(){
drawGraphs();
});
I have tried creating watch on the grid-cell width and height but it shows no change. I have not given the highchart width and height explicitly in the graph.config options because I read in the highcharts-ng documentation that it takes the parent width and height by default but its not happening. Can anyone guide me what could be the possible problem.
Seems to me that the angularjs-gridster plugin is not able to set the grid width and height before the highcharts directive is able to render itself. Please help.
I eventually did it. I needed to add the chart.reflow() method (which just resizes the chart instead of redrawing it so better performance wise also, I guess) in the func() options as provided in the highcharts-ng documentation.
graph.config = {
options: { },
series: [],
func: function (chart) {
$timeout(function(){
chart.reflow();
})
}
}
Hope it helps someone else.

How can I trigger resize event in AngularJS

I'm using Angular & Bootstrap, with the nav tab control to switch visibility of divs. In one div, I have a large img (CAD drawing of building). I also then overlay markers on the image. I want to scale the x/y position of the markers based on the image width & image naturalWidth. I'm using a resize directive to detect changes and update my scope.
My problem is that if user switches tabs and switches back to the div with the CAD img, the refresh doesn't happen until I resize the browser (or surprisingly if I press the CMD key on Mac).
Is there an angular way to trigger the resize event to force my markers to be recalculated. Or is there an event I can tap into that is fired when the is fully displayed ?
Or is there a more refined angular approach I should take?
This is the HTML, the resize directive I've written is on the tag.
<div id="imageDiv" style="position: relative;" ng-show="selectedLocation" >
<img ng-src="../maps/image/{{selectedLocation}}"
style=" max-width: 100%; max-height: auto; border:solid 1px black" resize imageonload />
</div>
And this is the resize directive (adapted from http://jsfiddle.net/jaredwilli/SfJ8c/)
directive('resize', function($window) {
return function(scope, element) {
var w = angular.element($window);
scope.imgCadImage = element;
scope.getWindowDimensions = function() {
return {
'h' : scope.imgCadImage[0].width,
'w' : scope.imgCadImage[0].height
};
};
scope.$watch(scope.getWindowDimensions, function(newValue, oldValue) {
if (scope.imgCadImage[0].naturalWidth)
scope.imgScale = newValue.w / scope.imgCadImage[0].naturalWidth;
else
scope.imgScale = 1;
console.log("watched resize event - scale = "+scope.imgScale)
scope.updateCADMarkersAfterResize();
}, true);
w.bind('resize', function() {
console.log("'resize' - scale = "+scope.imgScale)
scope.$apply();
});
};
}).
This worked for me when the above did not.
$timeout(function() {
$window.dispatchEvent(new Event("resize"));
}, 100);
I had to also use $timeout with a delay in my case to prevent errors about digest cycles already being in progress. Not all uses cases may need this.
dispatchEvent is well supported http://caniuse.com/#feat=dispatchevent
However, it you need IE9 and IE10 support you'll have to use their propritary method: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/fireEvent
Try injecting $timeout into the resize directive:
directive('resize', function($window, $timeout) {
... then add the following line to the bottom of it:
$timeout(function(){ w.triggerHandler('resize') });
This should trigger the handler you have bound to $window.resize just after the browser renders.
The accepted answer did not work for me - w is undefined.
This did:
$timeout(function(){
$(window).trigger('resize');
});

Categories