How to reference text that's in '<slot></slot>' in Vue.js - vue.js

How to reference text that's in in Vue.js?
Vue.component('component', {
template: `<button><slot></slot></button>`,
created: function() {
// i would like to access the text in slot here
}
});

Note: This answer applies to Vue v2 only.
The content inside the default slot, which is what you are describing, is exposed as this.$slots.default in the Vue. So the most naive way to get the text inside your button would be to use this.$slots.default[0].text.
Vue.component('component', {
template: `<button><slot></slot></button>`,
created: function() {
const buttonText = this.$slots.default[0].text;
}
});
The problem is that there may be more than one node inside the slot, and the nodes may not necessarily be text. Consider this button:
<button><i class="fa fa-check"></i> OK</button>
In this case, using the first solution will result in undefined because the first node in the slot is not a text node.
To fix that we can borrow a function from the Vue documentation for render functions.
var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
And write
Vue.component("mybutton", {
template:"<button><slot></slot></button>",
created(){
const text = getChildrenTextContent(this.$slots.default);
console.log(text)
}
})
Which will return all the text in the slot joined together. Assuming the above example with the icon, it would return, "OK".

For Vue 3.
The answer from #bert works well on Vue 2, but Vue 3 slots have a more complex structure.
Here is one way to get the slots text contents (from default slot) on Vue 3.
const getSlotChildrenText = children => children.map(node => {
if (!node.children || typeof node.children === 'string') return node.children || ''
else if (Array.isArray(node.children)) return getSlotChildrenText(node.children)
else if (node.children.default) return getSlotChildrenText(node.children.default())
}).join('')
const slotTexts = this.$slots.default && getSlotChildrenText(this.$slots.default()) || ''
console.log(slotTexts)

Run the code snippet below that get the slot text passed by parent :
I'm using "ref" :
<span ref="mySlot">
this.$refs.mySlot.innerHTML
Careful : <slot ref="refName"></slot> don't works because <slot> are not render on html.
You have to wrap the <slot></slot> with <div></div> or <span></span>
The code :
Vue.component('component', {
template: '<button>' +
'<span ref="mySlot">' +
'Text before<br />' +
'<slot name="slot1">' +
'Text by default' +
'</slot>' +
'<br />Text after' +
'</span>' +
'</button>',
mounted: function() {
console.log( this.$refs.mySlot.innerHTML);
}
});
new Vue({
el: '#app'
});
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<component>
<span slot="slot1">I'm overriding the slot and text appear in this.$refs.mySlot.innerHTML !</span>
</component>
</div>

You can access the slot text by joining the innerText of all the children inside the slot.
getSlotText() {
return this.$slots.default.map(vnode => (vnode.text || vnode.elm.innerText)).join('');
},

My use case was pretty simple, I had a default slot with only text.
Here's how I accessed the text in vue3 with script setup:
<script setup lang="ts">
import { computed, useSlots } from "vue";
const slots = useSlots();
const slotText = computed(() => {
return slots.default()[0].children; // This is the interesting line
});
</script>

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 do I pass the value from child to parent with this.$emit

