Why do my dynamically generated forms not update correctly when I delete one of them using Vue? - forms

I want to populate a form with multiple text input fields. However, when I delete one of these input fields, the UI doesn't update correctly according to the data present.
Condition.vue
<template v-slot:custom-form>
<el-row v-for="(condition, index) of customFormData" type="flex" :key="`${randomSalt}${index}`" class="condition row-bg" :justify="isElse(condition) ? 'start' : 'space-around'">
<el-col class="operator" :class="[ index === 0 ? 'operator-if' : '', isElse(condition) ? 'operator-else' : '']" :span=".5">
{{condition.type.toLowerCase() === 'else' ? 'ELSE' : index > 0 ? 'ELSE IF' : 'IF'}}
</el-col>
<el-col ref="index" v-if="condition.type.toLowerCase() !== 'else'" :span="6">
<editable-value name="inputAction" dataType="property" v-model="condition.inputAction" :schema="condition.schema"></editable-value>
</el-col>
<el-col v-if="condition.type.toLowerCase() !== 'else'" class="operator" :span=".5">
=
</el-col>
<el-col v-if="condition.type.toLowerCase() !== 'else'" :span="6">
<editable-value name="inputActionValue" dataType="value" v-model="condition.inputActionValue" :schema="condition.schema"></editable-value>
</el-col>
<el-col v-if="condition.type.toLowerCase() !== 'else'" class="operator" :span=".5">
THEN
</el-col>
<el-col :span="6">
<editable-value name="ouputAction" dataType="output" v-model="condition.ouputAction" :schema="condition.schema"></editable-value>
</el-col>
<el-col :span=".5">
<el-button icon="el-icon-delete" type="text" plain class="trans scale delete-condition el-col-offset-1 ev-controls" #click="e => { deleteCondition(index) }"></el-button>
</el-col>
</el-row>
</template>
export default {
components: { EditableValue },
data () {
return {
customFormData: [
{
type: 'if',
schema: Schema
}
],
salt: 1043423
}
},
computed: {
randomSalt () {
return this.salt + this.getRnd()
}
},
methods: {
deleteCondition (index) {
this.customFormData.splice(index, 1)
},
getRnd () {
return Math.floor((Math.random() * 1000000 / (Math.random() - 1)) + 1)
},
chainCondition (type) {
this.customFormData.push({ ...this.customFormData[this.customFormData.length], type, schema: this.schema })
}
}
}
Basically, each Editable-Values component represents an input field, customFormData is an array listing each row of input fields belonging to the main form. When deleting a row, we simply splice that row from the array and vice versa for addition.
Main Problem
Everything else works fine except when I delete a row that is neither the first nor last in the array. When such a deletion happens, the data (code) in the app is correct and represents the deletion.
The UI however, seems as if the row below the intended one was deleted. Using pseudocode, the behavior is as follows:
UI
<row 1 data="one"/>
<row 2 data="two" />
<row 3 data="three"/>
Code
data: {
1: 'one',
2: 'two',
3: 'three'
}
// If we delete row 2, the UI becomes as follows:
UI
<row 1 data="one"/>
<row 2 data="two" />
Code
data: {
1: 'one',
3: 'three'
}
What can I do to make sure that each row represents its data correctly.?
I tried forcing a rerender by changing the row key, however, this just ends up displaying empty input fields. I have also tried using an object instead of an array to display the rows, but the results are much worse, data doesn't seem so reactive with objects.
What can I do to fix this?

Your problem most probably comes from using v-for index as a key. key must be unique and stable - index satisfies only 1st condition (when the 2nd row is deleted, 3rd row now gets index 2 instead of 3 and Vue rendering mechanism makes false assumptions about what needs to be re-rendered/updated in the DOM)
Workaround with random key doesn't work either. computed caches the value and changes only when some of the reactive dependencies are changed (this.salt in this case) so you are actually using same key for all the rows. Changing it into method will lead to actually re-rendering everything all the time - this is not something you want either
Only way to solve it correctly is to assign unique ID to each row - it may very well be an index but it must be stored with each row so it does not change every time some row is deleted...

Related

sort multiple columns on Column header click in ag-grid

