setting scope to null blocks 2 way binding angular directive - angularjs

I have created a directive and I believe that two-way bind is being broken when I set a bound scope variable (textStyleOriginal) to null. What is a good way to resolve this issue?
.directive('textStylePalette', function($log, toastr, _){
return {
restrict: 'E',
templateUrl: 'app/palettes/text/text-style-palette.html',
scope: {
textStyleOriginal: '=textStyle'
},
link: textPaletteLinkFn
};
function textPaletteLinkFn(scope, elem, attr) {
scope._ = _;
scope.textStyle = null;
// Used when closing the palette
scope.deselectStyle = function() {
// I BELIEVE THE PROBLEM IS THE NEXT LINE
scope.textStyleOriginal = null;
scope.textStyle = null;
};
...
// THIS WATCH STOPS WORKING.
scope.$watch('textStyleOriginal', function(newVal, oldVal){
$log.debug('n: ' + newVal + '|o: ' + oldVal );
debugger;
if (newVal && newVal !== oldVal) {
...
}
});
}
The html where the binding is initially connected is as follows:
<text-style-palette ng-show="selectedStyle !== null" text-style="selectedStyle">
</text-style-palette>

I think I know what's the problem.
Since you have an isolated scope, you'll have the textStyleOriginal set from the parent scope. It means that if you override it with the value null, then you'll loose the reference to the original object.
E.g. even when you modify your textStyleOriginal in your parent scope, it won't take any effect in your directive, since you lost the reference to it already.

A few minutes after I asked the question I tried something that seemed to work. Leaving this question up to document my answer:
It was basically the simple, 'always make passed scope variables as part of an object'.
I made some changes so that the external selectedStyle that was feeding the directives was part of an object. here's the code
<cm-text-style-palette ng-show="selections.selectedStyle !== null" text-style="selections.selectedStyle">
</cm-text-style-palette>
Notice that it's selections.selectedStyle not just selectedStyle.
The issue has to do with how variable pointing works. For more details this video might help: https://egghead.io/lessons/angularjs-the-dot#/tab-transcript
Best of luck with your projects!

Related

Directive not working with ng-if

I have the following directive that works correctly unless i use a ng-if on the containing element as well. What I am trying to do is simply set a dynamic property on the scope with an associated function. The scope does not contain the property (undefined).
any help would be great!
app.directive('stRefresh', function () {
return {
require: '^stTable',
link: function (scope, ele, attr, ctrl) {
if (attr.stRefresh) {
var name = attr.stRefresh;
scope[name] = scope[name] || {};
scope[name].refresh = function () {
return ctrl.search();
};
}
}
}
});
It would be helpful to know what doesn't work, but I suspect that you have been bitten by the problem of nested scopes.
The ng-if directive creates a child scope. When you modify the scope in your directive:
scope[name] = scope[name] || {};
You are modifying the child scope. Due to prototypal inheritance, this does not modify the scope of the parent. Therefore, if you are expecting to see a change in the parent scope, you won't see it.
To fix it, you need to add a dot to the scope reference. Something like this:
scope.data[name] = scope.data[name] || {};
scope.data[name].refresh = function () {
return ctrl.search();
};
The scope.data property needs to be a reference to an object. Since you are not modifying the reference, you can modify the properties of that object from both parent and child scopes.

How to update a form controller in AngularJS after changing input constraints

I have been recently working on dynamically changing constraints on input fields on a form. The aim was to make a back-end-driven form, whereas all fields and their constraints are being sent to us by a server. Whilst I managed to get the constraints to be added/removed as we please by creating a simple directive, it seems like a form controller is not picking up these changes, and so the form is always $valid. Have a look at this jsfiddle, here is the directive:
myApp.directive('uiConstraints', [function(){
function applyConstraints(element, newVal, oldVal){
//remove old constraints
if(oldVal !== undefined && oldVal !== null){
for (var i = 0; i < oldVal.length; i++) {
element.removeAttr(oldVal[i].key);
}
}
//apply new constraints
if(newVal !== undefined && newVal !== null){
for (var i = 0; i < newVal.length; i++) {
var constraint = newVal[i];
element.attr(constraint.key, constraint.value);
}
}
}
function link(scope, element, attrs){
scope.$watch(attrs.uiConstraints, function(newVal, oldVal){
applyConstraints(element, newVal, oldVal);
});
}
return {
restrict : 'A',
link : link
};
}]);
The required behavior is so it works like on the official angularjs plunker. However, it seems the FormController is being created before the directive populates constraints on the input fields, and updating these constraints doesn't update the corresponding values in the FormController.
Does any1 know if I can force the FormController to pickup the changes to constraints made by the directive? And if so, how? I have no idea where to even start... Thanks.
-- EDIT --
I couldn't get plunker to work (show to others my latest changes) so here is jsfiddle of what I have: latest
To more in detail describe the issue:
go to the jsfiddle described
if you remove the initial value from the textbox, it will become red (invalid), however the controller won't pick that up and will still show:
myform.$valid = true
myform.myfield.$valid = true
-- EDIT --
The bounty description doesn't recognize Stack Overflow formatting (like 2 spaces for new line etc) so here it is in more readable form:
Since this is still unsolved and interesting question I decided to start a bounty.
The requirements are:
- works on ancient AngularJS(1.0.3) and newer (if it can't be done on 1.0.3 but someone did it on newer version of angular I will award bounty)
- initially a field has no constraints on it (is not required, max and min not set etc)
- at any time constraints for the field can change (it can become required, or a pattern for the value is set etc), as well as any existing constraints can be removed
- all constraints are stored in a controller in an object or array
- FormController picks up the changes, so that any $scope.FormName.$valid is being changed appropriately when constraints
on any fields in that form change
A good starting point is my jsfiddle.
Thanks for your time and good luck!
Check out this PLUNK
.directive('uiConstraints', ["$compile",
function($compile) {
function applyConstraints(element, newVal, oldVal) {
//apply new constraints
if (newVal !== undefined && newVal !== null) {
for (var i = 0; i < newVal.length; i++) {
var constraint = newVal[i];
element.attr(constraint.key, constraint.value);
}
}
}
return {
restrict: 'A',
terminal: true,
priority: 1000,
require: "^form",
link: function(scope, element, attrs, formController) {
var templateElement;
var previousTemplate;
templateElement = element.clone(); //get the template element and store it
templateElement.removeAttr("ui-constraints");// remove the directive so that the next compile does not run this directive again
previousTemplate = element;
scope.$watch(attrs.uiConstraints, function(newVal, oldVal) {
var clonedTemplate = templateElement.clone();
applyConstraints(clonedTemplate, newVal);
clonedTemplate.insertBefore(previousTemplate);
var control = formController[previousTemplate.attr("name")];
if (control){
formController.$removeControl(control);
}
if (previousTemplate) {
previousTemplate.remove();
}
$compile(clonedTemplate)(scope);
previousTemplate = clonedTemplate;
});
}
};
}]);
The idea here is to set terminal: true and priority: 1000 to let our directive be compiled first and skip all other directives on the same element in order to get the template element. In case you need to understand more, check out my answer: Add directives from directive in AngularJS.
After getting the template element, I remove the ui-constraints directive to avoid this directive getting compiled again and again which would add a $watch to the digest cycle every time we toggle the constraints.
Whenever the constraints change, I use this template element to build a new element containing all the constraints without the ui-constraints directive and compile it. Then I remove the previous element from the DOM and its controller from the Form Controller to avoid leaking and problems caused by previous element's controller existing in the Form Controller.
For starters, AngularJS has ng-required directive that allows you to toggle required validation
<input type="text" data-ng-required="isRequired"
data-ng-model="mydata" name="myfield" />
see the Fiddle:
Fiddle
Making Min and Max logic dynamic is a little more complex. Your current approach JQuery-like in alter elements which is NOT AngularJS strength. The ideal Answer is a directive like ng-minlnegth with more intelligence.
I'll look into that.
Quick and dirty solution:
scope.$watch(attrs.uiConstraints, function(newVal, oldVal){
applyConstraints(element, newVal, oldVal);
if (newVal != oldVal) {
element = $compile(element)(scope);
}
});
I don't like solution and it's not perfect. This is an interesting problem, so I'm still working on finding a more elegant solution.
Here is an example.
What went wrong for you was that you tried to use scope in your apply thing, and you were watching the wrong thing.
I changed your link function:
link : function(scope, element, attrs){
scope.$watch(attrs.constraints, function(newConstraints, oldConstraints){
applyConstraints(newConstraints, oldConstraints, element);
}, true);
}
And your toggle function now does some random attributes and stuff:
$scope.toggleConstraints = function(myconstraints){
var getRandomPropOrValueName = function(){
return Math.random().toString(36).substring(7);
}
$scope.myconstraints = [{key: getRandomPropOrValueName(), value: getRandomPropOrValueName() }];
};

AngularJS : Initializing isolated scope inside a directive

I have created a directive that accepts some attributes and initializes the isolated scope with these attributes. If an attribute isn't specified, then the isolated scope should be initialized with a calculated value.
I added a link function that inspects the scope and initializes the default values (if no value has been set using the attributes). The scope has been initialized, but if I set a default value then it will be overwritten later by the framework.
A workaround is to use $timeout(...) and set it afterwards, but this seems too much of a hack.
function ($timeout) {
return {
scope: { msg1: '#', msg2: '#' },
template: '<div>{{msg1}} {{msg2}} {{msg3}}</div>',
link: function ($scope, $elt, $attr) {
var action = function() {
if (!$scope.msg2) $scope.msg1 = 'msg1';
if (!$scope.msg2) $scope.msg2 = 'msg2';
if (!$scope.msg3) $scope.msg3 = 'msg3';
};
action();
//$timeout(action, 0);
}
};
});
I have prepared a JSFiddle to illustrate what is happening.
msg1 is initialized via the attribute and has the correct value at all times.
msg2 is not initialized via an attribute, but can be set using an attribute. This value is overwritten after the link method has been called.
msg3 is not initialized via an attribute, and this isn't even possible. This value is set when constructing the controller and works fine.
It seems that AngularJS creates the scope and updates its value after the controller is created and the directive is linked into the DOM. Can anyone tell me the recommended way to do this?
You have to operate on the attributes themselves if you want to set defaults for '#' type binding. Read about $compile
You can do it in the compile function:
compile: function(element, attrs) {
if (!attrs.msg1) attrs.msg1 = 'msg1';
if (!attrs.msg2) attrs.msg2 = 'msg2';
}
http://jsfiddle.net/5kUQs/9/
OR you can use the link function as well.
link: function ($scope, $elt, attrs) {
var action = function() {
console.log('msg1:' + $scope.msg1 + ', msg2:' + $scope.msg2 + ', msg3: ' + $scope.msg3);
if (!attrs.msg1) attrs.msg1 = 'msg1';
if (!attrs.msg2) attrs.msg2 = 'msg2';
if (!attrs.msg3) attrs.msg3 = 'msg3';
};
action();
}
http://jsfiddle.net/5kUQs/13/
The reason for this is that there is a binding with the attribute setup which overrides your changes to that scope variable. We need to modify the attribute that the value is being taken from.
# or #attr - bind a local scope property to the value of DOM
attribute. The result is always a string since DOM attributes are
strings
You can try initializing your $scope attributes in the controller of the directive, rather than the linking function. When the controller is initialized, the scope should already be set.
I know this is an old one but I came across it looking for an answer and while I didn't get it, I did update your fiddle to get this example working.
function MyController($scope){
var c = this;
c.msg1 = $scope.msg1;
c.msg2 = $scope.msg2;
var action = function() {
console.log('msg1:' + $scope.msg1 + ', msg2:' + $scope.msg2 + ', msg3: ' + $scope.msg3);
if (!c.msg2) c.msg1 = 'msg1';
if (!c.msg2) c.msg2 = 'msg2';
if (!c.msg3) c.msg3 = 'msg3';
};
action();
};

AngularJS: Parent scope is not updated in directive (with isolated scope) two way binding

I have a directive with isolated scope with a value with two way binding to the parent scope. I am calling a method that changes the value in the parent scope, but the change is not applied in my directive.(two way binding is not triggered). This question is very similar:
AngularJS: Parent scope not updated in directive (with isolated scope) two way binding
but I am not changing the value from the directive, but changing it only in the parent scope. I read the solution and in point five it is said:
The watch() created by the isolated scope checks whether it's value for the bi-directional binding is in sync with the parent's value. If it isn't the parent's value is copied to the isolated scope.
Which means that when my parent value is changed to 2, a watch is triggered. It checks whether parent value and directive value are the same - and if not it copies to directive value. Ok but my directive value is still 1 ... What am I missing ?
html :
<div data-ng-app="testApp">
<div data-ng-controller="testCtrl">
<strong>{{myValue}}</strong>
<span data-test-directive data-parent-item="myValue"
data-parent-update="update()"></span>
</div>
</div>
js:
var testApp = angular.module('testApp', []);
testApp.directive('testDirective', function ($timeout) {
return {
scope: {
key: '=parentItem',
parentUpdate: '&'
},
replace: true,
template:
'<button data-ng-click="lock()">Lock</button>' +
'</div>',
controller: function ($scope, $element, $attrs) {
$scope.lock = function () {
console.log('directive :', $scope.key);
$scope.parentUpdate();
//$timeout($scope.parentUpdate); // would work.
// expecting the value to be 2, but it is 1
console.log('directive :', $scope.key);
};
}
};
});
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
console.log('CTRL:', $scope.myValue);
};
});
Fiddle
Use $scope.$apply() after changing the $scope.myValue in your controller like:
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
$scope.$apply();
console.log('CTRL:', $scope.myValue);
};
});
The answer Use $scope.$apply() is completely incorrect.
The only way that I have seen to update the scope in your directive is like this:
angular.module('app')
.directive('navbar', function () {
return {
templateUrl: '../../views/navbar.html',
replace: 'true',
restrict: 'E',
scope: {
email: '='
},
link: function (scope, elem, attrs) {
scope.$on('userLoggedIn', function (event, args) {
scope.email = args.email;
});
scope.$on('userLoggedOut', function (event) {
scope.email = false;
console.log(newValue);
});
}
}
});
and emitting your events in the controller like this:
$rootScope.$broadcast('userLoggedIn', user);
This feels like such a hack I hope the angular gurus can see this post and provide a better answer, but as it is the accepted answer does not even work and just gives the error $digest already in progress
Using $apply() like the accepted answer can cause all sorts of bugs and potential performance hits as well. Settings up broadcasts and whatnot is a lot of work for this. I found the simple workaround just to use the standard timeout to trigger the event in the next cycle (which will be immediately because of the timeout). Surround the parentUpdate() call like so:
$timeout(function() {
$scope.parentUpdate();
});
Works perfectly for me. (note: 0ms is the default timeout time when not specified)
One thing most people forget is that you can't just declare an isolated scope with the object notation and expect parent scope properties to be bound. These bindings only work if attributes have been declared through which the binding 'magic' works. See for more information:
https://umur.io/angularjs-directives-using-isolated-scope-with-attributes/
Instead of using $scope.$apply(), try using $scope.$applyAsync();

