Angular - Directive reflecting ngModel array - angularjs

This is piggy-backing a little off an earlier question. I have been trying to work with directives and data from a model/array
My model looks something like this:
$scope.testModel = {
inputA:[1,2,3]
};
Then I would have inputs for each.
My directive is checking, on keyup, if the input is greater than a given number (10). If it is, then it sets it as 10.
link: function(scope, element) {
scope.$watch('ngModel',function(val){
element.val(scope.ngModel);
});
element.bind("keyup", function(event) {
if(parseInt(element.val())>10){
element.val(10);
scope.ngModel=element.val();
scope.$apply();
}
});
}
The problem is that I get an error, depending on the input:
TypeError: Cannot set property '0' of undefined
Here is the fiddle to see the error and code I have set up: https://jsfiddle.net/prXm3/3/
NOTE
I would prefer not to change the data set as I receive it directly from the server. I know I can change the model to inputA0:1,inputA1:2,inputA2:3 and get it to work, but then I would have to transform the data when my app gets it, and then re-transform it when I send to back to the server. I would prefer to leave the model as I have it set.

Since your directive is interacting with ngModel, you should work along with it in order to update both the model and the view:
angular.module('test', []).directive('myDirective', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function(viewValue) {
if(parseInt(viewValue) > 10) {
ngModel.$setViewValue(10);
ngModel.$render();
return 10;
}
else
return viewValue;
});
}
}
});
Working jsFiddle.
Perhaps you'd be interested in checking out the following posts for further information:
NgModelController
Developer's Guide on Forms

Related

Dynamically added directives call formatter instead of parser

I have an usecase where I need to dynamically add directives to an input field, depending on the configuration set in a DB.
It all seemed to work fine, but there were some strange quirks with these input fields.
I discovered that the strange behaviour is caused by the directives calling the formatters when I expect them to call the parsers.
I made a plunker to demonstrate this behaviour.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.test1 = 'World1';
$scope.test2 = 'World2';
});
app.directive('test', ['$log', function($log) {
return {
require : 'ngModel',
link : function(scope, elm, attrs, ctrl) {
function parse(viewValue) {
console.log('parsing', viewValue);
return viewValue;
}
function format(viewValue) {
console.log('formatting', viewValue);
return viewValue;
}
ctrl.$formatters.unshift(format);
ctrl.$parsers.unshift(parse);
}
};
}]);
app.directive('variabele', ['$compile', function($compile) {
return {
restrict : 'E',
template : '<div><input ng-model="ngModel" /></div>',
scope : {
ngModel : '='
},
require: ['ngModel'],
link: function(scope, elm, attrs, ctrl) {
console.log('testing');
var input = angular.element(elm.find("input"));
input.attr('test', '');
$compile(input)(scope);
}
};
}]);
plunker
It's a bit simplified from what I have to illustrate the problem. There are two input fields. One of which always has the test directive. The other has the variable directive which in turn adds the test directive dynamically.
In reality one or more directives are added which are defined in the database.
When you change the value of the first input field you can see in tghe console that the parser is called, but when you change the value of the second input field you see that the formatter is being called instead. I'm not sure what I'm doing wrong here.
EDIT: The original plunker was broken, so i fixed it. They now use a different model for each input field and the second input field correctly uses the variabele directive.
It is the expected behaviour,
Formatters change how model values will appear in the view.
Parsers change how view values will be saved in the model.
In your case, you bind the same value in both directive test and variabele. when you change value in test directive parsers are called ( view -> model) and in variabele it is the other way (model -> view) formatters are called.
for more info: How to do two-way filtering in angular.js?

Toggle edit and display of the fields in a form

