-
Notifications
You must be signed in to change notification settings - Fork 8
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
Comments
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 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 |
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.
The text was updated successfully, but these errors were encountered: