scope.$watch in angular directive does not work proprely - angularjs

I'm using Angular and Bootstrap.
I'm trying to replicate the functionality of ng-model for bootstrap checkbox. What i would like to accomplish is:
i would like that when i click on the checkbox (label really) the model change, and actually that works... but what does not work that when i try to watch the object for changes the behavior is weired, because i need two click insted of one for disable or enable the checkbox.
Moreover if inside the the label element that has as attribute cm-checkbox="model.prop" i put a {{model.anotherprop}} wont work (does not render anything).
From the documentation i understood that because i want the two-way data bind the scope must be defined as i did.
Thank you for your help!
I have the following HTML:
<label id="claim_free" class="checkbox" for="checkbox1" cm-checkbox="model.prop">
<span class="icons"><span class="first-icon fui-checkbox-unchecked"></span><span class="second-icon fui-checkbox-checked"></span></span><input name="claim_free" type="checkbox" value="" data-toggle="checkbox">
same lable text
</label>
And the following JS:
directive('cmCheckbox', function() {
return {
restrict: 'A',
scope: {'cmCheckbox':'='},
link: function(scope,elm,attrs) {
scope.$watch('cmCheckbox', function() {
console.log("first value for "+attrs.cmCheckbox+" is: "+scope.cmCheckbox);
if (!scope.cmCheckbox) {
console.log("checked");
$(elm).removeClass("checked");
$(elm).children("input").removeAttr("checked");
} else { // false and undefined
console.log("unchecked");
$(elm).addClass("checked");
$(elm).children("input").attr("checked","checked");
}
});
$(elm).on('click', function(e) {
e.preventDefault();
var currentValue = elm.hasClass("checked") ? false : true;
scope.$apply(function() {
scope.cmCheckbox = currentValue;
},true);
scope.$parent.$apply();
});
}
};
}).
Here is the jsFiddle: jsfiddle.net/pmcalabrese/66pCA/2

Related

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}}"

Angular Bootstrap-Select timing issue to refresh

I love Bootstrap-Select and I am currently using it through the help of a directive made by another user joaoneto/angular-bootstrap-select and it works as intended except when I try to fill my <select> element with an $http or in my case a dataService wrapper. I seem to get some timing issue, the data comes after the selectpicker got displayed/refreshed and then I end up having an empty Bootstrap-Select list.. though with Firebug, I do see the list of values in the now hidden <select>. If I then go in console and manually execute a $('.selectpicker').selectpicker('refresh') it then works. I got it temporarily working by doing a patch and adding a .selectpicker('refresh') inside a $timeout but as you know it's not ideal since we're using jQuery directly in an ngController...ouch!So I believe the directive is possibly missing a watcher or at least something to trigger that the ngModel got changed or updated. Html sample code:
<div class="col-sm-5">
<select name="language" class="form-control show-tick"
ng-model="vm.profile.language"
selectpicker data-live-search="true"
ng-options="language.value as language.name for language in vm.languages">
</select>
<!-- also tried with an ng-repeat, which has the same effect -->
</div>
then inside my Angular Controller:
// get list of languages from DB
dataService
.getLanguages()
.then(function(data) {
vm.languages = data;
// need a timeout patch to properly refresh the Bootstrap-Select selectpicker
// not so good to use this inside an ngController but it's the only working way I have found
$timeout(function() {
$('.selectpicker, select[selectpicker]').selectpicker('refresh');
}, 1);
});
and here is the directive made by (joaoneto) on GitHub for Angular-Bootstrap-Select
function selectpickerDirective($parse, $timeout) {
return {
restrict: 'A',
priority: 1000,
link: function (scope, element, attrs) {
function refresh(newVal) {
scope.$applyAsync(function () {
if (attrs.ngOptions && /track by/.test(attrs.ngOptions)) element.val(newVal);
element.selectpicker('refresh');
});
}
attrs.$observe('spTheme', function (val) {
$timeout(function () {
element.data('selectpicker').$button.removeClass(function (i, c) {
return (c.match(/(^|\s)?btn-\S+/g) || []).join(' ');
});
element.selectpicker('setStyle', val);
});
});
$timeout(function () {
element.selectpicker($parse(attrs.selectpicker)());
element.selectpicker('refresh');
});
if (attrs.ngModel) {
scope.$watch(attrs.ngModel, refresh, true);
}
if (attrs.ngDisabled) {
scope.$watch(attrs.ngDisabled, refresh, true);
}
scope.$on('$destroy', function () {
$timeout(function () {
element.selectpicker('destroy');
});
});
}
};
}
One problem with the angular-bootstrap-select directive, is that it only watches ngModel, and not the object that's actually populating the options in the select. For example, if vm.profile.language is set to '' by default, and vm.languages has a '' option, the select won't update with the new options, because ngModel stays the same. I added a selectModel attribute to the select, and modified the angular-bootstrap-select code slightly.
<div class="col-sm-5">
<select name="language" class="form-control show-tick"
ng-model="vm.profile.language"
select-model="vm.languages"
selectpicker data-live-search="true"
ng-options="language.value as language.name for language in vm.languages">
</select>
</div>
Then, in the angular-bootstrap-select code, I added
if (attrs.selectModel) {
scope.$watch(attrs.selectModel, refresh, true);
}
Now, when vm.languages is updated, the select will be updated too. A better method would probably be to simply detect which object should be watched by using ngOptions, but using this method allows for use of ngRepeat within a select as well.
Edit:
An alternative to using selectModel is automatically detecting the object to watch from ngOptions.
if (attrs.ngOptions && / in /.test(attrs.ngOptions)) {
scope.$watch(attrs.ngOptions.split(' in ')[1], refresh, true);
}
Edit 2:
Rather than using the refresh function, you'd probably be better off just calling element.selectpicker('refresh'); again, as you only want to actually update the value of the select when ngModel changes. I ran into a scenario where the list of options were being updated, and the value of the select changed, but the model didn't change, and as a result it didn't match the selectpicker. This resolved it for me:
if (attrs.ngOptions && / in /.test(attrs.ngOptions)) {
scope.$watch(attrs.ngOptions.split(' in ')[1], function() {
scope.$applyAsync(function () {
element.selectpicker('refresh');
});
}, true);
}
Well, this is an old one... But I had to use it. This is what I added in the link(..) function of the directive:
scope.$watch(
_ => element[0].innerHTML,
(newVal, oldVal) => {
if (newVal !== oldVal)
{
element.selectpicker('refresh');
}
}
)

