I don't know how to change a dynamic component based on a selectbox option with two-way binding to a Vuex Store.
My single file component (Fieldvalue) houses this dynamic component and also the selectbox.
Its type value comes from a Vuex Store, where it can be found by the fieldId, which is passed through the parent component.
So to a field with an ID like 41, there is a fieldvalue component.
The get() function of fieldtypeComponent has somehow to know the passed through fieldId value from the props, that the parent passes down, to have the ID for searching in the fieldvalues array of the store.
But it does not work how I'm doing it. (commented out lines)
Any ideas?
My Vuex store getter:
export function getFieldvalueTypeByFieldnameId(state){
return (fieldId) => {
const fieldvalue = state.form.fieldvalues.find(e => e.fieldId == fieldId);
return fieldvalue.type;
}
}
Mutation:
export function setFieldvalueType(state, obj){
const { type, fieldId } = obj;
const fieldvalue = state.form.fieldvalues.find( e => e.fieldId === fieldId );
fieldvalue.type = type;
}
My Vuex store state example:
{
"form": {
"name": "",
"fieldvalues": [
{
"formularId": null,
"fieldId": 41,
"value": {},
"type": ""
},
{
"formularId": null,
"fieldId": 44,
"value": {},
"type": ""
}
]
}
}
My single file component:
<template>
<div class="row">
<div class="col-3 self-center">
<q-select
square
filled
v-model="fieldtypeComponent"
:options="fieldtypes"
option-value="component"
map-options
emit-value
label="Type"
/>
</div>
<div class="col">
<template
v-if="fieldtypeComponent === null || fieldtypeComponent == undefined">
<p>no type selected</p>
</template>
<component v-else :is="fieldtypeComponent"></component>
</div>
</div>
</template>
<script>
import IconField from "components/fieldtypes/IconField";
import TextField from "components/fieldtypes/TextField";
export default {
name: "Fieldvalue",
data() {
return {
selected: "",
fieldtypes: [
{ label: "Icon", component: "IconField" },
{ label: "Text", component: "TextField" }
]
};
},
components: {
IconField,
TextField
},
computed: {
fieldtype: function() {
return this.$store.getters["formular/getFieldvalueTypeByFieldnameId"];
},
fieldtypeComponent: {
get() {
//return this.fieldtyp(this.fieldnameId);
//return this.$store.getters["formular/getFieldvalueType"];
return null;
},
set(type) {
const payload = {
type,
fieldId: this.forFieldnameId
};
this.$store.commit("formular/setFieldvalueType", payload);
}
}
},
props: {
forFieldnameId: { type: Number, required: true }
},
methods: {
changeComponent() {
this.fieldtypeComponent = this.selected;
const payload = {
type: this.selected,
fieldId: this.forFieldnameId
};
this.$store.commit("tarife/setFieldvalueTyp", payload);
}
}
};
</script>
Additional info on what I want to do:
I have some fields defined in a formular with a name. To each fieldname from that form I want to save a value later.
But that value (a javascript object/JSON) is different depending on the choosen component from the select input. (TextField or IconField)
I don't know if my way of passing down is right or too complex. Especially regarding to two-way binding or just emitting up to the parent component and let it handle the complete logic.
I need to somehow bind the value from in the chosen component to the Vuex store fieldvalue entry too.
Maybe my approach is wrong and I should do it totally different?
Related
I have a vuex store of "nodes". Each one has a type of Accordion or Block.
{
"1":{
"id":1,
"title":"Default title",
"nodes":[],
"type":"Block"
},
"2":{
"id":2,
"title":"Default title",
"nodes":[],
"type":"Accordion"
}
}
When I use the type to create a dynamic component it works great:
<ul>
<li v-for="(node, s) in nodes" :key="parentId + s">
<component :is="node.type" :node="node" :parent-id="parentId"></component>
</li>
</ul>
But when I change it, nothing happens in the view layer:
convert(state, { to, id }) {
state.nodes[id].type = to;
Vue.set(state.nodes[id], "type", to);
},
I even use Vue.set. How can I make this update?
It updates immediately if I then push another node into the array.
CodeSandbox:
https://codesandbox.io/s/romantic-darwin-dodr2?file=/src/App.vue
The thing is that your getter will not work, because it's not pure: Issue. But you can use deep watcher on your state instead:
<template>
<div class="home">
<h1>Home</h1>
<Sections :sections="nodesArr" :parent-id="null"/>
</div>
</template>
<script>
// # is an alias to /src
import Sections from "#/components/Sections.vue";
import { mapState } from "vuex";
export default {
name: "home",
components: {
Sections
},
data: () => {
return {
nodesArr: []
};
},
computed: {
...mapState(["nodes", "root"])
},
watch: {
root: {
handler() {
this.updateArr();
},
deep: true
}
},
mounted() {
this.updateArr();
},
methods: {
updateArr() {
this.nodesArr = this.root.map(ref => this.nodes[ref]);
}
}
};
</script>
I'm trying to set up a Vue component that takes a flat list of items in an array, groups them by a property for use in a sub-component, and emits the updated flat array.
My section component uses these grouped items in their v-model and emits the updated list. The section component is a drag-and-drop with some input fields, so items are changed under the section component and the updated list is emitted.
Here's an example of the component that takes the flat list as a prop:
<template>
<div>
<div v-for="section in template.sections" :key="section.id">
<h2>{{ section.name }}</h2>
<item-section :section="section" v-model="sectionData[section.id]"></item-section>
</div>
</div>
</template>
<script type="text/javascript">
import { groupBy } from "lodash";
import ItemSection from "#/components/Section.vue";
export default {
name: "ItemAssignment",
props: {
// All items in flat array
value: {
type: Array,
required: true,
default: () => [
/**
* {
* id: null,
* section_id: null,
* name: null
* }
*/
]
},
// Template (containing available sections)
template: {
type: Object,
default: () => {
return {
sections: [
/**
* {
* id: null,
* name: null
* }
*/
]
};
}
}
},
components: {
ItemSection
},
data() {
return {
sectionData: []
};
},
mounted() {},
computed: {
flattenedData() {
return Object.values(this.sectionData).flat();
}
},
methods: {},
watch: {
// Flat list updated
value: {
immediate: true,
deep: true,
handler(val) {
this.sectionData = groupBy(val, "section_id");
}
},
// --- Causing infinite loop ---
// flattenedData(val) {
// this.$emit("input", val);
// },
}
};
</script>
The parent of this component is basically this:
<template>
<div>
<!-- List items should be updatable here or from within the assignment component -->
<item-assignment v-model="listItems"></item-assignment>
</div>
</template>
<script type="text/javascript">
import ItemAssignment from "#/components/ItemAssignment.vue";
export default {
name: "ItemExample",
props: {
},
components: {
ItemAssignment
},
data() {
return {
listItems: []
};
},
mounted() {},
computed: {
},
methods: {
// Coming from API...
importExisting(list) {
var newList = [];
list.forEach(item => {
const newItem = {
id: null, // New record, so don't inherit ID
section_id: item.section_id,
name: item.name
};
newList.push(newItem);
});
this.listItems = newList;
}
},
watch: {
}
};
</script>
When emitting the finalized flat array, Vue goes into an infinite loop trying to re-process the list and the browser tab freezes up.
I believe the groupBy and/or Object.values(array).flat() method are stripping the reactivity out so Vue constantly thinks it's different data, thus the infinite loop.
I've tried manually looping through the items and pushing them to a temporary array, but have had the same issue.
If anyone knows a way to group and flatten these items while maintaining reactivity, I'd greatly appreciate it. Thanks!
So it makes sense why this is happening...
The groupBy function creates a new array, and since you're watching the array, the input event is triggered which causes the parent to update and pass the same value which gets triggered again in a loop.
Since you're already using lodash, you may be able to include the isEqual function that can compare the arrays
import { groupBy, isEqual } from "lodash";
import ItemSection from "#/components/Section.vue";
export default {
// ...redacted code...
watch: {
// Flat list updated
value: {
immediate: true,
deep: true,
handler(val, oldVal) {
if (!isEqual(val, oldVal))
this.sectionData = groupBy(val, "section_id");
}
},
flattenedData(val) {
this.$emit("input", val);
},
}
};
this should prevent the this.sectionData from updating if the old and new values are the same.
this could also be done in flattenedData, but would require another value to store the previous state.
I am using quasar framework and vuex for my app. The parent component is rendering child components with the data from vuex store. The child component is contenteditable and if i press enter key on it, the store is updated. But the computed property in parent component is not updating.
Here is my code:
parent-component.vue
<template>
<div>
<div v-for="(object, objKey) in objects"
:key="objKey">
<new-comp
:is=object.type
:objId="object.id"
></new-comp>
</div>
</div>
</template>
<script>
import ChildComponent from './child-component';
export default {
name: 'ParentComponent',
components: {
ChildComponent
},
computed : {
objects(){
return this.$store.state.objects.objects;
}
},
mounted() {
this.assignEnterKey();
},
methods: {
assignEnterKey() {
window.addEventListener('keydown',function(e) {
if(e.which === 13) {
e.preventDefault();
}
});
}
}
}
child-component.vue
<template>
<div contenteditable="true" #keydown.enter="addChildComp" class="child-container">
Tell your story
</div>
</template>
<script>
export default {
name: 'ChildComponent',
props: ['objId'],
data() {
return {
id: null
}
},
computed : {
serial(){
return this.$store.state.objects.serial;
}
},
methods: {
addChildComp() {
let newId = this.objId + 1;
let newSerial = this.serial + 1;
this.$store.commit('objects/updateObjs', {id: newId, serial: newSerial});
}
}
}
state.js
export default {
serial: 1,
objects: {
1:
{
"id" : 1,
"type" : "ChildComponent",
"content" : ""
}
}
}
mutation.js
export const updateObjs = (state, payload) => {
let id = payload.id;
let serial = payload.serial;
state.objects[serial] = {
"id" : id ,
"type" : "ChildComponent",
"content" : ""
};
}
Vuex mutations follow general Vue.js reactivity rules, this means that Vue.js reactivity traps are applicable to vuex mutations.
In order to maintain reactivity, when adding a property to state.objects you should either:
Use the special Vue.set method:
Vue.set(state.objects, serial, { id, "type" : "ChildComponent", "content" : ""})
Or, recreate state.objects object instead of mutating it:
state.objects = { ...state.objects, [serial]: { id, "type" : "ChildComponent", "content" : ""} }
What do I have: two components, parent and child.
Parent
<UserName :name=user.name></UserName>
...
components: {UserName},
data() {
return {
user: {
name: '',
...
}
}
},
created() {
this.fetchUser()
console.log(this.user) //<- object as it is expected
},
methods: {
fetchUser() {
let that = this
axios.get(//...)
.then(response => {
for (let key in response.data) {
that.user[key] = response.data[key]
}
})
console.log(that.user) //<- object as it is expected
}
}
Child
<h3 v-if="!editing" #click="edit">{{ usersName }}</h3>
<div v-if="editing">
<div>
<input type="text" v-model="usersName">
</div>
</div>
...
props: {
name: {
type: String,
default: ''
},
},
data() {
return {
editing: false,
usersName: this.name,
...
}
},
Problem: even when name prop is set at child, usersName data value is empty. I've inspected Vue debug extension - same problem.
What have I tried so far (nothing helped):
1) props: ['name']
2)
props: {
name: {
type: String
},
},
3) usersName: JSON.parse(JSON.stringify(this.name))
4) <UserName :name="this.user.name"></UserName>
P. S. when I pass static value from parent to child
<UserName :name="'just a string'"></UserName>
usersName is set correctly.
I've also tried to change name prop to some foobar. I guessed name might conflict with component name exactly. But it also didn't helped.
user.name is initially empty, and later gets a value from an axios call. usersName is initialized from the prop when it is created. The value it gets is the initial, empty value. When user.name changes, that doesn't affect the already-initialized data item in the child.
You might want to use the .sync modifier along with a settable computed, or you might want to put in a watch to propagate changes from the prop into the child. Which behavior you want is not clear.
Here's an example using .sync
new Vue({
el: '#app',
data: {
user: {
name: ''
}
},
methods: {
fetchUser() {
setTimeout(() => {
this.user.name = 'Slartibartfast'
}, 800);
}
},
created() {
this.fetchUser();
},
components: {
userName: {
template: '#user-name-template',
props: {
name: {
type: String,
default: ''
}
},
computed: {
usersName: {
get() { return this.name; },
set(value) { this.$emit('update:name', value); }
}
},
data() {
return {
editing: false
}
}
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
{{user.name}}
<user-name :name.sync=user.name></user-name>
</div>
<template id="user-name-template">
<div>
<input type="text" v-model="usersName">
</div>
</template>
should be passed like this...
<UserName :name="user.name"></UserName>
if data property is still not being set, in mounted hook you could set the name property.
mounted() {
this.usersName = this.name
}
if this doesn't work then your prop is not being passed correctly.
sidenote: I typically console.log within the mounted hook to test such things.
I'm trying to:
get element's data #click using getDetails method and put it into fileProperties: []
and then send that data to store using fileDetails computed property
This worked for my other components which have v-model and simple true/false state, but I'm not sure how to send the created by the method array of data to the store properly.
In other words, how do I make this computed property to get the data from fileProperties: [] and commit it to store? The fileDetails computed property below is not committing anything.
Code:
[...]
<div #click="getDetails(file)"></div>
[...]
<script>
export default {
name: 'files',
data () {
return {
fileProperties: []
}
},
props: {
file: Object
},
methods: {
getDetails (value) {
this.fileProperties = [{"extension": path.extname(value.path)},
{"size": this.$options.filters.prettySize(value.stat.size)}]
}
},
computed: {
isFile () {
return this.file.stat.isFile()
},
fileDetails: {
get () {
return this.$store.state.Settings.fileDetails
},
set (value) {
this.$store.commit('loadFileDetails', this.fileProperties)
}
}
}
}
</script>
store module:
const state = {
fileDetails: []
}
const mutations = {
loadFileDetails (state, fileDetails) {
state.fileDetails = fileDetails
}
}
Example on codepen:
https://codepen.io/anon/pen/qxjdNo?editors=1011
In this codepen example, how can I send over the dummy data [ { "1": 1 }, { "2": 2 } ] to the store on button click?
You are never setting the value for fileDetails, so the set method of the computed property is never getting called. Here's the documentation on computed setters.
If the fileProperties data is really just the same as the fileDetails data, then get rid of it and set fileDetails directly in your getDetails method.
Here's a working example:
const store = new Vuex.Store({
state: {
fileDetails: null
},
mutations: {
loadFileDetails (state, fileDetails) {
state.fileDetails = fileDetails
}
}
})
new Vue({
el: '#app',
store,
data() {
return {
fileProperties: null
}
},
methods: {
getDetails (value) {
this.fileDetails = [{"1": 1}, {"2": 2}]
}
},
computed: {
fileDetails: {
get () {
return this.$store.state.fileDetails
},
set (value) {
this.$store.commit('loadFileDetails', value)
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<div id="app">
<h1>element data:</h1>
{{fileDetails}}
<hr>
<h1>store data:</h1>
<p>(should display the same data on button click)</p>
{{fileDetails}}
<hr>
<button #click="getDetails">btn</button>
</div>