I have a component like this
text-box.vue
<template>
<3rd-party-text v-bind="$attrs" />
</template>
using it like
...
<text-box :value="my value" />
...
Now the issue is how to write a unit test for text-box.vue so that I can validate if the $attrs is updated, something like
wrapper = mount(TextBox, {
attrs: { value: 'test value' }
});
wrapper.vm.$attrs.value = 'update';
console.log(wrapper.vm.$refs.tb.value);
// still showing the 'test value' i.e. the one which was provided on mount
I see there are methods like setData and setProps but how to update $attrs?
Related
<div class="fligtInput flex flex-no-wrap">
<SelectFlight
v-model="flight.model1" placeholder="from"
></SelectFlight>
<SelectFlight
v-model="flight.model2" placeholder="to"
></SelectFlight>
</div>
I use (select option) SelectFlight Component and this returns a JSON object. I wanna take the selected options for both components in parent component.
I will take a stab at an example showing multiple options, knowing I don't have the full context of your specific problem, but I am sure this example will get you pointed in the right direction.
I took liberty with object names, and assumed values in JSON objects. Obviously you will ned to adjust this for your specific use case and data.
<template>
<div class="i-am-the-parent">
<!--
Display flight from name
-->
<div
v-if="flights.from"
class="display-text-from"
>
{{ flights.from.airportName }}
</div>
<SelectFlight
v-model="flights.from"
placeholder="from"
/>
<!-- Bind flight to "airportName" as a prop in parent -->
<ParentWrapperComponent
class="display-text-to"
:airport-name="flights.to ? flights.to.airportName : null"
>
<!--
Nested in ParentWrapperComponent
-->
<SelectFlight
v-model="flights.to"
placeholder="to"
/>
</ParentWrapperComponent>
<!--
Kitchen Sink
Here I am demonstrating multiple approaches:
1. Merge the data via a computed property and pass as a property named airports
2. Simply pass the flights data property as a flights property
3. Simply pass the properties "flights.from" data as property "from" and "flights.to" data as property "to"
-->
<SomeComponentToShowBoth
:airports="combineValues"
:flights="flights"
:from="flights.from"
:to="flights.to"
/>
</div>
</template>
<script>
import SelectFlight from '#/path/to/component/SelectFlight'
export default {
components: {
SelectFlight
},
data () {
return {
flights: {
from: null,
to: null
}
}
},
computed: {
combineValues () {
// this will return both json objects merged into one.
// It will update everytime this.flights.from or this.flights.to changes
return Object.assign({}, this.flights.from, this.flights.to)
},
combineFlightNames () {
// Perhaps you just want to pass the names?
return {
from: flights.from ? flights.from.airportName : null,
to: flights.to ? flights.to.airportName : null
}
}
}
}
</script>
References:
Vue Computed Properties
Vue Component Props
Vue Reactivity
In a Vue Native project I have been passing eventName as a prop from the parent component to the child component and then having the child component emit this eventName back to the parent component with whatever data it is providing to the parent. Here is an example with the pertinent sections of code shown:
parent: settings.vue
// code in template
<dynamic-picker
#event-setting1="(value) => setting1 = value"
:eventName="'event-setting1'"
:value="setting1"
:choices="config.picker.setting1.choices"
/>
<dynamic-picker
#event-setting2="(value) => setting2 = value"
:eventName="'event-setting2'"
:value="setting2"
:choices="config.picker.setting2.choices"
/>
<dynamic-picker
#event-setting3="(value) => setting3 = value"
:eventName="'event-setting3'"
:value="setting3"
:choices="config.picker.setting3.choices"
/>
// code in script
import DynamicPicker from '../../../components/dynamic-picker.vue';
export default {
components: {
DynamicPicker
}
}
child: dynamic-picker.vue
// code in template
<dynamic-picker
:items="choices"
:selected="selected"
:onValueChange="(value) => $emit(eventName, value)"
/>
// code in script
props: ['value', 'eventName', 'choices']
However, it seems to work if the child has a static event name, in which case the code changes to this:
parent: settings.vue
// code in template
<dynamic-picker
#event-setting="(value) => setting1 = value"
:value="setting1"
:choices="config.picker.setting1.choices"
/>
<dynamic-picker
#event-setting="(value) => setting2 = value"
:value="setting2"
:choices="config.picker.setting2.choices"
/>
<dynamic-picker
#event-setting="(value) => setting3 = value"
:value="setting3"
:choices="config.picker.setting3.choices"
/>
child: dynamic-picker.vue
// code in template
<dynamic-picker
:items="choices"
:selected="selected"
:onValueChange="(value) => $emit('event-setting', value)"
/>
// code in script
props: ['value', 'choices']
While I like this second approach better than the first because it is simpler, I don't understand why it works without causing issues. How can the child component be called multiple times in the parent file and emit the same event name no matter whether setting1, setting2 or setting3 is calling it without causing problems? I search the Vue docs but did not see this addressed.
Edit:
Two clarifications -
The dynamic-picker tag that appears in dynamic-picker.vue is imported as follows into that file:
import DynamicPicker from './dynamic-picker.js';
Added code above that appears in script tags for settings.vue.
If I understand your question correctly, each dynamic-picker tag maps to its own unique instance of a dynamic-picker component. As such, there is a one to one mapping from a given dynamic-picker to its parent, and only that one parent will hear an emit from the child.
For example, the first tag in the parent corresponds with child instance one, the second tag in the parent corresponds with child instance two, etc. Now, only the first tag is the parent of instance one. So, only the first tag will respond to an emit from instance one.
In my Vue component, I have a computed property which watches the state and the component, and returns an object.
currentOrganization () {
if (this.$store.state.organizations && this.selectedOrganization) {
return this.$store.state.organizations.filter(org => org.id === this.selectedOrganization)[0];
}
}
Unfortunately, this property does not update automatically when the component loads, though I can see that I have both this.$store.state.organizations and this.selectedOrganization. It does, however, update when I dispatch an action to my Vuex store via the select component below:
<b-form-select id="orgSelect"
v-model="selectedOrganization"
:options="this.$store.state.organizations.map(org => {return {value: org.id, text:org.name}})"
size="sm"
v-on:input="currentOrganizationChange()"
class="w-75 mb-3">
<template slot="first">
<option :value="null" disabled>Change Organization</option>
</template>
</b-form-select>
The component dispatches a Vuex action:
currentOrganizationChange () {
this.$store.dispatch('setCurrentOrganization', this.selectedOrganization);
},
How can I make currentOrganization() compute initially?
The computed will not calculate until you use it.
console.log it, or just write current organization; somewhere in code which executes and it will calculate :)
You should ensure that your computed always returns a value. A linter would have warned you of that error. Chances are your condition is failing -- perhaps this.selectedOrganization is falsy.
Im coming from a React background and it's simply enough to set your state from a prop and you could call setState({...}) to update the state, so, with vue / vuex, I find it difficult.
To simplify:
Vuex State
name: "Foo bar"
Vuex Action
addName
I can change the state no problem but I need to bind an input field and when change, the state is updated. Think of this as an update form where the user details are already pre-filled and they can change their name.
<input #change="addName(newName) v-model="newName" />
I could add a watch to watch for newName and update the state but, I need to pre-fill the input with the state. Ha! I could use beforeMount() but my state is not loaded as yet.
computed: {
...mapState([
'name'
]),
},
beforeMount() {
// this.newName = this.name
console.log('Mounted') // Shows in console
console.log(this.name) // nothing
}
Name shows in templete <pre>{{ name }}</pre>
Yo can use a computed setter
computed:{
name:{
get: function(){
return store.state.name;
},
set: function(newName){
store.dispatch('addName',newName);
}
}
}
enter code here
And set the v-model to the computed property name in your <input> tag :
<input v-model="name" />
Here is the working jsfiddle
I am loading data from the database which drives what type of component I display
An AJAX call goes off and returns me some data (this can be restructured if needed)
{
component_type: 'list-item',
props: {
name: 'test',
value: 'some value'
}
}
This is accessible on my parent object a variable called component
Within the template of my parent object I have the following
<component :is="component.component_type" ></component>
This works fine and loads the component as expected.
Next I want to add the properties from my data object into this tag too
<component :is="component.component_type" {{ component.props }} ></component>
This doesn't work and rejects writing a tag with {{ in it. I presume this is an error thrown by the browser rather than Vue, although I'm unsure.
For reference I want the output to actually look like:
<component :is="component.component_type" name='test' value='some value' ></component>
How can I go about passing in these properties? Ideally I'd like these to be tied to data / props of the parent as I'm showing so that they can easily be changed in database and the UI will change accordingly.
At worst I will generate it all on server side, but I'd rather do it via ajax as I'm currently trying to do.
In case anyone is wondering how to do this using Vue 2 you can just pass an object to v-bind:
<template>
<component :is="componentType" v-bind="props"></component>
</template>
<script>
export default {
data() {
return {
componentType: 'my-component',
props: {
foo: 'foofoo',
bar: 'barbar'
}
}
}
}
</script>
Following this thread, I see two options to do this.
one is to pass a single prop which is an object in itself, and pass all the relevant key values in it which can be used by the child component, something like following:
<component :is="component. component_type"
:options="component.props"
</component>
Other solution mentioned is to have a directive, where you pass the object and it will set the attributes which are keys in that object to corresponding values, you can see this in work here.
Vue.directive('props', {
priority: 3000,
bind() {
// set the last component child as the current
let comp = this.vm.$children[this.vm.$children.length - 1];
let values = null;
if(this._scope && this._scope.$eval) {
values = this._scope.$eval(this.expression);
} else {
values = this.vm.$eval(this.expression);
}
if(typeof values !== 'object' || values instanceof Array) {
values = { data: values };
}
// apply properties to component data
for(let key in values) {
if(values.hasOwnProperty(key)) {
let hkey = this.hyphenate(key);
let val = values[key];
if(typeof val === 'string') {
comp.$options.el.setAttribute(hkey, values[key]);
} else {
comp.$options.el.setAttribute(':' + hkey, values[key]);
}
}
}
console.log(comp.$options.el.outerHTML);
comp._initState();
},
/*
* Hyphenate a camelCase string.
*/
hyphenate(str) {
let hyphenateRE = /([a-z\d])([A-Z])/g;
return str.replace(hyphenateRE, '$1-$2').toLowerCase();
}
});
and use it like this:
<div class="app">
<component is="component.component_type" v-props="component.props"></component>
</div>
As far as I know, there is no rest props (spread props) syntax in Vue.js template.
A possible solution is render functions. You have full power of JavaScript when using render functions, so you can do something like this:
render (h) {
return h('foo', {
props: {
...yourData
}
})
}
I created a simple example here: http://codepen.io/CodinCat/pen/bgoVrw?editors=1010
both component type (like foo) and props can be dynamic (but you still need to declare all the possible fields of prop (like a, b and c) in your child components)
There is another solution is JSX.
Use the JSX babel plugin: https://github.com/vuejs/babel-plugin-transform-vue-jsx
then you can use the spread props syntax:
return <foo {...{yourData}}></foo>