storybook icon indicating copy to clipboard operation
storybook copied to clipboard

Vue decorators not reacting to toolbar in Canvas

Open andywd7 opened this issue 4 years ago • 13 comments

Describe the bug I have setup a toolbar and global decorator for changing a class to show theme changes. When I select an option from the toolbar, in Canvas, the session storage changes, if I console.log it it changes but the class doesn't change. It does work in Docs tab and if I select a different story. I'm guessing because it re-renders the DOM.

My example scenario is switching a light and dark theme class in a decorator using toolbar and globals.

To Reproduce Example repo: https://github.com/andywd7/storybook Example demo: https://trusting-montalcini-282027.netlify.app

Expected behavior I would expect the class in the decorator to change.

System

Environment Info:

  System:
    OS: macOS 10.15.7
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  Binaries:
    Node: 14.4.0 - ~/.nvm/versions/node/v14.4.0/bin/node
    npm: 6.14.8 - ~/.nvm/versions/node/v14.4.0/bin/npm
  Browsers:
    Chrome: 86.0.4240.80
    Safari: 14.0
  npmPackages:
    @storybook/addon-a11y: ^6.0.26 => 6.0.26 
    @storybook/addon-actions: ^6.0.26 => 6.0.26 
    @storybook/addon-cssresources: ^6.0.26 => 6.0.26 
    @storybook/addon-essentials: ^6.0.26 => 6.0.26 
    @storybook/addon-links: ^6.0.26 => 6.0.26 
    @storybook/vue: ^6.0.26 => 6.0.26

andywd7 avatar Oct 20 '20 21:10 andywd7

@backbone87 we should figure out a way to trigger a re-render when globals change, similar to what we did for args.

shilman avatar Oct 22 '20 03:10 shilman

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

stale[bot] avatar Dec 25 '20 19:12 stale[bot]

+1

adesombergh avatar Feb 09 '21 14:02 adesombergh

@phated can you take a look at this in vue3?

shilman avatar Feb 15 '21 12:02 shilman

Duplicate: https://github.com/storybookjs/storybook/issues/13791 ?

bodograumann avatar Mar 02 '21 14:03 bodograumann

Hi, is this still being looked at for Vue2? I tried both 6.2.9 and the latest 6.3.0 pre-release and it seems to still occur.

cearny avatar May 10 '21 11:05 cearny

+1

Still occurs with v6.3.2, using Vue. I am unable to change locale with i18n, based on a global. Is this planned be fixed at last?

mevbg avatar Jul 01 '21 16:07 mevbg

+1 Still occurs in 6.3.8. Have to use deprecated addon-contexts package instead.

william-will-angi avatar Sep 17 '21 18:09 william-will-angi

For anyone who wants some spaghetti code that forces it to work in the latest storybook by manually creating dom listeners to update the state:

// These should line up with the toolbar->items config in globalTypes
const themes = ['theme1', 'theme2', 'theme3', 'theme4']

const themeProvider = (story, context) => ({
  components: { story },
  data() {
    return { theme: context.globals.theme, listenerElement: null }
  },
  // You have a reactive "theme" data object that you can reference in your template
  template: `<div :data-theme="theme" class="@m-2"><story /></div>`,
  // Functionality added below is for facilitating changes to the global theme variable
  methods: {
    handleThemeButtonClick() {
      // Window of Story is encapsulated within canvas
      if (window && window.parent) {
        // Give some buffer time so that the storybook has time to re-render the state change after clicking the toolbar button
        setTimeout(() => {
          themes.forEach((themeId) => {
            window.parent.document
              .getElementById(themeId)
              .addEventListener('click', () => {
                this.theme = themeId
              })
          })
        }, 100)
      }
    },
  },
  mounted() {
    setTimeout(() => {
      // Window of Story is encapsulated within canvas
      if (window && window.parent) {
        const themeButton = window.parent.document.querySelector(
        // title value corresponds to description in globalTypes config
          '[title="Global theme for components"]'
        )
        this.listenerElement = themeButton
        themeButton.addEventListener('click', this.handleThemeButtonClick)
      }
    }, 100)
  },
  beforeDestroy() {
    if (this.listenerElement) {
      this.listenerElement.removeEventListener(
        'click',
        this.handleThemeButtonClick
      )
    }
  },
})

william-will-angi avatar Oct 06 '21 22:10 william-will-angi

For anyone facing this issue with Vue 2, here is how I solved it:

// .storybook/preview.js

import { i18n } from '@/plugins'; // the i18n variable contains a new VueI18n({...})

// Create a locale global and add it to toolbar
export const globalTypes = {
  locale: {
    name: 'Locale',
    description: 'Internationalization locale',
    defaultValue: 'en',
    toolbar: {
      icon: 'globe',
      items: [
        { value: 'en', right: '🇺🇸', title: 'English' },
        { value: 'fr', right: '🇫🇷', title: 'Français' },
      ],
    },
  },
};

// IMPORTANT PART: This creates an observable variable that can be watched by vue
const locale = Vue.observable({ value: null })

// This is a storybook decorator
const withLocale= (story, context) => {

  locale.value = context.globals.locale // Assign Storybook global to Observable variable

  return {
    i18n, // <= Add i18n to the decorator
    template: '<story />',
    created() {
      this.$watch(() => {
        // This function will be called whenever the storybook global changes
        // In my case, I just want to update the i18n locale 
        this.$i18n.locale = locale.value
      })
    }
  }
}

export const decorators = [withLocale];

bokub avatar Oct 28 '21 09:10 bokub

Is there any workaround for React

wes0310 avatar Dec 30 '21 03:12 wes0310

@backbone87 we should figure out a way to trigger a re-render when globals change, similar to what we did for args.

@shilman I'm finding that decorators are passed stale args (not globals) when you use change the story's controls. Your comment suggests that shouldn't be the case. Can you point me towards a PR that might shed some light on this?

In particular, "what we did for args": what did you do, and what did it fix/change?

markrian avatar Jan 24 '22 22:01 markrian

I'm finding that decorators are passed stale args (not globals) when you use change the story's controls.

This is what brought me here as well. I'm trying to learn whether storybook currently supports a way for decorators to receive updated control (args) values when the args are changed via the storybook controls UI. Is this documented somewhere?

vandor avatar Jun 29 '22 15:06 vandor

Any work around for react?

saaaaaaaaasha avatar May 07 '23 13:05 saaaaaaaaasha

For people using Vue 3

import { ref, watch } from "vue";

const scheme = ref('light')

const withColorScheme: Decorator = (Story, context) => {
  watch(
    () => context.globals.scheme,
    (newScheme) => scheme.value = newScheme,
    { immediate: true }
  )

  return {
    components: { Story },

    setup: () => ({ scheme }),

    template: `
      <div v-if="['both', 'light'].includes(scheme)">
        <story />
      </div>

      <div v-if="['both', 'dark'].includes(scheme)" class="dark">
        <story />
      </div>
    `
  }
}

const withLocale: Decorator = (Story, context) => {
  watch(
    () => context.globals.locale,
    (newLocale) => i18n.global.locale = newLocale,
    { immediate: true }
  )

  return {
    components: { Story },

    template: `<story />`
  }
}

const preview: Preview = {
  decorators: [withColorScheme, withLocale],
  globalTypes: {
    locale: {
      description: "Internationalization locale",
      defaultValue: "en",
      toolbar: {
        icon: "globe",
        items: [
          { value: "en", left: "🇺🇸", title: "English" },
          { value: "fr", left: "🇫🇷", title: "Français" },
        ],
        dynamicTitle: true
      },
    },
    scheme: {
      name: "Scheme",
      description: "Select light or dark mode",
      defaultValue: "Light",
      toolbar: {
        icon: "mirror",
        items: [
          { value: "both", left: "🌗", title: "Both" },
          { value: "dark", left: "🌚", title: "Dark" },
          { value: "light", left: "🌝", title: "Light" },
        ],
        dynamicTitle: true
      }
    }
  },

  // ...
}

LeCoupa avatar Jun 20 '23 13:06 LeCoupa

@LeCoupa Worked for me, thank you! What is nice about this solution is it actually works for both single stories and Autodocs. It also helped me isolate my styles from storybook's styles, so they don't interfere that much.

However, I feel like this kind of functionality should be provided by the Toggles add-on instead of workarounds like this.

tillsanders avatar Jun 30 '23 10:06 tillsanders

Is this still an issue? anyone have a solution for react?

davidmnoll avatar Aug 22 '23 23:08 davidmnoll

Still not working on Vue

marco-gagliardi avatar Feb 07 '24 08:02 marco-gagliardi

@marco-gagliardi Just scroll up and you'll see how to make it work for Vue 2 and Vue 3 !

Your comment adds no value to this thread

bokub avatar Feb 07 '24 10:02 bokub

@bokub will I see a workaround or a final solution?

marco-gagliardi avatar Feb 07 '24 11:02 marco-gagliardi

The issue would not be "open" if the problem was resolved by a long-term solution

That's why commenting "still not working" is pointless. We already know that it's "not working", we gave you workarounds, and you still think it's a good idea to spam the issue with complaints that add no value to the conversation?

bokub avatar Feb 07 '24 12:02 bokub

It could have been defined spam or complaints if a solution plan was shared, otherwise the correct name is upvoting, and yes, it is generally a good idea to be sure an issue doesn't get stale (other people did the same). Cheers

marco-gagliardi avatar Feb 07 '24 13:02 marco-gagliardi

For people using Vue 3

import { ref, watch } from "vue";

const scheme = ref('light')

const withColorScheme: Decorator = (Story, context) => {
  watch(
    () => context.globals.scheme,
    (newScheme) => scheme.value = newScheme,
    { immediate: true }
  )

  return {
    components: { Story },

    setup: () => ({ scheme }),

    template: `
      <div v-if="['both', 'light'].includes(scheme)">
        <story />
      </div>

      <div v-if="['both', 'dark'].includes(scheme)" class="dark">
        <story />
      </div>
    `
  }
}

const withLocale: Decorator = (Story, context) => {
  watch(
    () => context.globals.locale,
    (newLocale) => i18n.global.locale = newLocale,
    { immediate: true }
  )

  return {
    components: { Story },

    template: `<story />`
  }
}

const preview: Preview = {
  decorators: [withColorScheme, withLocale],
  globalTypes: {
    locale: {
      description: "Internationalization locale",
      defaultValue: "en",
      toolbar: {
        icon: "globe",
        items: [
          { value: "en", left: "🇺🇸", title: "English" },
          { value: "fr", left: "🇫🇷", title: "Français" },
        ],
        dynamicTitle: true
      },
    },
    scheme: {
      name: "Scheme",
      description: "Select light or dark mode",
      defaultValue: "Light",
      toolbar: {
        icon: "mirror",
        items: [
          { value: "both", left: "🌗", title: "Both" },
          { value: "dark", left: "🌚", title: "Dark" },
          { value: "light", left: "🌝", title: "Light" },
        ],
        dynamicTitle: true
      }
    }
  },

  // ...
}

it works but only in the current component

if I change to another vue storybook component and go back to the previous one the language change is no longer reactive

i mean: i18n.global.locale.value is now inmutable

christiancazu avatar Feb 07 '24 23:02 christiancazu

You don't really need to watch for changes like the example above. But, for some reason, you need to declare a ref var outside the decorator method. So, this will work:

import { Decorator } from '@storybook/vue3'
import { ref } from 'vue'

const theme = ref('light')

const withThemeDecorator: Decorator = (Story, context) => {
  theme.value = context.globals.theme

  return {
    components: { Story },
    setup: () => ({ theme }),
    template: `
        {{ theme }}
      `
  }
}

export default withThemeDecorator

JoaoHamerski avatar Mar 12 '24 03:03 JoaoHamerski