Nuxt render function for a string of HTML that contains Vue components - vue.js

I'm trying to solve this for Nuxt
Codesandbox of a WIP not working: https://codesandbox.io/s/zw26v3940m
OK, so I have WordPress as a CMS, and it's outputting a bunch of HTML. A sample of the HTML looks like this:
'<h2>A heading tag</h2>
<site-banner image="{}" id="123">Slot text here</site-banner>
<p>some text</p>'
Notice that it contains a Vue component <site-banner> that has some props on it (the image prop is a JSON object I left out for brevity). That component is registered globally.
I have a component that we wrote, called <wp-content> that works great in Vue, but doesn't work in Nuxt. Note the two render functions, one is for Vue the other is for Nuxt (obviously this is for examples sake, I wouldn't use both).
export default {
props: {
html: {
type: String,
default: ""
}
},
render(h, context) {
// Worked great in Vue
return h({ template: this.html })
}
render(createElement, context) {
// Kind of works in Nuxt, but doesn't render Vue components at all
return createElement("div", { domProps: { innerHTML: this.html } })
}
}
So the last render function works in Nuxt except it won't actually render the Vue components in this.html, it just puts them on the page as HTML.
So how do I do this in Nuxt? I want to take a string of HTML from the server, and render it on the page, and turn any registered Vue components into proper full-blown Vue components. Basically a little "VueifyThis(html)" factory.

This was what worked and was the cleanest, thanks to Jonas Galvez from the Nuxt team via oTechie.
export default {
props: {
html: {
type: String,
default: ""
}
},
render(h) {
return h({
template: `<div>${this.html}</div>`
});
}
};
Then in your nuxt.config.js file:
build: {
extend(config, ctx) {
// Include the compiler version of Vue so that <component-name> works
config.resolve.alias["vue$"] = "vue/dist/vue.esm.js"
}
}

And if you use the v-html directive to render the html?
like:
<div v-html="html"></div>
I think it will do the job.

Here's a solution on codesandbox: https://codesandbox.io/s/wpcontent-j43sp
The main point is to wrap the dynamic component in a <div> (so an HTML tag) in the dynamicComponent() template, as it can only have one root element, and as it comes from Wordpress the source string itself can have any number of top level elements.
And the WpContent component had to be imported.

This is how I did it with Nuxt 3 :
<script setup lang="ts">
import { h } from 'vue';
const props = defineProps<{
class: string;
HTML: string
}>();
const VNode = () => h('div', { class: props.class, innerHTML: props.HTML })
</script>
<template>
<VNode />
</template>
There was not need to update nuxt.config.ts.
Hopefully it will help some of you.

I made some changes to your codesandbox. seems work now https://codesandbox.io/s/q9wl8ry6q9
Things I changed that didn't work:
template can only has one single root element in current version of Vue
v-bind only accept variables but you pass in a string.

Related

Vue JS via CDN - why does adding a component replace the entire template?

I'm making a vue js app using the CDN like this:
<div id="bookingApp">
<select-service />
<div>
Hello there
</div>
</div>
... and in js ...
const SelectService = {
template: '<h1>In Here!!</h1>',
setup() {
}
}
const bookingApp = Vue.createApp({
components: { "select-service": SelectService },
data() {
return {
sid: -1,
rid: -1
}
}
});
bookingApp.mount('#bookingApp');
... However, in #bookingApp it only shows "In Here!!" and not "Hello there" below it.
If I remove the then it shows "Hello there" as expected.
Why is it that I can't have component and still show the Hello there as well?
You should not use self-closing tags for Vue components in DOM templates as HTML only allows self closing for well known types like <input> and <img>.
For more details see the documentation:
https://vuejs.org/guide/essentials/component-basics.html#dom-template-parsing-caveats

Nuxt link with dynamic `to` prop

I am trying to automatically add UTM tracking parameters to all nuxt links, so I made a component for this (let me know if there's a better way!).
<template>
<nuxt-link :to="url" :target="$attrs.target"><slot></slot></nuxt-link>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: 'ArticleLink',
props: {
to: {
type: String,
required: true,
},
date: {
type: Number,
required: true,
},
},
computed: {
url(): string {
const url = this.to.startsWith('/')
? `${process.env.VUE_APP_ORIGIN}${this.to}`
: this.to;
const urlObject = new URL(url);
urlObject.searchParams.append('utm_source', 'article');
urlObject.searchParams.append('date', this.date);
console.log(this.to, urlObject.href);
return urlObject.href;
},
},
});
</script>
The problem is when I use it on relative links, the to property for some reason combines the initial href, with the calculated one:
<article-link date="20221109" to="/">link</article-link>
this is rendered as:
http://localhost:3000/current/path/http://localhost:3000/?utm_source=article&date-20221109`
What's strange is the console.log shows the correct conversion from / to http://localhost:3000/?utm_source=article&date=20221109
Also strange is if I use Vue devtools to inspect the <NuxtLink> it shows a correct to prop.
Seems I'm using vue router links incorrectly (see: https://github.com/vuejs/vue-router/issues/1131)
When I pass an absolute URL to the :to prop, it adds it to the end of the current URL. So I have to return urlObject.pathname + urlObject.search instead of returning urlObject.href
:shrug:

Render v-html without extra wrapping tag?

In vue 2 v-html require to render an extra wrapping tag. But I'm trying something like following.
<select v-model="coutnry">
// here the options to be rendered.
</select>
<script>
...
data()}{
return{
countryTags:`<option value="BD">Bangladesh</option>`
}
}
</script>
Since, here I can't render any extra html tag, I tired <template v-html="countryTags"> and that didn't worked. The other solution around stackoverlflow seems little confusing. what's the proper solution ?
Unfortunately, Vue does not provide an easy way to accomplish this (More details about the reasons can be found here: https://github.com/vuejs/vue/issues/7431)
As explained there you can create a functional component to just render the HTML without a wrapper element:
Vue.component('html-fragment', {
functional: true,
props: ['html'],
render(h, ctx) {
const nodes = new Vue({
beforeCreate() { this.$createElement = h }, // not necessary, but cleaner imho
template: `<div>${ctx.props.html}</div>`
}).$mount()._vnode.children
return nodes
}
})
This component is created by a Vue core member (LinusBorg): https://jsfiddle.net/Linusborg/mfqjk5hm/

Vue: render <script> tag inside a variable (data string)

I'm new to Vue.js
I want to render a script tag inside a variable (data string).
I tried to us a v-html directive to do so, but it doesn't work Nothing is rendered
Any way I can achieve this?
I'd place a v-if directive on the script tag and put the content of it in a variable.
<script v-if="script">
{{script}}
</scrip>
If I understand you correctly, my answer is:
<template>
<div>
{{ strWithScriptTag }}
</div>
</template>
<script>
export default {
name: 'Example',
methods: {
htmlDecode(input) {
const e = document.createElement('div')
e.innerHTML = input
return e.childNodes[0].nodeValue
},
},
computed: {
strWithScriptTag() {
const scriptStr = '<script>https://some.domain.namet</script>'
return this.htmlDecode(scriptStr)
}
},
}
</script>
I think that by safety vue is escaping your <script> automatically and there is no way to avoid this.
Anyway, one thing you can do is eval(this.property) on created() lifecycle hook.
data: {
script: 'alert("this alert will be shown when the component is created")'
},
created() {
eval(this.script)
}
Use it with caution, as stated in vue js docs, this may open XSS attacks in your app

Best Practice for Reacting to Params Changes with Vue Router

When using Vue Router with routes like /foo/:val you have to add a watcher to react for parameter changes. That results in somewhat annoying duplicate code in all views that have parameters in the URL.
This could look like the following example:
export default {
// [...]
created() {
doSomething.call(this);
},
watch: {
'$route' () {
doSomething.call(this);
}
},
}
function doSomething() {
// e.g. request API, assign view properties, ...
}
Is there any other way to overcome that? Can the handlers for created and $route changes be combined? Can the reuse of the component be disabled so that the watcher would not be necessary at all? I am using Vue 2, but this might be interesting for Vue 1, too.
One possible answer that I just found thanks to a GitHub issue is the following.
It is possible to use the key attribute that is also used for v-for to let Vue track changes in the view. For that to work, you have to add the attribute to the router-view element:
<router-view :key="$route.fullPath"></router-view>
After you add this to the view, you do not need to watch the $route anymore. Instead, Vue.js will create a completely new instance of the component and also call the created callback.
However, this is an all-or-nothing solution. It seems to work well on the small application that I am currently developing. But it might have effects on performance in another application. If you really want to disable the reuse of the view for some routes only, you can have a look at setting the key's value based on the route. But I don't really like that approach.
I used this variant without :key prop on router-view component.
routes.js:
{
path: 'url/:levels(.*)',
name: ROUTES.ANY_LEVEL,
props: true,
component: (): PromiseVue => import('./View.vue'),
},
view.vue
<template>
<MyComponent :config="config" />
</template>
---*****----
<script>
data: () => ({ config: {} }),
methods: {
onConfigurationChanged(route) {
const { params } = route
if (params && params.levels) {
this.config = // some logic
} else {
this.config = null
}
},
},
beforeRouteUpdate(to, from, next) {
this.onConfigurationChanged(to)
next()
},
}
</script>
Inside the component, I use the config as a property. In my case, reactivity is preserved and the component is updated automatically from parameter changes inside the same URL.
Works on Vue 2
vue3 and script setup:
watch(route, () => { fetch()})
in import:
import { watch } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()