I have two directives in a single textbox. One is datetimepicker and focus directive.
DateTimePicker :
app.directive('datetimepicker', function() {
return {
restrict: 'A',
require : '?ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
element.datetimepicker({
format: "yyyy-mm-dd hh:ii:ss",
autoclose: true,
todayBtn: true
}).on('setDate', function(e) {
ngModelCtrl.$setViewValue(e.date);
scope.$apply();
});
}
};
});
Focus :
app.directive('focus',function($timeout) {
return {
restrict: 'A',
scope : {
trigger : '#focus'
},
link : function(scope, elem,attrs) {
var focusables = $(":focusable");
scope.$watch('trigger', function(value) {
if (value === "true") {
$timeout(function() {
elem[0].focus();
});
}
});
elem.bind('keydown', function(e) {
var code = e.keyCode || e.which;
if (code === 13) {
var current = focusables.index(this);
var next = focusables.eq(current + 1).length ? focusables.eq(current + 1) : focusables.eq(0);
next.focus();
e.preventDefault();
}
});
}
};
});
This is my textbox
<input datetimepicker ng-model='dob' focus/>
I am getting the below error
Error: [$compile:multidir] Multiple directives [focus, datepicker] asking for new/isolated scope on: <input datepicker="" ng-model="dob" focus="">
How to make these two directives work together in a textbox?
Try not to provide isolate scope manually to first directive because you don't need that so use scope:false, it will work so.
app.directive('datetimepicker', function() {
return {
restrict: 'A',
require : '?ngModel',
scope: false,
link: function(scope, element, attrs, ngModelCtrl) {
element.datetimepicker({
format: "yyyy-mm-dd hh:ii:ss",
autoclose: true,
todayBtn: true
}).on('setDate', function(e) {
ngModelCtrl.$setViewValue(e.date);
scope.$apply();
});
}
};
});
The problem you're getting is when at least two directives require new or isolated scopes.
As far as I can tell, your focus directive does not actually require isolated scope, as you can achieve focus using attrs.trigger.
Try to change your focus directive to:
app.directive('focus',function($timeout) {
return {
restrict: 'A',
link : function(scope, elem,attrs) {
var focusables = $(":focusable");
scope.$watch(attrs.trigger, function(value) {
if (value === "true") {
$timeout(function() {
elem[0].focus();
});
}
});
elem.bind('keydown', function(e) {
var code = e.keyCode || e.which;
if (code === 13) {
var current = focusables.index(this);
var next = focusables.eq(current + 1).length ? focusables.eq(current + 1) : focusables.eq(0);
next.focus();
e.preventDefault();
}
});
}
};
});
Related
I want to encapsulate a inputBox into AngularJS component. This component will automatically add some prefix to the input before it's passed to binding model data. For example the prefix is "testPrefix", when user input "ABC", the corresponding model data will be "testPrefixABC".
My code is like this:
angular.module('').component('dummyBox', {
bindings: {
ngModel: '='
},
require: {
ngModelCtrl: 'ngModel'
},
template: '<span><input type="text" ng-model="$ctrl.ngModel" /></span>',
controller: function() {
$ctrl.$onInit = () => {
$ctrl.ngModelCtrl.$parsers.push((viewValue) => {
if(viewValue) return "testPrefix" + viewValue;
});
$ctrl.ngModelCtrl.$formatters.push((modelValue) => {
return modelValue.substr("textPrefix".length);
});
}
}
});
<dummy-box ng-model="outScopeVar"></dummy-box>
<label>{{outScopeVar}}</label>
For now it's not working, the content in label is still the input value, no prefix string added. Any help will be appreciated.
Thanks in advance.
the above code works with directive instead of component,
app.directive("inputEncaps", function(){
return {
require: "ngModel",
link: function(scope, element, attrs, ngModel){
ngModel.$parsers.push((viewValue) => {
if(viewValue && !viewValue.includes("textPrefix")) {
return "testPrefix" + viewValue;
} else {
return viewValue;
}
});
ngModel.$formatters.push((modelValue) => {
let model = modelValue && modelValue.length >= "textPrefix".length ?
modelValue.substr("textPrefix".length) : modelValue;
return modelValue;
});
}
};
});
<input ng-model="inputVar" input-encaps/>
<label>{{inputVar}}</label>
I have created a directive for textarea which allows to edit data manipulation. In a form there are multiple textarea which as same directive. And when I trigger any event it traverse all elements which has assign directive.
Eg.There are 10 textarea fields and having directive "HzAutosave". Then when I trigger click on particular textarea it returns particular textarea's value, but it triggers socket io event to send data to server 10 times, whether there is only changes in a single field.
Directive
.directive("hzAutoSaveTextarea", ['$timeout', '$interval', 'HzSocket', function ($timeout, $interval, HzSocket) {
var currentElement = null;
return {
restrict: "E",
replace: true,
require: "ngModel",
scope: {},
template: "<textarea></textarea>",
compile: function (scope, element) {
return {
pre: function (scope, element, attrs) {
element.on("click", function (e) {
e.preventDefault();
currentElement = attrs.id;
console.log("focused element:" + currentElement);
angular.element(".autosave").removeClass("active-element");
element.addClass("active-element");
});
},
post: function (scope, element, attrs) {
var currentVal = null, previousVal = null;
$interval(function () {
currentVal = angular.element(".active-element").val();
if (null !== currentVal && undefined !== currentVal && "" !== currentVal) {
console.log("value::" + angular.element(".active-element").val());
if (previousVal !== currentVal) {
console.log("save data to console");
previousVal = currentVal;
var socket = io();
var data = {module: "photo", element: currentElement, value: currentVal};
HzSocket.emit('autosave', data);
}
}
}, 3000);
}
}
},
link: function (scope, element, attrs, ngModel) {
}
}
}]);
HTML
<hz-auto-save-textarea ng-model="asContainer" placeholder="Description" id="{{result.photo_id}}" class="album_commentarea margin_top5 autosave"></hz-auto-save-textarea>
I don't know how to prevent triggering multiple time socket request and other qualified events of JavaScript.
Move the autosave logic to the directive factory function which is executed only once:
.directive("hzAutoSaveTextarea", ['$timeout', '$interval', 'HzSocket', function ($timeout, $interval, HzSocket) {
var currentElement = null;
var currentVal = null, previousVal = null;
$interval(function () {
currentVal = angular.element(".active-element").val();
if (null !== currentVal && undefined !== currentVal && "" !== currentVal) {
console.log("value::" + angular.element(".active-element").val());
if (previousVal !== currentVal) {
console.log("save data to console");
previousVal = currentVal;
var socket = io();
var data = {module: "photo", element: currentElement, value: currentVal};
HzSocket.emit('autosave', data);
}
}
}, 3000);
return {
restrict: "E",
replace: true,
require: "ngModel",
scope: {},
template: "<textarea></textarea>",
compile: function (scope, element) {
return {
pre: function (scope, element, attrs) {
element.on("click", function (e) {
e.preventDefault();
currentElement = attrs.id;
console.log("focused element:" + currentElement);
angular.element(".autosave").removeClass("active-element");
element.addClass("active-element");
});
},
post: function (scope, element, attrs) {
}
}
}
}
}]);
This way only one $interval will be setup which is what you need.
One interval is created for each directive, therefore you get those 10 events. I'd suggest moving the interval to a common service.
I'm trying to build a characterCounter attribute directive for input fields. My thoughts are to require: 'ngModel' to get the length of the modelValue in ng-model and to pass a max-length in the scope of the directive.
<input ng-model="inputModel" max-character-counter max-length="10"/>
I have my directive most of the way there I'm just struggling with how I get the view to update. Any help with this is greatly appreciated.
angular.module('app').directive('maxCharacterCounter', [function(){
return {
restrict: 'A',
require: '?ngModel',
scope: {
maxLength: "="
},
link: function (scope, elem, attrs, ngModel) {
if (!ngModel) return;
console.log(ngModel);
ngModel.$render = function() {
console.log('render');
var el = angular.element(attrs.$$element);
el.after('<span class="input-group-addon">' + scope.charactersLeft + '</span>');
}
elem.on('blur keyup change', function() {
scope.$evalAsync(read);
});
read();
function read() {
scope.charactersLeft = ngModel.$modelValue.length == undefined ? scope.maxLength : scope.maxLength - ngModel.$modelValue.length;
console.log('Characters Left:', scope.charactersLeft);
console.log('View Value: ', ngModel.$viewValue);
console.log('Model Value: ', ngModel.$modelValue);
updateViewValue();
}
function updateViewValue() {
//How do I update the view for scope.charactersLeft
//ngModel.$viewValue(scope.charactersLeft);
console.log('scope: ', scope);
}
}
}}]);
Plunker: http://plnkr.co/edit/F0PzE6?p=preview
Final Solution:
angular.module('app').directive('maxCharacterCounter', ['$timeout', '$log', function($timeout, $log) {
return {
restrict: 'A',
require: '?ngModel',
scope: {
maxLength: "#"
},
link: function (scope, elem, attrs, ngModel) {
if (!ngModel) {
$log.warn('ngModel doesn\'t exist. There is no way to calculate characters left');
return;
}
elem.wrap('<div class="input-group"></div>');
elem.after('<span class="input-group-addon"></span>');
elem.on('blur keyup keydown change', function() {
scope.$eval(updateCharacterCount);
updateViewValue();
});
$timeout(function(){
scope.maxLength = scope.maxLength || 140;
scope.$eval(updateCharacterCount);
updateViewValue();
});
function updateCharacterCount() {
scope.charactersLeft = !ngModel.$viewValue ? scope.maxLength : scope.maxLength - ngModel.$viewValue.length;
}
function updateViewValue() {
var element = elem.next('span');
element.text(scope.charactersLeft);
element.toggleClass('redText', scope.charactersLeft <= 0 ? true : false);
}
}
}}]);
Try like below. add the second parameter of module.
angular.module('app', [])
.directive('maxCharacterCounter', function() {
return {
restrict: 'A',
require: '?ngModel',
scope: {
maxLength: "="
},
link: function(scope, elem, attrs, ngModel) {
if (!ngModel) return;
ngModel.$render = function() {
console.log('render');
var el = angular.element(attrs.$$element);
el.after('<span ng-bind="charactersLeft" class="input-group-addon">' + scope.charactersLeft + '</span>');
}
elem.on('blur keyup change', function() {
scope.$evalAsync(read);
});
read();
function read() {
scope.charactersLeft = ngModel.$modelValue.length == undefined ? scope.maxLength : scope.maxLength - ngModel.$modelValue.length;
updateViewValue();
}
function updateViewValue() {
angular.element(attrs.$$element).next('span').text(scope.charactersLeft);
}
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body ng-app="app">
<input ng-model="inputModel" max-character-counter max-length="10"/>
</body>
In Angular you can literally bind to the length of your input model, like so:
{{inputModel.length}}
Unless I'm missing something, isnt this all you need?
I have 2 directives: calculatorForm and calculatorAttribute. CalculatorForm is the parent directive, specifically a form which contains input tags which are calculatorAttribute directives.
I want the calculatorAttribute call calculatorForm function that changes a scope variable and trigger a watcher.
Here's my code:
angular
.module('calculator')
.directive('calculatorForm', ['CalculatorDataModel', 'CalculatorPriceModel',
function(CalculatorDataModel, CalculatorPriceModel) {
return {
restrict : 'A',
replace : true,
templateUrl : function(element, attrs) {
return attrs.templateUrl;
},
link : function(scope, element, attrs) {
scope.model = CalculatorDataModel;
scope.price = CalculatorPriceModel;
scope.model.initialize(calculator_data);
scope.updateSelectedSpecs = function(attribute_id, prod_attr_val_id) {
var selected_specs = JSON.parse(JSON.stringify(scope.model.selected_specs));
selected_specs[attribute_id] = prod_attr_val_id;
scope.model.selected_specs = selected_specs;
}
scope.$watch('model.selected_specs', function(selected_specs, previous_selected_specs) {
if (selected_specs != previous_selected_specs) {
scope.model.setCalculatorData();
scope.price.computePrice();
}
});
}
}
}
])
.directive('calculatorAttribute', [
function() {
return {
restrict : 'A',
template : "<input type='radio' name='attr{{attribute_id}}' ng-value='prod_attr_val_id'/>",
replace : true,
link : function(scope, element, attrs) {
scope.attribute_id = attrs.attributeId;
scope.prod_attr_val_id = attrs.prodAttrValId;
element.on('click', function() {
scope.$parent.updateSelectedSpecs(scope.attribute_id, scope.prod_attr_val_id);
});
}
}
}
]);
My problem is updateSelectedSpecs in the parent is called but watcher has never been triggered when I use element.on click in the child directive.
Please help everyone Thank you!!!
Okay, after wrestling with this for a bit, I managed to produce a working version of a slimmed-down example:
angular.module('myApp', [])
.directive('calculatorForm', function() {
return {
restrict: 'A',
replace: true,
transclude: true,
template: '<div ng-transclude></div>',
link: function(scope, element, attrs) {
scope.model = {};
scope.price = {};
scope.updateSelectedSpecs = function(attribute_id, prod_attr_val_id) {
scope.$apply(function() {
console.log('update selected specs');
var selected_specs = {};
selected_specs[attribute_id] = prod_attr_val_id;
scope.model.selected_specs = selected_specs;
});
}
scope.$watch('model.selected_specs', function(selected_specs, previous_selected_specs) {
console.log('new selected specs', selected_specs, previous_selected_specs);
if (selected_specs != previous_selected_specs) {
console.log("and they're different");
}
});
}
};
})
.directive('calculatorAttribute', function() {
return {
restrict: 'A',
template: "<input type='radio' name='attr{{attribute_id}}' ng-value='prod_attr_val_id'/>",
replace: true,
link: function(scope, element, attrs) {
scope.attribute_id = attrs.attributeId;
scope.prod_attr_val_id = attrs.prodAttrValId;
element.on('click', function() {
scope.$parent.updateSelectedSpecs(scope.attribute_id, scope.prod_attr_val_id);
});
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<form calculator-form ng-app="myApp">
<input calculator-attribute attribute-id=1 prod-attr-val-id=1>
</form>
Just look at the console to see it getting into the $watch. The problem seemed to be the fact that you didn't trigger a $digest cycle in your updateSelectedSpecs function. Usually a $timeout, $http call, or ngClick or other event would start the $digest cycle for you, but in this case you have to start it yourself using scope.$apply().
I have come across a problem my Angular knowledge is a little too limited to figure out. I want a non html5 placeholder attribute. Here is some code I found before on stack overflow that does this handsomely:
// Placeholder for non HTML5 browsers
app.directive("ngPlaceholder", function($log, $timeout) {
var txt;
return {
restrict: "A",
scope: { txt: "#ngPlaceholder" },
link: function(scope, elem, attrs) {
elem.on("focus", function() {
if(elem.val() === scope.txt) {
elem.val("");
}
scope.$apply()
})
elem.on("blur", function() {
if(elem.val() === "") {
elem.val(scope.txt);
}
scope.$apply()
})
// Initialise placeholder
$timeout(function() {
elem.val(scope.txt)
scope.$apply();
})
}
}
})
However... use it in conjunction with ng-model:
input(
type="text"
ng-model="card.number"
ng-placeholder="0000-0000-0000-0000")
And it obliterates the two way data binding!
Heres a plunker:
http://plnkr.co/edit/1AvVOxb5O6P5pU3wIuKv?p=preview
What am I missing?
Update Many people have voiced there solutions to this rather annoying problem here
Use $parent to refer the to model in the parent scope since the directive ngPlaceholder creates an isolated scope. (This is not specific to IE 9 though. )
<input type="text" ng-placeholder="0000-0000-0000-0000" ng-model="$parent.card.number2"/>
This one fixes the problem that in delay of $timeout, the $scope may be changed in meantime. It also makes it cross-browser.
// Placeholder for all browsers
app.directive("ngPlaceholder", function($log, $timeout) {
return {
restrict: "A",
link: function(scope, elem, attrs) {
var txt = attrs.ngPlaceholder,
model = attrs.ngModel,
placeholderSupport = 'placeholder' in document.createElement("input");
//Use HTML5 placeholder attribute.
if (placeholderSupport) {
attrs.$set("placeholder", txt);
return;
}
elem.on("focus", function(event) {
if (elem.val() === txt) {
elem.val("");
}
});
elem.on("blur", function(event) {
if (elem.val() === "") {
elem.val(txt);
}
});
scope.$watch(model, function (newValue, oldValue, scope) {
if (newValue === undefined || newValue === "") {
elem.val(txt);
//scope.$apply(); not needed, since scope fired this event.
}
}, true);
}
}
});
You seen the ngModel issue, so the isolate scope on ngPlaceholder should be removed. I realize that sza's workaround works, but the key thing I'd emphasize is that the ngPlaceholder doesn't need its own scope.
For example here I tweaked the directive and removed the references to scope by storing creating the txt variable as its own local variable.
http://plnkr.co/edit/43z1TZHFwmgLJ9wyystD?p=preview
// Placeholder for non HTML5 browsers
app.directive("ngPlaceholder", function($log, $timeout) {
var txt;
return {
restrict: "A",
link: function(scope, elem, attrs) {
var txt = attrs.ngPlaceholder;
elem.bind("focus", function() {
if(elem.val() === txt) {
elem.val("");
}
scope.$apply()
})
elem.bind("blur", function() {
if(elem.val() === "") {
elem.val(txt);
}
scope.$apply()
})
// Initialise placeholder
$timeout(function() {
elem.val(txt)
scope.$apply();
})
}
}
})
placeholder support in IE with normal color
app.directive('placeholder',['$timeout','$window', function($timeout,$window){
var i = document.createElement('input');
if ('placeholder' in i) {
return {}
}
return {
link: function(scope, elm, attrs){
var userAgent = $window.navigator.userAgent;
if(userAgent.indexOf("MSIE 9.0") <0){
return;
}
if (attrs.type === 'password') {
return;
}
$timeout(function(){
elm.val(attrs.placeholder).css({"color":'#ccc'});
elm.bind('focus', function(){
if (elm.val() == attrs.placeholder) {
elm.val('').css({"color":'#555'});
}
}).bind('blur', function(){
if (elm.val() == '') {
elm.val(attrs.placeholder).css({"color":'#ccc'});
}
});
});
}
}
}]);
live code here:http://plnkr.co/edit/ev6kQ3Ks31FhqAfCMDkc