AngularJS Directive Not Firing On-Change Callback - javascript

I've created a numeric stepper for use with CSS styles, but am having issues getting it to fire the ng-change when you type in it manually.
I created a log on the plunker to illustrate when the callback is being fired. As you can see from playing with it, it works fine when you click on the stepper arrows, but not when you type in the box directly.
Current Code Example: Plunker
HTML:
<div class="stepper-container">
<input type="text" ng-model="ngModel">
<button class="stepper-up fa fa-chevron-up" ng-click="increment()"></button>
<button class="stepper-down fa fa-chevron-down" ng-click="decrement()"></button>
</div>
JavaScript:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.myModel = null;
$scope.log = [];
$scope.someMethod = function () {
$scope.log.push('Change event on ' + $scope.myModel);
}
});
app.directive('numericStepper', function () {
return {
restrict: 'EA',
require: 'ngModel',
scope: {
ngModel: '='
},
replace: true,
templateUrl: 'numeric-stepper.html',
link: function (scope, element, attrs, ngModelCtrl) {
console.log('NumericStepper::link', ngModelCtrl.$viewValue);
var sizingUnit = null;
var css3Lengths = [
// Percentage
'%',
// Font Relative
'em', 'ex', 'ch', 'rem',
// Viewport Relative
'vw', 'vh', 'vmin', 'vmax',
// Absolute
'cm', 'mm', 'in', 'px', 'pt', 'pc'
];
scope.$watch(function () {
return ngModelCtrl.$modelValue;
}, function (newValue, oldValue) {
updateValue();
});
ngModelCtrl.$formatters.unshift(function (value) {
value = isNaN(parseInt(value)) ? 0 : value;
return value;
});
scope.increment = function () {
updateValue(1)
};
scope.decrement = function () {
updateValue(-1);
};
function updateValue(amount) {
var matches = ngModelCtrl.$viewValue.toString().split(/(-?\d+)/);
var value = (parseInt(matches[1]) || 0) + (amount || 0);
sizingUnit = matches[2].trim();
ngModelCtrl.$setViewValue(value + sizingUnit);
sanityCheck();
}
function sanityCheck() {
var validity = css3Lengths.indexOf(sizingUnit) != -1;
ngModelCtrl.$setValidity('invalidUnits', validity);
}
}
}
});

Change your template text box to include an isolate scope call for ngChange. In that function, use timeout to allow the model update/digest to happen before calling parent controllers change function...
So change your template textbox:
<input type="text" ng-model="ngModel" ng-change="textChanged()">
Then change your directive:
// $timeout works better here than watch
app.directive('numericStepper', function ($timeout) {
return {
restrict: 'EA',
require: 'ngModel',
scope: {
ngModel: '=',
ngChange: '&' // add me!
},
replace: true,
templateUrl: 'numeric-stepper.html',
link: function (scope, element, attrs, ngModelCtrl) {
console.log('NumericStepper::link', ngModelCtrl.$viewValue);
var sizingUnit = null;
var css3Lengths = [
// Percentage
'%',
// Font Relative
'em', 'ex', 'ch', 'rem',
// Viewport Relative
'vw', 'vh', 'vmin', 'vmax',
// Absolute
'cm', 'mm', 'in', 'px', 'pt', 'pc'
];
/********** DONT NEED THIS
// scope.$watch(function () {
// return ngModelCtrl.$modelValue;
// }, function (newValue, oldValue) {
// updateValue();
// });
******************/
// Add this function
scope.textChanged = function() {
$timeout(function(){
updateValue();
scope.ngChange(); }, 500); // could be lower
}
ngModelCtrl.$formatters.unshift(function (value) {
value = isNaN(parseInt(value)) ? 0 : value;
return value;
});
scope.increment = function () {
updateValue(1)
};
scope.decrement = function () {
updateValue(-1);
};
function updateValue(amount) {
var matches = ngModelCtrl.$viewValue.toString().split(/(-?\d+)/);
var value = (parseInt(matches[1]) || 0) + (amount || 0);
sizingUnit = matches[2].trim();
ngModelCtrl.$setViewValue(value + sizingUnit);
sanityCheck();
}
function sanityCheck() {
var validity = css3Lengths.indexOf(sizingUnit) != -1;
ngModelCtrl.$setValidity('invalidUnits', validity);
}
}
}
});
And a working plunker

