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()');
});
});
Related
I have an Angular 1.5.8 directive with isolated scope and two functions being passed in. I have found that when the names of these functions are all lower case that they work correctly but if they are camelCased then they do not work.
Note that I am talking about the value of the param not the param name itself. here's the html that uses the directive:
<buttons-radio model="contactInformationAcceptable" disabled="approved" callback="personalapprovalfieldchanged()" focus="focuscallback($event)"></buttons-radio>
Note the case of the callback and focus values. If I change these to camel case (and change the function definitions in the parent scope) then they don't work.
Here is the directive:
angular.module("manageApp").directive('buttonsRadio', ['$timeout', function ($timeout) {
return {
restrict: 'E',
scope: {
model: '=',
disabled: '=',
callback: '&',
focus: '&'
},
template: '<div class="form-group yesorno"> ' +
' <div class="col-xs-12">' +
' <button type="button" class="btn btn-success" ng-disabled="disabled" ng-class="{active: yesValue}" ng-click="clickYes()" ng-focus="localFocus($event)">Yes</button>' +
' <button type="button" class="btn btn-danger" ng-disabled="disabled" ng-class="{active: noValue}" ng-click="clickNo()" ng-focus="localFocus($event)">No</button>' +
' </div>' +
'</div>',
controller: function ($scope) {
$scope.$watch('model', function (value) {
if (value) {
if (value == 0) {
$scope.yesValue = false;
$scope.noValue = false;
}
if (value == 1) {
$scope.yesValue = true;
$scope.noValue = false;
}
if (value == 2) {
$scope.yesValue = false;
$scope.noValue = true;
}
}
});
$scope.localFocus = function ($event) {
$scope.focus({ $event: $event });
}
$scope.performCallback = function () {
$timeout(function () {
$scope.callback();
});
}
$scope.yesValue = false;
$scope.noValue = false;
$scope.clickYes = function () {
$scope.yesValue = !$scope.yesValue;
if ($scope.yesValue) {
$scope.noValue = false;
$scope.model = 1;
} else {
$scope.model = 0;
}
$scope.performCallback();
}
$scope.clickNo = function () {
$scope.noValue = !$scope.noValue;
if ($scope.noValue) {
$scope.yesValue = false;
$scope.model = 2;
} else {
$scope.model = 0;
}
$scope.performCallback();
}
}
}
}]);
Edit: Here is the parent controller that has the function I need to use:
angular.module("manageApp").controller('approvalPersonalController', ['$scope', '$http',
function ($scope, $http) {
//personalApprovalFieldChanged personalapprovalfieldchanged
$scope.personalapprovalfieldchanged = function () {
//a field has changed so save them all
console.log('field has changed - do something');
};
}]);
I am confused as to why this is as I have been through a Pluralsight course on directives and the camel case works OK in the plunks that I have created from the course but not in this real world example.
It does work (calls the correct functions at the correct times) but I would like to use camel case for the function names if possible.
Thanks
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);
}
}
}]);
I edited the TypeAhead directive that is part of Angular UI for AngularJS so it will only give suggestions based on the most recent word, delimited by space (" ").
I intend to use it to for something like a query builder, dynamically giving suggestions based on surrounding syntax. This works as expected for the first word, but once we get to the second word, the promise does not resolve anymore for some reason. The value of inputValue is correct and as expected, but the code inside
$q.when(parserResult.source(originalScope, locals)).then(function (matches) {
does not appear to get run. Please advise.
My code (exactly the same as original except for I added a function called getLastWord that truncates the current expression :
angular.module('customTypeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
.factory('customTypeaheadParser', ['$parse', function ($parse) {
// 00000111000000000000022200000000000000003333333333333330000000000044000
var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
return {
parse: function (input) {
var match = input.match(TYPEAHEAD_REGEXP);
if (!match) {
throw new Error(
'Expected customTypeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
' but got "' + input + '".');
}
return {
itemName: match[3],
source: $parse(match[4]),
viewMapper: $parse(match[2] || match[1]),
modelMapper: $parse(match[1])
};
}
};
}])
.directive('customTypeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$rootScope', '$position', 'customTypeaheadParser',
function ($compile, $parse, $q, $timeout, $document, $rootScope, $position, customTypeaheadParser) {
var HOT_KEYS = [9, 13, 27, 38, 40];
return {
require: 'ngModel',
link: function (originalScope, element, attrs, modelCtrl) {
//SUPPORTED ATTRIBUTES (OPTIONS)
//minimal no of characters that needs to be entered before customTypeahead kicks-in
var minLength = originalScope.$eval(attrs.customTypeaheadMinLength);
if (!minLength && minLength !== 0) {
minLength = 0;
}
//minimal wait time after last character typed before customTypeahead kicks-in
var waitTime = originalScope.$eval(attrs.customTypeaheadWaitMs) || 0;
//should it restrict model values to the ones selected from the popup only?
var isEditable = originalScope.$eval(attrs.customTypeaheadEditable) !== false;
//binding to a variable that indicates if matches are being retrieved asynchronously
var isLoadingSetter = $parse(attrs.customTypeaheadLoading).assign || angular.noop;
//a callback executed when a match is selected
var onSelectCallback = $parse(attrs.customTypeaheadOnSelect);
var inputFormatter = attrs.customTypeaheadInputFormatter ? $parse(attrs.customTypeaheadInputFormatter) : undefined;
var appendToBody = attrs.customTypeaheadAppendToBody ? originalScope.$eval(attrs.customTypeaheadAppendToBody) : false;
var focusFirst = originalScope.$eval(attrs.customTypeaheadFocusFirst) !== false;
//INTERNAL VARIABLES
//model setter executed upon match selection
var $setModelValue = $parse(attrs.ngModel).assign;
//expressions used by customTypeahead
var parserResult = customTypeaheadParser.parse(attrs.customTypeahead);
var hasFocus;
//create a child scope for the customTypeahead directive so we are not polluting original scope
//with customTypeahead-specific data (matches, query etc.)
var scope = originalScope.$new();
originalScope.$on('$destroy', function () {
scope.$destroy();
});
// WAI-ARIA
var popupId = 'customTypeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
element.attr({
'aria-autocomplete': 'list',
'aria-expanded': false,
'aria-owns': popupId
});
//pop-up element used to display matches
var popUpEl = angular.element('<div custom-typeahead-popup></div>');
popUpEl.attr({
id: popupId,
matches: 'matches',
active: 'activeIdx',
select: 'select(activeIdx)',
query: 'query',
position: 'position'
});
//custom item template
if (angular.isDefined(attrs.customTypeaheadTemplateUrl)) {
popUpEl.attr('template-url', attrs.customTypeaheadTemplateUrl);
}
var resetMatches = function () {
scope.matches = [];
scope.activeIdx = -1;
element.attr('aria-expanded', false);
};
var getMatchId = function (index) {
return popupId + '-option-' + index;
};
// Indicate that the specified match is the active (pre-selected) item in the list owned by this customTypeahead.
// This attribute is added or removed automatically when the `activeIdx` changes.
scope.$watch('activeIdx', function (index) {
if (index < 0) {
element.removeAttr('aria-activedescendant');
} else {
element.attr('aria-activedescendant', getMatchId(index));
}
});
var getLastWord = function (expression) {
if (expression === "") {
return "";
}
var temp = expression.split(" ");
return temp[temp.length - 1];
};
var getMatchesAsync = function (inputValue) {
inputValue = getLastWord(inputValue);
var locals = {$viewValue: inputValue};
isLoadingSetter(originalScope, true);
$q.when(parserResult.source(originalScope, locals)).then(function (matches) {
//it might happen that several async queries were in progress if a user were typing fast
//but we are interested only in responses that correspond to the current view value
var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
if (onCurrentRequest && hasFocus) {
if (matches && matches.length > 0) {
scope.activeIdx = focusFirst ? 0 : -1;
scope.matches.length = 0;
//transform labels
for (var i = 0; i < matches.length; i++) {
locals[parserResult.itemName] = matches[i];
scope.matches.push({
id: getMatchId(i),
label: parserResult.viewMapper(scope, locals),
model: matches[i]
});
}
scope.query = inputValue;
//position pop-up with matches - we need to re-calculate its position each time we are opening a window
//with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
//due to other elements being rendered
scope.position = appendToBody ? $position.offset(element) : $position.position(element);
scope.position.top = scope.position.top + element.prop('offsetHeight');
element.attr('aria-expanded', true);
} else {
resetMatches();
}
}
if (onCurrentRequest) {
isLoadingSetter(originalScope, false);
}
}, function () {
resetMatches();
isLoadingSetter(originalScope, false);
});
};
resetMatches();
//we need to propagate user's query so we can highlight matches
scope.query = undefined;
//Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
var timeoutPromise;
var scheduleSearchWithTimeout = function (inputValue) {
timeoutPromise = $timeout(function () {
getMatchesAsync(inputValue);
}, waitTime);
};
var cancelPreviousTimeout = function () {
if (timeoutPromise) {
$timeout.cancel(timeoutPromise);
}
};
//plug into $parsers pipeline to open a customTypeahead on view changes initiated from DOM
//$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
modelCtrl.$parsers.unshift(function (inputValue) {
inputValue = getLastWord(inputValue);
hasFocus = true;
if (minLength === 0 || inputValue && inputValue.length >= minLength) {
if (waitTime > 0) {
cancelPreviousTimeout();
scheduleSearchWithTimeout(inputValue);
} else {
getMatchesAsync(inputValue);
}
} else {
isLoadingSetter(originalScope, false);
cancelPreviousTimeout();
resetMatches();
}
if (isEditable) {
return inputValue;
} else {
if (!inputValue) {
// Reset in case user had typed something previously.
modelCtrl.$setValidity('editable', true);
return inputValue;
} else {
modelCtrl.$setValidity('editable', false);
return undefined;
}
}
});
modelCtrl.$formatters.push(function (modelValue) {
var candidateViewValue, emptyViewValue;
var locals = {};
// The validity may be set to false via $parsers (see above) if
// the model is restricted to selected values. If the model
// is set manually it is considered to be valid.
if (!isEditable) {
modelCtrl.$setValidity('editable', true);
}
if (inputFormatter) {
locals.$model = modelValue;
return inputFormatter(originalScope, locals);
} else {
//it might happen that we don't have enough info to properly render input value
//we need to check for this situation and simply return model value if we can't apply custom formatting
locals[parserResult.itemName] = modelValue;
candidateViewValue = parserResult.viewMapper(originalScope, locals);
locals[parserResult.itemName] = undefined;
emptyViewValue = parserResult.viewMapper(originalScope, locals);
return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
}
});
scope.select = function (activeIdx) {
//called from within the $digest() cycle
var locals = {};
var model, item;
locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
model = parserResult.modelMapper(originalScope, locals);
$setModelValue(originalScope, model);
modelCtrl.$setValidity('editable', true);
modelCtrl.$setValidity('parse', true);
onSelectCallback(originalScope, {
$item: item,
$model: model,
$label: parserResult.viewMapper(originalScope, locals)
});
resetMatches();
//return focus to the input element if a match was selected via a mouse click event
// use timeout to avoid $rootScope:inprog error
$timeout(function () {
element[0].focus();
}, 0, false);
};
//bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
element.bind('keydown', function (evt) {
//customTypeahead is open and an "interesting" key was pressed
if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
return;
}
// if there's nothing selected (i.e. focusFirst) and enter is hit, don't do anything
if (scope.activeIdx == -1 && (evt.which === 13 || evt.which === 9)) {
return;
}
evt.preventDefault();
if (evt.which === 40) {
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
scope.$digest();
} else if (evt.which === 38) {
scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
scope.$digest();
} else if (evt.which === 13 || evt.which === 9) {
scope.$apply(function () {
scope.select(scope.activeIdx);
});
} else if (evt.which === 27) {
evt.stopPropagation();
resetMatches();
scope.$digest();
}
});
element.bind('blur', function (evt) {
hasFocus = false;
});
// Keep reference to click handler to unbind it.
var dismissClickHandler = function (evt) {
if (element[0] !== evt.target) {
resetMatches();
if (!$rootScope.$$phase) {
scope.$digest();
}
}
};
$document.bind('click', dismissClickHandler);
originalScope.$on('$destroy', function () {
$document.unbind('click', dismissClickHandler);
if (appendToBody) {
$popup.remove();
}
// Prevent jQuery cache memory leak
popUpEl.remove();
});
var $popup = $compile(popUpEl)(scope);
if (appendToBody) {
$document.find('body').append($popup);
} else {
element.after($popup);
}
}
};
}])
.directive('customTypeaheadPopup', function () {
return {
restrict: 'EA',
scope: {
matches: '=',
query: '=',
active: '=',
position: '&',
select: '&'
},
replace: true,
templateUrl: 'html/templates/custom-typeahead-popup.html',
link: function (scope, element, attrs) {
scope.templateUrl = attrs.templateUrl;
scope.isOpen = function () {
return scope.matches.length > 0;
};
scope.isActive = function (matchIdx) {
return scope.active == matchIdx;
};
scope.selectActive = function (matchIdx) {
scope.active = matchIdx;
};
scope.selectMatch = function (activeIdx) {
scope.select({activeIdx: activeIdx});
};
}
};
})
.directive('customTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function ($templateRequest, $compile, $parse) {
return {
restrict: 'EA',
scope: {
index: '=',
match: '=',
query: '='
},
link: function (scope, element, attrs) {
var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'html/templates/custom-typeahead-match.html';
$templateRequest(tplUrl).then(function (tplContent) {
$compile(tplContent.trim())(scope, function (clonedElement) {
element.replaceWith(clonedElement);
});
});
}
};
}])
.filter('customTypeaheadHighlight', function () {
function escapeRegexp(queryToEscape) {
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
}
return function (matchItem, query) {
return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem;
};
});
I have the following directive:
app.directive('pagedownAdmin', ['$compile', '$timeout', function ($compile, $timeout) {
var nextId = 0;
var converter = Markdown.getSanitizingConverter();
converter.hooks.chain("preBlockGamut", function (text, rbg) {
return text.replace(/^ {0,3}""" *\n((?:.*?\n)+?) {0,3}""" *$/gm, function (whole, inner) {
return "<blockquote>" + rbg(inner) + "</blockquote>\n";
});
});
return {
restrict: "E",
scope: {
content: "=",
modal: '=modal'
},
template: '<div class="pagedown-bootstrap-editor"></div>',
link: function (scope, iElement, attrs) {
var editorUniqueId;
if (attrs.id == null) {
editorUniqueId = nextId++;
} else {
editorUniqueId = attrs.id;
}
scope.hideDiv = function () {
document.getElementById("wmd-button-bar-" + editorUniqueId).style.display = 'none';
};
scope.showDiv = function () {
document.getElementById("wmd-button-bar-" + editorUniqueId).style.display = 'block';
};
scope;
var newElement = $compile(
'<div>' +
'<div class="wmd-panel">' +
'<div data-ng-hide="modal.wmdPreview == true" id="wmd-button-bar-' + editorUniqueId + '" style="display:none;"></div>' +
'<textarea on-focus="showDiv()" on-blur="hideDiv()" data-ng-hide="modal.wmdPreview == true" class="wmd-input" id="wmd-input-' + editorUniqueId + '" ng-model="content" >' +
'</textarea>' +
'</div>' +
'<div data-ng-show="modal.wmdPreview == true" id="wmd-preview-' + editorUniqueId + '" class="pagedownPreview wmd-panel wmd-preview">test div</div>' +
'</div>')(scope)
;
iElement.append(newElement);
var help = angular.isFunction(scope.help) ? scope.help : function () {
// redirect to the guide by default
$window.open("http://daringfireball.net/projects/markdown/syntax", "_blank");
};
var editor = new Markdown.Editor(converter, "-" + editorUniqueId, {
handler: help
});
var editorElement = angular.element(document.getElementById("wmd-input-" + editorUniqueId));
editor.hooks.chain("onPreviewRefresh", function () {
// wire up changes caused by user interaction with the pagedown controls
// and do within $apply
$timeout(function () {
scope.content = editorElement.val();
});
});
editor.run();
}
}
}]);
Inside I have the showDiv and hideDiv function that would show and hide the page editor's menu when I click in and out of the textarea.
I am passing the functions to an event inside the compile:
//First try
<textarea onfocus="showDiv()" onblur="hideDiv()"></textarea>
When I click inside and outside the textarea I get the errors:
Uncaught ReferenceError: on is not defined
Uncaught ReferenceError: off is not defined
//Second try
<textarea on-focus="showDiv()" on-blur="hideDiv()"></textarea>
When I click in and out of textarea nothing is happening. No errors but not calling the function either.
Can anyone point me to the right direction? Thanks
Instead of using the same scope, instantiate a new scope (scope.$new()) and assign the functions to this new scope. Because otherwise you will override the function-references assigned by the scope-declaration to the scope-object.
var newScope = scope.$new();
newScope.hideDiv = function() {...};
newScope.showDiv = function() {...};
...
var newElement = $compile(...)(newScope);
And to use the function-references given to the original scope (of the directive) you can call those in the new-scopes functions (e.g. function() { scope.hideDiv(); }).
Working plnkr:
http://plnkr.co/edit/nGux3DOsrcPBcmz43p2A?p=preview
https://docs.angularjs.org/api/ng/service/$compile
https://code.angularjs.org/1.3.16/docs/api/ng/type/$rootScope.Scope#$new
Thank you guys for trying to help. I have found what was wrong with my code. I did a very silly/noob mistake. I used on-focus instead of ng-focus and on-blur instead of ng-blur.
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