How to delete the row without affecting the other rows - vue.js

I am creating a VueJs parent component that can create rows dynamically, and this component call another component which can populate 2 dropdowns with axios.
One for categories
The second one for subcategories (this dropdown can depend with the first one)
This is the first component for adding rows
<template>
<div>
<ul>
<li v-for="(input, index) in inputs" :key="index">
<request-part :index="index" :input="input" :inputs="inputs">
</request-part>
<hr />
</li>
</ul>
<button
type="button"
#click="addRow"
class="btn font-montserrat-regular btn-success btn-plus bt-radius-add"
>
Onderdeel toevoegen
</button>
</div>
</template>
<script>
export default {
data() {
return {
category: null,
selectedFile: null,
subcategory: null,
current: 0,
id: 0,
inputs: [],
categories: [],
subcategories: []
}
},
mounted() {
axios.get('/api/categories').then(res => {
this.categories = res.data
})
},
created() {
this.addRow()
},
methods: {
addRow() {
this.inputs.push({
category: '',
subcategory: '',
sku: '',
description: '',
image: ''
})
},
onFileChanged(event) {
this.selectedFile = event.target.files[0]
}
}
}
</script>
This is the second component for populating the dropdowns
<template>
<div class="border-0">
<div class="row">
<div class="col-md-8">
<div class="form-group ml-2">
<label class="gray-text-color font-montserrat-regular" :for="part">
{{ $t('labels.frontend.request.part') }} *
</label>
<div class="form-group brd3">
<select
:name="'r[' + index + '][category]'"
:id="category + index"
class="form-control light-gray-background arrow-select-position request-input"
v-model="input.category"
#change="onchangeCategorie"
required
>
<option :value="null" disabled selected>
{{ $t('labels.account.create.selectCategory') }}
</option>
<option
v-for="(option, index1) in categories"
:value="index1"
:key="index1"
>
{{ option }}
</option>
</select>
</div>
<div class="form-group brd3">
<select
:name="'r[' + index + '][subcategory]'"
:id="subcategory + index"
class="form-control light-gray-background arrow-select-position request-input"
v-model="input.subcategory"
required
>
<option :value="null" disabled selected>
{{ $t('labels.frontend.request.subCategory') }}
</option>
<option
v-for="(option, index1) in subcategories"
:value="index1"
:key="option.id"
>
{{ option }}
</option>
</select>
</div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="gray-text-color font-montserrat-regular" :for="sku">
{{ $t('labels.frontend.request.articleNumber') }}
</label>
<input
type="text"
:name="'r[' + index + '][sku]'"
:id="'sku' + index"
v-model="input.sku"
class="form-control light-gray-background request-input"
/>
</div>
</div>
</div>
<div class="row">
<div class="col-9">
<div class="form-group" style="margin-right:-40px">
<input
type="text"
:name="'r[' + index + '][description]'"
v-model="input.description"
class="form-control light-gray-background input-width-mobile request-input"
placeholder="Toelichting (optioneel)"
/>
</div>
</div>
<input
:id="'image' + index"
:name="'r[' + index + '][image]'"
type="file"
class="camera-button inputfile"
:change="input.image"
accept="image/*"
#change="onFileChanged"
/>
<label :for="'image' + index">
<img
src="../../../../../resources/assets/images/cameraIcon.png"
alt="Camera icon"
class="camera-button-position"
/>
</label>
<div class="pr-l-200 ft-14 mr-3">
<label>{{ $t('labels.frontend.request.image') }}</label>
</div>
<div id="preview">
<img v-if="url" :src="url" alt="no Image." />
<button
v-if="url != null"
type="button"
#click="url = null"
class="btn fa fa-trash btn-default bt-radius"
></button>
</div>
</div>
<button
type="button"
#click="deleteRow(index)"
class="btn btn-danger fa fa-trash bt-radius"
></button>
</div>
</template>
<script>
export default {
props: {
part: {
type: String,
default: null
},
sku: {
type: String,
default: null
},
description: {
type: String,
default: null
},
image: {
type: String,
default: null
},
index: {
type: Number,
default: 0
},
input: {
type: Object,
default: () => ({})
},
inputs: {
type: Array,
default: () => []
}
},
data() {
return {
test: null,
category: null,
selectedFile: null,
subcategory: null,
categories: [],
subcategories: [],
url: null
}
},
mounted() {
axios.get('/api/categories').then(res => {
this.categories = res.data
})
},
methods: {
deleteRow(index) {
console.log(index)
this.$delete(this.inputs, index)
},
onFileChanged(event) {
this.selectedFile = event.target.files[0]
this.input.image = this.selectedFile
this.url = URL.createObjectURL(this.selectedFile)
},
onchangeCategorie(e) {
axios.get('/api/categories/' + e.target.value).then(res => {
this.subcategories = res.data
})
}
}
}
</script>
<style>
#preview {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
}
#preview img {
max-width: 200px;
max-height: 200px;
border-radius: 5px;
border: 1px solid lightgray;
}
</style>
When i try to delete the first or any row from top to down, all the subcategories are gone.
When i delete the row from down to up it works fine

