Wrap element using v-if, otherwise just have content itself - vue.js

I have a set of elements being generated in a v-for directive that, if the object has a url property, get wrapped in an <a> tag - otherwise, I need it to just emit the element itself.
For example,
var data = {
events: [
{name: "Foo"},
{name: "Bar", url: "google.com"}
]
};
and the corresponding HTML:
<div v-for="event in events">
<span>{{event.name}}</span>
</div>
What I need is to wrap the <span> in an <a v-bind:href="url"> only if there is a url property present.
I understand I could use a v-if and use two spans, such as:
<span v-if="!event.url">{{event.name}}</span>
<a v-bind:href="event.url" v-if="event.url">
<span>{{event.name}}</span>
</a>
However, in my use case the <span> element here could be massive and I don't want to repeat myself just to wrap the element.
Is there a way to achieve a conditional wrap such as the above?

You can use v-html for example and render your logic inside a function:
function wrapSpan(el, link) { // link wrapper function
return `${el}`;
}
new Vue({
el: '#app',
data: {
events: [
{name: "Foo"},
{name: "Bar", url: "google.com"}
]
},
methods: {
element: function(i) {
const name = this.events[i].name;
const url = this.events[i].url || null;
const span = `<span style="color: green">${name}</span>`; // long span
return (url) ? wrapSpan(span, url) : span;
}
}
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<div v-for="(event, index) in events">
<span v-html="element(index)"></span>
</div>
</div>

Related

In VueJS using v-for, is this structure possible?

Given a collection of objects in the data of a component:
data: function () {
return [
{ id: 1, name: "foo", br: false },
{ id: 1, name: "bar", br: true },
{ id: 1, name: "baz", br: false }
]
}
...is it possible to render a structure like so...
<div id="1">foo</div>
<div id="2">bar</div><div class="break" />
<div id="3">baz</div>
In a nutshell, I need to have another div conditionally rendered at the same level as the items in the list. If it matters or helps, the individual items in the list are also components. I know how to set up the rest of the data and properties - it's just getting that additional HTML rendered in the list that I need to accomplish.
I want to avoid creating another item in the list and additional component to represent the break. No need to add the overhead of the additional Vue objects for the simple HTML div. This list may have > 100 items and "breaks" and it can add up quickly.
Yes. You should loop through the items like so:
<template v-for="item in items">
<div :id="item.id">
{{ item.name }}
</div>
<div class="break" v-if="item.br">
</div>
</template>
You can do it with a normal v-for and a normal v-if for your optional div
<html>
<head>
<script type = "text/javascript" src = "https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js">
</script>
</head>
<body>
<div id="app">
<div v-for="item in items">
<div :id="item.id">{{item.name}}</div>
<div v-if="item.br" class="break">
</div>
</div>
<script type = "text/javascript">
var vue_det = new Vue({
el: '#app',
data: {
items: [
{ id: 1, name: "foo", br: false },
{ id: 2, name: "bar", br: true },
{ id: 3, name: "baz", br: false }
]}
});
</script>
</body>
</html>
You should not be afraid of 100 divs or around so, a library like Vue is made to manage efficiently thousands of components

How to render HTML from a property of items being displayed with v-for loop?

I send an array of objects which each have a .html property that has HTML text in it, e.g. <h1>...</h1> or <h2>...</h2>
I want to have the HTML from each item display one after another in the DOM, like this:
<h1>...</h1>
<h2>...</h2>
<h2>...</h2>
<h1>...</h1>
<h2>...</h2>
However, all of these attempts do not work:
<div v-for="item in outlineItems" v-html="item.html"></div>
displays HTML wrapped in divs: <div><h1>...</h1></div> and <div><h2>...</h2></div>
<template v-for="item in outlineItems" v-html="item.html"></template>
displays nothing
<template v-for="item in outlineItems">{{item.html}}</template>
displays the literal HTML instead of rendering it
<template v-for="item in items"><template v-html="item.html"></template></template>
displays nothing
How can I simply display the contents of the .html property of each item so that the HTML in it renders, without any wrapping elements on it?
You could do it using a single wrapper element for the whole lot by concatenating all the HTML in a computed property:
new Vue({
el: '#app',
data () {
return {
outlineItems: [
{ html: '<h1>Heading 1</h1>' },
{ html: '<h2>Heading 2</h2>' },
{ html: '<h3>Heading 3</h3>' }
]
}
},
computed: {
outlineHtml () {
return this.outlineItems.map(item => item.html).join('')
}
}
})
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<div v-html="outlineHtml"></div>
</div>
Behind the scenes v-html sets the innerHTML of its corresponding DOM node. A <template> tag doesn't create a DOM node so the innerHTML can't be set anywhere.
I would add that v-html is considered an 'escape hatch'. Where possible you should avoid using it and let Vue create the HTML itself. Generally the approach would be to use a suitable data structure to hold the data (rather than a blob of markup) and then render that data structure within the template.
One possible solution is to create multiple unique components. You can even pass in props, and there are no wrappers
Vue.component('greeting', {
template: '<h1>Welcome to coligo!</h1>'
});
Vue.component('titles', {
template: '<h1>title 1</h1>'
});
Vue.component('title2', {
template: '<h2>Welcome to coligo!</h2>'
});
Vue.component('title3', {
template: '<h3>{{text}}</h3>',
props: ['text']
});
var vm = new Vue({
el: '#app',
data: {
items: [
{ type: 'greeting' },
{ type: 'titles' },
{ type: 'title2' },
{ type: 'title3', text: 'test' }
]
}
});
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<component v-for="(item,i) in items" :is="item.type" :text="item.text" :key="i"></component>
</div>

Conditional link behavior in VueJS

Couldn't find a proper name for the title, will be glad if someone figures out a better name.
I have a component which represents a product card. The whole component is wrapped in <router-link> which leads to product page.
However I have another case, when I do not need the component to lead to a product page, but instead I need to do some other action.
The only solution I found is to pass a callback function as a prop, and based on this, do something like:
<router-link v-if="!onClickCallback">
... here goes the whole component template ...
</router-link>
<div v-if="onClickCallback" #click="onClickCallback">
... here again goes the whole component template ...
</div>
How can I do this without copy-pasting the whole component? I tried to do this (real code sample):
<router-link class="clothing-item-card-preview"
:class="classes"
:style="previewStyle"
:to="{ name: 'clothingItem', params: { id: this.clothingItem.id }}"
v-on="{ click: onClick ? onClick : null }">
However I got this: Invalid handler for event "click": got null
Plus not sure if it's possible to pass prevent modificator for click and this just looks weird, there should be a better architectural solution
Commenting on the error, you could use an empty function instead of null, in the real code snippet
<router-link class="clothing-item-card-preview"
:class="classes"
:style="previewStyle"
:to="{ name: 'clothingItem', params: { id: this.clothingItem.id }}"
v-on="{ click: onClick ? onClick : null }">
This should works (replace a for "router-link" then insert right properties)
Further infos :
https://fr.vuejs.org/v2/guide/components-dynamic-async.html
v-bind is simply an Object where each keys is a props for your component, so here, I programmatically defined an object of properties depending on the wrapper (router link or a simple div). However we cannot do this for events (of course we could create our own event listener but it's a little bit tricky) so I simply but an handle method.
new Vue({
el: "#app",
data: {
products : [{onClickCallback : () => { alert("callback"); return true;}}, {}, {}]
},
methods : {
handleClick(product, event) {
if (!product.onClickCallback) return false
product.onClickCallback()
return true
},
getMyComponentName(product) {
if (product.onClickCallback) return "div"
return "a"
},
getMyComponentProperties(product) {
if (product.onClickCallback) return {is : "div"}
return {
is : "a",
href: "!#"
}
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<component
v-for="(product, index) in products"
:key="index"
v-bind="getMyComponentProperties(product)"
#click="handleClick(product, $event)"
>
<div class="product-card">
<div class="product-card-content">
<span v-show="product.onClickCallback">I'm a callback</span>
<span v-show="!product.onClickCallback">I'm a router link</span>
</div>
</div>
</component>
</div>
Do you have to use a <router-link>? If it can safely be a <div>, you could use something like
<div #click="handleClick" ...>
<!-- component template -->
</div>
and
methods: {
handleClick (event) {
if (this.onClickCallback) {
this.onClickCallback(event)
} else {
this.$router.push({ name: 'clothingItem', ... })
}
}
}
See https://router.vuejs.org/guide/essentials/navigation.html

Programmatically add v-on directives to DOM elements

<span #click="showModal = $event.target.innerHtml>Tag 1</span>
<span #click="showModal = $event.target.innerHtml>Tag 2</span>
<span #click="showModal = $event.target.innerHtml>Tag 3</span>
Clicking in any of the 3 spans will make this.showModal to have the value of each of the span content elements. But this code looks repetitive and unnecessary. I know I can create a component with v-for and have the data for the span contents somewhere else, but I want to know how to do this for very specific reasons. I'd like to have this:
<span>Tag 1</span>
<span>Tag 2</span>
<span>Tag 3</span>
And a function, e.g. in the hook mounted() of the component, that adds the v-on directive for click to each one of them.
Can you help me?
Thanks.
You could try something like this:
<template>
<span v-for="tag in tags" #click="showModal(tag)" v-text="tag"></span>
</template>
<script>
export default {
data() {
return {
tags: ['Tag 1', 'Tag 2', 'Tag 3']
}
},
methods: {
showModal(tag) {
console.log("Showing modal for tag:", tag)
}
}
}
</script>
Hope this helps!
You can add a method which is called on clicks that reads the element's HTML content.
The template:
<span #click="doStuff">Tag 1</span>
<span #click="doStuff">Tag 2</span>
<span #click="doStuff">Tag 3</span>
The method:
doStuff(e) {
this.showModal = e.target.innerHTML
}
You could set up a method to call when the tag is clicked and pass the id of the tag that was clicked through to handle appropriately.
Assuming that you have an array of the tag text:
data: function() {
return {
tagTotal: ['Tag 1', 'Tag 2', 'Tag 3'];
}
}
Then in the HTML section:
<span v-for="tag in tagTotal" #click="methodToCall(tag)">
{{ tag }}
</span>
Then in your mounted, methods, or created section you could add:
mounted: {
methodToCall: function(tag) {
showModal = tag;
// or 'this.showModal = tag' if showModal is a part of the componenet.
}
}
I've finally added the listeners manually with vanilla js, in order to save code:
mounted: function() {
let spans = document.querySelectorAll('span');
spans.forEach(el => {
el.addEventListener('click', this.clickTag);
})
}
methods: {
clickTag(event) { this.showModal = event.target.innerHTML }
}
It's important not using an arrow function for mounted because otherwise it won't bind the vue instance for this.
Thanks for your answers.
If direct-process Dom elements, custom directive will be one option.
Vue.config.productionTip = false
let vMyDirective = {}
vMyDirective.install = function install (_Vue) {
_Vue.directive('my-directive', {
inserted: function (el, binding, vnode) {
el.addEventListener('click', () => {
_Vue.set(vnode.context, binding.value.model, el.innerHTML)
}, false)
}
})
}
Vue.use(vMyDirective)
new Vue({
el: '#app',
data() {
return {
testValues: ['label a', 'label b'],
showModal: 'nothing!!!'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<h2>showModal: {{showModal}}</h2>
<div>
<p v-for="(item, index) in testValues" v-my-directive="{'model': 'showModal'}">Test:<span>{{item}}</span></p>
</div>
</div>

When using conditional rendering, how do I prevent repeating the child components on each condition?

Scenario
I have a custom button component in Vue:
<custom-button type="link">Save</custom-button>
This is its template:
// custom-button.vue
<template>
<a v-if="type === 'link'" :href="href">
<span class="btn-label"><slot></slot></span>
</a>
<button v-else :type="type">
<span class="btn-label"><slot></slot></span>
</button>
</template>
You can see from the template that it has a type prop. If the type is link, instead of the <button> element, I am using <a>.
Question
You'll notice from the template that I repeated the child component, i.e. <span class="btn-label"><slot></slot></span> on both root components. How do I make it so that I won't have to repeat the child components?
In JSX, it's pretty straightforward. I just have to assign the child component to a variable:
const label = <span class="btn-label">{text}</span>
return (type === 'link')
? <a href={href}>{label}</a>
: <button type={type}>{label}</button>
In this situation, I would probably opt to write the render function directly since the template is small (with or without JSX), but if you want to use a template then you can use the <component> component to dynamically choose what you want to render as that element, like this:
Vue.component('custom-button', {
template: '#custom-button',
props: [
'type',
'href',
],
computed: {
props() {
return this.type === 'link'
? { is: 'a', href: this.href }
: { is: 'button', type: this.type };
},
},
});
new Vue({
el: '#app',
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<custom-button type="button">Button</custom-button>
<custom-button type="submit">Submit</custom-button>
<custom-button type="link" href="http://www.google.com">Link</custom-button>
</div>
<template id="custom-button">
<component v-bind="props">
<span class="btn-label"><slot></slot></span>
</component>
</template>
Well you could always create a locally registered component...
// in custom-button.vue
components : {
'label' : {template : '<span class="btn-label"><slot></slot></span>'}
}