I have come across a problem my Angular knowledge is a little too limited to figure out. I want a non html5 placeholder attribute. Here is some code I found before on stack overflow that does this handsomely:
// Placeholder for non HTML5 browsers
app.directive("ngPlaceholder", function($log, $timeout) {
var txt;
return {
restrict: "A",
scope: { txt: "#ngPlaceholder" },
link: function(scope, elem, attrs) {
elem.on("focus", function() {
if(elem.val() === scope.txt) {
elem.val("");
}
scope.$apply()
})
elem.on("blur", function() {
if(elem.val() === "") {
elem.val(scope.txt);
}
scope.$apply()
})
// Initialise placeholder
$timeout(function() {
elem.val(scope.txt)
scope.$apply();
})
}
}
})
However... use it in conjunction with ng-model:
input(
type="text"
ng-model="card.number"
ng-placeholder="0000-0000-0000-0000")
And it obliterates the two way data binding!
Heres a plunker:
http://plnkr.co/edit/1AvVOxb5O6P5pU3wIuKv?p=preview
What am I missing?
Update Many people have voiced there solutions to this rather annoying problem here
Use $parent to refer the to model in the parent scope since the directive ngPlaceholder creates an isolated scope. (This is not specific to IE 9 though. )
<input type="text" ng-placeholder="0000-0000-0000-0000" ng-model="$parent.card.number2"/>
This one fixes the problem that in delay of $timeout, the $scope may be changed in meantime. It also makes it cross-browser.
// Placeholder for all browsers
app.directive("ngPlaceholder", function($log, $timeout) {
return {
restrict: "A",
link: function(scope, elem, attrs) {
var txt = attrs.ngPlaceholder,
model = attrs.ngModel,
placeholderSupport = 'placeholder' in document.createElement("input");
//Use HTML5 placeholder attribute.
if (placeholderSupport) {
attrs.$set("placeholder", txt);
return;
}
elem.on("focus", function(event) {
if (elem.val() === txt) {
elem.val("");
}
});
elem.on("blur", function(event) {
if (elem.val() === "") {
elem.val(txt);
}
});
scope.$watch(model, function (newValue, oldValue, scope) {
if (newValue === undefined || newValue === "") {
elem.val(txt);
//scope.$apply(); not needed, since scope fired this event.
}
}, true);
}
}
});
You seen the ngModel issue, so the isolate scope on ngPlaceholder should be removed. I realize that sza's workaround works, but the key thing I'd emphasize is that the ngPlaceholder doesn't need its own scope.
For example here I tweaked the directive and removed the references to scope by storing creating the txt variable as its own local variable.
http://plnkr.co/edit/43z1TZHFwmgLJ9wyystD?p=preview
// Placeholder for non HTML5 browsers
app.directive("ngPlaceholder", function($log, $timeout) {
var txt;
return {
restrict: "A",
link: function(scope, elem, attrs) {
var txt = attrs.ngPlaceholder;
elem.bind("focus", function() {
if(elem.val() === txt) {
elem.val("");
}
scope.$apply()
})
elem.bind("blur", function() {
if(elem.val() === "") {
elem.val(txt);
}
scope.$apply()
})
// Initialise placeholder
$timeout(function() {
elem.val(txt)
scope.$apply();
})
}
}
})
placeholder support in IE with normal color
app.directive('placeholder',['$timeout','$window', function($timeout,$window){
var i = document.createElement('input');
if ('placeholder' in i) {
return {}
}
return {
link: function(scope, elm, attrs){
var userAgent = $window.navigator.userAgent;
if(userAgent.indexOf("MSIE 9.0") <0){
return;
}
if (attrs.type === 'password') {
return;
}
$timeout(function(){
elm.val(attrs.placeholder).css({"color":'#ccc'});
elm.bind('focus', function(){
if (elm.val() == attrs.placeholder) {
elm.val('').css({"color":'#555'});
}
}).bind('blur', function(){
if (elm.val() == '') {
elm.val(attrs.placeholder).css({"color":'#ccc'});
}
});
});
}
}
}]);
live code here:http://plnkr.co/edit/ev6kQ3Ks31FhqAfCMDkc
Related
This is some directive that i use for detecting enter key
.directive('enterSubmit', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
elem.bind('keydown', function(event) {
var code = event.keyCode || event.which;
if (code === 13) {
if (!event.shiftKey) {
event.preventDefault();
scope.$apply(attrs.enterSubmit);
}
}
});
}
}
})
I have a function that i wanna get $event as a parameter to it.
$scope.test = function(evt){
var el = angular.element(evt.target);
console.log(el[0]);
}
But when i use $event as i could in built-in directives
<textarea enter-submit="test($event)"></textarea>
It says evt is undefined so how to do it?
Change your if to this:
if (!event.shiftKey) {
event.preventDefault();
var enterSubmitFunction = $parse(attrs['enterSubmit']);
enterSubmitFunction(scope, { $event: event });
scope.$apply();
}
You'll need to inject $parse now, and i'm not sure if you need to use scope.$parent in the enterSubmitFunction or just scope. Try both.
I have a situtation where in i want to avoid using $emit to share a scope variable and instead share some property using a underlying service, the problem is that the property value gets set on return of a promise response in directive 1 and by the time that property value is set in service through directive 1, directive 2 is already loaded and hence the property comes as undefined in directive 2.
Any ideas?
With the provided information, thought of writing this code fragment. Hope this will give you some insights to find the best answer.
angular.module('myApp').service('SomeService', function($http) {
this.readData = function(dataUrl) {
// read data;
return $http.get(dataUrl)
.then(function(res) {
return res.data;
}, function(res) {
return res;
}
}
return this;
});
angular.module('myApp').controller('MyController', function($scope, SomeService) {
$scope.readData = function(url) {
SomeService.readData(url)
.then(function(res) {
$scope.data = res;
}, function(res) {
// Display error
}
}
}
angular.module('myApp').directory('myDirectory1', function() {
return {
restrict: 'A',
link: function(scope, elm, attrs) {
scope.data = scope.readData(url);
}
}
});
angular.module('myApp').directory('myDirectory2', function() {
return {
restrict: 'A',
scope: {
data : '#'
},
link: function(scope, elm, attrs) {
scope.$watch('data', function(newVal) {
// Do some stuffs
});
}
}
});
Perhaps extract the functionality that delivers the promise from your directive1 to the service, and in both directives use
.then(function(data){ ... } )
I do not want a user to enter spaces in a text field. I don't want it on submit validation but rather - a space will not show up on the text field when they click it.
The selected answer is arguably not very unobtrusive. And if you need to use it in multiple places, you'll end up with duplicated code.
I prefer to prevent the input of spaces using the following directive.
app.directive('disallowSpaces', function() {
return {
restrict: 'A',
link: function($scope, $element) {
$element.bind('input', function() {
$(this).val($(this).val().replace(/ /g, ''));
});
}
};
});
This directive can be invoked like this:
<input type="text" disallow-spaces>
<input ng-model="field" ng-trim="false" ng-change="field = field.split(' ').join('')" type="text">
Update:
To improve code quality you can create custom directive instead. But don't forget that your directive should prevent input not only from keyboard, but also from pasting.
<input type="text" ng-trim="false" ng-model="myValue" restrict-field="myValue">
Here is important to add ng-trim="false" attribute to disable trimming of an input.
angular
.module('app')
.directive('restrictField', function () {
return {
restrict: 'AE',
scope: {
restrictField: '='
},
link: function (scope) {
// this will match spaces, tabs, line feeds etc
// you can change this regex as you want
var regex = /\s/g;
scope.$watch('restrictField', function (newValue, oldValue) {
if (newValue != oldValue && regex.test(newValue)) {
scope.restrictField = newValue.replace(regex, '');
}
});
}
};
});
If you want to achieve it without writing directive
ng-keydown="$event.keyCode != 32 ? $event:$event.preventDefault()"
THe directive Jason wrote did not work for me. I had to change return false to: e.preventDefault() like so:
app.directive('disallowSpaces', function() {
return {
restrict: 'A',
link: function($scope, $element) {
$element.bind('keydown', function(e) {
if (e.which === 32) {
e.preventDefault();
}
});
}
}
});
This works to prevent entering any special chars including spaces:
app.directive('noSpecialChar', function() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, element, attrs, modelCtrl) {
modelCtrl.$parsers.push(function(inputValue) {
if (inputValue == null)
return ''
let cleanInputValue = inputValue.replace(/[^\w]|_/gi, '');
if (cleanInputValue != inputValue) {
modelCtrl.$setViewValue(cleanInputValue);
modelCtrl.$render();
}
return cleanInputValue;
});
}
}
});
Use without jquery
angular.module('app').directive('disallowSpaces', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: {
maxvalue: '=',
},
link: function ($scope, $element, attr, ngModelCtrl) {
$element.bind('keydown', function () {
function transformer(text) {
if (text) {
var transformedInput = text.replace(/ /g, '');
ngModelCtrl.$setViewValue(transformedInput);
ngModelCtrl.$render();
return transformedInput;
}
return undefined;
}
ngModelCtrl.$parsers.push(transformer);
});
},
};
});
// use disallow-spaces
<input type="text" ng-model="name" disallow-spaces />
You can achieve this without writing a directive.
<input ng-model="myModel" ng-keydown="$event.keyCode != 32 ? $event:$event.preventDefault()">
For Angular 9 ,Keycode is not support.
Below code can help you for that.
keyDownHandler(event) {
if (event.code === 'Space') {
event.preventDefault();
}
}
I'm trying to understand directives, and I'm having problems with two way data binding.
My directive will be used to submit a form when "enter" is pressed in a textarea.
I found a solution in another SO thread (see the code below in the scope definition of the directive), but I don't like it because it means that if I change the model name, I need to change the directive as well..
--> Here is the problem in codepen.io
Here is the html part:
<div ng-app="testApp" ng-controller="MyController">
<textarea ng-model="foo" enter-submit="submit()"></textarea><br/>
Binding: {{foo}}
</div>
Here is the javascript part:
var app = angular.module('testApp', []);
function MyController($scope) {
$scope.foo = "bar"
$scope.submit = function() {
console.log("Submitting form");
}
}
app.directive('enterSubmit', function () {
return {
restrict: 'A',
scope: {
submitFn: '&enterSubmit',
foo: '=ngModel' // <------------------- dont't like this solution
},
link: function (scope, elem, attrs) {
elem.bind('keydown', function(event) {
var code = event.keyCode || event.which;
if (code === 13) {
if (!event.shiftKey) {
event.preventDefault();
scope.submitFn();
}
}
});
}
}
});
Thanks for your help !
When multiple directives are used on an element, normally you don't want any of them to use an isolate scope, since that forces all of them to use the isolate scope. In particular, isolate scopes should not be used with ng-model – see Can I use ng-model with isolated scope?.
I suggest creating no new scope (the default, i.e., scope: false):
app.directive('enterSubmit', function () {
return {
restrict: 'A',
//scope: {
// submitFn: '&enterSubmit',
// foo: '=ngModel' // <------------------- dont't like this solution
//},
link: function (scope, elem, attrs) {
elem.bind('keydown', function(event) {
var code = event.keyCode || event.which;
if (code === 13) {
if (!event.shiftKey) {
event.preventDefault();
scope.$apply(attrs.enterSubmit);
}
}
});
}
}
});
Having a directive in angular that is a reusable component, what is the best practice to expose a public API that can be accessed from the controller?
So when there are multiple instances of the component you can have access from the controller
angular.directive('extLabel', function {
return {
scope: {
name: '#',
configObj: '='
},
link: function(scope, iElement, iAttrs) {
// this could be and exposed method
scope.changeLabel = function(newLabel) {
scope.configObj.label = newLabel;
}
}
}
});
Then when having:
<ext-label name="extlabel1" config-obj="label1"></ext-label>
<ext-label name="extlabel2" config-obj="label2"></ext-label>
<ext-label name="extlabel3" config-obj="label3"></ext-label>
How can I get the access the scope.changeLabel of extLabel2 in a controller?
Does it make sense?
Does this work for you?
angular.directive('extLabel', function() {
return {
restrict: 'E',
scope: {
api: '='
},
link: function(scope, iElement, iAttrs) {
scope.api = {
doSomething: function() { },
doMore: function() { }
};
}
};
});
From containing parent
<ext:label api="myCoolApi"></ext:label>
And in controller
$scope.myCoolApi.doSomething();
$scope.myCoolApi.doMore();
I like Andrej's and use this pattern regularly, but I would like to suggest some changes to it
angular.directive('extLabel', function {
return {
scope: {
api: '=?',
configObj: '='
},
// A controller, and not a link function. From my understanding,
// try to use the link function for things that require post link actions
// (for example DOM manipulation on the directive)
controller: ['$scope', function($scope) {
// Assign the api just once
$scope.api = {
changeLabel: changeLabel
};
function changeLabel = function(newLabel) {
$scope.configObj.label = newLabel;
}
}]
}
});
<ext-label name="extlabel1" config-obj="label1"></ext-label>
<ext-label api="label2api" name="extlabel2" config-obj="label2"></ext-label>
<ext-label name="extlabel3" config-obj="label3"></ext-label>
In controller of course label2api.changeLabel('label')
I faced this problem when writing a directive to instantiate a dygraph chart in my Angular applications. Although most of the work can be done by data-binding, some parts of the API require access to the dygraph object itself. I solved it by $emit()ing an event:
'use strict';
angular.module('dygraphs', []);
angular.module('dygraphs').directive('mrhDygraph', function ($parse, $q) {
return {
restrict: 'A',
replace: true,
scope: {data: '=', initialOptions: '#', options: '='},
link: function (scope, element, attrs) {
var dataArrived = $q.defer();
dataArrived.promise.then(function (graphData) {
scope.graph = new Dygraph(element[0], graphData, $parse(scope.initialOptions)(scope.$parent));
return graphData.length - 1;
}).then(function(lastPoint) {
scope.graph.setSelection(lastPoint);
scope.$emit('dygraphCreated', element[0].id, scope.graph);
});
var removeInitialDataWatch = scope.$watch('data', function (newValue, oldValue, scope) {
if ((newValue !== oldValue) && (newValue.length > 0)) {
dataArrived.resolve(newValue);
removeInitialDataWatch();
scope.$watch('data', function (newValue, oldValue, scope) {
if ((newValue !== oldValue) && (newValue.length > 0)) {
var selection = scope.graph.getSelection();
if (selection > 0) {
scope.graph.clearSelection(selection);
}
scope.graph.updateOptions({'file': newValue});
if ((selection >= 0) && (selection < newValue.length)) {
scope.graph.setSelection(selection);
}
}
}, true);
scope.$watch('options', function (newValue, oldValue, scope) {
if (newValue !== undefined) {
scope.graph.updateOptions(newValue);
}
}, true);
}
}, true);
}
};
});
The parameters of the dygraphCreated event include the element id as well as the dygraph object, allowing multiple dygraphs to be used within the same scope.
In my opinion, a parent shouldn't access a children scope. How would you know which one to use and which one to not use. A controller should access his own scope or his parent scopes only. It breaks the encapsulation otherwise.
If you want to change your label, all you really need to do is change the label1/label2/label3 variable value. With the data-binding enabled, it should work. Within your directive, you can $watch it if you need some logic everytime it changes.
angular.directive('extLabel', function {
return {
scope: {
name: '#',
configObj: '='
},
link: function(scope, iElement, iAttrs) {
scope.$watch("configObj", function() {
// Do whatever you need to do when it changes
});
}
}
});
Use these directives on the element that you want to go prev and next:
<carousel>
<slide>
<button class="action" carousel-next> Next </button>
<button class="action" carousel-prev> Back </button>
</slide>
</carousel>
.directive('carouselNext', function () {
return {
restrict: 'A',
scope: {},
require: ['^carousel'],
link: function (scope, element, attrs, controllers) {
var carousel = controllers[0];
function howIsNext() {
if ((carousel.indexOfSlide(carousel.currentSlide) + 1) === carousel.slides.length) {
return 0;
} else {
return carousel.indexOfSlide(carousel.currentSlide) + 1;
}
}
element.bind('click', function () {
carousel.select(carousel.slides[howIsNext()]);
});
}
};
})
.directive('carouselPrev', function () {
return {
restrict: 'A',
scope: {},
require: ['^carousel'],
link: function (scope, element, attrs, controllers) {
var carousel = controllers[0];
function howIsPrev() {
if (carousel.indexOfSlide(carousel.currentSlide) === 0) {
return carousel.slides.length;
} else {
return carousel.indexOfSlide(carousel.currentSlide) - 1;
}
}
element.bind('click', function () {
carousel.select(carousel.slides[howIsPrev()]);
});
}
};
})