Your problem is caused by the fact that your rows don't have a proper stable unique ID, and that you're instead using their array index as the :key in your v-for directive. The reason this is a problem is that when you delete an element from an array using .$delete(), all the later elements get shifted down to a new, lower index so that the array remains contiguous.
The solution is to give your rows a unique ID. A simple global counter will do just fine:
var counter = 0; // global counter for row IDs (or anything else that needs one)
export default {
// ...
methods: {
addRow() {
this.inputs.push({
category: '',
subcategory: '',
sku: '',
description: '',
image: '',
id: ++counter // this gives each row a distinct ID number
})
},
// ...
}
Then you can use this unique ID as the :key in your v-for directive:
<li v-for="(input, index) in inputs" :key="input.id">
<request-part :index="index" :input="input" :inputs="inputs">
</request-part>
<hr />
</li>

Related

Vue v-model/v-for doesn't update on mount, but after first manual change

I have a dropdown list "functions" that is filled with database entries and a dropdown list with 2 hardcoded entries. When the vue website is opened the dropdown list remains empty but as soon as I change the value of the other dropdown field the desired data from the database is available.
I'm a bit confused because I expected that adding "retrieveFunctions()" into the mounted() function would trigger the v-for, and even more confused that changing something in another select field suddenly triggers it.
The HTML code:
<template>
<div class="submit-form">
<div v-if="!submitted">
<div class="row">
<div class="col-sm-12">
<p><a style="width:500px" class="btn btn-info" data-toggle="collapse" href="#generalInformation" role="button" aria-expanded="true" >
General Information</a></p>
<div class="collaps show" id="generalInformation">
<!-- NAME -->
<div class="form-group">
<input placeholder="Name" type="text" class="form-control"
id="name" required v-model="component.name" name="name">
</div>
<!-- DOMAIN -->
<div class="input-group mb-3">
<div class="input-group-prepend">
<label style="width:100px" class="input-group-text" for="inputGroupDomain">Domain</label>
</div>
<select v-model="component.domain"
class="custom-select"
id="inputGroupDomain"
>
<option value="Power System">Power System</option>
<option value="ICT">ICT</option>
</select>
</div>
<!-- FUNCTION -->
<div class="input-group mb-3">
<div class="input-group-prepend">
<label style="width:100px" class="input-group-text" for="inputGroupFunction">Functions</label>
</div>
<select v-model="_function" class="custom-select" id="inputGroupFunction">
<option :class="{ active: index == currentIndex }"
v-for="(_function, index) in functions"
:key="index"
value= _function.name>
{{ _function.name }}
</option>
</select>
</div>
</div>
<p>
<button #click="saveComponent" class="btn btn-success">Add Component</button>
</p>
</div>
</div>
</div>
<div v-else>
<h4>Component was added succesfully!</h4>
<button class="btn btn-success" #click="newComponent">Proceed</button>
</div>
The script part:
<script>
import FunctionDataService from "../services/FunctionDataService";
export default {
name: "add-component",
data() {
return {
component: {
id: null,
name: "",
type: "",
domain: "",
},
submitted: false
};
},
methods: {
retrieveFunctions() {
FunctionDataService.getAll()
.then(response => {
this.functions = response.data;
console.log(response.data);
})
.catch(e => {
console.log(e);
});
},
refreshList() {
this.retrieveFunctions();
},
},
mounted() {
this.retrieveFunctions();
}
};
</script>
refreshList() {
this.retrieveFunctions();
},
},
mounted() {
this.retrieveFunctions();
}
};
</script>
State in the beginning: Dropdown list empty
State after selecting something in the upper dropdown list: Database entries are visible and correct
You need to initiate all responsive properties on the data return object with either a value (empty string, array, object, etc) or null. Currently it's missing the _function attribute used in the select v-model and the functions array used in the v-for. You can try to change the data to the following:
data() {
return {
_function: "",
functions: [],
component: {
id: null,
name: "",
type: "",
domain: "",
},
submitted: false
};
},

