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.
Related
I'm not much in Angularjs and use the directive that put focus on particular element. It looks as following:
appModule.directive('autoFocus', function ($timeout) {
return {
restrict: 'AC',
link: function (scope, element, attrs) {
$timeout(function () {
element[0].focus();
}, 0);
}
};
});
The usage looks as following:
<button auto-focus class="uui-button lime-green btn" ng-click="copyToClipboard()">
Copy
</button>
I'd like to rearrange directive above to have ability to write: auto-focus="true" or auto-focus="false".
UPDATE:
I've updated code as shown below, but it doesn't work that is the focus is always there regardless I write auto-focus="true" or auto-focus="false".
appModule.directive('autoFocus', function ($timeout) {
return {
restrict: 'AC',
link: function (scope, element, attrs) {
var hasFocus = attrs.autoFocus;
if (hasFocus) {
$timeout(function () {
element[0].focus();
}, 0);
}
}
};
});
You can set the property to true or false and access the value via attrs.autoFocus inside directive's link function
Edit:
appModule.directive('autoFocus', function ($timeout) {
return {
restrict: 'AC',
link: function (scope, element, attrs) {
var hasFocus = attrs.autoFocus;
if (hasFocus ==="true") {
$timeout(function () {
element[0].focus();
}, 0);
}
}
};
});
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().
Was wondering how I should handle async functions in $parsers.
The below code doesn't update the scope.
I'm using AngularJS 1.2 so can't make use of the new and fancy 1.3 features.
http://plnkr.co/edit/uk9VMipYNphzk8l7p9iZ?p=preview
Markup:
<input type="text" name="test" ng-model="test" parse>
Directive:
app.directive('parse', function($timeout) {
return {
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
$timeout(function() {
return viewValue;
});
});
}
};
});
If you are looking for async validation function, I did something like that some time ago and release it as a library. Check the custom-remote-validator directive here.
The basic idea was use ngModelController $setValidity after receiving validation result from server. This is the directive source code
.directive('customRemoteValidator', [function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elm, attr, ngModelCtrl) {
var validateFunctionNames = attr["remoteValidateFunctions"].split(",");
var validatorNames = attr["customRemoteValidator"].split(",");
ngModelCtrl.$parsers.push(function (value) {
angular.forEach(validateFunctionNames, function (functionName, index) {
if (!scope[functionName]) {
console.log('There is no function with ' + functionName + ' available on the scope. Please make sure the function exists on current scope or its parent.');
} else {
var result = scope[functionName](value);
if (result.then) {
result.then(function (data) { //For promise type result object
ngModelCtrl.$setValidity(validatorNames[index], data);
}, function (error) {
ngModelCtrl.$setValidity(validatorNames[index], false);
});
}
}
});
return value;
});
}
};
}])
Having a directive in angular that is a reusable component, what is the best practice to expose a public API that can be accessed from the controller?
So when there are multiple instances of the component you can have access from the controller
angular.directive('extLabel', function {
return {
scope: {
name: '#',
configObj: '='
},
link: function(scope, iElement, iAttrs) {
// this could be and exposed method
scope.changeLabel = function(newLabel) {
scope.configObj.label = newLabel;
}
}
}
});
Then when having:
<ext-label name="extlabel1" config-obj="label1"></ext-label>
<ext-label name="extlabel2" config-obj="label2"></ext-label>
<ext-label name="extlabel3" config-obj="label3"></ext-label>
How can I get the access the scope.changeLabel of extLabel2 in a controller?
Does it make sense?
Does this work for you?
angular.directive('extLabel', function() {
return {
restrict: 'E',
scope: {
api: '='
},
link: function(scope, iElement, iAttrs) {
scope.api = {
doSomething: function() { },
doMore: function() { }
};
}
};
});
From containing parent
<ext:label api="myCoolApi"></ext:label>
And in controller
$scope.myCoolApi.doSomething();
$scope.myCoolApi.doMore();
I like Andrej's and use this pattern regularly, but I would like to suggest some changes to it
angular.directive('extLabel', function {
return {
scope: {
api: '=?',
configObj: '='
},
// A controller, and not a link function. From my understanding,
// try to use the link function for things that require post link actions
// (for example DOM manipulation on the directive)
controller: ['$scope', function($scope) {
// Assign the api just once
$scope.api = {
changeLabel: changeLabel
};
function changeLabel = function(newLabel) {
$scope.configObj.label = newLabel;
}
}]
}
});
<ext-label name="extlabel1" config-obj="label1"></ext-label>
<ext-label api="label2api" name="extlabel2" config-obj="label2"></ext-label>
<ext-label name="extlabel3" config-obj="label3"></ext-label>
In controller of course label2api.changeLabel('label')
I faced this problem when writing a directive to instantiate a dygraph chart in my Angular applications. Although most of the work can be done by data-binding, some parts of the API require access to the dygraph object itself. I solved it by $emit()ing an event:
'use strict';
angular.module('dygraphs', []);
angular.module('dygraphs').directive('mrhDygraph', function ($parse, $q) {
return {
restrict: 'A',
replace: true,
scope: {data: '=', initialOptions: '#', options: '='},
link: function (scope, element, attrs) {
var dataArrived = $q.defer();
dataArrived.promise.then(function (graphData) {
scope.graph = new Dygraph(element[0], graphData, $parse(scope.initialOptions)(scope.$parent));
return graphData.length - 1;
}).then(function(lastPoint) {
scope.graph.setSelection(lastPoint);
scope.$emit('dygraphCreated', element[0].id, scope.graph);
});
var removeInitialDataWatch = scope.$watch('data', function (newValue, oldValue, scope) {
if ((newValue !== oldValue) && (newValue.length > 0)) {
dataArrived.resolve(newValue);
removeInitialDataWatch();
scope.$watch('data', function (newValue, oldValue, scope) {
if ((newValue !== oldValue) && (newValue.length > 0)) {
var selection = scope.graph.getSelection();
if (selection > 0) {
scope.graph.clearSelection(selection);
}
scope.graph.updateOptions({'file': newValue});
if ((selection >= 0) && (selection < newValue.length)) {
scope.graph.setSelection(selection);
}
}
}, true);
scope.$watch('options', function (newValue, oldValue, scope) {
if (newValue !== undefined) {
scope.graph.updateOptions(newValue);
}
}, true);
}
}, true);
}
};
});
The parameters of the dygraphCreated event include the element id as well as the dygraph object, allowing multiple dygraphs to be used within the same scope.
In my opinion, a parent shouldn't access a children scope. How would you know which one to use and which one to not use. A controller should access his own scope or his parent scopes only. It breaks the encapsulation otherwise.
If you want to change your label, all you really need to do is change the label1/label2/label3 variable value. With the data-binding enabled, it should work. Within your directive, you can $watch it if you need some logic everytime it changes.
angular.directive('extLabel', function {
return {
scope: {
name: '#',
configObj: '='
},
link: function(scope, iElement, iAttrs) {
scope.$watch("configObj", function() {
// Do whatever you need to do when it changes
});
}
}
});
Use these directives on the element that you want to go prev and next:
<carousel>
<slide>
<button class="action" carousel-next> Next </button>
<button class="action" carousel-prev> Back </button>
</slide>
</carousel>
.directive('carouselNext', function () {
return {
restrict: 'A',
scope: {},
require: ['^carousel'],
link: function (scope, element, attrs, controllers) {
var carousel = controllers[0];
function howIsNext() {
if ((carousel.indexOfSlide(carousel.currentSlide) + 1) === carousel.slides.length) {
return 0;
} else {
return carousel.indexOfSlide(carousel.currentSlide) + 1;
}
}
element.bind('click', function () {
carousel.select(carousel.slides[howIsNext()]);
});
}
};
})
.directive('carouselPrev', function () {
return {
restrict: 'A',
scope: {},
require: ['^carousel'],
link: function (scope, element, attrs, controllers) {
var carousel = controllers[0];
function howIsPrev() {
if (carousel.indexOfSlide(carousel.currentSlide) === 0) {
return carousel.slides.length;
} else {
return carousel.indexOfSlide(carousel.currentSlide) - 1;
}
}
element.bind('click', function () {
carousel.select(carousel.slides[howIsPrev()]);
});
}
};
})