What is a best "Angular Way" to implement the directive that will have a shared timer for all it instances?
For example I have a directive "myComponent" and on the page it appears many times.
Inside of the component, exists some text that blink with some interval.
Because of business requirements and performance considerations, I would like that there will be single "timeout" that will toggle the blink for all instances at once (after document is ready).
I thought about the writing some code within directive definition:
//Pseudo code
angular.module("app",[]).directive("myComponent", function($timeout){
$(function() { $timeout(function(){ $(".blink").toggle(); }, 3000); } );
return {
//Directive definition
};
});
Or by using some kind of service that will receive the $element and add remove class to it:
//Pseudo code
angular.module("app",[])
.service("myService", function($timeout){
var elements = [];
this.addForBlink = function(element) { elements.push(element) };
$(function() { $timeout(function(){ $(elements).toggle(); }, 3000); } );
})
.directive("myComponent", function(myService){
return {
compile:function($element){
myService.addForBlink($element);
return function() {
//link function
}
}
};
});
In my opinion the most elegant and efficient would be to combine both these approaches by specifying the logic of the directive in the very directive initialization function. Here is a scaffold of what I actually mean:
app.directive('blinking', function($timeout){
var blinkingElements = [];
var showAll = function() {
for(var i = 0; i < blinkingElements.length; i++){
blinkingElements[i].addClass("blinking");
}
};
var hideAll = function() {
for(var i = 0; i < blinkingElements.length; i++){
blinkingElements[i].removeClass("blinking");
}
};
var blink = function () {
$timeout(showAll, 500);
$timeout(function(){
hideAll();
if (blinkingElements.length > 0) {
blink();
}
}, 1000);
};
return {
link : function(scope, element, attrs){
blinkingElements.push(element);
if (blinkingElements.length == 1) {
blink();
}
element.on("$destroy", function(){
var index = blinkingElements.indexOf(element);
blinkingElements.splice(index, 1);
});
}
}
});
And here is the working demo.
Moreover you can inject some service that will be responsible for configuration (setting the intervals and / or class) or you can provide the configuration by passing an object directly to the attribute. In the latter case you can enable applying different classes for different elements, but you should think of some policy how to deal with situation, when the interval was set more than once.
Related
I would like to build a directive that adds and remove a second directive based on a boolean flag.
So, when boolFlag = true, the div in example.html should recompile to include my-dir as an attribute. And it should have additional attributes required by my-dir in the form of "test=1".
Here is the basics of what I have so far:
example.html ----
<div add-remove when={{boolFlag}} dir-name="my-dir" dir-attrs="test, 1"></div>
add-remove-directive.js ----
function($compile, $timeout) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
var attrArray = attrs.dirAttrs.split(',');
for (a in attrArray) {
attrArray[a] = attrArray[a].trim();
}
attrs.$observe('when', function() {
// flagged when the directive should be attached to the element
var isWhen = attrs.when === 'true';
if (isWhen) {
for (a = 0; a < attrArray.length; a+=2) {
elem.attr(attrArray[a], attrArray[a+1]);
}
newScope = scope.$new();
cElem = $compile(elem)(newScope);
elem.replaceWith(cElem);
$timeout(function() {
scope.$destroy();
});
}
// if the flag is not set, remove the dynamic directive from the element
// but only if the element already has the given directive
else {
normDirName = attrs.$normalize(attrs.dirName);
console.log(normDirName);
console.log(r, 'elsed', attrs);
if (!attrs.hasOwnProperty(normDirName)) return;
elem.removeAttr(scope.dirName);
for (a = 0; a < attrArray.length; a+=2) {
elem.removeAttr(attrArray[a]);
}
newScope = scope.$new();
cElem = $compile(elem)(newScope);
elem.replaceWith(cElem);
$timeout(function() {
scope.$destroy();
});
}
}
}
---EDIT---
Sorry, that was an error with me copying it over from my project. It is functional up to how I described in the original post.
Basically where it's at now is I can dynamically "my-dir" directive correctly with the corresponding required attributes "test=1".
But the problems I'm running into are:
that there are two scopes active the original one without "my-dir" and another scope WITH "my-dir".
my else statement doesn't properly remove "my-dir" from the div when boolFlag is set to false.
I'm battling with setTimeout(), unable to even grasp what the problem could be. I first thought that it was a scope problem but can't fix it.
I'd like to delay the hiding/unhiding of a DIV to let my CSS transition on opacity kick in but when I click the alert_button to fade then hide the alert, it only fades and I'm left with an invisible div. Delayed $scope.alert_token doesn't switch to 'true' and the opacity of my alert stuck on 1.
app.js :
angular.module('myApp', [])
.controller('myCtrl', function($scope) {
$scope.alert_token = true // hide if true
$scope.alert_message = ""
$scope.p_name = ""
$scope.isInArray = function(arr, item) {
// IF ITEM IS ALREADY IN ARRAY
if (arr.indexOf(item) > -1) {
$scope.alert_token = !$scope.alert_token
$scope.alert_message = "Entry already exists"
setTimeout(function() {
document.getElementById("alert").style.opacity = "1"
}, 305)
}
else ...
}
$scope.submit = function(listType) {
if (listType == "player") {
$scope.isInArray(p_list, $scope.p_name)
$scope.p_name = ""
}
else ...
}
$scope.closeAlert = function() {
document.getElementById("alert").style.opacity = "0"
setTimeout(function() {
$scope.alert_token = !$scope.alert_token
}, 305)
}
Anything happening outside angular's knowledge is not updated to the DOM. In you case its setTimeout. Instead use $timeout.
......
.controller('myCtrl', function($scope, $timeout) {...
^^^^^^^^
//Other code
....
$scope.closeAlert = function() {
document.getElementById("alert").style.opacity = "0"
$timeout(function() {//$timeout
$scope.alert_token = !$scope.alert_token
}, 305)
}
Also since you are using angularJS, to update CSS properties I would recommend you to use ngClass and ngStyle
I have an interesting situation.
I have a directive with isolate scope that generate list of numbers and the user can choose numbers like in lottery.
The problem i have is that i required minimum of 1 line, if the user pick only one line so when he click play i want to auto trigger the next directive in the ng-repeat to pick for him numbers, I made this plunker so you guys can understand better and help me.
http://plnkr.co/edit/vWGmSEpinf7wxRUnqyWq?p=preview
<div ng-repeat="line in [0,1,2,3]">
<div line line-config="lineConfig">
</div>
</div>
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.lineConfig = {
guessRange: 10
}
$scope.lines = [];
$scope.$on('lineAdded', function(event, line) {
$scope.lines.push(line);
});
$scope.play = function() {
/// here i want to check if $scope.lines.length
//is less then one if yes then auto trigger the next
//line directive to auto do quick pick and continue
}
})
.directive('line', function() {
return {
restrict: 'A',
templateUrl: 'line.html',
scope: {
lineConfig: '='
},
link: function($scope, elem, attr) {
var guessRange = $scope.lineConfig.guessRange;
$scope.cells = [];
$scope.line = {
nums: []
};
$scope.$watch('line', function(lotLine) {
var finaLine = {
line: $scope.line
}
if ($scope.line.nums.length > 4) {
$scope.$emit('lineAdded', finaLine);
}
}, true);
_(_.range(1, guessRange + 1)).forEach(function(num) {
$scope.cells.push({
num: num,
isSelected: false
});
});
$scope.userPickNum = function(cell) {
if (cell.isSelected) {
cell.isSelected = false;
_.pull($scope.lotLine.nums, cell.num);
} else {
cell.isSelected = true;
$scope.lotLine.nums.push(cell.num);
}
};
$scope.quickPick = function() {
$scope.clearLot();
$scope.line.nums = _.sample(_.range(1, guessRange + 1), 5);
_($scope.line.nums).forEach(function(num) {
num = _.find($scope.cells, {
num: num
});
num.isSelected = true;
});
}
$scope.clearLot = function() {
_($scope.cells).forEach(function(num) {
num.isSelected = false;
});
$scope.line.nums = [];
}
}
}
})
You could pass the $index (exists automatically in the ng-repeat scope) - variable into the directive and cause it to broadcast an event unique for ($index + 1) which is the $index for the next instance.
The event could be broadcasted from the $rootScope or a closer scope that's above the repeat.
Then you could capture the event in there.
Probably not the best way to do it.
I can try to elaborate if anything is unclear.
EDIT
So I played around alittle and came up with this:
http://plnkr.co/edit/ChRCyF7yQcN580umVfX1?p=preview
Rather
Rather than using events or services I went with using a directive controller to act as the parent over all the line directives inside it:
.directive('lineHandler', function () {
return {
controller: function () {
this.lines = [];
}
}
})
Then requiring 'lineHandler' controller inside the 'line' directive - the controller being a singleton (same instance injected into all the line directives) - you can then setup that controller to handle communication between your directives.
I commented most of my code in the updated plnkr and setup an example of what I think you requested when clicking in one list - affecting the one beneath.
I hope this helps and if anything is unclear I will try to elaborate.
I have divs that expand and contract when clicked on. The Masonry library has worked great for initializing the page. The problem I am experiencing is that with the absolute positioning in place from Masonry and the directive below, when divs expand they overlap with the divs below. I need to have the divs below the expanding div move down to deal with the expansion.
My sources are:
http://masonry.desandro.com/
and
https://github.com/passy/angular-masonry/blob/master/src/angular-masonry.js
/*!
* angular-masonry <%= pkg.version %>
* Pascal Hartig, weluse GmbH, http://weluse.de/
* License: MIT
*/
(function () {
'use strict';
angular.module('wu.masonry', [])
.controller('MasonryCtrl', function controller($scope, $element, $timeout) {
var bricks = {};
var schedule = [];
var destroyed = false;
var self = this;
var timeout = null;
this.preserveOrder = false;
this.loadImages = true;
this.scheduleMasonryOnce = function scheduleMasonryOnce() {
var args = arguments;
var found = schedule.filter(function filterFn(item) {
return item[0] === args[0];
}).length > 0;
if (!found) {
this.scheduleMasonry.apply(null, arguments);
}
};
// Make sure it's only executed once within a reasonable time-frame in
// case multiple elements are removed or added at once.
this.scheduleMasonry = function scheduleMasonry() {
if (timeout) {
$timeout.cancel(timeout);
}
schedule.push([].slice.call(arguments));
timeout = $timeout(function runMasonry() {
if (destroyed) {
return;
}
schedule.forEach(function scheduleForEach(args) {
$element.masonry.apply($element, args);
});
schedule = [];
}, 30);
};
function defaultLoaded($element) {
$element.addClass('loaded');
}
this.appendBrick = function appendBrick(element, id) {
if (destroyed) {
return;
}
function _append() {
if (Object.keys(bricks).length === 0) {
$element.masonry('resize');
}
if (bricks[id] === undefined) {
// Keep track of added elements.
bricks[id] = true;
defaultLoaded(element);
$element.masonry('appended', element, true);
}
}
function _layout() {
// I wanted to make this dynamic but ran into huuuge memory leaks
// that I couldn't fix. If you know how to dynamically add a
// callback so one could say <masonry loaded="callback($element)">
// please submit a pull request!
self.scheduleMasonryOnce('layout');
}
if (!self.loadImages){
_append();
_layout();
} else if (self.preserveOrder) {
_append();
element.imagesLoaded(_layout);
} else {
element.imagesLoaded(function imagesLoaded() {
_append();
_layout();
});
}
};
this.removeBrick = function removeBrick(id, element) {
if (destroyed) {
return;
}
delete bricks[id];
$element.masonry('remove', element);
this.scheduleMasonryOnce('layout');
};
this.destroy = function destroy() {
destroyed = true;
if ($element.data('masonry')) {
// Gently uninitialize if still present
$element.masonry('destroy');
}
$scope.$emit('masonry.destroyed');
bricks = [];
};
this.reload = function reload() {
$element.masonry();
$scope.$emit('masonry.reloaded');
};
}).directive('masonry', function masonryDirective() {
return {
restrict: 'AE',
controller: 'MasonryCtrl',
link: {
pre: function preLink(scope, element, attrs, ctrl) {
var attrOptions = scope.$eval(attrs.masonry || attrs.masonryOptions);
var options = angular.extend({
itemSelector: attrs.itemSelector || '.masonry-brick',
columnWidth: parseInt(attrs.columnWidth, 10) || attrs.columnWidth
}, attrOptions || {});
element.masonry(options);
var loadImages = scope.$eval(attrs.loadImages);
ctrl.loadImages = loadImages !== false;
var preserveOrder = scope.$eval(attrs.preserveOrder);
ctrl.preserveOrder = (preserveOrder !== false && attrs.preserveOrder !== undefined);
scope.$emit('masonry.created', element);
scope.$on('$destroy', ctrl.destroy);
}
}
};
}).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 () {
ctrl.removeBrick(id, element);
});
scope.$on('masonry.reload', function () {
ctrl.scheduleMasonryOnce('reloadItems');
ctrl.scheduleMasonryOnce('layout');
});
scope.$watch('$index', function () {
if (index !== undefined && index !== scope.$index) {
ctrl.scheduleMasonryOnce('reloadItems');
ctrl.scheduleMasonryOnce('layout');
}
index = scope.$index;
});
}
}
};
});
}());
Like with many non-Angular libraries, it appears the answer lies in wrapping the library in an Angular directive.
I haven't tried it out but it appears that is what this person did
You can use angular's $emit, $broadcast, and $on functionality.
Inside your masonry directive link function:
scope.$on('$resizeMasonry', ctrl.scheduleMasonryOnce('layout'));
Inside your masonryBrick directive link function or any other child element:
scope.$emit('$resizeMasonry');
Use $emit to send an event up the scope tree and $broadcast to send an event down the scope tree.
I have a responsive template that I am trying to use with my Angularjs app. This is also my first Angular app so I know I have many mistakes and re-factoring in my future.
I have read enough about angular that I know DOM manipulations are suppose to go inside a directive.
I have a javascript object responsible for template re-sizes the side menu and basically the outer shell of the template. I moved all of this code into a directive and named it responsive-theme.
First I added all the methods that are being used and then I defined the App object at the bottom. I removed the function bodies to shorten the code.
Basically the object at the bottom is a helper object to use with all the methods.
var directive = angular.module('bac.directive-manager');
directive.directive('responsiveTheme', function() {
return {
restrict: "A",
link: function($scope, element, attrs) {
// IE mode
var isRTL = false;
var isIE8 = false;
var isIE9 = false;
var isIE10 = false;
var sidebarWidth = 225;
var sidebarCollapsedWidth = 35;
var responsiveHandlers = [];
// theme layout color set
var layoutColorCodes = {
};
// last popep popover
var lastPopedPopover;
var handleInit = function() {
};
var handleDesktopTabletContents = function () {
};
var handleSidebarState = function () {
};
var runResponsiveHandlers = function () {
};
var handleResponsive = function () {
};
var handleResponsiveOnInit = function () {
};
var handleResponsiveOnResize = function () {
};
var handleSidebarAndContentHeight = function () {
};
var handleSidebarMenu = function () {
};
var _calculateFixedSidebarViewportHeight = function () {
};
var handleFixedSidebar = function () {
};
var handleFixedSidebarHoverable = function () {
};
var handleSidebarToggler = function () {
};
var handleHorizontalMenu = function () {
};
var handleGoTop = function () {
};
var handlePortletTools = function () {
};
var handleUniform = function () {
};
var handleAccordions = function () {
};
var handleTabs = function () {
};
var handleScrollers = function () {
};
var handleTooltips = function () {
};
var handleDropdowns = function () {
};
var handleModal = function () {
};
var handlePopovers = function () {
};
var handleChoosenSelect = function () {
};
var handleFancybox = function () {
};
var handleTheme = function () {
};
var handleFixInputPlaceholderForIE = function () {
};
var handleFullScreenMode = function() {
};
$scope.App = {
//main function to initiate template pages
init: function () {
//IMPORTANT!!!: Do not modify the core handlers call order.
//core handlers
handleInit();
handleResponsiveOnResize(); // set and handle responsive
handleUniform();
handleScrollers(); // handles slim scrolling contents
handleResponsiveOnInit(); // handler responsive elements on page load
//layout handlers
handleFixedSidebar(); // handles fixed sidebar menu
handleFixedSidebarHoverable(); // handles fixed sidebar on hover effect
handleSidebarMenu(); // handles main menu
handleHorizontalMenu(); // handles horizontal menu
handleSidebarToggler(); // handles sidebar hide/show
handleFixInputPlaceholderForIE(); // fixes/enables html5 placeholder attribute for IE9, IE8
handleGoTop(); //handles scroll to top functionality in the footer
handleTheme(); // handles style customer tool
//ui component handlers
handlePortletTools(); // handles portlet action bar functionality(refresh, configure, toggle, remove)
handleDropdowns(); // handle dropdowns
handleTabs(); // handle tabs
handleTooltips(); // handle bootstrap tooltips
handlePopovers(); // handles bootstrap popovers
handleAccordions(); //handles accordions
handleChoosenSelect(); // handles bootstrap chosen dropdowns
handleModal();
$scope.App.addResponsiveHandler(handleChoosenSelect); // reinitiate chosen dropdown on main content resize. disable this line if you don't really use chosen dropdowns.
handleFullScreenMode(); // handles full screen
},
fixContentHeight: function () {
handleSidebarAndContentHeight();
},
setLastPopedPopover: function (el) {
lastPopedPopover = el;
},
addResponsiveHandler: function (func) {
responsiveHandlers.push(func);
},
// useful function to make equal height for contacts stand side by side
setEqualHeight: function (els) {
var tallestEl = 0;
els = jQuery(els);
els.each(function () {
var currentHeight = $(this).height();
if (currentHeight > tallestEl) {
tallestColumn = currentHeight;
}
});
els.height(tallestEl);
},
// wrapper function to scroll to an element
scrollTo: function (el, offeset) {
pos = el ? el.offset().top : 0;
jQuery('html,body').animate({
scrollTop: pos + (offeset ? offeset : 0)
}, 'slow');
},
scrollTop: function () {
App.scrollTo();
},
// wrapper function to block element(indicate loading)
blockUI: function (ele, centerY) {
var el = jQuery(ele);
el.block({
message: '<img src="./assets/img/ajax-loading.gif" align="">',
centerY: centerY !== undefined ? centerY : true,
css: {
top: '10%',
border: 'none',
padding: '2px',
backgroundColor: 'none'
},
overlayCSS: {
backgroundColor: '#000',
opacity: 0.05,
cursor: 'wait'
}
});
},
// wrapper function to un-block element(finish loading)
unblockUI: function (el) {
jQuery(el).unblock({
onUnblock: function () {
jQuery(el).removeAttr("style");
}
});
},
// initializes uniform elements
initUniform: function (els) {
if (els) {
jQuery(els).each(function () {
if ($(this).parents(".checker").size() === 0) {
$(this).show();
$(this).uniform();
}
});
} else {
handleUniform();
}
},
updateUniform : function(els) {
$.uniform.update(els);
},
// initializes choosen dropdowns
initChosenSelect: function (els) {
$(els).chosen({
allow_single_deselect: true
});
},
initFancybox: function () {
handleFancybox();
},
getActualVal: function (ele) {
var el = jQuery(ele);
if (el.val() === el.attr("placeholder")) {
return "";
}
return el.val();
},
getURLParameter: function (paramName) {
var searchString = window.location.search.substring(1),
i, val, params = searchString.split("&");
for (i = 0; i < params.length; i++) {
val = params[i].split("=");
if (val[0] == paramName) {
return unescape(val[1]);
}
}
return null;
},
// check for device touch support
isTouchDevice: function () {
try {
document.createEvent("TouchEvent");
return true;
} catch (e) {
return false;
}
},
isIE8: function () {
return isIE8;
},
isRTL: function () {
return isRTL;
},
getLayoutColorCode: function (name) {
if (layoutColorCodes[name]) {
return layoutColorCodes[name];
} else {
return '';
}
}
};
}
};
});
Originally the App.init() object method would be called at the bottom of any regular html page, and I have others that do certain things also that would be used on specific pages like Login.init() for the login page and so forth.
I did read that stackoverflow post
"Thinking in AngularJS" if I have a jQuery background? and realize that I am trying to go backwards in a sense, but I want to use this template that I have so I need to retro fit this solution.
I am trying to use this directive on my body tag.
<body ui-view="dashboard-shell" responsive-theme>
<div class="page-container">
<div class="page-sidebar nav-collapse collapse" ng-controller="SidemenuController">
<sidemenu></sidemenu>
</div>
<div class="page-content" ui-view="dashboard">
</div>
</div>
</body>
So here is my problem. This kinda sorta works. I don't get any console errors but when I try to use my side menu which the javascript for it is in the directive it doesn't work until I go inside the console and type App.init(). After that all of the template javascript works. I want to know how to do responsive theme stuff in these directives. I have tried using it both in the compile and link sections. I have tried putting the code in compile and link and calling the $scope.App.init() from a controller and also at the bottom after defining everything. I also tried putting this in jsfiddle but can't show a true example without having the console to call App.init().
My end design would be having some way to switch the pages through ui-router and when a route gets switched it calls the appropriate methods or re-runs the directive or something. The only method that will run on every page is the App.init() method and everything else is really page specific. And technically since this is a single page app the App.init() only needs to run once for the application. I have it tied to a parent template inside ui-router and the pages that will switch all use this shell template. There are some objects that need to access other to call their methods.
Im sorry in advance for maybe a confusing post. I am struggling right now trying to put together some of the ways that you do things from an angular perspective. I will continue to edit the post as I get responses to give further examples.
You said I have read enough about angular that I know DOM manipulations are suppose to go inside a directive but it sounds like you missed the point of a directive. A directive should handle DOM manipulation, yes, but not one directive for the entire page. Each element (or segment) of the page should have its own directive (assuming DOM manip needs to be done on that element) and then the $controller should handle the interactions between those elements and your data (or model).
You've created one gigantic directive and are trying to have it do way too much. Thankfully, you've kinda sorta designed your code in such a way that it shouldn't be too hard to break it up into several directives. Basically, each of your handle functions should be its own directive.
So you'd have something like:
.directive('sidebarMenu', function(){
return {
template: 'path/to/sidebar/partial.html',
link: function(scope, elem, attrs){
// insert the code for your 'handleSidebarMenu()' function here
}
};
})
.directive('horizontalMenu', function(){
return {
template: 'path/to/horizontal/partial.html',
link: function(scope, elem, attrs){
// insert the code for your 'handleHorizontalMenu()' function here
}
};
})
and then your view would look something like:
<body ui-view="dashboard-shell" responsive-theme>
<div class="page-container">
<div class="page-sidebar nav-collapse collapse">
<horizontal-menu></horizontal-menu>
<sidebar-menu></sidebar-menu>
</div>
<div class="page-content" ui-view="dashboard">
</div>
</div>
</body>
And then you don't need a SidebarmenuController because your controller functions shouldn't be handling DOM elements like the sidebar. The controller should just handling the data that you're going to display in your view, and then the view (or .html file) will handle the displaying and manipulation of that data by its use of the directives you've written.
Does that make sense? Just try breaking that huge directive up into many smaller directives that handle specific elements or specific tasks in the DOM.