Vue.js - Open Dialog for editing record - javascript

I have a list with buttons to the right for each record, it looks like this:
https://imgur.com/IVzn1ZZ
When I hit one of these buttons, I want a dialogue to pop up in which there are input fields like textinput, dropdowns, checkboxes etc.. This dialogue will enable to edit the respective record (and ONLY this record).
The Inputfields shall be labeled accordingly, basically each inputfield shall receive a label derived from the respective column header (see the tableheader) which the inputfield references.
This way, the user shall know which field of the record hes applying changes to.
I'm a beginner in vue.js. I have never before used dialogues in vue.js. I know dialogues are not deemed best practice in webapplications, but its a designdecision our team has come to for several reasons and now I have to stick with it.
Our vue app is a vue-cli app. Most recent distribution. What options does vue offer me to do this? Are third party plugins or the like recommendable for this?
The dialogue might possibly display a LOT of data. It basically depends on what data the user is allowed to see. So I really need some approach which is powerful enough to handle at least low double digit number of inputoptions "ergonomically" :D

For the dialogue, you could use something like bootstrap-vue's modal component.
You can put whatever form HTML you need within the modal component.
Assuming every record in your set has the same schema, then you could have a data property e.g. selectedRecord and bind the inputs in your form to the properties of selectedRecord, then when one of your record buttons is clicked, it should populate selectedRecord with the clicked record, and show the modal.
e.g.
<template>
<div>
<ul>
<li v-for="record in records"
:key="record.id">
<span>{{record.name}}</span>
<button class="btn btn-primary"
#click="startEditing(record)">Edit
</button>
</li>
</ul>
<b-modal
ref="selectedRecordModal"
id="modal-1"
title="BootstrapVue"
#ok="save()"
>
<form v-if="selectedRecord">
<div class="form-group" v-if="editable('name')">
<label for="name">Name</label>
<input type="text"
id="name"
name="name"
v-model="selectedRecord.name"
class="form-control">
</div>
<div class="form-group" v-if="editable('extra')">
<label for="extra">Extra</label>
<input type="text"
id="extra"
name="extra"
v-model="selectedRecord.extra"
class="form-control">
</div>
</form>
</b-modal>
</div>
</template>
<script>
import Vue from 'vue';
import { BModal } from 'bootstrap-vue'
export default {
components: {
BModal
},
data() {
return {
records: [
{ id: 1, name: 'record 1' },
{ id: 2, name: 'record 2' },
{ id: 3, name: 'record 3', extra: 'thing'},
],
selectedRecord: null,
user: {
permissions: null
}
}
},
created() {
//Replace with code for setting permissions dynamically
this.user.permissions = {name: true, extra: true};
},
methods: {
editable(field) {
return (this.selectedRecord[field] && this.hasEditPermission(field));
},
hasEditPermission(field) {
return !!this.user.permissions[field];
},
startEditing(record) {
this.selectedRecord = Vue.util.extend({}, record);
this.$refs.selectedRecordModal.show();
},
save() {
//Validate this.selectedRecord and post to backend, update the original record, hide the modal etc.
this.$refs.selectedRecordModal.hide();
}
}
}
</script>

Related

What is better to use to communicate with a child component - $emit or $ref?

Tell me, please.
There is a page for changing user data. This page consists of fields for entering information about the user and a Submit button.
I need to create a popup component that performs the same functions.
The form itself will now be in a child component. The submit button is now moved to the parent component.
Therefore, I need to pass the entered data to the parent component. Everything is complicated by the fact that initially data comes from the server with previously entered information about the user. Therefore, initially you need to transfer data to the child component through props, and then, when changing them from the child, transfer it to the parent.
But what if there are a lot of variables?
For example: I need to create 15 props and then pass this data through $emit. The data itself is passed either through the #input event of the <input> tag or the #change event of the <select> tag.
I thought of three options, but I don't know which one is better. Or maybe some should not be used at all.
Using $emit
Parent component
<template>
<Child
:first-props="user.first"
:second-props="user.second"
....
:fifteenth-props="user.fifteenth"
/>
<button #click='submit'>Submit</button>
</template>
<script>
import Child from '#/components/Child.vue'
import { mapGetters } from 'vuex'
export default {
data: () => ({
first: '',
second: '',
...
fifteenth: ''
}),
components: {
Child
},
computed: {
...mapGetters({
user: 'user/getUser'
})
},
methods: {
submit() {
//Sending data
}
},
mounted: {
this.$store.dispatch('user/getUserData')
}
}
</script>
Child component
<template>
<div>
<input type="text" value="first" #input="username" />
<input type="text" value="second" #input="name" />
<input type="text" value="fifteenth" #input="surname" />
</div>
</template>
<script>
export default {
props: {
first: {
type: String,
required: true
},
first: {
type: String,
required: true
},
...
fifteenth: {
type: String,
required: true
}
},
methods: {
username() {
this.$emit('changeUsername', first)
},
name() {
this.$emit('changeName', second)
},
surname() {
this.$emit('changeSurname', fifteenth)
}
}
}
</script>
In this variant, I am worried about the number of props. I do not know if this can somehow affect the speed and quality of the component.
Using $ref
Parent component
<template>
<Child
ref='childComponent'
/>
<button click="submit">Submit</button>
</template>
<script>
import Child from '#/components/Child.vue'
export default {
data: () => ({
first: '',
second: '',
...
fifteenth: ''
}),
components: {
Child
},
method: {
submit() {
this.$refs.childComponent.submit()
}
}
}
</script>
Child component
<template>
<div>
<input type="text" v-model="first" #input="username" />
<input type="text" v-model="second" #input="name" />
<input type="text" v-model="fifteenth" #input="surname" />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data: () => ({
first: '',
second: '',
...
fifteenth: ''
}),
computed: {
...mapGetters({
user: 'user/getUser'
})
},
methods: {
submit() {
//Sending data
}
},
mounted: {
this.$store.dispatch('user/getUserData')
}
}
</script>
In this variant, there is not much interaction between the parent and child component. And the parent component fires only the submit method. Plus, you don't need to change the existing code too much, as you just need to move the button. But here I am concerned: is it not a bad decision to apply ref in this way ? And the second problem is the availability of data when the button is clicked. It is more likely that the child component has not yet received the necessary data or simply has not loaded yet, and the user has already pressed the button.
For example, the parent component already has a main title and a submit button. But the child component is still loading.
Although the solution here can be to make the button available for pressing only after loading the child component using :disabled="isDisabled".
Using vuex
This option immediately seems wrong to me, since the data will be used only inside this component. And vuex seems to me suitable only for global cases.
I have tried all three options and they all work. But I wanted the code not only to work, but also to be correct.
$ref and $emit don't serve the same purpose, there's generally no choice between them.
Option 1 can use a single data prop in a child:
<input
type="text"
:value="formData.first"
#input="$emit('updateFormData', { key: 'first', value: $event.target.value }))"
/>
And merge the changes in a parent:
<Child
:formData="formData"
#updateFormData="formData[$event.key] = $event.value"
/>
Option 2 is possible in its current state. Alternatively, a child can be made self-contained and contain submit button, then there is no use for $ref. It can emit submit event in case there's business logic associated with form submit that shouldn't be moved to a child.
Option 3 is possible but the use of global store for local purposes is not justified, local stores can be achieved with Vuex dynamic modules.

Vuejs v-model for an entire form instead of individual input?

Edit: This is for Vue 2.X
I am creating a single Vue.js component that's purpose is to create "custom" forms.
This component is basically one form by itself with multiple input fields. In this form you can choose to add questions, define the input type, etc. The entire data is stored in an array, where the data represents a form.
In the future, this array of questions can be parsed and used to display a form. This allows users to create their own forms without hardcoding it.
The problem:
As mentioned, the entire data that I want to store is an array of questions, where each question is represented by an object. For example [ { 'question_name': "How are you", 'input_type': "text" }, ... ] etc etc.
However, because this form is not an input field by itself, and instead the data comprises of many smaller inputs, I cannot do a v-model on my form component easily.
Thus there is no way to make the entire form data "reactive" from the parent component, the same way input fields are reactive with the v-model variable they are binded to.
For example, in my Parent.vue I might want this
<CustomFormComponent v-model="formData" /> # form data is the array I mentioned
and be able to interact with the "formData" from the parent directly, with the CustomFormComponent being reactive to such changes.
A good solution but not the best:
One way of course is to add a setter and getter method to my custom form component. When I need the data for submission I can call the getter. When I need to add pre-determined questions I can use the setter.
Does anyone know if what I want to do is possible?
You don't specify the version of VueJS you're running. I'll assume it's VueJS 2, which is the one I've used most, but surely it can be done with 3 too:
<template>
<div id="app">
<p>My Form</p>
<CustomForm v-model="questions" />
</div>
</template>
<script>
import CustomForm from "./components/CustomForm";
export default {
name: "App",
data() {
return {
questions: [
{ id: 1, label: "1", answer: "" },
{ id: 2, label: "2", answer: "" },
{ id: 3, label: "3", answer: "" },
{ id: 4, label: "4", answer: "" },
{ id: 5, label: "5", answer: "" },
],
};
},
components: {
CustomForm,
},
};
</script>
And then CustomForm.vue:
<template>
<form>
<div v-for="question in value" :key="question.id">
<p>{{ question.label }}</p>
<input
:id="question.id"
:value="question.answer"
#input="(e) => (question.answer = e.target.value)"
/>
</div>
</form>
</template>
<script>
export default {
name: "CustomForm",
props: {
value: Array,
},
};
</script>
Obviously, you could change this to also have other types of DOM elements, like select, checkbox... It's a simple example.

InertiaJS keep form data after validation error

In my InertiaJS/VueJS project I have a prop that receive some data from the backend:
event: {
type: Object,
default: () => { return {} }
},
That's how the event obj looks in the backend:
['name' => 'Event Name']
I use toRefs to convert the reactive prop and update its properties in the UI:
const eventRef = toRefs(props).event
So the Event has the name 'Event Name' when the component loads, when I update the event name in the UI to 'New Name' and submit the form, I send the eventRef obj in the request to create the new event:
Inertia.post(url, eventRef, only: ['global'])
If there's a validation error in the backend, I return it to the frontend and show the error in the UI (This is working without problems). The problem I have is that Inertia (or maybe VueJS) is returning the object eventRef to his previous state when the component is created. Which means that the name property of the eventRef changes to 'Event Name' again, instead of staying with 'New Name` that was updated in the UI. I would like to preserve the state of the object after I submit the form. This is my Inertia response:
component: "Events/EventNew"
props: {
global: {} // Global object
}
url: "/app/qa/events/new"
version: null
As you can see I'm not even getting the 'event' prop from the backend, so it shouldn't be updated. After reading Inertia docs I thought that a simple preserveState:true in the request options would do the job but this is not happening. Every time the server returns an Inertia response, the eventRef obj is 'reset'.
What am I missing here? I would appreciate some help
I believe I had the same problem using Inertia with Vue2. If I understood correctly, you probably seeing this on a form where you trying to update and entry, right? Your validation is working but the form keeps resetting itself to the previous state. If that's the case, what solved this for me was this:
Instead of using Inertia.post() directly, use the Inertia Form Helper instead
Vue 2
<template>
<form #submit.prevent="form.post('/login')">
<!-- email -->
<input type="text" v-model="form.email">
<div v-if="form.errors.email">{{ form.errors.email }}</div>
<!-- password -->
<input type="password" v-model="form.password">
<div v-if="form.errors.password">{{ form.errors.password }}</div>
<!-- remember me -->
<input type="checkbox" v-model="form.remember"> Remember Me
<!-- submit -->
<button type="submit" :disabled="form.processing">Login</button>
</form>
</template>
<script>
export default {
data() {
return {
form: this.$inertia.form({
email: null,
password: null,
remember: false,
}),
}
},
}
</script>
Vue 3
<template>
<form #submit.prevent="form.post('/login')">
<!-- email -->
<input type="text" v-model="form.email">
<div v-if="form.errors.email">{{ form.errors.email }}</div>
<!-- password -->
<input type="password" v-model="form.password">
<div v-if="form.errors.password">{{ form.errors.password }}</div>
<!-- remember me -->
<input type="checkbox" v-model="form.remember"> Remember Me
<!-- submit -->
<button type="submit" :disabled="form.processing">Login</button>
</form>
</template>
<script>
import { useForm } from '#inertiajs/inertia-vue3'
export default {
setup () {
const form = useForm({
email: null,
password: null,
remember: false,
})
return { form }
},
}
</script>
I solved the problem, it was the toRefs that was modifying the props in the component after the request was sent. Using a reactive object was the solution:
const eventRef = reactive(props.event)

VueJS - get input values from a recursive tree

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.

Vue - Render components depending on state of parent data

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>

Categories