Open laurencedorman opened 2 years ago
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>
Thank you so much for taking the time to sketch this out for me @thibaudcolas, I’ll get stuck in!
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.
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.
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.