Angular strap tooltip visible by default

I using http://mgcrea.github.io/angular-strap/#/tooltips#tooltips
and i can't use Scope methods ($show(), $hide()). Help me please. How i can use this methods?
I have input in ng-repeat
<div ng-repeat="item in data.queue" >
<input type="text" maxlength="40" bs-tooltip data-animation="am-flip-x" data-title="{{item.file.tooltip_title}}">
<div>
And i need to set visible tooltips if item.file.flag=== true, and then hide tooltip of beyond the 5 second.
To get show() and hide() methods you gotta do everything on javascript side. Something like this:
markup
<div id="div1">some</div>
directive
app.directive('someThing', ['$tooltip', '$timeout', function($tooltip, $timeout){
return {
link: function($scope){
$scope.someFunction = function (item){
$timeout(function(){
var target = angular.element(document.getElementById('div1'));
var myTooltip = $tooltip(target, { title:'tip!!', trigger:'manual', placement:'top'});
myTooltip.$promise.then(function() { myTooltip.show(); });
$timeout(function(){
myTooltip.$promise.then(function() { myTooltip.hide(); });
}, 4000);
}, 1500);
};
}
};
}]);
The Angular way of solving your problem is using, for instance, data-bs-show="item.file.flag".
It will show your tooltip while item.file.flag == true.
The bsShow attribute expects a boolean value, so if you need to hide after 5 seconds, you might have another flag and set it to false after this time using $timeout.
It is possible to use $show()/$hide() but it's tricky and ugly so I'd avoid that if possible.

Directive inside bs-tooltip is not evaluated

I am trying to have a <ul> element with its own directive (checkStrength) inside of an AngularStrap bs-tooltip title property, like this:
$scope.tooltip = {
title: '<ul id="strength" check-strength="pw"></ul>',
checked: false
};
The behavior I want is as follows: when a user clicks on the input textbox, a tooltip will appear showing the strength of the password as they enter it in the textbox.
This does not work, as shown in the two Plunkers below:
Custom "checkStrength" directive outside bs-tooltip works fine: Plunker
Custom "checkStrength" directive inside bs-tooltip does not work: Plunker
Ok, it doesn't appear that this is supported out of the box. You are going to have to create your own binding directive
Directive
.directive('customBindHtml', function($compile) {
return {
link: function(scope, element, attr) {
scope.$watch(attr.customBindHtml, function (value) {
element.html(value);
$compile(element.contents())(scope);
});
}
};
});
This go into Angular Straps code and make the follow modification on line 10 of tooltip.js in the plunker
Template
<div class="tooltip-inner" custom-bind-html="title"></div>
Then set the html property in the config to false.
Config
.config(function($tooltipProvider) {
angular.extend($tooltipProvider.defaults, {
html: false
});
})
Example: Plunker

AngularJS: attrs.$observe not firing after the first time

jsFiddle here
In the fiddle, if you click on the submit button for the first time, notice that the <input> correctly gets focused on. On subsequent clicks, the focus isn't set any longer.
I noticed that the $observe callback isn't getting triggered when submit() changes isFocused, so I added a blur listener to explicitly reset the variable but that didn't help either.
How do I make the focus stick on submit?
Edit: Final working fiddle. Using $watch instead because I want it to work with arbitrary expressions
If you watch the element you'll notice that when the blur callback is fired the focus-on attribute isn't set to false. The reason for this is that the blur function is outside angular's scope and so it isn't aware of changes you make inside it.
To make angular aware of the change you need wrap any changes inside a call to $apply
element.bind('blur', function () {
scope.$apply(function() {
scope.isFocused = false;
console.log('blur');
});
});
http://jsfiddle.net/6vfGm/7/
I don't think you can observe the focusOn attribute because it doesn't really exist as an attribute of the directive. When you do <input focus-on='{{isFocused}}' /> the focus-on is actually your directive name.
Try this instead:
HTML
<div ng-app='foo'>
<form ng-controller='FormCtrl' ng-submit='submit()'>
<input focus-on focused='isFocused' />
<button>Submit</button>
</form>
</div>
Javascript
app.directive('focusOn', function () {
return {
scope: { focused: '=' },
link: function(scope, element) {
scope.$watch('focused', function (newValue) {
console.log('$watch', newValue)
if (newValue) {
element[0].focus();
}
});
element.bind('blur', function () {
scope.focused = false;
scope.$apply(); // required so that the scope is updated correctly.
console.log('blur');
});
}
};
});
app.controller('FormCtrl', function ($scope) {
$scope.submit = function () {
$scope.isFocused = true;
console.log('submit');
};
});
I created a focused attribute to bind the isFocused property to the directive scope (I could have named it focusOn, but I guess it would be weird to have <input type="text" focus-on focus-on="isFocused" />).
Here's your updated jsFiddle.
Update: I changed the attribute name from ng-model to focused so it makes more sense.

Resources