Nuxt2: Theme-class assigned to body disappears after routing

41 Views Asked by At

I've got a nuxt2 app in ssr. I've implemented custom-theming plugins (code provided below).

The theme gets saved in cookies and when the clients makes a request, the server knows what theme, because of the sent cookies. The theme gets applied in a plugin by assigning a certain class to the body:

this.ctx.app.head.bodyAttrs = this.ctx.app.head.bodyAttrs || {};
this.ctx.app.head.bodyAttrs.class = this.value + '-mode';

When ssr-rendered content hits the client, everything is fine, but with the first navigating to another internal-link, the attributes of the body get deleted (not only the assigned classes, also the data-n-head).

The code is somewhat close to what color-mode uses, because I need to replace color-mode.

Thank you!

The serverside-plugin:

import BaseTheming, { getModeFromCookies } from "./BaseTheming"

export default (ctx, inject) => {
  inject(
    'colorMode', new ServerSideTheming(ctx)
  );
}

class ServerSideTheming extends BaseTheming {
  ctx;
  constructor(ctx) {
    super(getModeFromCookies(ctx.req.headers?.cookie ?? ''), ctx.route.path);
    this.ctx = ctx;
    this.saveSettings();
  }

  get preference() {
    return this._activeTheme;
  }

  set preference(value) {
    this._activeTheme = value;
    this.saveSettings();
  }

  saveSettings() {
    this.ctx.app.head.bodyAttrs = this.ctx.app.head.bodyAttrs || {};
    this.ctx.app.head.bodyAttrs.class = this.value + '-mode';
  }
}

client-side plugin:

import Vue from 'vue';
import { defaultThemes, excludedRoutes } from './BaseTheming';

export default (ctx, inject) => {
  const colorMode = new Vue({
    data() {
      return {
        themes: defaultThemes,
        activeTheme: defaultThemes[0],
        excludedRoutes,
      };
    },
    computed: {
      isRouteExcluded() {
        return this.excludedRoutes.some((route) => ctx.route.path.includes(route));
      },
      preference: {
        get() {
          return this.activeTheme;
        },
        set(value) {
          const classList = document.body.classList;
          this.themes.forEach((theme) => {
            if (theme === value) {
              classList.add(theme + '-mode');
            } else {
              classList.remove(theme + '-mode');
            }
          });
          this.activeTheme = value;
          this.saveSettings();
        }
      },
      value() {
        return this.activeTheme;
      }
    },
    created() {
      if (this.isRouteExcluded) {
        this.activeTheme = defaultThemes[0];
      } else {
        this.parseBodyClassList();
      }
    },
    methods: {
      saveSettings() {
        if (!this.isRouteExcluded) {
          try {
            const now = new Date(Date.now());
            now.setDate(now.getDate() + 10000);
            const variable = `nuxt-color-mode=${this.value};`;
            document.cookie = `${variable}expires=${now.toUTCString()};path=/`;
          } catch (ex) {
            console.error('localstorage blocked', ex);
          }
        }
      },
      parseBodyClassList() {
        // can be removed by June 2023
        const themeFromPrefColorMode = localStorage.getItem('nuxt-color-mode');
        if (themeFromPrefColorMode) {
          this.preference = themeFromPrefColorMode;
          localStorage.removeItem('nuxt-color-mode');
        } else {
          document.body.classList.forEach((className) => {
            const theme = className.split('-')[0];
            if (defaultThemes.includes(theme)) {
              this.activeTheme = theme;
            }
          });
        }
      }
    }
  });

  // watching the body for changes, because something deletes the assigned classes
  const observer = new MutationObserver((mutationList) => {
    for (const mutation of mutationList) {
      if (mutation.attributeName === 'class' && document.body.classList.length === 0) {
        colorMode.preference = colorMode.value;
      }
    }
  });
  observer.observe(document.body, { attributes: true, attributeOldValue: true });

  inject('colorMode', colorMode);
};

the base-theming class for completeness:

export const defaultThemes = ['light', 'dark'];
export const excludedRoutes = ['embed', 'angebot/topic'];

export default class BaseTheming {
  themes = defaultThemes;
  _activeTheme = this.themes[0];
  excludedRoutes = ['embed', 'angebot/topic'];
  route = '';

  get isRouteExcluded() {
    return this.excludedRoutes.some((route) => this.route.includes(route));
  }

  constructor(preset, route) {
    this.route = route;
    if (this.isRouteExcluded) {
      this._activeTheme = defaultThemes[0];
    } else if (this.themes.includes(preset)) {
      this._activeTheme = preset;
    }
  }

  get value() {
    return this._activeTheme;
  }

  saveSettings() {
    throw new Error('saveSettings not implemented');
  }

  getSettings() {
    throw new Error('getSettings not implemented');
  }
}

export function getModeFromCookies(cookie) {
  const value = `; ${cookie}`;
  const parts = value.split(`; nuxt-color-mode=`);
  if (parts.length === 2) {
    const theme = parts.pop().split(';').shift();
    if (defaultThemes.includes(theme)) {
      return theme;
    }
  }
  return defaultThemes[0];
}
0

There are 0 best solutions below