Background
I have a typical web app with a left column menu; and a right column for content.
If I put a menu in the left column and use <router-view></router-view> in the right, as you would expect the content is swapped out as you navigate routes.
See a basic gist.run here. This is obviously not my actual app so ignore the oversimplifications. You can see by clicking on one of the menu items it changes style, then click one of the home/page links and the menu item keeps its applied class.
The problem
If I add a layout for these two routes, the menu looses its state when changing routes. i.e. the applied "active" style is lost because the layout is reloaded, instead of just the slot being reloaded. gist.run here:
app.js:
config.map([
{ route: '', name: 'home', moduleId: 'home', layoutView: "layout.html", layoutViewModel: "layout" },
{ route: 'page', name: 'page', moduleId: 'page', layoutView: "layout.html", layoutViewModel: "layout" }
]);
layout.html:
<template>
<!-- left menu content here -->
<div class="layout">
<!-- route view on right: -->
<slot></slot>
</div>
</template>
My home.html and route.html pages are identical to the previous gist without layouts:
<template>
Home
</template>
Note that slots are not needed here (e.g. <div slot="name">...) because there is only one slot. I've tried including them and the result is the same.
I would expect that the layout/router behaviour should look the same - that the layout isn't reloaded on each route change - even though it's wired differently underneath.
This is a problem because I may have animations on the menu items (sliding, dropdowns, active classes etc.) and I don't want those starting from the default position each time a route is changed. I may also have controls in the left panel such as a search box or other form, and may not want that refreshing on each route change.
Is there any way I can make the layout behave the same (keep "state") as the router-view?
As a way around it I've considered:
an initial state that I put together from the current route (could get messy or not cover all options)
saving state in a service or singleton somehow, but I can't figure out if this is possible with a view
router viewports. Not quite as flexible because I can't have multiple different app.html's with different viewports laid out in different ways. It also requires a module-per-viewport or other trickery, and can't have all the content from the same view as with slots...
Please tell me if there's something else I can do or if I'm doing it all wrong!
thanks.
I just yesterday asked about this over at the Aurelia Gitter room, and the response I got was that layout lifecycle is tied to the routed module's lifecycle. In practice, this means that:
If your activationStrategy is refresh, the layout will be destroyed and recreated even when navigating to the same route with different parameters.
If your activationStrategy is invokeLifecycle, the layout will not be destroyed and recreated if you navigate to the same route, but it will be if you navigate to a different route.
So if you have some state that you want to keep in the parent structure, and still be able to have multiple positions where content is projected on a per-route basis, viewports are the way to go, with the caveats that you already noted.
Related
This is a follow up to #1458. I'm looking for some direction on how Nuxt expects this to be handled.
I have a menu. When I click on a nuxt-link in the menu, I want to have time to close the menu before the page transition happens. The thing is, I only want that to happen when you click on the nuxt-link in the menu, not every time I go to a certain route (as the previous issue described using a middlewear on the route).
So there are a few different ways to do this, and I'm curious what the "Nuxt" way is?
The way we currently do this, disable the nuxt-link and capture the click, then do a router.push().
<nuxt-link :to="path" event="disabled" #click.native="delayLoad"/>
// Methods
delayLoad(event) {
this.$store.commit("CLOSE_MENU")
setTimeout(()=>{
this.$router.push(event.target.pathname)
}, 2000)
}
Is this a good idea? I just always have an aversion to hijacking nuxt-link and browser navigation like this. It seems janky.
The other ideas we played with were using a query param on the nuxt-link, and then using that in a middlewear to delay the page transition. That seemed worse to me, because now my URL's have a query param in them that is used for an animation, seems like that is abusing query params. This also triggers the page loading progress bar, which isn't really the intent, it's to have a sequenced animation happen, then page load.
It seems to me that perhaps nuxt-link should have a delay prop, or perhaps the page transition config should allow for a delay (like it does with duration)?
I wanted to do this as well and came up with the following solution. Using the new slots api you can more elegantly customise the nuxt-link behaviour:
<nuxt-link v-slot="{ route, href }" :to="path" custom>
<a :href="href" #click.prevent="$emit('navigate', route)">
<slot></slot>
</a>
</nuxt-link>
This will make the link emit a navigate event with the route as a param. You then listen for this event wherever you include your menu component, like this:
<template>
<transition
name="fade"
#after-leave="maybeNavigate"
>
<MainMenu
v-if="menuIsVisible"
#navigate="navigate"
/>
</transition>
</template>
<script>
export default {
data: () => ({
menuIsVisible: false,
navigateToOnMainMenuClose: null,
}),
methods: {
navigate(route) {
this.navigateToOnMainMenuClose = route
this.menuIsVisible = false
},
maybeNavigate() {
if (this.navigateToOnMainMenuClose) {
this.$router.push(this.navigateToOnMainMenuClose)
this.navigateToOnMainMenuClose = null
}
},
},
}
</script>
Whenever you click a nav link in the menu, the route will be stored and the menu will close. After the menu out animation has finished, maybeNavigate() will push the stored route, if there is one. This removes the need for a setTimeout and if you manage to click multiple links in quick succession only the last one will be stored and navigated to.
Since nuxt-link is essentially a wrapped version of vue's router-link, if you look at the documentation for that there is an event property that accepts string or string[], looking at it's source code here: https://github.com/vuejs/vue-router/blob/dev/src/components/link.js#L86
you can see it will register a listener for disabled in this instance. It may make more sense to pass an empty array so that no event listeners are registered, but that's at the cost of readability.
Otherwise, #click.native is the suggested way to handle custom click handlers for router-link (see: https://github.com/vuejs/vue-router/issues/800#issuecomment-254623582).
The only other concerns I can think of are what happens if you click 2 different links in rapid succession and what happens if you click more than once. May just want to add a variable to track if a link has been clicked to prevent firing setTimeout multiple times, which could navigate you from page A to B and then to C as all timeouts will fire if not canceled. Or maybe you want to only navigate to the 'last' link clicked, so if another link is clicked, you would cancel the earlier setTimeout. This is realistically an edge case that probably won't be an issue, but worth exploring.
Otherwise, IMO, looks good to me. This seems like the simplest way to implement this without having to create a custom component / plugin. I'm no expert, but is likely how I would implement this functionality as well. It would be nice to see a delay option though since I can see myself using that functionality as well with vuetify.
Another potential method would be to do your store commit in beforeTransition: https://nuxtjs.org/api/configuration-transition/
Though I'm not sure that there is access to the store there, so you might have to write a custom plugin for that as well. Again, seems more complicated than it's worth for a simple delayed animation. Simple, working code is sometimes the best solution, even if it's not the most extensible option.
See also: How can I transition between two nuxt pages, while first waiting on a child component transition/animation to finish?
for another way of handling this.
I'm building a single-file-based Vue application from a template generated with the Vue UI tool.
I understand how a .vue file defines the styling/structure/behavior of a component, how smaller components can be composed into bigger components, and how the top-level "App" component mounts everything to an HTML Div.
As the user progresses through the app, though -- say from a login screen to a master screen to a detail screen -- what's the accepted approach to switching out the current screen-level component?
Ty in advance.
--The Vuebie
This is quite an open ended question so ill just show you what I have done in my own projects. I split my components directory into two directories; 'pages' and 'common'. (Ignore the 'firebase' directory is it beyond the scope of this question).
The common directory holds components that may be used in a page or re used in several different pages.
For example the 'account form' is used in my 'Edit Account page' and the category bar is used in several of my pages.
The pages directory holds components that are technically no different from my common components but they represent full pages on my website. A page component may contain several common components.
Now the biggest distinction between common and pages is in the router. I route different paths relative to the main url (that is probably not the technically correct description but hopefully you get the point) to each of the pages. Here is my index.js file from my router directory:
As you can see, I have a route pointing to each one of my pages. You can " switch out the current screen-level component" (as you put it) by using router-link tag's to navigate between different page components. These are clickable urls that your client can use, they can also be wrapped in buttons and such.
For example, this router link navigates to my home page, the component name is 'Helloworld'. See its corresponding reference in my router's index.js and in the pages directory so you can connect it all in your head.
<router-link class="nav-item nav-word" :to="{ name: 'HelloWorld' }">
Finally, I will talk a bit about the App.vue file. The App.vue acts like a base component as it contains the 'router view' tag within it's template:
<router-view/>
This means that every page that you route will be placed in the position of the 'router view tag'. I.e this tag will be replaced with the page. It is common practise to surround this tag with html code that you would like to be shown in each page. For example I have my router view tag between my nav bar and footer. So that the nav bar and footer will show on each page.
Faced such a problem - I send data to the props of the /router-link/ tag, when I click on the link, I go to the article, it gets the data, everything works. Well, if you press the "back" button in the browser and then "forward" there will be no articles, there will be empty fields without data. How can this be avoided?
This is link to the article
<h3 class="database-article__title">
<router-link
:to="{name : 'article',params: {
id: item.id ,
type:item.type ,
name: item.name,
text: item.text,
author: item.author,
isFavorite: item.isFavorite
}}"> {{item.name}} </router-link>
</h3>
Little part of article-template.vue
<div class="content-type marketing">
{{$route.params.type}}
</div>
<h3 class="database-article__title">
{{$route.params.name}}
</h3>
<div class="database-article__text">
{{$route.params.text}}
</div>
Once again, the data transfer is good, when you click on the link, everything is displayed. The problem is that when clicking on the buttons in the browser "back" and "forward" - the browser history is not saved.
Does anyone know the solution to the problem, or where i can read how to solve it?
Thanks!
My guess is that your article route does not specify any of those params in its path. When you click the link, vue-router will remember the params object you specified in the <router-link> and will be accessible through $route.params in the article component.
However, when you click the browser back then forward buttons, the transition to the article route did not occur by clicking the <router-link> like it did the first time, and since those params were not included in the route's path, $route.params will be empty.
I'm guessing you're just trying to pass data from one route to another. If you want it to persist across history state changes (i.e. browser back/forward), then either:
The data needs to be included in the URL, either as params (e.g. /article/:id/:type etc, this needs to be specified upfront in the route's path) or in the query string (e.g. /article?id=1&type=foo). This isn't ideal for this situation.
(Recommended) Store the item object in such a way that it can be accessed by any route. Vuex is one way, but this may be overkill.
Realistically your URLs should only need to have the article's ID in it, like this /article/1. All the other stuff like type/name/etc don't belong in the URL. From the ID you should be able to fetch the full article object either from a REST API (XHR request), or obtain it from some in-memory data store abstraction (Vuex or anything else really).
Vuetify has a pretty flexible layout scheme encompassing a menu, toolbar, content, and footer, that allows some nice-looking material design schemes, e.g. Google Contacts:
Consider a standard setup with the layout being controlled by the router, with a fixed menu across the site, but a dynamic toolbar that changes with the page shown. What's the best practice for changing the content of the toolbar depending on what page is shown? In the Google Contacts example, we only want the search bar to show up on the Contacts page.
Based on my rudimentary knowledge of Vue, it seems like defining a router layout with a scoped slot. There are likely a lot of other hacky ways to achieve this. But I'm looking for a clean, modular way to define toolbar content across pages.
Ideas:
As of a while ago vue-router didn't support named slots. But this seems to have changed recently, although there is no documentation.
Named views seem to be a nice way to support tie the toolbar content to the main page with vue-router. But there doesn't seem to be a good way for the toolbar to 'talk' to the main page as there would be with a slot.
You can define multiple router views in your application. Imagine your layout looks extremely simplified like this:
<v-app id="app">
<router-view name="navigation"></router-view>
<router-view></router-view>
</v-app>
Then you can define a route with components for both router views:
{
path: '/hello',
components: {
default: MyHelloComponent,
navigation: MyNavigationForHelloComponent
}
}
Documentation
Working Example from Documentation
I have a scenario where there are two major components on a page; a frame-like component that contains common functionality for many applications (including a bookmark/tab bar) and my actual application code.
Since the frame doesn't actually own the page that it's included on, it seems like it would be incorrect for it to define any routes, however the current page may define their own routes that may match one of those links. In that case, I'd like vue-router to handle those anchor clicks and navigate appropriately rather than doing a full page reload.
Here's a simplified template of what this looks like:
Frame (an external dependency for my app):
<Frame>
<TabStrip>
</TabStrip>
<slot></slot>
<Frame>
App1:
<Frame>
<App>You're looking at: {{ pageId }}!</App>
</Frame>
So when any of the app1 domain links are clicked from that tab strip, I want my route definitions in app1 to pick that up rather than it causing a page load. Since that component is owned by the frame, I don't have access to write <router-link> since links to many different apps may co-exist there.
Any thoughts?
Whoo, this is an old one! However, since this question was high in my search results when I was researching this problem, I figured I should answer it.
My use-case was similar to the one in the comments: I needed to capture normal <a> links within rendered v-html and parse them through the router (the app is rendering Markdown with a light modification that generates internal links in some cases).
Things to note about my solution:
I'm using Vue3, not Vue2; the biggest difference is that this is the new Vue3 composition-style single page component syntax, but it should be easy to backport to Vue2, if necessary, because the actual things it's doing are standard Vue.
I stripped out the markdown logic, because it doesn't have anything to do with this question.
Note the code comment! You will very likely need to design your own conditional logic for how to identify links that need to be routed vs. other links (e.g. if the application in the original question has same-origin links that aren't handled by the Vue app, then copy/pasting my solution as-is won't work).
<script setup>
import { useRouter } from "vue-router"
const router = useRouter()
const props = defineProps({
source: {
type: String,
required: true,
},
})
function handleRouteLink(event) {
const target = event.target
// IMPORTANT! This is where you need to make a decision that's appropriate
// for your application. In my case, all links using the same origin are
// guaranteed to be internal, so I simply use duck-typing for the
// properties I need and compare the origins. Logic is inverted because I
// prefer to exit early rather than nest all logic in a conditional (pure
// style choice; works fine either way, and a non-inverted conditional is
// arguably easier to read).
if (!target.pathname || !target.origin || target.origin != window.location.origin) {
return
}
// We've determined this is a link that should be routed, so cancel
// the event and push it onto the router!
event.preventDefault()
event.stopPropagation()
router.push(target.pathname)
}
</script>
<template>
<div v-html="source" #click="handleRouteLink"></div>
</template>