How to use Framework7-React in Storybook?

There’s no Framework7 wrapper for Storybook like material ui for storybook. So, I try to create a simple Framework7 wrapper. So far this is my code :

Part of ./storybook/config.js

import f7 from './f7-react-sb/f7';
        
addDecorator(f7());

Here is f7.js

import React from 'react';
import F7Wrapper from './F7Wrapper';

import 'framework7/css/framework7.min.css';
import Framework7 from 'framework7';
import Framework7React from 'framework7-react';

Framework7.use(Framework7React);

const f7 = () => {
  return getStory => {
    const story = getStory();

    return <F7Wrapper>{story}</F7Wrapper>;
  };
};

export default f7;

And F7Wrapper.js

import React from 'react';
import { App, Statusbar, View } from 'framework7-react';

export default ({ config, children }) => (
  <App params={config}>
    <Statusbar />

    <View>{children}</View>
  </App>
);

Its working properly with the first stories. But when I select another stories from left panel, Its return error and say

Framework7 is already initialized and can’t be initialized more than once

Is Am I missing something?

A decorator is going to wrap every component. Since you’ve defined the application startup in a decorator, it gets called multiple times and you get the error you’re seeing.

I think you need to call addParameters in your config. It’s not well documented on the Storybook side. F7 documents their app initialization process here, which is useful. And Storybook has a React example where they utilize addParameters. (I wish they had API documentation on the @storybook/vue and @storybook/react modules.)

I’m using F7/Vue/Storybook and I ended up with a config like this:

// Docs on configuration:
// https://github.com/storybookjs/storybook/blob/next/docs/src/pages/guides/guide-vue/index.md
// https://github.com/storybookjs/storybook/blob/17dcfb84194c9876d4452aae123443f9748f62b6/examples/vue-kitchen-sink/.storybook/config.js
//
// =========================================
// Added configurations for our dependencies
// =========================================

// Import Framework7
import Framework7 from 'framework7/framework7-lite.esm.bundle.js'

// Import Framework7-Vue Plugin
import Framework7Vue from 'framework7-vue/framework7-vue.esm.bundle.js'

// Import Framework7 Styles
import 'framework7/css/framework7.bundle.css'

// Import Icons and App Custom Styles
import '../src/css/icons.css'
import '../src/css/app.scss'

// Init Framework7-Vue Plugin
// NOTE: this does not involve Vue directly at all, so some other configuration must be needed...
// unless F7.use() is calling Vue.use() as a side effect?
Framework7.use(Framework7Vue)

// =========================================
// Default configuration for storybook
// =========================================
import {configure, addParameters} from '@storybook/vue'

// addParameters is passing parameters to the app root which is a F7 component... but what registered the F7 components into Vue in the first place?
addParameters({
  f7params: {
    name: 'my-client', // App name
    theme: 'auto', // Automatic theme detection
    // App root data
    data: function() {
      return {}
    },

    // App routes
    //routes: routes,
  },
})

// automatically import all files ending in *.stories.js
configure(require.context('../src/stories', true, /\.stories\.js$/), module)

Note that on the F7 side I still have confusion around the loading process, as noted above. At what point does Vue.use actually get called to register the F7 components? Is that happening as a side effect of F7.use()? That seems pretty magical and nonintuitive if it’s the case. Can anyone from F7 comment?

Scratch that, I see what’s happening. You’re right to use the App decorator. The problem is that Framework7 calls F7.init() when the App component is mounted (same for React). So you’re in a pickle - you have to decorate your stories with this in order for them to be styled properly and have the right this properties on them, but it blows up when you render it multiple times.

What to do? I just imported/extended the App component and overrode the mounted method with my own that implements a singleton pattern.

import app from 'framework7-vue/components/app'
import f7 from 'framework7-vue/utils/f7'

let hasRun = false
export default Object.assign(app, {
  mounted() {
    const self = this
    const {params = {}, routes} = self.props
    const el = self.$refs.el
    const parentEl = el.parentNode

    if (parentEl && parentEl !== document.body && parentEl.parentNode === document.body) {
      parentEl.style.height = '100%'
    }

    // added this code to protect with singleton and avoid re-initialization
    if (!hasRun) {
      hasRun = true
      f7.init(el, params, routes)
    }
  },
})

Again, the same pattern should work for React.

F7 developers: Are there any contexts in which it would not be ok to do this? It would be nice to have builtin compatibility with an outside rendering or testing library.

Only with F7 update where mounted can be potentially modified. I guess, it is easier to add similar check to the f7 itself and don’t init it again in this case

Yes, that was my thought. If F7 had this check it would work without issue in environments like Storybook, Jest, Mocha, etc.

Edit: or even better if the init() method itself checked to see if it has already been run and spit out a warning and return instead of blowing up. Then it would not need to be reimplemented in every UI framework plugin.

1 Like