AngularJS - Calling function in directive when a 2-way bound value changes

I've got a directive all setup with 2-way data binding on the attributes using = and I can see everything is working well with that. Now I'm stuck at the need to call a function within the directive whenever one of my bound attributes changes in the parent scope, and I can't figure out how to pull that off.
I'm basically creating a version of the ui checkbox button that works with arrays of objects. You pass the directive an allowed array (which contains all the different options) and an applied array (which contains the same objects from allowed). For checking if an object is in the allowed array I have another array that is the just the id properties. Within the directive this is working great, but if the applied array changes outside of the directive the id array never gets updated.
The Directive:
angular.module('MyApp',[])
.directive('btnCheckboxGroup', function(){
return {
restrict: 'E',
// controller: DirCtrl,
templateUrl: 'btnCheckboxGroup.html',
scope: {
allowed: '=',
applied: '=',
id: '=',
title: '='
},
link: function(scope, elem, attrs){
scope.abp = [];
// this works right away, but how do I run it when the parent scope updates it?
angular.forEach(scope.applied, function(obj){
scope.abp.push( obj[scope.id] );
});
scope.addRemove = function(a){
var index = scope.abp.indexOf(a[scope.id]);
// doesn't exist, add it
if(index === -1){
scope.abp.push(a[scope.id]);
scope.applied.push(a);
// does exist, remove it
} else {
scope.abp.splice(index, 1);
for(var i in scope.applied){
if(scope.applied[i][scope.id]==a[scope.id]){
scope.applied.splice(i,1);
break;
}
}
}
}// end addRemove()
}
};
});
JSFiddle
I've tried lots of variations of things like scope.$watch, attrs.$observe, and attempted at one point to try one-way data-binding with # and that made lots of things crash.
So whats the magic I'm missing here?
You can pass a third parameter to $watch to change the way it compares the old and the new value. See the Angular docs
$watch(watchExpression, [listener], [objectEquality]);
If you set this to true, it will pick up changes in the content of the array and not only when the array reference changes. This does have a performance impact (depending on the length of the array). Checking only the length of the array does not cover the case where the number of elements stay the same but the elements themselves do change.
In your case you would need to do something like this:
scope.$watch(
"applied",
function() {
scope.abp = [];
angular.forEach(scope.applied, function(obj){
scope.abp.push( obj[scope.id] );
});
},
true);
Is this what you're looking for?
scope.$watch(function() {
return scope.applied.length;
}, function(val) {
console.log(val);
console.log(scope.applied);
});
The array on scope doesn't change but its length does, so if you were using the string-variant of $watch it won't fire, but using a function and looking at the length of the array will. More on this in the docs

Resources