AngularJS: ng-change called before an select element is changed - javascript

As far as I can tell, ng-change is called before ng-model is actually changed in a select element. Here's some code to reproduce the issue:
angular.module('demo', [])
.controller('DemoController', function($scope) {
'use strict';
$scope.value = 'a';
$scope.displayValue = $scope.value;
$scope.onValueChange = function() {
$scope.displayValue = $scope.value;
};
})
.directive("selector", [
function() {
return {
controller: function() {
"use strict";
this.availableValues = ['a', 'b', 'c'];
},
controllerAs: 'ctrl',
scope: {
ngModel: '=',
ngChange: '='
},
template: '<select ng-model="ngModel" ng-change="ngChange()" ng-options="v for v in ctrl.availableValues"> </select>'
};
}
]);
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<div ng-app="demo" ng-controller="DemoController">
<div selector ng-model="value" ng-change="onValueChange">
</div>
<div>
<span>Value when ng-change called:</span>
<span>{{ displayValue }}</span>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<script src="demo.js"></script>
</body>
</html>
If you run this, you should change the combobox 2 times (e.g. 'b' (must be different than the default), then 'c').
The first time, nothing happens (but the value displayed in the text should have changed to match the selection).
The second time, the value should change to the previous selection (but should have been set to the current selection).
This sounds really similar to a couple previous posts: AngularJS scope updated after ng-change and AngularJS - Why is ng-change called before the model is updated?. Unfortunately, I can't reproduce the first issue, and the second was solved with a different scope binding, which I was already using.
Am I missing something? Is there a workaround?

It's good idea not to use the same model value in the directive. Create another innerModel which should be used inside directive and update the "parent" model when needed with provided NgModelController.
With this solution, you don't hack the ng-change behavior - just use what Angular already provides.
angular.module('demo', [])
.controller('DemoController', function($scope) {
$scope.value = 'a';
$scope.displayValue = $scope.value;
$scope.onValueChange = function() {
$scope.displayValue = $scope.value;
};
})
.directive("selector", [
function() {
return {
controller: function() {
this.availableValues = ['a', 'b', 'c'];
},
require: 'ngModel',
controllerAs: 'ctrl',
scope: {
'ngModel': '='
},
template: '<select ng-model="innerModel" ng-change="updateInnerModel()" ng-options="v for v in ctrl.availableValues"> </select>',
link: function(scope, elem, attrs, ngModelController) {
scope.innerModel = scope.ngModel;
scope.updateInnerModel = function() {
ngModelController.$setViewValue(scope.innerModel);
};
}
};
}
]);
<div ng-app="demo" ng-controller="DemoController">
<div selector ng-model="value" ng-change="onValueChange()">
</div>
<div>
<span>Value when ng-change called:</span>
<span>{{ displayValue }}</span>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
Inspired by this answer.

It's a digest issue.. Just wrap the onValueChange operations with $timeout:
$scope.onValueChange = function() {
$timeout(function(){
$scope.displayValue = $scope.value;
});
};
Don't forget to inject $timeout in your controller.
... or you can check this link on how to implement ng-change for a custom directive

Related

AngularJS Calling the controller method from directive