What I trying to achieve here is to pass the const randomNumber inside the child component [src/components/VueForm/FormQuestion.vue] that need to be passed to parent component [src/App.vue]. Therefore I use $emit to pass the date, but since this is my first time working with $emit, I am not really sure how to do that. Could someone help me with this.
In order to run this app, I would add a working code snippet. Click on the start button and fill in the input fields. When the input field validates correctly it will pop up the button and if the user clicks on that is should pass the data to the parent. At the end it should be stored inside the App.vue in localStorage, so therefore I want to receive the randomNumber from that child component.
working code snippet here
// child component
<template>
<div class="vue-form__question">
<span class="question" :class="{ big: !shouldShowNumber }"> {{ getRandomNumber() }} </span>
</div>
</template>
<script>
export default {
methods: {
getRandomNumber() {
const randomNumber = Math.floor((Math.random() * 3) + 1);
const question = this.question.question;
this.$emit('get-random-number', question[randomNumber]);
return question[randomNumber];
}
}
};
// parent component
<template>
<div id="app">
<vue-form
:data="formData"
#complete="complete"
#getRandomNumber="newRandomNumber"
></vue-form>
</div>
</template>
<script>
import VueForm from "#/components/VueForm";
import data from "#/data/demo";
export default {
data() {
return {
formData: data
}
},
components: {
VueForm
},
created() {
this.complete()
},
methods: {
complete(data) {
// Send to database here
// localStorage.setItem('questions', data.map(d => d.question[this.randomNumber] + ': ' + d.answer));
},
}
};
</script>
v-on:get-random-number (or the superior short-hand syntax: #get-random-number). Just like you'd listen to any other event, such as #click or #mouseenter.
Though I don't know off the top of my head if dashes are valid in event names. Might have to camelcase it.

How to change vue.js components data outside scope

I want to change vue.js data outside the default export statement. Given the example below, how would I go about doing that?
<template>
<div>
<h6 class="font-weight-normal mb-3">{{ name }}</h6>
</div>
</template>
<script>
export default {
data() {
return {
name: ""
}
}
}
let changeName = (name) => {
//How do I change the name data property here
}
</script>
If you assign the component to a variable/constant, you should be able to simply trigger the proxy setter of the data object or with component-level methods.
const component = new Vue({
data() {
return {
name: "Initial value."
}
},
methods: {
changeName(newName) {
this.name = newName;
}
}
});
// Mount it to an element (for demo purposes)
component.$mount('#app');
document.getElementById('btn-setter').onclick = function() {
component.name = 'Changed with SETTER';
};
document.getElementById('btn-method').onclick = function() {
component.changeName('Changed with METHOD');
};
// Uncomment this to start exporting it.
// export default component;
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h6 class="font-weight-normal mb-3">{{ name }}</h6>
<button id="btn-setter">Change with setter</button>
<button id="btn-method">Change with method</button>
</div>
You can write any function you want in the page outside of the component (or export statement) but you would need to invoke it in your methods section or somewhere in the component. I use this for functions that create default values, instead of importing them from outside just write a function initVal = () => someVal then in the data or computed or somewhere reference initVal (no this).

Caret position in editable div after prop change using Vue.js

I'm using Vue.js and have the following code.
When I type in div and this.content is updated, the caret is always reset to the beginning.
<template>
<div>
<div contenteditable="true"
v-html="content"
#input="onContentChange($event)">
</div>
</div>
</template>
<script>
export default {
props: ['content'],
methods: {
onContentChange: function(e) {
this.content = e.target.innerHTML;
},
},
}
</script>
<style>
</style>
How can I preserve the caret's position and update the content?
I've seen some other similar posts, but the solutions there either are not for Vue.js, or don't work in my case, or I might have failed to apply them correctly.
I've tested a few scenarios and I think what you actually need is plainly the Create a reusable editable component in this post.
However, if you want to have everything in one component, in chrome the following code works:
<template>
<div
ref="editable"
contenteditable
#input="onInput"
>
</div>
</template>
<script>
export default {
data () {
return {
content: 'hello world'
}
},
mounted () {
this.$refs.editable.innerText = this.content
},
methods: {
onInput (e) {
this.content = e.target.innerText
}
}
}
</script>
Note that the Vue plugin in Chrome doesn't seem to update correctly the value of content in this scenario, therefore you have to click on refresh on the top right of the vue plugin.
First we preserve current click on the contenteditable, then change HTML content, and set new selection.
const range = document.getSelection().getRangeAt(0)
const pos = range.endOffset
this.$el.innerHTML = this.content
const newRange = document.createRange()
const selection = window.getSelection()
const node = this.$el.childNodes[0]
newRange.setStart(node, node && pos > node.length ? 0 : pos)
newRange.collapse(true)
selection.removeAllRanges()
selection.addRange(newRange)

How can I test a custom input Vue component

In the Vue.js documentation, there is an example of a custom input component. I'm trying to figure out how I can write a unit test for a component like that. Usage of the component would look like this
<currency-input v-model="price"></currency-input>
The full implementation can be found at https://v2.vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
The documentation says
So for a component to work with v-model, it should (these can be configured in 2.2.0+):
accept a value prop
emit an input event with the new value
How do I write a unit test that ensures that I've written this component such that it will work with v-model? Ideally, I don't want to specifically test for those two conditions, I want to test the behavior that when the value changes within the component, it also changes in the model.
You can do it:
Using Vue Test Utils, and
Mounting a parent element that uses <currency-input>
Fake an input event to the inner text field of <currency-input> with a value that it transforms (13.467 is transformed by <currency-input> to 13.46)
Verify if, in the parent, the price property (bound to v-model) has changed.
Example code (using Mocha):
import { mount } from '#vue/test-utils'
import CurrencyInput from '#/components/CurrencyInput.vue'
describe('CurrencyInput.vue', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data: { price: null },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
})
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
In-browser runnable demo using Jasmine:
var CurrencyInput = Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\
v-bind:value="value"\
v-on:input="updateValue($event.target.value)">\
</span>\
',
props: ['value'],
methods: {
// Instead of updating the value directly, this
// method is used to format and place constraints
// on the input's value
updateValue: function(value) {
var formattedValue = value
// Remove whitespace on either side
.trim()
// Shorten to 2 decimal places
.slice(0, value.indexOf('.') === -1 ? value.length : value.indexOf('.') + 3)
// If the value was not already normalized,
// manually override it to conform
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// Emit the number value through the input event
this.$emit('input', Number(formattedValue))
}
}
});
// specs code ///////////////////////////////////////////////////////////
var mount = vueTestUtils.mount;
describe('CurrencyInput', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data() { return { price: null } },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
});
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
// load jasmine htmlReporter
(function() {
var env = jasmine.getEnv()
env.addReporter(new jasmine.HtmlReporter())
env.execute()
}())
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css">
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
<script src="https://npmcdn.com/vue#2.5.15/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-template-compiler#2.5.15/browser.js"></script>
<script src="https://rawgit.com/vuejs/vue-test-utils/2b078c68293a41d68a0a98393f497d0b0031f41a/dist/vue-test-utils.iife.js"></script>
Note: The code above works fine (as you can see), but there can be improvements to tests involving v-model soon. Follow this issue for up-to-date info.
I would also mount a parent element that uses the component. Below a newer example with Jest and Vue Test Utils. Check the Vue documentation for more information.
import { mount } from "#vue/test-utils";
import Input from "Input.vue";
describe('Input.vue', () => {
test('changing the input element value updates the v-model', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Brendan Eich';
await wrapper.find('input').setValue(name);
expect(wrapper.vm.$data.name).toBe(name);
});
test('changing the v-model updates the input element value', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Bjarne Stroustrup';
await wrapper.setData({ name });
const inputElement = wrapper.find('input').element;
expect(inputElement.value).toBe(name);
});
});
Input.vue component:
<template>
<input :value="$attrs.value" #input="$emit('input', $event.target.value)" />
</template>