addon to what #doog abides already said
you can use $timeout interval as 0 and it will work the same then
scope.textChanged = function() {
$timeout(function(){
updateValue();
scope.ngChange(); }, 0); // could be Zero

Related

how to Trigger the event in Angular when user change the windows height ?

I need to count the element's position each time the user change windows height.so I create a directive.
'use strict';
angular.module "myApp"
.directive 'uibPosition',['$position','$document','$timeout', ($position,$document,$timeout)->
return {
restrict: 'EAC',
link: (scope, element, attr) ->
//I want to Trigger this event when user change windows height
$timeout ->
if $document.height() - $position.position(element).top < 500
element[0].querySelector('.custom-popup-wrapper').style.top = 'auto'
element[0].querySelector('.custom-popup-wrapper').style.bottom = 40 + 'px'
else
element[0].querySelector('.custom-popup-wrapper').style.top = $position.position(element).top+40+'px'
}
]
how to do it?I really appreciate it
You can add the watcher to the directive:
var w = angular.element($window);
scope.getWindowDimensions = function () {
return {
"h": w.height(),
"w": w.width()
};
};
var watcherTimeout;
scope.$watch(scope.getWindowDimensions, function (newValue) {
$timeout.cancel(watcherTimeout);
watcherTimeout = $timeout(function(){
scope.callback(); // call callback function
}, 300);
}, true);
w.bind("resize", function () {
scope.$apply();
});

How to change attribute on a selected directive from a controller

I build an application with interactive SVG map with angular.js
Whole app controlled by controller, svg wrapped in a directive, and every <path> in svg is a partial directive too.
When user clicked on a wall, <path> id is memorized in a custom service and next when user select a color, I need to fill this wall with that color.
The problem is that I can't access directive within a controller. I also cant share a scope with a directive because i have multiple nested directives in my controller and want to access them by id.
Maybe there is a design mistake? how can I achieve this workflow?
There is my code for controller, service and directives
ColorPicker Controller
app.controller("ColorPicker", function($scope, $timeout, appConfig, ColorService) {
$scope.colors = [];
$scope.currentColorSet = 0;
$scope.svgTemplateUrl = appConfig.arSvg[$scope.$parent.roomType.id][$scope.$parent.roomStyle.id] + 'over.svg';
$scope.maxColorSet = Math.max.apply(Math, jsondata.roomtype[$scope.$parent.roomType.id].data.roomstyle[$scope.$parent.roomStyle.id].colors.map(function(color) {
return color.color.group;
}));
$scope.sliderConfig = {
min: 0,
max: $scope.maxColorSet,
step: 1
}
$scope.emptyColors = ( ColorService.getColors().length === 0 );
$scope.currentProps = {};
$scope.applyColor = function(color) {
console.log($scope.currentProps);
//**here I need to set fill with selected color to selected directive**
}
$scope.prevColorSet = function(){
if($scope.currentColorSet > 0) {
$scope.currentColorSet--;
generateColorSet($scope.currentColorSet);
}
}
$scope.nextColorSet = function(){
if($scope.currentColorSet < $scope.maxColorSet) {
$scope.currentColorSet++;
generateColorSet($scope.currentColorSet);
}
}
generateColorSet = function(setIndex){
$scope.colors = [];
for(var i = 0; i < jsondata.roomtype[$scope.$parent.roomType.id].data.roomstyle[$scope.$parent.roomStyle.id].colors.length; i++){
if(jsondata.roomtype[$scope.$parent.roomType.id].data.roomstyle[$scope.$parent.roomStyle.id].colors[i].color.group == setIndex) {
$scope.colors.push(jsondata.roomtype[$scope.$parent.roomType.id].data.roomstyle[$scope.$parent.roomStyle.id].colors[i].color)
}
}
}
generateColorSet($scope.currentColorSet);
$scope.$watch("currentColorSet", function(newValue, oldValue) {
generateColorSet($scope.currentColorSet);
});
});
Directive for whole SVG
app.directive('svgmap', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var svg = $(element).find('svg');
svg.removeAttr('xmlns:a');
svg.attr('width', '869px');
svg.attr('height', '489px');
element.append(svg);
var regions = element[0].querySelectorAll('path');
angular.forEach(regions, function (path, key) {
var regionElement = angular.element(path);
regionElement.attr("region", "");
$compile(regionElement)(scope);
});
},
}
}]);
Directive for region (wall)
app.directive('region', ['$compile','ColorService','$timeout', function ($compile, ColorService, $timeout) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.properties = {
elementId : element.attr("id"),
stroke: '',
fill: '#fff'
};
scope.regionClick = function () {
scope.currentProps = scope.properties;
if(ColorService.selectedRegion != scope.properties.elementId) {
scope.properties.stroke = '#e5514e';
ColorService.selectedRegion = scope.properties.elementId;
} else {
scope.properties.stroke = '';
ColorService.selectedRegion = '';
}
console.log('click: ' + scope.properties.elementId + ' : ' + scope.properties.stroke);
};
scope.setStroke = function () {
scope.stroke = '#e5514e';
};
scope.removeStroke = function() {
if(ColorService.selectedRegion != scope.properties.elementId) {
scope.properties.stroke = '';
}
//console.log('enter: ' + scope.elementId + ' : ' + scope.stroke);
};
scope.removeColor = function() {
console.log('removed');
scope.properties.fill = '#fff';
ColorService.remove(scope.properties.elementId);
};
ColorService.getColors().forEach(function(item) {
if(item.id === scope.properties.elementId) {
scope.properties.fill = item.info.val;
}
});
element.attr("ng-click", "regionClick()");
element.attr("ng-attr-stroke", "{{properties.stroke}}");
element.attr("ng-attr-fill", "{{properties.fill}}");
element.attr("ng-mouseenter", "setStroke()");
element.attr("ng-mouseleave", "removeStroke()");
element.attr("ng-right-click", "removeColor()");
element.after('<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />');
element.removeAttr("region");
$compile(element)(scope);
}
}
}]);

How to re-render directive after a variable is changed?

I am using a directive for star rating. But the template the is loaded before data is loaded from HTTP. So i want to reload directive template after HTTP request is successful.
HTML
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js" type="text/javascript"></script>
</head><body>
<div ng-app="myapp" ng-controller="movieCtrl">
<div star-rating rating="starRating" read-only="false" max-rating="10" click="click(param)" mouse-hover="mouseHover(param)"
mouse-leave="mouseLeave(param)"></div>
</div></body></html>
JS
var app = angular.module('myapp', []);
app.controller("movieCtrl", function($scope, $http) {
$scope.starRating = 0;
$scope.hoverRating = 0;
$scope.mouseHover = function(param) {
$scope.hoverRating1 = param;
};
$scope.mouseLeave = function(param) {
$scope.hoverRating1 = param + '*';
};
//problem here
//actual data coming via http
//when value is changed i want to re-render below directive template
setTimeout(function() {
$scope.starRating = 5
}, 1000);
});
app.directive('starRating', function() {
return {
scope: {
rating: '=',
maxRating: '#',
readOnly: '#',
click: "&",
mouseHover: "&",
mouseLeave: "&"
},
restrict: 'EA',
template: "<div style='display: inline-block; margin: 0px; padding: 0px; cursor:pointer;' ng-repeat='idx in maxRatings track by $index'> \
<img ng-src='{{((hoverValue + _rating) <= $index) && \"http://www.codeproject.com/script/ratings/images/star-empty-lg.png\" || \"http://www.codeproject.com/script/ratings/images/star-fill-lg.png\"}}' \
ng-Click='isolatedClick($index + 1)' \
ng-mouseenter='isolatedMouseHover($index + 1)' \
ng-mouseleave='isolatedMouseLeave($index + 1)'></img> \
</div>",
compile: function(element, attrs) {
if (!attrs.maxRating || (Number(attrs.maxRating) <= 0)) {
attrs.maxRating = '5';
};
},
controller: function($scope, $element, $attrs) {
$scope.maxRatings = [];
for (var i = 1; i <= $scope.maxRating; i++) {
$scope.maxRatings.push({});
};
$scope._rating = $scope.rating;
$scope.isolatedClick = function(param) {
if ($scope.readOnly == 'true') return;
$scope.rating = $scope._rating = param;
$scope.hoverValue = 0;
$scope.click({
param: param
});
};
$scope.isolatedMouseHover = function(param) {
if ($scope.readOnly == 'true') return;
$scope._rating = 0;
$scope.hoverValue = param;
$scope.mouseHover({
param: param
});
};
$scope.isolatedMouseLeave = function(param) {
if ($scope.readOnly == 'true') return;
$scope._rating = $scope.rating;
$scope.hoverValue = 0;
$scope.mouseLeave({
param: param
});
};
}
};
});
See Codepen for more info.
Here is a simple rating directive which uses stars, note that the logic is in the link function, rather than the controller.
function starRating() {
return {
restrict: 'EA',
template:
'<ul class="star-rating" ng-class="{readonly: readonly}">' +
// see ng-repeat here? this will update when scope.stars is updated
' <li ng-repeat="star in stars" class="star" ng-class="{filled: star.filled}" ng-click="toggle($index)">' +
' <i class="fa fa-star"></i>' + // or &#9733
' </li>' +
'</ul>',
scope: {
ratingValue: '=ngModel',
max: '=?', // optional (default is 5)
onRatingSelect: '&?', // callback
readonly: '=?' // set whether this should be changeable or not
},
link: function(scope, element, attributes) {
if (scope.max == undefined) {
scope.max = 5;
}
function updateStars() { // update to rating value
scope.stars = [];
for (var i = 0; i < scope.max; i++) {
scope.stars.push({
filled: i < scope.ratingValue
});
}
};
scope.toggle = function(index) {
if (scope.readonly == undefined || scope.readonly === false){
scope.ratingValue = index + 1;
scope.onRatingSelect({
rating: index + 1
});
}
};
scope.$watch('ratingValue', function(oldValue, newValue) {
if (newValue) {
updateStars();
}
});
}
};
}
Use $scope.$apply() on setTimeout function and your code will work fine
also i have made simple modification to your code .. check here
i created a service to share data b/n controllers
added some $watch function to detect value change
var app = angular.module('myapp', []);
app.controller("movieCtrl", function($scope, $http, share) {
$scope.starRating = 0;
$scope.hoverRating = 0;
$scope.mouseHover = function(param) {
$scope.hoverRating1 = param;
};
$scope.mouseLeave = function(param) {
$scope.hoverRating1 = param + '*';
};
$scope.$watch('starRating', function() {
share.rating = $scope.starRating
});
setTimeout(function() {
console.log('timeout set');
$scope.starRating = 5;
$scope.$apply();
}, 1000);
});
app.factory('share', function() {
var obj = {
rating: 0
}
return obj;
});
app.directive('starRating', function() {
return {
scope: {
rating: '=',
maxRating: '#',
readOnly: '#',
click: "&",
mouseHover: "&",
mouseLeave: "&"
},
restrict: 'EA',
templateUrl: "star1.html",
compile: function(element, attrs) {
if (!attrs.maxRating || (Number(attrs.maxRating) <= 0)) {
attrs.maxRating = '5';
};
},
controller: function($scope, $element, $attrs, share) {
$scope.maxRatings = [];
$scope.rating = share.rating;
$scope.$watch('rating', function() {
$scope._rating = share.rating;
});
for (var i = 1; i <= $scope.maxRating; i++) {
$scope.maxRatings.push({});
};
$scope._rating = share.rating;
$scope.isolatedClick = function(param) {
if ($scope.readOnly == 'true') return;
$scope.rating = $scope._rating = param;
$scope.hoverValue = 0;
$scope.click({
param: param
});
};
$scope.isolatedMouseHover = function(param) {
if ($scope.readOnly == 'true') return;
$scope._rating = 0;
$scope.hoverValue = param;
$scope.mouseHover({
param: param
});
};
$scope.isolatedMouseLeave = function(param) {
if ($scope.readOnly == 'true') return;
$scope._rating = $scope.rating;
$scope.hoverValue = 0;
$scope.mouseLeave({
param: param
});
};
}
};
});

