I have a page built in angular. The page has a set of data displayed that's paginated. I have the page searchable with a search box. The pagination is build by hand due to some custom rules about what comprises each page. But building the pagination is set in a buildPages() method, and from there it's a simple call to build the pages that directly updates the scope the page draws from.
So typically a search works like so:
Type in search.
Watcher catches the change on search.
Watcher sets search var.
Watcher calls buildPages();
buildPages rebuilds pagination and updates scope.
Scope is update, pagination and displayed data is redrawn.
Now all of this works just fine. But I needed to add in a dialog to ask the user if they want to change the search or leave it, and this dialog only appears sometimes based on certain criteria.
My problem is that if I open a dialog and call buildPages() from a button click in the dialog, it properly updates the scope, but it doesn't redraw the page.
So, for example, you have 50 pages and you're displaying page 1 of 50. You then type in a search. The search filters out 25 of those 50 pages, leaving you with 25 pages left. The page you were initially on is also filtered out. Without the dialog, typing in the search will result in you appearing on page 1/25 on the first filtered page. However, triggering the search from the dialog will still show you on page 1 of the unfiltered page, at page 1/50. Clicking next on the pagination control will properly take you to the second filtered page 2/25, and clicking back from there will properly show you on page 1/25 of the filtered pages.
Even doing something as simple as triggering a hover will cause the page to properly redraw correctly.
What's going on with this? Why is the dialog preventing the redraw?
Here's the code I'm using in my watcher (slightly simplified).
$scope.$watch('searchText', function(newVal, oldVal) {
currentSearch = newVal;
if(newVal !== oldVal){
if(displayFlag){
j$( "#dialog-confirm").dialog({
resizable: false,
width:450,
height:240,
modal: true,
buttons: {
"Search": function() {
j$( this ).dialog( "close" );
buildPages();
},
Cancel: function() {
j$( this ).dialog( "close" );
return false;
}
}});
}
else
{
buildPages();
}
}
});
I've tried including a return of either true or false in the search click. I've also tried switching the order of the dialog close and buildPages in the search click.
I am not sure if j$() is an angular library. It appears that it may not be.
Remember that Angular does not know about any type of events or changes except its own, therefore, if you are performing an action with jQuery or another library like that .dialog, after you performe the call, you may need to call $scope.$digest(); or $scope.$apply() to trigger the angular digest cycle.
scope.$digest() will only fire watchers on current scope, scope.$apply (recommended) will evaluate passed function and run $rootScope.$digest().
buttons: {
"Search":
function() {
j$( this ).dialog( "close" );
buildPages();
$scope.$apply(); //After function is executed a cycle will occur.
},
Cancel: function() {
j$( this ).dialog( "close" );
return false;
}
}
I believe in your situation that the view may not being updated because angular isn't being told to perform a digest cycle.
Related
This is an angular 1 app.
I have a directive that looks like this:
function addLiquidity() {
return {
restrict: 'E',
scope: {
expanded: '='
},
templateUrl: 'liquidity.html',
controller: 'addLiquidityCtrl'
};
}
And the html that looks like this:
<add-excess-liquidity expanded="addExpanded"></add-excess-liquidity>
In another html I have this:
expanded: {expanded}
This is false by default thanks to the controller that in its relevant part contains this:
$scope.addExpanded = false;
Now, when clicking a button to show form for adding new stuff, "addExpanded" is set to true.
When then the button to actually save the form is clicked "addExpanded" is set to false again:
$scope.addExpanded = false;
I can see in the controller that that function is called and that addExpanded behaves correctly in the controller.
Actually, it behaves correctly in the view too. So the value in html:
expanded: {expanded}
reflects the value in the controller.
However, as soon as I switch to another tab (this app is using nav-tabs) and come back to the tab I was before, then the value in html is not reflecting the controller anymore(which is still doing its job like before, going through the same steps as before, included setting the "expanded" to false).
I tried forcing view update with both timeout and "apply" but didn't help. Also, it seems somwehere else an apply is fired at the end of every function.
I really don't understand what's wrong here. Where should I look? Something happen (I guess) when leaving the tab the first time. But so many things happen so it is not easy to debug. Any clue anybody?
EDIT
I added a watch like this:
$scope.$watch( 'addExpanded',
function(newValue, oldValue){
console.log('addExpanded Changed');
console.log(newValue);
console.log(oldValue);
}
);
First time I go to the tab then it will print to console. I then leave the tab, then come back to it. And this time, when clicking on button, nothing is printed to the console.
Roughly, what happens when I change tab is that the div containing the tab-content is set to "display:none/block" (I guess) as it is pure css that is showing tab content upon selection.
In the main controller a few things are done depending on which tab was selected, but I cannot see things that would "destroy" the connection between the GUI and the controller/directive.
Also, each time I go to that tab, I see that the javascript connected to the directive is initiating. And the variable "expanded" is reset to false.
The issue is that, for some reason, the connection between javascript and the GUI is in some way "lost" when visiting the tab the second time (and for all the future until I refresh the page).
I have a collapsible sidebar navigation, and any time users click a link on it, I have the sidebar hide automatically. I achieve this in the run method by using the following code:
app.run(function($rootScope) {
$rootScope.$on('$routeChangeSuccess', function () {
$rootScope.closeNavigation();
})
});
Where app is my Angularjs module.
The problem is that it gets a little unintuitive to use, when you click a link in the navigation for the page you're already on, and then nothing happens. What I would like is to have the sidebar close anyway, so that the users still get focus on the content, even if it's the same content.
But Angularjs doesn't execute the $routeChangeSucess event, if there is no route change happening. So what can I use instead?
You simply need to use ng-show = isPanelVisible and ng-click = closePanel() on the html sidepanel tag (e.g. a <div>).
Then, define $scope.closePanel() in the controller associated to the actual view. For instance
$scope.closePanel = function() {
$scope.isPanelVisible = false;
}
See an example here. Code is here. Note that it doesn't use ng-show, but the behaviour is the same.
I'm currently altering a website which was devided into iframes to now being embedded (with AngularJS), without any iframes.
There is a big problem with this: I had a Kendo UI auto-complete drop-down element for selecting locations. The behavior with iframe and embedded is totally different concerning scrolling in the area around/beneath the auto-complete drop-down.
Old app: the site (iframe) around scrolled and the drop-down still was visible and moved with the rest of the site until you selected an item.
New app: the drop-down box closes immediately and you have to retype some input to get it open again. Unacceptable usability!
How do I get an auto-complete drop-down (doesn't have to be Kendo if not possible) which does have the OLD scrolling behavior in embedded mode?
Well, I found a workaround which works fine for me:
In the directive html, I added a callback for the event k-close. In this callback in the controller I prevented the default behavior of close event (of course under specific conditions) with the following code in the controller:
$scope.closeCallback= function (e) {
if (someConditionForWhichDropdownShouldntBeClosed) {
e.preventDefault();
}
};
and here's the HTML of the directive:
<input
ng-model="model"
kendo-auto-complete="source"
k-data-source="locationDataSource"
k-select="selectLocation"
k-close="closeCallback">
In my case, I prevented the Dropdown being closed as long as no item was selected.
For this I added a new boolean scope variable which was false by default, was set true if dropdown opened:
$scope.locationDataSource = new kendo.data.DataSource({
type: "json",
serverFiltering: true,
transport: {
read: function (options) {
$scope.keepKendoDropdownOpen = true;
someOtherFuncionalityAfterSelectingAnItem();
}
}
});
and set false again after selecting (in the callback of the directive's k-select).
Would be nice to also watch if the user presses ESC or something, but until now it's okay enough.
Please feel free to make my solution better or post other solutions! :-)
(Follow on questions from Placeholder Hidden)
I'd like my form to validate existing data when it is loaded. I can't seem to get that to happen
I jQuery.each of my controls and call focus() and blur(), is there a better way than this? I tried to call ctrl.checkValidity(), but it wasn't always defined yet. When it was, it still didn't mark the controls.
I seem to have a timing issue too, while the focus and blur() fire, the UI does not update. It's as if the Webshims are not fully loaded yet, even though this fires in the $.webshims.ready event.
I also tried to call $('#form').submit(), but this doesn't fire the events as I expected. The only way I could make that happen was to include an input type='submit'. How can I pragmatically case a form validation like clicking a submit button would?
Here's a jsFiddle that demonstrates the problem. When the form loads, I want the invalid email to be marked as such. If you click the add button it will be marked then, but not when initially loaded. Why?
Focus and blur in the control will cause it to be marked.
BUT, clicking ADD will too (which runs the same method that ran when it was loaded). Why does it work the 2nd time, but not when initially loaded?
updateValidation : function () {
this.$el.find('[placeholder]').each(function (index, ctrl) {
var $ctrl = $(ctrl);
if( $ctrl.val() !== "" && (ctrl.checkValidity && !ctrl.checkValidity()) ) {
// alert('Do validity check!');
$ctrl.focus();
$ctrl.blur();
}
});
}
I see this in FF 17.0.5. The problem is worse in IE9, sometimes taking 2 or 3 clicks of ADD before the fields show in error. However, I get errors on some of the js files I've liked 'due to mime type mismatch'.
This has to do with the fact, that you are trying to reuse the .user-error class, which is a "shim" for the CSS4 :user-error and shouldn't be triggered from script. The user-error scripts are loaded after onload or as soon as a user seems to interact with an invalid from.
From my point of view, you shouldn't use user-error and instead create your own class. You can simply check for validity using the ':invalid' selector:
$(this)[ $(this).is(':invalid') ? 'addClass' : 'removeClass']('invalid-value');
Simply write a function with similar code and bind them to events like change, input and so on and call it on start.
In case you still want to use user-error, you could do the following, but I would not recommend:
$.webshims.polyfill('forms');
//force webshims to load form-validation module as soon as possible
$.webshims.loader.loadList(['form-validation']);
//wait until form-validation is loaded
$.webshims.ready('DOM form-validation', function(){
$('input:invalid')
.filter(function(){
return !!$(this).val();
})
.trigger('refreshvalidityui')
;
});
I am doing form validation in a function when a user wants to preview an invoice which is called by a route:
routes: {
"new" : "newInvoice",
"new/:assignmentid" : "newInvoiceAssignment",
"edit/:invoiceid" : "editInvoice",
"preview/:invoiceid" : "previewInvoice",
"preview" : "preview",
"delete/:invoiceid" : "deleteInvoiceModal",
"whyCant" : "whyCant",
"whatsThis" : "whatsThis"
},
And here is my two buttons (actually, a button and an href) on the form:
<div class="span8 alignRight">
<button id="saveInvoiceDraft" type="submit" class="btn btn-warning">Save Draft</button>
<a id="previewInvoice" class="btn btn-primary">Preview & Send</a>
</div>
When this invoice is created, the URL for the tag is set with:
var url = '#preview';
$('#previewInvoice').attr('href',url);
And finally, when I click on the "Preview & Send" button, the previewInvoice(invoiceid) function below runs, properly catches the one form field missing and displays the error message. At that point even if I populate the form field, that button is dead and no longer responds. However, the "Save Draft" button works perfectly and mimic's the same code as in the previewInvoice() function.
I know there is probably a better way to do this, but I was following that way it was done in another section of the app I inherited. Actually, as I am typeing this I am wondering since the sendDraft() function works and its a button and the previewInvoice() function does not, the fact that it is a href might have something to do with it.
function previewInvoice(invoiceid) {
var invoice = new Invoice({"invoiceid": invoiceid});
invoice.set({"invoiceid": invoiceid,"invoicestatus": "draft"});
formGetter(invoice);
validateInvoiceForm(invoice);
if (window.errors.length == 0) {
//business logic here
if (window.panel == undefined) {
// business logic here
}
else {
//save business logic here
}
}
else {
showInvoiceErrors();
}
}
Any ideas why the button no longer responds? I am not seeing any error's in the console. I added a consol.log inside the function to display the value of a different form element, and it displays the first time in the console, but if I change the data and click the button again, that log does not update, which to me is another clue that it is just not firing.
Backbone.History listens to hashchange events to trigger navigation to routes. When you first click the previewInvoice button, the URL hash fragment is set to #preview, and the matching route is triggered.
When you click the same button the second time, the hash doesn't actually change, and therefore the router doesn't catch it.
I'm having a hard time recommending a good solution to this problem. Normally I would recommend catching the click event and calling router.navigate("preview", {trigger:true}); manually. However, based on your code sample it looks like your application is built around the Router, and there isn't a View layer for DOM event handling as you would expect in most Backbone applications.
On the Router level this is a bit trickier to solve. You could use router.navigate to set a dummy hash after the preview route has been executed. This would cause the link to trigger a hashchange on the second time as well. Unfortunately this would mean that the preview page would not be bookmarkable, and since you're not using pushState, would leave an extraneous history entry.
I'm afraid this issue will have to either be solved with a hacky fix (as outlined above) or a major refactoring.
I have this same problem, my solution is to put in a handler that does a fake "in-between" route that is hidden from history, so that Backbone.history will register the navigation as a change and trigger the action.
Put a class on links that you need the route action to trigger regardless if the URL is the same:
do it
In your Backbone view, put in an event handler:
events: {
"click .js-ensureNav": "_ensureNav",
},
Actual handler:
_ensureNav: function (event) {
var route_name = $(event.target).attr('href').slice(1);
Backbone.history.navigate("fake");
Backbone.history.navigate(route_name, {trigger: true, replace: true});
},
Instead of
var url = '#preview';
$('#previewInvoice').attr('href',url);
Try this
var date = new Date();
var url = '#preview?date:' + date ;
$('#previewInvoice').attr('href',url);
So Every time new request will be generated and this will solve your problem.