Detect who change the model (user input vs controller) angularjs - javascript

I have an input which connected to model. Also, the input has directive which $watch the model.
There are 2 ways that the model will change.
The user will type in the textbox.
The code will change it (no matter what is the reason)
My question is
Is there a way to find out who change the model, the user interaction or the code, in the directive?
Example:
angular.module('app', [])
.controller('ctrl', function($scope) {
})
.directive('dir', function($rootScope){
return {
require: 'ngModel',
link: function(scope, element, attrs) {
$rootScope.logs = [];
scope.$watch(attrs.ngModel, function() {
// here I need to check if the change was from the UI or from the controller
$rootScope.logs.push('change');
});
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<div data-ng-app="app" data-ng-controller="ctrl">
<input type="text" data-ng-model="model" data-dir="" />
<button data-ng-click="model = 'asd'">Set "model" to defferent value</button>
{{model}}
<hr />
<h3>console <button data-ng-click="$root.logs = []">clear console</button></h3>
<ul>
<li data-ng-repeat="log in $root.logs track by $index" data-ng-bind="log"></li>
</ul>
</div>
http://jsbin.com/vufawur/edit?html,js,output
Update
Example2:
angular.module('app', [])
.controller('ctrl', function($scope, $timeout) {
$timeout(function() {
$scope.model = 'asd';
}, 3000);
})
.directive('dir', function($rootScope){
return {
require: 'ngModel',
link: function(scope, element, attrs) {
$rootScope.logs = [];
scope.$watch(attrs.ngModel, function() {
// here I need to check if the change was from the UI or from the controller
$rootScope.logs.push('change');
});
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<div data-ng-app="app" data-ng-controller="ctrl">
...wait until data "return from the server"<br />
<input type="text" data-ng-model="model" data-dir="" />
{{model}}
<hr />
<h3>console <button data-ng-click="$root.logs = []">clear console</button></h3>
<ul>
<li data-ng-repeat="log in $root.logs track by $index" data-ng-bind="log"></li>
</ul>
</div>

ext-change External Change Directive for ng-model
Use a $viewChangeListener to save the last user input and have the watch handler compare that to discriminate external changes to the model from user input changes to the model.
.directive('extChange', function(){
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var lastUserInput = modelCtrl.$viewValue;
modelCtrl.$viewChangeListeners.push(function() {
lastUserInput = modelCtrl.$viewValue;
});
scope.$watch(attrs.ngModel, function watchHandler (value) {
if (value!==lastUserInput) {
scope.$eval(attrs.extChange, {$value:value});
}
});
}
}
});
The example directive saves that last user input. When the watch handler gets a value that is different, it invokes the Angular expression defined by the ext-change attribute. The value of the change is exposed as $value.
<input ng-model="someInput"
ng-change="userInput=someInput"
ext-change="extInput=$value">
The ext-change directive works with the ng-model directive and complements the ng-change directive.
In this example, the ext-change directive only updates the extInput variable on external changes to the model. The ng-change directive only updates the userInput variable for user changes.
The DEMO on JSFiddle
The directive can also be used to invoke functions.
<input ng-model="someInput"
ng-change="userEvent(someInput)"
ext-change="externalEvent($value)">

Do not use $watch. You should not use it, you have to not use it, you are going to have trouble if you use $watch, you are already in trouble, don't use it.
Angular JS - you probably shouldn't use $watch in your controllers.
Is it an antipattern to use angular's $watch in a controller?
Use control flow and events. It is possible that you already have a lot of watcher and scope soup, it is not too late, refactor as soon as possible, it is for your best.
angular.module('app', [])
.controller('ctrl', function($scope) {
})
.directive('dir', function($rootScope) {
return {
require: 'ngModel',
link: function($scope, element, attrs) {
$rootScope.logs = [];
$scope.modelChange = function(reason) {
$rootScope.logs.push(reason);
};
$scope.modelChangedFromInput = function(model) {
$scope.modelChange('From input');
};
$scope.buttonClick = function() {
$scope.model = 'asd';
$scope.modelChange('From button');
};
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<div data-ng-app="app" data-ng-controller="ctrl">
<input type="text" data-ng-model="model" data-dir="" data-ng-change="modelChangedFromInput()" />
<button data-ng-click="buttonClick()">Set "model" to different value</button>
{{model}}
<hr />
<h3>console <button data-ng-click="$root.logs = []">clear console</button>
</h3>
<ul>
<li data-ng-repeat="log in $root.logs track by $index" data-ng-bind="log"></li>
</ul>
</div>

Related

Binding ng-model to a Service parameter from inside a Template

I keep a data model in a service so that various controllers can use them. Inside the controller I scope it so that elements can bind to it using ng-model:
In the controller:
angular.module('hiveApp').controller('weatherController', function($scope, $rootScope, weatherModel, messageService, utilityService, $http, $timeout, $cookies, $window, $controller) {
$scope.weatherModel = weatherModel;
Then in the HTML element:
<input type="text" id="city" ng-model="weatherModel.city" />
So far so good, this works. The problem occurs when I bring a directive into the mix. I have a directive that handles a pair of radio buttons, and makes use of a template. That template attempts to use ng-model to reference that same weatherModel service parameters, however while it works from the HTML in the page itself, the directive template doesn't work:
app.directive('radiopair', function() {
return {
restrict: 'E',
template: `
<div style="color:white; float:left">
<input type="radio" id="metric" name="conversion" value="metric"
ng-model="weatherModel.conversion" selected> Metric<br>
<input type="radio" id="imperial" name="conversion" value="imperial"
ng-model="weatherModel.conversion"> Imperial<br>
</div>
`,
link: function ($scope, element, attrs) {
element.on('click', function (event) {
event.currentTarget.selected = true;
$scope.refreshTable();
});
}
}
});
When I toggle between the two buttons, the ng-model=weatherModel.conversion value never gets updated. I figure this has got to be some scoping issue but I'm hitting a wall as to how to fix it.
Instead of using a click handler to invoke the refreshTable function, use the ng-change directive:
app.directive('radiopair', function() {
return {
restrict: 'E',
template: `
<div style="color:white; float:left">
<input type="radio" id="metric" name="conversion" value="metric"
ng-change="refreshTable()"
ng-model="weatherModel.conversion" selected> Metric<br>
<input type="radio" id="imperial" name="conversion" value="imperial"
ng-change="refreshTable()"
ng-model="weatherModel.conversion"> Imperial<br>
</div>
`,
//link: function ($scope, element, attrs) {
// element.on('click', function (event) {
// event.currentTarget.selected = true;
// $scope.refreshTable();
// });
//}
}
});
Avoid manipulating the select attribute of <input> elements. That should be done by the ng-model directive and the ngModelController.

Angularjs Pass all attributes on a directive to the view

I'm building a tiny angular directive <my-input> on top of a normal HTML <input>.
And because this is going to be available in a framework, I need to allow people to pass whichever attribute they might use from the directive to the input element. For example:
<my-directive disabled="disabled" type="email">
would render
<input disabled="disabled" type="email">
I know that if I have a static list of attributes, I can manually do it.. but the problem is I can't predict what attributes will be added.. so I'm looking for a solution that passes all the attributes from the directive to the input element.
Thanks
If you want to pass multiple attributes to the view, you can perform it into the link function.
Here is your directive :
Directive
(function(){
function myInput($compile) {
return{
restrict: 'E',
templateUrl: 'template.html',
link: function(scope, elm, attrs){
//Convert camelCase to dash
function toDash(str) {
return str.replace(/\W+/g, '-')
.replace(/([a-z\d])([A-Z])/g, '$1-$2');
}
//Retrieve input into the template
var input = angular.element(document.querySelector('#myInput'));
//Loop on attrs
for (var key in attrs) {
if (key[0] !== '$'){
//Remove all attribute to the top level element
elm.removeAttr(toDash(key));
//Add our attribute to the input
input.attr(toDash(key), attrs[key]);
//Compile and link it to the scope
$compile(input)(scope);
}
}
}
};
}
angular
.module('app')
.directive('myInput', myInput);
})();
With the template :
template.html
<input type="text" id="myInput">
For example, in a controller you can set some variable :
Controller
(function(){
function Controller($scope) {
$scope.show = true;
$scope.toto = 'Hello !!'
}
angular
.module('app', [])
.controller('ctrl', Controller);
})();
And call your directive :
<body ng-app="app" ng-controller="ctrl">
<my-input disabled="disabled" ng-model="toto" ng-show="show"></my-input>
</body>
So it will remove all attributes to the my-input element, and set it into your template.

Passing a function to an Angular validation directive

I have the following code, note the name-valid and validation-function tags on the input field.
<form name="createForm" novalidate>
<div style="display: flex; width: 300px">
<div style="flex: 3;">
Name
</div>
<div style="flex: 5;">
<input type="text" name="listName" ng-model="newListName"
ng-minlength="3" name-valid validation-function="someFunction"/>
</div>
</div>
<div ng-show="createForm.listName.$error.unique &&
!renameGoldenForm.listName.$error.minlength">already exists</div>
<div ng-show="createForm.listName.$error.minlength">too short</div>
<div style="margin-top: 10px">
<button ng-click="createList()" ng-disabled="createForm.listName.$invalid">
Create</button>
</div>
</form>
And here is the JS:
window.angular.module("myModule").directive("nameValid", [
"$log",
function($log) {
return {
require: "ngModel",
scope: {
validationFunction: "="
},
link: function(scope, ele, attrs, c) {
scope.$watch(attrs.ngModel, function() {
var v = scope[attrs.ngModel];
if (!v || !((v).trim()) || v.length < 4) {
c.$setValidity("unique", true);
c.$setValidity("minlength", false);
return;
}
scope.validationFunction(v, scope.selectedListId)
.success(function(data) {
c.$setValidity("unique", data.unique);
c.$setValidity("minlength", data.minlength);
});
});
}
};
}
]);
The problem is that having require and scope seems to break.
Is there a way to pass a custom validate function to my directive? I'm not sure how to go about it.
I've tried removing require: 'ngModel' and adding ngModel in scope, but that did not work either.
If I remove scope and hard code the function in the watch block, that works, but obviously, that defeats the purpose of having a pointer to a specific function.
To bind a controller function to your directive, you have to use the & bindings (expression binding) which allows the directive to call an expression or a function defined by a DOM attribute.
For example :
Controller
(function(){
function Controller($scope, $q) {
//Declare the func which will be bind to the directive
$scope.func = function (data1, data2) {
return new $q(function(resolve){
resolve(data1 === data2);
});
}
}
angular
.module('app', [])
.controller('ctrl', Controller);
})();
Then we will bind this function into your directive, and we can call it into the link function.
Directive
(function(){
function directive() {
return{
restrict: 'AE',
scope: {
function: '&'
},
link:function(scope, element, attrs) {
//Then, pass an object as argument to your function
var promise = scope.function({data1: 5, data2: 5});
//Retrieve result
promise.then(function(data){
console.log(data);
});
}
};
}
angular
.module('app')
.directive('directive', directive);
})();
To finish, you can call your directive with a function attribute, in order to bind the function to your directive.
HTML
<body ng-app="app" ng-controller="ctrl">
<directive function="func(data1, data2)"></directive>
</body>

ngModel formatters change the $scope

Having an input with ngModel which has $formatters and $parsers -
link: function(scope, element, attrs, ngModel) {
//format text going to user (model to view)
ngModel.$formatters.push(function(obj) {
return obj.first;
});
}
After I make any change in the input , I miss this model on the scope , mean - {{person.first}} display nothing .
Here is the full code -
var myAppModule = angular.module('myApp', []);
myAppModule.controller('myCtrl',function ($scope) {
$scope.person = {
first: 'Alice',
last: 'Bob'
}
})
.directive('myDrtv',function(){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
var _ngModel = ngModel;
//format text going to user (model to view)
ngModel.$formatters.push(function(obj) {
return obj.first;
});
//format text from the user (view to model)
ngModel.$parsers.push(function(value) {
return value;
});
}
}
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="myCtrl">
Name : <input my-drtv ng-model="person" type="text"><br>
{{person.first}}
</div>
</div>
How could I change the input and still see it in {{person.first}} ?
Please don't answer me change it to ng-model="person.first" I looking solution with - ng-model="person"
Change your input to the following:
<input my-drtv ng-model="person.first" type="text">
They way you have it, you are clobbering person and changing it from an object to a string.
Edit: If you want to keep first name and last name separate, then do something like this:
<input my-drtv ng-model="fullname" type="text">
and then in link, watch for changes and update person:
scope.$watch('fullname', function(nv) {
person.first = extractFirstName(nv);
person.last = extractLastName(nv);
});
(I left it to you to supply the extract functions).

Use Directive two times in one controller AngularJS

I want to use same directives multiple times in one controller in AngularJS. I want to create a list widget that can be used multiple times. I can display two widgets at the same time under the same controller. But, I am unable to bind teamA and teamB data to ng-repeat in my directive. In addition to that, the code fails during addTeamMember() because datasource is undefined. I was hoping that datasource will be updated with teamA and teamB respectively.
Here is the HTML code.
<div ng-controller="myCtrl"><div class="container">
<my-directive datasource="model.teamA"></my-directive>
<my-directive datasource="model.teamB"></my-directive>
</div></div>
Controller.js:
angular.module('app',[])
.controller('myCtrl', [ '$scope', function($scope){
$scope.teamA = {};
$scope.teamB = {};
} ] );
Directive.js:
angular.module('app', [] )
.directive('myDirective', function(){
return{
restrict: 'AE',
templateUrl: 'directive_html.html',
scope: {
datasource: "=TeamMembers"
},
transclude: true,
link: function(scope, elem, attrs){
scope.addTeamMember = function(){
scope.datasource.push({});
};
scope.removeTeamMember = function(item){
scope.datasource.splice(item, 1);
};
}
};
}) ;
directive_html.html:
<div><div class="container">
<div class="form-group" ng-repeat="member in TeamMembers">
<textarea ng-model="member.text" rows="2"></textarea>
Remove
<div>
<button type="button" ng-click="addTeamMember()">Add</button>
</div></div>
Could anyone please help me out here? I want to create custom Widgets that can be used multiple places either in same controllers or in different controllers.
Thanks
As #Neozaru pointed out in comments. You are expecting the directive attribute to be called team-members here:
<div ng-controller="myCtrl"><div class="container">
<my-directive team-members="model.teamA"></my-directive>
<my-directive team-members="model.teamB"></my-directive>
</div></div>
You do this when you define the isolated scope as:
scope: {
datasource: "=TeamMembers"
}
The above line is saying, "team-members is what the outside attribute will be named, but internally I'll refer to the referenced object as scope.datasource".

Categories

Resources