How to use vuetify's v-img inside v-html - vuejs2

I would like to use <v-img> of vuetify inside v-html to replace the standard <img>, i.e.
<div v-html="'<v-img src="some_image_url" />'">
I understand that v-html is meant for only standard HTML component, and I would like to know if there is anyway to use a custom component (such as <v-img>) inside v-html.

The example below uses some cheap and cheerful RegExps to do the parsing, nothing I would use in production code. My focus was on how to avoid using v-html rather than finding a reliable way to parse out the <img> tags.
The key thing I'm trying to demonstrate is how you can parse the text into chunks and then iterate over the chunks in the template to create v-img components. I've used a dummy component for v-img but the principle would be exactly the same for the real thing.
new Vue({
el: '#app',
components: {
vImg: {
template: '<strong>[<slot/>]</strong>'
}
},
data () {
return {
text: 'Something something <img src="somepath"> and <img src="otherpath">'
}
},
computed: {
chunks () {
const re = /(<img\s[^>]*>)/g
const text = this.text
const parts = text.split(re).filter(part => part)
return parts.map(part => {
if (part.match(re)) {
const matchSrc = part.match(/\ssrc="([^"]*)"/)
return {
src: matchSrc && matchSrc[1]
}
}
return part
})
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<template v-for="chunk in chunks">
<template v-if="typeof chunk === 'string'">{{ chunk }}</template>
<v-img v-else>{{ chunk.src }}</v-img>
</template>
</div>

I managed to solve it with v-runtime-template, which can be found here:
https://github.com/alexjoverm/v-runtime-template
Cheers!

Related

How to conditionally nest elements in Vue.js?

Is there any way to do this kind of conditional nesting with Vue?
(Apparently <component is="template"> outputs a non parsed <template> tag into the DOM but does not render anything)
<component :is="condition ? 'div' : 'template'">
<!-- 2 elements here -->
</component>
The purpose is to avoid unneeded markup or repeating my 2 elements code twice in a v-if v-else.
Also having a sub component with the 2 elements would not help as Vue components need only 1 root, so a wrapper would be needed there too.
What I am looking for is an equivalent to:
<div v-if="condition">
<span>element 1</span>
<span>element 2</span>
</div>
<template v-else>
<span>element 1</span>
<span>element 2</span>
</template>
but without rewriting twice the span elements.
(Also posted it on Vue.js forum https://forum.vuejs.org/t/how-to-conditionally-nest-elements/95384)
Thanks for any help!
Using Vue 2:
There is no straight forward solution to this using Vue 2, but you can use Functional Components for this purpose, as functional components do not have the single-root limitation.
So first, create a my-span functional component which will be rendered in DOM with multiple nodes like:
<span>element 1</span>
<span>element 2</span>
using:
Vue.component('my-span', {
functional: true,
render: function (createElement, context) {
const span1 = createElement('span', 'element 1');
const span2 = createElement('span', 'element 2');
return [span1, span2]
},
})
You can create as many nodes you want, with any element you want and simply return that as an array.
In Vue 2.5.0+, if you are using single-file components, template-based functional components can be declared with:
<template functional>
</template>
Next, create a component just to wrap the <my-span> above like:
Vue.component('my-div', {
template: '<div><my-span /></div>'
})
Then using Vue’s <component> element with the is special attribute, we can dynamically switch between the <my-div> and <my-span> components like:
<component :is="condition ? 'my-div' : 'my-span'"></component>
This will result in the desired behaviour you are looking for. You can also inspect the rendered DOM to verify this.
Working Demo:
Vue.component('my-span', {
functional: true,
render: function (createElement, context) {
const span1 = createElement('span', 'element 1');
const span2 = createElement('span', 'element 2');
return [span1, span2]
},
})
Vue.component('my-div', {
template: '<div><my-span /></div>'
})
new Vue({
el: "#myApp",
data: {
condition: true
},
methods: {
toggle() {
this.condition = !this.condition;
}
}
})
#myApp{padding:20px}
#myApp div{padding:10px;border:2px solid #eee}
#myApp span{padding:5px;margin:5px;display:inline-flex}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="myApp">
<button #click="toggle">Toggle</button><br>
<component :is="condition ? 'my-div' : 'my-span'"></component>
</div>
Using Vue 3:
In Vue 3, it would ve very easy to implement as we can have multiple root nodes in Vue 3, as you can see MySpan component has a template with multiple spans:
const MySpan = { template: '<span>element 1</span><span>element 2</span>' };
Working Demo:
const { createApp, ref } = Vue;
const MySpan = { template: '<span>element 1</span><span>element 2</span>' };
const MyDiv = {
components: { MySpan },
template: '<div><my-span /></div>'
};
const App = {
components: { MyDiv, MySpan },
setup() {
const condition = ref(true);
const toggle = () => {
condition.value = !condition.value;
};
return { condition, toggle };
}
};
createApp(App).mount("#myApp");
#myApp{padding:20px}
#myApp div{padding:10px;border:2px solid #eee}
#myApp span{padding:5px;margin:5px;display:inline-flex}
<script src="//unpkg.com/vue#next"></script>
<div id="myApp">
<button #click="toggle">Toggle</button><br>
<component :is="condition ? 'my-div' : 'my-span'"></component>
</div>

How to save an existing content?

I can't get how to use v-html to save an existing content. For example:
<div ref="content" v-html="content">Hello, World! A lot of divs</div>
How to make to div content was replace only when I will assign a some not null value with content? Or how to make it in another way? Or is the single way to request div content asynchronously?
The next way works, of course, but I lose a data binding.
this.$refs['content'].innerHTML = "New content";
P.S. I am migrating from jQuery and still can't think in Vue.js philosophy clearly.
Actualy, you must read vue documentation.
In your component you must declare content in data, and simply change it in oher places, i.e. in button's click handler or inside component's methods:
new Vue({
el: "#root",
data: function () {
return {
content: 'Hello, World! A <b>lot</b> of divs'
};
},
methods: {
changeText: function() {
this.content = 'This text from component';
}
}
});
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="root">
<div v-html="content"></div>
<button v-on:click="content = 'This text from button'">Click me</button>
<button v-on:click="changeText">And me</button>
</div>
You could assign a default value to content.
data () {
return {
content: 'Hello, World! A lot of divs'
}
}
When you'll assign a new value to content it will get rendered.
Another way would be to check if content is not null and have 2 different divs using v-if/v-else for conditional rendering.
<div v-if="content" v-html="content"></div>
<div v-else>Hello, World! A lot of divs</div>
and the script
export default {
name: 'customComponent',
data () {
return {
content: null
}
}
}

Vue component communication

I'm looking for a concise example of two Vue components. The first component should contain a text input or textarea. The second component displays a character counter. I would like the first component to emit change events, and the second component should listen for those events and display its computed values (character count). I'm new to Vue and trying to wrap my head around the best way to implement this functionality. It seems rather straightforward in pure JavaScript but doing it the Vue way is not as clear to me. Thanks.
Here is how I'd do it in JavaScript:
Here's the textarea:
<textarea id="pagetext" name="pagetext"
onChange="characterCount();"
onKeyup="characterCount();">Type here</textarea>
Here's the JavaScript:
function characterCount()
{
var characters=document.myForm.pagetext.value.length;
document.getElementById('charcounter').innerHTML=characters+"";
}
My concern with Vue is passing the entire value around... for performance reasons this seems less than ideal. I may want my text editing Vue component to self-contain the value and emit the stats, ie the value for character count which would then be observed by a text stats component.
You can create a "Model" for value of textarea and provide this model to second component by using following way https://v2.vuejs.org/v2/guide/components-props.html
I've written up a snippet with four examples: your original, a simple Vue app (no components) that does the same thing, and two apps with two components that are coordinated by the parent.
The simple Vue app is actually more concise than the pure JavaScript app, and I think it shows off the reason for having a framework: your view doesn't act as a store for your program data, from which you have to pull it out.
In the final example, the parent still owns pageText, but passes it down to the my-textarea component. I like to hide the emitting behind the abstraction of a settable computed, so that the element can use v-model. Any changes are emitted up to the parent, which changes pageText, which propagates back down to the component.
I think your performance concerns fall into the realm of premature optimization, but it is possible not to use the text content as data at all, and only be concerned with the length. The fourth example does that. emitLength could have used event.target.value.length, but I wanted to use it in the mounted to initialize the length properly, so I used a ref.
function characterCount() {
var characters = document.myForm.pagetext.value.length;
document.getElementById('charcounter').innerHTML = characters + "";
}
new Vue({
el: '#app',
data: {
pageText: 'Type here'
}
});
new Vue({
el: '#app2',
data: {
pageText: 'Type here'
},
components: {
myTextarea: {
props: ['value'],
template: '<textarea name="pagetext" v-model="proxyValue"></textarea>',
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
}
},
textLength: {
props: ['value'],
template: '<div>{{value}}</div>'
}
}
});
new Vue({
el: '#app3',
data: {
textLength: null
},
components: {
myTextarea: {
template: '<textarea ref="ta" name="pagetext" #input="emitLength">Type here</textarea>',
methods: {
emitLength() {
this.$emit('change', this.$refs.ta.value.length);
}
},
mounted() {
this.emitLength();
}
},
textLength: {
props: ['value'],
template: '<div>{{value}}</div>'
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<form name="myForm">
<textarea id="pagetext" name="pagetext" onChange="characterCount();" onKeyup="characterCount();">Type here</textarea>
</form>
<div id="charcounter"></div>
<div id="app">
<h1>Vue (simple)</h1>
<form>
<textarea name="pagetext" v-model="pageText"></textarea>
</form>
<div>{{pageText.length}}</div>
</div>
<div id="app2">
<h1>Vue (with components)</h1>
<form>
<my-textarea v-model="pageText"></my-textarea>
</form>
<text-length :value="pageText.length"></text-length>
</div>
<div id="app3">
<h1>Vue emitting stats</h1>
<form>
<my-textarea #change="(v) => textLength=v"></my-textarea>
</form>
<text-length :value="textLength"></text-length>
</div>

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

Conditionally rendering parent element, keep inner html

Is there any built-in way to go about conditionally showing a parent element?
To illustrate:
<a v-show-but-keep-inner="someCondition">
<span>This is always rendered no matter what</span>
</a
I think it's a job for custom directive. I made this one as a quick POC:
Vue.directive('showButKeepInner', {
bind(el, bindings) {
bindings.def.wrap = function(el) {
// Find all next siblings with data-moved and move back into el
while (el.nextElementSibling && el.nextElementSibling.dataset.moved) {
el.appendChild(el.nextElementSibling).removeAttribute('data-moved')
}
el.hidden = false
}
bindings.def.unwrap = function(el) {
// Move all children of el outside and mark them with data-moved attr
Array.from(el.children).forEach(child => {
el.insertAdjacentElement('afterend', child).setAttribute('data-moved', true)
})
el.hidden = true
}
},
inserted(el, bindings) {
bindings.def[bindings.value ? 'wrap' : 'unwrap'](el)
},
update(el, bindings) {
bindings.def[bindings.value ? 'wrap' : 'unwrap'](el)
}
})
new Vue({
el: '#app',
data: {
someCondition: false
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js"></script>
<div id="app">
<p>
<button v-on:click="someCondition = !someCondition">{{ someCondition }}</button>
</p>
<a v-show-but-keep-inner="someCondition" href="/">
<span>This is always rendered no matter what</span>
</a>
</div>
For Vue v3.x, the following would work:
<component
:is="condition ? 'custom-component' : 'slot'"
custom-component-prop
...
>
...
</component>
For Vue v2.x, a workaround is to do:
<component
:is="condition ? 'custom-component' : 'v-div'"
custom-component-prop
...
>
...
</component>
// VDiv.vue
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
inheritAttrs: false,
}
</script>
The tradeoff is there will be an extra element like div being rendered, since Vue v2.x doesn't support fragment.
I just ran into the same Problem.
Vue.js Core team member LinusBorg provides a great solution for this use case using a functional component with a custom render function:
Vue.component('with-root', {
functional: true,
props: ['show'],
render(h, ctx) {
const children = ctx.children.filter(vnode => vnode.tag) // remove unnecessary text nodes
// console.log(children)
if (children.length !== 1) {
console.warn('this component accepts only one root node in its slot')
}
if (ctx.props.show) {
return children[0]
} else {
return children[0].children
}
}
})
new Vue({
el: '#app',
data: {
show: true
}
})
<script src="https://unpkg.com/vue#2.6.14/dist/vue.js"></script>
<div id="app">
<with-root v-bind:show="show">
<a href="#">
<span>This is always rendered no matter what</span>
</a>
</with-root>
<br>
<button #click="show = !show">Toggle</button>
<pre>{{$data}}</pre>
</div>
His fiddle: https://jsfiddle.net/Linusborg/w9d8ujn8/
Source:
https://forum.vuejs.org/t/conditionally-render-parent-element/9324/2
If someone happens to be using the vue-fragment (https://www.npmjs.com/package/vue-fragment) library, the following works:
<component :is="someCondition ? 'a' : 'fragment'">
<span>This is always rendered no matter what</span>
</component>
That being said, I do not recommend to use one library just to do this. But if you already do, it can be useful.
Without Vuejs or other framework context, solely DOM.
You can't remove a DOM element without removing its children too.
What you could do is get the children of a DOM element and replace them with the parent or something similar like that.
With Vuejs you may be able to hide this functionality behind a directive or component, but I think this would be overcomplicating what you want to achieve.
If you want your anchor not to be clickable in certain cases, you could do something like v-on:click.prevent="yourCondition && xxx()". On top of that you could use css classes to hide the fact that it's still an anchor v-bind:class="{ fakeAnchor: yourCondition}".
Though the simplest solution may be to just duplicate your html.
<a v-show="someCondition">
<span>This is always rendered no matter what</span>
</a>
<span v-show="!someCondition">This is always rendered no matter what</span>
The best solution depends on what your case is. If the real inner content will be much larger it might not be okay to duplicate that. If that's the case you could encapsulate that in another vue component.
Maybe this approach helps you (using 'is'):
<template lang="pug">
component(is=someCondition?"v-show-but-keep-inner":"my-another-component")
v-form
v-layout
v-flex
v-btn(#click="doThat")
</template>
This way parent component changes depending on 'someCondition' and children are the same for both conditions.