How do I replace my angular location without a controller refresh? - javascript

Suppose I have an Angular app for editing eCards. Creating a new eCard uses a path like #/ecard/create and editing an existing eCard uses a path like #/ecard/:id. A tabbing system lets us have multiple eCards open for editing at a time.
We'd like an autosave feature like what users would expect from e.g. modern webmail or wiki software (or StackOverflow itself). We don't want to save an eCard draft the moment the user opens the Create form, which would give us a lot of drafts of blank eCards, so we start autosaving once the user starts typing.
I'd like to write code like this in our controller (this is simplified to not include e.g. error handling or stopping the autosave when the tab is closed, etc):
$scope.autosave = function () {
ECardService.autosave($scope.eCard).then(function (response) {
$location.path('/ecard/' + response.id).replace();
$timeout($scope.autosave, AUTOSAVE_INTERVAL);
});
};
$timeout($scope.autosave, AUTOSAVE_INTERVAL);
The above code works great, except for one thing: when the location changes, our controller reloads and the view re-renders. So if the user is in the middle of typing when the autosave completes, there's a brief flicker, and they lose their place.
I've considered several approaches to mitigate this problem:
1) Change the path to use the search path and set reloadOnSearch to false in the ngRoute configuration. So the path would change from #/ecard?id=create to e.g. #/ecard/id=123 and thus not force a reload. The problem is that I might have multiple eCards open and I do want changing from e.g. #/ecard/id=123 to #/ecard/id=321 to trigger a route change and reload the controller. So this isn't really feasible.
2) Don't bother editing the URL and deal with the back button giving a weird behavior in this case. This is tempting, but if a user opens their list of existing eCards and tries to open the specific eCard that has been saved, we want the tabbing system to recognize that it should just display the currently existing tab rather than open a new tab.
We could theoretically address this by updating our tabbing system to be smarter; instead of just checking the path, it could check both the path and the persistent id, which we could store somewhere. This would make the tabbing system significantly more complex, and that seems like overkill for this feature.
3) Only change the URL when the user is not actively editing, e.g. write a $scope.userIsIdle() function which returns true if it's been at least 10 seconds since the user made any edits, then update the path based on that. A simplified version of this would look something like:
$scope.updatePathWhenSafe = function (path) {
if ($scope.userIsIdle()) {
$location.path(path).replace();
} else {
$timeout(function () {
$scope.updatePathWhenSafe(path);
}, 1000);
}
};
I ended up going with option #3; it was significantly simpler than option #2, but a lot more complicated to implement and test than I'd like, especially once I account for edge cases such as "what if the tab is no longer the active tab when this timeout fires?" I'd love for option #4 to be possible.
4) Go outside Angular to edit the current location and history, assuming this is necessary and possible. This would be my preferred solution, but my research indicates it's not safe/advisable to try to go around the $location service for changing your path or editing history. Is there some safe way to do this? It would make things so much simpler if I could just say, "Change the current path but don't reload the controller."
Is option #4 possible/feasible? If not, then is there a better way? Maybe some magical "Do it the angular way but somehow don't refresh the controller"?

This is not angular way, but it can be useful. After receiving data you can check whether there is an focused element (user is typing). If so, then you need to define a function that is performed once when element lose focus. If no focused element, the change url immediately.
Like this:
ECardService.autosave($scope.eCard).then(function (response) {
if($(':focus').length){ //if there is focused element
$(':focus').one('blur', function(){ //
$location.path('/ecard/' + response.id).replace(); //perform once
});
}
else{
$location.path('/ecard/' + response.id).replace();
}
});
Of course this is not the most elegant solution, but it seems to solve your problem.

If you have code that needs to run across multiple view controllers AngularJS provides a root scope for such instances. You can find the documentation here.
However I would recommend against having a tabbing system that is actually multiple views. Having multiple items open means to have them all in your work space.
You might want to consider a single view with Angular directives for your e-cards. That way they could each have their own scope and would be available at an instance without re-rendering the page.
They would also be able to share the functions defined in the controller's $scope, without the need for an app wide root scope. Note that scope has to be enabled on directives. scope: true
Check out the AngularJS site for tutorial and documentation on this.

It seems that the best solution for the problem you're describing would be to use a state machine like ui-router.
With a library like that one, you can have a single page app that has multiple states (that you can also make part of the url), so whenever the state changes, you can save your e-card and you'll never have any visible reloads because you're working on a single page application.

So I understand the path wants to reflect the id of the latest version, so in that case you would need to refresh every save.
But, what about if the path was something like ecard/latest as a alias for the latest version. That way you wouldn't have to refresh your view since you don't have to change your path, and just implement something in the back-end directs the param latest to the id of the latest version.

It turns out there's a way to do exactly what I want, although it's not officially blessed by Angular. Someone opened an Angular ticket for this exact use case: https://github.com/angular/angular.js/issues/1699
The proposed change was submitted as a pull request and rejected: https://github.com/angular/angular.js/pull/2398
Based on the comments in the original ticket, I implemented a workaround that looks like this:
app.factory('patchLocationWithSkipReload', function ($location, $route, $rootScope) {
$location.skipReload = function () {
var prevRoute = $route.current;
var unregister = $rootScope.$on('$locationChangeSuccess', function () {
$route.current = prevRoute;
unregister();
});
return $location;
};
});
I'm then able to basically (error handling omitted for brevity) say
ECardService.autosave($scope.eCard).then(function (response) {
$location.skipReload().path('/ecard/' + response.id).replace();
$scope.resetAutosaveTimeout();
});
Basic testing shows this works great!

Related

Loop variables setting on localhost, but not on live site

I'll start off by saying that as an avid browser (but having never posted), thanks in advance for any insight.
I am making a page for my employers code department, which needs to use dynamic dropdowns, since the amount of dropdowns fluctuates per page.
Context:
Originally we were using standard Bootstrap dropdowns, but some of the web providers our clients deal with aren't loading in bootstrap. My initial thought was to side-load it in, however, we work on templates that must work across the board for all of our clients and all of their web providers. Long story short, there were stability issues with bootstrap CSS files overriding various websites that weren't built with bootstrap in mind.
Onto the problem:
Here is my CodePen for you to view.
dropdown = () => {
const dropdown = document.getElementsByClassName("dropdown");
for (let i = 0; i < dropdown.length; i++) {
const activeDropdown = dropdown[i].id,
down = "#downChev-" + activeDropdown,
up = "#upChev-" + activeDropdown,
downIcon = document.querySelector(down),
upIcon = document.querySelector(up),
dropdownNumID = "#" + activeDropdown,
dropdownID = "#dropdown-" + activeDropdown;
if (event.target.matches(dropdownNumID)) {
if (upIcon.classList.contains("show-icon")) {
upIcon.classList.remove("show-icon");
downIcon.classList.add("show-icon");
} else if (downIcon.classList.contains("show-icon")) {
downIcon.classList.remove("show-icon");
upIcon.classList.add("show-icon");
}
document.querySelector(dropdownID).classList.toggle("dropdown-show")
}
}
}
Currently I've built out a dynamic dropdown system that utilizes ID's specific to each dropdown. The problem is that in the first loop that goes through the dropdowns to create the supporting variables, it doesn't create the activeDropdown variable and instead returns a blank variable when logged.
I've troubleshooted in multiple scenarios and have come up with the following:
On localhost and in the CodePen, the code seems to work fine. Once added to the live sites, it breaks at initializing the activeDropdown variable within the loop, returning a blank variable when logged.
If I remove the if condition right after the variables are set in the loop, and log the activeDropdown variable. It logs fine. This leads me to believe that it may be a synch issue. I then turned the if condition into its own function and added a settimeout, allowing the variables to be initialized prior to running the execution items within the new delayed function. Once that was added, the activeDropdown variable was no longer setting properly. It just returns blank when logged.
I'm not sure if my logic of execution is off, or if I've overcomplicated the entire thing. The only real goal here is to make a dynamic structure that allows my front-end dev team to add or remove model divs at will, with a simple modification to the ID's as the list grows.
It's also possible that other scripts that we don't have access to are creating the issue on the live site. However, I assume you guys will be able to find something wrong with my code lol.
It's difficult to know what's going on since your production environment is likely very different from the code pen, but I think a couple things could be happening.
1) Could be that the JavaScript is running before the entire page has loaded. Make sure your JavaScript code that is equivalent to the CodePen is loaded last on the page. (i.e. put the <script></script> tag at the very end of the document).
2) Some variables could be conflicting, since you are loading the JavaScript directly in the body of the document in the global scope. I have some general stylistic recommendations that may help you solve your problem:
From looking at your CodePen, you're using two different methods to react to click events: You have setup an event listeners with a callback on window.onclick, as well as 2) a function called dropdown that you are attaching to an onclick attribute in the HTML.
I would recommend staying away from the inline onclick attributes, and instead added an event listener to the container of the dropdown. That way your markup is not coupled to the dropdown JS, and you are listening to clicks within that container and not the whole application. You can use the same event listener for opening the dropdowns and closing dropdowns. See this stackoverflow post for more information about this.
In addition, I recommend adding a wrapping function around your code to prevent you from polluting the global scope. This function can be immediately invoked when the page loads, and will add the event listener on the container. You can read more about IIFE's here.
Here a CodePen demonstrating these two recommendations. While I don't know if this will help you solve your problem, hopefully at least you'll learn something new!