nesting an object inside an object

Looking for some tips on how to nest objects inside objects using a form. My form currently changes the key and value of an object. However, I'm now wanting a second button to be able to create a child (correct termanology?)form input. below you can see an example. I've spent the morning looking at props but I'm unsure if this is the correct way to go, any suggestions are greatly appriciated
{
"color": "black",
"category": "hue",
"type": "primary",
"code": {
"rgba": [255,255,255,1],
"hex": "#000"
}
},
<form id="app">
<h1>
Title goes here
</h1>
<hr>
<div class="row">
<div class="col-xs-2">
<button type="button" v-on:click="addNewObject" class="btn btn-block btn-success">
(Add +) Parent
</button>
</div>
<div class="col-xs-10 text_info">
Click 'Add +' to add an object
</div>
</div>
<div v-for="(object, index) in objects">
<div class="row">
<div class="col-xs-1">
<label> </label>
<button type="button" v-on:click="removeObject(index)" class="btn btn-rem btn-block btn-danger">
Delete
</button>
<button type="button" v-on:click="addNewChildObject()" class="btn btn-rem btn-block btn-success btn-suc">
add { }
</button>
</div>
<div class="form-group col-xs-7">
<div class="test">
<select v-model="object.type" class="selectBox classic">
<option value="" disabled selected hidden>Choose Datatype</option>
<option v-for="type in types"> {{ type }}</option>
</select>
<input v-model="object.name" :name="'objects[' + index + '][name]'" type="string" class="form-control" placeholder="Enter key">
<input v-model="object.dataValue" :name="'objects[' + index + '][dataValue]'" type="string" class="form-control" placeholder="Enter value">
</div>
</div>
</div>
</div>
<hr>
<div>
<pre v-if="seen">{{ mappedObjects }}</pre>
</div>
<button type="button" class="btn-primary" v-on:click="seen = !seen">{{ seen ? 'Click to Hide the JSON' : 'Click to Show the JSON' }}</button>
</form>
const getDefaultObject = () => ({
name: '',
dataValue: '',
type: ''
})
const app = new Vue({
el: '#app',
computed: {
mappedObjects() {
return this.objects.map(({
name,
dataValue,
type
}) => ({
[name]: dataValue,
type
}))
}
},
props: {
},
data() {
return {
seen: false,
types: ['string', 'character', 'number', 'int', 'floating-point', 'boolean', 'date;'],
objects: []
}
},
methods: {
addNewObject: function() {
this.objects.push(getDefaultObject())
},
removeObject: function(index) {
Vue.delete(this.objects, index);
},
addNewChildObject: function () {
}
}
})
If you want n forms with n children just create a model for it following that same structure and pass props to the form.
parent = {
...props,
children: []
}
child = {
...props
}
If the forms are too complex (or a little complex really), split them in separate components and pass children as props.
If you want to use the same form both in parent and children take a look at slots, they will allow you to create flexible layouts.

