Thank you in advance!
I added custom directive to present field in phone number format. It works well untill gulp minifies the js files. After that it throws error
[$injector:unpr] Unknown provider: eProvider <- e <- phoneInputDirective
Is the special way to register the directive somewhere or so? I am not sure what is going on. Please help.
careApp.directive('phoneInput', function ($filter, $browser) {
return {
require: 'ngModel',
link: function ($scope, $element, $attrs, ngModelCtrl) {
var listener = function () {
var value = $element.val().replace(/[^0-9]/g, '');
$element.val($filter('tel')(value, false));
};
// This runs when we update the text field
ngModelCtrl.$parsers.push(function (viewValue) {
return viewValue.replace(/[^0-9]/g, '').slice(0, 10);
});
// This runs when the model gets updated on the scope directly and keeps our view in sync
ngModelCtrl.$render = function () {
$element.val($filter('tel')(ngModelCtrl.$viewValue, false));
};
$element.bind('change', listener);
$element.bind('keydown', function (event) {
var key = event.keyCode;
// If the keys include the CTRL, SHIFT, ALT, or META keys, or the arrow keys, do nothing.
// This lets us support copy and paste too
if (key == 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) {
return;
}
$browser.defer(listener); // Have to do this or changes don't get picked up properly
});
$element.bind('paste cut', function () {
$browser.defer(listener);
});
}
};
});
careApp.filter('tel', function () {
return function (tel) {
if (!tel) { return ''; }
var value = tel.toString().trim().replace(/^\+/, '');
if (value.match(/[^0-9]/)) {
return tel;
}
var country, city, number;
switch (value.length) {
case 1:
case 2:
case 3:
city = value;
break;
default:
city = value.slice(0, 3);
number = value.slice(3);
}
if (number) {
if (number.length > 3) {
number = number.slice(0, 3) + '-' + number.slice(3, 7);
}
else {
number = number;
}
return ("(" + city + ") " + number).trim();
}
else {
return city;
}
};
});
I figured this one out. The problem was dependence injection.
In the directive declaration the dependency should go before function in [] brackets.
In my case it was:
careApp.directive('phoneInput',['$filter', '$browser', function ($filter, $browser)
{....}
])
Related
Is it possible to decide whether to use templateUrl parameter in the link function of AngularJS directive?
Suppose I have the following directive:
app.directive('sitesAndImprovements', function() {
return {
restrict: 'E',
replace:true,
templateUrl: '<path-to-file>/site-and-improvments.html',
link: function (scope, elem, attrs) {
scope.testClick = function() {
var myScope = scope;
//debugger;
}
scope.constructionCompleteClick = function () {
if (scope.construction_complete == 'Yes') {
scope.hold_back = '';
scope.percent_complete = 100;
} else
if (scope.construction_complete == 'No') {
scope.hold_back = '1';
if (scope.percent_complete == 100) {
scope.percent_complete = '';
}
}
}
scope.calcTotal = function () {
var total;
total = (scope.main || 0) + (scope.second || 0) + (scope.third || 0) + (scope.fourth || 0);
scope.total = total || null;
}
}
}
})
I want to control whether to use or not to use the templateUrl and also the replace parameters in the link() function.
This is because I already implemented this directive in about 10+ places without using templateUrl and now I want to start using this feature, but I don't want to make changes to existing and working code.
Is that possible and how?
Tarek
I don't think you can do that in the link, but I believe you can turn templateUrl into a function that can return different values for the directive.
Try doing something like this for your templateUrl:
templateUrl: function() {
if (someCondition) {
return '<path-to-file>/site-and-improvments.html';
} else {
return null;
}
},
app.directive('sitesAndImprovements', function() {
return {
restrict: 'E',
replace:function(){
if (aCondition){
return true;
} else {
return false;
}
},
templateUrl: function(){
if (aCondition){
return '<path-to-file>/site-and-improvments.html';
} else {
return undefined;
}
},
link: function (scope, elem, attrs) {
scope.testClick = function() {
var myScope = scope;
//debugger;
}
scope.constructionCompleteClick = function () {
if (scope.construction_complete == 'Yes') {
scope.hold_back = '';
scope.percent_complete = 100;
} else
if (scope.construction_complete == 'No') {
scope.hold_back = '1';
if (scope.percent_complete == 100) {
scope.percent_complete = '';
}
}
}
scope.calcTotal = function () {
var total;
total = (scope.main || 0) + (scope.second || 0) + (scope.third || 0) + (scope.fourth || 0);
scope.total = total || null;
}
}
}
})
Explanation : As stated in the source code, the template will be compiled only if templateUrl is given :
...
if (directive.templateUrl) {
hasTemplate = true;
assertNoDuplicate('template', templateDirective, directive, $compileNode);
templateDirective = directive;
if (directive.replace) {
replaceDirective = directive;
}
// eslint-disable-next-line no-func-assign
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
...
Please, note that aCondition could be an attribute passed to the directive to enable/disable the templateUrl and the replace. Also, keep in mind that the replace is deprecated.
I have several form fields which need to be displayed in USD ($n.nn), and have used a custom currency directive successfully. But I've added NgModelOptions to control debounce:
{ updateOn: 'default blur', debounce: { 'default':100, 'blur': 0 } }"
This works fine EXCEPT when the field is updated then exited rapidly, in which case the prior value displays. Entering then leaving the field will then cause the correct value to display.
Any ideas on how to combine a directive or currency filter with NgModelOptions so the value displayed reflects the model value on exit?
Here's the current directive:
.directive('ngCurrency', function ($filter, $locale) {
return {
require: 'ngModel',
scope: {
min: '=min',
max: '=max',
ngRequired: '=ngRequired'
},
link: function (scope, element, attrs, ngModel) {
function decimalRex(dChar) {
return RegExp("\\d|\\" + dChar, 'g');
}
function clearRex(dChar) {
return RegExp("((\\" + dChar + ")|([0-9]{1,}\\" + dChar + "?))&?[0-9]{0,2}", 'g');
}
function decimalSepRex(dChar) {
return RegExp("\\" + dChar, "g");
}
function clearValue(value) {
value = String(value);
var dSeparator = $locale.NUMBER_FORMATS.DECIMAL_SEP;
var clear = null;
if (value.match(decimalSepRex(dSeparator))) {
clear = value.match(decimalRex(dSeparator))
.join("").match(clearRex(dSeparator));
clear = clear ? clear[0].replace(dSeparator, ".") : null;
}
else if (value.match(decimalSepRex("."))) {
clear = value.match(decimalRex("."))
.join("").match(clearRex("."));
clear = clear ? clear[0] : null;
}
else {
clear = value.match(/\d/g);
clear = clear ? clear.join("") : null;
}
return clear;
}
ngModel.$parsers.push(function (viewValue) {
cVal = clearValue(viewValue);
return parseFloat(cVal);
});
element.on("blur", function () {
element.val($filter('currency')(ngModel.$modelValue));
scope.$apply();
});
ngModel.$formatters.unshift(function (value) {
return $filter('currency')(value);
});
scope.$watch(function () {
return ngModel.$modelValue;
}, function (newValue, oldValue) {
runValidations(newValue);
});
function runValidations(cVal) {
if (!scope.ngRequired && isNaN(cVal)) {
return;
}
if (scope.min) {
var min = parseFloat(scope.min);
ngModel.$setValidity('min', cVal >= min);
}
if (scope.max) {
var max = parseFloat(scope.max);
ngModel.$setValidity('max', cVal <= max);
}
}
}
}
})
element.on("blur", function () {
element.val($filter('currency')(ngModel.$modelValue));
scope.$apply();
});
What I assume is happening is that you are changing a field and leaving it, which causes the digest cycle to trigger - and that makes the scope.$apply() inside your event callback try to execute while it is already being executed. A workaround to this is usually to wrap the whole call in a $timeout.
element.on("blur", function () {
$timeout(function() {
element.val($filter('currency')(ngModel.$modelValue));
scope.$apply();
});
});
I'm pretty sure that when this happens the console will display some kind of error message - for future reference.
Is there any reason why you are not using the filter directly on the model such as: {{ my.model | currency }} instead of adding an event? Seems like bad practice.
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);
});
}
};
});
First I want to say that I am a complete beginner in AngularJS and just attempting to understand the basic concepts. I have a background in Java and PHP.
I am building a part of a website. Right now the angular app only consists of opening and closing 2 drop down menus registrationDropDown and loginDropDown. I want them to work so that only one can be open at a time ie. if I open one, and the other is already open, the older one is forced to close.
I have a service to manage the variables that determine whether the drop downs should be open or closed and 2 controllers, one for login and one for registration, both include $watch for the respective variables.
THE PROBLEM
I want the app to work so that only one of the drop downs can be open at one time.
JSFIDDLE: http://jsfiddle.net/F5p6m/3/
angular.module("ftApp", [])
.factory('dropDownService', function () {
var loginDropDownStatus = false;
var registrationDropDownStatus = false;
return {
getLoginDropDownStatus: function () {
return loginDropDownStatus;
},
showLoginDropDown: function () {
console.log("showing login drop down");
registrationDropDownStatus = false;
loginDropDownStatus = true;
console.log("loginDropDownStatus" + loginDropDownStatus + "registrationDropDownStatus" + registrationDropDownStatus);
},
hideLoginDropDown: function () {
console.log("hiding login drop down");
loginDropDownStatus = false;
console.log("loginDropDownStatus" + loginDropDownStatus);
},
getRegistrationDropDownStatus: function () {
return registrationDropDownStatus;
},
showRegistrationDropDown: function () {
console.log("showing registration drop down");
registrationDropDownStatus = true;
loginDropDownStatus = false;
console.log("registrationDropDownStatus" + registrationDropDownStatus);
},
hideRegistrationDropDown: function () {
console.log("hiding registration drop down");
registrationDropDownStatus = false;
console.log("registrationDropDownStatus" + registrationDropDownStatus);
}
};
}) .controller("LoginDropDownController", function ($scope, dropDownService) {
$scope.loginDropDownStatus = dropDownService.getLoginDropDownStatus();
$scope.$watchCollection('loginDropDownStatus', function(newValue, oldValue) {
console.log("watcher is working");
console.log("value is " + newValue + oldValue);
console.log("LOGIN new value is " + newValue);
$scope.loginDropDownStatus = newValue;
});
$scope.toggleDropDown = function () {
if ( $scope.loginDropDownStatus == false ) {
dropDownService.showLoginDropDown();
dropDownService.hideRegistrationDropDown();
$scope.loginDropDownStatus = true;
} else if ( $scope.loginDropDownStatus == true ) {
dropDownService.hideLoginDropDown();
$scope.loginDropDownStatus = false;
}
};
})
.controller("RegistrationDropDownController", function ($scope, dropDownService) {
$scope.registrationDropDownStatus = dropDownService.getRegistrationDropDownStatus();
$scope.$watch('registrationDropDownStatus', function(newValue, oldValue) {
console.log("watcher is working");
console.log("value is " + newValue + oldValue);
console.log("new value is " + newValue);
$scope.registrationDropDownStatus = newValue;
});
$scope.toggleDropDown = function () {
if ( $scope.registrationDropDownStatus == false ) {
dropDownService.showRegistrationDropDown();
dropDownService.hideLoginDropDown();
$scope.registrationDropDownStatus = true;
} else if ( $scope.registrationDropDownStatus == true ) {
dropDownService.hideRegistrationDropDown();
$scope.registrationDropDownStatus = false;
}
};
})
Edit:
Here is probably the shortest option:
angular.module("ftApp", [])
.controller("ctrl", function ($scope) {
$scope.toggle = function(menu){
$scope.active = $scope.active === menu ? null : menu;
}
})
FIDDLE
One controller, no service.
Previous Answer:
I think you have quite a bit of code to get something very simple done. Here is my solution:
angular.module("ftApp", [])
.service('dropDownService', function () {
this.active = null;
this.toggle = function(menu){
this.active = this.active === menu ? null : menu;
}
})
.controller("LoginDropDownController", function ($scope, dropDownService) {
$scope.status = dropDownService;
$scope.toggleDropDown = function () {
dropDownService.toggle("login");
};
})
.controller("RegistrationDropDownController", function ($scope, dropDownService) {
$scope.status = dropDownService;
$scope.toggleDropDown = function () {
dropDownService.toggle("reg");
};
})
FIDDLE
You can make it even shorter by only using one controller. You wouldn't even need the service then.
You are overcomplicating things. All you need your service to hold is a property indicating which dorpdown should be active.
Then you can change that property's value from the controller and check the value in the view to determine if a dropdown should be shown or hidden.
Something like this:
<!-- In the VIEW -->
<li ng-controller="XyzController">
<a ng-click="toggleDropdown()">Xyz</a>
<div ng-show="isActive()">Dropdown</div>
</li>
/* In the SERVICE */
.factory('DropdownService', function () {
return {
activeDropDown: null
};
})
/* In the CONTROLLER */
controller("XyzDropdownController", function ($scope, DropdownService) {
var dropdownName = 'xyz';
var dds = DropdownService;
$scope.isActive = function () {
return dropdownName === dds.activeDropdown;
};
$scope.toggleDropdown = function () {
dds.activeDropdown = (dds.activeDropdown === dropdownName) ?
null :
dropdownName;
};
})
See, also, this short demo.
Based on what exactly you are doing, there might be other approaches possible/preferrable:
E.g. you could use just on controller to control all dropdowns
or you could use two instances of the same controller to control each dropdown.
See my updated fiddle. I simplified the code and removed the service. Because you just used two variables to control visibility, you don't need a service nor $watch. You need to keep variables in the $rootScope, otherwise changes in a controller is not visible to another controller due to isolated scopes.
angular.module("ftApp", [])
.controller("LoginDropDownController", function ($scope, $rootScope) {
$rootScope.loginDropDownStatus = false;
$scope.toggleDropDown = function () {
if ($rootScope.loginDropDownStatus == false) {
$rootScope.registrationDropDownStatus = false;
$rootScope.loginDropDownStatus = true;
} else if ($rootScope.loginDropDownStatus == true) {
$rootScope.loginDropDownStatus = false;
}
};
}).controller("RegistrationDropDownController", function ($scope, $rootScope) {
$rootScope.registrationDropDownStatus = false;
$scope.toggleDropDown = function () {
if ($rootScope.registrationDropDownStatus === false) {
$rootScope.loginDropDownStatus = false;
$rootScope.registrationDropDownStatus = true;
} else if ($scope.registrationDropDownStatus === true) {
$rootScope.registrationDropDownStatus = false;
}
};
})
This code can be simplified further. I'll leave that to you.
I have an angular directive that listens for a google place 'place_changed' event. The code works just fine in desktop browsers.
However, it doesn't work on mobile safari in iOS7 (haven't tested older versions). Tapping a location doesn't seem to be firing the place_changed event and the input field doesn't get populated with the location value.
Is there some other event I should be triggering, listening for?
angular.module('app').directive('googlePlace', function (Location) {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, element, attributes, model) {
var autocomplete = new google.maps.places.Autocomplete(element[0], {types: ['(cities)']});
google.maps.event.addListener(autocomplete, 'place_changed', function () {
if (autocomplete.getPlace()){
var googlePlace = autocomplete.getPlace();
$scope.googlePlace = googlePlace;
Location.setGooglePlace(googlePlace);
var location = googlePlace.address_components[0].long_name + ', ';
if (googlePlace.address_components.length === 2) {
location = location + googlePlace.address_components[1].long_name;
} else if (googlePlace.address_components.length === 3) {
location = location + googlePlace.address_components[2].long_name;
} else if (googlePlace.address_components[3].short_name === 'US') {
location = location + googlePlace.address_components[2].short_name;
} else {
location = location + googlePlace.address_components[3].long_name;
}
$scope.displayLocation = location;
$scope.$apply(function () {
setTimeout(function(){
model.$setViewValue($scope.displayLocation);
model.$setValidity(element.name, true);
element.val($scope.displayLocation);
}, 100);
});
} else {
$scope.googlePlace = null;
$scope.displayLocation = null;
model.$setViewValue($scope.displayLocation);
model.$setValidity(element.name, true);
element.val($scope.displayLocation);
}
});
element.on('blur', function () {
setTimeout(function(){
if (attributes.required && !$scope.googlePlace){
model.$setValidity(element.name, false);
} else if (element.val() && !$scope.googlePlace){
model.$setValidity(element.name, false);
} else if (!element.val()){
Location.setGooglePlace(null);
model.$setValidity(element.name, true);
} else{
model.$setViewValue($scope.displayLocation);
model.$setValidity(element.name, true);
element.val($scope.displayLocation);
}
}, 300); // this seems to matter even though 0
});
element.on('keypress', function (e) {
$scope.googlePlace = null;
Location.setGooglePlace(null);
if (e.which === 13) {
google.maps.event.trigger(autocomplete, 'place_changed');
}
});
}
};
});