Above all, I have the plnkr at here.
I am trying to create a series of directive that support in-place toggle of text display and edit within a form. As I understand, there is a similar module like xeditable available, but we need to do something different down the road. So I started with an experiment to start with something similar.
First, I create a directive that allows toggling edit/display by setting an attribute editEnabled on the directive called editableForm. The following code does not do anything special other than a line of log message.
function editableForm ($log) {
var directive = {
link: link,
require: ['form'],
restrict: 'A',
scope: {
editEnabled: "&editEnabled"
}
};
return directive;
function link(scope, element, attrs, controller) {
//$log.info('editEnabled: ' + scope.editEnabled());
$log.info('editEnabled: ' + attrs.editEnabled); //this also works
}
} //editableForm
Then I wrote the following directive to override the input tag in html:
//input directive
function input($log) {
var directive = {
link: link,
priority: -1000,
require: ['^?editableForm', '?ngModel'],
restrict: 'E'
};
return directive;
function link(scope, element, attrs, ngModel) {
ngModel.$render = function() {
if (!ngModel.$viewValue || !ngModel.$viewValue) {
return;
}
element.text(ngModel.$viewValue);
};
$log.info('hello from input');
$log.info('input ngModel: ' + attrs.ngModel);
// element.val('Hello');
scope.$apply(function() {
ngModel.$setViewValue('hello');
ngModel.$render();
});
}
} //input
I was trying to show the ngModel value of the input as text in the input directive, however, it doesn't seem to do anything in my testing. Could someone spot where I am doing wrong? I wish to replace each input fields with text/html (e.g. <span>JohnDoe</span> for Username).
My first attempt on input is a proof of concept. If it works, I will keep working on other tags like button, select, etc.
Long shot here... Your requiring both editableForm and ngModel in your input directive. So the fourth parameter of your link function should be an array of controllers in the respective order of the require array, not the ngModel controller as you are expecting.
I didnt go any further in examining your code, but check it out.

Adding ngDisabled inside directive

I'm trying to "overload" all inputs in my application. And in doing so, I'd also like to have ngDisabled used based upon a flag in the directive's scope.
Here's what I got and where I'm stuck is getting the ng-disabled to work on the element. I'm guessing I need to re-compile the element or something after I modify it? I'm also calling the directive by using the object notation:
angular.module("MimosaApp").directive({
"textarea": appInputs,
"input": appInputs
});
var appInputs = function($compile, Device) {
return {
restrict: 'E',
require: '?ngModel',
priority: 101,
template: function(tElement, tAttrs) {
tElement.attr("ng-disabled", 'isDisabled');
return tElement;
},
link: function($scope, element, attrs) {
$compile(element);
element.on("focus", function() {
console.log($scope);
})
},
controller: function($scope, $element) {
$scope.isDisabled = true;
console.log($scope);
}
}
};
What I'm seeing is... nothing is disabled even though I set isDisabled to true in the scope. What am I missing?
Update 1
Ok, so maybe I do need to clarify it a bit. When a user interacts with an input of some kind, I currently have a message being sent back to the server and then sent out to all the other connected clients. This way the user's view changes based upon another user's interactions.
To take advantage of Angular better, I was thinking of trying to use the angular ngDisabled directive. When a user focuses an element, other users would see the element get disabled.
I currently keep track of a 'global' UI state on the server and send this JSON object out to the clients which then update themselves. So I was hoping to have elements get disabled (or other CSS classes) based upon a scope flag (Or other behavior). Something like $scope.fieldsDisabled[fieldName] and set it to true/false.
Maybe I'm thinking about it wrong by going the directive way.
This making any sense? haha
In the directive life cycle template function gets called before compile so ideally it should work fine because you are setting the attribute inside template function. Can you try changing the attribute inside the compile function. Something like this.
var appInputs = function($compile, Device) {
return {
restrict: 'E',
require: '?ngModel',
priority: 101,
compile: function(tElement, tAttrs) {
tElement.attr("ng-disabled", 'isDisabled');
return function($scope, element, attrs) {
element.on("focus", function() {
console.log($scope);
});
}
},
controller: function($scope, $element) {
$scope.isDisabled = true;
console.log($scope);
}
}
};

Angularjs: how to update my model only once