Vue-formulate - Group item collapsible / toggle collapse

Is there a possibility to make group item collapsible?
<FormulateInput type="group" name="employments" :repeatable="true" label="Employments"
add-label="+ Add Employment" #default="groupProps">
<!-- Clickable area -->
<div class="group text-sm font-semibold py-2 cursor-pointer relative" #click="groupProps.showForm">
....
</div>
<!-- Nested form: must be collapsible accordion -->
<div class="nested-form" v-show="groupProps.showForm">
....
</div>
</FormulateInput>
I thought to add showForm property to the group context.
For this I need to do Custom input types or is there some other way?
If anyone has any other ideas?
Thanks
I figured it out with the gist of #jpschroeder.
CollapsableGroupItem.vue:
<template>
<div class="group-item" :data-is-open="itemId === showIndex">
<div class="group group-item-title text-sm font-semibold py-2 cursor-pointer relative hover:text-blue-400" #click="toggleBody">
<slot name="title" v-bind="groupItem">#{{ context.index }}</slot>
</div>
<div class="group-item-body" v-show="itemId === showIndex">
<slot name="body">
<FormulateInput type="pelleditor" name="description" label="Description"/>
</slot>
</div>
</div>
</template>
<script>
export default {
name: "CollapsableGroupItem",
props: {
context: {
type: Object,
required: true,
},
showIndex: {
type: [Number, String],
required: true,
},
groupItem: {
type: Object,
required: true,
},
},
data () {
return {
itemId: this.context.name + this.context.index
}
},
created: function () {
// show current item
this.$emit("open", this.itemId);
},
methods: {
toggleBody() {
if (this.itemId === this.showIndex) {
// dont show anything
this.$emit("open", -1);
} else {
// show this one
this.$emit("open", this.itemId);
}
},
}
};
FormTemplate.vue:
<CollapsableGroupItem
:context="context"
:show-index="showIndex"
:group-item="educations[context.index]"
#open="showIndex = $event"
>
<template v-slot:title="education">
<span v-if="education.institution || education.degree"
>
{{ education.institution }}
<span v-if="education.institution && education.degree">at</span>
{{ education.degree }}
</span>
...
</template>
<template v-slot:body>
...
</template>
</CollapsableGroupItem>
Maybe it will help someone else or will be useful 😀

Logo to the left, form to the right

