I have a problem with $scope.$watch call, when it obviously should be called.
I have a paginator (bootstrap UI) inside my html document:
<pagination total-items="paginatorTotalItems" items-per-page="paginatorItemsPerPage"
page="paginatorCurrentPage" max-size="paginatorSize" class="pagination-sm"
boundary-links="true">
</pagination>
A certain part, where my items are shown (for them I need a paginator):
<div ng-show="reviews" ng-repeat="review in reviewsPerPage">
...
</div>
And a Controller:
...
$scope.reviewsArray = [];
$scope.paginatorItemsPerPage = 1;
$scope.paginatorSize = 3;
$scope.reviewsPerPage = [];
$scope.paginatorTotalItems = $scope.reviews.result.total;
//restangular object to Array
for (var i = 0; i < $scope.paginatorTotalItems; i++) {
$scope.reviewsArray.push($scope.reviews.result.reviews[i]);
};
$scope.paginatorCurrentPage = 1;
$scope.$watch('paginatorCurrentPage', function () {
var begin = (($scope.paginatorCurrentPage - 1) * $scope.paginatorItemsPerPage);
var end = begin + $scope.paginatorItemsPerPage;
console.log($scope.paginatorCurrentPage);
console.log(begin + ' ' + end);
$scope.reviewsPerPage = $scope.reviewsArray.slice(begin,end);
console.log($scope.reviewsPerPage);
});
So, making long story short, I have a variable paginatorCurrentPage, that I change by clicking numbers in my <pagination>, but $watch does not react. This $watch is called only once: when I'm assigning it a value of 1 (after making an array from my restangular object), after that $watch is never called anymore.
Also I'm cheking how paginatorCurrentPage changes in my html file:
<p>Current : {{paginatorCurrentPage}}</p>
And it actually works, this variable is changing, when i switch my pagination buttons, but $watch is not called.
Sorry for my English, and Thank you!
Edited :
I have updated my bootstrap UI, so now in paginator I use ng-model istead of page. And I realized that variable paginatorCurrentPage changes only in my view, but in controller I still have my default $scope.paginatorCurrentPage = 1. Problem still exists.
Thanks for all comments. The problem was about scope. I rewrote ng-model in paginator: ng-model="paginatorPage.current"
and changed
$scope.paginatorCurrentPage = 1;
to
$scope.paginatorPage = {current : 1};
And thanks to #Leo Farmer for advice about dots in directives.
Related
I wanted to create a list of settings that a user can change in the HTML procedurally through javascript.
Much like this: Quality: - 0 +
My approach to this was making an Option class with a value property and prev() and next() methods that change the value within its range. I'm extending this class so it can be a Range, Bool, etc. This is working fine.
My issue is that I can't seem to be able to incorporate this into the HTML. My current solution works only for the last option created, the others don't trigger the onclick event functions, and even if they did I don't feel like this is the right approach to it. How can I make this work in an elegant way?
I have tried the solution shown in this question but it prevents me from accessing the class instance with this.
class UIManager {
constructor (wrapperID, settings) {
this.wrapper = document.getElementById(wrapperID)
this.settings = settings
}
updateUI () {
this.wrapper.innerHTML = ``
for (let id = 0; id < this.settings.options.length; ++id) {
let option = this.settings.options[id]
this.wrapper.innerHTML += `
<li>
<div class="label">
${option.name}
</div>
<div class="option">
<input id="prev${id}" class="open" type="button" value="<">
${option.value}
<input id="next${id}" class="close" type="button" value=">">
</div>
</li>
`
let prevButton = document.getElementById(`prev${id}`)
let nextButton = document.getElementById(`next${id}`)
prevButton.onclick = _ => {
this.settings.options[id].prev()
this.updateUI()
}
nextButton.onclick = _ => {
this.settings.options[id].next()
this.updateUI()
}
}
}
}
The answer to the question you linked as a potential solution is usable so long as you bind the class instance member this to the anonymous function using Function.protoype.bind.
The code would look something like this (using the previously linked answer as the starting point):
for ( var i = 0; i < itemLists.length; i++ ) (function(i){
itemLists[i].onclick = function() {
// do something using `this`
}
}).bind(this)(i);
You mentioned in the comments that this didn't work, but that it was related to overwriting innerHTML and not due to the binding.
Hope this gives a small part of the larger picture.
Try closure to store the values required for functions created from inside the loop.
Something like this :
prevButton.onclick = (function(settings, updateUI) {
return function() {
settings.options[id].prev();
updateUI();
};
})(this.settings, this.updateUI);
nextButton.onclick = (function(settings, updateUI) {
return function() {
settings.options[id].next();
updateUI();
}
})(this.settings, this.updateUI);
I am making a cart application in Angular using Angular Bootstrap.
When hovering over the cart icon a tooltip should appear. The tooltip's content should change based on if the item is already in the cart or not.
So, here is the html:
<h3><i class="fa fa-shopping-basket" ng-click="add2Cart(item.Name)" tooltip-placement="right" uib-tooltip-html="itemtooltiptext(item.Name)" aria-hidden="true"></i></h3>
Basically, in order to check if the item is already in the cart, I want the tooltip text to resolve from a function. My understanding from the documentation is this is supported as long as the HTML is trusted.
It says,
uib-tooltip-html $ - Takes an expression that evaluates to an HTML string. Note that this HTML is not compiled. If compilation is required, please use the uib-tooltip-template attribute option instead. The user is responsible for ensuring the content is safe to put into the DOM!
So my itemtooltiptext() function is...
$scope.itemtooltiptext = function(name) {
if (localStorage.getItem("cart") === null) {
return $sce.trustAsHtml("Add " + name + " to Cart!");
} else {
var cart = JSON.parse(localStorage.getItem("cart"));
for (var i = 0; i < cart.length; i++) {
if (cart[i] == name) {
console.log("already in cart");
return $sce.trustAsHtml(name + "already in Cart!");
}
}
return $sce.trustAsHtml("Add " + name + " to Cart!");
}
}
This results in an
Infinite $digest Loop Error
As detailed here: https://stackoverflow.com/a/19370032
But the problem is I need it to come from a function with the various conditions? So should I be using a template? I don't understand how that would work any better because I still need dynamic text served from the template... so what is the solution?
Thank you.
This is not how you use uib-tooltip-html, apparently it causes an infinite digest loop, fortunately the demo plunk shows you how to do it.
You need to get/calculate your html, bind to some scope variable and bind it into uib-tooltip-html
js
$scope.itemtooltiptext = function() {
$scope.htmlTooltip = $sce.trustAsHtml('I\'ve been made <b>bold</b>!');
};
$scope.itemtooltiptext();
html
<button uib-tooltip-html="htmlTooltip" class="btn btn-default">Tooltip</button>
If you still want to bind a function to your tooltip, you can do like so
<button uib-tooltip="{{itemtooltiptext()}}" class="btn btn-default">Tooltip</button>
Note that this approache will have the function invoked every digest cycle.
I ran into this infinite digest cycle issue where I needed a dynamic tooltip... it caused angular to recalculate it every time as a new value (even though it was the same). I created a function to cache the computed value like so:
$ctrl.myObj = {
Title: 'my title',
A: 'first part of dynamic toolip',
B: 'second part of dynamic tooltip',
C: 'some other value',
getTooltip: function () {
// cache the tooltip
var obj = this;
var tooltip = '<strong>A: </strong>' + obj.A + '<br><strong>B: </strong>' + obj.B;
var $tooltip = {
raw: tooltip,
trusted: $sce.trustAsHtml(tooltip)
};
if (!obj.$tooltip) obj.$tooltip = $tooltip;
else if (obj.$tooltip.raw !== tooltip) obj.$tooltip = $tooltip;
return obj.$tooltip;
}
};
Then in the html, I accessed it like this:
<input type="text" ng-model="$ctrl.myObj.C"
uib-tooltip-html="$ctrl.myObj.getTooltip().trusted">
I want to use $watch in order to trigger a function each time one of those 3 values is changed :
html:
<input type="hidden" name="source_x" id="source_x" ng-model="source_x"/>
<input type="hidden" name="source_y" id="source_y" ng-model="source_y"/>
<input type="hidden" name="id" id="id" ng-model="id"/>
I just started angular and I want to use the $watch to trigger a function.
Those value are changed each time I drag one div with the draggable function below :
$("#div").draggable({
helper : 'clone',
stop:function(event,ui) {
var wrapper = $("#container-emote").offset();
var borderLeft = parseInt($("#container-emote").css("border-left-width"),10);
var borderTop = parseInt($("#container-emote").css("border-top-width"),10);
var pos = ui.helper.offset();
$("#source_x").val(pos.left - wrapper.left - borderLeft);
$("#source_y").val(-(pos.top - wrapper.top - borderTop)+185);
$("#id").val(2);
}
});
I started with this but I think it is not right because if I move one div I am going to call 3 times the function ? Moreover I don't know if I can use it with input hidden.. Thank you !
//Fonction
$scope.$watch($scope.source_x, createEmote);
$scope.$watch($scope.source_y, createEmote);
$scope.$watch($scope.id, createEmote);
function createEmote(newValue, oldValue, scope){
}
UPDATE : answer fiddle
I just add a function at the end of stop of drag
jsfiddle.net/5e7zbn5z/1/
You need to use your $scope.$watch like so:
$scope.$watch(function() {
return $scope.source_x + $scope.source_y + $scope.id;
}, function() {
$scope.createEmote();
});
And have your createEmote be a function on your $scope:
$scope.createEmote = function() {
// do something with $scope.source_x, etc
}
EDIT
As noted in the comments by #Sergey the exact watcher function will be dependent on your expected data. You could also just duplicate it and change which variable is returned (Similar to your existing code) if you want.
I'm now developing website and there has edit note field features in ng-repeat. To edit note field, user need to click link to display form first then key-in into it and then save it as follow. Problem is i cannot hide that input after successfully saved. Coding is as follow.
index.jade
tr(data-ng-repeat="application in job.applications")
td.notes
div.bold #{getMessage('Notes:')}
div.normal
div(ng-hide='showDetails')
{{application.note}}
.br
a.admin_edit_gray(href='#', ng-click="showDetails = ! showDetails") Edit Note
div(ng-show='showDetails')
textarea.form-control.small-text-font(ng-model='editableTitle', ng-show='showDetails', maxlength="100", ng-trim="false")
div.editable
div(ng-if="editableTitle.length == 100")
| #{getMessage('max 100 symbols.')}
a.small-text-editButton(href='#', ng-click='save(application, editableTitle, application.id)') Save
| |
a.small-text-cancelButton(href='#', ng-click="showDetails = ! showDetails") close
controller.js
$scope.showDetails = false;
$scope.noteFormData = {};
$scope.save = function(application, editableTitle, appId) {
$scope.noteFormData = {
appId: appId,
note: editableTitle
};
mytestService.writeNote($scope.noteFormData).then(
function (notemessage) {
application.note = notemessage;
alert('Note is successfully saved.');
$scope.showDetails = false;
}
);
};
I've tried to hide form as $scope.showDetails = false; after successfully saved. But it does not work at all. Please help me how to solve that issue.
You are creating showDetails inside the $scope of the ngRepeat. Each iteration of the loop creates a new child $scope of the controller's $scope.
In this way, just set $scope.showDetails from the controller will not work.
In order to fix that you need to get the reference to the object that is being iterated and set the show details:
Instead of:
ng-click="showDetails=!showDetails"
Use:
ng-click="application.showDetails=!application.showDetails"
After that, when submiting, you can choose which one you would like to show or hide by using the correct reference or by iterating over all itens of the array and setting showDetails to false.
Instead of:
$scope.showDetails = false;
Use:
application.showDetails = false;
set a variable in controller and set its value false .After your save() function is executed successfully set that variable to true. And in the view page put an condition of ng-show on tr if that value that is true.
I have an angular object(model) created in controller.
$scope.deletedres = [];
I am trying to append a new DOM to the html body along with the angular object(modal) as shown below.
$('body').append('<span>'+restaurant.name+' have been removed.</span><a class="btn-flat yellow-text" href="#"; ng-click="addRestaurant($scope.deletedres[$scope.deletedres.length-1])">Undo<a>');
When I view it with google chrome dev tools, it shows that $scope.deletedres as [object Object] and addRestaurant() function receive nothing.
Can anyone enlighten me on this issue?
Is there any other ways to reference/pass an angular modal to a newly created DOM?
The way you are adding the DOM is wrong. Add the html inside the scope of controller. Use ng-show to show or hide the dom. JQuery is not necessary.
Example
<span ng-show="restaurant.delete">{{restaurant.name}} have been removed.</span>
<a class="btn-flat yellow-text" href="#"; ng-click="restaurant.delete=false">Undo<a>
This is just an example you can improve on
When you use jQuery to add fragments of HTML there is no way for angular to parse it. Thats the reason your angular code inside the html is working.
You can use $compile service.
var html = '<span>{{restaurant.name}} have been removed.</span><a class="btn-flat yellow-text" href="#"; ng-click="addRestaurant(deletedres[deletedres.length-1])">Undo</a>';
var linkFn = $compile(html);
var content = linkFn(scope);
$('body').append(content);
Still as noted by Harish it's wrong. All manipulations with DOM must be done in directives. You can create directive that will be responsible for showing some message (or custom html template) on button click.
Dmitry Bezzubenkov is right. If you want to manipulate DOM with Angular, you should do that with your custom directive, rather than do that in your controller directly. And to do so, you may refer to $compile service. Here's the official document for that.
However, in your case, I believe what you actually want to do is remove the item from a list while enable the item to be recovered from deletion. In this sense, you may try this approach with Angular:
In your controller, create a array for original restaurant list and another for deleted restaurant list. (Let's say, $scope.res and $scope.deletedres)
Register a delete function and bind that to delete button with ng-click. In this function, you will remove the item from $scope.res and then push the item to $scope.deletedres
Register another undo function. Basically do the same thing as delete function but in reverse. That is, move a item from $scope.deletedres to $scope.res. Bind this item to UNDO text in your message box.
use ng-repeat to show your $scope.res list in the main container, and $scope.deletedres in the message box container.
Thanks to the 2-way data binding from Angular, now you can delete or undo the action by clicking to different item.
It would be something like this:
angular
.module('modelTest', [])
.controller('MainCtrl', function($scope) {
$scope.res = [
{id: 1, name: 'Restaurant1'},
{id: 2, name: 'Restaurant2'},
{id: 3, name: 'Restaurant3'}
];
$scope.deletedres = [];
$scope.delete = function(id) {
var item, obj, i, j;
for(i = 0, j = $scope.res.length; i < j; i++) {
obj = $scope.res[i];
if(obj.id === id) {
$scope.deletedres.push(obj);
$scope.res.splice(i, 1);
}
}
};
$scope.undo = function(id) {
var item, obj, i, j;
for(i = 0, j = $scope.deletedres.length; i < j; i++) {
obj = $scope.deletedres[i];
if(obj.id === id) {
$scope.res.push(obj);
$scope.deletedres.splice(i, 1);
}
}
}
});
Here's the sample code.