durandal router.navigate not working - durandal

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');
});
}

Related

Lazy loading & loading states with vue-router, vite & vuejs 2.x

I'm migrating an old project from vue-cli to vite. I followed the migration guide and everything worked great, but there's something it's not working, or at least, not as intended, when I was using vue-cli I tried to implement the loading states as shown in their documentation but then I saw the following pull request explaining how to achieve the wanted behavior (with the downside of losing navigation guards on those routes).
Now, after migrating I noticed that neither the loading/error components are rendered at all, even setting a timeout really small, however, I see in the networking tab that my components are being loaded, but never rendered.
Do you have any suggestions of why might this occur?.
// This is the lazyLoadView that was working before migrating to vite.
function lazyLoadView(AsyncView) {
const AsyncHandler = () => ({
component: AsyncView,
// A component to use while the component is loading.
loading: import("./components/loaders/loader.vue").default,
// A fallback component in case the timeout is exceeded
// when loading the component.
error: import("./components/loaders/error.vue").default,
// Delay before showing the loading component.
// Default: 200 (milliseconds).
delay: 1,
// Time before giving up trying to load the component.
// Default: Infinity (milliseconds).
timeout: 2,
});
return Promise.resolve({
functional: true,
render(h, { data, children }) {
// Transparently pass any props or children
// to the view component.
return h(AsyncHandler, data, children);
},
});
}
And the routes I have:
const routes = [
{
path: "/foo/",
component: () => lazyLoadView(import("./components/bar.vue")),
}
]
Let me know if you find why might this be happening.
So I figured out:
Looks like the loading & error components were also lazy loaded, they were skipped. So continued trying to obtain the main one until shown (that's why didn't render the loading besides of only showing a message).
So in order to fix it I had to import them at the top and include them in the lazyLoadView like this:
//These two imports at the top
import loaderComp from "./components/loaders/loader.vue";
import errorComp from "./components/loaders/error.vue";
function lazyLoadView(AsyncView) {
const AsyncHandler = () => ({
component: AsyncView,
// A component to use while the component is loading.
// must NOT be lazy-loaded
loading: loaderComp,
// A fallback component in case the timeout is exceeded
// when loading the component.
// must NOT be lazy-loaded
error: errorComp,
// Delay before showing the loading component.
// Default: 200 (milliseconds).
delay: 1,
// Time before giving up trying to load the component.
// Default: Infinity (milliseconds).
timeout: 2,
});
return Promise.resolve({
functional: true,
render(h, { data, children }) {
// Transparently pass any props or children
// to the view component.
return h(AsyncHandler, data, children);
},
});
}

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();

How to add Matomo tracking code in VueJS Single Page Apps?

I wanted to confirm whether I got my analytics tracking setup correctly in my single page application within the VueJS framework.
I am using the Vue plugin for Matomo which is found here:
https://github.com/AmazingDreams/vue-matomo
I imported the VueMatomo plugin in my main.js entry file like so:
import VueMatomo from 'vue-matomo';
Then, I assign the VueMatomo as a global method in my main.js file like so:
Vue.use(VueMatomo, {
// Configure your matomo server and site
host: 'https://matomo.example.com', <-- i configured this to match my real site
siteId: 5, <--- i configured this to match my real site
// Enables automatically registering pageviews on the router
router: router,
// Enables link tracking on regular links. Note that this won't
// work for routing links (ie. internal Vue router links)
// Default: true
enableLinkTracking: true,
// Require consent before sending tracking information to matomo
// Default: false
requireConsent: false,
// Whether to track the initial page view
// Default: true
trackInitialView: true,
// Changes the default .js and .php endpoint's filename
// Default: 'piwik'
trackerFileName: 'piwik',
// Whether or not to log debug information
// Default: false
debug: false
});
That gives me access to the Matomo API (_paq) in my components. However, this is where I am confused.
For example, I have a view called overview.vue which is the main page of the site. In this vue template, I have the following code in my created() hook. Since I am using a SPA, I need to somehow get the name of the page that the user is on and push it to the Matomo Reporting Tool. This is what I did:
<template>...snip...</template>
<script>
export default {
name: 'OverView',
created: function() {
window._paq.push(['setCustomUrl', '/' + window.location.hash.substr(1)]);
window._paq.push(['setDocumentTitle', 'Overview Page']);
window._paq.push(['trackPageView']);
}
};
</script>
Is the above adequate or is there a better lifecyle hook (mounted?) for the tracking code? Perhaps navigation guards are more appropriate?
Thank you
I got matomo working on my vue.js app (v 2.6.10).
I'm using a trial account from https://matomo.org/
In my main.js file:
// Analytics
import VueMatomo from "vue-matomo";
Vue.use(VueMatomo, {
host: "https://example.matomo.cloud", // switch this to your account
siteId: 1, // switch this as well you can find the site id after adding the website to the dashboard.
router: router,
enableLinkTracking: true,
requireConsent: false,
trackInitialView: true,
trackerFileName: "piwik",
debug: true
});
I can confirm that all of my nested routes are tracked. I can see what pages i viewed on my matomo dashboard.
To get custom events working just add the following:
this.$matomo.trackEvent("Event Category", "Event Name", "event action");
To give this some context, for my app i'm using it in a computed property:
computed: {
selectedMapDataType: {
get() {
return this.$store.state.mapDataType;
},
set(selected) {
this.$matomo.trackEvent("Dashboard Update", "Dashboard Data", selected);
this.$store.dispatch("updateMapDataType", selected);
}
},
...}

Laravel 5.1 and Backbone.js Application

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 ?

How to use durandal router to activate dialogs?

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.
});