My Durandal application's startup logic in shell.js requires sending the user to one of two possible views depending on some conditional logic. Basically, if certain options have not previously been selected I'll send them to a Setup page, otherwise I'll send them to the normal start page.
Previously, in Durandal 1.x I would just pass a string indicating the starting module when calling activate. But in Durandal 2.x that has been deprecated.
So first, I'm wondering what is the recommended way to do this from the standpoint of the routes array? Should I just register both routes as if neither is the start module (like below) then conditionally add a another route to the array with the default route of ''?
{ route: "setup", moduleId: "setup", title: "Setup", nav: false },
{ route: "Students", moduleId: "students", title: "Students", nav: false }
The second part of my question involves how to handle the need to make a call to a web service as part of my conditional logic for determining which module is the start module. My startup logic involves checking the local storage of the browser, but sometimes I'll also need to make an ajax request to the server to get a bit of information.
My understanding is that router.activate() is a promise. Can I actually just create my own promise and resolve it by calling router.activate() after the ajax call has completed? Or is there another way I'll need to handle that? Sample code for how I was thinking I might handle this:
var deferred = $.Deferred();
//Setup and conditional logic here
var routes = setupRoutes();
$.get('/api/myStartupEndpoint')
.done(function(results) {
//check results and maybe alter routes
deferred.resolve(router.map(routes).activate());
});
return deferred.promise();
Does that make sense? I'm still converting my app over to Durandal 2.0.1 so I haven't been able to try this yet, but regardless of whether it does or not I want to find out what the recommended approach would be in this scenario.
The way I'd do it is this - actually I think it's similar to how you're already thinking, so I hope it makes sense:
In your main.js, set your application root to the same module, no matter the circumstances. Since one of the main features of D2 is child routers, I'd suggest using the module name "root" for your application root, as it makes it easier to distinguish from "shell" modules (which I use for setting up child routers):
app.start().then(function () {
app.setRoot("root", "entrance");
});
In your root module, setup the routes as you have described:
{ route: "setup", moduleId: "setup", title: "Setup", nav: false },
{ route: "students", moduleId: "students", title: "Students", nav: false }
When your root module activates, check if the user has been setup or not. Use the result of that check to work out if you want the user to be sent to the setup page, or the students page. Note however that you must activate your router before redirecting; you can use $.when to help you here:
var checkUserSetup = function () {
var deferred = $.Deferred();
// check local storage, fire off ajax request, etc.
// resolve the deferred once you know if the user is setup or not
deferred.resolve(false);
return deferred.promise();
};
root.activate = function() {
mapRoutes();
var deferred = $.Deferred();
$.when(checkUserSetup(), router.activate()).done(function(isUserSetup) {
if (isUserSetup) {
router.navigate("students");
} else {
router.navigate("setup");
}
deferred.resolve();
});
return deferred.promise();
}
Hopefully this also answers the second part of your question; but just in case - yes, you can return a promise from the activate method, and durandal will "block" until you've resolved that promise. However, note that the router's activate method also returns a promise - so your code won't quite work. You're resolving your deferred with another deferred; you'd need to do something more like this:
root.activate = function() {
var deferred = $.Deferred();
//Setup and conditional logic here
var routes = setupRoutes();
$.get('/api/myStartupEndpoint')
.done(function(results) {
//check results and maybe alter routes
//don't resolve the deferred until the router has completed activation
router.map(routes).activate().done(deferred.resolve);
});
return deferred.promise();
}
Hope that helps.
Related
I am having trouble getting dependency injection working for my AuthorizerService. Obviously, dep-inj is not ready until after Aurelia "starts", but I wasn't sure how to access it.
main.js:
aurelia.container.registerInstance(HttpClient, http.c());
// set your interceptors to take cookie data and put into header
return aurelia.start().then(() => {
let Authorizer = new AuthorizerService();
aurelia.container.registerInstance(AuthorizerService, Authorization);
console.log('Current State: %o', Authorizer.auth);
Authorizer.checkCookieAndPingServer().then(() => { console.log('Current State: %o', Authorizer.auth); aurelia.setRoot(PLATFORM.moduleName('app')); }, () => { aurelia.setRoot(PLATFORM.moduleName('login-redirect')); });
});
Now the problem is that if I do "new AuthorizerService()" then "this.http.fetch()" is not available in AuthorizerService.js.
Am I meant to pass "http.c()" (which delivers the HttpClient instance) as a parameter inside:
checkCookieAndPingServer(http.c())
or is there another way?
Can I delete "new AuthorizerService()" and just do (I made this up):
aurelia.container.getInstance(AuthorizerService);
Somehow FORCE it to do dependency-injection and retrieve the "registered Instance" of "http.c()"?
I can't just check cookie. I have to ping server for security and the server will set the cookie.
I think this is all sorts of wrong, because I need a global parameter that just is false by default, then it does the query to backend server and setsRoot accordingly. Perhaps only query backend in the "login page"? Okay but then I would need to do "setRoot(backtoApp); aurelia.AlsoSetLoggedIn(true);" inside login module. But when I setRoot(backtoApp) then it just starts all over again.
In other words, when setRoot(login); then setRoot(backToApp); <-- then AuthorizerService instance doesn't have its proper data set (such as loggedIn=true).
EDIT: Better Solution maybe:
main.js:
return aurelia.start().then(() => {
let Authorizer = aurelia.container.get(AuthorizerService);
let root = Authorizer.isAuthenticated() ? PLATFORM.moduleName('app') : PLATFORM.moduleName('login');
console.log('Current State: %o', Authorizer.auth);
aurelia.setRoot(root);
});
Authorizer.js
constructor(http) {
this.http = http;
this.auth = {
isAuthenticated: false,
user: {}
}
}
"this.auth" is no longer static. No longer "static auth = { isAuthenticated: false }" which was some example code I had found.
So now "auth" gets set inside "login" module. But this means the "login" module is displayed every single time the app loads briefly, before being redirected back to "setRoot(backToApp)"
If the class you want to get the instance is purely based on service classes and has no dependencies on some Aurelia plugins, it doesn't need to wait until Aurelia has started to safely invoke the container.
For your example:
aurelia.container.getInstance(AuthorizerService);
It can be
aurelia.container.get(AuthorizerService);
And you should not use new AuthorizerService(), as you have noticed in your question.
I've created a HapiJS project, using my own MVC pattern.
When I want to log from inside my controllers in some cases. Currently when I want to log from my controllers I simply invoke request.log. I'm using Good as a logging plugin.
For example:
const user = function(req, res){
// do stuff
req.log(['info'], 'some log info here');
};
module.exports = {
user,
};
How can I log from inside my models where I have no request object? I don't want to have to pass in my request object into the methods of the model.
If you plan to register models as plug in, you will have access to the server object and so, you will be able to use server.methods
EDIT
In my company we declare routes as plug in (see code below)
exports.register = function (server, options, next) {
server.route({
method: 'POST',
path: '/FOO/BAR'
handler(request, reply) {}
});
return next();
};
exports.register.attributes = {
name: 'routes-foobar'
};
And we register as such :
server.register([
require('./route-foo-bar'),
...,
]);
This way we have the server objects in our route
What I would do in your case is register my models as server methods and use them in my routes.
The same goes for logging.
I would register my log function as a server method and call them from inside my models
I don't know if it's the good way to do that but that's a least a working one
I am trying to generate some routes dynamically in a child router basing on a parameter provided within the url.
So I have my main router configured with that route :
mydomain.com/#/page/:page that is loading a module "page"
And in my module page I have a configureRouter function which is supposed to fetch the sections relative to the page specified in the url before to add them in the child router:
public async configureRouter(config, router){
page = ???
sections = wait fetchPageSections(page)
//for each section, add route in child router
}
My problem here is how to retrieve the :page parameter, since this will be available only (if I understand well) in the activate() function, that will be called after configureRouter(). However, since that part of the route has already been "matched" in the parent router, I think there should be a way to retrieve it in the configureRouter function.
Thanks.
I don't know if there is a way to retrieve the :page parameter in the configureRouter() method, because as far as I know, at this point the new route has not been triggered yet.
You can retrieve the :page parameter in the created() method.
created() {
let page = this.router.parentInstruction.params.page;
// do something and add routes
//this.router.addRoute({ route: "test", moduleId: "test", nav: true, title: "test" });
//this.router.refreshNavigation();
}
A second option would be using a global object to hold the desired parameter. You could inject this object in the constructor() method of the view component. However, this would be an overkill and unsafe to use. I do not think it is a good idea.
A third option, and the easiest one in my opinion, is using mapUnknownRoutes() for dynamic routes:
router.configure(config => {
config.mapUnknownRoutes(instruction => {
//read the instruction.fragment to get
//you can return a promise and make async requests
return instruction;
});
});
I hope this helps!
The router you are passed in configureRouter(config, router) is the child router. Luckily, it has a parent property that will provide you with what you want.
router.parent.currentInstruction.params.page should give you what you're looking for: the page parameter from the parent router.
I would love to a #signin route that would open a dialog on top of whatever page there was before.
Let's consider this example app this the following routes:
router.map([
{route: '', moduleId: 'vm/home', title: "Home"},
{route: 'about', moduleId: 'vm/about', title: "About"},
{route: 'signin', moduleId: 'vm/signin', title: 'Sign In'}
]);
Here are example use cases:
User is on # and navigates to #signin: we should see a Sign In dialog on top of Home page
User is on #about and navigates to #signin: we should see a Sign In dialog on top of About page
User navigates to http://localhost:9000/#signin: we should see a Sign In dialog on top of Home page
User is on #signin and closes dialog: we should see a page that was behind the dialog (there's always a page behind).
The dialog and router are both plugins and have no interactions between eachother.
Also having the router display dialog would ignore how the router works - it has a div which it dumps content into. Dialogs exist outside of all of this.
However if you wanted to (I may do this aswell), you could try this.
Add dialog: true to the route map.
Override router.loadUrl method. Check if the route is a dialog route as we marked before, and activate the dialog instead.
I would make the dialog a child route, so then you can know which view to display beneath the dialog. Otherwise you could just have to show the dialog over anything and ignore routing entirely.
Edit: I don't think this would entirely work actually. loadUrl returns a boolean. You could open the dialog and return false to cancel navigation.
Edit2:
My Attempt
The loadUrl method loops through all routes, and each has a callback, so ideally we need to insert our logic into this array.
for (var i = 0; i < handlers.length; i++) {
var current = handlers[i];
if (current.routePattern.test(coreFragment)) {
current.callback(coreFragment, queryString);
return true;
}
}
This array is added to using the routers route method. Durandal calls this method when you map routes, so ideally we could add some extra parameters to the route config and let Durandal handle these. However the configureRoute function is internal to the routing module, so we will need to edit that and make sure we copy changes over when updating Durandal in the future.
I created a new list of dialog routes:
{ route: 'taxcode/add(/:params)', moduleId: 'admin/taxcode/add', title: 'Add Tax Code', hash: '#taxcode/add', nav: false, dialog: true, owner: '#taxcodes' },
{ route: 'taxcode/edit/:id', moduleId: 'admin/taxcode/edit', title: 'Edit Tax Code', hash: '#taxcode/edit', nav: false, dialog: true, owner: '#taxcodes' }
The idea of an owner, is that if there is a case where the initial route is this, we need something behind the dialog.
Now replaced the router.route call in configureRoute with this:
router.route(config.routePattern, function (fragment, queryString) {
if (config.dialog) {
if (!router.activeInstruction()) {
// No current instruction, so load one to sit in the background (and go back to)
var loadBackDrop = function (hash) {
var backDropConfig = ko.utils.arrayFirst(router.routes, function (r) {
return r.hash == hash;
});
if (!backDropConfig) {
return false;
}
history.navigate(backDropConfig.hash, { trigger: false, replace: true });
history.navigate(fragment, { trigger: false, replace: false });
queueInstruction({
fragment: backDropConfig.hash,
queryString: "",
config: backDropConfig,
params: [],
queryParams: {}
});
return true;
};
if (typeof config.owner == 'string') {
if (!loadBackDrop(config.owner)) {
delete config.owner;
}
}
if (typeof config.owner != 'string') {
if (!loadBackDrop("")) {
router.navigate("");
return; // failed
}
}
}
var navigatingAway = false;
var subscription = router.activeInstruction.subscribe(function (newValue) {
subscription.dispose();
navigatingAway = true;
system.acquire(config.moduleId).then(function (dialogInstance) {
dialog.close(dialogInstance);
});
})
// Have a route. Go back to it after dialog
var paramInfo = createParams(config.routePattern, fragment, queryString);
paramInfo.params.unshift(config.moduleId);
dialog.show.apply(dialog, paramInfo.params)
.always(function () {
if (!navigatingAway) {
router.navigateBack();
}
});
} else {
var paramInfo = createParams(config.routePattern, fragment, queryString);
queueInstruction({
fragment: fragment,
queryString: queryString,
config: config,
params: paramInfo.params,
queryParams: paramInfo.queryParams
});
}
});
Make sure you import dialog into the module.
Well maybe all of that is not needed when using a trick with the activation data of your home viewmodel.
Take a look at my Github repo I created as an answer.
The idea is that the route accepts an optional activation data, which the activate method of your Home VM may check and accordingly show the desired modal.
The benefit this way is that you don't need to touch the existing Durandal plugins or core code at all.
I'm though not sure if this fully complies with your request since the requirements didn't specify anything detailed.
UPDATE:
Ok I've updated the repo now to work with the additional requirement of generalization. Essentially now we leverage the Pub/Sub mechanism of Durandal inside the shell, or place it wherever else you want. In there we listen for the router nav-complete event. When this happens inspect the instruction set and search for a given keyword. If so then fire the modal. By using the navigation-complete event we ensure additionally that the main VM is properly and fully loaded.
For those hacks where you want to navigate to #signin, just reroute them manually to wherever you want.
Expanding on my suggestion in the comments, maybe something like this would work. Simply configure an event hook on router:route:activating or one of the other similar events and intercept the activation of /#signin. Then use this hook as a way to display the dialog. Note that this example is for illustrative purposes. I am unable to provide a working example while I'm at work. :/ I can complete it when I get home, but at least this gives you an idea.
router.on('router:route:activating').then(function (instance, instruction) {
// TODO: Inspect the instruction for the sign in route, then show the sign in
// dialog and cancel route navigation.
});
I have configured some basic routes that are available for all users before they log in:
App.config(function ($routeProvider) {
$routeProvider.
when('/login', { templateUrl: 'views/login.html', controller: PageStartCtrl.Controller }).
otherwise({ redirectTo: '/login' });
});
So the only thing user can do is to log in. After the user logs in, I would like to register additional routes like this:
$http
.post('api/Users/Login', { User: userName, Password: userPassword })
.success(function (response : any) {
App.config(function ($routeProvider) {
$routeProvider
.when('/dashboard',
{ templateUrl: 'part/dashboard.html',
controller: DashboardCtrl.Controller });
});
However, I suppose I should call .config method only once, because the $routeProvider is brand new instance that knows nothing about /login route. Further debugging showed me that the first instance of $resourceProvider is used when resolving view change.
Q: Is there a way how to register routes later?
Solution from Add routes and templates dynamically to $routeProvider might work, but is quite ugly (involved global variable nastyGlobalReferenceToRouteProvider).
Since routes are defined on a provider level, normally new routes can only be defined in the configuration block. The trouble is that in the configuration block all the vital services are still undefined (most notably $http). So, on the surface it looks like w can't define routes dynamically.
Now, it turns out that in practice it is quite easy to add / remove routes at any point of the application life-cycle! Looking at the $route source code we can see that all the routes definition are simply kept in the $route.routes hash which can be modified at any point in time like so (simplified example):
myApp.controller('MyCtrl', function($scope, $route) {
$scope.defineRoute = function() {
$route.routes['/dynamic'] = {templateUrl: 'dynamic.tpl.html'};
};
});
Here is the jsFiddle that demonstrates this in action: http://jsfiddle.net/4zwdf/6/
In reality, if we want to be close to what AngularJS is doing the route definition logic should be a bit more complex as AngularJS is also defining a redirect route to correctly handle routes with / at the end (make it effectively optional).
So, while the above technique will work, we need to note the following:
This technique depends on the internal implementation and might break if the AngularJS team decides to change the way routes are defined / matched.
It is also possible to define the otherwise route using the $route.routes as the default route is stored in the same hash under the null key
I found that the answer by #pkozlowski.opensource works only in angularjs 1.0.1. However, after angular-route.js becomes an independent file in the later version, directly set the $route doesn't work.
After reviewing the code, I find the key of $route.routes is no longer used to match location but $route.route[key].RegExp is used instead. After I copy the origin when and pathRegExp function, route works. See jsfiddle:
http://jsfiddle.net/5FUQa/1/
function addRoute(path, route) {
//slightly modified 'when' function in angular-route.js
}
addRoute('/dynamic', {
templateUrl: 'dynamic.tpl.html'
});