I got Masonry to work in my AngularJS app using the angular-masonry directive but I want to be able to call a function or method in my controller that will trigger a reload of items in the container. I see in the source code (lines 101-104) there is a reload method but I'm not sure how to invoke that. Any ideas?
Thanks!
In case it's of use to someone in the future, Passy watches for an event called masonry.reload.
So, you can issue this event and Passy should call 'layout' on the masonry element, e.g. call
$rootScope.$broadcast('masonry.reload');
In my case, I had some third-party javascript decorating my bricks, so I needed to repaint after that was done. For reason (I was not able to figure out why), I needed to wrap the event broadcast in a timeout, I think the Passy scheduler was eating up the event and not repainting. E.g. I did:
$timeout(function () {
$rootScope.$broadcast('masonry.reload');
}, 5000);
This way you don't have to modify Passy directly.
I don't think you can trigger the reload() method directly, since it is part of the controller which is only visible within the directives.
You might either ...
A) trigger a reload directly of masonry via the element, jQuery-style
(see http://jsfiddle.net/riemersebastian/rUS9n/10/):
$scope.doReloadRelatively = function(e) {
var $parent = $(e.currentTarget).parent().parent();
$parent.masonry();
}
$scope.doReloadViaID = function() {
$('#container').masonry();
}
B) or extend the directives yourself, i.e. add necessary watches on masonry-brick and call the reload() method within (depends on what use-case you have).
.directive('masonryBrick', function masonryBrickDirective() {
return {
restrict: 'AC',
require: '^masonry',
scope: true,
link: {
pre: function preLink(scope, element, attrs, ctrl) {
var id = scope.$id, index;
ctrl.appendBrick(element, id);
element.on('$destroy', function () {
console.log("masonryBrick > destroy")
ctrl.removeBrick(id, element);
});
scope.$watch(function () {
return element.height();
},
function (newValue, oldValue) {
if (newValue != oldValue) {
console.log("ctrl.scheduleMasonryOnce('layout');");
ctrl.scheduleMasonryOnce('layout');
}
}
);
...
In the code block above, I've simply added a watch on the element with the class 'masonry-brick' in order to trigger a reload of masonry whenever the elements' height changes.
I've created a jsFiddle for testing passys' angular-masonry myself, feel free to check it out!
http://jsfiddle.net/riemersebastian/rUS9n/10/
EDIT:
Just found a similar post on stackoverflow which could be another solution to this problem:
AngularJS Masonry for Dynamically changing heights
The answer from #SebastianRiemer helped me.
But for future persons that need help try instead use the following
scope.$watch(function () {
return element.height();
},
function (newValue, oldValue) {
if (newValue != oldValue) {
ctrl.scheduleMasonryOnce('reloadItems');
ctrl.scheduleMasonryOnce('layout');
}
});
Related
So I am working on a application that has many many sections that are loaded via jquery ajax, some when it starts and some in response to other sections, and some are even nested. What I wan't to be able to do is say "Oh, the content in mainContainer changed?" Well, go compile the code in that section.
The reason is that I would like to be able to put ui-sref's into parts of the code we don't otherwise want to mess with yet. I came up with a solution but it isn't working the way I thought, I added a $watch to a custom directive but it fires a bazillion times. Here is the code.
Hopefully some one can explain how to get it to run once only when the content in X <div> is changed via one of the many ajax calls that happen, that way I don't need to immediately turn all of the old code into angular and can link to new code that is using angular.
app.directive('ngMainContainer', function ($compile, $timeout) {
console.log("main container directive");
return function (scope, elem, attrs) {
scope.$watch(function () {
return elem;
}, function (val) {
console.log("Something changed. " + val);
$timeout(function () {
$compile(val)(scope);
}, 0);
});
};
});
again, I thought putting a watch on the element passed into the directive was the answer however this runs even when I move the mouse and click on the page somewhere outside of the container that was loaded via jquery ajax.
Here is a solution that may work for you. I place directive ngMainContainer on our element we want to observe the html contents of.
In our controller I append <p>{{ content }}</p> which I first $compile, then update the scope var afterwards like $scope.html = angular.element(myDiv).html(). This is where the our directive $watch fires.
Here is the full working example. The <button> is appending html, but this should emulate the resolution of an ajax call request complete.
JSFiddle Link
app.directive('ngMainContainer', [function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
scope.$watch('html', function(n, o) {
if(n !== o) {
console.log(elem.contents())
// html has changed, do something!
}
});
}
}
}]);
app.controller('ctrl', ['$scope', '$compile', function($scope, $compile) {
$scope.content = "scope content";
var myDiv = document.getElementById('main');
$scope.appendContent = function() { // Replicate Ajax Callback
var dynamic = $compile('<p ng-cloak>{{ content }}</p>')($scope); // Compile content
angular.element(myDiv).append(dynamic);
$scope.html = angular.element(myDiv).html()
}
}]);
Edit
Here is a way to do this while appending content to the <div> with no notion of angular on the code that is performing the appending
Updated JSFiddle
<button id="noAngular" onclick="noAngularAppend()">Append Content - No Angular</button>
function noAngularAppend() {
angular.element(document.getElementById('main')).scope().externallyAppend('<p ng-cloak>{{ content }}</p>');
}
Directive Function (inject $compile)
scope.externallyAppend = function(element) {
var node = $compile(element)(scope);
elem.append(node);
scope.$apply(scope.html = elem.contents());
}
Sal, I up voted your answer because it ultimately did help me figure this out but in the end it wasn't quite what I was going for.
This is what I ended up with. In the controller for the main application there is function with only two lines in it.
$scope.doReshing = function (element) {
$compile(angular.element(element))($scope);
$scope.$apply();
}
Then in which ever partial that has an ng directive in it right at the top of the document.ready() I put the following.
var removeableContentContainer = $('#removableContentContainer');
angular.element(removeableContentContainer).scope().doReshing(removeableContentContainer);
This does exactly what I want. It lets me take an existing application change virtually nothing about how it works and lets me add ui-srefs in the "old" code and not have to worry about how or when it loads, when it does load, the document.ready() fires and it will tell angular that it needs to compile. This was my goal, you can pull bits of code in at any point in time and have them essentially act as though they were parts of that page with it's controller.
So i've looked around for someone who has tried to attempt what I am and have had no luck... So here we go. In response to the Kendo Lab's post *Disclaimer*: While Angular Kendo UI is not supported under the formal Kendo UI support agreement, this is an active community project, and we would LOVE for you to open an issue, or fork the repo and submit a pull request. StackOverflow is also a great place to get assistance. I have no choice but to come to StackOverflow. Here's my situation.
I have angular kendo setup in my web app and it runs GREAT! (Bit of a learning curve but that's how things go). I want to know test that directives i've written work properly and would LOVE to be able to test the with the kendo directives i'm looking for.
Now for the code
resize-directive.js
app.directive("ibsResizeGraphDirective", ['$window', function ($window) {
return function (scope, element) {
//Bind to window resize so that we can have the elements respond.
//There is no element.resize event and watching for width change doesn't work
angular.element($window).bind('resize', function () {
scope.$apply();
});
//Watch for element.width() change
scope.$watch(function () {
return $(element).width();
}, function (newValue, oldValue) {
if (newValue != oldValue) {
scope.graph.options.chartArea.width = $(element).width();
// Turn off transitions so that the graphs don't redraw everytime the window changes
if (oldValue != 0 && scope.graph.options.transitions) {
scope.graph.options.transitions = false;
}
scope.graph.refresh();
}
})
//...
};
}]);
as you can see i'm trying to basically check the size of the chart's element and set the chartArea.width accordingly.
The biggest problem i'm having is getting the chart to even show up. To help make things easier on our end we decided to wrap up our chart declaration into a directive!
chart.js
app.directive('ibsChart', [ "ibsMainService", function (ibsMainService) {
return {
// Restrict E for element
restrict: 'E',
// Here we setup the template for the code we want to replace our directive
template:"<div ibs-resize-graph-directive \n\
ibs-owner-warehouse-listener-directive \n\
ibs-graph-culture-caption \n\
kendo-chart='graph' \n\
k-options='chartOptions' \n\
k-data-source='dataSource' \n\
class='col-lg-4'/>",
replace: true,
scope: {
//...
},
controller: function($scope){
//...
},
link: function (scope, element, attrs) {
//...
};
}]);
and finally my tests...which I can't even get my chart to render properly...so how am I supposed to even check that the width changed?!
resize-test.js
//Setup items before each test
beforeEach(module('dynamanMain', 'kendo.directives'));
//Initialization Tests
it('should render the chart', angular.mock.inject(function ($compile, $rootScope, $timeout) {
var scope = $rootScope.$new();
ele = $compile('<ibs-chart-directive></ibs-chart-directive>')(scope);
scope.$apply();
//$timeout.flush();
//Test that the kendo grid was created over the div element
console.log(ele.find('div')); //This basically outputs nothing
}));
And here is a screenie of the result
There is not chart rendered (which I know it's because i'm not appending it to the body)
There is no element in the body after my scripts (which when doing the angular-kendo tests they appear)
I get the prototype returned from my element.find('div')
This is definitely a longer post but I wanted to be as thorough as I could to get a good answer. Anyone have any thoughts?
I'm a bonehead. I was not targeting the directive correctly.
ele = $compile('<ibs-chart-directive></ibs-chart-directive>')(scope);
should be
ele = $compile('<ibs-chart></ibs-chart>')(scope);
because my directive is defined as
app.directive('ibsChart', [ "ibsMainService", function (ibsMainService) {
as a result of making these changes I was also able to complete the test and test the width of the chart and whether or not the resize function was being called....I LOVE finding small things like that after searching for days.....
I'm porting a jQuery webapp to AngularJS (<- beginner!).
To integrate bxSlider along with some templating stuff, I wrote following directive:
[Edit] better have a look at jsFiddle jsfiddle.net/Q5AcH/2/ [/Edit].
angular.module('myApp')
.directive('docListWrapper', ['$timeout', function ($timeout) {
return {
restrict: 'C',
templateUrl: 'partials/doc-list-wrapper.html',
scope: { docs: '=docs'},
link: function (scope, element, attrs) {
$timeout(function () {
element
.children('.doc-list')
.not('.ng-hide')
.bxSlider(); // <-- jQuery plugin doing heavy DOM manipulation
}, 100); // <-------------- timeout in millis
}
};
}]);
Without $timeout there is the problem that bxSlider cannot calculate sizes of the freshly created elements or doesn't find them at all.
I'm a bit concerned that using a long timeout-value might cause flickering while using a short value could cause problems on slow machines.
In my real application (of course with more data and more sections than in the jsFiddle) I observed something strange:
When I play around with the timeout value, using 10 or more milliseconds is enough so the jQuery plugin bxSlider finds a complete DOM. With less time waiting (9 millis or less), the plugin is not able to wrap the <ul> as it should.
But the problem of a very nasty flickering is still present.
In the fiddle, probably due to a smaller DOM, the flickering is not visible in Chrome + Firefox, only with Internet Explorer 10.
I don't want to rely on empiric values for $timeout which could be highly dependent on machine, os, rendering engine, angular version, blood preasure, ...
Is there a robust workaround?
I've found some examples with event listeners ($on, $emit) and with some magic done with ng-repeat $scope.$last. If I can remove flickering, I'd accept some coupling between components, even this does not fit nice with AngularJS' ambition.
Your problem is a racing condition problem, so you can't just remove the $timeout. Pretty much what happens is:
Angular compiles both elements;
Angular links you bx-slider element;
bx-slider looks for <li> elements (none at this time) and create the list;
Angular links the ng-repeat and build the <li> list and resolve the bindings.
So, to solve the first aspect of racing condition (build the component only after all <li> are ready), you should expose a update method at bxSlider directive and create a sub-directive that would call a update function in the bxSlider controller, using the $scope.$last trick:
.directive('bxSlider', function () {
var BX_SLIDER_OPTIONS = {
minSlides: 2,
maxSlides: 7,
slideWidth: 120
};
return {
restrict: 'A',
require: 'bxSlider',
priority: 0,
controller: function() {},
link: function (scope, element, attrs, ctrl) {
var slider;
ctrl.update = function() {
slider && slider.destroySlider();
slider = element.bxSlider(BX_SLIDER_OPTIONS);
};
}
}
}])
.directive('bxSliderItem', function($timeout) {
return {
require: '^bxSlider',
link: function(scope, elm, attr, bxSliderCtrl) {
if (scope.$last) {
bxSliderCtrl.update();
}
}
}
})
This solution would even give you the ability to add new itens to the model, for everytime you have a new $last item, the bxSlider would be built. But again, you would run into another racing condition. During step 3, the slider component duplicates the last element, in order to show it just before the first, to create a 'continuity' impression (take a look at the fiddle to understand what I mean). So now your flow is like:
Angular compiles both elements;
Angular links you bx-slider element;
Angular links the ng-repeat and build the <li> list;
Your code calls the parent update function, that invokes your component building process, that duplicates the last element;
Angular resolves the bindings.
So now, your problem is that the duplications made by the slider, carries only the templates of the elements, as Angular hadn't yet resolved it bindings. So, whenever you loop the list, you gonna see a broken content. To solve it, simply adding a $timeout of 1 millisecond is enough, because you gonna swap the order of steps 4 and 5, as Angular binding resolution happens in the same stack as the $digest cycle, so you should have no problem with it:
.directive('bxSliderItem', function($timeout) {
return {
require: '^bxSlider',
link: function(scope, elm, attr, bxSliderCtrl) {
if (scope.$last) {
$timeout(bxSliderCtrl.update, 1);
}
}
}
})
But you have a new problem with that, as the Slider duplicates the boundaries elements, these duplications are not overviewed by AngularJs digest cycle, so you lost the capability of model binding inside these components.
After all of this, what I suggest you is to use a already-adapted-angularjs-only slide solution.
So, summarizing:
you can use 1 millisecond delay in your solution, because Angular digest cycle is synchronous; - but you lost the capability to add new items to your list
you can use a $scope.$last trick with the $timeout as well - but you lost Angular bindings and if this components have any instance (selected, hover), you gonna have problem
you could use an already written solution (like the one I suggested).
you could write your own AngularJs native solution.
My answer seems round-about but might remove your need for $timeout. Try making another directive and attaching it to the li element. Something like the following pseudo code:
angular.module('myApp').directive('pdfClick', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
$element.bxSlider().delegate('a', 'click', pdfClicked);
}
}
});
<li class="doc-thumbnail" ng-repeat="doc in docs" pdfClick>
It should attach the click event to every list item's anchor generated by ng repeat.
Data hasn't yet arrived at scope at rendering time!
It turned out the problem was that the data has not been present at the time the directive was executed (linked).
In the fiddle, data was accessible in the scope very fast. In my application it took more time since it was loaded via $http. This is the reason why a $timeout of < 10ms was not enough in most cases.
So the solution in my case was, instead of
angular.module('myApp')
.directive('docListWrapper', ['$timeout', function ($timeout) {
return {
restrict: 'C',
templateUrl: 'partials/doc-list-wrapper.html',
scope: { docs: '=docs'},
link: function (scope, element, attrs) {
$timeout(function () { // <-------------------- $timeout
element
.children('.doc-list')
.not('.ng-hide')
.bxSlider();
}, 10);
}
};
}]);
I now have this:
angular.module('myApp')
.directive('docListWrapper', [function () {
return {
restrict: 'C',
templateUrl: 'partials/doc-list-wrapper.html',
scope: { docs: '=docs'},
link: function (scope, element, attrs) {
scope.$watch('docs', function () { // <---------- $watch
element
.children('.doc-list')
.not('.ng-hide')
.bxSlider();
});
}
};
}]);
Maybe there is a more elegant solution for this problem, but for now I'm happy that it works.
I hope this helps other AngularJS beginners.
In a nutshell:
I try to do something like this inside my directive - namely change the value of model that is liked to 'trigger' attribute:
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
and it does not work. Why? Should I do it other way around?
And here is full story:
I have a dialog that is triggered when showRemoveDialog flag is set. This flag is set when user clicks Remove button.
Here is a dialog's opening tag:
<div remove-dialog trigger="{{showRemoveDialog}}" class="modal fade" id="myModal">
Then I have a directive removeDialog:
myApp.directive("removeDialog", function () {
return {
restrict: 'A',
link: function (scope, element, attrs, controller) {
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
attrs.$observe('trigger', function (newValue) {
if (newValue) {
angular.element(element).modal('show');
} else {
angular.element(element).modal('hide');
}
});
},
controller: 'DeleteController'
};
});
As you can see, I observe trigger attribute and when it changes to true (user clicks Remove button), I show the dialog:
$scope.remove = function () {
$scope.showRemoveDialog = true;
};
And it works.
But if the value of trigger changes to false/null I want to close it - for instance Cancel button was clicked, or X icon was clicked. And if one of these two actions occur, I need to set back trigger value to false/null, so that the next time when user click on Remove button value would change from false -> true, and my dialog appears once again.
The problem is that this piece of code does not work:
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
I mean it does not set the value of {{showRemoveDialog}} in scope to null. I already tried $apply function, but still in wain.
I guess I'm doing something really wrong in angular. Please help.
Yes, the idea you have come up with is kind of confusing, changing the attribute will not actually change the scope variable, so to fix this you would have to change the scope variable, in this case you know what the variables name is so it would work, but for other elements you might not know what the variable is. To fix this specific issue you would have to do.
scope.showRemoveDialog = null;
scope.$apply();
This is not very dynamic though. Here is what I would do (not tested).
Pass the variable name in as a string
trigger="showRemoveDialog"
Then in your directive get some help from $parse
myApp.directive("removeDialog", function ( $parse ) { ....
The link function...
link: function (scope, element, attrs, controller) {
var variableName = attrs.trigger;
angular.element(element).on('hidden.bs.modal', function () {
$parse(variableName + ' = null')(scope);
scope.$apply(); // Might not be needed.
});
scope.$watch(variableName, function (newValue) {
if (newValue) {
angular.element(element).modal('show');
} else {
angular.element(element).modal('hide');
}
}, true); // true might not be needed.
},
Also you don't need to do angular.element(element) as the element passed to the link function should already be wrapped.
The first argument to the jQuery on() method is the event you're listening for. I've never seen it used with custom events before, only standard Javascript ones like "keydown". So my first question would be have you tested that the event hook is ever called? If not put a console.log("event called"); before you try to set your element's trigger attribute.
Another thing I would mention is that setting an attribute to null like that wont work. Have a look at the AngularJS source code . Instead I would set the attribute to false.
Lastly I would recommend just using the Angular UI Bootstrap library that includes a nice modal feature - or something else, I don't mind but reinventing the wheel here seems unnecessary.
Can I watch for changing attributes in directive?
I have something like this:
<online-wathers></online-watchers>
It shows online connected users to the video broadcast and requires a broadcast-id. Obviously without this attribute it won't work.
This video broadcast starts not on document ready, but when a publisher actually wants to start it. And when he starts - the backend gives me the broadcast id.
For now I just append it to the DOM with broadcast-id="xx" when api returns me broadcast-id.
$(".online-watchers-container")
.html($("<online-watchers broadcast-id='"+broadcast.id+"' />"));
$(".chat-container")
.html($("<chat broadcast-id='"+broadcast.id+"' />"));
$(".request-container")
.html($("<live-requests broadcast-id='"+broadcast.id+"'></live-requests>"));
// compile directives
angular.bootstrap(angular.element("online-watchers, chat, live-requests"), ['MyApp']);
But is there any internal way to watch adding or changing an attributes? So on page load I will already have <online-watchers></online-watchers> in DOM and when I get response with broadcastId i just add attribute to element in DOM and directive reacts on it.
Please, if you treat it like shit-code, I'll much appreciate if you show some examples of better solution.
Thanks.
Yes, you can
function MyDirective() {
return {
link: function (scope, iElement, iAttrs) {
scope.$watch(iAttrs.scopeProp, function (val) {
console.log("prop: " + val);
});
iAttrs.$observe("interpolated", function (val) {
console.log("interpolate: " + val);
});
}
}
}
.
<my-directive scope-prop="someId" interpolated="{{someId + 1}}"
.
function MyCtrl($scope) {
$scope.someId = 12;
}
For more examples, check out: How to get evaluated attributes inside a custom directive