I am using Bulma and Vue, and I am trying to create a header for the site that consists of a logo on the left and a login form on the right.
This gives me a logo on the left, and then from the end of the logo until the end of the screen on the right, I have the elements shown there.
How do I do what I want? Thanks.
Template
<header>
<div class="navbar">
<a class="navbar-brand" href="/">FreeSongsâ„¢</a>
<form class="navbar-menu" #submit.prevent="signin" accept-charset="utf-8" autocomplete="on">
<div class="field-body ">
<FormField type="email" required="required" :tabindex="1" placeholder="Email" name="login[email]" autocomplete="email" v-model="stageName" v-validate="'required'" autocapitalize="off" autofocus="autofocus"></FormField>
<FormField type="password" required="required" :tabindex="2" placeholder="Password" name="login[password]" autocomplete="current-password" v-model="email" v-validate="'required|email'"></FormField>
<button class="button is-success" tabindex="3" type="submit" id="signin">Sign in</button>
<a class="btn btn-link" tabindex="4" href="/forgot">Forgot password?</a>
</div>
</form>
</div>
</header>
FormField Component
<template>
<div class="field">
<label v-if="label" class="label" :for="id">{{label}}</label>
<input :type="type" class="input" :class="{'is-danger':this.$validator.errors.has(label)}" :tabindex="tabindex" :name="name" :id="id" :autocomplete="autocomplete" :value="value" #input="updateValue" #change="updateValue" #blur="$emit('blur')" :disabled="disabled" :required="required" :placeholder="placeholder" />
<span v-show="this.$validator.errors.has(label)" class="subtitle is-6 has-text-danger">{{ this.$parent.errors.first(label) }}</span>
</div>
</template>
<script>
export default {
name: "FormField",
//inject: ['$validator'],
inject: {
$validator: '$validator'
},
$_veeValidate: {
name() {
return this.label;
},
// fetch the current value from the innerValue defined in the component data.
value() {
return this.value;
}
},
props: {
value: String,
placeholder:String,
id: {
type: String,
default: () => {
const rand = Math.floor((Math.random() * 10000) + 1); //TODO: Create enough margin so there won't be a chance it has the same ID as other elemnts. Change the method?
const id = `undefined_${Date.now()*rand}`; //${this._uid}
return id;
}
},
label: {
type: String,
required: false
},
type: {
type: String,
default: "text"
},
name: {
type: String,
required: true
},
autocomplete: {
type: String,
required: false
},
disabled: {
type: Boolean,
default: false
},
required:{
type:Boolean,
default:false
},
tabindex:{
type:Number
},
autocapitalize:{
type:String,
},
autofocus:{
type:Boolean
}
},
computed: {
},
created: function() {
console.log("Created");
},
mounted: function() {
console.log("Mounted");
},
methods: {
updateValue(e) {
this.$emit("input", e.target.value);
}
}
};
</script>
The documentation outlines how to do this:
https://bulma.io/documentation/components/navbar/
First, the navbar is split into two.
|navbar-brand|navbar-menu|
navbar-brand will always show on the left, the navbar-menu fills the rest of the space on the right.
Inside the navbar-menu, you can specify which side items will show with two more elements.
|navbar-start|navbar-end|
<nav class="navbar">
<div class="navbar-brand">
This is on the left of the bar.
</div>
<div class="navbar-menu">
This spans the rest of the space on the right of the bar.
<div class="navbar-start">
This is on the left.
<div class="navbar-item">Your items on the left</div>
</div>
<div class="navbar-end">
This is on the right.
<div class="navbar-item">Your items on the right</div>
</div>
</div>
</nav>

To do List with Vue js 2 using component or v-model

