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

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

Related

node webkit: open thousands of urls in browser

I am using following snippet to open a link in default browser.
<template>
<div>
<a #click.prevent="fireUpLink">External Link</a>
</div>
</template>
.
<script>
/* global nw */
export default {
methods: {
fireUpLink: function() {
nw.Shell.openExternal("http://example.com/");
}
}
};
</script>
But lets say if I have thousands of links, this solution is not scalable. Is there any better way?
In a Vue SFC, it expects a referenced variable to be defined or imported in the component, or be global. If you reference it from the global window object, it should work.
window.nw.Shell.openExternal('http://example.com');
For Vue, as shown by Max, <a #click.prevent="window.nw.Shell.openExternal('http://example.com')">Link</a> works.
You could also just create a component:
<template>
<a
:href="url"
class="link"
#click.prevent="openExternal"
><slot></slot></a>
</template>
<script>
export default {
name: 'ExternalLink',
props: {
url: {
type: String,
required: true
}
},
methods: {
openExternal: function () {
window.nw.Shell.openExternal(this.url);
}
}
};
</script>
Then just reference it like this:
<external-link url="http://example.com">Link</external-link>
Alternatively you could create a mixin that has the openExternal method in it, and globally install it across all components, so you can just do <a #click.prevent="openExternal('http://example.com')>
If you are using something other than Vue, which does not use a Virtual DOM, then you could just add a class="external-link" then target all elements on the page with that class and handle them.
$('.external-link').click(function (evt) {
// Prevent the link from loading in NW.js
evt.preventDefault();
// Get the `href` URL for the current link
let url = $(this).attr('href');
// Launch the user's default browser and load the URL for the link they clicked
window.nw.Shell.openExternal(url);
});

Use existing CMS output as data for Single File Components (Vue.js)

I have access to the templates that CMS uses to generate pages. This CMS is very primitive. The output is pieces of HTML.
I want to use this data in Single File Components. Most importantly, this data should not be rendered by the browser. I figured that wrapping the CMS output into a noscript tag would work. Then I just parse the string from the noscript to get HTML.
This method is pretty dirty and it does not use the power of Vue.js templates. I'm wondering if there is a better way?
CMS template:
<noscript id="cms-output">
<!-- HTML generated by CMS -->
</noscript>
Main JavaScript file:
import Vue from 'vue'
import App from './app.vue'
const cmsOutput = document.getElementById('cms-output')
const parser = new DOMParser()
Vue.prototype.$cms = parser.parseFromString(cmsOutput.innerHTML, 'text/html')
Vue.config.productionTip = false
new Vue({ render: (h) => h(App) }).$mount('#app')
Single File Component:
<template>
<div v-html="content"></div>
</template>
<script>
export default {
computed: {
content: function() {
const contentElement = this.$cms.querySelector('.content')
// contentElement manipulations here (working with descendants, CSS classes, etc)
return contentElement.outerHTML
}
}
}
</script>
You can use DOM-injected HTML (or even JavaScript strings) as a template in your SFC but you'll need to enable Vue's runtime compiler. Add the following to the project's vue.config.js:
module.exports = {
runtimeCompiler: true
}
Wrap the content of your HTML output in an x-template:
<script type="text/x-template" id="cms-output">
...
</script>
In your SFC, don't use <template></template> tags. Instead, use the template option in your component (this is what the runtime compiler is needed for):
<script>
export default {
template: '#cms-output'
}
</script>
Now you can use the template just as if it were defined in the SFC, with directives, mustache syntax, etc.
EDIT (based on feedback)
There's nothing unique or complex about this if I understand correctly. Use a normal component / template. Since the output isn't ready to be used as a template then there is no choice but to parse it. You could load it from AJAX instead of embedding it as in your question but either way works. Your component could look something like this:
export default {
data() {
return {
data1: '',
data2: '',
dataN: ''
}
},
created() {
const contentElement = this.$cms.querySelector('.content');
const arrayOfData = parseTheContent(contentElement);
this.data1 = arrayOfData[1];
this.data2 = arrayOfData[2];
...
this.dataN = arrayOfData[100];
}
}
And you'd use a standard template:
<template>
<div>
Some stuff {{ data1 }}. Some more stuff {{ data2 }}.<br />
{{ dataN }}
</div>
</template>

Nuxt render function for a string of HTML that contains Vue components

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.

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

Nuxt.js global events emitted from page inside iframe are not available to parent page

I'm trying to create a pattern library app that displays components inside iframe elements, alongside their HTML. Whenever the contents of an iframe changes, I want the page containing the iframe to respond by re-fetching the iframe's HTML and printing it to the page. Unfortunately, the page has no way of knowing when components inside its iframe change. Here's a simplified example of how things are setup:
I have an "accordion" component that emits a global event on update:
components/Accordion.vue
<template>
<div class="accordion"></div>
</template>
<script>
export default {
updated() {
console.log("accordion-updated event emitted");
this.$root.$emit("accordion-updated");
}
}
</script>
I then pull that component into a page:
pages/components/accordion.vue
<template>
<accordion/>
</template>
<script>
import Accordion from "~/components/Accordion.vue";
export default {
components: { Accordion }
}
</script>
I then display that page inside an iframe on another page:
pages/documentation/accordion.vue
<template>
<div>
<p>Here's a live demo of the Accordion component:</p>
<iframe src="/components/accordion"></iframe>
</div>
</template>
<script>
export default {
created() {
this.$root.$on("accordion-updated", () => {
console.log("accordion-updated callback executed");
});
},
beforeDestroy() {
this.$root.$off("accordion-updated");
}
}
</script>
When I edit the "accordion" component, the "event emitted" log appears in my browser's console, so it seems like the accordion-updated event is being emitted. Unfortunately, I never see the "callback executed" console log from the event handler in the documentation/accordion page. I've tried using both this.$root.$emit/this.$root.$on and this.$nuxt.$emit/this.$nuxt.$on and neither seem to be working.
Is it possible that each page contains a separate Vue instance, so the iframe page's this.$root object is not the same as the documentation/accordion page's this.$root object? If so, then how can I solve this problem?
It sounds like I was correct and there are indeed two separate Vue instances in my iframe page and its parent page: https://forum.vuejs.org/t/eventbus-from-iframe-to-parent/31299
So I ended up attaching a MutationObserver to the iframe, like this:
<template>
<iframe ref="iframe" :src="src" #load="onIframeLoaded"></iframe>
</template>
<script>
export default {
data() {
return { iframeObserver: null }
},
props: {
src: { type: String, required: true }
},
methods: {
onIframeLoaded() {
this.getIframeContent();
this.iframeObserver = new MutationObserver(() => {
window.setTimeout(() => {
this.getIframeContent();
}, 100);
});
this.iframeObserver.observe(this.$refs.iframe.contentDocument, {
attributes: true, childList: true, subtree: true
});
},
getIframeContent() {
const iframe = this.$refs.iframe;
const html = iframe.contentDocument.querySelector("#__layout").innerHTML;
// Print HTML to page
}
},
beforeDestroy() {
if (this.iframeObserver) {
this.iframeObserver.disconnect();
}
}
}
</script>
Attaching the observer directly to the contentDocument means that my event handler will fire when elements in the document's <head> change, in addition to the <body>. This allows me to react when Vue injects new CSS or JavaScript blocks into the <head> (via hot module replacement).