I need to update my model after is has loaded its data. So i tried to write a directive which can do that. I didn't thought it would be this hard :(
I first tried a filter, which should be more simple, but got this error.
Error: [ngModel:nonassign] Expression 'editpage.url | addUrl' is non-assignable.
So now i try the directive way. This is my html code in the view:
<input ng-model="editpage.url" add-url type="text" class="light_txtbox" readonly>
And this is my directive:
app.directive('addUrl', [function() {
return {
restrict: 'A',
require: '?ngModel',
replace: true,
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return;
// what to do next?
}
};
}]);
In the "what to do next" part i tried a watch like this:
scope.$watch(attrs.ngModel, function(site) {
if (typeof site !== undefined) {
ngModel.$setViewValue('www.mysite.com/' + site);
ngModel.$render();
}
});
But of course now the model is updated and "hey, i am changed so update again!" and again and again...
I only need the update to take place once. I think i need another approach, but can not figure out what to do.
You can unregister a $watch
var unregister = scope.$watch(attrs.ngModel, function() {
if (shouldStopWatching) {
unregister();
}
});
where shouldStopWatching is whatever condition you need (i.e. stop on second call of callback etc)

AngularJS - In a directive that changes the model value, why do I have to call $render?

I made a directive designed to be attached to an element using the ngModel directive. If the model's value matches something the value should then set to the previous value. In my example I'm looking for "foo", and setting it back to the previous if that's what's typed in.
My unit tests passed fine on this because they're only looking at the model value. However in practice the DOM isn't updated when the "put back" triggers. Our best guess here is that setting old == new prevents a dirty check from happening. I stepped through the $setViewValue method and it appears to be doing what it ought to. However it won't update the DOM (and what you see in the browser) until I explicitly call ngModel.$render() after setting the new value. It works fine, but I just want to see if there's a more appropriate way of doing this.
Code is below, here's a fiddle with the same.
angular.module('myDirective', [])
.directive('myDirective', function () {
return {
restrict: 'A',
terminal: true,
require: "?ngModel",
link: function (scope, element, attrs, ngModel) {
scope.$watch(attrs.ngModel, function (newValue, oldValue) {
//ngModel.$setViewValue(newValue + "!");
if (newValue == "foo")
{
ngModel.$setViewValue(oldValue);
/*
I Need this render call in order to update the input box; is that OK?
My best guess is that setting new = old prevents a dirty check which would trigger $render()
*/
ngModel.$render();
}
});
}
};
});
function x($scope) {
$scope.test = 'value here';
}
Our best guess here is that setting old == new prevents a dirty check from happening
A watcher listener is only called when the value of the expression it's listening to changes. But since you changed the model back to its previous value, it won't get called again because it's like the value hasn't changed at all. But, be careful: changing the value of a property inside a watcher monitoring that same property can lead to an infinite loop.
However it won't update the DOM (and what you see in the browser) until I explicitly call ngModel.$render() after setting the new value.
That's correct. $setViewValue sets the model value as if it was updated by the view, but you need to call $render to effectively render the view based on the (new) model value. Check out this discussion for more information.
Finally, I think you should approach your problem a different way. You could use the $parsers property of NgModelController to validate the user input, instead of using a watcher:
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function(viewValue) {
if(viewValue === 'foo') {
var currentValue = ngModel.$modelValue;
ngModel.$setViewValue(currentValue);
ngModel.$render();
return currentValue;
}
else
return viewValue;
});
}
I changed your jsFiddle script to use the code above.
angular.module('myDirective', [])
.directive('myDirective', function () {
return {
restrict: 'A',
terminal: true,
require: "?ngModel",
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function(viewValue) {
if(viewValue === 'foo') {
var currentValue = ngModel.$modelValue;
ngModel.$setViewValue(currentValue);
ngModel.$render();
return currentValue;
}
else
return viewValue;
});
}
};
});
function x($scope) {
$scope.test = 'value here';
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<h1>Foo Fighter</h1>
I hate "foo", just try and type it in the box.
<div ng-app="myDirective" ng-controller="x">
<input type="text" ng-model="test" my-directive>
<br />
model: {{test}}
</div>

Resources