Same Directives multiple times on a page taking last model value always - angularjs

I am trying to add a copy icon beside each label to copy the text and my code is like below. I am passing the model value of each label as an input to the directive.
When I am clicking on the copy icon, always the last model value is showing why?
Isolating the scope is working . But, I would like to know whats happening internally.
Please explain in detail.
function copytext() {
var directive = {
restrict: 'EA',
template: `
<i class="imd imd-content-copy cursor-pointer"
ng-click="click()" ></i>
`,
link: function(scope, ele, attrs) {
scope.click = function() {
alert(attrs.data)
};
}
};
return directive;
}
<label>Job No</label>: {{vm.jobInfo.jobNumber}}
<copytext data="{{vm.jobInfo.jobNumber}}"></copytext>
<label>Customer </label>: {{vm.jobInfo.customerCode}}
<copytext data="{{vm.jobInfo.customerCode}}"></copytext>
<label>Reference</label>: {{vm.jobInfo.reference}}
<copytext data="{{vm.jobInfo.reference}}"></copytext>

<label>Job No</label>: {{vm.jobInfo.jobNumber}}
<copytext data="{{vm.jobInfo.jobNumber}}" onclick="<attr.data=referencevalue>"></copytext>
<label>Customer </label>: {{vm.jobInfo.customerCode}}
<copytext data="{{vm.jobInfo.customerCode}}" onclick="<attr.data=referencevalue>"></copytext>
<label>Reference</label>: {{vm.jobInfo.reference}}
<copytext data="{{vm.jobInfo.reference}}" onclick="<attr.data=referencevalue>"></copytext>
#georgeawg ,The above is just psuedo code, dont think about its syntax. The last html elements data attribute is reference, so all the above elements click handler also takes the reference.. isn't it?

Related

In Angular, bind attribute from scope variable *before* ngModel binds the input’s `value`

In my Angular app, I defined a custom slider directive that wraps <input type="range">. My custom directive supports ngModel to bind the slider’s value to a variable. The custom directive also requires a fraction-size attribute. It does a calculation on the value and then uses the result to set the step value of the wrapped <input>.
I am seeing a bug when I combine these two features – ngModel and my bound attribute value. They are run in the wrong order.
Here is a demonstration:
angular.module('HelloApp', []);
angular.module('HelloApp').directive('customSlider', function() {
var tpl = "2 <input type='range' min='2' max='3' step='{{stepSize}}' ng-model='theNum' /> 3";
return {
restrict: 'E',
template: tpl,
require: 'ngModel',
scope: {
fractionSize: '='
},
link: function(scope, element, attrs, ngModelCtrl) {
scope.stepSize = 1 / scope.fractionSize;
scope.$watch('theNum', function(newValue, oldValue) {
ngModelCtrl.$setViewValue(newValue);
});
ngModelCtrl.$render = function() {
scope.theNum = ngModelCtrl.$viewValue;
};
}
};
});
angular.module('HelloApp').controller('HelloController', function($scope) {
$scope.someNumber = 2.5;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<div ng-app="HelloApp" ng-controller="HelloController">
<h3>Custom slider</h3>
<custom-slider ng-model="someNumber" fraction-size="10"></custom-slider>
<h3>View/edit the slider’s value</h3>
<input ng-model="someNumber"></input>
</div>
The above slider should start at the middle, which represents 2.5. But it actually starts all the way at the right (representing 3). The slider fixes itself and allows the value 2.5 if you drag it, or if you change its bound value by editing the text field.
I have figured out why this is happening in the demonstration – I just don’t know how to fix it. Currently, when a new custom slider is dynamically added to the page, the wrapped <input>’s step is undefined, and defaults to 1. Then ngModel sets the value to 2.5 – but since step is 1, the value in the input is rounded to 3. Finally, step is set to 0.1 – too late for it to matter.
How can I ensure that the step attribute’s value is bound before ngModel sets the input’s value?
In the example above, the slider is on the page at page load. In my real code, multiple new sliders are added dynamically. They should all bind their step and value in the correct order whenever they are added.
A workaround I don’t like
A workaround is to hard-code the step in the template instead of setting it dynamically. If I were to do this, my custom slider directive would have no use, and I would remove it and just use an <input> directly:
<input type="range" min="2" max="3" step="0.1" ng-model="someNumber">
If I use that, the slider’s value is set correctly, without rounding. But I want to to keep my custom directive, customSlider, so that the calculation of step is abstracted.
Things I tried that didn’t work
Changing the order of the step and ng-model attributes in the template doesn’t have any effect.
I started trying to add a compile function, instead of just having a link function. But I can’t set stepSize in compile because scope isn’t available during the compile phase.
I tried splitting my directive’s link function into pre-link and post-link functions. But whether I set scope.stepSize in pre or in post, the page works as before.
Manually calling scope.$digest() right after setting scope.stepSize just throws Error: [$rootScope:inprog] $digest already in progress.
I don’t think custom directive priorities are a good fit for this, because there is only one custom directive involved. My binding of step’s value isn’t a custom directive, it is just raw {{}}-binding. And I think binding step is a simple enough task that it shouldn’t be wrapped in its own directive.
Taking #pixelbits' answer of direct DOM manipulation further, I wouldn't have another ng-model on the inner input at all, and instead always set/get the properties on the input directly
You have one abstraction level to think about when setting/getting values from the input. Raw DOM events and elements.
As such you are not limited to what Angular allows on such elements (indeed: what it does seems to not be able to handle your use case without work-around). If the browser allows it on the range input, you can do it.
Have 2 ngModelControllers in play on the one UI widget that sets one value can get a bit confusing, at least for me!
You still have access to the outer ngModelController pipeline and all its functionality regarding parsers and validators, if you need/want to use it.
You save from having an extra watcher (but this could be a micro/premature optimization).
See an example below.
angular.module('HelloApp', []);
angular.module('HelloApp').directive('customSlider', function() {
var tpl = "2 <input type='range' min='2' max='3' /> 3";
return {
restrict: 'E',
template: tpl,
require: 'ngModel',
scope: {
fractionSize: '='
},
link: function(scope, element, attrs, ngModelCtrl) {
var input = element.find('input');
input.prop('step', 1 / scope.fractionSize);
input.on('input', function() {
scope.$apply(function() {
ngModelCtrl.$setViewValue(input.prop('value'));
});
});
ngModelCtrl.$render = function(value) {
input.prop('value', ngModelCtrl.$viewValue);
};
}
};
});
angular.module('HelloApp').controller('HelloController', function($scope) {
$scope.someNumber = 2.5;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<div ng-app="HelloApp" ng-controller="HelloController">
<h3>Custom slider</h3>
<custom-slider ng-model="someNumber" fraction-size="10"></custom-slider>
<h3>View/edit the slider’s value</h3>
<input ng-model="someNumber"></input>
</div>
Also available at http://plnkr.co/edit/pMtmNSy6MVuXV5DbE1HI?p=preview
In your link function, manipulate the DOM by adding the step attribute.
You can also simplify your binding with the outer ngModel by putting theNum: '=ngModel' in scope.
var app = angular.module('HelloApp', []);
app.directive('customSlider', function () {
var tpl = "2 <input type='range' min='2' max='3' ng-model='theNum' /> 3";
return {
restrict: 'E',
template: tpl,
require: 'ngModel',
scope: {
fractionSize: '=',
theNum: '=ngModel'
},
link: function (scope, element, attrs, ngModelCtrl) {
var e = element.find('input')[0];
var step = 1 / scope.fractionSize;
e.setAttribute('step', step);
scope.$watch('fractionSize', function (newVal) {
if (newVal) {
var step = 1 / newVal;
e.setAttribute('step', step);
}
});
}
};
});
angular.module('HelloApp').controller('HelloController', function($scope) {
$scope.someNumber = 2.5;
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<div ng-app="HelloApp" ng-controller="HelloController">
<h3>Custom slider</h3>
<custom-slider ng-model="someNumber" fraction-size="10"></custom-slider>
<h3>View/edit the slider’s value</h3>
<input ng-model="someNumber"></input> {{ someNumber }}
</div>
I'm not sure if this is the correct way to do it, but adding a timeout to your main controller solves the issue.
$timeout(function() {
$scope.someNumber = 2.5;
});
Edit: While it seems like a dirty hack at first, but note that it is common for (final) $scope variables to be assigned later than templates, because of additional ajax calls to retrieve the values.

Angular binding does not work in data- attribute

I am using some css html template that comes with many html components and with lots of data-attributes for various things. For example for slider it has something like
<div class="slider slider-default">
<input type="text" data-slider class="slider-span" value="" data-slider-orientation="vertical" data-slider-min="0" data-slider-max="200" data-slider-value="{{ slider }}" data-slider-selection="after" data-slider-tooltip="hide">
</div>
Here I am trying to bind the value
data-slider-value="{{ slider }}"
But it's not working. Variable 'slider' is set in the $scope as:
$scope.slider = 80;
Same value 80 shows up right when I bind it as:
<h4>I have {{ slider }} cats</h4>
I have also tried
ng-attr-data-slider-value="{{ slider }}"
It didn't work.
Update
The directive has something like this
function slider() {
return {
restrict: 'A',
link: function (scope, element) {
element.slider();
}
}
};
where element.slider(); calls the code in bootstrap-slider.js (from here) for each of the sliders.
I played with this for a while, and came up with a few options for you. See my Plunkr to see them in action.
Option 1: No need to update the scope value when the slider changes
This will work with the HTML from your question. The following is what you should change the directive code to.
app.directive('slider', function slider() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
attrs.$observe('sliderValue', function(newVal, oldVal) {
element.slider('setValue', newVal);
});
}
}
});
Option 2: Two way binding to the scope property
If you need the scope property to be updated when the slider handle is dragged, you should change the directive to the following instead:
app.directive('sliderBind', ['$parse',
function slider($parse) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var val = $parse(attrs.sliderBind);
scope.$watch(val, function(newVal, oldVal) {
element.slider('setValue', newVal);
});
// when the slider is changed, update the scope
// property.
// Note that this will only update it when you stop dragging.
// If you need it to happen whilst the user is dragging the
// handle, change it to "slide" instead of "slideStop"
// (this is not as efficient so I left it up to you)
element.on('slideStop', function(event) {
// if expression is assignable
if (val.assign) {
val.assign(scope, event.value);
scope.$digest();
}
});
}
}
}
]);
The markup for this changes slightly to:
<div class="slider slider-default">
<input type="text" data-slider-bind="slider2" class="slider-span" value="" data-slider-orientation="vertical" data-slider-min="0" data-slider-max="200" data-slider-selection="after" data-slider-tooltip="hide" />
</div>
Note the use of the data-slider-bind attribute to specify the scope property to bind to, and the lack of a data-slider-value attribute.
Hopefully one of these two options is what you were after.
I use .attr and it works for me. Try this:
attr.data-slider-value="{{slider}}"

Angularjs - ng-disabled default value

I have a button with this attribute: ng-disabled="ProductionBtnDisabled".
When angular renders the html the value of ProductionBtnDisabled is undefined and after initiating the controller ProductionBtnDisabled has the correct value.
So, at first the ng-disabled is not disabled because undefined=false in javascript/angular. This is a problem for me. I want the default value to be true.
Is any one has any suggestion to handle this?
What about using ng-cloak? It didn't work for me. I don't mind hiding the buttons until the scope is rendered.
Thanks!
I think there is no default settings for ngDisabled. If you needs a global solution, you can try directive.
.directive('specialDisabled',function() {
return {
restrict: 'A',
scope:{specialDisabled: '='},
link: function(scope, element, attrs) {
scope.$watch(function(){return scope.specialDisabled}, function(){
// set disabled attribute here
element[0].disabled = !scope.specialDisabled;
});
}
}
});
Then, you can called specialDisabled directive anywhere.
<input type="checkbox" ng-model="chk">
<button special-disabled="chk">My Button</button>
http://plnkr.co/edit/BA2ntTrzmItwvEj8UKOc?p=preview

Obtaining element attributes within a custom directive used within ng-repeat

This is driving me a little bit crazy. I need to read the src of an image within a custom attribute:
app.directive('imgTransform', function () {
return {
retrict: 'A',
link: function (scope, elem, attrs) {
console.log(elem.attr('ng-src'));
}
}
});
This works fine when used like so:
<img ng-src='http://lorempixel.com/100/100/technics' alt="" img-transform="" />
However, it does not work inside ng-repeat:
<p ng-repeat='image in images'>
<img ng-src='{{image}}' alt="" img-transform="" />
</p>
The value returned is {{image}}. How do I get the actual value?
Try using the attrs:
console.log(attrs.ngSrc);
Fiddle: http://jsfiddle.net/6SuWD/
The reason for this could be that ng-repeat uses the original DOM as template and recreates it for each iteration. For some (obscure to me) reason, you are reading the attribute of the template. This explanation could be very wrong though...
However, since Angular gives you the API to access the attributes, it would be safer to go with it anyway.
You will have to watch for changes in this attribute using $observe since ng-repeat interpolates the values of ng-src. See this reference.
LukaszBachman is correct. When you pass interpolated values to a directive, the interpolation hasn't fired yet when the directive is in its linking phase.
If you were to do console.log(attrs); - you would clearly see that there is an actual value on ngSrc, when looking in the browser console. However, since interpolation hasn't kicked in yet you dont have access to it.
This would get you the actual value of ngSrc:
myApp.directive('imgTransform', function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
attrs.$observe('ngSrc', function (val) {
console.log(val);
});
}
}
});

