I would like to set a property (blurred in this case) on an input element, just like AngularJs sets $invalid, $valid etc. on elements.
Is this possible from a directive, and if so, how?
Angular set's properties on the element's controller, not on the element itself.
Controls (e.g. inputs, selects etc) have an NgModelController, while forms/ngForms have a FormController.
The NgModelController is responsible for informing the parent FormController (if any) about changes in properties like $invalid.
The directives that use NgModelController (e.g. input, select etc) are responsible for determining when a value is valid or not and call the appropriate methods of their NgModelController.
For something as simple as setting a property on the controller, you need a directive that gets access to the controller instance (by require-ing it) and you are good to go:
.directive('blurMonitor', function () {
return {
restrict: 'A',
require: 'ngModel', // to get access to the controller
link: function postLink(scope, elem, attrs, ctrl) { // ctrl===NgModelController
/* Initialize the property */
ctrl.$blurred = true;
/* Update the property value on focus */
elem.on('focus', function () {
/* Since JS events fire asynchronously and outside
* of the "Angular context" we need to call `$apply()` */
scope.$apply(function () {
ctrl.$blurred = false;
});
});
/* Update the property value on blur */
elem.on('blur', function () {
/* Since JS events fire asynchronously and outside
* of the "Angular context" we need to call `$apply()` */
scope.$apply(function () {
ctrl.$blurred = true;
});
});
}
};
});
<form name="form1">
<input type="text" name="text1" ng-model="something" blur-monitor />
<p>Blurred: {{form1.text1.$blurred}}</p>
</form>
See, also, this short demo.
Related
I'm usin a directive to show a div on the screen only when the screen size is smaller than 600px. The problem is, the scope value isn't being updated, even using $apply() inside the directive.
This is the code:
function showBlock($window,$timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.isBlock = false;
checkScreen();
function checkScreen() {
var wid = $window.innerWidth;
if (wid <= 600) {
if(!scope.isBlock) {
$timeout(function() {
scope.isBlock = true;
scope.$apply();
}, 100);
};
} else if (wid > 600) {
if(scope.isBlock) {
$timeout(function() {
scope.isBlock = false;
scope.$apply();
}, 100);
};
};
};
angular.element($window).bind('resize', function(){
checkScreen();
});
}
};
}
html:
<div ng-if="isBlock" show-block>
//..conent to show
</div>
<div ng-if="!isBlock" show-block>
//..other conent to show
</div>
Note: If I don't use $timeout I'll get the error
$digest already in progress
I used console logs inside to check if it's updating the value, and inside the directive everything works fine. But the changes doesn't go to the view. The block doesn't show.
You should use do rule in such cases to get the advantage of Prototypal Inheritance of AngularJS.
Basically you need to create a object, that will will have various property. Like in your case you could have $scope.model = {} and then place isBlock property inside it. So that when you are inside your directive, you will get access to parent scope. The reason behind it is, you are having scope: true, which says that the which has been created in directive is prototypically inherited from parent scope. That means all the reference type objects are available in your child scope.
Markup
<div ng-if="model.isBlock" show-block>
//..conent to show
</div>
<div ng-if="!model.isBlock" show-block>
//..other conent to show
</div>
Controller
app.controller('myCtrl', function($scope){
//your controller code here
//here you can have object defined here so that it can have properties in it
//and child scope will get access to it.
$scope.model = {}; //this is must to use dot rule,
//instead of toggle property here you could do it from directive too
$scope.isBlock = false; //just for demonstration purpose
});
and then inside your directive you should use scope.model.isBlock instead of scope.isBlock
Update
As you are using controllerAs pattern inside your code, you need to use scope.ag.model.isBlock. which will provide you an access to get that scope variable value inside your directive.
Basically you can get the parent controller value(used controllerAs pattern) make available controller value inside the child one. You can find object with your controller alias inside the $scope. Like here you have created ag as controller alias, so you need to do scope.ag.model to get the model value inside directive link function.
NOTE
You don't need to use $apply with $timeout, which may throw an error $apply in progress, so $timeout will run digest for you, you don't need to worry about to run digest.
Demo Here
I suspect it has something to do with the fact that the show-block directive wouldn't be fired if ng-if="isBlock" is never true, so it would never register the resize event.
In my experience linear code never works well with dynamic DOM properties such as window sizing. With code that is looking for screens size you need to put that in some sort of event / DOM observer e.g. in angular I'd use a $watch to observe the the dimensions. So to fix this you need to place you code in a $watch e.g below. I have not tested this code, just directional. You can watch $window.innerWidth or you can watch $element e.g. body depending on your objective. I say this as screens will be all over the place but if you control a DOM element, such as, body you have better control. also I've not use $timeout for brevity sake.
// watch window width
showBlock.$inject = ['$window'];
function bodyOverflow($window) {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($window.innerWidth, function (newWidth, oldWidth) {
if (newWidth !== oldWidth) {
return isBlock = newWidth <= 600;
}
})
}
};
}
// OR watch element width
showBlock.$inject = [];
function bodyOverflow() {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($element, function (new, old) {
if (newWidth) {
return isBlock = newWidth[0].offsetWidth <= 600;
}
})
}
};
}
I have created directive myInput that require ngModel controller and then alert ngModelCtrl.$viewValue in input-event trigger catch when I stoke the key. I get different result between IE and Chrome/Firefox
As follow:
http://jsfiddle.net/southbridge/zgyv14g0/
In IE It displays previous value of Input before I've stroked the keyboard ,but in Chrome/Firefox It displays current value .
app.directive('myInput',function(){
return {
restrict:'A',
require:'ngModel',
link:function(scope,element,attrs,ngModelCtrl){
element.on('input',function(){
var x=ngModelCtrl.$viewValue;
alert(x);
});
}
}
});
I am not sure if the input event will trigger after the $viewvalue property has been updated by angular (most possibly not), because angular updates viewvalue property by listening to input/change etc.. events itself.
However one more criteria is the priority of the directive (if you are angular 1.3 and less then provide your directive a priority greater than ngModels i.e ex:1, priority of ngModel has been revised to 1 starting 1.3) and which handler runs first. But if you really just need the value of the input element you could just try accessing it with value property inside the handler i.e this.value
However if you really need the current $viewvalue best bet would be to use $viewChangeListeners of the ngModelController.
.directive('myInput',function(){
return {
restrict:'A',
require:'ngModel',
priority: 1, //if required
link:function(scope,element,attrs,ngModelCtrl){
ngModelCtrl.$viewChangeListeners.push(handleViewValueChange);
function handleViewValueChange(){
alert(ngModelCtrl.$viewValue);
}
//element.on('input', handleViewValueChange); //Or just use this.value in the handler
}
}
});
Array of functions to execute whenever the view value has changed. It is called with no arguments, and its return value is ignored. This can be used in place of additional $watches against the model value.
angular.module('app', []).controller('ctrl', function($scope) {
}).directive('myInput', function() {
return {
restrict: 'A',
require: 'ngModel',
priority: 1, //If needed
link: function(scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$viewChangeListeners.push(handleViewValueChange);
function handleViewValueChange() {
var x = ngModelCtrl.$viewValue;
alert(x);
}
element.on('input', function() {
alert(ngModelCtrl.$viewValue); //or hust this.value
});
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.3/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<input my-input ng-model="model" />{{model}}
</div>
I'm trying to create a server-validate directive, which asynchronously validates form input by submitting a partial form to our server back-end and parsing the response. I hoped to use it like this:
<input type="text" ng-model="stuff" server-validate />
(I'm combining it with another directive on the wrapping <form>, that specifies what URL to use etc...) In order for the form not to submit validation requests on page load, I need to set ng-model-options="{ updateOn: 'blur' }", but I'd like to *not* have to do this on every element in the form. Instead, I'd like theserver-validate` to specify this behavior as well.
I've tried a couple of things in the link function, for example attrs['ngModelOptions'] = '{updateOn: "blur"}' and attrs['ngModelOptions'] = { updateOn: 'blur' }, but neither had any effect at all.
Is there a way to apply this through my own directive, without having to specify anything else?
They have a great example of what you want over at the docs:
directive('contenteditable', ['$sce', function($sce) {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$evalAsync(read);
});
read(); // initialize
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
}]);
So the only thing that looks like what you would change would be to remove the keyup and change events from the element.on. Then in your blur you would also do the server request. Docs on ngModelController: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
Using a directive focus-me="inTextModeInput" in a text input
app.directive('focusMe', function($timeout) {
/*focuses on input
<input type="text" focus-me="focusInput">
*/
return {
scope: { trigger: '=focusMe' },
link: function(scope, element) {
scope.$watch('trigger', function(value) {
if(value === true) {
$timeout(function() {
element[0].focus();
scope.trigger = false;
});
}
});
}
};
});
Actually having 2 inputs, both uses focus-me
When i programatically set the value to focus on an input the ng-blur of other is not called.
NOTE : i am also using this in an ng-repeat.
Isolated scope
The blur is called, but you're not seeing that because you've created a directive with an isolated scope. The ng-blur is executed on the $parent scope. You should only use an isolated scope when the directive is implementing re-useable templates.
Two way binding on trigger
The line 'scope.trigger = false' is also setting a different boolean value because it's on a different scope. If you want to assign a value to a variable from a directive you should always wrap the value inside another object: var focus = { me: true } and set it like trigger=focus.me.
A better solution
But I wouldn't set the trigger to false at all. AngularJS is a MVC/MVVM based framework which has a model state for the user interface. This state should be idempotent; meaning that if you store the current state, reload the page and restore the state the user interface should be in the exact same situation as before.
So what you probably need is a directive that
Has no isolated scope (which allows all other directives to work: ng-blur, ng-focus, ...)
Keeps track of a boolean, which indicates the focus state
Sets this boolean to false when the element has lost focus
It's probably easier to see this thing in action: working plunker.
Maybe this (other) plunker will give you some more insight on scopes and directives.
Code
myApp.directive('myFocus', function($parse, $timeout) {
return {
restrict: 'A',
link: function myFocusLink($scope, $element, $attrs, ctrls) {
var e = $element[0];
// Grab a parser from the provided expression so we can
// read and assign a value to it.
var getModel = $parse($attrs.myFocus);
var setModel = getModel.assign;
// Watch the parser -- and focus if true or blur otherwise.
$scope.$watch(getModel, function(value) {
if(value) {
e.focus();
} else {
e.blur();
}
});
function onBlur() {
$timeout(function() {
setModel($scope, false);
});
}
function onFocus() {
$timeout(function() {
setModel($scope, true);
});
}
$element.on('focus', onFocus);
$element.on('blur', onBlur);
// Cleanup event registration if the scope is destroyed
$scope.$on('$destroy', function() {
$element.off('focus', onFocus);
$element.off('blur', onBlur);
});
}
};
});
I am attemping to add the required directive to an element, at some point in the future.
In the example, its if the model field is dirty, then make the element required.
I have attempted to just set the required attribute (being a little optimistic)
I am now compiling and linking the element and attempting to replace the old elemenet with the new element.
My element just disappears from the page?
Am I going about this the right way?
app.directive('requiredIfDirty', function ($compile, $timeout) {
return {
restrict: "A",
require: // element must have ng-model attribute.
'ngModel',
link: // scope = the parent scope
// elem = the element the directive is on
// attr = a dictionary of attributes on the element
// ctrl = the controller for ngModel.
function (scope, elem, attr, ctrl) {
var unsubscribe = scope.$watch(attr.ngModel, function (oldValue, newValue) {
if(angular.isUndefined(oldValue)) {
return;
}
attr.$set("required", true);
$timeout(function () {
var newElement = $compile(elem)(scope);
elem.replaceWith(newElement);
}, 1);
unsubscribe();
});
}
};
});
You would have to use Transclusion in your directive. This would allow you to yank your content, append required to it and then compile that. This is a great tutorial that explains the basic concept: Egghead.io - AngularJS - Transclusion Basics
You dont actually need to do that. Angular actually has a directive ng-required
see
http://docs.angularjs.org/api/ng.directive:input.text
You can provide an expression into ng-required on any field that has ng-model and it will add the required validator to it based on the expression evaluating to true.
From the docs
ngRequired(optional) – {string=} – Adds required attribute and
required validation constraint to the element when the ngRequired
expression evaluates to true. Use ngRequired instead of required when
you want to data-bind to the required attribute.