Keep query strings when navigating between child routers - javascript

Here's my child router and the view code:
router.createChildRouter().makeRelative({
moduleId: 'products',
fromParent: true,
dynamicHash: ':id'
}).map([
{ route: 'specs', title: 'Specs', moduleId: 'specs', nav: true },
{ route: 'reviews', title: 'Reviews', moduleId: 'reviews', nav: true }
]).buildNavigationModel();
<ul class="tabs" data-bind="foreach: router.navigationModel()">
<li data-bind="css: { selected: isActive }">{{title}}</li>
</ul>
which will produce two tabs with the following URLs:
example.com/#products/200/specs
example.com/#products/200/reviews
I have a toolbar on my reviews page that sorts the content by date or by rating, so the URL becomes:
example.com/#products/200/reviews?date=asc&rating=desc
But when I switch tabs and come back, I lose the query strings. Is there way to keep them in the URL until I clear the filters?

Without knowing your exact implementation is there any reason to not just set this inside of a modules whose responsibility is to manage state?
products/(id)/specs view model / w/e -
define (['config.queryparams'], function (queryParams) {
var ctor = function () {
var self = this;
// Assume the queryParms module has an observable called parameters
self.queryparameters = queryParams.parameters;
}
ctor.prototype.active(id, querystring) {
var self = this;
// Set the queryParams.parameters observable to the query string
self.queryparameters(querystring);
}
return ctor;
});
Then in your config.queryparams module -
define([], function () {
var parameters = ko.observable();
var module = {
parameters: parameters
}
});
Now just reference config.queryparams from your other module and it is shared. Make sense?

Related

Authenticated and public routes in Backbone Marionette

We have a large Marionette app, with sub apps/modules.
Each of these registers its own router within the App.addInitializer.
What is the best way to flag certain routes as public and others as requiring authentication?
I have a way in the app to check if the user is authenticated or not, but I'm trying to avoid having to implement that check in every route handler.
PrivateModuleRouter.Router = Marionette.AppRouter.extend({
appRoutes: {
"privateRoute(/)" : "handlePrivateRoute",
}
});
var API = {
handlePrivateRoute: function() {
//I don't want to repeat this everywhere..
if(!Auth.isAuthenticated()) {
App.navigate('/login', {trigger:true});
} else {
PrivateRouteController.showForm();
}
};
App.addInitializer(function(){
new PrivateModuleRouter.Router({
controller: API
});
});
Is there way in the route definition to flag it as private, and then a top level route handler performs this check?
If it's on a Router event though, this may not trigger if the route handler was triggered directly (not passing trigger:true, and calling API.handlePrivateRoute() directly.
Disclaimer: as I don't personally use Marionette, this answer is based on Backbone only.
The execute function
Backbone provides the execute function in the router as a way to handle that kind of logic. Even the example has authentication logic in it:
var Router = Backbone.Router.extend({
execute: function(callback, args, name) {
if (!loggedIn) {
goToLogin();
return false;
}
args.push(parseQueryString(args.pop()));
if (callback) callback.apply(this, args);
}
});
The authentication router
One way to avoid repeating the execute in each router would be to make a base router for your app.
var BaseRouter = Backbone.Router.extend({
constructor: function(prefix, opt) {
// get the hash
this.auth = _.result(this, "auth", {});
BaseRouter.__super__.constructor.apply(this, arguments);
},
// requires auth by default?
authDefault: false,
/**
* Check the `auth` hash for a callback. Returns `authDefault` if
* the callback is not specified.
* #param {String} callbackName name of the function.
* #return {Boolean} true if the callback is private.
*/
hasAuth: function(callbackName) {
return _.result(this.auth, callbackName, this.authDefault);
},
// To easily override the auth logic in a specific router
checkAuth: function(){
return Auth.isAuthenticated();
},
execute: function(callback, args, name) {
if (this.hasAuth(name) && !this.checkAuth()) {
this.navigate('/login', { trigger: true });
return false;
}
}
});
Defining the specific routers
Then for each of your router, extend BaseRouter.
var SpecificRouter = BaseRouter.extend({
routes: {
'*otherwise': 'home', // notice the catch all
'public': 'publicRoute',
'private': 'privateRoute',
'unspecified': 'defaultAccessRoute'
},
/**
* The auth hash works like this:
* "functionName": [boolean, true if needs auth]
*
* home and publicRoute could be left out as it's the default here.
*/
auth: {
home: false, // public
publicRoute: false, // public
privateRoute: true, // needs authentication
// defaultAccessRoute will be public because BaseRouter
// defines `authDefault: false`.
},
home: function() {},
publicRoute: function() {},
privateRoute: function() {},
defaultAccessRoute: function() {},
});
And for a router which all routes are private by default:
var PrivateRouter = BaseRouter.extend({
authDefault: true,
routes: {
'*otherwise': 'home', // private
'only-private': 'onlyPrivate', // private
},
// ...snip...
/**
* Optional example on how to override the default auth behavior.
*/
checkAuth: function() {
var defaultAuthResult = PrivateRouter.__super__.checkAuth.call(this);
return this.specificProperty && defaultAuthResult;
}
});
In github you can find many solution for calling some methods before router's execution. For marionette you can use ideas from marionette-lite extension based in filters system.
You should define filter, for example RequresAuthFilter as:
import { Filter } from 'marionette-lite';
const RequresAuthFilter = Filter.extend({
name: 'requresAuth', // name is used in controller for detect filter
async: true, // async mode
execution: Filter.Before,
handler(fragment, args, next) {
// Requesting server to check if user is authorised
$.ajax({
url: '/auth',
success: () => {
this.isSignedIn = true;
next();
},
error: () => {
Backbone.navigate('login', true);
}
});
},
});
or short sync way:
import { Filter } from 'marionette-lite';
const RequresAuthFilter = Filter.extend({
name: 'requresAuth',
handler(fragment, args) {
if (!window.isSignedIn) {
Backbone.navigate('login', true);
}
},
});
And add this filter to Controller as:
const AppController = Marionette.Object.extend({
// Add available filters map
filtersMap: [
new RequresAuthFilter()
],
filters: {
// e.g. Action that need authentication and if user isn't
// authenticated gets redirect to login page
requresAuth: ['logout', 'private'],
},
logout() { /* ... */ },
private() { /* ... */ }
});

How do I set/read a query string when using the router in aurelia?

When using the aurelia.io framework router, what is the preferred method for reading and setting a query string?
For example, in the url: http://www.myapp.com/#/myroute1/?s=mystate
How do I read and set the ?s=mystate part of the url and have the aurelia router navigate correctly and remember that state, such that whenever I arrive in my route1 viewmodel I can read that state variable and do something with it?
On viewmodel you can implement method activate(params, routeConfig) and object params should contain your query variables
activate(params, routeConfig){
console.log(params.s); //should print mystate
}
To navigate to some route, you have to specify the name of this route in app.js (name: 'redirect')
config.map([
{ route: ['','welcome'], moduleId: './welcome', nav: true, title:'Welcome' },
{ route: 'flickr', moduleId: './flickr', nav: true, title:'Flickr' },
{ route: 'redirect', moduleId: './redirect', name: 'redirect'},
{ route: 'child-router', moduleId: './child-router', nav: true, title:'Child Router' }
]);
and then use method NavigateToRoute with parameters.
router.navigateToRoute('redirect', { s:'mystate'}, {replace: true});
If you want to change queryParams and reload page with it (e.g. change page number on pagination link click) -- you must use determineActivationStrategy with replace.
Create view model and add determineActivationStrategy function:
import {activationStrategy} from 'aurelia-router';
export class ModelView{
determineActivationStrategy() {
return activationStrategy.replace;
}
}
Add activate event to your model for saving params:
activate(params, routeConfig, navigationInstruction){
this.queryParams = params ? params : {};
}
Add code for reload or navigate with same or new parameters:
moveToNewPage(newPageNumber){
this.queryParams.page = newPageNumber; // change page param to new value
this.router.navigateToRoute(this.router.currentInstruction.config.name, this.queryParams);
}
P.S. for this.router access use inject and don't forget to set name of router in app configuration:
import {Router} from 'aurelia-router';
export class ModelView{
static inject() { return [Router] };
constructor(router){
this.router = router;
}
}
As per the latest configuration, the query string will not be retrieved if you didn't set the push state.
Set push state to true. and add base tag in index.html.
<base href="http://localhost:9000/"> - index.html
config.options.pushState = true; - app.js -> configureRouter method.

How to get the type of route path in Ember

If I have in the router map:
this.resource('detail', { path: '/detail/:type' }, function() {
...
});
And I retrive the currentPanth in my Ember Application code:
currentPath: '',
ApplicationController : Ember.Controller.extend({
updateCurrentPath: function() {
App.set('currentPath', this.get('currentPath'));
console.log('currentPath',App.currentPath);
}.observes('currentPath')
}),
When I navigate in my app, I get the route names by console, but when It is "detail" I get "detail.index". How can I get the type?
you only have access to the params in the route, ie. when you are defining your model:
App.Router.map(function() {
this.resource('photo', { path: '/photos/:photo_id' });
});
App.PhotoRoute = Ember.Route.extend({
model: function(params) {
return Ember.$.getJSON('/photos/'+params.photo_id);
}
});
Or you can also use paramsFor, also in the route only.
Depending on what you are trying to acomplish maybe query params suit better

Meteor JS: Conditional subscriptions, based on passed arguments, in Iron Router?

This is really two questions:
Is it possible to conditionally subscribe to collections within Iron Router's waitOn option?
Is it possible to pass in objects as an argument in Router.go()?
I am trying to reduce the delay in rendering a view, when creating a new post within my app. I tried passing in an isNew property as an argument for Router.go(), but had no luck:
// Router call after creating a new post
Router.go('postPage', {
_id: id,
isNew: true,
post: newPostObject
});
// router.js
Router.map(function() {
this.route('postsList', {
path: '/'
});
this.route('postPage', {
path: '/:_id',
waitOn: function() {
//This returns only _id for some reason.
console.log(this.params);
if (this.params.isNew != true) {
return [
Meteor.subscribe('singlePost', this.params._id),
Meteor.subscribe('images', this.params._id),
]
}
},
data: function() {
if (this.params.isNew == true) {
return this.params.post
else {
return Posts.findOne(this.params._id);
}
}
});
});
After some digging, it looks likes Iron Router supports an options hash as a third argument in the Router.go() method:
Router.go( 'postPage', {_id: id}, {isNew: true} );
To access it in the route, you can use then use this.options. To get the value of isNew in the above example, use this.options.isNew.
you can only access Dynamic Path Segments by this.params. so to get this.params.isNew working you need to have it like this.
this.route('postPage', {
path: '/:_id/:isNew',
waitOn: function() {}
...
});

Backbone model - extending api calls

I'm working on my first nodejs/backbone project. What I need to do is to extend user model api with additional methods. As REST uses universal post/get/put request, how do you extended backbone model with other api calls (ie. block user account where I don't want to update user and make /user/deactivate url)?
I can take ugly routes, but I looking for the "right way" from pros.
My backbone model
define(["jquery", "backbone"],
function($, Backbone) {
var User = Backbone.Model.extend({
urlRoot: '/user',
defaults: {
username: '',
password: '',
email: ''
}
});
return User;
}
);
My nodejs "router"
app.put('/user', userController.register);
app.post('/user', userController.update);
app.get('/user', userController.list);
app.delete('/user', userController.delete);
Why not add block and deactivate attributes to your model, then just use standard REST / CRUD API calls.
Then your actions block and deactivate would just be standard model updates, with logic to handle blocked and active model states in your API methods.
define(["jquery", "backbone"],
function($, Backbone) {
var User = Backbone.Model.extend({
urlRoot: '/user',
defaults: {
username: '',
password: '',
email: '',
blocked: false,
active: true
},
block: function () {
this.set('blocked', true);
this.save();
},
deactivate: function () {
this.set('active', false);
this.save();
},
});
return User;
}
);
EDIT - Based on your comment, the need to distinguish between updates
If you need to distinguish between field updates on the server, in order to run route specific validation for example. Then you're probably going to need to call custom routes for each specific action. A neat way of doing this would be to override the url during the call to save the model.
An updated model example.
define(["jquery", "backbone"],
function($, Backbone) {
var User = Backbone.Model.extend({
urlRoot: '/user',
defaults: {
username: '',
password: '',
email: '',
blocked: false,
active: true
},
block: function () {
this.set('blocked', true);
this.save(this, { url: '/user/block' });
},
deactivate: function () {
this.set('active', false);
this.save(this, { url: '/user/deactivate' });
},
});
return User;
}
);
Then you would have the following routes
app.put('/user', userController.register);
app.post('/user', userController.update);
app.get('/user', userController.list);
app.delete('/user', userController.delete);
app.post('/user/block', userController.block);
app.post('/user/deactivate', userController.deactivate);

Categories

Resources