Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent layout component re-rendering #38

Open
ma53 opened this issue Apr 20, 2018 · 13 comments
Open

Prevent layout component re-rendering #38

ma53 opened this issue Apr 20, 2018 · 13 comments
Labels
enhancement New feature or request question Further information is requested

Comments

@ma53
Copy link

ma53 commented Apr 20, 2018

I've been refactoring a work project using this repo as reference, and I've run into a snag. When navigating from one view to another, I'd like to avoid re-rendering the layout components that the views have in common. I'm after this for two reasons: one is efficiency, of course, but the more visible issue is that I'd like to transition the components that are entering and leaving.

For example, my "home" and "404" views use a layout that does not include a sidebar. The rest of my views use a second layout that does. When moving between "home" and another route, I'd like the sidebar to appear with a simple, satisfying transition; so, I wrap it in a <transition appear> tag and it works as anticipated. However, the sidebar also transitions out and in between routes that share the layout.

Thinking it would solve the problem, I removed the key attribute from the <router-view> in my app.vue , but to no effect.

I know there's not much in particular to be done without looking at the source (which I am not permitted to share), but can you think of anything off the top of your head that I should check? Something basic I might have overlooked?

@chrisvfritz
Copy link
Collaborator

For this effect, I'd recommend using layouts a little bit differently. Instead of defining them in your view components, you could define them as a meta property on your routes (potentially with a default to fall back on). Below is an example refactor that I believe should achieve what you want. 🙂

diff --git a/src/app.vue b/src/app.vue
index 6857411..b9702a8 100644
--- a/src/app.vue
+++ b/src/app.vue
@@ -9,6 +9,14 @@ export default {
       return title ? `${title} | ${appConfig.title}` : appConfig.title
     },
   },
+  computed: {
+    LayoutComponent() {
+      return (
+        (this.$route.meta && this.$route.meta.layout) ||
+        require('@layouts/main').default
+      )
+    },
+  },
 }
 </script>
 
@@ -18,7 +26,22 @@ export default {
     Even when routes use the same component, treat them
     as distinct and create the component again.
     -->
-    <router-view :key="$route.fullPath"/>
+    <transition
+      name="fade"
+      mode="out-in"
+    >
+      <component
+        :is="LayoutComponent"
+        :key="LayoutComponent.name || LayoutComponent.__file"
+      >
+        <transition
+          name="fade"
+          mode="out-in"
+        >
+          <router-view :key="fullPath"/>
+        </transition>
+      </component>
+    </transition>
   </div>
 </template>
 
@@ -88,4 +111,17 @@ h6 {
 #nprogress .bar {
   background: $color-link-text;
 }
+
+// ===
+// Transitions
+// ===
+
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.5s;
+}
+.fade-enter,
+.fade-leave-to {
+  opacity: 0;
+}
 </style>
diff --git a/src/router/views/404.vue b/src/router/views/404.vue
index 4ce46e9..1921d60 100644
--- a/src/router/views/404.vue
+++ b/src/router/views/404.vue
@@ -1,12 +1,9 @@
 <script>
-import Layout from '@layouts/main'
-
 export default {
   page: {
     title: '404',
     meta: [{ name: 'description', content: '404' }],
   },
-  components: { Layout },
   props: {
     resource: {
       type: String,
@@ -17,15 +14,13 @@ export default {
 </script>
 
 <template>
-  <Layout>
-    <h1 :class="$style.title">
-      404
-      <span v-if="resource">
-        {{ resource }}
-      </span>
-      Not Found
-    </h1>
-  </Layout>
+  <h1 :class="$style.title">
+    404
+    <span v-if="resource">
+      {{ resource }}
+    </span>
+    Not Found
+  </h1>
 </template>
 
 <style lang="scss" module>
diff --git a/src/router/views/home.vue b/src/router/views/home.vue
index 540e538..dc71078 100644
--- a/src/router/views/home.vue
+++ b/src/router/views/home.vue
@@ -1,22 +1,20 @@
 <script>
 import appConfig from '@src/app.config'
-import Layout from '@layouts/main'
 
 export default {
   page: {
     title: 'Home',
     meta: [{ name: 'description', content: appConfig.description }],
   },
-  components: { Layout },
 }
 </script>
 
 <template>
-  <Layout>
+  <div>
     <h1>Home Page</h1>
     <img
       src="@assets/images/logo.png"
       alt="Logo"
     >
-  </Layout>
+  </div>
 </template>
diff --git a/src/router/views/loading.vue b/src/router/views/loading.vue
index df01943..eaf6e8f 100644
--- a/src/router/views/loading.vue
+++ b/src/router/views/loading.vue
@@ -1,25 +1,20 @@
 <script>
-import Layout from '@layouts/main'
-
 export default {
   page: {
     title: 'Loading page...',
     meta: [{ name: 'description', content: 'Loading page...' }],
   },
-  components: { Layout },
 }
 </script>
 
 <template>
-  <Layout>
-    <transition appear>
-      <BaseIcon
-        :class="$style.loadingIcon"
-        name="sync"
-        spin
-      />
-    </transition>
-  </Layout>
+  <transition appear>
+    <BaseIcon
+      :class="$style.loadingIcon"
+      name="sync"
+      spin
+    />
+  </transition>
 </template>
 
 <style lang="scss" module>
diff --git a/src/router/views/login.vue b/src/router/views/login.vue
index 7352feb..0f17226 100644
--- a/src/router/views/login.vue
+++ b/src/router/views/login.vue
@@ -1,5 +1,4 @@
 <script>
-import Layout from '@layouts/main'
 import { authMethods } from '@state/helpers'
 import appConfig from '@src/app.config'
 
@@ -8,7 +7,6 @@ export default {
     title: 'Log in',
     meta: [{ name: 'description', content: `Log in to ${appConfig.title}` }],
   },
-  components: { Layout },
   data() {
     return {
       username: '',
@@ -43,36 +41,34 @@ export default {
 </script>
 
 <template>
-  <Layout>
-    <form
-      :class="$style.form"
-      @submit.prevent="tryToLogIn"
+  <form
+    :class="$style.form"
+    @submit.prevent="tryToLogIn"
+  >
+    <BaseInput
+      v-model="username"
+      name="username"
+    />
+    <BaseInput
+      v-model="password"
+      name="password"
+      type="password"
+    />
+    <BaseButton
+      :disabled="tryingToLogIn"
+      type="submit"
     >
-      <BaseInput
-        v-model="username"
-        name="username"
+      <BaseIcon
+        v-if="tryingToLogIn"
+        name="sync"
+        spin
       />
-      <BaseInput
-        v-model="password"
-        name="password"
-        type="password"
-      />
-      <BaseButton
-        :disabled="tryingToLogIn"
-        type="submit"
-      >
-        <BaseIcon
-          v-if="tryingToLogIn"
-          name="sync"
-          spin
-        />
-        <span v-else>Log in</span>
-      </BaseButton>
-      <p v-if="authError">
-        There was an error logging in to your account.
-      </p>
-    </form>
-  </Layout>
+      <span v-else>Log in</span>
+    </BaseButton>
+    <p v-if="authError">
+      There was an error logging in to your account.
+    </p>
+  </form>
 </template>
 
 <style lang="scss" module>
diff --git a/src/router/views/profile.vue b/src/router/views/profile.vue
index 3a500f3..ea57ae4 100644
--- a/src/router/views/profile.vue
+++ b/src/router/views/profile.vue
@@ -1,6 +1,4 @@
 <script>
-import Layout from '@layouts/main'
-
 export default {
   page() {
     return {
@@ -13,7 +11,6 @@ export default {
       ],
     }
   },
-  components: { Layout },
   props: {
     user: {
       type: Object,
@@ -24,12 +21,12 @@ export default {
 </script>
 
 <template>
-  <Layout>
+  <div>
     <h1>
       <BaseIcon name="user"/>
       {{ user.name }}
       Profile
     </h1>
     <pre>{{ user }}</pre>
-  </Layout>
+  </div>
 </template>
diff --git a/src/router/views/timeout.vue b/src/router/views/timeout.vue
index 97a96d8..af443af 100644
--- a/src/router/views/timeout.vue
+++ b/src/router/views/timeout.vue
@@ -1,6 +1,4 @@
 <script>
-import Layout from '@layouts/main'
-
 export default {
   page: {
     title: 'Page timeout',
@@ -8,16 +6,13 @@ export default {
       { name: 'description', content: 'The page timed out while loading.' },
     ],
   },
-  components: { Layout },
 }
 </script>
 
 <template>
-  <Layout>
-    <h1 :class="$style.title">
-      The page timed out while loading
-    </h1>
-  </Layout>
+  <h1 :class="$style.title">
+    The page timed out while loading
+  </h1>
 </template>
 
 <style lang="scss" module>

Let me know if that solves the problem for you.

@chrisvfritz
Copy link
Collaborator

I'm going to assume this solves the problem, but happy to reopen if it doesn't. 🙂

@ma53
Copy link
Author

ma53 commented May 2, 2018

It did indeed! Thanks, @chrisvfritz, you're the best.

@marceloavf
Copy link

Hey @chrisvfritz, you did an awesome refactor here!

I implemented this content in my project but I got a little problem,

On the first load, it always show a little of the default layout until it gets totally loaded and goes to the desired layout specified in the meta, is there a way to solve this?

I was thinking about making a "loading" kind of layout to be the default and set the main to everyone else, but I don't think it's the best solution :/

LayoutTransition

@chrisvfritz
Copy link
Collaborator

@marceloavf That idea with the loading layout sounds fine actually. 🙂 You'd just have to always define a layout. You could also simply not have a default layout, or only show the default layout after the component for the current route has finished downloading.

@marceloavf
Copy link

Nice @chrisvfritz, I was thinking about this last idea, but I couldn't find a way to implement this. 😞

@chrisvfritz
Copy link
Collaborator

@marceloavf The easiest way might be to add something like a new currentRouteStatus property to the router, e.g. in src/router/index.js:

router.currentRouteStatus = Vue.observable({
  isLoaded: false,
})

router.beforeEach((routeTo, routeFrom, next) => {
  router.currentRouteStatus.isLoaded = !routeTo.matched.some(
    (route) =>
      typeof route.components === 'function' ||
      typeof route.components.default === 'function'
  )
  // ...

Then update isLoaded inside the AsyncHandler:

function lazyLoadView(AsyncView) {
  const AsyncHandler = () => ({
    component: AsyncView.then((componentConfig) => {
      require('@router').default.currentRouteStatus.isLoaded = true
      return componentConfig
    }),
    // ...

And finally, inside the template of app.vue, you could check $router.currentRouteStatus.isLoaded to see if the current route is loaded and only render the layout if it is. I haven't tested this strategy, so there may be edge cases I'm not currently thinking of, but this should give you a starting point. 🙂

@marceloavf
Copy link

Works like charm @chrisvfritz, just a question, this code will not block this one from lazyLoadView?

    // A component to use while the component is loading.
    loading: require('@views/_loading').default,
    // Delay before showing the loading component.
    // Default: 200 (milliseconds).
    delay: 400,

@chrisvfritz
Copy link
Collaborator

chrisvfritz commented Apr 1, 2019

@marceloavf Great question. In order to prevent it from interfering, you'd have to render the <RouterView> without a wrapping layout when the current route is not loaded, rather than just rendering nothing.

@dnewkerk
Copy link

@chrisvfritz thanks for the alternate way of using layouts above. I ran into a similar issue with the layout being re-rendered on route changes. For reference, the app I'm working on (tagnifi.com) has a filterable data table, and the selected filter params get persisted in the URL so the filtered search can be saved/shared. However every filter change causes the layout to re-render, so the form loses the current tab index, some temporary state on the page is lost, etc.

I tried using the alternate approach you provided above, and while it resolves the above issue with route changes, I can't figure out now if/how I can use multiple named slots in my layout. The filter area appears in a specific spot of the layout using <template v-slot:before-content>...</template> (it stays sticky at the top of the page while only the results scroll) though I get compile errors now when trying to do this without the Layout wrapper in my component.

Anyhow if you might be able to point me in the right direction, it would be greatly appreciated. Thanks!

@chrisvfritz
Copy link
Collaborator

@dnewkerk Hopefully you've solved the issue on your own by now, but you may want $route.path in that case rather than $route.fullPath (see the Vue Router docs for the difference).

Also, I'm reopening this as I'm thinking about updating the routing strategy to something along these lines, as I've had a number of projects that have needed this kind of strategy and am thinking there might be more advantages than disadvantages for most projects.

@chrisvfritz chrisvfritz reopened this Oct 10, 2019
@chrisvfritz chrisvfritz added enhancement New feature or request question Further information is requested labels Oct 10, 2019
@myleslee
Copy link

@chrisvfritz is there a branch that contains the code changes illustrated inside this comment? Thanks!

@wrurik
Copy link

wrurik commented Nov 1, 2020

@dnewkerk Did you ever find a solution to the multiple named slots issue?
I'm facing the same problem now and the only ways I could think of fixing it are:

  1. include content for all named slots in route meta data.
  2. maybe use something like portal-vue

Both solutions seem don't feel quite right for me, so I'm hoping you are willing to share what you came up with?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request question Further information is requested
Projects
None yet
Development

No branches or pull requests

6 participants