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

Using window.store when creating a story #6726

Closed
chopfitzroy opened this issue May 6, 2019 · 21 comments
Closed

Using window.store when creating a story #6726

chopfitzroy opened this issue May 6, 2019 · 21 comments

Comments

@chopfitzroy
Copy link

Describe the bug
When creating a story I am trying to use the window.store as the store (I include a script in preview-head.html which sets up window.store so it does exist in the iframe).

Basically I am working on a site that uses decoupled vue components built as libraries which communicate data through a global vuex store on the window, unfortunately this prevents mapActions working with storybook because the context of this.$store is wrong.

I have tried the following based of this issue and also this post:

storiesOf("Some component", module)
    .add("using global store", () => ({
        components: {
            ModuleName
        },
        template: "<module-name />",
        store: window.store
    }));

Unfortunately the context of window is wrong at this point, I also tried setting up the store in the config file with Vue.use(Vuex) but because I register the modules dynamically I couldn't get this to work either as I register them against window.store when the module is loaded with the following script:

export default async (identifier, storeModule) => {
    const store = window.store;
    const exists = store.state[identifier];
    if (!window.components.registeredModules.includes(identifier)) {
        storeModule.namespaced = true;
        store.registerModule(identifier, storeModule, {
            preserveState: exists
        });
        window.components.registeredModules.push(identifier);
    }
};

To Reproduce
Steps to reproduce the behavior:

  1. Try to use store: window.store when creating a story.
  2. Serve storybook.
  3. See context is wrong.

Expected behavior
To be able to reference objects on the iframe window.

System:

  • OS: Ubuntu 18.10
  • Device: Desktop
  • Browser: Chrome
  • Framework: Vue
  • Version: 5.0.11

Additional context
I am building the decoupled components as outlined here.

@shilman shilman added this to the 5.0.x milestone May 9, 2019
@github-actions
Copy link
Contributor

github-actions bot commented May 9, 2019

Automention: Hey @elevatebart @pksunkara, you've been tagged! Can you give a hand here?

@chopfitzroy
Copy link
Author

So I have been continuing work on this, and I am not sure it is a context issue so much as a timing issue.

It looks like store: window.store does work, however due to the order in which the component is mounted and the external script is loaded into the iframe the components do not have the correct reference to the store but it is actually there.

Could this be correct?

@shilman
Copy link
Member

shilman commented May 17, 2019

@CrashyBang could you test that theory with a setTimeout?

@chopfitzroy
Copy link
Author

chopfitzroy commented May 17, 2019

Hey @shilman,

Okay that is sort of working (5000ms delay), the notification system which is also on the window now works, however the store is still having issues:

Uncaught (in promise) TypeError: Cannot read property 'state' of undefined

This is coming from mapState this would lead me to believe that this.$store is not referenced correctly during the components render. I am trying to reference it like so:

storiesOf("Event List", module).add(
  "with valid data",
  () => ({
    components: {
      EventList
    },
    store: window.store,
    data: () => {
      return { initalEnabled: true };
    },
    template: `<event-list :initial-enabled="initalEnabled" />`
  })
);

NOTE: In the storybook interface window.store can be logged in the console, as well as if I open up the iframe directly (on port 8081).

NOTE: I will also need to fix the issue regarding the window properties not being accessible without a setTimeout, currently I have a mixin that forces the components to wait (via a Promise) until the window is ready before rendering but because they appear to being rendered instantly (without the delay) they don't seem to correctly reference the promise.

@chopfitzroy
Copy link
Author

Hey @shilman,

I keep coming back to this when I have time so sorry for the sporadic posting, I tried to extend on the setTimeout idea by delaying the storiesOf function like so:

setTimeout(() => {
    storiesOf("Event List", module).add("with valid data", () => {
        return {
            components: {
                EventList
            },
            store: window.store,
            data: () => {
                return {
                    initalEnabled: true
                };
            },
            template: `<event-list :initial-enabled="initalEnabled" />`
        }
    });
}, 0);

But this just gave me the following:

2019-05-21_15-57

In an ideal world I would actually like to do something like this:

(async (ready) => {
    await ready;
    storiesOf("Event List", module).add("with valid data", async () => {
        return {
            components: {
                EventList
            },
            store: window.store,
            data: () => {
                return {
                    initalEnabled: true
                };
            },
            template: `<event-list :initial-enabled="initalEnabled" />`
        }
    });
})(window.somePromise);

Where window.somePromise is a Promise that resolves once all the assets provided by the script in preview-head.html have loaded.