MWC: how to rerender on URL change

Heyo everyone,
story time - skip if you don't care
I'm just starting out with Meteor + Polymer using Synthesis by #aruntk and I'm very happy about the results and greatful for the time he's invested in this project. There's one issue I'm having though.
I've previously only changed a iron-pages object to change the content of my view. Putting that in a FlowRouter like FlowRouter.route("/", action: {ironpages.select("home");}); works just fine. However, my site is getting more complex and I want to rerender a whole section now. I'm being told to do it reactively which is (to my poor understanding) the preferred way of building Apps here.
tl;dr - skip to here if you don't care about stories
So what I did is just putting mwcLayout.render("test-layout",{"main":"yas-manual-page"}); in my Router action. However, I have to reload to make the changes visible which is not what I want.
the router action is being called when changing the URL
the mwcLayout.render() call works if I reload the page once in the initial building of the site
calling mwcLayout.render() again at a later point does not do anything
I've read up on the topic and people say it's a problem with single-page apps and not building it reactively and whatnot, but I have no idea how this is not reactive. It's reacting to the URL change.
Please, if you have a minute, share some insight with me, I'm really stuck. :slight_smile:
Have a wonderful day y'all!
disclaimer: it's a repost form the Meteor forums which suggests coming here instead.
This behavior is added as a feature of mwc layout to prevent multiple re rendering during each route change. Workarounds here are to create another mwc layout or to set third argument forceRender. From the mwc:layout docs
forceRender
In mwc:layout we dont re render the layout unless the new layout is not equal to the current layout or forceRender argument is set. This is to prevent unwanted rerendering while changing routes(even if you change a param/queryparam the route gets rerun so does the render function written inside FlowRouter action). forceRender comes in handy when you have to change the rendering while keeping the current layout.
...
<mwc-layout id="demo-landing">
<div region="header"></div>
<div region="main"></div>
</mwc-layout>
...
imports/startup/client/router.js
...
action:function(params,queryParams){
mwcLayout.render("demo-landing",{"main":"test-layout1","header":"test-header"});
}
...
Now if you try
mwcLayout.render("demo-landing",{"main":"test-layout2","header":"test-header"});
from console it wont work since layout is not changed and forceRender is not set.
This works->
mwcLayout.render("demo-landing",{"main":"test-layout","header":"test-header"},true);

Iron Router hook on a route before actually changing the template

i am trying to show the user a payment popup as soon as he clicks on a payed object.
But after he pays he should directly enter the content he clicked on.
Therefore i think its a good solution to solve this with the router, because i want every link on the page that redirects to this content to show this popup.
My problem is i want to show the popup before redirecting the user.
So i tryed the onBeforeAction hook and stuff but everything working with the iron router seems to only hook in after the URL of the browser changed and the current template was unloaded.
Do you have an idea how to get this kind of behavior?
Cheers
Based on this answer, here is how you can hook the router using Router.onStop():
// onStop hook is executed whenever we LEAVE a route
Router.onStop(function(){
//check if the current route is the page from where you need to show your
//popup and show it based on, for instance, a session variable containing
//the previously clicked content id.
});
It's a common use case that I don't think is directly achievable within the iron router framework at present (although I would be delighted to be corrected!). As you've discovered, onBeforeAction is run before the page has rendered but after the new route has been run, so the old page has already disappeared.
Effectively, you're looking to queue the running of a new route until a certain action has been completed. The use case for which I've experienced this requirement is page transitions, for which the best solution appears to be to do completely the opposite of what you propose: i.e. to add the logic to an event attached to the link, and only redirect to the new route once that logic has been satisfactorily completed (i.e. the popup has been closed in your case).
I agree that doing something in the router would be a sensible way to approach this, but I'm not sure it's possible in iron router as things stand. Note that this has already been raised though!
Will this workshop?
'unload - runs just once when you leave the route for a new route.'
From
https://github.com/EventedMind/iron-router/blob/devel/DOCS.md#unload-hook

