Vue watch page url changes - vuejs2

I'm trying to watch page url. I don't use Vue Router.
My final goal is to set page url as input value:
<template>
<div>
<input v-model="pageUrl">
</div>
</template>
<script>
export default {
data() {
return {
pageUrl: window.location.href,
link: ''
}
},
watch: {
pageUrl: function() {
this.link = window.location.href
}
}
}
</script>
The example above doesn't work somewhy.
I've also tried
watch: {
'window.location.href': function() {
this.link = window.location.href
}
},
Input value is being set only once on component render.
What can be wrong?

well, that is exactly the reason you want to use vue-router!
vue can only detect changes in reactive properties: https://v2.vuejs.org/v2/guide/reactivity.html
if you want to react to changes in the url, you have 2 ways:
https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event or
https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event
i would rather use vue-router or a similar plugin.

Related

Render and Compile String using vue.js

There is a requirement where all html elements are defined in a JSON file and used in the template.
There is a function - "markComplete" which needs to be triggered on change of a checkbox.
Code Template:
<template>
<span v-html="htmlContent"></span>
</template>
<script>
data(){
return{
htmlContent: "<input type='checkbox' v-on:change='markComplete'>"
}
}
</script>
Above code won't work as onChange event won't be mounted, and I get Uncaught ReferenceError: markComplete is not defined
Is there any way to make this work?
You are trying to compile the string as Vue Templates using v-html.
Note that the contents are inserted as plain HTML - they will not be compiled as Vue templates
Read about v-html in Vue Docs.
As solution you can read this article
You don't want to use a library? Checkout the code below:
First create a js file (preferably RenderString.js):
import Vue from "vue"
Vue.component("RenderString", {
props: {
string: {
required: true,
type: String
}
},
render(h) {
const render = {
template: "<div>" + this.string + "</div>",
methods: {
markComplete() {
console.log('the method called')
}
}
}
return h(render)
}
})
Then in your parent component:
<template>
<div><RenderString :string="htmlContent" /></div>
</template>
<script>
import RenderString from "./components/RenderString"
export default {
name: "App",
data: () => ({
htmlContent: "<input type='checkbox' v-on:change='markComplete'>"
})
}
</script>
Note: I didn't run the code above but I created a similar working CodeSandbox Example

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).

Vue model not updating

When I try to update my custom text-area component's model data this.message='<span id="foo">bar</span> the text and html does not display in the htmltextarea tag like it should, but I can see the update applied in the Vue dev tool's console. I've also tried switching to an object instead of a string and using Vue.set, but this does not work either.
Any suggestions on how to fix this?
The goal with the htmlTextArea component is to get the users text from the htmlTextArea tag (this works), manipulate this text and bind it back to the textarea, but with HTML in it.
Custom text-area component:
<template>
<div contenteditable="true" #input="updateHTML" class="textareaRoot"></div>
</template>
<script>
export default {
// Custom textarea
name: 'htmlTextArea',
props:['value'],
mounted: function () {
this.$el.innerHTML = this.value;
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML);
}
}
}
</script>
Other component:
<template>
...
<htmlTextArea id="textarea" v-model="message"></htmlTextArea>
...
</template>
<script>
data: {
return {
message: 'something'//this works
}
}
...
methods: {
changeText() {
this.message='<span id="foo">bar</span>'//this does not
}
},
components: {
htmlTextArea
}
</script>
You need to set the value explicitly after the value props change. you can watch for value change.
<template>
<div contenteditable="true" #input="updateHTML" class="textareaRoot"></div>
</template>
<script>
export default {
// Custom textarea
name: "htmlTextArea",
props: ["value"],
mounted: function() {
this.$el.innerHTML = this.value;
},
watch: {
value(v) {
this.$el.innerHTML = v;
}
},
methods: {
updateHTML: function(e) {
this.$emit("input", e.target.innerHTML);
}
}
};
</script>
Change the data property into a function, as you have it defined it is not reactive.
data () {
return {
message: 'something'//this works
}
}
Now when you update the message property in your method, the component will update accordingly.
Reactivity in depth

Why is my computed property not computing?

Below is a single-file Vue.js template. The note object is passed to the template as a prop, and includes audio_length and id.
<template>
<span v-show="note.audio_length > 0" class="audio-element-wrapper">
<audio controls preload="none">
<source :src="audioURL" type="audio/webm">
Your browser does not support the audio element.
</audio>
<span>
({{note.audio_length}}s)
</span>
</span>
</template>
<script>
module.exports = {
mounted: function() {
console.log("mounted audioelement", this.note.id);
},
props: ['note'],
computed: {
audioURL: function() {
return "/notes/getAudio/" + this.note.id;
}
}
};
</script>
However, when I load the component, the computed audioURL results in /notes/getAudio/undefined. Apparently, it runs before the note property is loaded. The code inside the curly braces is interpreted correctly. How can I bind the src property to the correctly computed url?
i suppose is not working because your audioURL computed value return a path without your file format.
try to change your computed:
computed: {
audioURL: function() {
return "/notes/getAudio/" + this.id + ".webm";
}
}
For a better debug i advise you to install vue-devtools
Thanks for everyone's help. It turns out that the problem isn't really with Vuejs per se. It's a browser issue. Even though the src element was actually modified, it doesn't update the action of the element until you call .load()
So, the solution that worked was as follows:
<template>
<span v-show="note.audio_length > 0" class="audio-element-wrapper">
<audio ref="player" controls preload="none">
<source :src="audioURL" type="audio/webm">
Your browser does not support the audio element.
</audio>
<span>
</template>
And then, in the scripts:
updated: function() {
this.audioURL = "/notes/getAudio/" + this.note.id;
this.$refs.player.load();
},
Solution 1 (vue 2.x)
If you want to pass the object properties as a props then you have to use v-bind directive, see the docs here.
I suppose in your parent component you have something like that for data property:
data: function () {
return {
note: {
audio_lenght: 100,
id: 12
}
}
Then write your component as:
<your-component v-bind='note'></your-component>
And in YourComponent.vue:
...
props: ['id', 'audio_lenght']
...
computed: {
audioURL: function() {
return "/notes/getAudio/" + this.id;
}
}
Solution 2
Write your component as:
<your-component :note='note'></your-component>
In YourComponent.vue:
props: {
note: {
type: Object
}
},
computed: {
audioURL: function() {
return "/notes/getAudio/" + this.note.id;
}
}