I have the need to create a dynamic form, which builds up in a tree kind of way. The form can change at any time by the user (who creates/designs the form) and so the inputs I have change dynamically too.
Example:
root
--groeisnelheid
----niveau
------beginner (input radio)
------recreatief (input radio)
------competitie (input radio)
------tour (input radio)
----input text
----begeleiding
------another input
------and another
--another category
----speed
------input
------input
As you can see, not the easiest form... The user (admin user in this case) has the ability to edit or create new forms.
I probably have underestimated the job, since I am trying to create the input side of it, and am already struggling.
What I have done so far:
TreeComponent.vue
<template>
<div class="tree">
<ul class="tree-list">
<tree-node :node="treeData"></tree-node>
</ul>
</div>
</template>
<script>
export default {
props: {
treeData: [Object, Array]
},
data() {
return {
treeValues: []
};
},
methods: {
sendForm: function() {}
}
};
</script>
TreeNodeComponent.vue
<template>
<li v-if="node.children && node.children.length" class="node">
<span class="label">{{ node.name }}</span>
<ul>
<node v-for="child in node.children" :node="child" :key="child.id"></node>
</ul>
</li>
<div v-else class="form-check form-check-inline">
<input
type="radio"
class="form-check-input"
:name="'parent-' + node.parent_id"
:id="node.id"
/>
<label for class="form-check-label">{{ node.name }}</label>
</div>
</template>
<script>
export default {
name: "node",
props: {
node: [Object, Array]
}
};
</script>
This results in all the inputs showing up as I want. But now the real question is; how do I get the value of these inputs in my root component (TreeComponent.vue), so I can send this to the server. Either on change or when the user proceeds in the form.
I am used to working with v-model on this, but I have no clue on how to use this on recursive components, since the documentation only covers setting the data of the direct parent.
Any help would be much appreciated.
One way of doing this is to pass a prop down from TreeComponent to each node.
<template>
<div class="tree">
<ul class="tree-list">
<tree-node :node="treeData" :form="formRepo"></tree-node>
</ul>
</div>
</template>
Then each node passes the prop down to its children.
<node v-for="child in node.children" :node="child" :key="child.id" :form="form"></node>
This way each node will have a direct reference to the TreeComponent.
In each node you can watch the model and update the form prop. You need to use your child.id so you know which field is which.
Your formRepo can be a fully fledged object, but a hash could work just as well.
data() {
return {
treeValues: [],
formRepo: {
}
};
}
Note: if you want formRepo to be reactive you'll need to use Vue.set to add new keys to it.
Thank you for your answer.
I managed to solve it by posting the data to the server on every change, since this was a handy feature.
The way I did so was:
Call function from the input (same call for text inputs)
<input
type="radio"
class="form-check-input"
:name="'parent-' + node.parent_id"
:id="node.id"
#input="change($event, node.id, node.parent_id)"
/>
Have some data variables to fill (Route is required for the axios request)
data() {
return {
input: {
id: null,
parentId: null,
radio: false,
value: null
},
route: null
};
},
And then some magic. The change method. (Left the axios bit out.)
methods: {
change: function(event, id, parentId) {
this.input.parentId = parentId;
this.input.id = id;
if (event.target.value === "on" || event.target.value === "off") {
this.input.radio = true;
this.input.value = event.target.value === "on" ? true : false;
} else {
this.input.value = event.target.value;
}
if (this.input.value) {
axios.put().then().catch()
}
}
}
I know there is some room of improvement in the validation bit. If a user enters 'on' in a text field, this will probably fail. So there is work to be done, but the basic filling of a form is working.
As to if this is the best way, I have no clue, since I'm new to Vue.
Related
Hi I am trying to implement a treeview. I have a component for it which is hierarchical. The problem I am facing is that emit is not working inside tree component. Please help me out. here is my code...
TreeItem.vue
<template>
<div class="node" :style="{ marginLeft: depth * 20 + 'px' }">
<span class="menu-item" v-if="type == 'country'">
<button class="menu-button menu-button--orange" #click="addState()">
Add State
</button>
</span>
<TreeItem
v-if="expanded"
v-for="child in node.children"
:key="child.name"
:node="child"
:depth="depth + 1"
/>
</div></template>
<script>
export default {
name: "TreeItem",
props: {
node: Object,
depth: {
type: Number,
default: 0,
},
emits: ["add-state"],
},
data() {
return {
expanded: false,
};
},
methods: {
addState() {
console.log("Adding State"); //Prints this message to console
this.$emit("add-state");
},
},
};
</script>
AddressManager.vue
Inside template:
<div>
<tree-item :node="treedata" #add-state="addState"></tree-item>
</div>
Inside Methods:
addState() {
console.log("State will be added"); //Never prints this message
},
We need to emit on button click. But the emit is not working.
So I have figured out the problem... We need to replace this.$emit() with this.$parent.$emit()
Here is the change-
addState() {
console.log("Adding State"); //Prints this message to console
this.$parent.$emit("add-state");
},
Hopefully this resolved my issue.
You can use v-on="$listeners" to pass emits on levels above within the depths https://v2.vuejs.org/v2/guide/components-custom-events.html#Binding-Native-Events-to-Components it would just forward them to the parent.
However in nested scenarios it is more convenient using store (vuex) and assign a property then watch property changes from wherever you would like to notice its change. If you use vuex: https://vuex.vuejs.org/api/#watch store properties can be watched.
I'm trying to render components depending on the state of an array in the parent (App.vue). I'm not sure at all that this is the correct approach for this use case (new to Vue and not experienced programmer) so I will gladly take advice if you think this is not the right way to think about this.
I'm trying to build a troubleshooter that consists of a bunch of questions. Each question is a component with data that look something like this:
data: function() {
return {
id: 2,
question: "Has it worked before?",
answer: undefined,
requires: [
{
id: 1,
answer: "Yes"
}
]
}
}
This question is suppose to be displayed if the answer to question 1 was yes.
My problem is I'm not sure on how to render my components conditionally. Current approach is to send an event from the component when it was answered, and to listen to that event in the parent. When the event triggers, the parent updates an array that holds the "state" of all answered questions. Now I need to check this array from each component to see if there are questions there that have been answered and if the right conditions are met, show the question.
My question is: How can I check for data in the parent and show/hide my component depending on it? And also - is this a good idea or should I do something different?
Here is some more code for reference:
App.vue
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions"/>
<two #changeAnswer="updateActiveQuestions"/>
</div>
</div>
</template>
<script>
import one from './components/one.vue'
import two from './components/two.vue'
export default {
name: 'app',
components: {
one,
two
},
data: function() {
return {
activeQuestions: []
}
},
methods: {
updateActiveQuestions(event) {
let index = this.activeQuestions.findIndex( ({ id }) => id === event.id );
if ( index === -1 ) {
this.activeQuestions.push(event);
} else {
this.activeQuestions[index] = event;
}
}
}
}
</script>
two.vue
<template>
<div v-if="show">
<h3>{{ question }}</h3>
<div class="c-troubleshooter__section">
<div class="c-troubleshooter__input">
<input type="radio" id="question-2-a" name="question-2" value="ja" v-model="answer">
<label for="question-2-a">Ja</label>
</div>
<div class="c-troubleshooter__input">
<input type="radio" id="question-2-b" name="question-2" value="nej" v-model="answer">
<label for="question-2-b">Nej</label>
</div>
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
id: 2,
question: "Bla bla bla?",
answer: undefined,
requires: [
{
id: 1,
answer: "Ja"
}
]
}
},
computed: {
show: function() {
// Check in parent to see if requirements are there, if so return true
return true;
}
},
watch: {
answer: function() {
this.$emit('changeAnswer', {
id: this.id,
question: this.question,
answer: this.answer
})
}
}
}
</script>
Rendering questions conditionally
as #Roy J suggests in comments, questions data probably belongs to the parent. It is the parent who handles all the data and who decides which questions should be rendered. However, there are plenty of strategies for this:
Display questions conditionally with v-if or v-show directly in the parent template:
Maybe the logic to display some questions is not at all generic. It can depend upon more things, user settings... I don't know. If that's the case, just render the questions conditionally directly in the parent, so you don't need to access the whole questions data in any question. Code should be something like the following:
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions" v-if="displayQuestion(1)"/>
<two #changeAnswer="updateActiveQuestions" v-if="displayQuestion(2)"/>
</div>
</div>
</template>
<script>
import one from './components/one.vue'
import two from './components/two.vue'
export default {
name: 'app',
components: {
one,
two
},
data: function() {
return {
activeQuestions: [],
}
},
methods: {
updateActiveQuestions(event) {
let index = this.activeQuestions.findIndex( ({ id }) => id === event.id );
if ( index === -1 ) {
this.activeQuestions.push(event);
} else {
this.activeQuestions[index] = event;
}
},
displayQuestion(index){
// logic...
}
},
}
</script>
Pass a reference to the previous question to every question:
If any question should be visible only when the previous question has been answered or viewed or something like that, you can pass that as a prop to every question, so they know wether they must render or not:
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions"/>
<two #changeAnswer="updateActiveQuestions" prev="activeQuestions[0]"/>
</div>
</div>
</template>
And in two.vue:
props: ['prev'],
computed: {
show: function() {
return this.prev && this.prev.status === 'ANSWERED';
// or some logic related to this, idk
}
},
just pass the whole data to the children:
As you coded it, you can just pass the whole questions data as a prop to every question component, then use it in a computed property. This is not what I would do, but just works, and since objects are references this is not necessarily unperformant.
Using a generic component:
It seems weird to have a one.vue, two.vue for every question, and sure does not scale well.
I'm not really sure how modular I can do them since the template for each question can be a bit different. Some have images or custom elements in them for example, while others don't.
If template are really different from each question to another, this can get complicated. However, if, as I suspect, they share common HTML structure, with a defined header or a common 'ask' button at the bottom and stuff like that, then you should be able to address this using Vue slots.
Apart from template issues, I suppose that every question in your app can get an arbitrary number of 'sub-questions' (as two.vue having question-2-a and question-2-b). This will require a complex and flexible data structure for the questions data (which will get more complex when you start to add multiple choices, multiple possible answers etc. etc.). This can get very complex but you should probably work on this until you can use a single question.vue component, this will surely pay out.
tip: avoid watchers
You're using v-model to answer in the two.vue template, then using a watcher to track changes in the answer variable and emit the event. This is convoluted and difficult to read, you can use #input or #change events on the <input> element instead:
<input type="radio" id="question-2-a" name="question-2" value="ja" v-model="answer" #input="emitAnswer">
And then instead of the watcher, have a method:
emitAnswer() {
this.$emit('changeAnswer', {
id: this.id,
question: this.question,
answer: this.answer
})
This is a pretty broad question, but I'll try to give some useful guidance.
First data should be used for internal state. Very often, a component should use props for things you might think would be data it owns. That is the case here: the questions need to be coordinated by the parent, so the parent should own the data. That allows you to make a sensible function to control whether a question component displays.
Having the parent own the data also allows you to make one question component that configures itself according to its props. Or you might have a few different question component types (you can use :is to select the right one), but almost certainly some of them are reusable if you pass their question/answer/other info in.
To update answers, you will emit changes from the question components and let the parent actually change the value. I use a settable computed to allow the use of v-model in the component.
new Vue({
el: '#app',
data() {
return {
questions: [{
id: 1,
question: 'blah 1?',
answer: null
},
{
id: 2,
question: 'blah 2?',
answer: null,
// this is bound because data is a function
show: () => {
const q1 = this.questions.find((q) => q.id === 1);
return Boolean(q1.answer);
}
},
{
id: 3,
question: 'Shows anyway?',
answer: null
}
]
};
},
components: {
questionComponent: {
template: '#question-template',
props: ['props'],
computed: {
answerProxy: {
get() {
return this.answer;
},
set(newValue) {
this.$emit('change', newValue);
}
}
}
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div class="c-troubleshooter">
<question-component v-for="q in questions" v-if="!q.show || q.show()" :props="q" #change="(v) => q.answer = v" :key="q.id">
</question-component>
</div>
<h2>State</h2>
<div v-for="q in questions" :key="q.id">
{{q.question}} {{q.answer}}
</div>
</div>
<template id="question-template">
<div>
{{props.question}}
<div class="c-troubleshooter__input">
<input type="radio" :id="`question-${props.id}-a`" :name="`question-${props.id}`" value="ja" v-model="answerProxy">
<label :for="`question-${props.id}-a`">Ja</label>
</div>
<div class="c-troubleshooter__input">
<input type="radio" :id="`question-${props.id}-b`" :name="`question-${props.id}`" value="nej" v-model="answerProxy">
<label :for="`question-${props.id}-b`">Nej</label>
</div>
</div>
</template>
I have a lot of rows generated by a loop. Each of the rows has own 'input' with description and action buttons. One of this buttons is edit button responsible for changing 'disabled' attribute value.
I do not have an idea how I should do it. Ok, I know that I should use $emit.
<template>
<section id="tasks-list">
<ul>
<task
v-for="(item, index) in filteredTasksList"
v-bind:key="item.id"
v-bind:index="index"
v-bind:item="item"
v-bind:tasks="tasks"
/>
</ul>
</section>
</template>
and task component:
<template>
<li :class="{checked: item.status}" class="task">
<div class="task-description">
<span>{{item.description}}</span>
<input type="text" v-model="item.description" :disabled="true">
</div>
<div class="task-actions">
<button class="btn-edit" v-on:click="disableItem()">{{translation.edit}}</button>
</div>
</li>
</template>
<script>
export default {
name: 'task',
props: {
item: { required: true },
index: { reuqired: true },
tasks: { required: true },
search: { require: true }
},
methods: {
disableItem(event){
//part responsible for changing disabled attr
}
}
}
</script>
Since disabled state and it's toggle handler are both inside <task> component, you can keep the logic inside that component. There is no need to $emit an event to a parent component, unless you want to manage state from parent.
Inside <task> component it can be achieved by using a boolean variable and changing it value on button click, via disableItem handler.
Input element should be changed to:
<input type="text" v-model="item.description" :disabled="isDisabled">
Add we should create a variable in component's data and update disableItem method as follows:
data () {
return {
isDisabled: false
}
},
disableItem () {
this.isDisabled = true
}
Also, it is not necessary to execute disableItem method in click handler, it can be just passed as a reference v-on:click="disableItem"
I have a list of instruments that should render a c-input with autosuggest window when the user types something. Also, I need an option for c-input to add or remove autosuggest component.
/* instrument component */
<template>
<c-input ref="input"
:values="inputValue"
:placeholder="placeholder"
#input="onInput"
#change="onChangeInput"
#reset="reset" />
<autosuggest
v-if="showSuggests"
:inputValue="inputValue"
:suggests="suggests"
#onSelectRic="selectRicFromList"
></autosuggest>
</div>
</template>
<script>
export default {
name: 'instrument',
data: () => ({
suggests: [],
inputValue: '',
}),
computed: {
showSuggests() {
return this.isNeedAutosuggest && this.showList;
},
showList() {
return this.$store.state.autosuggest.show;
},
isloading() {
return this.$store.state.instruments.showLoading;
},
defaultValue() {
if (this.instrument.name) {
return this.instrument.name;
}
return '';
},
},
[...]
};
</script>
This is a parent component:
<template>
<div>
<instrument v-for="(instrument, index) in instruments"
:key="instrument.name"
:instrument="instrument"
:placeholder="$t('change_instrument')"
:isNeedAutosuggest="true" /> <!--that flag should manage an autosuggest option-->
<instrument v-if="instruments.length < maxInstruments"
ref="newInstrument"
:isNeedAutosuggest="true" <!-- here too -->
:placeholder="$t('instrument-panel.ADD_INSTRUMENT')" />
</div>
</template>
The main issues are I have so many autosuggests in DOM as I have instruments. In other words, there is should be 1 autosuggest component when the option is true. Moving autosuggest to the parent level is not good because of flexibility and a lot of logically connected with c-input.
Have you any ideas to do it?
[UPDATE]
Here is how I've solve this;
I created an another component that wraps input and autosuggest components. If I need need an input with autosuggest I will use this one, either I will use a simple input.
/* wrapper.vue - inserted into the Instrument.vue*/
<template>
<span>
<fc-input ref="input"
:values="value"
:placeholder="placeholder"
:isloading="isloading"
#input="onInput"
#changeInput="$emit('change', $event)"
#resetInput="onResetInput" />
<fc-autosuggest
v-if="isSuggestsExist"
:suggests="suggests"
/>
</span>
</template>
You can do it if you create a function inside each instrument component, which will call the parent component and search the first component instrument to find autosuggest. Function will be like that:
name: 'instrument',
...
computed: {
autosuggestComponent () {
// this is a pseudo code
const parentChildrenComponents = this.$parent.children();
const firstChild = parentChildrenComponents[0];
const autosuggestEl = firstChild.$el.getElementsByTagName('autosuggest')[0];
// return Vue component
return autosuggestEl.__vue__;
}
},
methods: {
useAutosuggestComponent () {
this.autosuggestComponent.inputValue = this.inputValue;
this.autosuggestComponent.suggests = [{...}];
}
}
This solution is not so beautiful, but it allows to keep the logic inside the instrument component.
But my advice is create some parent component which will contain instrument components and I suggest to work with autosuggest through the parent. You can create autosuggest component in the parent and pass it to the children instruments. And if instrument doesn't receive a link to a autosuggest (in props), than it will create autosuggest inside itself. It will allow to use instrument for different conditions.
Let me know if I need to explain my idea carefully.
First of all : I'm using laravel spark and the given setup of vue that comes with spark.
I have a "home" component with the prop "custom". Within custom there's a "passwords" array. (Entry added by code of directive, it's initialized empty)
My component ( alist) which should be bound against the data
<template id="passwords-list-template">
<div class="password" v-for="password in list">
<ul>
<li>{{ password.name }}</li>
<li>{{ password.description }}</li>
</ul>
</div>
</template>
<script>
export default {
template: '#passwords-list-template',
props: ['list'],
};
</script>
Usage
<passwords-list :list="custom.passwords"></passwords-list>
Using vue devtools I can see that my data is updating, however my list is not. Also other bindings like
<div v-show="custom.passwords.length > 0">
Are not working ...
UPDATE : Parent component (Home)
Vue.component('home', {
props: ['user', 'custom'],
ready : function() {
}
});
Usage
<home :user="user" :custom="spark.custom" inline-template>
Update 2: I played around a little bit using jsfiddle. It seems like changing the bound data object using $root works fine for me when using a method of a component. However it does not work when trying to access it using a directive
https://jsfiddle.net/wa21yho2/1/
There were a lot of errors in your Vue code. First of all, your components where isolated, there wasn't an explicit parent-child relationship.Second, there were errors in the scope of components, you were trying to set data of the parent in the child, also, you were trying to set the value of a prop, and props are by default readonly, you should have written a setter function or change them to data. And finally, I can't understand why were you trying to use a directive if there were methods and events involve?
Anyway, I rewrote your jsfiddle, I hope that you find what you need there. The chain is Root > Home > PasswordList. And the data is in the root but modified in home, the last component only show it. the key here are twoWay properties, otherwise you wouldn't be able to modify data through properties.
Here is a snippet of code
Home
var Home = Vue.component('home', {
props: {
user: {
default: ''
},
custom: {
twoWay: true
}
},
components: {
passwordList: PasswordList
},
methods: {
reset: function () {
this.custom.passwords = [];
}
}
});
// template
<home :custom.sync="spark.custom" inline-template>
{{custom | json}}
<button #click="reset">
reset in home
</button>
<password-list :list="custom.passwords"></password-list>
<password-list :list="custom.passwords"></password-list>
</home>
Here is the full jsfiddle