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...
Related
I have a directive to drag and drop.
The drag and drop works well, but I have a problem with updating the model.
After I drop some text into textarea, the text is showing ok, but the model is not updating.
What am I missing here?
//markup
<textarea drop-on-me id="editor-texto" ng-trim="false" ng-model="mymodel"
name="templateSms.template">test.</textarea>
//directive
angular
.module('clinang')
.directive('dragMe', dragMe)
.directive('dropOnMe', dropOnMe);
dragMe.$inject = [];
function typeInTextarea(el, newText) {
var start = el.selectionStart
var end = el.selectionEnd
var text = el.value
var before = text.substring(0, start)
var after = text.substring(end, text.length)
el.value = (before + newText + after)
el.selectionStart = el.selectionEnd = start + newText.length
el.focus()
}
function dragMe() {
var DDO = {
restrict: 'A',
link: function(scope, element, attrs) {
element.prop('draggable', true);
element.on('dragstart', function(event) {
event.dataTransfer.setData('text', event.target.id)
});
}
};
return DDO;
}
dropOnMe.$inject = [];
function dropOnMe() {
var DDO = {
restrict: 'A',
link: function(scope, element, attrs) {
element.on('dragover', function(event) {
event.preventDefault();
});
element.on('drop', function(event) {
event.preventDefault();
var data = event.dataTransfer.getData("text");
var x=document.getElementById(data);
typeInTextarea(event.target,x.getAttribute('data-value'))
});
}
};
return DDO;
}
Update your textarea model inside typeInTextarea function and using $apply run digest cycle to update the model change across whole app. For that with your current structure of directives with only link functions you'll need to pass scope to the typeInTextarea function (as a parameter).
So your function will be:
function typeInTextarea(scope, el, newText) {
var start = el.selectionStart
var end = el.selectionEnd
var text = el.value
var before = text.substring(0, start)
var after = text.substring(end, text.length)
el.value = (before + newText + after);
scope.mymodel.textnote = el.value;
el.selectionStart = el.selectionEnd = start + newText.length;
el.focus();
}
and dropOnMe function will be:
function dropOnMe() {
var DDO = {
restrict: 'A',
link: function(scope, element, attrs) {
element.on('dragover', function(event) {
event.preventDefault();
});
element.on('drop', function(event) {
event.preventDefault();
var data = event.dataTransfer.getData("text");
var x=document.getElementById(data);
typeInTextarea(scope, event.target,x.getAttribute('data-value'))
scope.$apply();
});
}
};
return DDO;
}
Check out this example (I don't know which element you're dragging so e.g. I've considered span element & just used innerHTML for that ):
https://plnkr.co/edit/wGCNOfOhoopeZEM2WMd1?p=preview
Below is a directive I am using, where I am trying to update a template URL based on a variable in a factory:
.directive('poGenericNotification',['errorHandler', function(errorHandler) {
return {
controller: 'ErmModalCtrl',
restrict: 'EA',
scope: {
title: '=',
errorList: '=',
errMsg: '=',
error: '=',
info: '=',
numNotifications: '=',
messageOverflow: '='
},
template: "<div ng-include='getTemplateUrl()' class='generic-notif-container' ng-click='openErm()'></div>",
transclude: true,
link: function(scope) {
scope.$watch(errorHandler.getMostRecentError(), function(mostRecentError) {
scope.getTemplateUrl = function() {
if (mostRecentError.type === "Alert") {
return 'src/alerts/templates/error-alert.html';
}
else if (mostRecentError.type === "Info") {
return 'src/alerts/templates/info-alert.html';
}
}
}, true);
}
}
}])
Here is the factory it is referencing:
.factory('errorHandler', function () {
var errorArray = [];
var mostRecentError = {
type:'', message: '', timestamp: ''
};
function compareObjs(a,b) {
//sorting function
}
errorArray.addError = (function (type, message) {
var timestamp = Date.now();
errorArray.push({type: type, message: message, timestamp: timestamp});
errorArray.sort(compareObjs);
errorArray.generateMostRecentError();
});
//....some functions
errorArray.generateMostRecentError = function() {
if (errorArray[0].message.length > 138) {
mostRecentError.message = errorArray[0].message.slice(0, 138) + "...";
messageOverflow = true;
} else {
mostRecentError.message = errorArray[0].message;
messageOverflow = false;
}
mostRecentError.type = errorArray[0].type;
mostRecentError.timestamp = errorArray[0].timestamp;
console.log(mostRecentError);
}
errorArray.getMostRecentError = function() {
console.log(mostRecentError);
return mostRecentError;
}
return errorArray;
})
I want to be able to add/remove errors from other controllers and have it update the directive. Currently, for the initial value the $watch mostRecentError callback is undefined, then it never updates. What have I missed?
You need to pass the function to the $watch instead of its result. Change to:
scope.$watch(errorHandler.getMostRecentError, ...);
To have the function called on every digest loop you should replace
errorHandler.getMostRecentError()
by
errorHandler.getMostRecentError
otherwise you're watching the result of the function call as a variable attached to the directive's scope.
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.
I have a directive which I've used for texts in my app:
module.directive("enaText", function (textService) {
return {
restrict: "AE",
link: function (scope, element, attributes) {
// Catching <enaText key="[key]"> and <div ena-text="[key]">
scope.$watchCollection(function () {
return [attributes.key, attributes.enaText];
}, function (values) {
var key = values[0] || values[1];
if (!key) {
return;
}
var text = textService.get(key) || key;
// Not using a template to easier support HTML in text value
element.html(text || "");
}, true);
}
};
});
My textService helps with getting the text in the current language from sessionStorage (initially from a database). This version of the directive works just as intended:
<div ena-text="page_title"></div>
Which gets the text with name/key "page_title" and puts it in the div.
Now I want to extend the directory to be able to use scope variables in the text strings from textService and possibly also filters. This is what I have so far:
module.directive("enaTextNew", function (textService, $interpolate, $parse, $compile) {
return {
restrict: "AE",
link: function (scope, element, attributes) {
var regex = /^([^\|]*)(\|.+)?/;
// Catching <enaText key="[key]"> and <div ena-text="[key]">
scope.$watchCollection(function () {
return [attributes.key, attributes.enaTextNew];
}, function (values) {
var expression = values[0] || values[1];
if (!expression) {
return;
}
var match = expression.match(regex);
var key = match[1].trim();
var filter = match[2];
var text = textService.get(key) || key;
text = $interpolate(text)(scope);
if (filter) {
text = scope.$eval("'" + text + "'" + filter);
}
// Not using a template to easier support HTML in text value
element.html(text || "");
}, true);
}
};
});
This works fine when I use it in:
<div ena-text-new="character_count|uppercase"></div>
Which gets the text "{{count}} characters of max {{max}}", uses variables count and max from scope and then adds the uppercase filter. The result is for example: "0 CHARACTERS OF MAX 100".
The only problem is that even though scope.count (or scope.max) is changed, it's not reflected in the result of the directive.
This specific string and the filter is just an example. Filters will propably not be necessary, I've tried without it but it didn't do any difference. But the important thing is the scope variables.
The user PSL helped me get on the right track using http://plnkr.co/edit/ir9Ews. I made some changes to it, because a new watch would else be created every time the attributes values changed. This is what I use now:
module.directive("enaText", function (textService, $interpolate) {
return {
restrict: "AE",
link: function (scope, element, attributes) {
var keyFilterRegex = /^([^\|]*)(\|.+)?/;
var originalText;
var filter;
// Catching <enaText key="[key]"> and <div ena-text="[key]">
scope.$watchCollection(function () {
return [attributes.key, attributes.enaText];
}, function (values) {
var expression = values[0] || values[1];
if (!expression) {
originalText = undefined;
filter = undefined;
return;
}
var match = expression.match(keyFilterRegex);
originalText = textService.get(match[1]);
filter = match[2];
});
scope.$watch(function () {
return $interpolate(originalText || "")(scope);
}, function (text) {
if (filter) {
text = scope.$eval("'" + text + "'" + filter);
}
// Not using a template to easier support HTML in text value
element.html(text);
});
}
};
});
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;
};