storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
84.68k stars 9.32k forks source link

Vue decorators not reacting to toolbar in Canvas #12840

Open andywd7 opened 4 years ago

andywd7 commented 4 years ago

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

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

stale[bot] commented 3 years ago

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!

adesombergh commented 3 years ago

+1

shilman commented 3 years ago

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

bodograumann commented 3 years ago

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

cearny commented 3 years ago

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.

mevbg commented 3 years ago

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

william-will-angi commented 3 years ago

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

william-will-angi commented 3 years ago

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
      )
    }
  },
})
bokub commented 3 years ago

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];
wes0310 commented 2 years ago

Is there any workaround for React

markrian commented 2 years ago

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

vandor commented 2 years ago

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?

saaaaaaaaasha commented 1 year ago

Any work around for react?

LeCoupa commented 1 year ago

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
      }
    }
  },

  // ...
}
tillsanders commented 1 year ago

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

davidmnoll commented 1 year ago

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

marco-gagliardi commented 9 months ago

Still not working on Vue

bokub commented 9 months ago

@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

marco-gagliardi commented 9 months ago

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

bokub commented 9 months ago

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?

marco-gagliardi commented 9 months ago

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

christiancazu commented 9 months ago

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

JoaoHamerski commented 8 months ago

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
kalnode commented 1 month ago

I'm using Vue 3 + SB 8.2.9, and @LeCoupa 's solution works fine.

I've only tested theme switching, between component views, docs views and hard reloads. Everything seems to work. Did not try the language switcher though. Will make a note to scrutinize again in the future; perhaps one day the watcher isn't needed.