I also tried messing around with adding a mixin to the global Vue instance storybook uses, (in config.js):

import Vue from 'vue';
Vue.mixin({
    data() {
        return {
            ready: false
        }
    },
    async created() {
        await window.components.resourcesLoaded;
        this.ready = true;
    }
});

And then adding v-if="ready" to the template string when setting up the story, this did not work either even though the ready value appeared to be added to the storybook Vue instance.

Cheers.

@shilman shilman modified the milestones: 5.0.x, 5.1.x Jun 5, 2019
@stale stale bot added the inactive label Jun 26, 2019
@chopfitzroy
Copy link
Author

Still working on this, just having trouble finding the time.

@stale stale bot removed the inactive label Jun 26, 2019
@chopfitzroy
Copy link
Author

Okay so I had a bit more of a play with this today.

Basically I tried modifying node_modules/@storybook/core/src/server/templates/index.ejs so that the script insertions included the defer attribute (I'm honestly just messing around at this point to see if I can come up with anything).

So the new code looked like this:

<% dlls.forEach(file => { %>
  <script src="<%= file %>" defer></script>
<% }); %>

<% files.js.forEach(file => { %>
  <script src="<%= file %>" defer></script>
<% }); %>

I also added defer to the script in my preview-head.html.

The issue was when I tried to re-build my storybook the new defer attributes are not present on the injected scripts in the iframe, I mean I may be updating the wrong file...?

Let me know what you think, at this point I am just trying to isolate the problem a fix can be made from there and hopefully in such a way that it can go up stream.

Cheers.

@chopfitzroy
Copy link
Author

Okay making more progress here,

I need to be modifying node_modules/@storybook/core/dist/server/templates/index.ejs (Obviously still messing around) and it appears to be somewhat working, so I moved:

<% dlls.forEach(file => { %>
    <script src="<%= file %>" defer></script>
<% }); %>

<% files.js.forEach(file => { %>
    <script src="<%= file %>" defer></script>
<% }); %>

Into the <head> and added defer, I am not sure if you would want to merge this up stream but it does mean the <script>'s in preview-head.html are guaranteed to load before the bundles (provided defer is added) and I would assume that you almost always want to load this code first...?

This meant that I could await the promise returned by my initialize script before rendering any components. I did this like so:

(async ready => {
  await ready;
  storiesOf("Event List", module).add(
    "with valid data",
    () => {
      return {
        components: {
          EventList
        },
        data: () => {
          return {
            initalEnabled: true
          };
        },
        template: `<event-list :initial-enabled="initalEnabled" />`
      };
    }
  );
})(window.components.resourcesLoaded);

Now this worked, but it could not read this.$store on the components so next I had to add store: window.store to the story and:

import Vue from 'vue';
import Vuex from 'vuex'

Vue.use(Vuex);

To the storybook config.js it can now find the store but is having some issues registering store modules, which I am investigating now, I am interested in how I can get this merged up stream to help anyone else with a more decoupled setup.

@chopfitzroy
Copy link
Author

Okay working further still I now have the following issue rendering components, if I try to render like this:

storiesOf("Event List", module).add(
  "with valid data",
  () => ({
      components: {
        EventList
      },
      store: window.store,
      data: () => {
        return {
          initalEnabled: true
        };
      },
      template: `<event-list :initial-enabled="initalEnabled" />`
    })
);

Nothing works given that nothing has loaded, however if I try to render like this:

(async function initStories(ready) {
  await ready;
  const response = storiesOf("Event List", module).add(
    "with valid data",
    () => ({
        components: {
          EventList
        },
        store: window.store,
        data: () => {
          return {
            initalEnabled: true
          };
        },
        template: `<event-list :initial-enabled="initalEnabled" />`
      })
  );
  console.log(response);
})(window.components.resourcesLoaded);

Nothing renders either but the response does show a valid story instance, it looked like it was working before becuase it would render the component the first time I ran npm run storybook:serve but would stop rendering after a refresh. It does not render at all when using npm run storybook:build I am really not sure what this is at this point.

@chopfitzroy
Copy link
Author

chopfitzroy commented Jun 30, 2019

After a bit more research I believe I need something like the proposed API in #713 but from what I can tell this was never implemented.

I also see there is some conversation happening in #6885 potentially we will see something happen here.

UPDATE: Original PR to add this functionality was #1253 but it was never merged.

@backbone87
Copy link
Contributor

how does your component access the store in an app (not storybook)? also via window.store or via this.$store (and store is injected during component mount: new Vue({ store: window.store, /* ... */ }).mount('#target');)

@backbone87
Copy link
Contributor

backbone87 commented Jul 9, 2019

you could try the following decorator:

function waitForStore() {
  return () => ({
    data() {
      return { store: null };
    },
    async created() {
      this.store = await GET_WINDOW_STORE();
    },
    render(h) {
      if(!this.store) {
        return h('span', 'Waiting for store...');
      }

      return h('story', { store: this.store }); // this may not work, then try the following
      return h({ components: { story: this.$options.components.story }, template: '<story></story>', store: this.store });
    }
  })
}

storiesOf(...).addDecorator(waitForStore()).add(...);

@chopfitzroy
Copy link
Author

Hey @backbone87,

Components access the store via this.$store and yes when I mount them I essentially do it how you have described with window.store.

I will try the suggested decorator as soon as possible! Thanks!

@storybookjs storybookjs deleted a comment from stale bot Jul 13, 2019
@ndelangen ndelangen removed this from the 5.1.x milestone Jul 13, 2019
@chopfitzroy
Copy link
Author

chopfitzroy commented Jul 16, 2019

Hey @backbone87,

That works! Thank you so much for that! The only issue I have now is setting up the store on the components I am using in the stories, when loading them on the front end I have a function that sets them up with the store (I know the whole way we are doing this is weird but it fits the use case) like so:

initializeComponent = function (config) {
    return new Promise(async function (resolve, reject) {
        // @NOTE we have to wait for this to resolve before loading any components
        // - This is because some components (bootstrap-vue) require the global "Vue"
        await components.resourcesLoaded;
        const component = await loadComponent(config.url);
        const props = config.props || {};
        new Vue({
            store: window.store,
            i18n: window.components.i18n,
            render: function (h) {
                // @NOTE props will not be reactive
                // - https://forum.vuejs.org/t/passing-props-as-object-during-render-should-they-be-reactive/62271
                return h(component, {
                    props: props
                });
            }
        }).$mount(config.element);
        return resolve();
    });
};

So that is how all the components we load in get their reference to window.store whereas when loading the component like this:

import EventList from "@/components/EventList.vue";

storiesOf("Event List", module)
  .addDecorator(waitForStore())
  .add("with valid data", () => {
    return {
      components: {
        EventList
      },
      data: () => {
        return {
          initalEnabled: true
        };
      },
      template: `<event-list :initial-enabled="initalEnabled" />`
    };
  });

The EventList component has no reference to this.$store you wouldn't happen to know how I can achieve this? No worries if I need to start digging deeper on this one, my original issue has been solved, thanks a million!

@backbone87
Copy link
Contributor

so you can access the store in the story via this.$store, but not in your component? that is strange. but inside your component this.$parent.$store works?

@chopfitzroy
Copy link
Author

chopfitzroy commented Jul 22, 2019

Hey @backbone87,

So this is an issue with my setup, basically I load each component on demand and create the Vue instance as I need it, hence the initializeComponent function.

As you can see as I load each component I pass it a reference to the store at the time:

new Vue({
    store: window.store,
    i18n: window.components.i18n,
    render: function (h) {
        // @NOTE props will not be reactive
        // - https://forum.vuejs.org/t/passing-props-as-object-during-render-should-they-be-reactive/62271
        return h(component, {
            props: props
        });
    }
}).$mount(config.element);

Basically there is no "global" Vue instance in my project so each component gets handed the store reference as it is instantiated, unfortunately I am not sure how to do this via storybook, I essentially need to pass the EventList component the store reference when storybook renders it but I am not sure how, or if it is even possible?

EDIT: And no this.$store and this.$store.$parent both appear to be undefined.

@chopfitzroy
Copy link
Author

chopfitzroy commented Jul 22, 2019

From what I can tell, I would need to add the store here but from what I can see there are no hooks that would enable me to do this, I also doubt this is somewhere that the developers would like to add hooks as it would be pretty easy for users to mess things up.

@stale
Copy link

stale bot commented Aug 25, 2019

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 stale bot added the inactive label Aug 25, 2019
@chopfitzroy
Copy link
Author

Still working on this one, what I am encountering now though potentially warrants a new issue, so I will close this for now and revisit when I have more time.

@anipendakur
Copy link

Is there another issue to keep track of this? Since this has been closed already?

@chopfitzroy
Copy link
Author

Hey @anipendakur,

No there is currently no new issue as I have not had time to revisit this, feel free to open a new issue and reference this one.

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants