Gridster jQuery plugin on AngularJS ng-repeat content going bad - angularjs

I'm trying to integrate Gridster with AngularJS, but without too much success yet.
Reading the documentation on Angular UI's ui-jq directive, I get the impression that this (check fiddle) should work. But when I look a bit further with Chrome's debugger, it turns out that on this line, it doesn't find any children at all.
I suspect that somewhere in the ng-repeat directive, AngularJS decides to rip out the part that will be repeated, and I see why, but that doesn't solve my problem. I'd welcome any clue that would help me to get a little further.
Update 1
I started turning it into a directive, hoping that would improve things. However, the nested ng-repeat is also getting in the way in case of a homegrown directive. I tried postponing hooking up the jQuery plugin as long as I could ($evalAsync) and alike, and eventually ended up using a $timeout. That's the only way in which I could get it working.
Update 2
I think the original approach would have never given me what I needed. So implemented a custom directive. See my answer below.

I eventually ended up writing my own directives for it. I needed to be sure that every change to the underlying data would be seen by gridster, but at the same time, I didn't want to write my own monitoring on the data model and replace everything you normally do within gridster with a directive that hides all of that. (It would involve implementing most of ng-repeat within the directive itself.)
This is what I have (and assume "foo" to be the name of my module):
foo.directive "gridster", () -> {
restrict: "E"
template: '<div class="gridster"><div ng-transclude/></div>'
transclude: true
replace: true
controller: () ->
gr = null
return {
init: (elem) ->
ul = elem.find("ul")
gr = ul.gridster().data('gridster')
return
addItem: (elm) ->
gr.add_widget elm
return
removeItem: (elm) ->
gr.remove_widget elm
return
}
link: (scope, elem, attrs, controller) ->
controller.init elem
}
foo.directive "gridsterItem", () -> {
restrict: "A"
require: "^gridster"
link: (scope, elm, attrs, controller) ->
controller.addItem elm
elm.bind "$destroy", () ->
controller.removeItem elm
return
}
With this, I can have a gridster generated view, by adding this:
<gridster>
<ul>
<li ... ng-repeat="item in ..." gridster-item>
<!-- Have something here for displaying that item. -->
<!-- In my case, I'm switching here based on some item properties. -->
</li>
</ul>
</gridster>
Whenever items are added to or removed from the collection observed by the ng-repeat directive, they will be automatically added and removed from gridster controlled view.
EDIT
Here is a plunk demonstrating a slightly modified version of this directive:
angular.module('ngGridster', []);
angular.module('ngGridster').directive('gridster', [
function () {
return {
restrict: 'E',
template: '<div class="gridster"><div ng-transclude/></div>',
transclude: true,
replace: true,
controller: function () {
gr = null;
return {
init: function (elem, options) {
ul = $(elem);
gr = ul.gridster(angular.extend({
widget_selector: 'gridster-item'
}, options)).data('gridster');
},
addItem: function (elm) {
gr.add_widget(elm);
},
removeItem: function (elm) {
gr.remove_widget(elm);
}
};
},
link: function (scope, elem, attrs, controller) {
var options = scope.$eval(attrs.options);
controller.init(elem, options);
}
};
}
]);
angular.module('ngGridster').directive('gridsterItem', [
function () {
return {
restrict: 'EA',
require: '^gridster',
link: function (scope, elm, attrs, controller) {
controller.addItem(elm);
elm.bind('$destroy', function () {
controller.removeItem(elm);
});
}
};
}
]);

If you want you can try and roll your own wrapper for gridster. I spent most of the night last night and it was a little glitchy. The way gridster handles serialization isn't straightforward, etc. Anyway I stumbled on this project and it works really well.
https://github.com/ManifestWebDesign/angular-gridster
I couldn't find an online demo so I made a plunk of it:
http://plnkr.co/edit/r5cSY1USjtr2bSs7rvlC?p=preview

This will be fixed in the next release of
https://github.com/angular-ui/angular-ui/pull/347
The new deferred attribute will take care of the problem as soon as I can figure out why the stupid unit tests won't pass.

There's also Angular Gridster.
Came across it and this question while researching Gridster implementation in Angular.

