Sailsjs: Multiple Layouts - node.js

I realize this is the same issue raised here: How to use multiple layout within a SailsJS app?. I'm not sure if something has changed with Sails.js or I'm just a numbskull. Hoping someone can help...
The application I'm developing has two sides to it, a public side, and an admin side. For the life of me, I cannot get a view to use a different layout than the default. I'm using the default "ejs" templating engine.
The location of the alternate layout resides here:
/views/layoutadmin.ejs
Here's my /config/routes.js file:
module.exports.routes = {
'/': {
view: 'home/index'
},
'/dashboard': {
view: 'admin/index'
,controller: 'dashboard'
,action: 'index'
}
};
Here's my /api/controllers/DashboardController.js file:
module.exports = {
index: function (req, res) {
res.view({ layout: 'layoutadmin' });
}
};

The problem here is that you're specifying both a view and a controller/action in your route config. You can only do either/or. You want:
module.exports.routes = {
'/': {
view: 'home/index'
},
'/dashboard': {
controller: 'dashboard'
,action: 'index'
}
};
And in DashboardController:
module.exports = {
index: function (req, res) {
res.view('admin/index', { layout: 'layoutadmin' });
}
};

You could create a policy and change the layout in it:
Check this out:
https://github.com/vimia/blew/blob/master/api/policies/isPjaxRequest.js
I change the layout with the param...
You can create a isAdminModule policy, at config/policies.js, put something like:
AdminController: [ '*': 'isAdminModule']
Then all the admin requests will have another layout...

Related

How to get domain in middleware, then pass it to components/store before axios calls [Nuxt]

I want to access the domain name before any axios calls are made. Because the axios calls need the domain URL to make them.
So, I want to get the domain first, then pass it to the store/components so they can load properly.
My understanding is that the domain is held in the req object of the context which is passed to the midddleware.
How would I get it and then pass it to the store and the components?
You can do it like this. Use nuxtServerInit. This function is called on the serverside, and only once if you reload the page. For this you need to go to your store and add this:
//store
actions: {
nuxtServerInit(store, context){
store.commit("setUrl", context.req.originalUrl);
}
}
Well thats all. People also like to destructure the arguments:
actions: {
nuxtServerInit({ commit},{req}){
commit("setUrl", req.originalUrl);
}
}
I am not sure if its originalUrl or just url... or maybe something different.
To expand on lfaruki's answer, you can do it that way. Here is my solution:
store/index.js
export const state = () => ({
domain: '',
});
export const mutations = {
setDomain(state, domain) {
state.domain = domain;
},
};
export const actions = {
nuxtServerInit(store, context) {
store.commit('setDomain', context.req.headers.host);
},
};
export const getters = {
domain: (state) => state.domain,
};
middleware/domain.js
export default function ({ route, store, redirect }) {
console.log('hey look at that', store.getters['domain']);
}
nuxt.config.js
export default {
...
router: {
middleware: 'domain'
},
}

Localized routes in koa

I'm developing a site with multiple languages. Some routes will therefore also have to be localized and I'm not sure how to do this properly.
I'm using #koa/router for routing.
For this example it's only English and Swedish but the site will handle more languages.
I can setup routes to match words in different languages like
router.get('/(create-account|skapa-konto)/', (ctx, next) => {
ctx.body = translate('signup_welcome');
await next();
});
But, I want the English site to only respond to '/sign-up' and send 404 for '/skapa-konto' (and vice versa).
In the real world the route would point to some controller function. So if I set up individual routes for each language I would have to change all localized routes manually should the controller function change in the future. That's something I would like to avoid ;)
Any suggestions?
I ended up solving this by extending the Router like this:
const LocalizedRouter = class extends Router {
/**
* Set up route mapping
* #param {object} options
*/
constructor(options) {
if (!Array.isArray(options.languages)) {
throw new TypeError('Languages must be of type Array');
}
super(options);
this.languages = options.languages;
}
/**
* Router function for GET method
* #param {string | Object<string, string>} RouteCollection
*/
get(routes, func) {
if (typeof(routes) === 'string') {
super.get(routes, func);
return;
}
if (typeof(routes) === 'object') {
for(const key in routes) {
if(!this.languages.includes(key)) {
continue;
}
if(typeof(func) !== 'function') {
throw new TypeError('Middleware must be a function');
}
const checkLanguageAndMount = async (ctx, next) => {
if(ctx.state.lang !== key) {
return next();
}
return func(ctx, next);
};
super.get(routes[key], checkLanguageAndMount);
}
return;
}
throw new TypeError('"Routes" must be a string or an object');
}
};
I can then set up my routes like this:
const myRouter = new LocalizedRouter({
languages: ['en', 'sv']
});
myRouter.get({
'en': '/create-account',
'sv': '/skapa-konto'
}, (ctx, next) => {
ctx.body = translate('signup_welcome');
await next();
};
This can probably be cleaned up but it does solve what I wanted to do.
EDIT: Fixed bug that caused 404 if two languages had identical paths
This problem interested me so I created a small github repo with some code. I'll try to explain here:
I created an array with some options:
const localeConfig = [
{
locale: "en",
routes: [
{
path: "/sign-up",
controllers: [enController],
method: "GET",
},
],
prefix: false,
},
{
locale: "se",
routes: [
{
path: "/skapa-konto",
controllers: [seController],
method: "GET",
},
],
prefix: false,
},
];
I then pass this object to a setupRoutes function that basically iterates the array, generating all the routes according to those options.
const setupRoutes = (localeConfig) => {
// Have some check to prevent duplicate routes
localeConfig.forEach((opt) => {
// Adding prefix according to option
const localePrefix = opt.prefix ? `/${opt.locale}` : "";
opt.routes.forEach((route) => {
const path = `${localePrefix}${route.path}`;
router[route.method.toLowerCase()].apply(router, [
path,
...route.controllers,
]);
});
});
};
So, for instance, if you were to change any of the controllers in either language you would only need to update the specific locale object.route.controllers. I imagine you could even have each different locale in a different file to have some modularity.
The github repo is here and I would really like to have you contribute to it if you have any idea on how to improve this.
Cheers!

Showing a dialog on all pages after rendering is done

I'm trying to add a popup that's supposed to be displayed when the user naviagates to a new page if certain conditions are met. The problem is that even if I add a post render step to the router configuration, on views that are composed by several custom elements they sometimes import resources after the post render step has executed which causes the popup to dissapear and pretty much lock the page. This is a small reproducable example:
router.configure(config => {
config.addPostRenderStep(CustomPostRenderStep);
return config;
});
...
#inject(DialogService)
class CustomPostRenderStep{
constructor(
private mDialogService: DialogService
) {
}
run(navigationInstruction: NavigationInstruction, next: Next) {
this.mDialogService.open({
viewModel: TestModal
lock: true
});
return next();
}
}
The popup will on some pages appear on the screen for a brief moment before it disappears but the body html tag will still have the class "ux-dialog-open".
Adding a timeout helps, but how long the timeout needs to be is different on different systems which makes it an unreliable solution. Like this:
#inject(DialogService)
class CustomPostRenderStep{
constructor(
private mDialogService: DialogService
) {
}
run(navigationInstruction: NavigationInstruction, next: Next) {
window.setTimeout(() => {
this.mDialogService.open({
viewModel: TestModal
lock: true
});
}, 200);
return next();
}
}
What is the proper way to wait for everything to be rendered before displaying the popup?
A better option would be to utilize events fired by router after navigation success
https://ilikekillnerds.com/2015/09/understanding-aurelia-router-events/
you can subscribe to router:navigation:success in your app and create your dialog aferwards
router:navigation:success
You should use TaskQueue in order to be called on the next rendering loop.
See: http://aurelia.io/docs/api/task-queue/class/TaskQueue/method/queueTask
This should work:
#inject(DialogService, TaskQueue)
class CustomPostRenderStep {
constructor(
private mDialogService: DialogService
private mTaskQueue: TaskQueue
) {
}
run(navigationInstruction: NavigationInstruction, next: Next) {
this.mTaskQueue.queueTask(() => {
this.mDialogService.open({
viewModel: TestModal,
lock: true
});
});
return next();
}
}
I do not really know the RenderPipeline very well but maybe you could call next() inside your queued task.
View my Gist fork that is working, maybe you should investigate what is closing your dialog by setting breakpoints in aurelia-dialog code.
Here's an example: Aurelia Router Demo | PostRenderStep modal dialog example
app.js
import { inject } from 'aurelia-framework';
import { DialogService } from 'aurelia-dialog';
import { TestModal} from 'test-modal';
export class App {
configureRouter(config, router) {
config.title = 'Aurelia';
config.map([
{route: ['', 'home'], name: 'home', moduleId: 'home/home', nav: true, title: 'Home'},
{route: 'profile', name: 'profile', moduleId: 'profile/profile', nav: true, title: 'Profile'},
{route: 'settings', name: 'settings', moduleId: 'settings/settings', nav: true, title: 'Settings'}
]);
config.addPostRenderStep(PostRenderStep);
this.router = router;
}
}
#inject(DialogService, TestModal)
class PostRenderStep {
constructor(dialogService, testModal){
this.dialogService = dialogService;
this.testModal = testModal;
}
run(routingContext, next) {
this.dialogService.open({
viewModel: TestModal,
lock: true
});
return next();
}
}
Plus, which versions are you using?

Refresh(F5) ngview with angularJS

I've got an issue with my Angular application. The application is running well the first time, it is possible to navigate inside, but it fails when I try to refresh the page with F5. When F5 key is pressed, the partial is called to the server, and the server obviously responds with the partial view, but not the whole application.
Node routing:
app.get('/', node_routes.welcome);
...
app.get('/profile', pass.ensureAuthenticated, profile_routes.profile);
app.get('/profile/:name', pass.ensureAuthenticated, profile_routes.partials);
app.post('/profile/update', pass.ensureAuthenticated, profile_routes.update);
...
Controller:
exports.profile = function(req, res) {
var user = req.user;
Profile.findOne({ username: user.username }, function(err, profile) {
if (err) {
res.redirect('/');
}
res.render("profile");
});
};
exports.partials = function(req, res) {
var name = req.params.name;
var user = req.user;
Profile.findOne({ username: user.username }, function(err, profile) {
if (err) {
res.redirect('/');
}
res.render(path.join(__dirname + '/../views/profile/' + name));
});
};
exports.update = function(req, res){
var profile = req.body;
delete profile._id;
Profile.update({'username':profile.username},profile,{safe:true}, function(err, result){
if(err) {
console.log('Error updating profile. ' + err);
res.redirect('/profile');
}
else{
console.log('' + result + ' profile updated for user: ' + profile.username);
res.redirect('/profile');
}
});
};
Angular application
myApp.config(['$routeProvider','$locationProvider', function($routeProvider,$locationProvider){
$routeProvider
.when('/profile/update', {
controller: 'myJobOfferListCtrl',
templateUrl: '/profile/update',
reloadOnSearch: false
})
.when('/profile', {
controller: 'myJobOfferListCtrl',
templateUrl: '/profile/dashboard'
})
.when('/profile/dashboard', {
controller: 'myJobOfferListCtrl',
templateUrl: '/profile/dashboard',
reloadOnSearch: false
})
.when('/profile/offers', {
controller: 'myJobOfferListCtrl',
templateUrl: '/profile/offers',
reloadOnSearch: false
})
.otherwise({redirectTo: '/profile'});
$locationProvider.html5Mode(true);
}]);
My profile page
extends layout
block content
div.container-fluid(ng-app="appProfile", ng-controller="myJobOfferListCtrl", ng-init="initProfile()")
div.row-fluid
div.span3(ng-controller="navBarCtrl")
div.well.sidebar-nav
ul.nav.nav-list
li.well.nav-header.
My Profile ({{data.profile.username}})
li(ng-class="{ active: isActive('dashboard')}")
a(href="/profile/dashboard") Dashboard
li(ng-class="{ active: isActive('update')}")
a(href="/profile/update") Update profile
li.divider
li.well.nav-header My Jobs
li(ng-class="{ active: isActive('offers')}")
a(href="/profile/offers") Offers
li(ng-class="{ active: isActive('application')}")
a(href="/profile/application") Applications
div.span9(ng-view, ng-cloak)
And a sample partial view page
div.row-fluid
div(ng-init="initMyOffers()")
accordion.span8.offset1(close-others="true")
accordion-group(ng-repeat="item in data.myJobs", heading="{{item.title}}")
accordion-heading
{{item.title}}
i.pull-right.icon-remove-circle.removeButton(ng:click="remove(item)")
p {{item.description}}
hr
div.footer
div.pull-left
i.icon-calendar.calendar
{{item.dueDate | date:'d MMMM yyyy'}}
div.pull-right
i.icon-user
{{item.author}}
How can I reload a partial page when refreshing with F5 ?? What I expected with angular is that when trying to refresh for example the page /profile/dashboard, the partial /views/profile/dashboard.jade is called but also the views/profile.jade. And what about the $scope ?
Sorry, but I'm a little confioused... thanks for you help !
I think it is a common problem and the user is not supposed to use F5 in your application. I do not think, it is possible to stop default browser action after F5 has been pressed.
One simple solution is to add this or similar script to every view:
<script>
if (angular == undefined)
// alert("It is SPA (Single Page Application) -- do not press F5 anymore, please.");
window.location.replace("your/main/url");
</script>
A bit more complicated would be realization of the following scenario.
user hits f5
server sends partial view for current url current_url
client codes detects lack of angular and send request to be redirected to current_url
server sends modified version of the index.html file [with e.g. some hidden input field, that stores current_url]
after angular has been loaded, the application checks the hidden field and change the location accordingly
The simplest and most elegant way to avoid losing stuff when refreshing an Angular app is to store whatever values you need in a cookie by using $cookies service. Here's an example. Hope this helps.
"use strict";
angular.module('myApp').factory('SessionSrv', [ '$cookies', function($cookies){
var _myString = $cookies.get('myString'); // e.g. "foo"
var _myObject = $cookies.getObject('myObject'); // e.g. {foo: "bar"}
return {
setMyString: function(myString){
_myString = myString;
$cookies.put('myString', _myString);
},
getMyString: function(){
return _myString;
},
setMyObject: function(myObject){
_myObject = myObject;
$cookies.putObject('myObject', _myObject);
},
getMyObject: function(){
return _myObject;
},
};
}]);
Finally implemented the solution as proposed by artur :
<script>
if (typeof angular == 'undefined')
window.location.replace("/main_url");
</script>

Backbone views not rendering in order

In my app I have layouts and views for those layouts. Layouts only change on login/logout, but the other special case is on pageload I need to load the proper layout. However, in my defaultAction my layout does not actually render after it returns and so when the view tries to render, the el it is supposed to be contained in does not exist.
// Filename: router.js
var app_router;
define( [ 'views/layouts/beta', 'views/beta/requestInvite', 'views/beta/login', 'views/app/dashboard' ],
function(betaLayout, requestInviteView, loginView, dashboardView) {
var AppRouter = Backbone.Router.extend( {
routes : {
// Pages
'login' : 'login',
'dashboard' : 'dashboard',
// Default
'*actions' : 'defaultAction'
},
// Pages
login : function() {
loginView.render();
},
dashboard : function() {
dashboardView.render();
},
// Default
defaultAction : function(actions) {
betaLayout.render();
requestInviteView.render();
}
});
var initialize = function() {
app_router = new AppRouter;
$('a').live('click', function() {
var href = $(this).attr('href');
// only navigate to real links
if(href == undefined)
return;
app_router.navigate(href, {trigger: true});
return false;
});
Backbone.history.start({pushState: true});
};
return {
initialize : initialize
};
});
How can I have my layout render completely before my view?
Define a callback for betaLayout.render() to take as an argument, that gets executed when the render is actually complete.
E.g., betaLayout.render() would look something like:
render: function(callback) {
/* code to render your element,
using the following line when all
asynchronous calls are complete */
if (callback) callback();
}
and your defaultAction would use it like so, passing the second render() as its callback.
betaLayout.render(requestInviteView.render);
The problem was that because my layout didn't render before my view was initialized, the el was empty. What I did was convert all of my objects to return the object instead of the instance, and let them render once they were initialized. This way, when I declare a new MyLayout and then a new MyView, I can be guaranteed that MyView's el is valid.

Resources