Vue2: replace slot contents with compiled component based on regex? - javascript

I have a component Tooltip that adds some extra functionality.
I want to define a wrapper component ApplyTooltips, which has a default slot and replaces all instances of [[<any text here>]] with <Tooltip text="<any text here>"/> (e.g. very similar to a Markdown parser)
I am basing my approach on this fiddle: https://jsfiddle.net/Herteby/pL13vda2/
template
<template>
<div>
<div class="tooltip-wrapper">
<slot>
</slot>
<template v-for="node, n in parsed">
<a v-if="n % 2">{{node}}</a>
<template v-else>{{node}}</template>
</template>
</div>
</div>
</template>
script
export default {
name: 'ApplyTooltips',
data: () => ({
el: null,
}),
computed: {
text() {
return this.el ? this.el.innerHTML : ''
// return this.$slots.default[0].text
},
re() {
return /\[\[(.+?)\]\]/gi // captures [[words]]
},
parsed() {
return this.text.split(this.re) // every other value in this list will be a captured pattern
}
},
mounted() {
this.el = this.$el
// console.log(this.$slots.default[0].elm)
}
}

Related

how to wrap a component content with html tag dynamically in vue

Hi i want to wrap the content of a component with some specific html tag let say button for this example.
i have a function which dynamically returns a value which i use as a prop, based on that i want to wrap the content of a component.
i know i could have achieved this way too <button><compA/></button> it does not solve my problem beacuse i need to change it in 100 places.
My expected result:
<button><div>press me i'm button</div></button>
<div>don't wrap me with button leave me as it is</div>
Note: :wrappwithbutton="" having true for 1st usage and false for 2nd usage
const localComponent = {
name:'first-comp',
template:`<div> {{text}}</div>`,
props:['wrappwithbutton','text'],
}
const app = new Vue({
el:'#app',
name:'app',
components:{'first-comp':localComponent},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<first-comp :wrappwithbutton="true" text="press me i'm button"></first-comp>
<br/>
<hr/>
<br/>
<first-comp :wrappwithbutton="false" text="don't wrap me with button leave me as it is"></first-comp>
</div>
This is a perfect example for render functions. Instead of using a template you can use a render function to render the template for you. Read more about render functions
const localComponent = {
name:'first-comp',
props:['wrappwithbutton', 'text'],
methods: {
btnClick() {
if (this.wrappwithbutton) console.log('button')
}
},
render(h) {
return h(this.wrappwithbutton ? 'button' : 'div', [
h('div', this.text)
])
}
}
const app = new Vue({
el:'#app',
name:'app',
components:{'first-comp':localComponent},
});
Vue.config.productionTip = false
Vue.config.devtools = false
You can even go a step further and make your localComponent to be more dynamic with the parent passing a prop with the tag that should be rendered:
const localComponent = {
name:'first-comp',
props:['tag', 'text'],
methods: {
btnClick() {
if (this.wrappwithbutton) console.log('button')
}
},
render(h) {
return h(this.tag, [
h('div', this.text)
])
}
}
If you would like to have a single div and not two divs you can do:
render(h) {
if (this.tag === 'div') {
return ('div', this.text);
}
return h(this.tag ? 'button' : 'div', [
h('div', this.text)
])
}
This is my idea, but I think the template should have a more concise way of writing
const localComponent = {
name: "first-comp",
template: `
<template v-if="wrappwithbutton">
<button>
<div> {{text}}</div>
</button>
</template>
<template v-else>
<div> {{text}}</div>
</template>
`,
props: ["wrappwithbutton", "text"]
};
const app = new Vue({
el: "#app",
name: "app",
components: { "first-comp": localComponent }
});

Vue js render text with html content

What i'm trying to do is have a root element with a prop that holds inner html like: hello<b>hey</b>
but i can't use v-html because this element also has children for example:
<template>
<component :is="element.tag" contenteditable="true">
<div contenteditable="false">
<span class="delete-obj" :id="'delete'+element.id" >delete</span>
</div>
<RenderString :string="element.content" />
</component>
</template>
<script>
import Vue from "vue";
Vue.component("RenderString", {
props: {
string: {
required: true,
type: String
}
},
render(h) {
const render = {
template: this.string ,
methods: {
markComplete() {
console.log('the method called')
}
}
}
return h(render)
}
})
export default {
name: "VElement",
props: {
element: {
required: false,
default: null
},
},
}
</script>
I have tried the above and I have tried using slots. I can solve it with vanilla JavaScript like element.innerText, but I don't want to. The main goal is that the element is editable when they type they are editing element.content that will be rendered and the div that's inside it is normal HTML that I also need.
The main problem is that the inner HTML that I want doesn't have a root element.
The element is something like:
{id:1,tag:"div",content:"hello<b>hey</b>"}
I want the final result to be:
<div contenteditable="true">
<div contenteditable="false">
<span class="delete-obj" :id="'delete'+element.id" >delete</span>
</div>
hello<b>hey</b>
<div>
And I want to edit hello<b>hey</b> when I click inside I don't want it wrapped in anything else and if I put v-html on the outer div the inner div is gone.
If you really aren't able to wrap the content in a parent element, maybe a good approach is to write a VUE directive that render the text.
JS FIDDLE FULL DEMO
//DIRECTIVE
Vue.directive('string', {
inserted(el, bind) {
/**
* Here you can manipulate the element as you need.
*/
el.insertAdjacentText('beforeend', bind.value);
}
});
//TEMPLATE
<template>
<component :is="element.tag" contenteditable="true" v-string="element.content">
<div contenteditable="false">
<span>delete</span>
</div>
</component>
</template>
Like Seblor said, v-html will work with nested html strings.
Simply replacing the RenderString component with a <div v-html="element.content"/> should get you what you want.
Tested with the given example of hello<b>hey</b>:
Vue.component('VElement', {
name: "VElement",
props: {
element: {
required: false,
default: null
},
},
template: '\
<component :is="element.tag" contenteditable="true">\
<div contenteditable="false">\
<span class="delete-obj" :id="\'delete\'+element.id">delete</span>\
</div>\
<div v-html="element.content"/>\
</component>'
})
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<v-element :element="{id:1, tag:'div', content:'hello<b>hey</b>'}" />
</div>

Create anchor tags dynamically in Vue.JS

I have a JS-object and I need to be able to create html elements out of it and render it in Vue.JS. My solution until now was to get the object, create the HTML-elements as strings out of it and then just add it to the template. However, although this shows the elements correctly, the anchor tags are not clickable.
<template>
<div>
<template v-if="elementData">
<ul>
<li v-for="(value, key) in elementData" :key="key">
<div v-html='value'></div>
</li>
</ul>
</template>
</div>
</template>
<script>
const about = [
[
'This is normal text ',
{
text: 'im a link',
link: 'https://www.stack-overflow.com',
},
', is not bad ',
],
[
'More text and text',
],
];
export default {
data() {
return {
elementData: [],
};
},
mounted() {
this.setData();
},
methods: {
setData() {
this.elementData = about.map((paragraph) => {
let pElement = '<p>';
paragraph.forEach((part) => {
if (typeof part === 'object') {
const link = `<a target="_" :href="${ part.link }">${ part.text }</a>`;
pElement = pElement.concat(link);
} else pElement = pElement.concat(part);
});
pElement.concat('</p>');
return pElement;
});
},
},
};
</script>
The problem probably comes from me not creating the actual html-elements (like when using document.createElement('div') with vanilla JS). However, I don't know how to do this Vue.JS.
Instead of manipulating the DOM manually, you can use Vue's conditional rendering to achieve the same goal. Take a look at this solution below:
<template>
<div>
<template v-if="elementData">
<ul>
<li v-for="(paragraph, key) in elementData" :key="`paragraph-${key}`">
<span v-for="(part, partKey) in paragraph" :key="`part-${partKey}`">
<!-- Render an anchor tag if the part is a link -->
<template v-if="isLink(part)"
><a :href="part.link">{{ part.text }}</a></template
>
<!-- Else, just put output the part -->
<template v-else>{{ part }}</template>
</span>
</li>
</ul>
</template>
</div>
</template>
<script>
const about = [
[
"This is normal text ",
{
text: "im a link",
link: "https://www.stackoverflow.com",
},
", is not bad ",
],
["More text and text"],
];
export default {
...
data() {
return {
elementData: [],
};
},
mounted() {
this.setData();
},
methods: {
isLink(e) {
// You can change this condition if you like
return e && typeof e === "object" && e.link && e.text;
},
setData() {
this.elementData = about;
},
},
};
</script>
Notice how we just created elements based on conditions. We render an anchor tag if the part is an object, and has the link and text attribute. Else, we just display the string.
See live demo

In Vue2 tag input, trying to display tag name from tag object in autocomplete

I have a Vue component with a tag input where I make an ajax call to the db to retrieve suggestions as the user is typing. I am using #johmun/vue-tags-input for this. Everything works fine except that instead of the autocomplete listing options including only the tag attribute of the Tag model, it includes the entire object.
I want to list only the tag attribute in the view, but I want to reference the array of entire Tag objects when it comes time to create the association with the user.
This what the current dropdown look like in the browser:
Here is my input component removing the irrelevant parts, so it meets SO's size constraints:
<template>
<div >
<b-container class="mt-8 pb-5">
<b-row class="justify-content-center">
<b-col lg="5" md="7">
<form>
...
<div v-if="step === 3">
<h2><strong>What topics are you interested in?</strong> (e.g tag1, tag2, etc...)</h2>
<h2>A few popular ones:
<button #click.prevent="addToTags(item)" class="btn btn-sm btn-success" v-for="item in existingTags.slice(0, 3)" :key="item.id">
{{ item.tag }}
</button>
</h2>
<vue-tags-input
v-model="tag"
v-on:keyup.native="getTags"
:tags="tags"
:autocomplete-items="filteredItems"
:autocomplete-min-length=3
#tags-changed="confirmedTags"
/>
</div>
...
</form>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import VueTagsInput from '#johmun/vue-tags-input';
import UsersService from '#/services/UsersService'
import TagsService from '#/services/TagsService'
import TagRelationsService from '#/services/TagRelationsService'
export default {
name: 'UserOnboard',
data() {
return {
tag: '',
tags: [],
...
}
};
},
components: {
VueTagsInput
},
computed: {
filteredItems() {
return this.existingTags.filter(i => {
return i.tag.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
...
user() {
return this.$store.state.auth.user
},
existingTags() {
return this.$store.state.tags.existingTags
}
},
...
methods:{
...
},
addToTags(newTag) {
if (!this.tags.includes(newTag)) {
this.tags.push(newTag)
}
// on button click add appropriate tag to tags array
// console.log('tag array is: ',tags)
},
confirmedTags(event) {
this.tags=event
console.log(event)
},
...
getTags() { //debounce need to be inside conditional
console.log('gettin tags')
// if (this.tag.length >2) {
this.$store.dispatch('debounceTags', this.tag)
// }
}
}
}
</script>
Also, here is the debounceTags method which runs via vuex:
import TagsService from '#/services/TagsService'
import { debounce } from "lodash";
export const state = {
existingTags: []
}
export const mutations = {
setTags (state, tags) {
state.existingTags = tags
}
}
export const actions = {
debounceTags: debounce(({ dispatch }, data) => {
console.log("Inside debounced function.");
dispatch("getTags" ,data);
}, 300),
async getTags ({ commit }, data) {
await TagsService.getTags(data)
.then(function (response) {
console.log('before setting tags this is resp.data: ', response)
commit('setTags', response);
});
}
}
export const getters = {
}

Can you pass an external function to a Vue component as a prop?

I have a legacy codebase mostly built in PHP. I'm researching how to turn commonly used parts of the code into re-usable Vue components that can be plugged in as needed.
In one case, I have an onclick event in the html which will need to be individually passed to a child component. onclick="func()"
I want to be able to pass that func to the component from the markup, without defining this one-time use function as a property method either on the component or its parents.
I can't find anything in the Vue docs or elsewhere on how to do that. Every attempt I make gives an error:
Property or method "hi" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
Is there a way to pass externally-defined functions in the global scope to a Vue instance?
Vue tabs:
Vue.config.devtools = true;
Vue.component('tabs', {
template: `
<div class="qv-tabs">
<div class="tab">
<ul>
<li v-for="tab in tabs"
:class="{'is-active' : tab.isActive}"
#click="tab.callHandler"
>
<a :href="tab.href" #click="selectTab(tab)">{{tab.name}}</a>
</li>
</ul>
</div>
<div class="tab-content">
<slot></slot>
</div>
</div>
`,
data(){
return{
tabs: []
};
},
created(){
this.tabs = this.$children;
},
methods:{
selectTab(selectedTab){
this.tabs.forEach(tab => {
tab.isActive = (tab.name == selectedTab.name);
});
},
otherHi() {
alert('other hi');
}
}
});
Vue.component('tab', {
template: `
<div v-show="isActive">
<slot></slot>
</div>
`,
props: {
name: {required: true},
selected: {default: false},
callHandler: Function,
clickHandler: {
type: Function,
default: function() { console.log('default click handler') }
}
},
data(){
return{
isActive: false
}
},
methods: {
callHandler() {
console.log('call handler called');
this.clickHandler();
}
},
computed:{
href(){
return '#' + this.name.toLowerCase().replace(/ /g, '-');
}
},
mounted(){
this.isActive = this.selected;
}
});
new Vue({
el: '.app',
methods: {
vueHi() { alert('hi from vue'); }
}
});
function hi() {
alert('hi!');
}
Markup:
<div class="app">
<tabs>
<tab name="Tab 1" :selected="true" v-bind:call-handler="hi">
<p>Tab content</p>
</tab>
<tab name="Tab 2">
<p>Different content for Tab 2</p>
</tab>
</tabs>
</div>
You could define your methods like this in your component :
...
methods: {
hi
}
...
But you will have to define it in every component where you need this function. Maybe you can use a mixin that define the methods you want to access from yours components, and use this mixins in these components ? https://v2.vuejs.org/v2/guide/mixins.html
Anoter solution ( depending on what you try to achieve ) is to add your method to the Vue prototype like explained here :
https://v2.vuejs.org/v2/cookbook/adding-instance-properties.html
Vue.prototype.$reverseText = function(string) {
return string.split('')
.reverse()
.join('')
}
With this method defined in the Vue prototype, you can use the reverseText method like this in all of your components template :
...
<div> {{ $reverseText('hello') }} </div>
...
Or from yours script with this :
methods: {
sayReverseHello() {
this.$reverseText('hello')
}
}

Categories

Resources