Prompt as from a directive to cause a method of the controller.
Directive
app.directive('scroll', function($location){
return{
restrict: 'A',
link: function(scope, element, attrs){
element.on('scroll', function(){
let fh = $('#ngview').height();
let nh = Math.round($(element).height() + $(element).scrollTop());
if(fh == nh){
//Here we do what we need
}
})
}
}
});
HTML markup
<div class="col-md-12 middle-body" scroll>
<div ng-show="showUserModal" ng-include="'partial/loginModal.html'"></div>
<div class="user-loader" ng-show="loading">
<div class="spinner"></div>
</div>
<div ng-view id="ngview">
</div>
</div>
app is the main application module
var app = angular.module('app',
[
'ngRoute',
'lastUpdateModule',
'selectedByGenreModule',
'currentFilmModule',
'httpFactory',
'userModule',
'accountModule'
]);
The controller from which you want to call the method is described in a separate file
and connects via require
const SelectedByGenreModule = require('../controllers/selectedByGenre.controller.js')
and passed as a dependency to the main module
So it is from this controller that I need to call the method in the directive.
Tell me how to do it correctly. I left through $rootScope but it did not work out
As far as I know, the directive has the same scope as the controller in which it is located. That is, the directive is in the controller which is the parent for the controller from which you need to call the method.
It sounds like you want your directive to trigger an action defined by your controller. I'd recommend passing the function to the directive via the scope property. See the example below.
var app = angular.module('ExampleApp', []);
app.directive('scroll', function($location) {
return {
restrict: 'A',
scope: {
scroll: '='
},
link: function(scope, element, attrs) {
element.on('scroll', function() {
let fh = $('#ngview').height();
let nh = Math.round($(element).height() + $(element).scrollTop());
if (fh == nh) {
scope.scroll();
}
})
}
}
});
app.controller('ExampleCtrl', function($scope) {
$scope.onScroll = function() {
console.log('Scrolled!')
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div ng-app="ExampleApp" ng-controller="ExampleCtrl">
<div class="col-md-12 middle-body" scroll="onScroll">
<div ng-show="showUserModal" ng-include="'partial/loginModal.html'"></div>
<div class="user-loader" ng-show="loading">
<div class="spinner"></div>
</div>
<div ng-view id="ngview">
</div>
</div>
</div>
You can require parent controllers using the require property when defining the directive, the ^^ tells angular to look up the DOM for a parent, otherwise it will only look on the local element.
app.directive('scroll', function($location){
return{
restrict: 'A',
require: '^^selectedByGenreCtrl', // Use the correct controller name here
link: function(scope, element, attrs, selectedByGenreCtrl){
element.on('scroll', function(){
let fh = $('#ngview').height();
let nh = Math.round($(element).height() + $(element).scrollTop());
if(fh == nh){
//Here we do what we need
}
})
}
}
});

Converting jquery function to angularjs

I have this function in jQuery but I'm facing problem converting that into angularJs function:
$('p').each(function() {
$(this).html($(this).text().split(/([\.\?!])(?= )/).map(
function(v){return '<span class=sentence>'+v+'</span>'}
));
It would really help if someone could explain to me how one would implement these lines of code in angularJs
Thanks in advance guys
easiest way is just splitting into an array and binding that to your controller.
More in depth way would be to roll your own custom directive, let me know if you want to see a way with a directive
<html ng-app="MyCoolApp">
<head>
</head>
<body ng-controller="MyCoolController">
<p ng-repeat="text in Array">
{{text}}
</P>
</body>
</html>
<script type="text/javascript">
var myApp = angular.module('MyCoolApp',[]);
myApp.controller('MyCoolController', ['$scope', function($scope) {
var dataSource = [];//your data split
$scope.Array = dataSource;
}]);
</script>
edit
Updated to use custom directive. YOu will need to tweak the regEx to split properly plunkr example
split.html
<div>
<textarea ng-model="input"></textarea>
<button ng-click="Process(input)">Process</button>
<p>
<span style="border:1px solid red" ng-repeat="text in Output track by $index">
{{text}}
</span>
</p>
</div>
script.js
(function(angular) {
'use strict';
angular.module('splitExample', [])
.directive('mySplit', function() {
return {
templateUrl: 'split.html',
controller: function($scope){
$scope.Process = function(text){
$scope.Output = text.split(/([\.\?!])(?= )/);
console.log(text)
}
}
};
})
})(window.angular);
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example - example-directive-tabs-production</title>
<script src="//code.angularjs.org/snapshot/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body ng-app="splitExample">
<my-split>
</my-split>
</body>
</html>
You need to write a directive for it, DOM Manipulation is only allowed in directives in the link function otherwise it is a very bad practice. Here is the code inside link function of directive.
link: function (scope, element, attributes) {
var el= $(element);
el.find('p').each(function(){
el.html(el.text().split(/([\.\?!])(?= )/).map(
function(value){
return '<span class=sentence>'+value+'</span>'
}
));
});
}
Hope it helps you, I am unable to know actually what you are trying to do.. otherwise i would have helped you with full solution and proper directive approach, for any further assistance let me know. Thanks.
Just giving my suggestion : JQuery approach is incompatible with AngularJS.
Updated directive as per your requirement:
angular.module('yourAppModuleName').directive('contentClick', ['$timeout', function($timeout) {
return {
restrict: 'A',
require: 'ngModel',
scope: {
contentEditable: '=contentEditable'
},
link: function(scope, element, attributes) {
scope.$watch(attributes.contentEditable, function(value) {
if (value === undefined)
return;
scope.contentEditable = attributes.contentEditable;
});
if(scope.contentEditable == 'true') {
scope.$watch(attributes.ngModel, function(value) {
if (value === undefined)
return;
value.split(/([\.\?!])(?= )/).map(
function(v){
return '<span onclick="myFunction(this)" >'+v+'</span>'
}
);
});
}
//without timeout
//function myFunction(x) {
// x.style.background="#000000";
//}
//with timeout
function myFunction(x) {
$timeout(function() {
x.style.background = "green";
}, 10000);
}
}
};
}]);
HTML:
<div contentEditable="isEditable" style="cursor:pointer" ng-model="content" content-click></div>
Controller:
yourAppModuleName.controller('myController', ['$scope', function($scope) {
$scope.isEditable = true;
$scope.content;
}]);

How to initialize the value of an input[range] using AngularJS when value is over 100

I try to initialize a slider using AngularJS, but the cursor show 100 when the value is over 100.
Setting the value 150 in a range [50,150] fails with this code :
<html ng-app="App">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>
angular.module('App', ['App.controllers']);
angular.module('App.controllers', []).controller('AppController', function($scope) {
$scope.min = 50;
$scope.max = 150;
$scope.value = 150;
});
</script>
</head>
<body ng-controller="AppController" >
{{min}}<input ng-model="value" min="{{min}}" max="{{max}}" type="range" />{{max}}<br/>
value:{{value}}
</body>
</html>
The cursor is badly placed (it show 100 instead of 150).
How to display the cursor to its correct place ?
An explanation of what occurs could be on this forum
Update
This bug is reported as issue #6726
Update
The issue #14982 is closed by the Pull Request 14996 and solve the issue see answer.
After searches and tries , a possible way is to define a custom directive :
<html ng-app="App">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>
angular.module('App', ['App.controllers']);
angular.module('App.controllers', []).controller('AppController', function($scope) {
$scope.min = 50;
$scope.max = 150;
$scope.value = 150;
}).directive('ngMin', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr) { elem.attr('min', attr.ngMin); }
};
}).directive('ngMax', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr) { elem.attr('max', attr.ngMax); }
};
});
</script>
</head>
<body ng-controller="AppController" >
{{min}}<input ng-model="value" ng-min="{{min}}" ng-max="{{max}}" type="range" />{{max}}<br/>
value:{{value}}
</body>
</html>
Even it is working this is a non standard extension in order to manage a very basic use case.
Try this new one.
You can configure angular to make it interpolate these values.
And you can use your initial code after that ...
Isn't it magic ?
Use this code only once in your app. Once angular is configured, it will be working for all the future ranges you will use.
<html ng-app="App">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>
angular.module('App', ['App.controllers']);
angular.module('App.controllers', [])
/* Modify angular to make it interpolate min and max, for ngModel when type = range */
.config(['$provide', function($provide) {
$provide.decorator('ngModelDirective', ['$delegate', function($delegate) {
var ngModel = $delegate[0], controller = ngModel.controller;
ngModel.controller = ['$scope', '$element', '$attrs', '$injector', function(scope, element, attrs, $injector) {
if ('range' === attrs.type) {
var $interpolate = $injector.get('$interpolate');
attrs.$set('min', $interpolate(attrs.min || '')(scope));
attrs.$set('max', $interpolate(attrs.max || '')(scope));
$injector.invoke(controller, this, {
'$scope': scope,
'$element': element,
'$attrs': attrs
});
}
}];
return $delegate;
}]);
}])
.controller('AppController', function($scope) {
$scope.min = 50;
$scope.max = 150;
$scope.value = 150;
});
</script>
</head>
<body ng-controller="AppController" >
{{min}}<input ng-model="value" min="{{min}}" max="{{max}}" type="range"/>{{max}}<br/>
value:{{value}}
</body>
</html>
My lazy way of addressing this bug was to divide the min/max and step values by 100. So 300 becomes 3.0, etc. and values fall below 100. Then I multiply things back as needed.
Since this commit, the initial code give the expected result.
Using release 1.6.0 allow to original code to show slider correctly :
<html ng-app="App">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.0/angular.min.js"></script>
<script>
angular.module('App', ['App.controllers']);
angular.module('App.controllers', []).controller('AppController', function($scope) {
$scope.min = 50;
$scope.max = 150;
$scope.value = 150;
});
</script>
</head>
<body ng-controller="AppController" >
{{min}}<input ng-model="value" min="{{min}}" max="{{max}}" type="range" />{{max}}<br/>
value:{{value}}
</body>
</html>

Call parent controllers' function from directive in angular

jsfiddle
I have a ng-click within a directive named ball. I am trying to call MainCtrl's function test() and alert the value of ng-repeat's alignment of ball.
Why cant i recognize the MainCtrl's test function?
var $scope;
var app = angular.module('miniapp', []);
app.controller('MainCtrl', function($scope) {
$scope.project = {"name":"sup"};
$scope.test = function(value) {
alert(value);
}
$scope.test2 = function(value) {
alert('yo'+value);
}
}).directive('ball', function () {
return {
restrict:'E',
scope: {
'test': '&test'
},
template: '<div class="alignment-box" ng-repeat="alignment in [0,1,2,3,4]" ng-click="test(alignment)" val="{{alignment}}">{{alignment}}</div>'
};
});
html
<div ng-app="miniapp">
<div ng-controller="MainCtrl">
{{project}}
<ball></ball>
</div>
</div>
You need to pass the test() method from the controller into the directive...
<div ng-app="miniapp">
<div ng-controller="MainCtrl">
{{project}}
<ball test="test"></ball>
</div>
</div>
Change & to = in directive:
scope: {
'test': '=test'
}
Fiddle: http://jsfiddle.net/89AYX/49/
You just need to set the controller in your directive as:
controller: 'MainCtrl'
so the code for your directive should look like:
return {
restrict:'E',
scope: {
'test': '&test'
},
template: '<div class="alignment-box" ng-repeat="alignment in [0,1,2,3,4]" ng-click="test(alignment)" val="{{alignment}}">{{alignment}}</div>',
controller: 'MainCtrl'
};
the one way is to not isolate a directive scope...just remove the scope object from directive.
Another way is to implement an angular service and put a common method there, inject this service wherever you need it and in the directive call function that will be insight isolated scope and there call a function from directive

