My app has a main view with some hidden elements that can be activated by the user. Like a typical sidebar on mobiles that slides in from the left. Or a cart that reveals the details when you tap on it.
The information in those initially hidden elements is always up to date since their content is mapped in the main app template. So my routes would not need to render any DOM.
The transitions are JS based. But now I want those states to be reflected in the URL, in order to get a consistent back button behavior.
How can I achieve that using the ember framework?
Update to make it more clear, what I am talking about:
To my understanding triggering routes in ember has mainly two side-effects:
Represent the new state in the URL, enabling consistent browser history support.
Render templates that manipulate the DOM based on some data
In my case, when for instance a user taps on the minimized cart I need:
Represent the new state in the URL, enabling consistent browser history support.
Execute my showCart() JS Function (no DOM changes, no template rendering)
When the user now taps on the browser back button, closeCart() should be executed (based on the fact that the state in the URL carries the information that the cart is open).
The problem is, where can they access these slide in windows? Can they only be accessed from a single location in your route map? Or can the user click on it regardless of whatever view they are in? And if they click it from different views, do you move them back to a different route just to pop out some slide in window?
If they can only click it from one place, then you could just add code in your setupController of that specific route to fire the js to slide out the window.
setupController: function(){
Ember.run.scheduleOnce('afterRender', this, function(){
//run some js to slide out the window
});
}
Honestly if they can click a button anywhere and have the slide out appear, I wouldn't try putting it into the url. Just my opinion though.
You can use the activate and deactivate methods from your show route, to know when is entered or exited from your route:
App.ShowRoute = Ember.Route.extend({
activate: function() {
alert('Entering in show');
// here would be your showCart
},
deactivate: function() {
alert('Exiting from show');
// and here the closeCart
}
});
I created a live demo here, to you see this working.
Related
I have an angular application which is kind of the main application that hosts sub application inside it. The sub applications are also angular applications. The way that I load sub application is through Iframes.
The sub-applications are shown as a list and when I click on an tab, that application is loaded. When I am making some changes to the data in an application and if I click on another sub-tab, I wanted to show a warning message saying " changes will be lost". I am able to achieve it by using beforeunload event as below. I can check to see if there are any unsaved changes and show the warning popup.
#HostListener('window:beforeunload', ['$event'])
unloadNotification($event: any) {
if (**my logic here) {
$event.returnValue =true;
}
}
The only problem with this is, when I click the other sub-tab, the host application highlights that sub-tab and then the warning pop-up is shown. If I click on stay button on the popup, I am able to be on the sub-tab I want but on the host application the other sub-tab is highlighted. So I am trying to see a way to not highlight the other tab if I want to stay on the current sub-tab. Something before beforeunload.
My understanding is you have to update an existing project that's been constructed in such a way that the beforeunload event is employed to achieve something similar to the Angular feature called route guards. You do or don't load the selected iframe depending on your logic implemented with the help of beforeunload event. You don't seem to favor changing this implementation that's why seeking a solution that covers your requirement applying a similar approach like another window event.
If my understanding is correct, you need to refactor this implementation so that the "guarding" happens depending on the inner workings of the "host application" where it actually has to be in the first place. The issue you have arises because by the time the iframe's unload event is canceled, host application's canceled tab gets already selected.
In short, this seems to be handled in your tab's selection event.
My answer may or may not propose a solution you would accept, since we don't have all the details like which component suit you use, I can only present a pseudo solution:
Inside the component where your navigation tab's ui logic takes place:
onTabSelected(selectedIndex) {
if (..your logic here) {
loadIframe(selectedIndex);
highlightTab(selectedIndex);
}
}
There is no such event as before beforeUnload. Two applications i.e. host and child can communicate via postMessage. so in beforeUnload event you can send a postMessage to child application to highlight existing tab. for eg:-
let say you had reference of child window in variable name child1.
child1.postMessage('{'tabId': 'tab1'}');
Your child application can receive this message and highlight the tab with identifier tab1.
Maybe we can create a communication system using the window.top object for example let's say that in the child apps you assign some id.
Then in the main app which hosts the others you create a subobject to window.top.appsMap = {};
Then from each child app you update a boolan value for example
window.top.appsMap['applicationId'] = hasChangesOrNot
Then from the top app where you change tabs you can check if the current opened tab app id has changes and not switch the tab but just try to unload the current iframe, it's not the best solution but if should work you can maybe also set some communication channel backwards which would trigger from the main app which hosts the others the save from the child app
In child app
const origin = this.getCurrentHostOrigin();
if (cancelSwitchTab) {
window.parent.postMessage({}, origin);
}
In host app:
this.renderer.listen(this.windowsRef.nativeWindow, 'message', event => {
const message = event.data;
*** your logic to revert highlight to current tab ***
});
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
So I have a set of navigation links at the top of my page.
One of them links to a list of hardware the users owns showing name and description.
I click the nav link and my list of hardware if rendered fine.
Go into database, change the name of one of the hardware pieces.
Now i click the nav link again on the page. I would expect backbone to
render the page again. But it doesn't.
I have a message print to the console every time render is called. The second click does not run the render function on the view.
I imagine backbone prevents re rendering the view or calling events when a route that is already open is activated. Is there any way to get it to rerender the already open view when I click on nav link a second time?
You are correct that Backbone prevents a route from triggering if it's already on that route.
There are a few solutions.
You could forcefully trigger the route by hijacking the links and calling router.navigate("route/path", {trigger: true});.
Or, even better, avoid triggering the router and use an api object. It's something Derick Bailey has written about here. Every click after the first would use the api to re-run the render code, bypassing the Backbone router. You can see a more advanced use of this method in the bbclonemail app.
I have an AngularJS single-page app displaying 3 views (which are actually 3 directives). To illustrate my question, let's assume my UI is identical to that of GMail and that my 3 views are:
Navigation Pane (left) -- where GMail displays folders like "Inbox", "Drafts"...
Toolbar (top right) -- where GMail displays buttons
Content Pane (bottom right) -- where GMail displays messages
These 3 views need to update themselves whenever the path changes. For example:
The Navigation Pane needs to highlight a specific item.
The Toolbar needs to show/hide certain buttons.
The Content Pane needs to load and display specific data from the server.
What's the best way to do this in AngularJS?
So far, I have:
Ruled out the use of $routeProvider.when() because I need to update three views, and $routeProvider only supports one ngView.
Created a SERVICE that watches the current path and uses $rootScope.$broadcast() to warn the controllers that the path changed.
(OR, IN LIEU OF #2) Created a CONTROLLER that does the same as #2.
Caught the event broadcasted by #2 or #3 with $scope.$on() (I do this in a view controller).
This kind of works, but I have several issues:
The "path change" event is often broadcasted BEFORE my event listeners are in place, especially at initial page load. This is probably due to the fact that my view templates are loaded from the server and the listeners can't be set up before the templates have finished loading.
Isn't there a more efficient/automated way to watch for path changes in AngularJS? (See my code below, it seems pretty "manual".)
Does the code that watches for path changes belong in a SERVICE or in CONTROLLER? (I'd lean towards a service, since a controller is more to add behavior to a view.)
How to guarantee that my views will catch the "path changed" event? Should I use an event at all? (maybe I could store the path in a service, and have my views watch the service instead?)
My code to watch for path changes (I put this in a service or in a controller):
var watchExpression = function() { return $location.path(); };
var listener = function(newPath, oldPath) {
// Broadcast event on $rootScope
$rootScope.$broadcast('PathChanged', newPath);
};
$rootScope.$watch(watchExpression, listener);
Then, in a view controller, I catch the event:
$scope.$on('PathChanged', function(event, path) {
// Update view based on new path.
});
Any help appreciated. Thanks.
Your final version seems fine with the locationChangeSuccess, but for others reading this, I think you ruled out the $routeProvider too quickly. You can have one ng-view for the main content pane that changes with the path, and then other independent ("static") controllers/templates for the navigation pane and toolbar.
Now to listen to route changes in these other 2 controllers:
$rootScope.$on('$routeChangeSuccess', function(evt, cur, prev) {
...do what you have to do here, maybe set a $rootScope.path as you did
})
All using native Angular functionality. I actually do this in http://provok.in, a website I built using Angular, and I have a similar layout (well, not exactly, but I have "static" sections and an ng-view for the main content, dynamically updated based on the path, by the routeProvider).
Is there a good pattern for implementing a Backbone.js Router that responds differently for these two events:
Direct navigation - A user navigates directly to page by putting the url directly into the browser or in the application via a call to Router.navigate()
Browser "back" button - The user presses the "back" button to go to a page that they were at previously.
The reason I ask is because if the router is called as a result of the user pressing the "back" button, I'd like to just put the old view back into its element via $('element_id').html(view.el) without the reloading data into the model and re-rendering the view.
If the router is called as a result of the user navigating directly to the page by entering the url into the browser, clicking on a link, or a call to Router.navigate() from within the application, I would like the router to instruct the model to re-request data and trigger a re-rendering of the view.
Any help and suggestions would be appreciated, thanks so much!
I faced a similar problem a while back. This may be more than what you want, but here's what I found works.
Direct navigation
For direct navigation, the usual flow is something like this:
Initialize the app: Most backbone.js projects will have some code that initializes the app (App.init). Although a lot of examples insert the JSON for the collection (todos, etc.) in the actual HTML, I personally like to use this code as an opportunity to fetch the collection after the page has loaded, something like this (coffeescript):
window.App =
...
init: ->
#todos = new App.Todos()
#todos.deferred = #todos.fetch()
#threads.deferred.done ->
App.appRouter = new App.AppRouter(collection: self.todos)
...
(The use of jQuery's deferred is to make sure that the collection is fetched before actually rendering the page.)
Initialize the router: Here you get a chance to assign an element to the router and assign the collection to the router (I'm using the 'SwappingRouter' from thoughtbot's backbone-support, which I highly recommend checking out):
App.Router = Support.SwappingRouter.extend
...
initialize: (options) ->
#el = $('.content')
#collection = options.collection
Execute the route handler: This is the last step, at which point the collection is already initialized and the router has a pointer to it, so we just have to create the view and render it:
show: (id) ->
view = new App.TodosView(model: #collection.get(id))
#swap(view)
(Swap renders the view and does a few other things to clean up.)
Browser back button
In this case, neither App.init nor AppRouter.initialize is called, so the collection by default won't be reloaded. Backbone will automatically call navigate on the previous route, so depending on what's in your route handler, the view may be re-rendered. In the example above, it would be (swap calls render), but you could work around this.
The key problem here and always with backbone is that you're working with a state-less (HTTP) protocol and a state-ful (rich client-side app) environment at the same time. Lining the two up so they work well together can be pretty tricky.