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

Usage with Vue #7

Open
laurencedorman opened this issue Jan 13, 2022 · 4 comments
Open

Usage with Vue #7

laurencedorman opened this issue Jan 13, 2022 · 4 comments
Labels
enhancement New feature or request

Comments

@laurencedorman
Copy link

How tightly coupled is this with React ? I'm reading through the code but I'm having trouble seeing what would need to be done to get it working with Vue.

@laurencedorman laurencedorman added the question Further information is requested label Jan 13, 2022
@thibaudcolas
Copy link
Member

thibaudcolas commented Jan 13, 2022

Hey @laurencedorman, not very much.

The current implementation technically is coupled, but the current implementation is very hacky, and could easily be refactored to be framework-agnostic. Here is where most of the magic happens: https://github.com/torchbox/storybook-django/blob/main/src/TemplatePattern.js#L30-L129

This is technically a React component, but we only use it to render a <div>, store a bit of state – and the bulk of the logic is built with vanilla JS executed imperatively in the relevant React lifecycle phases.

Here is a PoC of a Vue version. Note I don’t do Vue very often myself so this likely isn’t correct. In particular, we’d want the API call to be made on every render of the component, not just on mount.

<template>
  <component ref="elt" :is="computedTag"></component>
</template>
<script lang="ts">
import type { PropType } from 'vue'
import Vue from 'vue'

/**
 * Inserts HTML into an element, executing embedded script tags.
 * @param {Element} element
 * @param {string} html
 */
const insertHTMLWithScripts = (element, html) => {
    element.innerHTML = html;

    Array.from(element.querySelectorAll('script')).forEach((script) => {
        const newScript = document.createElement('script');
        Array.from(script.attributes).forEach((attr) =>
            newScript.setAttribute(attr.name, attr.value),
        );

        newScript.appendChild(document.createTextNode(script.innerHTML));
        script.parentNode.replaceChild(newScript, script);
    });
};

export default Vue.extend({
  name: "TemplatePattern",
  props: {
    element: {
      type: String,
      required: true,
    },
    apiPath: {
      type: String,
      required: true,
    },
    template: {
      type: String,
      required: true,
    },
    context: {
      type: Object,
      required: true,
    },
    tags: {
      type: Object,
      required: true,
    },
  },
  data(): {
    error: Error | null;
  } {
    return {
      error: null,
    };
  },
  computed: {
    computedTag(): string {
      return this.element || 'div'
    },
  },
  mounted() {
    //   TODO Should be called whenever the component re-renders, not just on mount.
    this.getRenderedPattern();
  },
  methods: {
    async getRenderedPattern(): Promise<void> {
      const url = this.apiPath || window.PATTERN_LIBRARY_API;
      let template_name = window.PATTERN_LIBRARY_TEMPLATE_DIR
        ? this.template
            .replace(window.PATTERN_LIBRARY_TEMPLATE_DIR, ";;;")
            .replace(/^.*;;;/, "")
        : this.template;
      template_name = template_name.replace(".stories.js", ".html");

      window
        .fetch(url, {
          method: "POST",
          mode: "same-origin",
          cache: "no-cache",
          credentials: "omit",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            template_name,
            config: {
              context,
              tags,
            },
          }),
        })
        .catch(() => {
          if (this.$refs.elt) {
            insertHTMLWithScripts(this.$refs.elt, "Network error");
          }
        })
        .then((res) => {
          if (res.ok) {
            setError(null);

            return res.text();
          }

          return res.text().then((serverError) => {
            let errName = serverError.split("\n")[0];
            let stack = serverError;

            if (serverError.includes("TemplateSyntaxError")) {
              try {
                let templateError;
                templateError = serverError.split("Template error:")[1];
                templateError = templateError.split("Traceback:")[0];
                templateError = templateError
                  .split("\n")
                  .filter((l) => l.startsWith("   "))
                  .map((l) => l.replace(/^\s\s\s/, ""))
                  .join("\n");

                const errCleanup = document.createElement("div");
                errCleanup.innerHTML = templateError;
                stack = errCleanup.innerText;

                let location = serverError
                  .split("\n")
                  .find((l) => l.startsWith("In template"));
                errName = `TemplateSyntaxError ${location ? location : ""}`;
              } catch {}
            }

            const error = new Error(errName);
            error.stack = stack;

            setError(error);

            return "Server error";
          });
        })
        .then((html) => {
          if (this.$refs.elt) {
            insertHTMLWithScripts(this.$refs.elt, html);
            window.document.dispatchEvent(
              new Event("DOMContentLoaded", {
                bubbles: true,
                cancelable: true,
              })
            );
          }
        });
    },
  },
});
</script>

@laurencedorman
Copy link
Author

Thank you so much for taking the time to sketch this out for me @thibaudcolas, I’ll get stuck in!

@thibaudcolas
Copy link
Member

Here is an up-to-date POC implementation based on the latest release of storybook-django, which includes framework-agnostic APIs:

<template>
  <component ref="elt" :is="computedTag"></component>
</template>
<script lang="ts">
import type { PropType } from 'vue'
import Vue from 'vue'
import { renderPattern, simulateLoading } from 'storybook-django';

const getTemplateName = (template?: string, filename?: string): string =>
  template ||
  filename?.replace(/.+\/templates\//, '').replace(/\.stories\..+$/, '.html') ||
  'template-not-found';

export default Vue.extend({
  name: "TemplatePattern",
  props: {
    element: {
      type: String,
      required: true,
    },
    template: {
      type: String,
      required: false,
    },
    filename: {
      type: String,
      required: false,
    },
    context: {
      type: Object,
      required: true,
    },
    tags: {
      type: Object,
      required: true,
    },
  },
  data(): {
    error: Error | null;
  } {
    return {
      error: null,
    };
  },
  computed: {
    computedTag(): string {
      return this.element || 'div'
    },
  },
  mounted() {
    //   TODO Should be called whenever the component re-renders, not just on mount.
    this.getRenderedPattern();
  },
  methods: {
    async getRenderedPattern(): Promise<void> {
      const templateName = getTemplateName(this.template, this.filename);

      renderPattern(window.PATTERN_LIBRARY_API, template_name, this.context, this.tags)
        .catch((err) => simulateLoading(this.$refs.elt, err))
        .then(res => res.text())
        .then((html) => simulateLoading(this.$refs.elt, html));
    },
  },
});
</script>

I believe it should be possible to add support for Vue directly in storybook-django – will give this a go in a future release.

@thibaudcolas thibaudcolas added enhancement New feature or request and removed question Further information is requested labels Apr 12, 2022
@thibaudcolas
Copy link
Member

thibaudcolas commented Apr 12, 2022

I have released a new version of the project, with framework-agnostic APIs (documented in the README), and an optional React component. It seems possible to also add support for Vue 3 in a similar way. Here is the reference React implementation:

https://github.com/torchbox/storybook-django/blob/main/src/react.js

The only thing that is still coupled with the React implementation is data- attributes that are only needed to support automated tests. They’d be pretty straightforward to re-implement in a Vue version.

woodcoder added a commit to woodcoder/storybook-django that referenced this issue Apr 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants