bencodezen / vue-enterprise-boilerplate

An ever-evolving, very opinionated architecture and dev environment for new Vue SPA projects using Vue CLI.
7.78k stars 1.32k forks source link

Prevent layout component re-rendering #38

Open ma53 opened 6 years ago

ma53 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

I'm going to assume this solves the problem, but happy to reopen if it doesn't. πŸ™‚

ma53 commented 6 years ago

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

marceloavf commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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

chrisvfritz commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

@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 commented 4 years ago

@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.

myleslee commented 4 years ago

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

wrurik commented 3 years ago

@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?