Laravel 5.1 and Backbone.js Application - api

I'm new in Backbone and I'm trying to build an app on top of an existing Laravel API. I understand the concept of every Backbone component (Model, Collection, View, Router), but I'm having a bit of trouble defining an architecture for my app. When a user logs in, it gets redirected to a dashboard, with a few options in a navbar.
var LeadsView = Backbone.View.extend({});
var SourcesView = Backbone.View.extend({});
var TeamsView = Backbone.View.extend({});
var AppRouter = Backbone.Router.extend({
routes: {
// navbar option Leads
"leads": "_leadsView",
// navbar option Sources
"sources": "_sourcesView",
// navbar option Teams
"teams": "_teamsView"
},
_leadsView: function() {
var leads = new LeadsView();
},
_sourcesView: function(query, page) {
var sources = new SourcesView();
},
_teamsView: function(query, page) {
var teams = new SourcesView();
},
});
I get that a Backbone View acts like a Controller in Laravel, so with each of these defined routes, there's a pointer to a View ( LeadsView, SourcesView, TeamsView). When any one of the views gets instantiated, that view will take care of the model/collection, UI binding, and every other logic for the app interaction. My question is ... Is This approach good, conventional ? If not, how can one use routes to bind each navigation option to an action in Backbone ?

Related

How to get SPA navigation working with external framework that uses innerHTML for content

In my Vue.js app, I am using a bootstrap-based framework that generates the html for my header and a menu nav with links, which is then inserted into the page by assigning innerHTML to a mount point.
But when I use the generated content to navigate, the entire page reloads since the links aren't using <router-link>.
One attempt at a fix:
In the Vue app, I assigned a method called goto on the window object that would perform programmatic router navigation.
I was then able to pass javascript:window.goto("myPageName"); as the href attribute, but this comes with many undesirable side-effects.
How can I cleanly make the links navigate without reloading the page?
(The framework needs jQuery as a dependency, so that is able to be used in a solution.)
I was able to use a MutationObserver that watches for subtree changes and adds a custom click handler when it detects the links being added via .innerHTML.
With this method, I specify vue-goto:myPageName as the href attribute, and then the handler will take care of making it an SPA link.
import { router } from "#/router";
import { store } from "#/store";
export const attrib = "vue-goto";
export const prefix = attrib + ":";
function onChange() {
// find all links matching our custom prefix to which we have not yet added our custom handler
const links = window.$(`a[href^='${prefix}']`).not(`[${attrib}]`);
// add custom attribute for us to grab later
links.attr(attrib, function() {
// jQuery doesn't like arrow functions
return window
.$(this)
.attr("href")
.substr(prefix.length)
.trim();
});
// Update href on the link to one that makes sense
links.attr("href", function() {
return router.resolve({
name: window.$(this).attr(attrib), // grab attribute we saved earlier
params: { lang: store.state.language }, // in our case, our pages are qualified by a language parameter
}).href;
});
// Override default click navigation behaviour to use vue-router programmatic navigation
links.click(function(e) {
e.preventDefault(); // prevent default click
const routeName = window.$(this).attr(attrib);
const goto = {
name: routeName,
lang: store.state.language,
};
router.push(goto).catch(ex => {
// add catch here so navigation promise errors aren't lost to the void causing headaches later
// eslint-disable-next-line no-console
console.error(
`Error occurred during navigation from injected [${prefix}${routeName}] link`,
"\n",
ex,
);
});
});
}
let observer;
export function init() {
if (observer) observer.unobserve(document.body);
observer = new MutationObserver(onChange);
observer.observe(document.body, {
characterData: false,
childList: true,
subtree: true, // important, we want to see all changes not just at toplevel
attributes: false,
});
}
init();

VueJS show splash screen after login (different landing page)

I have my VueJS application in which the user logins and depending on the role a specific landing page is pushed in the router.
Here the sample code or the store that handles login:
login({ dispatch, commit, state }, data) {
return API.post('api/account/login', data).then(({ data }) => {
if (data.success) {
var view = getView(data.data, state)
router.push(view);
}
return Promise.resolve(data);
});
},
So as you can see I can have different landing pages after login depending on user role. What I need to do is that no matter what landing page is I have to show a Splash screen (modal) after the user logins.
If its only one landing page for all I guess I could add something in the mount event of that view to show the modal, but because I have 9 different landing views I dont think the best way is to add the same code to the 9 views.
Is there a way I can do that in a propper or best practice way?
Thanks
If you have 9 different routes a user can be directed to, could you:
create a modal component
load it into these views/routes
open it when the route is mounted
This way the modal code is centralized in its own component, and can watch for a property change to appear.
As a followup on my comment: you can mount a separate Vue instance. You can do it with something like this:
createNewInstance: function(){
const splash = new Vue({
methods: {
closeHandler() {
return function() {
splash.$destroy();
splash.$el.remove();
};
}
},
render(h) {
return h(YourSplashComponent, {
mounted() {
setTimeout(this.closeHandler(), 3000)
}
});
}
}).$mount();
document.body.appendChild(splash.$el);
}
Just be sure to import and create a component on where YourSplashComponent is stated in the example.
Call the function right before you do trigger your route