Related

AngularJS $parser not being called when dynamically adding the directive

So what i'm trying to achieve is to be able to add a directive that contains a parser through another directive.
When directly adding the parser directive on an html element it works completely fine. the parser directive i currently use:
.directive('parseTest', [
function () {
return {
restrict: 'A',
require: ['ngModel'],
link: {
post: function (scope, element, attributes, ctrls) {
var controller = ctrls[0];
controller.$parsers.unshift(function (value) {
var result = value.toLowerCase();
controller.$setViewValue(value);
controller.$render();
return result;
})
}
}
}
}
])
Now when i add this directive through another directive the parser never gets called weirdly enough. The directive that generated the parsetest directive:
.directive('generateTest', ['$compile',
function ($compile) {
return {
restrict: 'A',
compile: function (elem, attrs) {
elem.attr('parse-test', '');
elem.removeAttr('generate-test');
var linkFn = $compile(elem);
return function (scope, element, attr) {
linkFn(scope);
}
}
}
}
])
The following works fine:
<input class="form-control col-sm-6" ng-model="model.parsetest" parse-test/>
The following doesn't work (While the generated result html is the same)
<input class="form-control col-sm-6" ng-model="model.generateTest" generate-test />
So my question is how can i get the parser working when it is in a dynamicly added directive?
Note, i already tried the solution to a similar issue from this question, but that doesn't work for me.
EDIT: Here is a plnkr that demonstrates the issue, both fields have the parse-test directive applied to it that should make the value in the model lowercase, but it only works for the one that is not dynamically added as shown in the console logs.
So I've found the solution, so for anyone that stumbles on the same issue here it is.
The 2 small changes have to made to the directive that generates the directive that contains a parser or formatter.
First of set the priority of the directive to a number higher or equal as 1. Secondly put terminal on true. Those 2 settings seem to resolve the issue.
The problem probably lies in that the default execution of nested directives makes it so that the parser and formatters get inserted slightly to late which is why we need to make sure the directive gets generated first thing before anything else.
This is just an assumption of why it works tho, if anyone else has an explanation it would be great :)
As for the code, the directive that generates another directive should look something like:
directive('generateTest', ['$compile',
function ($compile) {
return {
restrict: 'A',
terminal: true,
priority: 1,
compile: function (elem, attrs) {
attrs.$set('parseTest', '');
attrs.$set('generateTest', undefined);
var linkFn = $compile(elem);
return function (scope, element, attr) {
linkFn(scope);
}
}
}
}
])

Adding directives to an element using another directive

I am trying to create a directive that adds some html code but also adds additional attributes/directives.
Using the code below, an ng-class attribute is indeed added, but it seems angular does not recognize it as a directive anymore. It is there, but it has no effect.
How can I get it to work? Thanks.
The Angular code:
angular.module('myModule', [])
.directive('menuItem', function () {
return {
restrict: 'A',
template: '<div ng-if="!menuItem.isSimple" some-other-stuff>{{menuItem.name}}</div>'
+'<a ng-if="menuItem.isSimple" ng-href="{{menuItem.link}}">{{menuItem.name}}</a>',
scope: {
menuItem: '='
},
compile: function (element, attrs) {
element.attr('ng-class', '{active : menuItem.isActivated()}');
}
}
});
And the html:
<li menu-item="menuItem" ng-repeat="menuItem in getMenuItems()" />
EDIT:
The solution by #Abdul23 solves the problem, but another problem arises: when the template contains other directives (like ng-if) these are not executed. It seems the problem just moved.
Is it possible to also make the directives inside the template work?
Or perhaps insert the html using the compile function instead of the template parameter. Since I want a simple distinction based on some value menuItem.isSimple (and this value will not change), perhaps I can insert only the html specific to that case without using ng-if, but how?
You need to use $compile service to achieve this. See this answer.
For your case it should go like this.
angular.module('myModule', [])
.directive('menuItem', function ($compile) {
return {
restrict: 'A',
template: '<a ng-href="{{menuItem.link}}">{{menuItem.name}}</a>',
scope: {
menuItem: '='
},
compile: function (element, attrs) {
element.removeAttr('menu-item');
element.attr('ng-class', '{active : menuItem.isActivated()}');
var fn = $compile(element);
return function(scope){
fn(scope);
};
}
}
});

