Using VueJs in Sitecore and Asp.Net MVC razor pages (Part 2)
A few months ago I created a prototype to explore using VueJs in our existing Sitecore application. Unfortunately JSS was not an option for us. I didn't take the prototype further as the project direction shifted but recently I've needed to revisit the approach. In this post I will take the previous prototype much further and discuss in more detail the options available.
The problem
You'd think it would be fairly straightforward to use VueJs and Sitecore together. However the complexity is in splitting the template from the component definition. To render fields from Sitecore (and to enable page editor), we need to have our markup in .cshtml files which means the template cannot live in a .vue file along with the Javascript code.
Possible solutions
Caveat - As per the last post this is exploratory code, mainly because I couldn't find much help online to solve this problem. The solutions outlined here though are much more complete than the previous post which I received quite a few questions about - so hopefully more to get started with now!
Note: I have updated to Typescript in these examples and used the webpack config from https://github.com/microsoft/TypeScript-Vue-Starter. I will Provide links to my github with all the code from these examples at the end of the post.
Option 1 - X-Templates
The previous post focused on X-Templates as a solution to the problem as these give us separation of markup and component code. https://vuejs.org/v2/guide/components-edge-cases.html#X-Templates
The big problem with the X-Template solution from the previous post is that if you have multiple of the same component on a page the first variant will be rendered for each instance. (As both have the same Id).
Here is a solution to that:
app.ts
/* Components */
import HelloWorld from "./Components/HelloWorld/hello-world";
new Vue({
el: `#app`,
components: {
HelloWorld
}
});
HelloWorld.ts
import Vue, { VNode } from "vue";
import { Component, Mixins, Prop } from "vue-property-decorator";
@Component({
name: "hello-world"
})
export default class HelloWorld extends Vue
{
@Prop({default: "Example"})
testprop!: string;
mounted = () => {
console.log("Props: " + this.testprop);
}
data = () =>
{
return {
message: "Hello from component data!"
};
}
methods = {
testClick: () => { console.log("I have been clicked!"); }
};
template: string = "#hello-world-template";
static renderIndex: number = -1;
render(createElement): VNode {
HelloWorld.renderIndex += 1;
this.template = `#hello-world-template_${HelloWorld.renderIndex}`;
return createElement(this);
}
}
HelloWorld.cshtml
@model MyNamespace.MyModel
<hello-world testprop="This is a message from props"></hello-world>
<!-- RenderIndex needs to come from your c# controller -->
<script type="text/x-template" id="hello-world-template_@Model.RenderIndex">
<div>
<h1>@Sitecore.FieldFor(m => m.MyField)</h1>
<div @@click="testClick()" class="container">
{{ message }}
</div>
</div>
</script>
Layout.cshtml
...
<body>
<div class="main" id="app">
@Html.Sitecore().Placeholder("full-screen")
</div>
<script type="text/javascript" src="~/Scripts/bundle.js"></script>
</body>
...
There is one last problem to this approach. The x-template definition needs to live after the closing tag of the Vue app i.e <div id="app">
or you will get the following error: Property or method "message" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property
(If you want to know why this happens: https://stackoverflow.com/questions/49193378/why-does-position-of-x-template-section-matter-for-vue-templates-to-work-with-da)
You could easily use a simple html helper to poke the x-template script within each cshtml file to the end of the layout page.
Option 2 - Using slots
Interestingly a buddy of mine, Chris Howson, also had this problem to solve and uses a clever approach utilising slots (https://vuejs.org/v2/guide/components-slots.html#Scoped-Slots) to solve the problem. I'll link to Chris' blog post about this once he's had time to create it.
This is arguably a cleaner solution than the x-template approach. The only downsides are the extra code you need to write in render and some people might argue using slots in this way is a little strange. For me, it's a great solution.
app.ts
/* Components */
import HelloWorld from "./Components/HelloWorld/hello-world";
new Vue({
el: `#app`,
components: {
HelloWorld
}
});
HelloWorld.ts
import Vue from "vue"
import { Component, Mixins, Prop } from "vue-property-decorator";
@Component({
name: "hello-world"
})
export default class HelloWorld extends Vue
{
@Prop({default: "Example"})
testprop: string;
mounted() {
console.log("Props: " + this.testprop);
}
message: string = "Hello from component data!";
methods = {
testClick() { console.log("i have been clicked!"); }
};
render() {
return (<any>this.$scopedSlots).default({
message: this.message,
testClick: this.methods.testClick
});
}
}
HelloWorld.cshtml
<hello-world testprop="This is a message from props" v-slot="data">
<div @@click="data.testClick()">
<h1>@Sitecore.FieldFor(m => m.MyField)</h1>
<div>{{ data }}</div>
</div>
</hello-world>
Github examples
I'll update this post with links to github code containing even more complete examples shortly. Hopefully the code above gives some fresh ideas for tackling this problem.