Vuex and Electron carrying state over into new window

I'm currently building an application using Electron which is fantastic so far.
I'm using Vue.js and Vuex to manage the main state of my app, mainly user state (profile and is authenticated etc...)
I'm wondering if it's possible to open a new window, and have the same Vuex state as the main window e.g.
I currently show a login window on app launch if the user is not authenticated which works fine.
function createLoginWindow() {
loginWindow = new BrowserWindow({ width: 600, height: 300, frame: false, show: false });
loginWindow.loadURL(`file://${__dirname}/app/index.html`);
loginWindow.on('closed', () => { loginWindow = null; });
loginWindow.once('ready-to-show', () => {
loginWindow.show();
})
}
User does the login form, if successful then fires this function:
function showMainWindow() {
loginWindow.close(); // Also sets to null in `close` event
mainWindow = new BrowserWindow({width: 1280, height: 1024, show: false});
mainWindow.loadURL(`file://${__dirname}/app/index.html?loadMainView=true`);
mainWindow.once('resize', () => {
mainWindow.show();
})
}
This all works and all, the only problem is, the mainWindow doesn't share the same this.$store as its loginWindow that was .close()'d
Is there any way to pass the Vuex this.$store to my new window so I don't have to cram everything into mainWindow with constantly having to hide it, change its view, plus I want to be able to have other windows (friends list etc) that would rely on the Vuex state.
Hope this isn't too confusing if you need clarification just ask. Thanks.
Although I can potentially see how you may do this I would add the disclaimer that as you are using Vue you shouldn't. Instead I would use vue components to build these seperate views and then you can achieve your goals in an SPA. Components can also be dynamic which would likely help with the issue you have of hiding them in your mainWindow, i.e.
<component v-bind:is="currentView"></component>
Then you would simply set currentView to the component name and it would have full access to your Vuex store, whilst only mounting / showing the view you want.
However as you are looking into it I believe it should be possible to pass the values of the store within loginWindow to mainWindow but it wouldn't be a pure Vue solution.
Rather you create a method within loginWindows Vue instance that outputs a plain Object containing all the key: value states you want to pass. Then you set the loginWindows variable to a global variable within mainWindow, this would allow it to update these values within its store. i.e.
# loginWindow Vue model
window.vuexValuesToPass = this.outputVuexStore()
# mainWindow
var valuesToUpdate = window.opener.vuexValuesToPass
then within mainWindows Vue instance you can set up an action to update the store with all the values you passed it
Giving the fact that you are using electron's BrowserWindow for each interaction, i'd go with ipc channel communication.
This is for the main process
import { ipcMain } from 'electron'
let mainState = null
ipcMain.on('vuex-connect', (event) => {
event.sender.send('vuex-connected', mainState)
})
ipcMain.on('window-closed', (event, state) => {
mainState = state
})
Then, we need to create a plugin for Vuex store. Let's call it ipc. There's some helpful info here
import { ipcRenderer } from 'electron'
import * as types from '../../../store/mutation-types'
export default store => {
ipcRenderer.send('vuex-connect')
ipcRenderer.on('vuex-connected', (event, state) => {
store.commit(types.UPDATE_STATE, state)
})
}
After this, use the store.commit to update the entire store state.
import ipc from './plugins/ipc'
var cloneDeep = require('lodash.clonedeep')
export default new Vuex.Store({
modules,
actions,
plugins: [ipc],
strict: process.env.NODE_ENV !== 'production',
mutations: {
[types.UPDATE_STATE] (state, payload) {
// here we update current store state with the one
// set at window open from main renderer process
this.replaceState(cloneDeep(payload))
}
}
})
Now it remains to send the vuex state when window closing is fired, or any other event you'd like. Put this in renderer process where you have access to store state.
ipcRenderer.send('window-closed', store.state)
Keep in mind that i've not specifically tested the above scenario. It's something i'm using in an application that spawns new BrowserWindow instances and syncs the Vuex store between them.
Regards
GuyC's suggestion on making the app totally single-page makes sense. Try vue-router to manage navigation between routes in your SPA.
And I have a rough solution to do what you want, it saves the effort to import something like vue-router but replacing components in the page by configured routes is always smoother than loading a new page: when open a new window, we have its window object, we can set the shared states to the window's session storage (or some global object), then let vuex in the new window to retrieve it, like created() {if(UIDNotInVuex) tryGetItFromSessionStorage();}. The created is some component's created hook.

durandal router.navigate not working

I need to navigate to another view from a event handler in my code, I do it like this
define(['durandal/system', 'durandal/app', 'durandal/viewLocator', 'plugins/router', 'underscore'],
function (system, app, viewLocator, router, _) {
system.log('starting app');
//>>excludeStart("build", true);
system.debug(true);
//>>excludeEnd("build");
app.title = 'Destiny';
app.configurePlugins({
router: true,
dialog: true,
widget: true,
observable: true
});
router.map('destination', 'viewmodels/destination');
router.activate();
_.delay(function() {
app.start().then(function() {
//Replace 'viewmodels' in the moduleId with 'views' to locate the view.
//Look for partial views in a 'views' folder in the root.
viewLocator.useConvention();
//Show the app by setting the root view model for our application with a transition.
app.setRoot('viewmodels/locationPicker', 'flip');
});
}, 1500);
}
);
Here I define a mapping between my moudle and the router - destination and set root view as another view locationPicker
in my another view locationPicker.js, I navigate to it like this
router.navigate('destination');
from the developer tool, I see that my view model destination.js file is loaded without error, but the view does not change at all. Why is this happening? BTW, I am using the durandal 2.0.1 version
The router plugin is initialized in the call to app.start. Therefore, you're configuring the plugin before initialization, and the configuration isn't being registered. Also, I'm not familiar with your syntax for registering the route. The more standard way is to pass in a list of objects with a route pattern and module id. Please try the following:
define(['durandal/system', 'durandal/app', 'durandal/viewLocator', 'plugins/router'],
function (system, app, viewLocator, router) {
system.log('starting app');
//>>excludeStart("build", true);
system.debug(true);
//>>excludeEnd("build");
app.title = 'Destiny';
app.configurePlugins({
router: true,
dialog: true,
widget: true,
observable: true
});
app.start().then(function() {
router.map([
{ route: 'destination', moduleId: 'viewmodels/destination' }
]).activate();
//Replace 'viewmodels' in the moduleId with 'views' to locate the view.
//Look for partial views in a 'views' folder in the root.
viewLocator.useConvention();
//Show the app by setting the root view model for our application with a transition.
app.setRoot('viewmodels/locationPicker', 'flip');
});
}

How to hide templates with AngularJS ngView for unauthorized users?

I have a basic PHP app, where the user login is stored in the HTTP Session. The app has one main template, say index.html, that switch sub-view using ngView, like this
<body ng-controller='MainCtrl'>
<div ng-view></div>
</body>
Now, this main template can be protected via basic PHP controls, but i have sub-templates (i.e. user list, add user, edit user, etc.) that are plain html files, included from angular according to my route settings.
While i am able to check for auth what concern the request of http services, one user is able to navigate to the sub-template url and access it. How can i prevent this from happen?
I would create a service like this:
app.factory('routeAuths', [ function() {
// any path that starts with /template1 will be restricted
var routeAuths = [{
path : '/template1.*',
access : 'restricted'
}];
return {
get : function(path) {
//you can expand the matching algorithm for wildcards etc.
var routeAuth;
for ( var i = 0; i < routeAuths.length; i += 1) {
routeAuth = routeAuths[i];
var routeAuthRegex = new RegExp(routeAuth.path);
if (routeAuthRegex.test(path)) {
if (routeAuth.access === 'restricted') {
return {
access : 'restricted',
path : path
};
}
}
}
// you can also make the default 'restricted' and check only for 'allowed'
return {
access : 'allowed',
path : path
};
}
};
} ]);
And in the main/root controller listen for $locationChangeStart events:
app.controller('AppController', ['$scope', '$route', '$routeParams', '$location', 'routeAuths',
function(scope, route, routeParams, location, routeAuths) {
scope.route = route;
scope.routeParams = routeParams;
scope.location = location;
scope.routeAuth = {
};
scope.$on('$locationChangeStart', function(event, newVal, oldVal) {
var routeAuth = routeAuths.get(location.path());
if (routeAuth.access === 'restricted') {
if (scope.routeAuth.allowed) {
event.preventDefault();
}
else {
//if the browser navigates with a direct url that is restricted
//redirect to a default
location.url('/main');
}
scope.routeAuth.restricted = routeAuth;
}
else {
scope.routeAuth.allowed = routeAuth;
scope.routeAuth.restricted = undefined;
}
});
}]);
Demo:
plunker
References:
angularjs services
location
UPDATE:
In order to fully prevent html template access then it's best done on the server as well. Since if you serve the html from a static folder on server a user can access the file directly ex: root_url/templates/template1.html thus circumventing the angular checker.
If you want to block them from going to that page create a service: http://docs.angularjs.org/guide/dev_guide.services.creating_services
This service can be dependency injected by all your controllers that you registered with the routeParams.
In the service you can would have a function that would check to see if the person is logged in or not and then re-route them (back to the login page perhaps?) using http://docs.angularjs.org/api/ng.$location#path. Call this function in each of the controllers like so:
function myController(myServiceChecker){
myServiceChecker.makeSureLoggedIn();
}
The makeSureLoggedIn function would check what current url they're at (using the $location.path) and if it's not one they're allowed to, redirect them back to a page that they are allowed to be.
I'd be interested to know if there's a way to prevent the routeParams from even firing, but at least this will let you do what you want.
Edit: Also see my answer here, you can prevent them from even going to the page:
AngularJS - Detecting, stalling, and cancelling route changes