In documentation, sorting api i.e columns API method "applyColumnState" used for sorting multiple columns on external button click
But Can we sort multiple columns on a Column header click?
For eg, On Column A header cell click I want Column A to be sorted desencding and Column B to be sort ascending. Is this possible?
from the docs : https://www.ag-grid.com/documentation/javascript/row-sorting/#sorting-api
you can manually sort through multiple columns, one after one using the ColumnState API:
gridOptions.columnApi.applyColumnState({
state: [
{ colId: 'country', sort: 'asc', sortIndex: 0 },
{ colId: 'sport', sort: 'asc', sortIndex: 1 },
],
defaultState: { sort: null },
});
if you want to click on a header and sort an other one, you can disable sorting on the header in question, listen for the click on it an execute the above applyColumnState to manually sort.
you can listen to the click on the header by adding a listener on the .ag-header-cell class (https://stackoverflow.com/a/57812319/6641693) or simply by creating your own header component that would trigger any function you want using headerComponentFramework on the column Definition :
headerComponentFramework: (params) =>{
return (
<div>
.....
</div>
)
}

Can Autocomplete component have different value and option types?

As per docs, Autocomplete component has no distinction between option and actual value.
I have a list of options as objects with ids. When I select an option I want to get its id as a value, not the object itself. Also, when I set the value of Autocomplete I want to pass in id only.
Is it possible?
<Autocomplete
options={[{id: 1, label: 'foo'}, {id: 2, label: 'bar'}]}
value={1}
onChange={(_, value) => { /* value should be number (id) */ }}
/>
Update: option label should remain configurable
Ciao, unfortunately value on onChange event returns one of the options selected. So is not possible to return only one attribute of the element.
The only thing you can do is take the value.id:
<Autocomplete
options={[
{ id: 1, label: "foo" },
{ id: 2, label: "bar" }
]}
getOptionLabel={(option) => option.label} // this to show label on Autocomplete
getOptionSelected={(option, value) => option.id === value.id} // this to compare option on id value
onChange={(event, value) => console.log(value.id)} // here access to id property of value object
...
/>
Here a codesandbox example.

React Forms Updating nested state object - reconstructing the state object from input

I've searched the react documentation, as well as several questions here and this article, but I'm confused as to how to accomplish this task.
https://goshakkk.name/array-form-inputs/
I'm trying to iterate over a nested state object in a react application. the questions is an array of objects, each with title, description and options.
questions: [
{
title: 'EKG',
description: 'EKG changes affect risk',
options: [
{
value: '1',
description: 'ST Wave changes'
}
],
}
right now I'm just working with the title value before I work with the entire object.
I'm trying to create an event updater for the edit/new form, but I'm having trouble putting the question object back together to update state.
Here is my form:
{this.state.questions.map((q, i) => (
<div>
<label>Question {i+1 + '.'}</label>
<input
type="text"
placeholder={`Question #${i+1}`}
name="title"
defaultValue={q.title}
onChange={this.handleQuestionChange(i)}
/>
<button type="button" className="small">-</button>
</div>
)
)}
And here is my updater:
handleQuestionChange = (i) => (e) => {
console.log(e.target.name, e.target.value, i)
let stateCopy = Object.assign({}, this.state)
const name = e.target.name
const questions = stateCopy.questions.map((question, j) => (
j === i ?
question[name] = e.target.value
:
question[name] = question[name]
))
console.log(stateCopy.questions, questions)
stateCopy.questions = questions
this.setState({questions: stateCopy.questions})
}
it handles the first update fine, but since my conditional if statement in the handler squashes the object into one field, the 2nd update creates an error. I thought that question[name] would update the key:value pair, but it return NAN so I end up with this when logging the before and after question state:
{title: "EKG", description: "EKG changes affect risk", options: Array(1), NaN: "EKG4"}
changing the handler to this gets me closer...at least returns an object, but I can't access the variable name from e.target.value dynamically:
const questions = stateCopy.questions.map((question, j) => (
j === i ?
question[name] = {question: {name: e.target.value}}
:
question[name] = {question: {name: name}}
))
which gives me this for the question object, and eliminates the other two key value pairs. I don't think I need to manually iterate over every key: value pair just to set one of them.
{name: "EKGn"}
Any tips? I realize this may be more of a javascript / Object manipulation question than a react one, but i've tried several variations including using an string literal ES6 syntax for the dynamic name and it's still not working. x:
question
The problem was in the .map function. I'm posting the working code here, in case it helps anyone else. I'm not sure what the etiquette is for answering your own question.
const questions = stateCopy.questions.map((question, j) => {
if (j === i ) question[name] = e.target.value;
return question;
})

Filter by date range

In Ember it is easy to filter an array where you are looking for matching values ( Only return name == "John) What I can't figure out is how to filter with a greater than or less than ( Return all objects whose startDate is before today
In my app I have a collection of deliverables. I want to divide these deliverables into three categories: Due within ten days, Past due, and then the rest.
I found the following example in another SO post, but can't figure out how to use it to accomplish my goal
filterComputed: function() {
return this.get('content').filter(function(item, index, enumerable){
return item.firstName == 'Luke';
});
}.property('content.#each')
You can just do:
this.get('content').filter(function(item){
return item.get('someProperty') > someVar;
});
This should return an array of objects within your defined date range. Should work in Ember ^2.x.
filterComputed: computed('content.#each', 'startDate', 'endDate', function() {
return this.get('content').filter(function(item) {
var contentDate = item.get('date'); // expecting item to have a date property
return contentDate > this.get('startDate') && bookingDate < this.get('endDate');
});
})
With ES6 you could even do something like this:
filterComputed: computed('content.#each', 'startDate', 'endDate', function() {
return this.get('content').filter(item => item.get('date') > this.get('startDate') && item.get('date') < this.get('endDate'));
})
If you have a simpler requirement, computed.filterBy() might be right for you. https://emberjs.com/api/classes/Ember.computed.html#method_filterBy
Also helpful: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

JQGrid Dynamic Select Data

I have utilised the example code at Example Code at this link
and I have got my grid to show a dynamically constructed select dropdown on add and edit. However when it is just showing the data in the grid it shows the dropdown index instead of its associated data. Is there a way to get the grid to show the data associated with the index instead of the index itself.
e.g. the data on my select could be "0:Hello;1:World"; The drop down on the edit/add window is showing Hello and World and has the correct indexes for them. If the cell has a value of 1 I would expect it to show World in the grid itself but it is showing 1 instead.
Here is the row itself from my grid:
{ name: 'picklist', index: 'picklist', width: 80, sortable: true, editable: true,
edittype: "select", formatter: "select", editrules: { required: true} },
I am filling the dynamic data content in the loadComplete event as follows:
$('#mygrid').setColProp('picklist', { editoptions: { value: picklistdata} });
picklist data is a string of "0:Hello;1:World" type value pairs.
Please can anyone offer any help. I am fairly new to JQGrids so please could you also include examples.
I know you have already solved the problem but I faced the same problem in my project and would like to offer my solution.
First, I declare a custom formatter for my select column (in this case, the 'username' column).
$.extend($.fn.fmatter, {
selectuser: function(cellvalue, options, rowdata) {
var userdata;
$.ajax({
url:'dropdowns/json/user',
async:false,
dataType:'json',
cache:true,
success: function(data) {
userdata = data;
}
});
return typeof cellvalue != 'undefined' ? userdata[cellvalue] : cellvalue ;
}
});
This formatter loads up the mapping of id and user in this case, and returns the username for the particular cellvalue. Then, I set the formatter:'selectuser' option to the column's colModel, and it works.
Of course, this does one json request per row displayed in the grid. I solved this problem by setting 10 seconds of caching to the headers of my json responses, like so:
private function set_caching($seconds_to_cache = 10) {
$ts = gmdate("D, d M Y H:i:s", time() + $seconds_to_cache) . " GMT";
header("Expires: $ts");
header("Pragma: cache");
header("Cache-Control: max-age=$seconds_to_cache");
}
I know this solution is not perfect, but it was adequate for my application. Cache hits are served by the browser instantly and the grid flows smoothly. Ultimately, I hope the built-in select formatter will be fixed to work with json data.
If you save in jqGrid ids of the select elements and want to show the corresponding textes then you should use formatter:'select' in the colModel (see http://www.trirand.com/jqgridwiki/doku.php?id=wiki:predefined_formatter#formatter_type_select) together with the edittype: "select".
The Usage of stype: 'select' could be also interesting for you if you plan to support data searching.