Detect if a transclude content has been given for a angularjs directive

I have a directive (a progressbar) which should have two possible states, one without any description and one with a label on the left side.
It would be cool to simple use the transcluded content for this label.
Does anyone know how I can add a class to my directive depending whether a transclude content has been given or not?
So I want to add:
<div class="progress" ng-class="{withLabel: *CODE GOES HERE*}">
<div class="label"><span ng-transclude></span>
<div class="other">...</div>
</div>
Thanks a lot!
After release of Angular v1.5 with multi-slot transclusion it's even simpler. For example you have used component instead of directive and don't have access to link or compile functions. Yet you have access to $transclude service. So you can check presence of content with 'official' method:
app.component('myTransclude', {
transclude: {
'slot': '?transcludeSlot'
},
controller: function ($transclude) {
this.transcludePresent = function() {
return $transclude.isSlotFilled('slot');
};
}
})
with template like this:
<div class="progress" ng-class="{'with-label': withLabel}">
<div class="label"><span ng-transclude="slot"></span>
<div class="other">...</div>
</div>
Based on #Ilan's solution, you can use this simple $transclude function to know if there is transcluded content or not.
$transclude(function(clone){
if(clone.length){
scope.hasTranscluded = true;
}
});
Plnkr demonstrating this approach with ng-if to set default content if nothing to transclude: http://plnkr.co/hHr0aoSktqZYKoiFMzE6
Here is a plunker: http://plnkr.co/edit/ednJwiceWD5vS0orewKW?p=preview
You can find the transcluded element inside the linking function and check it's contents:
Directive:
app.directive('progressbar', function(){
return {
scope: {},
transclude: true,
templateUrl: "progressbar.html",
link: function(scope,elm){
var transcluded = elm.find('span').contents();
scope.withLabel = transcluded.length > 0; // true or false
}
}
})
Template:
<div class="progress" ng-class="{'with-label': withLabel}">
<div class="label"><span ng-transclude></span>
<div class="other">...</div>
</div>
You can also create your custom transclusion directive like so:
app.directive('myTransclude', function(){
return {
link: function(scope, elm, attrs, ctrl, $transclude){
$transclude(function(clone){
// Do something with this:
// if(clone.length > 0) ...
elm.empty();
elm.append(clone);
})
}
}
})
Based on the solution from #plong0 & #Ilan, this seems to work a bit better since it works with whitespace as well.
$transcludeFn(function(clonedElement) {
scope.hasTranscludedContent = clonedElement.html().trim() === "";
});
where previously <my-directive> </my-directive> would return that it has a .length of 1 since it contains a text node. since the function passed into $transcludeFn returns a jQuery object of the contents of the transcluded content, we can just get the inner text, remove whitespace on the ends, and check to see if it's blank or not.
Note that this only checks for text, so including html elements without text will also be flagged as empty. Like this: <my-directive> <span> </span> </my-directive> - This worked for my needs though.

Resources