I want make sure all directives are ready. How to? Here is what I try, but not correct.
leafUi.factory('leafState', function($rootScope) {
var unreadyUi = [], readyUi = [];
return {
unready: function(ui) {
console.log(ui + ' unready');
unreadyUi.push(ui);
},
ready: function(ui) {
console.warn(ui + ' ready');
readyUi.push(ui);
if (readyUi.length === unreadyUi.length) {
$rootScope.$broadcast('leafUiReady', 'all leaf ui component is ready');
// unreadyUi = null;
// readyUi = null;
}
}
}
});
leafUi.directive('leafScroll', function($timeout, leafState, leafScroll) {
return {
restrict: 'E',
transclude: true,
link: function(scope, ele, attrs, ctrl, transclude) {
leafState.unready('leafScroll');
// more code .....
leafState.ready('leafScroll');
}
};
});
leafUi.directive('leafContent', function($timeout, leafState, leafScroll) {
return {
restrict: 'E',
transclude: true,
link: function(scope, ele, attrs, ctrl, transclude) {
leafState.unready('leafContent');
// more code .....
leafState.ready('leafContent');
}
};
});
// more directive.....
Here is an example log:
From the log, we can know that directive ready and unready can be separated by other directive and length of readyUi has many chance to equal to unreadyUi.
So how can I assure all directives are ready?
Perhaps add another condition to what you define as leafUiReady could help you. Since the directives are loaded almost synchronously you get the logic that your undreadyUi and readyUi are almost always the same length during compilation.
Perhaps add logic like this to your leafState factory where you also look at the DOM ready-function.
leafUi.factory('leafState', function($rootScope) {
var unreadyUi = [], readyUi = [];
var domReady = false;
angular.element(document).ready(function () {
domReady = true;
if (readyUi.length === unreadyUi.length) {
$rootScope.$broadcast('leafUiReady', 'all leaf ui component is ready');
// unreadyUi = null;
// readyUi = null;
}
});
return {
unready: function(ui) {
console.log(ui + ' unready');
unreadyUi.push(ui);
},
ready: function(ui) {
console.warn(ui + ' ready');
readyUi.push(ui);
if (domReady && readyUi.length === unreadyUi.length) {
$rootScope.$broadcast('leafUiReady', 'all leaf ui component is ready');
// unreadyUi = null;
// readyUi = null;
}
}
}
});
Perhaps change some other logic to make this code a bit cleaner.
Related
I have used the following directory in my template
i want to change the model value of drop-down to id for it i have used as bellow
<md-select flex class="md-select-form" ng-model="education.degree" placeholder="Degree" save-id id="education.degree_id" ">
<md-option ng-value="degree" ng-repeat="degree in ['High School', 'Associates Degree', 'Bachelor Degree', 'Masters Degree', 'M.B.A', 'M.D', 'Ph.D', 'other']">{{degree}}</md-option>
</md-select>
.directory code
.directive('saveId', function(Profile){
return {
require: 'ngModel',
scope: {
id: '=',
requiredParam:'#'
},
link: function(scope, element, attrs, ngModel) {
console.log("initial loading");
// view --> model (change to string)
ngModel.$parsers.push(function(viewValue){
var keepGoing = true;
Profile.getDegreeList().then(function(data) {
angular.forEach(data, function(ob){
if(keepGoing) {
if(ob.degree == viewValue){
scope.id = ob.degree_id;
keepGoing = false;
}
}
});
console.log("within promise"+scope.id); //it executes second
});
console.log(scope.id); //it executes first
return scope.id;
});
return scope.id;
}
};
})
Once if i try to return the value of ng-model in finally block it also not working
.directive('saveId', function(Profile){
return {
require: 'ngModel',
scope: {
id: '=',
requiredParam:'#'
},
link: function(scope, element, attrs, ngModel) {
console.log("initial loading");
// view --> model (change to string)
ngModel.$parsers.push(function(viewValue){
// var id = -1;
var keepGoing = true;
Profile.getDegreeList().then(function(data) {
angular.forEach(data, function(ob){
if(keepGoing) {
if(ob.degree == viewValue){
scope.id = ob.degree_id;
keepGoing = false;
}
}
});
}).finally(function(res){
console.log(scope.id);
return scope.id;
});
});
return scope.id;
}
};
})
i have used Profile.getDegreeList() service to assign the id to relevant drop-down element by using ngModel.$parsers.push(). The problem is in case of service usage before promise going to complete it returns the previous assigned id .
i want to prevent it and need to return the promise id.
how to solve this issue please help?
You can use finally method which will be executed after executing promise.
Profile.getDegreeList()
.success(function(data)
{
angular.forEach(data, function(ob)
{
if (keepGoing)
if (ob.degree == viewValue) {
scope.id = ob.degree_id;
keepGoing = false;
}
});
console.log("within promise"+scope.id);
})
.finally(function(res)
{
// your code
}
);
Here you go first make a call and then use parsers.
Profile.getDegreeList().then(function(data) {
angular.forEach(data, function(ob) {
if (keepGoing) {
if (ob.degree == viewValue) {
scope.id = ob.degree_id;
keepGoing = false;
}
}
});
ngModel.$parsers.push(function(viewValue) {
var keepGoing = true;
return scope.id ;
});
});
See other links too for more guidance.
I have implemented a simple directive to check username (it should match a regexp, and some more constraint; on blur it's capitalized).
This is my current implementation:
app.directive('checkUserName', function() {
return {
require: 'ngModel',
scope: {
value: '=ngModel'
},
link: function(scope, elm, attrs, model) {
var USERNAME_REGEXP = /^[^_.$\[\]#\/][^.$\[\]#\/]*$/;
model.$parsers.unshift(function(viewValue) {
var user;
var retval;
if (!viewValue) {
model.$setValidity('required', false);
retval = null;
}
if (USERNAME_REGEXP.test(viewValue)) {
if (!user) {
model.$setValidity('taken', true);
model.$setValidity('invalid', true);
retval = viewValue;
} else {
model.$setValidity('taken', false);
model.$setValidity('invalid', true);
}
} else {
model.$setValidity('taken', true);
model.$setValidity('invalid', viewValue === '');
retval = viewValue;
}
return retval;
});
model.$formatters.push(function(modelValue) {
if (modelValue) {
modelValue = capitalize(modelValue);
}
return modelValue;
});
}
};
});
app.directive('renderOn', function() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, elm, attrs, ctrl) {
var event = attrs.renderOn;
elm.bind(event, function() {
var viewValue = ctrl.$modelValue;
for (var i in ctrl.$formatters) {
viewValue = ctrl.$formatters[i](viewValue);
}
ctrl.$viewValue = viewValue;
ctrl.$render();
});
}
};
});
function capitalize(text) {
return text.charAt(0).toUpperCase() + text.slice(1);
}
So the question is:
How do I test it?
I did set up a fiddle with the karma test I did write, but the blur handling is obviously wrong...
A secondary question is: is this 'split' implementation (checkUserName + renderOn) advisable strategy when implementing both 'parsing' and 'formatting' directives?
How can i be notified when a directive is resized?
i have tried
element[0].onresize = function() {
console.log(element[0].offsetWidth + " " + element[0].offsetHeight);
}
but its not calling the function
(function() {
'use strict';
// Define the directive on the module.
// Inject the dependencies.
// Point to the directive definition function.
angular.module('app').directive('nvLayout', ['$window', '$compile', layoutDirective]);
function layoutDirective($window, $compile) {
// Usage:
//
// Creates:
//
var directive = {
link: link,
restrict: 'EA',
scope: {
layoutEntries: "=",
selected: "&onSelected"
},
template: "<div></div>",
controller: controller
};
return directive;
function link(scope, element, attrs) {
var elementCol = [];
var onSelectedHandler = scope.selected();
element.on("resize", function () {
console.log("resized.");
});
$(window).on("resize",scope.sizeNotifier);
scope.$on("$destroy", function () {
$(window).off("resize", $scope.sizeNotifier);
});
scope.sizeNotifier = function() {
alert("windows is being resized...");
};
scope.onselected = function(id) {
onSelectedHandler(id);
};
scope.$watch(function () {
return scope.layoutEntries.length;
},
function (value) {
//layout was changed
activateLayout(scope.layoutEntries);
});
function activateLayout(layoutEntries) {
for (var i = 0; i < layoutEntries.length; i++) {
if (elementCol[layoutEntries[i].id]) {
continue;
}
var div = "<nv-single-layout-entry id=slot" + layoutEntries[i].id + " on-selected='onselected' style=\"position:absolute;";
div = div + "top:" + layoutEntries[i].position.top + "%;";
div = div + "left:" + layoutEntries[i].position.left + "%;";
div = div + "height:" + layoutEntries[i].size.height + "%;";
div = div + "width:" + layoutEntries[i].size.width + "%;";
div = div + "\"></nv-single-layout-entry>";
var el = $compile(div)(scope);
element.append(el);
elementCol[layoutEntries[i].id] = 1;
}
};
}
function controller($scope, $element) {
}
}
})();
Use scope.$watch with a custom watch function:
scope.$watch(
function () {
return [element[0].offsetWidth, element[0].offsetHeight].join('x');
},
function (value) {
console.log('directive got resized:', value.split('x'));
}
)
You would typically want to watch the element's offsetWidth and offsetHeight properties. With more recent versions of AngularJS, you can use $scope.$watchGroup in your link function:
app.directive('myDirective', [function() {
function link($scope, element) {
var container = element[0];
$scope.$watchGroup([
function() { return container.offsetWidth; },
function() { return container.offsetHeight; }
], function(values) {
// Handle resize event ...
});
}
// Return directive definition ...
}]);
However, you may find that updates are quite slow when watching the element properties directly in this manner.
To make your directive more responsive, you could moderate the refresh rate by using $interval. Here's an example of a reusable service for watching element sizes at a configurable millisecond rate:
app.factory('sizeWatcher', ['$interval', function($interval) {
return function (element, rate) {
var self = this;
(self.update = function() { self.dimensions = [element.offsetWidth, element.offsetHeight]; })();
self.monitor = $interval(self.update, rate);
self.group = [function() { return self.dimensions[0]; }, function() { return self.dimensions[1]; }];
self.cancel = function() { $interval.cancel(self.monitor); };
};
}]);
A directive using such a service would look something like this:
app.directive('myDirective', ['sizeWatcher', function(sizeWatcher) {
function link($scope, element) {
var container = element[0],
watcher = new sizeWatcher(container, 200);
$scope.$watchGroup(watcher.group, function(values) {
// Handle resize event ...
});
$scope.$on('$destroy', watcher.cancel);
}
// Return directive definition ...
}]);
Note the call to watcher.cancel() in the $scope.$destroy event handler; this ensures that the $interval instance is destroyed when no longer required.
A JSFiddle example can be found here.
Here a sample code of what you need to do:
APP.directive('nvLayout', function ($window) {
return {
template: "<div></div>",
restrict: 'EA',
link: function postLink(scope, element, attrs) {
scope.onResizeFunction = function() {
scope.windowHeight = $window.innerHeight;
scope.windowWidth = $window.innerWidth;
console.log(scope.windowHeight+"-"+scope.windowWidth)
};
// Call to the function when the page is first loaded
scope.onResizeFunction();
angular.element($window).bind('resize', function() {
scope.onResizeFunction();
scope.$apply();
});
}
};
});
The only way you would be able to detect size/position changes on an element using $watch is if you constantly updated your scope using something like $interval or $timeout. While possible, it can become an expensive operation, and really slow your app down.
One way you could detect a change on an element is by calling
requestAnimationFrame.
var previousPosition = element[0].getBoundingClientRect();
onFrame();
function onFrame() {
var currentPosition = element[0].getBoundingClientRect();
if (!angular.equals(previousPosition, currentPosition)) {
resiszeNotifier();
}
previousPosition = currentPosition;
requestAnimationFrame(onFrame);
}
function resiszeNotifier() {
// Notify...
}
Here's a Plunk demonstrating this. As long as you're moving the box around, it will stay red.
http://plnkr.co/edit/qiMJaeipE9DgFsYd0sfr?p=preview
A slight variation on Eliel's answer worked for me. In the directive.js:
$scope.onResizeFunction = function() {
};
// Call to the function when the page is first loaded
$scope.onResizeFunction();
angular.element($(window)).bind('resize', function() {
$scope.onResizeFunction();
$scope.$apply();
});
I call
$(window).resize();
from within my app.js. The directive's d3 chart now resizes to fill the container.
Here is my take on this directive (using Webpack as bundler):
module.exports = (ngModule) ->
ngModule.directive 'onResize', ['Callback', (Callback) ->
restrict: 'A'
scope:
onResize: '#'
onResizeDebounce: '#'
link: (scope, element) ->
container = element[0]
eventName = scope.onResize || 'onResize'
delay = scope.onResizeDebounce || 1000
scope.$watchGroup [
-> container.offsetWidth ,
-> container.offsetHeight
], _.debounce (values) ->
Callback.event(eventName, values)
, delay
]
I have a search input field with a requery function bound to the ng-change.
<input ng-model="search" ng-change="updateSearch()">
However this fires too quickly on every character. So I end up doing something like this alot:
$scope.updateSearch = function(){
$timeout.cancel(searchDelay);
searchDelay = $timeout(function(){
$scope.requery($scope.search);
},300);
}
So that the request is only made 300ms after the user has stopped typing. Is there any solution to wrap this in a directive?
As of angular 1.3 this is way easier to accomplish, using ngModelOptions:
<input ng-model="search" ng-change="updateSearch()" ng-model-options="{debounce:3000}">
Syntax: {debounce: Miliseconds}
To solve this problem, I created a directive called ngDelay.
ngDelay augments the behavior of ngChange to support the desired delayed behavior, which provides updates whenever the user is inactive, rather than on every keystroke. The trick was to use a child scope, and replace the value of ngChange to a function call that includes the timeout logic and executes the original expression on the parent scope. The second trick was to move any ngModel bindings to the parent scope, if present. These changes are all performed in the compile phase of the ngDelay directive.
Here's a fiddle which contains an example using ngDelay:
http://jsfiddle.net/ZfrTX/7/ (Written and edited by me, with help from mainguy and Ryan Q)
You can find this code on GitHub thanks to brentvatne. Thanks Brent!
For quick reference, here's the JavaScript for the ngDelay directive:
app.directive('ngDelay', ['$timeout', function ($timeout) {
return {
restrict: 'A',
scope: true,
compile: function (element, attributes) {
var expression = attributes['ngChange'];
if (!expression)
return;
var ngModel = attributes['ngModel'];
if (ngModel) attributes['ngModel'] = '$parent.' + ngModel;
attributes['ngChange'] = '$$delay.execute()';
return {
post: function (scope, element, attributes) {
scope.$$delay = {
expression: expression,
delay: scope.$eval(attributes['ngDelay']),
execute: function () {
var state = scope.$$delay;
state.then = Date.now();
$timeout(function () {
if (Date.now() - state.then >= state.delay)
scope.$parent.$eval(expression);
}, state.delay);
}
};
}
}
}
};
}]);
And if there are any TypeScript wonks, here's the TypeScript using the angular definitions from DefinitelyTyped:
components.directive('ngDelay', ['$timeout', ($timeout: ng.ITimeoutService) => {
var directive: ng.IDirective = {
restrict: 'A',
scope: true,
compile: (element: ng.IAugmentedJQuery, attributes: ng.IAttributes) => {
var expression = attributes['ngChange'];
if (!expression)
return;
var ngModel = attributes['ngModel'];
if (ngModel) attributes['ngModel'] = '$parent.' + ngModel;
attributes['ngChange'] = '$$delay.execute()';
return {
post: (scope: IDelayScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes) => {
scope.$$delay = {
expression: <string>expression,
delay: <number>scope.$eval(attributes['ngDelay']),
execute: function () {
var state = scope.$$delay;
state.then = Date.now();
$timeout(function () {
if (Date.now() - state.then >= state.delay)
scope.$parent.$eval(expression);
}, state.delay);
}
};
}
}
}
};
return directive;
}]);
interface IDelayScope extends ng.IScope {
$$delay: IDelayState;
}
interface IDelayState {
delay: number;
expression: string;
execute(): void;
then?: number;
action?: ng.IPromise<any>;
}
This works perfectly for me: JSFiddle
var app = angular.module('app', []);
app.directive('delaySearch', function ($timeout) {
return {
restrict: 'EA',
template: ' <input ng-model="search" ng-change="modelChanged()">',
link: function ($scope, element, attrs) {
$scope.modelChanged = function () {
$timeout(function () {
if ($scope.lastSearch != $scope.search) {
if ($scope.delayedMethod) {
$scope.lastSearch = $scope.search;
$scope.delayedMethod({ search: $scope.search });
}
}
}, 300);
}
},
scope: {
delayedMethod:'&'
}
}
});
Using the directive
In your controller:
app.controller('ctrl', function ($scope,$timeout) {
$scope.requery = function (search) {
console.log(search);
}
});
In your view:
<div ng-app="app">
<div ng-controller="ctrl">
<delay-search delayed-method="requery(search)"></delay-search>
</div>
</div>
I know i'm late to the game but,hopefully this will help anyone still using 1.2.
Pre ng-model-options i found this worked for me, as ngchange will not fire when the value is invalid.
this is a slight variation on #doug's answer as it uses ngKeypress which doesn't care what state the model is in.
function delayChangeDirective($timeout) {
var directive = {
restrict: 'A',
priority: 10,
controller: delayChangeController,
controllerAs: "$ctrl",
scope: true,
compile: function compileHandler(element, attributes) {
var expression = attributes['ngKeypress'];
if (!expression)
return;
var ngModel = attributes['ngModel'];
if (ngModel) {
attributes['ngModel'] = '$parent.' + ngModel;
}
attributes['ngKeypress'] = '$$delay.execute()';
return {
post: postHandler,
};
function postHandler(scope, element, attributes) {
scope.$$delay = {
expression: expression,
delay: scope.$eval(attributes['ngKeypressDelay']),
execute: function () {
var state = scope.$$delay;
state.then = Date.now();
if (scope.promise) {
$timeout.cancel(scope.promise);
}
scope.promise = $timeout(function() {
delayedActionHandler(scope, state, expression);
scope.promise = null;
}, state.delay);
}
};
}
}
};
function delayedActionHandler(scope, state, expression) {
var now = Date.now();
if (now - state.then >= state.delay) {
scope.$parent.$eval(expression);
}
};
return directive;
};
I have this directive, which is basicly wrapper for typeahead plugin from bootstrap. Everything is working like a charm. But now I have to set initial vaule in typeahead's input field. The value is passed as a string in attrs.init. But I don't know how to insert it into text field.
angular.module('rcApp')
.directive('rcAutocomplete', ['$injector', function ($injector) {
return {
scope: {
model: '#',
search: '#',
key: '#',
show: '#',
init: '#',
ngModel: '='
},
template: '<input type="text">',
replace: true,
restrict: 'E',
require: 'ngModel',
link: function (scope, element, attrs) {
// inject model service
var service = $injector.get(attrs.model);
// define search function
var searchFunction = attrs.search;
// holds picked object id
scope.id = 0;
// holds objects that matched query, mapped by "show" value
scope.map = {};
element.on('focusout ac.itempicked', function () {
scope.$apply(function () {
scope.ngModel = scope.id;
});
});
// launch typehead plugin
element.typeahead(
{
source: function (query, process) {
// clear cache
scope.id = 0;
scope.map = {};
service[searchFunction](query).then(function (result) {
var dataValues = [];
var fieldsToShow = scope.show.split('|');
$.each(result.data, function (index, dataItem) {
// generate key-show string
var valueHash = '';
for (var i = 0; i < fieldsToShow.length; i++) {
valueHash += dataItem[fieldsToShow[i]] + ' ';
}
valueHash = $.trim(valueHash);
// map results
scope.map[valueHash] = dataItem;
// prepare return strings
dataValues.push(valueHash);
});
// return content
process(dataValues);
});
},
updater: function (item) {
if (typeof scope.key === 'undefined') {
scope.id = scope.map[item];
}
else {
scope.id = scope.map[item][scope.key];
}
element.trigger('ac.itempicked');
return item;
}
}
);
}
};
}]);
** UPDATE **
Solution, that worked for me is adding code like this to link function:
// init value
if (typeof attrs.init !== 'undefined') {
window.setTimeout(function () {
element.val(attrs.init);
scope.$apply();
}, 10);
}
But still I don't quite understand why "element.val(attrs.init);" don't updated the view, and calling scope.$apply() did, but throw an "$digest already in progress" error. Wrapping it in window.setTimeout helped, but this also is a hack for me....
It's got to be a way to make it cleaner/simpler...