How can I include HTML into a page with AngularJS from an HTML file?

I have some code that I have taken over. It looks like this:
<div id="init"
data-admin-template
ng-show="ss.display.init"
data-template-url="init.html">
</div>
There's a directive adminTemplate:
app.directive('adminTemplate', ['stateService', function (stateService) {
return {
restrict: 'A',
templateUrl: function (elem, attrs) {
return "/Content/app/admin/templates/" + attrs.templateUrl;
},
link: function (scope, element, attrs) {
scope.stateService = stateService;
}
};
}]);
I am not very familiar with templates and it seems that this directive does not really do much. Is there a way I can do this without the directive?
As #Oli said in the comments, ng-include is definitely one solution. There are others as well - angular likes to offer you just enough options to leave you doubting yourself - but i'm wondering what you'd actually gain by changing it.
With ng-include you'd need to add a controller to to your html, to supply stateService (if you don't do it here, you'd have to add it to every different admin template). So you'd end up with:
<div id="init"
ng-include="/Content/app/admin/templates/init.html"
ng-show="ss.display.init"
ng-controller="AdminController">
</div>
So you end up with the same amount of attributes, you need the whole template path and it becomes less readable. Looking at what you have now, it's clear to see the intent.
You could also go one step further and give it the flexibility of being an element or attribute
<admin-template
id="init"
ng-show="ss.display.init"
data-template-url="init.html">
</admin-template>
app.directive('adminTemplate', ['stateService', function (stateService) {
return {
restrict: 'EA',
templateUrl: function (elem, attrs) {
return "/Content/app/admin/templates/" + attrs.templateUrl;
},
link: function (scope, element, attrs) {
scope.stateService = stateService;
}
};
}]);
It may appear to be doing very little, but my feeling is that the previous developer has refactored out repetition, to get to this little directive. I'd argue that as it is it removes boiler plate, allows easy re-usability and communicates intent well in your mark up.

Using Angular, how do I bind a click event to an element and on click, slide a sibling element down and up?

I'm working with Angular and I'm having trouble doing something that I normally use jQuery for.
I want to bind a click event to an element and on click, slide a sibling element down and up.
This is what the jQuery would look like:
$('element').click(function() {
$(this).siblings('element').slideToggle();
});
Using Angular I have added an ng-click attribute with a function in my markup:
<div ng-click="events.displaySibling()"></div>
And this is what my controller looks like:
app.controller('myController', ['$scope', function($scope) {
$scope.events = {};
$scope.events.displaySibling = function() {
console.log('clicked');
}
}]);
So far this is working as expected but I don't know how to accomplish the slide. Any help is very much appreciated.
Update
I have replaced what I had with a directive.
My markup now looks like this:
<div class="wrapper padding myevent"></div>
I have removed what I had in my controller and have created a new directive.
app.directive('myevent', function() {
return {
restrict: 'C',
link: function(scope, element, attrs) {
element.bind('click', function($event) {
element.parent().children('ul').slideToggle();
});
}
}
});
However, I still can't get the slide toggle to work. I don't believe slideToggle() is supported by Angular. Any suggestions?
I'm not sure exactly on the behaviour that you're talking about, but I would encourage you to think in a slightly different way. Less jQuery, more angular.
That is, have your html something like this:
<div ng-click="visible = !visible"></div>
<div ng-show="visible">I am the sibling!</div>
You can then use the build in ng-animate to make the sibling slide - yearofmoo has an excellent overview of how $animate works.
This example is simple enough that you can put the display logic in the html, but I would otherwise encourage you to as a rule to put it into the controller, like this:
<div ng-click="toggleSibling()"></div>
<div ng-show="visible"></div>
Controller:
app.controller('SiblingExample', function($scope){
$scope.visible = false;
$scope.toggleSibling = function(){
$scope.visible = !$scope.visible;
}
});
This kind of component is also a prime candidate for a directive, which would package it all up neatly.
app.directive('slideMySibling', [function(){
// Runs during compile
return {
// name: '',
// priority: 1,
// terminal: true,
// scope: {}, // {} = isolate, true = child, false/undefined = no change
// controller: function($scope, $element, $attrs, $transclude) {},
// require: 'ngModel', // Array = multiple requires, ? = optional, ^ = check parent elements
restrict: 'A', // E = Element, A = Attribute, C = Class, M = Comment
// template: '',
// templateUrl: '',
// replace: true,
// transclude: true,
// compile: function(tElement, tAttrs, function transclude(function(scope, cloneLinkingFn){ return function linking(scope, elm, attrs){}})),
link: function($scope, iElm, iAttrs, controller) {
iElm.bind("click", function(){
$(this).siblings('element').slideToggle();
})
}
};
}]);
Usage would be something like
<div slide-my-sibling><button>Some thing</button></div><div>Some sibling</div>
Note all the "code" above is just for the sake of example and hasn't been actually tested.
http://plnkr.co/edit/qd2CCXR3mjWavfZ1RHgA
Here's a sample Plnkr though as mentioned in the comments this isn't an ideal setup since it still has the javascript making assumptions about the structure of the view so ideally you would do this with a few directives where one requires the other or by using events (see $emit, $broadcast, $on).
You could also have the directive create the children "programmatically" via JavaScript and circumvent the issue of not knowing what context the directive is being used in. There are a lot of potential ways to solve these issues though none of the issues will stop it from functionally working they are worth noting for the sake of re-usability, stability, testing, etc.
As per this link : https://docs.angularjs.org/api/ng/function/angular.element
AngularJs element in your code snippet represents JQuery DOM object for related element. If you want to use JQuery functions, you should use JQuery library before angular loads. For more detail, please go through above link.
Best practice:
<div ng-if="view"></div>
$scope.view = true;
$scope.toggle = function(){
$scope.view = ($scope.view) ? false : true;
}