Hello I have here one code with two "todo list" implementations in Vuejs but I have a problem.
1 Using a vue component i am getting a waring about how to use the parent variable.
2 Doing it on the main function I cannot keep the old value for the discard implementation.
please find the working code
Running! todo list in codepen
Vue.component('ntodo-item', {
template: '\
<transition name="fade">\
<div id="if" class="row" v-if="edit">\
<div class="col-md-7">\
<input class="form-control" v-model="title">\
</div>\
<div id="sssss" class="col-md-5">\
<button class="btn btn-danger roundButton" v-on:click="$emit(\'edit\')">Discard</button>\
<button class="btn btn-success roundButton" v-on:click="updateValue">Save</i></button>\
</div>\
</div>\
<div id="else" class="row" v-else>\
<div class="col-md-7">\
{{ title }}\
</div>\
<div id="ssaaas" class="col-md-5">\
<button class="btn btn-danger roundButton" v-on:click="$emit(\'remove\')">Remove</button>\
<button id="aaa" class="btn btn-default roundButton" v-on:click="$emit(\'edit\')">Edit</button>\
</div>\
</div>\
</transition>\
',
props: [
'title' ,
'edit'
],
methods: {
updateValue: function () {
this.$emit('input', this.title);
}
}
})
var app14 = new Vue({
el: '#app-14',
data: {
newTodoText: '',
newTodoText2: '',
todos: [
{
id: 1,
title: 'Do the dishes',
edit:0
},
{
id: 2,
title: 'Take out the trash',
edit:0
},
{
id: 3,
title: 'Mow the lawn',
edit:0
}
],
todos2: [
{
id: 1,
title: 'Do the dishes',
edit:0
},
{
id: 2,
title: 'Take out the trash',
edit:0
},
{
id: 3,
title: 'Mow the lawn',
edit:0
}
],
nextTodoId: 4,
nextTodoId2: 4
},
methods: {
addNewTodo: function () {
this.todos.push({
id: this.nextTodoId++,
title: this.newTodoText,
edit:0
})
this.newTodoText = ''
this.todos = _.orderBy(this.todos, 'id', 'desc');
},
editTodo: function (item){
// console.log(item.title)
item.edit^= 1
},
updateValue: function (item, newValue){
item.title=newValue
item.edit^= 1
},
addNewTodo2: function () {
this.todos2.push({
id: this.nextTodoId2++,
title: this.newTodoText2,
edit:0
})
this.newTodoText2 = ''
this.todos2 = _.orderBy(this.todos2, 'id', 'desc');
},
editTodo2: function (item){
console.log(item.title)
item.edit^= 1
},
deleteTodo2: function (item){
this.todos2.splice(item.id, 1);
},
updateValue2: function(text){
console.log(text);
}
}
})
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
transform-origin: left center;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
transform: scale(0.5);
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.js"></script>
<div class="col-md-12">
<div class="graybox">
<h5>app14</h5>
<div id="app-14">
`enter code here`<div class="row">
<div class="col-md-6">
<h5> todo list using "ntodo-item" component</h5>
<p>This one show me a warning because the child cannot edit the va passed by the parent but it is working and spected</p>
<input class="form-control"
v-model="newTodoText"
v-on:keyup.enter="addNewTodo"
placeholder="Add a todo"
>
<hr>
<ul>
<li
is="ntodo-item"
v-for="(todo, index) in todos"
v-bind:key="todo.id"
v-bind:title="todo.title"
v-bind:edit="todo.edit"
v-on:input="updateValue(todo, $event)"
v-on:remove="todos.splice(index, 1)"
v-on:edit="editTodo(todo)"
></li>
</ul>
</div>
<div class="col-md-6">
<h5> todo list update</h5>
<p> This one is working without any warn but I dont know how to discard changes. I dont want to create a temp var because I want to be able to edit all of them at the same time. </p>
<input v-model="newTodoText2"
v-on:keyup.enter="addNewTodo2"
placeholder="Add a todo"
class="form-control"
>
<hr>
<ul>
<transition-group name="fade" >
<li v-for="(todo2, index) in todos2":key="todo2.id">
<div id="if" class="row" v-if="todo2.edit">
<div class="col-md-7">
<input class="form-control" ref="todo2" v-model="todo2.title">
</div>
<div id="sssss" class="col-md-5">
<button class="btn btn-success roundButton" v-on:click="editTodo2(todo2)">ok</button>
</div>
</div>
<div id="else" class="row" v-else>
<div class="col-md-7">
{{todo2.title}}
</div>
<div id="ssaaas" class="col-md-5">
<button class="btn btn-danger roundButton" v-on:click="todos2.splice(index, 1)">Remove</button>
<button id="aaa" class="btn btn-default roundButton" v-on:click="editTodo2(todo2)">Edit</button>
</div>
</div>
</li>
</transition>
</ul>
</div>
</div>
</div>
</div>
</div>
.
Echoing my comment:
Create a local variable copy of your title prop and emit that variable's changes on edit. If they discard the edit just reset the local variable to the value of the title prop. Working example on CodeSandbox here.
Todo Item Component
<button class="btn btn-danger roundButton" #click="discardEdit">Discard</button>
...
data() {
return {
// our local copy
localTitle: null,
};
},
mounted() {
this.localTitle = this.title;
},
methods: {
updateValue: function() {
this.$emit("input", this.localTitle);
},
discardEdit: function() {
// just set local back to title prop value
this.localTitle = this.title;
this.$emit('edit');
},
}