Angularjs using component to encapsulate inputbox - angularjs

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>

Related

Calling parent directive scope function from child directive which triggers parent watcher

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().

Angularjs two directives on one textbox

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();
}
});
}
};
});

angular restriction to not allow spaces in text field

I do not want a user to enter spaces in a text field. I don't want it on submit validation but rather - a space will not show up on the text field when they click it.
The selected answer is arguably not very unobtrusive. And if you need to use it in multiple places, you'll end up with duplicated code.
I prefer to prevent the input of spaces using the following directive.
app.directive('disallowSpaces', function() {
return {
restrict: 'A',
link: function($scope, $element) {
$element.bind('input', function() {
$(this).val($(this).val().replace(/ /g, ''));
});
}
};
});
This directive can be invoked like this:
<input type="text" disallow-spaces>
<input ng-model="field" ng-trim="false" ng-change="field = field.split(' ').join('')" type="text">
Update:
To improve code quality you can create custom directive instead. But don't forget that your directive should prevent input not only from keyboard, but also from pasting.
<input type="text" ng-trim="false" ng-model="myValue" restrict-field="myValue">
Here is important to add ng-trim="false" attribute to disable trimming of an input.
angular
.module('app')
.directive('restrictField', function () {
return {
restrict: 'AE',
scope: {
restrictField: '='
},
link: function (scope) {
// this will match spaces, tabs, line feeds etc
// you can change this regex as you want
var regex = /\s/g;
scope.$watch('restrictField', function (newValue, oldValue) {
if (newValue != oldValue && regex.test(newValue)) {
scope.restrictField = newValue.replace(regex, '');
}
});
}
};
});
If you want to achieve it without writing directive
ng-keydown="$event.keyCode != 32 ? $event:$event.preventDefault()"
THe directive Jason wrote did not work for me. I had to change return false to: e.preventDefault() like so:
app.directive('disallowSpaces', function() {
return {
restrict: 'A',
link: function($scope, $element) {
$element.bind('keydown', function(e) {
if (e.which === 32) {
e.preventDefault();
}
});
}
}
});
This works to prevent entering any special chars including spaces:
app.directive('noSpecialChar', function() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, element, attrs, modelCtrl) {
modelCtrl.$parsers.push(function(inputValue) {
if (inputValue == null)
return ''
let cleanInputValue = inputValue.replace(/[^\w]|_/gi, '');
if (cleanInputValue != inputValue) {
modelCtrl.$setViewValue(cleanInputValue);
modelCtrl.$render();
}
return cleanInputValue;
});
}
}
});
Use without jquery
angular.module('app').directive('disallowSpaces', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: {
maxvalue: '=',
},
link: function ($scope, $element, attr, ngModelCtrl) {
$element.bind('keydown', function () {
function transformer(text) {
if (text) {
var transformedInput = text.replace(/ /g, '');
ngModelCtrl.$setViewValue(transformedInput);
ngModelCtrl.$render();
return transformedInput;
}
return undefined;
}
ngModelCtrl.$parsers.push(transformer);
});
},
};
});
// use disallow-spaces
<input type="text" ng-model="name" disallow-spaces />
You can achieve this without writing a directive.
<input ng-model="myModel" ng-keydown="$event.keyCode != 32 ? $event:$event.preventDefault()">
For Angular 9 ,Keycode is not support.
Below code can help you for that.
keyDownHandler(event) {
if (event.code === 'Space') {
event.preventDefault();
}
}

Non HTML5 Placeholder Directive that works with ng-model?

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

How to expose a public API from a directive that is a reusable component?

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()]);
});
}
};
})

Resources