Communicating with sibling directives

Goal: Create behaviors using directives with communication between 2 sibling elements (each their own directive).
A behavior to use in example: The article content is hidden by default. When the title is clicked, I want the related article content to display.
The catch: The related article elements need to associate to each other without being nested in a single parent element or directive.
<div article="article1">this is my header</div>
<div id="article1" article-content>this is content for the header above</div>
<div article="article2">this is my header</div>
<div id="article2" article-content>this is content for the header above</div>
I know it would be easier to place the content inside the article directive, however this question is to find out how to solve a situation like this.
Can the content directive pass itself to the related article directive somehow?
This code isn't very useful as it is now, but it's a starting point. How would I accomplish this?
.directive('article', function(){
return {
restrict: "A",
controller: function($scope) {
$scope.contentElement = null;
this.setContentElement = function(element) {
$scope.contentElement = element;
}
},
link: function(scope, element) {
element.bind('click', function(){
// Show article-content directives that belong
// to this instance (article1) of the directive
}
}
}
}
.directive('articleContent', function(){
return {
require: "article",
link: function(scope, element, attrs, articleCtrl) {
// Maybe reference the article i belong to and assign element to it?
// I can't though because these are siblings.
}
}
}
None of the directive require options will allow you to require sibling directives (as far as I know). You can only:
require on the element, using require: "directiveName"
tell angular to search up the DOM tree using require: "^directiveName"
or require: "^?directiveName" if you don't necessarily need the parent controller
or require: "^\?directiveName" if you don't necessarily need the parent DOM wrapper
If you want sibling to sibling communication, you'll have to house them in some parent DOM element with a directive controller acting as an API for their communication. How this is implemented is largely dependent on the context at hand.
Here is a good example from Angular JS (O Reilly)
app.directive('accordion', function() {
return {
restrict: 'EA',
replace: true,
transclude: true,
template: '<div class="accordion" ng-transclude></div>',
controller: function() {
var expanders = [];
this.gotOpened = function(selectedExpander) {
angular.forEach(expanders, function(expander) {
if(selectedExpander != expander) {
expander.showMe = false;
}
});
};
this.addExpander = function(expander) {
expanders.push(expander);
}
}
}
});
app.directive('expander', function() {
return {
restrict: 'EA',
replace: true,
transclude: true,
require: '^?accordion',
scope: { title:'#' },
template: '<div class="expander">\n <div class="title" ng-click="toggle()">{{ title }}</div>\n <div class="body" ng-show="showMe" \n ng-animate="{ show: \'animated flipInX\' }"\n ng-transclude></div>\n</div>',
link: function(scope, element, attrs, accordionController) {
scope.showMe = false;
accordionController.addExpander(scope);
scope.toggle = function toggle() {
scope.showMe = !scope.showMe;
accordionController.gotOpened(scope);
}
}
}
})
Usage (jade templating):
accordion
expander(title="An expander") Woohoo! You can see mme
expander(title="Hidden") I was hidden!
expander(title="Stop Work") Seriously, I am going to stop working now.
Or you can create a service just for directive communication, one advantage of special service vs require is that your directives won't depend on their location in html structure.
The above solutions are great, and you should definitely consider using a parent scope to allow communication between your directives. However, if your implementation is fairly simple there's an easy method built into Angular that can communicate between two sibling scopes without using any parent: $emit, $broadcast, and $on.
Say for example you have a pretty simple app hierarchy with a navbar search box that taps into a complex service, and you need that service to broadcast the results out to various other directives on the page. One way to do that would be like this:
in the search service
$rootScope.$emit('mySearchResultsDone', {
someData: 'myData'
});
in some other directives/controllers
$rootScope.$on('mySearchResultsDone', function(event, data) {
vm.results = data;
});
There's a certain beauty to how simple that code is. However, it's important to keep in mind that emit/on/broadcast logic can get nasty very quickly if you have have a bunch of different places broadcasting and listening. A quick google search can turn up a lot of opinions about when it is and isn't an anti-pattern.
Some good insight on emit/broadcast/on in these posts:
http://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
http://nathanleclaire.com/blog/2014/04/19/5-angularjs-antipatterns-and-pitfalls/
If there is a list of articles and its content we can do it without any directive, using ng-repeat
<div ng-repeat="article in articles">
<div article="article1" ng-click='showContent=true'>{{article.header}}</div>
<div id="article1" article-content ng-show='showContent'>{{article.content}}</div>
</div>
So you need to define the article model in controller. We are making use of local scope created by ng-repeat.
Update: Based on your feedback, you need to link them together.You can try
<div article="article1" content='article1'>this is my header</div>
<div id="article1" article-content>this is content for the header above</div>
and in your directive
use
link: function(scope, element,attrs) {
element.bind('click', function(){
$('#'+attrs.content).show();
}
}
And the final method could be to use $rootScope.$broadcast and scope.$on methods to communicate between to controllers. But in this approach you need to track from where the message came and who is the intended recipient who needs to process it.
I had the exact same problem and I was able to solve it.
In order to get one directive to hide other sibling directives, I used a parent directive to act as the API. One child directive tells the parent it wasn't to be shown/hidden by passing a reference to its element, and the other child calls the parent toggle function.
http://plnkr.co/edit/ZCNEoh
app.directive("parentapi", function() {
return {
restrict: "E",
scope: {},
controller: function($scope) {
$scope.elements = [];
var on = true;
this.toggleElements = function() {
if(on) {
on = false;
_.each($scope.elements, function(el) {
$(el).hide();
});
} else {
on = true;
_.each($scope.elements, function(el) {
$(el).show();
});
}
}
this.addElement = function(el) {
$scope.elements.push(el);
}
}
}
});
app.directive("kidtoggle", function() {
return {
restrict: "A",
require: "^parentapi",
link: function(scope, element, attrs, ctrl) {
element.bind('click', function() {
ctrl.toggleElements();
});
}
}
});
app.directive("kidhide", function() {
return {
restrict: "A",
require: "^parentapi",
link: function(scope, element, attrs, ctrl) {
ctrl.addElement(element);
}
}
});
I had the same issue with a select all/ select item directive I was writing. My issue was the select all check box was in a table header row and the select item was in the table body. I got around it by implementing a pub/sub notification service so the directives could talk to each other. This way my directive did not care about how my htlm was structured. I really wanted to use the require property, but using a service worked just as well.

Resources