ng-pattern not working inside directive

I'm trying to wrap an <input> in a directive so that I can handle date validation and convert it from a string to an actual Date object and maintain the Date version in the original scope. This interaction is working as expected. But the ng-pattern on the <input> element isn't acting right. It is never invalidating the <input>, regardless of what is entered.
HTML
<pl-date date="date"></pl-date>
JS
.directive("plDate", function (dateFilter) {
return {
restrict: 'E',
replace: true,
template: '<input id="birthDateDir" name="birthDate" type="text" ng-pattern="{{getDatePattern()}}" ng-model="dateInput">',
scope: {
date: '='
},
link: function (scope) {
scope.dateInput = dateFilter(scope.date, 'MM/dd/yyyy');
scope.$watch('date', function (newVal) {
if (newVal !== scope.tmp) {
if (!newVal) {
scope.dateInput = null;
} else {
scope.dateInput = dateFilter(scope.date, 'MM/dd/yyyy');
}
}
});
scope.getDatePattern = function () {
var exp = '/';
// Removed for brevity
exp += '/';
return exp;
};
scope.$watch('dateInput', function (newVal) {
if (newVal !== null) {
scope.date = new Date(newVal);
scope.tmp = scope.date;
}
});
}
};
JSFiddle here: https://jsfiddle.net/e5qu5rgy/1/
Any help at all is greatly appreciated!
So it looks like the problem can be fixed by changing the link function for the directive to be a controller function instead, as follows
.directive("plDate", function (dateFilter) {
return {
restrict: 'E',
replace: true,
template: '<input id="birthDateDir" name="birthDate" class="formField" type="text" ng-pattern="{{getDatePattern()}}" ng-model="dateInput">',
scope: {
date: '='
},
controller: function ($scope, $element, $attrs) {
$scope.dateInput = dateFilter($scope.date, 'MM/dd/yyyy');
$scope.$watch('date', function (newVal) {
if (newVal !== $scope.tmp) {
if (!newVal) {
$scope.dateInput = null;
} else if (newVal.toString() !== "Invalid Date") {
$scope.dateInput = dateFilter($scope.date, 'MM/dd/yyyy');
}
}
});
$scope.getDatePattern = function() {
var exp = '/';
// Months with 31 days
exp += '^(0?[13578]|1[02])[\/.](0?[1-9]|[12][0-9]|3[01])[\/.](18|19|20)[0-9]{2}$';
//Months with 30 days
exp += '|^(0?[469]|11)[\/.](0?[1-9]|[12][0-9]|30)[\/.](18|19|20)[0-9]{2}$';
// February in a normal year
exp += '|^(0?2)[\/.](0?[1-9]|1[0-9]|2[0-8])[\/.](18|19|20)[0-9]{2}$';
// February in a leap year
exp += '|^(0?2)[\/.]29[\/.](((18|19|20)(04|08|[2468][048]|[13579][26]))|2000)$';
exp += '/';
return exp;
};
$scope.$watch('dateInput', function (newVal) {
if (newVal !== null) {
$scope.date = new Date(newVal);
$scope.tmp = $scope.date;
}
});
}
};
});
Before going into production, the controller needs to be changed over to use an array for its arguments to protect against minification.

testing a directive's template function where attrs defined in link function

How do you test an angularjs directive's template function's attributes where the attributes have both functions and angular binding values that are defined in the link function?
This is the directive.
var app = angular.module('mmApp', []);
app.directive('mmField', function(){
return {
'restrict': 'E',
'priority': 5,
'replace': true,
'scope': {
'path': '#',
'label': '#',
'type': '#',
'editable': '#'
},
//this is the template function and this is where labelText() does not evaluate at least not where I test it.
'template': '<div class="mm-field">' +
'<label for="{{inputId()}}" ng-show="labelText()">{{labelText()}}</label> ' +
'</div>',
'link': function (scope, element, attrs) {
var query = null;
//this is where the labelText() function is defined
scope.labelText = function () {
var labelAttrValue = (scope.label || attrs['withLabel'] || '');
// cater for custom labels specified via the label or with-label attribute
if (labelAttrValue && labelAttrValue.toLowerCase() !== 'true' && labelAttrValue.toLowerCase() !== 'false') {
return (labelAttrValue || '') + ':';
} else if (labelAttrValue.toLowerCase() !== 'false' && scope.field) {
return (scope.field['name'] || 'FIELD_NAME_NOT_DEFINED') + ':';
} else if (labelAttrValue.toLowerCase() == 'false') {
return '';
} else {
return 'Loading...';
}
};
}
])
This is where the directive is on a html page.
<mm-field with-label editable="false" path="{{rootPath}}.name"></mm-field>
I am using mocha and chai test suite. This is how I want to test it.
describe('InputId', function () {
it.only('should generate an ID', function () {
var element = $($compile('<mm-field with-label="MONKEY" editable="false" path="something.name"></mm-field>' +
'</div>')($scope));
$scope.$digest();
expect(element.find('label').attr('ng-show')).to.equal('an evaluated value of labelText()');
});
});

Categories