AngularJS Odd Directive Scope Behavior

After describing my setup, my questions are below in bold.
index.html
<div ng-controller="MyCtrl">
<user-picker placeholder="Type a name..."></user-picker>
</div>
Setup:
var app = angular.module('app', ['app.directives', 'app.controllers']);
var directives = angular.module('app.directives', []);
var ctrls = angular.module('app.controllers', []);
Controller:
ctrls.controller('MyCtrl', function($scope) {
$scope.foo = 'this is a foo';
});
Directive:
directives.directive('userPicker', function() {
return {
restrict: 'E',
replace: true,
scope: {
placeholder: '#'
},
templateUrl: 'file.html',
link: function postLink($scope, ele, attrs) {
console.log($scope);
console.log('[ng] Scope is: ');
console.log($scope.placeholder);
console.log($scope.$parent.foo);
}
});
file.html (the directive):
<span>
<input placeholder="{{placeholder}}" type="text">
</span>
So what I want to end up with, is generally working:
<span placeholder="Type a name...">
<input placeholder="Type a name..." type="text">
</span>
The placeholder attribute is correctly resolved.
Is this the right way to accomplish this? Note that the attribute ends up in two places.
Why this odd behavior:
Secondly, I am baffled by the results of console.log($scope). The console output reveals the accurately set placeholder attribtue on the $scope object. However, even still, the very next console.log($scope.placeholder) statement returns "undefined". How is this possible, when the console output clearly shows the attribute is set?
My goals are:
Move or copy the placeholder attribute from the parent down to the child <input> tag.
Have access to the template scope from within linking function.
Reference the parent MyCtrl controller that this directive sits within.
I was almost there, until I ran into the odd behavior noted above. Any thoughts are appreciated.
Instead of attempting to read this off the scope would reading the attrs work?
Some HTML
<script type="text/ng-template" id="file.html">
<span>
<input placeholder="{{placeholder}}" type="text"/>
</span>
</script>
<body ng-app="app">
<div ng-controller="MyCtrl">
<user-picker placeholder="Type a name..."></user-picker>
</div>
</body>
Some JS
var app = angular.module('app', ['app.directives', 'app.controllers']);
var directives = angular.module('app.directives', []);
var ctrls = angular.module('app.controllers', []);
ctrls.controller('MyCtrl', function ($scope) {
$scope.foo = 'this is a foo';
});
directives.directive('userPicker', function () {
return {
restrict: 'E',
replace: true,
scope: {
placeholder: '#'
},
templateUrl: 'file.html',
link: function postLink($scope, ele, attrs) {
console.log($scope);
console.log('[ng] Scope is: ');
console.log(attrs["placeholder"]);
console.log($scope.$parent.foo);
}
}
});
A Fiddle
http://jsfiddle.net/Rfks8/

Categories

Resources