I'm building an app that has a user settings panel that pops up in a modal dialog. The panel should be accessible from any page in the app. I'm trying to figure out the best way to build this in Ember.
I would like to build it in such a way that when the app redirects to the "/settings" route, the modal dialog appears with the current route in the background as you would expect. Then when the modal is closed the app redirects back to that route.
If the user goes directly to "/settings" from her browser then the modal will appear with a default page in the background.
Here is what I have so far:
export default Ember.Route.extend({
defaultParentRoute: "project.index",
beforeModel: function(transition){
// get the name of the current route or if
// user came directly to this url, use default route name
var parent = this.get("defaultParentRoute");
var application = this.controllerFor('application');
if(application && application.get('currentRouteName')) {
parent = application.get('currentRouteName');
}
this.set("parentRoute", parent);
},
renderTemplate: function(){
// make sure the current route is still rendered in the main outlet
this.render(this.get("parentRoute"));
// render this route into the 'modal' outlet
this.render({
into: 'application',
outlet: 'modal'
});
},
actions: {
removeModal: function(page){
// go back to the previous route
this.transitionTo(this.get("parentRoute"));
}
}
});
This works pretty well when navigating to the the route from a link in the app. However if a user goes straight to "myapp/settings" in her browser then the default page template gets rendered but without any data or it tries to use the 'model' data from my settings route.
How do I make sure the template underneath the modal gets rendered with the appropriate data?
Here's a JS Bin to demonstrate. Try clicking on 'settings' from any page in the app, then refresh the browser while the settings modal is open.
This organization seems a bit unnatural given Ember conventions. Generally the URL is supposed to represent a serialized version of state sufficient to reconstruct where the user was (see this old discussion).
It seems you want to put the modal state and the current route into the URL. It might be more natural for the settings modal panel to be accessible from other routes but to not change the URL, and then have another separate route which is dedicated to settings and shows only the settings.
Modal panels seem more like a drop-down menu, the opening and closing of which do not change the URL, even though they represent a minor state change.
If the reason you want to have the settings modal reflected in the URL is so that people can bookmark it or share the link, one option would be to have a permalink available on the settings page that gets them to the other dedicated route that is shareable.
The tricky bit with not having the settings as a route is that there is not an obvious place to load the model which behaves as nicely or works as easily as a route's model hook (i.e., which waits until the promise is resolved to complete the transition).
One way around this is to put functionality to load the settings model in a settings service, which can be injected anywhere:
SomeController = Ember.Controller.extend({
settings: Ember.inject.service()
});
And have the settings service only show itself once the model has loaded.
SettingsService = Ember.Service.extend({
settingsLoaded: false,
ensureLoaded: function() {
var _this = this;
return new Ember.RSVP.Promise (resolve) {
if (_this.get('settingsLoaded')) {
resolve()
} else {
_this.store.find('settings', someId).then(function(model) {
_this.set('model', model);
_this.set('settingsLoaded', true);
resolve();
});
}
};
}
});
Finally you can have a function on some controller that wants to show the settings modal only show it once the settings are loaded:
showSettings: function() {
this.get('settings').ensureLoaded().then(function() {
... # something to show the modal pane.
});
}
Related
I'm building an angular app where I present a navigation bar on the left with normal ui-sref's going to states like schedules and clients. On the clients view I present a list of clients (which animates in, slides in from left). Then I want to achieve the follow:
When a client is clicked on, load their information into the main part of the screen
Update the URL so that if they were to refresh at this point the same client would be selected
DO NOT re-instantiate the clients controller, as doing so re-animates the client list sliding in.
I've got #1 working, but can't get 2 or 3 to work in a way that I want. I can get the url to update, but doing so re-instantiates the clients controller and re-animates the client list, and no amount of {notify: false} or any other combo of options I've tried seems to do the trick. I did see the $urlRouter.deferIntercept() but I'm not sure how that applies to this situation. Do I need multiple views to achieve this, where clicking on a client just updates the "profile" section of the page? Thanks so much!
You can achieve this by loading a particular client in a substate of clients, with an associated URL that identifies the client.
$stateProvider.state('clients', {
url: 'clients/',
resolve: {
clients: function () {
// return the clients collection or a promise which will resolve with it
}
}
// template, controller, etc
});
$stateProvider.state('clients.client', {
url: 'client/{id:[1-9][0-9]+}/', // as this is a substate, this gets appended the the parent state's URL for an end result of something like /#/clients/client/1
resolve: {
client: ['$stateParams', 'clients', function ($stateParams, clients) {
// Lookup the client in clients using $stateParams.id and return it or a promise that will resolve with it
}]
}
// template, controller, etc
});
Then put a ui-view in the template for the clients state to give the clients.client substate somewhere to render.
When you first load any URL that starts with clients/ you'll enter the clients state. Navigation to and between substates will only run the substate transition code (resolves, controller, etc). Until you leave the clients parent state and return, the clients transition code will not run again.
The ui.router readme has good info about nesting states, and they a provide demo app that really helped me understand the idea of a state hierarchy. That demo bears a lot of similarity to what you're building.
So I'm trying to update a list view in a parent tab window using Angular.js with the Ionic Framework, but I can't quite seem to figure out how to do it.
Here is the code for the child window:
$scope.currentUsername = Parse.User.current().get("username");
$scope.saveChanges = function saveChanges(user){
var currentUser = Parse.User.current();
var newUserName = user.newUsername;
currentUser.set("username", newUserName);
currentUser.save(null, {
success: function(currentUser){
alert("Changes successfully made!");
}
});
$state.go('tab.more');
}
$scope.cancelChanges = function cancelChanges(){
$state.go('tab.more');
}
And I am trying to take the data once it's sent to Parse back to the parent view but I don't know how to refresh the page after the script is run. In my function cancelChanges() I simply used $state.go to go back to the parent view because why update the view when there is not data to be updated.
Here is the code for my parent window:
$scope.currentUser = Parse.User.current().get("username");
$scope.editProfile = function editProfile(){
$state.go('tab.more-editusername');
}
$scope.logOut = function logOut(){
Parse.User.logOut();
alert("Logout successful!");
$state.go('login');
}
ionic views are cached by default... so I do not believe the update is happening unless you force it to
From Documentation
View LifeCycle and Events Views can be cached, which means controllers normally only load once, which may affect your controller
logic. To know when a view has entered or left, events have been added
that are emitted from the view's scope. These events also contain data
about the view, such as the title and whether the back button should
show. Also contained is transition data, such as the transition type
and direction that will be or was used.
you can try and listen for the $ionicView.enter event and then update the view with the new data
I would like to know if it possible to extend in some way the mechanism Marionette Layouts are based on creating a sort of stack like navigation.
Marionette behaviour.
Before a region show()'s a view it calls close() on the currently displayed view. close() acts as the view's destructor, unbinding all events, rendering it useless and allowing the garbage collector to dispose of it.
My scenario.
Suppose I have a sort of navigation mechanism where a Layout acts as controller and first displays an ItemView called A, then a click somewhere allows to switch to ItemView B. At this point, an action on B (like for example a tap on a back button) allows to return to A without recreating it.
How is it possible to achieve the previous scenario without creating again A and maintaning its state?
For iOS people, I would like to mimic a sort of UINavigationController.
Any advice?
EDIT
My goal is to restore a prev cached view with its state without creating it again.
My scenario is the following. I have a layout with two regions: A e B.
I do a click somehere within A and A and B are closed to show C and D. Now a back click would restore A and B with their states. Events, models, etc...but since views are closed events are removed.
Use a backbone router to listen to URL change events. Setup routes for each of your views and then have the router call the layout to change the view it's displaying in response to each route. The user could click back or forward any number of times and the app responds accordingly and displays the correct view. Your router might look like:
var Router = Backbone.router.extend({
routes: {
'my/route/itemViewA': 'showItemViewA',
'my/route/itemViewB': 'showItemViewB'
},
showItemViewA: function () {
layout.showItemView('a');
},
showItemViewB: function () {
layout.showItemView('b');
}
});
Your layout might look something like this:
var Layout = Backbone.Marionette.Layout.extend({
regions: {
someRegion: 'my-region-jquery-selector'
},
initialize: function () {
this.createViews();
},
createViews: function () {
this.views = {
a: new Backbone.Marionette.ItemView,
b: new Backbone.Marionette.ItemView
};
},
showItemView: function (view) {
this.someRegion.show(this.views[view]);
// You might want to do some other stuff here
// such as call delegateEvents to keep listening
// to models or collections etc. The current view
// will be closed but it won't be garbage collected
// as it's attached to this layout.
}
});
The method of communication between the router and the layout doesn't have to be a direct call. You could trigger further application-wide events or do anything else you can think of. The router above is very basic but gets the job done. You could create a more intelligent router to use a single route with parameters to determine dynamically which itemView to show.
Every time the user does something that requires changing views, you can update the browser's history by using router.navigate('my/route/itemViewB', {trigger: true});. Also, if you set up your app to only render on history change events then you don't need to set up two mechanisms for rending each view.
I use this pattern in my own apps and it works very well.
#Simon's answer is headed in the correct direction. However, the only way to stop Marionette from closing views is to modify a bit of it's Region code.
var NoCloseRegion = Marionette.Region.extend({
open: function(view) {
// Preserve the currentView's events/elements
if (this.currentView) { this.currentView.$el.detach(); }
// Append the new view's el
this.$el.append(view.el);
}
});
The, when be sure to specify our new Region class when creating the Layout view
var Layout = Backbone.Marionette.Layout.extend({
regions: {
someRegion: {
selector: 'my-region-jquery-selector',
regionType: NoCloseRegion
},
},
initialize: function () {
this.createViews();
},
createViews: function () {
this.views = {
a: new Backbone.Marionette.ItemView,
b: new Backbone.Marionette.ItemView
};
},
showItemView: function (name) {
// Don't `show`, because that'll call `close` on the view
var view = this.views[name];
this.someRegion.open(view)
this.someRegion.attachView(view)
}
});
Now, instead of calling show which closes the old view, renders the new, and attaches it to the region (and triggers a few events), we can detach the old view, attach the new, and open it.
I'm working on a simple web app using Ember. I am rendering a nested resource into the application template rather than it's parent resource.
This works fine except if I press the back button I go back to the parent resource but the parent template is not rendered into the application outlet. I can refresh the page and bingo it renders then.
Router:
Movies.Router.map(function () {
this.resource('list', { path: '/list' }, function() {
this.route('add');
// Nested resource example
this.resource('movies', { path: '/:list_id/movies' }, function() {
});
});
this.route('boxoffice');
});
Movies Route:
Movies.MoviesRoute = Ember.Route.extend({
model: function(params) {
return Movies.List.find(params.list_id);
},
renderTemplate: function() {
this.render('movies', {
// template outlet to render into (will mess up your back btn!)
into: 'application'
});
}
});
Thanks in advance!
By default the ember Router uses the browser's hash to load routes of your application and will keep it in sync. This relies on a hashchange event existing in the browser.
But you can setup ember to use the browser history API instead of hash which is the default. This can be accomplished in different way's. For example like this:
App.Router = Ember.Router.extend({
location : Ember.Location.create({
implementation : 'history' // can be hash, history or none
})
});
Or by a more simpler approach by reopening the router like this:
App.Router.reopen({
location: 'history'
});
This way using the browser back & forward buttons would work as expected.
For more info on the history API see here.
Hope it helps
I'm trying to figure out following scenario:
Lets say that I have two views: one for viewing items and one for buying them. The catch is that buying view is a sub view for viewing.
For routing I have:
var MyRouter = Backbone.Router.extend({
routes: {
'item/:id': 'viewRoute',
'item/:id/buy': 'buyRoute'
}
});
var router = new MyRouter;
router.on("route:viewRoute", function() {
// initialize main view
App.mainview = new ViewItemView();
});
router.on("route:buyRoute", function() {
// initialize sub view
App.subview = new BuyItemView();
});
Now if user refreshes the page and buyRoute gets triggered but now there is no main view. What would be best solution to handle this?
I am supposed that the problem you are having right now is that you don't want to show some of the stuff inside ViewItem inside BuyView? If so then you should modularized what BuyView and ViewItem have in common into another View then initialize it on both of those routes.
Here is a code example from one of my apps
https://github.com/QuynhNguyen/Team-Collaboration/blob/master/app/scripts/routes/app-router.coffee
As you can see, I modularized out the sidebar since it can be shared among many views. I did that so that it can be reused and won't cause any conflicts.
You could just check for the existence of the main view and create/open it if it doesn't already exist.
I usually create (but don't open) the major views of my app on booting up the app, and then some kind of view manager for opening/closing. For small projects, I just attach my views to a views property of my app object, so that they are all in one place, accessible as views.mainView, views.anotherView, etc.
I also extend Backbone.View with two methods: open and close that not only appends/removes a view to/from the DOM but also sets an isOpen flag on the view.
With this, you can check to see if a needed view is already open, then open it if not, like so:
if (!app.views.mainView.isOpen) {
//
}
An optional addition would be to create a method on your app called clearViews that clears any open views, perhaps with the exception of names of views passed in as a parameter to clearViews. So if you have a navbar view that you don't want to clear out on some routes, you can just call app.clearViews('topNav') and all views except views.topNav will get closed.
check out this gist for the code for all of this: https://gist.github.com/4597606