I'm having an issue with a $watch that is really just boggling my brain. I'm almost certain that it is somehow a scope issue. Here's the basics. I'd like a tooltip on a span surrounding a checkbox and it's label so that the tooltip activates on hover over the checkbox or label. something like this.
(note:I've added the {{values.isChecked}} just to watch the values in the two different scopes)
HTML
{{values.isChecked}}
<span tooltip-placement="right" tooltip="This is my tooltip">
<input type="checkbox" id="myCheckbox" ng-model="values.isChecked">
<label for="myCheckbox"> My Checkbox</label> {{values.isChecked}}
</span>
from angular controller
$scope.values.isChecked = true;
$scope.watch("values.isChecked",function(newValue,oldValue){
alert("Made it to the watch");
}
The most odd behavior is that my watch catches the transition of the value from true to false. But does not catch the transition from false to true. I can click it several times and it will make it to the watch going from true to false, but not false to true. If it catches the true to false, and then catches true to false again, well, it HAD to have changed from false to true in order to trigger the watch again. I know the value is actually changing, the two places wehre I added it to the page via {{values.isChecked}}, both show the values change as expected, it's just not firing my watch when I CHECK the box. only when I UNCHECK it.
All my code is on a different box on an isolated network, so I can't actually copy and paste any, so tried to just type in the relevant stuff.
Also if I just take out the Span that has the tooltip on it, it works just fine. I'm aware that bootstrap UI's tooltip does create a new scope. so suspect that, but don't know why it works for one transition, but not the other.
I have even gone as far as capturing the scope inside the tooltip and adding my watch there such as...
myChildScope = angular.element('#myCheckBox').scope()
myChildScope.$watch("values.isChecked",function(newValue,oldValue){
...
It behaves incorrectly, the exact same way. Also behaves the exact same (bad) way if I try to add an ng-click or ng-change to the checkbox element.
Two things to try, I'm not sure how your code is setup but for issues of a watch not catching things when you think it should, generally one of these will work.
$scope.watch("values.isChecked",function(newValue,oldValue){
alert("Made it to the watch");
}, true);
The true tells it to compare for object equality using angular.equals instead of comparing for reference equality. Since it's a boolean primitive... probably more of what you want to use.
The other option, which may or may not help in your case, it to use
$scope.watchCollection("values",function(newValue,oldValue){
alert("Made it to the watch");
});
And see if anything in the values object changes.
Also, you could change isChecked to an object,
$scope.isChecked = { checked: false }
and $watch the object rather than the boolean itself.
Related
I have a checkbox like this:
{{input type="checkbox" checked=property}}
When I click the checkbox the property is changed. Now I like to call a function to persist the property. But what is the best way to do this?
I could bind an observer to property, but this is consindered bad.
I could bind an action to input oder click.
{{input ... input=(action "actionName") click=(action "actionName")}}
This doesn't work because the property has still the old value when the action is called.
Extending the class Checkbox:
import Checkbox from '#ember/component/checkbox';
Checkbox.reopen({
onChange: null,
change() {
this._super();
if (this.onChange) this.onChange();
}
});
...
{{input ... onChange=(action "actionName")}}
But the function change is not documented, even though the source code indicates that it is feasable to override.
I could bind an action to didUpdate or didUpdateAttrs, but the action is called twice. In my case this wouldn't be a problem, because the property is part of a model, so I could call model.get('hasDirtyAttributes') in the action.
[Update] In my test case the action was called twice but in my real code it is only called once, so this seems to be the best solution?[/Update]
So what is the proper ember.js-way to do this?
I personally prefer one way binding with checkboxes. I use the native input like so:
<input type="checkbox" checked={{myValue}} onclick={{action (mut myValue) value="target.checked"}}>
Check this twiddle to see it live in action. This is the easiest, non-library approach for 1-way bound checkboxes, which I would argue is the "ember-way" as of 2.x (although I do know people that still like and use the 2-way bound input which more requires observers in cases like what you described as far as I can tell).
In one of my projects, I use ember-paper which uses a nicely displayed checkbox without input and I believe the correct aria-bindings for a11y. They leverage click to invoke onChange
click() {
if (!this.get('disabled')) {
invokeAction(this, 'onChange', !this.get('value'));
}
// Prevent bubbling, if specified. If undefined, the event will bubble.
return this.get('bubbles');
}
such that you can use with the following api
{{#paper-checkbox value=value1 onChange=(action (mut value1))}}
A checkbox: {{value1}}
{{/paper-checkbox}}
You can see the example and how the template and component js are implemented for inspiration should you want a better looking checkbox than native
Have a look at my ember-twiddle example.
I would suggest using input helper,
({input type="checkbox" click=(action "setProperty") checked=my_property}}
toggle the property inside the action
setProperty() {
// toggle the property on click
this.toggleProperty("my_property");
console.log(this.get('my_property')); // returns true or false based on the checked value
.. // do whatever stuff you want
}
If your at the most recent version of ember 3.9, have a look at this
We are working on an HTML page which makes use of a Bootstrap tooltip on a certain <span> tag. For those who have not heard of tooltip, it is a popup of sorts which appears when hovering over the element to which it is attached. Here is a screenshot showing the <span> in question, and what happens on hover:
The premise behind adding the tooltip was that in the event that we truncate the text, the tooltip would provide an option for viewing the entire text.
However, we would now like to condtionally show the tooltip only when there is no ellipsis in the text. We defined the tooltip-enable property in the <span>:
<span uib-tooltip="{{someName}}" tooltip-placement="right" tooltip-enable="{{showToolTip}}">{{someNameShortened}}</span>
The key thing here is tooltip-enable="{{showToolTip}}", which binds the property to a scoped variable in the controller for this page. And here is the relevant (and abbreviated) controller code:
mainApp.controller('repoListController',['$scope', '$rootScope', ...,
function($scope,$rootScope, ...) {
$scope.showToolTip = false;
var repositoryList= function(){
repositoryService.getRepositoryList(function(data) {
var repoList = data;
repoList.shortenedDisplayName = repositoryService.getShortRepoName(repoList.repoName, DISPLAY_NAME_MAX_LENGTH);
// if the repository's name be sufficiently large (i.e. it has an ellipsis)
// then show the tooltip. Otherwise, the default value is false (see above)
if (repoList.repoName.length > DISPLAY_NAME_MAX_LENGTH) {
$scope.showTooltip = true;
}
});
}
repositoryList();
}]);
Based on the research I have done, the common solution for why a change to a scoped variable is not reflected in the UI is to run $scope.$apply(), or some variation on this. Running apply(), as I understand it, will tell Angular JS to do a digest cycle, which will propagate changes in the scope to the UI. However, trying to do an apply() from the code which toggles showToolTip resulted in errory. I inspected the value of $scope.$root.$$phase while running the code which updates the showToolTip variable, and the phase was digest.
So now I am at a loss to explain this. If the code is already in a digest, then why would changes not be reflected in the UI? Also, if the code is already in digest, then how could I force Angular to sync the changes to the UI?
Two things need fixing...
Don't use string interpolation for your boolean showToolTip
<span uib-tooltip="{{someName}}" tooltip-placement="right"
tooltip-enable="showToolTip">{{someNameShortened}}</span>
JavaScript variables / properties are case sensitive. In your getRepositoryList handler, you have $scope.showTooltip. It should be $scope.showToolTip (two capital "T"s)
Crappy Plunker demo ~ http://plnkr.co/edit/W7tgJmeQAJj0fmfT72PR?p=preview
I am trying to place $watch on a value in angular to setup controls differently depending on instructions from the controller and I have tried to follow the lead in angular-leaflet-directive using leafletScope.$watch("variable").
I have added a new $watch with equality comparison as it's an object:
leafletScope.$watch("controls", function(controlOpts) {...}, true)
I then call it in the controller using:
angular.extend($scope, {
controls: {}
}
This is to initialise the controls as this seems to be required.
I then call it later on an event using:
$scope.controls = { new object }
If I log the change in controls and then also on the $watch event, I get the following sequence:
control $watch event logged
$scope.control change event logged
no further logs
The fact that the watch isn't called after the $scope is changed suggests I am doing this wrong.
Can anyone advise me where, or if I have reached the wrong conclusion in my simple test.
If I do the same and change my "center" model, I get:
center$watch event logged
$scope.center change event logged
center$watch event logged with new value
This turned out to be a clash between angular-leaflet-directive and the html minifier I was using html-minifier. The angular-leaflet-directive notation for leaflet controls is controls so in my html file I had
<leaflet center="centre" controls="controls" layers="layers" width="1200px" height="800px"></leaflet>
the html-minifier took controls="controls" to be a boolean attribute and with the collapse boolean attributes flag on reduced it to
<leaflet center="centre" controls layers="layers" width="1200px" height="800px"></leaflet>
Which wasn't good. I will request this is either better documented in angular-leaflet-directive or they change the name of controls to leaflet-controls or something to avoid this happening to unsuspecting folk like me in the future.
Incidently the true flag on the watch proved to be too resource intensive and I removed it and it still worked..
Assuming a given form such as <form name="myForm">, it's easy enough to watch for validity, error, dirty state, etc. using a simple watch:
$scope.$watch('myForm.$valid', function() {
console.log('form is valid? ', $scope.myForm.$valid);
});
However, there doesn't appear to be an easy way to watch if any given input in this form has changed. Deep watching like so, does not work:
$scope.$watch('myForm', function() {
console.log('an input has changed'); //this will never fire
}, true);
$watchCollection only goes one level deep, which means I would have to create a new watch for every input. Not ideal.
What is an elegant way to watch a form for changes on any input without having to resort to multiple watches, or placing ng-change on each input?
Concerning the possible duplicate and your comment:
The directive solution in that question works, but it's not what I had in mind (i.e. not elegant, since it requires blur in order to work).
It works if you add true as third parameter for your $watch:
$scope.$watch('myFormdata', function() {
console.log('form model has been changed');
}, true);
Further information see the docs.
Working Fiddle (check console log)
Another more angular way would be to use angular's $pristine. This boolean property will be set to false once you manipulate the form model:
Fiddle
Based on my experience with my forms (new dev, but working with Angular for a while now), the elegant way to watch a form for changes is actually not to use any type of watch statement at all actually.
Use the built-in Angular boolean $pristine or $dirty and those values will change automatically on any input field or checkbox.
The catch is: it will not change the value if you add or splice from an array which had me stumped for a while.
The best fix for me was to manually do $scope.MyForm.$setDirty(); whenever I was adding or removing from my different arrays.
Worked like a charm!
I have a textarea tag with jquery.nicescroll pluggin and ng-model attached to it.
<textarea id="paper" ng-model="paper"></textarea>
In my code I apply a $watch on this ng-model variable.
$scope.$watch("paper", onTextChange);
Everything is good except that onTextChange is fired not only when I type something, but when I click away from textareaб and also when I switch to another tab.
How can I prevent it so that onTextChange is fired only when the text is changed, meaning when I type in something or delete chars?
Demo with instructions: plunker
here's a fix:
http://plnkr.co/edit/kycmUrthYU38Ukdz0jJG?p=preview
setTimeout(
function() {
$scope.$watch("paper", function(newtext, oldtext) {
if (newtext !== oldtext) {
onTextChange();
}
});
}, 100)
So the issue is that watch fires the function whenever angularjs tells the app to digest. What you did was tell it to call the 'change' function EVERY time, when you should have passed in a checker function to check the change happened. It's about 'watching', not about 'watching for changes' - the function argument is supposed to see if you need to do something.
Extra note:
AngularJS sets up watchers for all kinds of things on various elements - here's a bit more info. I believe the blur corresponds to ng-touched which triggers the digest What gets added to $scope.$$watchers by default in Angular? And what triggers $digests?
It is a bad idea to use setTimeout. Instead, you could use ng-model-options="{ getterSetter: true }" and write a method to get/set the value (Modified get/set Plunk) and handle the text change condition within this method.