I have the following markup:
<div ng-controller="DataController as vm">
<div ng-repeat="name in vm.users track by $index">
{{name}}
</div>
<form name="form" validation="vm.errors">
<input validator ng-model="vm.name" name="vm.name" placeholder="name" type="text" />
Add
</form>
</div>
I have the following controller:
function DataController($scope) {
var vm = this;
vm.name = "Mary";
vm.users = ["Alice", "Peter"];
vm.errors = 1;
vm.add = function(name) {
vm.errors++;
vm.users.push(name);
}
}
Every time I add a user I increase the value of errors.
I need to watch this variable inside a directive so I have:
app.directive("validation", validation);
function validation() {
var validation = {
controller: ["$scope", controller],
restrict: "A",
scope: {
validation: "="
}
};
return validation;
function controller($scope) {
this.errors = $scope.validation;
}
}
app.directive("validator", validator);
function validator() {
var validator = {
link: link,
replace: false,
require: "^validation",
restrict: "A"
};
return validator;
function link(scope, element, attributes, controller) {
scope.$watch(function() {
return controller.errors;
}, function () {
console.log(controller.errors);
});
}
The console.log shows the initial value but not new values:
https://jsfiddle.net/qb8o006h/2/
If I change vm.errors to an array, add the values, and watch its length then it works fine:
https://jsfiddle.net/nprx63qa/2/
Why is my first example does not work?
I update your code, you can access to the property scope.vm.errors which is updated, if you debug the code, you will see that the property controller.errors is not updated (after each digest all the watches are called to re-evaluate them). If you access the property errors from the scope you can add the $scope.$watch and make it work. However I would not recommend to have a $scope.$watch inside a directive. But that's up to you :
var app = angular.module('app', []);
app.controller("DataController", DataController);
function DataController($scope) {
var vm = this;
vm.name = "Mary";
vm.users = ["Alice", "Peter"];
vm.errors = 1;
vm.add = function(name) {
vm.errors++;
vm.users.push(name);
}
}
app.directive("validation", validation);
function validation() {
var validation = {
controller: ["$scope", controller],
restrict: "A",
scope: {
validation: "="
}
};
return validation;
function controller($scope) {
this.errors = $scope.validation;
}
}
app.directive("validator", validator);
function validator() {
var validator = {
link: link,
replace: false,
require: "^validation",
restrict: "A"
};
return validator;
function link(scope, element, attributes, controller) {
scope.$watch(function() {
return scope.vm.errors
}, function () {
console.log(scope.vm.errors);
});
}
}
https://jsfiddle.net/kcvqn5kL/
In both of your examples inside validation directive controller you assign errors property a reference to $scope.validation value.
In the first example the value is numeric and thus immutable - 1 - the reference value cannot be modified. The vm.add modifies property value of the controller instance. The change is then propagated to validation directive $scope.validation but not to the validation directive controller instance $errors property.
In the second example the value is an array and thus mutable - [] - the reference value can be modified. The vm.add does not modify property value of the controller instance. Thus the validation directive controller instance errors property value is the very same Array instance - hence it's length changes.
One way to use a immutable value (as in your first example) is to $watch a controller function as in this example:
function link(scope, element, attributes, controller) {
scope.$watch(controller.errors, function (newValue) {
console.log(newValue);
});
}
Where controller.errors is defined as follows:
function controller($scope) {
this.errors = function(){ return $scope.validation; };
}
You can find the following answer(s) useful:
Why form undefined inside ng-include when checking $pristine or $setDirty()?
Related
I have the following (https://jsfiddle.net/f30bj43t/5/) HTML:
<div ng-controller="DataController as vm">
<div ng-repeat="name in vm.users track by $index">{{name}}</div>
<form name="form" validation="vm.errors">
<input validator ng-model="vm.name" name="vm.name" placeholder="name" type="text" />
Add
</form>
</div>
This form adds names to a list and the controller is:
app.controller("DataController", DataController);
function DataController() {
var vm = this;
vm.name = "Mary";
vm.users = ["Alice", "Peter"];
vm.errors = [];
vm.add = function(name) {
if (name == "Mary") {
var error = { property: "name", message: "name cannot be Mary"};
if (vm.errors.length == 0)
vm.errors.push(error);
} else {
vm.users.push(name);
vm.errors = [];
}
}
}
On the form I added validation="vm.errors" that defines which variable holds the errors to be used in each validator directive ...
Then in each validator directive I will use that variable to pick the correct error and display it ...
app.directive("validation", validation);
function validation() {
var validation = {
controller: ["$scope", controller],
replace: false,
restrict: "A",
scope: {
validation: "="
}
};
return validation;
function controller($scope) {
this.getErrors = function () {
return $scope.validation;
}
}
}
app.directive("validator", validator);
function validator() {
var validator = {
link: link,
replace: false,
require: "^validation",
restrict: "A"
};
return validator;
function link(scope, element, attributes, controller) {
var errors = controller.getErrors();
console.log(errors);
// do something with errors
}
}
PROBLEM
In link function of validator directive I need to track changes of the variable passed by validation="vm.errors" so I can in each validator check if a error occurs and take action.
But console.log(errors) seems to have no effect ...
How can I solve this?
You can do it with either $watch or $broadcast and $on.
I preffer to use the $on, because watching a variable consumes more than listening to an event and I'd say that to your case it's better to use $broadcast and $on, because you're not watching a scope variable, but a variable from a controller.
If you want to learn more about $on and $watch, here's a suggestion: Angular JS $watch vs $on
And here's your JSFiddle with the modifications.
Here you have a working jsfiddle: https://jsfiddle.net/f30bj43t/8/
I am logging the vm.errors variable into the console each time it changes:
function link(scope, element, attributes, controller) {
scope.$watch('vm.errors.length', function () {
if(scope.vm.errors){
console.log(scope.vm.errors);
}
});
}
This is possible due to the scope inheritance. On your case, vm is a variable added to the controller's scope, and all directives inside this scope will inherit it by default (there are ways to avoid that).
Anyhow it seems you are creating too much directives. On your case, for me would be enough having everything in the controller.
Cheers
Javier
I have created a global variable in factory. And I have accessed the global variable in my controller but upon changing the value in the directive it is unable to update in the controller.
My directive is
myApp.directive('quiz', function(quizFactory) {
return {
restrict: 'AE',
scope: {},
templateUrl: 'templete.html',
controller: function($scope){
},
link: function(scope, elem, attrs,UserService) {
scope.start = function() {
UserService.var1=true;
}
}
};
My Factory is
myApp.factory('UserService', function() {
return {
var1 : false
};
});
My controller is
myApp.controller('LearnSetQue', ['$scope','UserService',
function($scope,UserService){
$scope.var2=UserService.var1;
$scope.var3=UserService.var1;
}
]);
Here start is button function
<button ng-click="start()">Start</button>
Here upon clicking the start button the var1 should become true var1=true,var2=true and var3=true and how can I update that in the controller.
First in the service, you should return an object with the properties you want to share across your app:
myApp.factory('UserService', function() {
var properties = { var1: false };
return {
getProperties : function() { return properties; }
};
});
Then in the directive
scope.start = function() {
UserService.getProperties().var1=true;
}
And in the controller you should have:
myApp.controller('LearnSetQue', ['$scope','UserService',
function($scope,UserService){
$scope.properties = UserService.getProperties();
]);
And then on the view, just reference the var1 directly
<div>{{ properties.var1 }}</div>
I have a directive like below:
angular.module('buttonModule', []).directive('saveButton', [
function () {
function resetButton(element) {
element.removeClass('btn-primary');
}
return {
restrict: 'E',
replace: 'false',
scope: {
isSave: '='
},
template:
'<button class="btn" href="#" style="margin-right:10px;" ng-disabled="!isSave">' +
'</button>',
link: function (scope, element) {
console.log(scope.isSave);
scope.$watch('isSave', function () {
if (scope.isSave) {
resetButton(scope, element);
}
});
}
};
}
]);
and the jasmine test as below:
describe('Directives: saveButton', function() {
var scope, compile;
beforeEach(module('buttonModule'));
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
compile = $compile;
}));
function createDirective() {
var elem, compiledElem;
elem = angular.element('<save-button></save-button>');
compiledElem = compile(elem)(scope);
scope.$digest();
return compiledElem;
}
it('should set button clean', function() {
var el = createDirective();
el.scope().isSaving = true;
expect(el.hasClass('btn-primary')).toBeFalsy();
});
});
The issue is the value of isSaving is not getting reflected in the directive and hence resetButton function is never called. How do i access the directive scope in my spec and change the variable values. i tried with isolateScope but the same issue persists.
First note that you are calling the resetButton function with two arguments when it only accepts one. I fixed this in my example code. I also added the class btn-primary to the button element to make the passing of the test clearer.
Your directive is setting up two-way databinding between the outer scope and the isolated scope:
scope: {
isDirty: '=',
isSaving: '='
}
You should leverage this to modify the isSaving variable.
Add the is-saving attribute to your element:
elem = '<save-button is-saving="isSaving"></save-button>';
Then modify the isSaving property of the scope that was used when compiling (you also need to trigger the digest loop to make the watcher detect the change):
var el = createDirective();
scope.isSaving = true;
scope.$apply();
expect(el.hasClass('btn-primary')).toBeFalsy();
Demo: http://plnkr.co/edit/Fr08guUMIxTLYTY0wTW3?p=preview
If you don't want to add the is-saving attribute to your element and still want to modify the variable you need to retrieve the isolated scope:
var el = createDirective();
var isolatedScope = el.isolateScope();
isolatedScope.isSaving = true;
isolatedScope.$apply();
expect(el.hasClass('btn-primary')).toBeFalsy();
For this to work however you need to remove the two-way binding to isSaving:
scope: {
isDirty: '='
}
Otherwise it would try to bind to something non-existing as there is no is-saving attribute on the element and you would get the following error:
Expression 'undefined' used with directive 'saveButton' is
non-assignable!
(https://docs.angularjs.org/error/$compile/nonassign?p0=undefined&p1=saveButton)
Demo: http://plnkr.co/edit/Ud6nK2qYxzQMi6fXNw1t?p=preview
I want to compile a third-party api (uploadcare) to a directive.
The api will return the data info after uploaded in async then I want to do something with the return data in my controller but I have to idea how to pass the return data from directive to controller. Below is my code.
in js
link: function (scope, element, attrs) {
//var fileEl = document.getElementById('testing');
var a = function() {
var file = uploadcare.fileFrom('event', {target: fileEl});
file.done(function(fileInfo) {
//scope.$apply(attrs.directUpload)
//HERE IS MY PROBLEM.
//How can I get the fileInfo then pass and run it at attrs.directUpload
}).fail(function(error, fileInfo) {
}).progress(function(uploadInfo) {
//Show progress bar then update to node
console.log(uploadInfo);
});
};
element.bind('change', function() {a()});
}
in html
<input type="file" direct-upload="doSomething()">
in controller
$scope.doSomething = function() {alert(fileInfo)};
AngularJS allows to execute expression in $parent context with specified values, in your case doSomething().
Here's what you need to do that:
In directive definition, mark directUpload as expression:
scope: {
directUpload: "&"
}
In done callback, call:
scope.directUpload({fileInfo: fileInfo})
Update markup:
<input type="file" direct-upload="doSomething(fileInfo)">
To summorize: scope.directUpload is now a callback, which executes expression inside attribute with specifeid values. This way you can pass anything into controller's doSomething.
Read $compile docs for detailed explanation and examples.
Example you might find useful:
angular
.module("app", [])
.directive("onDone", function ($timeout) {
function link (scope, el, attr) {
$timeout(function () {
scope.onDone({
value: "something"
});
}, 3000)
}
return {
link: link,
scope: {
onDone: "&"
}
}
})
.controller("ctrl", function ($scope) {
$scope.doneValue = "nothing";
$scope.done = function (value) {
$scope.doneValue = value;
};
})
<body ng-controller="ctrl">
Waiting 3000ms
<br>
<div on-done="done(value)">
Done: {{doneValue}}
</div>
</body>
You can pass through an object to the scope of the directive using = within the directive to do two way data binding. This way you can make updates to the data within the directive on the object and it will be reflected in it's original location in the controller. In the controller you can then use $scope.watch to see when the data is changed by the directive.
Something like
http://plnkr.co/edit/gQeGzkedu5kObsmFISoH
// Code goes here
angular.module("myApp",[]).controller("MyCtrl", function($scope){
$scope.something = {value:"some string"}
}).directive("simpleDirective", function(){
return {
restrict:"E",
scope:{someData:"="},
template:"<button ng-click='changeData()'>this is something different</button>",
link: function(scope, iElem, iAttrs){
scope.changeData=function(){
scope.someData.value = "something else";
}
}
}
});
I'm trying to put together an Angular directive that will be a replacement for adding
ng-disabled="!canSave(schoolSetup)"
On a form button element where canSave is a function being defined in a controller something like the following where the parameter is the name of the form.
$scope.canSave = function(form) {
return form.$dirty && form.$valid;
};
Ideally I'd love the directive on the submit button to look like this.
can-save="schoolSetup"
Where the string is the name of the form.
So... how would you do this? This is as far as I could get...
angular.module('MyApp')
.directive('canSave', function () {
return function (scope, element, attrs) {
var form = scope.$eval(attrs.canSave);
function canSave()
{
return form.$dirty && form.$valid;;
}
attrs.$set('disabled', !canSave());
}
});
But this obviously doesn't bind properly to the form model and only works on initialisation. Is there anyway to bind the ng-disabled directive from within this directive or is that the wrong approach too?
angular.module('MyApp')
.directive('canSave', function () {
return function (scope, element, attrs) {
var form = scope.$eval(attrs.canSave);
scope.$watch(function() {
return form.$dirty && form.$valid;
}, function(value) {
value = !!value;
attrs.$set('disabled', !value);
});
}
});
Plunker: http://plnkr.co/edit/0SyK8M
You can pass the function call to the directive like this
function Ctrl($scope) {
$scope.canSave = function () {
return form.$dirty && form.$valid;
};
}
app.directive('canSave', function () {
return {
scope: {
canSave: '&'
},
link: function (scope, element, attrs) {
attrs.$set('disabled', !scope.canSave());
}
}
});
This is the template
<div ng-app="myApp" ng-controller="Ctrl">
<div can-save="canSave()">test</div>
</div>
You can see the function is called from the directive. Demo