How to better organize multi-views in a web single page app?

Assume I have a lot of views in my single page app. It means, I put some view inside a div and show this or that view depending on user's actions. So, I usually show on view at a time. For example, I have these views - dashboard, settings, entries, entry details.
Depending on current state of the app one of these views is visible and others aren't. When I go this way dashboard > entries > entry details, then edit some entry details and click save or back button, I want the app to go back to entries. But if I have a link on dashboard that, for example goes to the latest edited entry, I can go this way dashboard > entry details, and in this case I want to get back to the dashboard by clicking save or back button.
This case can become even more complex with deeper views paths. Right now I manage it like this - When a button responsible for views switching is clicked, the variable state is being changed to something like from_dashboard_to_entries. I listen for that variable to change, then do all the view switching from another function depending on the state variable value. This way I have to manually define all possible scenarios and test all the possible combinations of cases. Right now this approach works quite well, but I am worried about deeper, or longer views sequences.
My idea is to somehow create a history of views in an array or something but I am not clearly understand how to do it better.
Please share your thoughts how you would organize this.
Simply don't worry about the source view - you don't have to know it to change to the destination view. If you want a history, instead of manually storing it, consider using the HTML5 History API, preferably with a wrapper library (e.g. History.js).
Rather, We should be setting the view while changing the state of application. This may optimize the initial app launch time by not loading all the views at launch and will give a way for on-demand loading.

Multiple Dashboard widget instances don't survive widget update. Any way to prevent this?

I've written a Mac OS X Dashboard to show the StackOverflow flair of yourself and other people.
My problem is that whenever I update that widget to a new version, all previous instances are removed and a single new instance is created on the Dashboard. So if you previously followed the flair of 4 people you'll have to recreate the widgets and enter their user-IDs again. :(
Is there any way to keep all running instances while updating a widget?
Checking the preferences file, I see that a new instance is created after a new version of the widget is deployed. Is this by design? Does all widgets work like this, by any chance?? If so, can this be manually circumvented somehow?
You can find the widget in question, including the project files, at http://widget.huxhorn.de
I've changed the code in remove() to set the preferences to the correct values instead of null as suggested below - but this doesn't help, either.
What's my mistake?? Help! I'm stuck!
The other problem of my widget has been fixed:
I accidentally called
widget.preferenceForKey(null, dashcode.createInstancePreferenceKey(userIdPrefKey));
instead of
widget.preferenceForKey(dashcode.createInstancePreferenceKey(userIdPrefKey));
but this is now fixed (not yet released).
I know about this tool that takes over the install process: http://junecloud.com/software/mac/smart-widget-installer.html
(source: junecloud.com)
I think your calls to setPreferenceForKey and preferenceForKey look strange. Where does that null come from. Shouldn't they look like
widget.preferenceForKey(dashcode.createInstancePreferenceKey(userIdPrefKey))
widget.setPreferenceForKey(value, dashcode.createInstancePreferenceKey(userIdPrefKey));
Does the syntax with null
widget.setPreferenceForKey(null, dashcode.createInstancePreferenceKey(userIdPrefKey))
in remove() delete the preference? That would explain you situation I guess. On Update the remove() gets surley called and thus on restart of the widget the preferences are gone.
I guess there is only one way to keep your preferences during an update. Create one preferencekey without relying on dashboard.createInstancePreferenceKey but instead think yourself of a unique key value.
In this preferenceKey you could then save all ever entered userids (by any instance) with a growing (say comma seperated) list. When a widget instance is opened an the widget.preferenceForKey(dashcode.createInstancePreferenceKey(userIdPrefKey)) isn't already set you could let the user choose one of those by giving him a select or such a thing
I was just thinking maybe the update process is nice enough to migrate the preferences to the new verison. Even when you don't remove the preferences I think the widget.identifier (createInstancePreferenceKey) will have changed an thus you can't access the "old" settings.

Categories

Resources