Input box losing focus, caught in re-render loop - input

I've read the previous posts about similar problems and I have added as much as possible as per the suggestions( keys etc, onFocus(only works on a single input)), unfortunately, my react knowledge is insufficient to fix the issue.
The input boxes are losing focus when a single character is typed, a re-render of the component is happening( I think) I can continue to input in the boxes(clicking into the inputs every time calculator working) and the outcomes and sums all work as expected.
Any help would be very much appreciated
Thank you
import { Table, THead, Tr, Th, TBody, Td } from '#twilio-paste/core/table'
import { Text } from '#twilio-paste/text';
import { Input } from '#twilio-paste/core/input';
import { Label } from '#twilio-paste/core/label';
import { styled } from '#twilio-paste/styling-library';
import { Box } from '#twilio-paste/core/box'
const StyledCheckboxContainer = styled(Box)`
> * {
width: 50%;
margin-bottom: 8px;}`
const StyledColumn = styled.div`
display: flex;
flex-direction: column;
flex-basis: 100%;
flex: 1;
margin-left: 8px;
margin-right: 8px; `
const StyledText = styled.div`
font-size: 12px;
color: #606B85;`
const StyledInputContainer = styled.div`
margin-bottom: 24px;`
export default function BudgetCalculator() {
const [monthlyAmount, setMonthlyAmount] = useState(0);
const [interestRate, setInterestRate] = useState(0);
const [deposit, setDeposit] = useState(0);
const [feeAtStart, setFeeAtStart] = useState(0);
const [feeAtEnd, setFeeAtEnd] = useState(0);
var decimalInterest = 1 + (interestRate / 100)
var monthlyAPR = ((decimalInterest) ** (1 / 12)) - 1
function annuityCalculation(monthDynamicArr: number) {
var total = monthlyAmount * ((1 - (1 + monthlyAPR) ** -(monthDynamicArr))) / monthlyAPR
return total
}
let monthsArr = [12, 24, 30, 36, 42, 48, 54, 60]
let monthDynamicArr = monthsArr.map(month => {
return annuityCalculation(month)
})
let totalWithFee = monthDynamicArr.map(month => (month + +feeAtEnd) - feeAtStart)
let totalWithDeposit = totalWithFee.map(month => month + +deposit)
const InputBoxes = () => {
return (
<StyledColumn>
<StyledInputContainer>
<Label required htmlFor="monthly_budget">Monthly Budget</Label>
<Input
key="monthly_budget"
type="number"
value={`${monthlyAmount}`}
onChange={(e) => setMonthlyAmount(parseInt(e.target.value) || 0)}
placeholder="0"
insertBefore={<Text as="span" fontWeight="fontWeightSemibold">£</Text>}
/>
</StyledInputContainer>
<StyledInputContainer>
<Label required htmlFor="apr">APR</Label>
<Input
key="apr"
type="number"
value={`${interestRate}`}
onChange={(e) => setInterestRate(parseInt(e.target.value) || 0)}
placeholder="0"
insertAfter={<Text as="span" fontWeight="fontWeightSemibold">%</Text>}
/>
</StyledInputContainer>
<StyledInputContainer>
<Label htmlFor="deposit">Deposit</Label>
<Input type="number"
key="deposit"
value={`${deposit}`}
onChange={(e) => setDeposit(parseInt(e.target.value) || 0)}
placeholder="0"
insertBefore={<Text as="span" fontWeight="fontWeightSemibold">£</Text>}
/>
</StyledInputContainer>
<StyledInputContainer>
<Label htmlFor="fee_at_start">Fee at start</Label>
<Input
key="fee_at_start"
type="number"
value={`${feeAtStart}`}
onChange={(e) => setFeeAtStart(parseInt(e.target.value) || 0)}
placeholder="0"
insertBefore={<Text as="span" fontWeight="fontWeightSemibold">£</Text>}
/>
</StyledInputContainer>
<StyledInputContainer>
<Label htmlFor="fee_at_end">Fee at end</Label>
<Input key="fee_at_end"
type="number"
value={`${feeAtEnd}`}
onChange={(e) => setFeeAtEnd(parseInt(e.target.value) || 0)}
placeholder="0"
insertBefore={<Text as="span" fontWeight="fontWeightSemibold">£</Text>}
/>
</StyledInputContainer>
</StyledColumn>
);
}
const tableMonths = monthsArr.map((month) => {
return (
<Tr>
<Td key={"months"}>
{month} months
</Td>
</Tr>
)
})
const tableLoan = totalWithFee.map((money) => {
return (
<Tr>
<Td key={"fee"}>
£ {money.toFixed(2)}
</Td>
</Tr>
)
})
const tableDeposit = totalWithDeposit.map((money) => {
return (
<Tr>
<Td key={"totalDeposit"}>
£ {money.toFixed(2)}
</Td>
</Tr>
)
})
const TableExample = () => {
return (
<StyledColumn>
<Table>
<THead>
<Tr>
<Th>Term</Th>
<Th>Loan</Th>
<Th>Total</Th>
</Tr>
</THead>
<TBody>
<Td>
<Tr>
{tableMonths}
</Tr>
</Td>
<Td>
<Tr>
{tableLoan}
</Tr>
</Td>
<Td>
<Tr>
{tableDeposit}
</Tr>
</Td>
</TBody>
</Table>
</StyledColumn >
)
}
return (
<>
<StyledCheckboxContainer
display="flex"
flexWrap="wrap"
>
<InputBoxes />
<TableExample />
</StyledCheckboxContainer>
<StyledText>
Approximate
</StyledText>
</>
)
}

The issue here is that you are creating InputBoxes component each render. Since this is a new component reference it unmounts any previously existing version and mounts the new version. This is why the input focus is lost.
Don't declare React components inside the function component body of another React component.
The path of least resistance is to rename InputBoxes to inputBoxes and declare it as a JSX literal instead of as a React component.
const inputBoxes = (
<StyledColumn>
<StyledInputContainer>
<Label required htmlFor="monthly_budget">
Monthly Budget
</Label>
<Input
...
/>
</StyledInputContainer>
<StyledInputContainer>
<Label required htmlFor="apr">
APR
</Label>
<Input
...
/>
</StyledInputContainer>
<StyledInputContainer>
<Label htmlFor="deposit">Deposit</Label>
<Input
...
/>
</StyledInputContainer>
<StyledInputContainer>
<Label htmlFor="fee_at_start">Fee at start</Label>
<Input
...
/>
</StyledInputContainer>
<StyledInputContainer>
<Label htmlFor="fee_at_end">Fee at end</Label>
<Input
...
/>
</StyledInputContainer>
</StyledColumn>
);
...
return (
<>
<StyledCheckboxContainer display="flex" flexWrap="wrap">
{inputBoxes}
<TableExample />
</StyledCheckboxContainer>
<StyledText>Approximate</StyledText>
</>
);
The alternative would be to keep InputBoxes a component and declare it outside BudgetCalculator and pass all the values/callbacks in as props.
Example:
const InputBoxes = ({
monthlyAmount,
setMonthlyAmount,
interestRate,
setInterestRate,
deposit,
setDeposit,
feeAtStart,
setFeeAtStart,
feeAtEnd,
setFeeAtEnd
}) => {
return (
<StyledColumn>
<StyledInputContainer>
<Label required htmlFor="monthly_budget">
Monthly Budget
</Label>
<Input
...
/>
</StyledInputContainer>
<StyledInputContainer>
<Label required htmlFor="apr">
APR
</Label>
<Input
...
/>
</StyledInputContainer>
<StyledInputContainer>
<Label htmlFor="deposit">Deposit</Label>
<Input
...
/>
</StyledInputContainer>
<StyledInputContainer>
<Label htmlFor="fee_at_start">Fee at start</Label>
<Input
...
/>
</StyledInputContainer>
<StyledInputContainer>
...
/>
</StyledInputContainer>
</StyledColumn>
);
};
function BudgetCalculator() {
...
return (
<>
<StyledCheckboxContainer display="flex" flexWrap="wrap">
<InputBoxes
{...{
monthlyAmount,
setMonthlyAmount,
interestRate,
setInterestRate,
deposit,
setDeposit,
feeAtStart,
setFeeAtStart,
feeAtEnd,
setFeeAtEnd
}}
/>
<TableExample />
</StyledCheckboxContainer>
<StyledText>Approximate</StyledText>
</>
);
}

Related

Unable to set the value to the first input field using id of the field - in vue

I have an input field and a button (when clicked on displays a dropdown with few items) when selecting the items it has to be shown on the first input field. Similarly when clicking on the 2nd button where the dropdown is shown the selected value is shown in the 2nd input field. This entire runs in a for loop , which is where I am facing the problem.
<tr v-for="items in itemList">
<td valign="top"> {{items}} </td>
<td align="left" nowrap>
<input v-model="itemCode" type="text" :id="'item_code_'+items"
#input="handleInput"
size="20" maxlength="27"
autocomplete="off">
<br/>
</td>
<td align="left" nowrap>
<a id="myDropdown" class="dropdown" style="text-decoration:none;font-
size:10pt; padding-left: 10px;padding-right: 10px;"
#click="loadFavs()"
href="javascript:void(0)" title="Click to choose an item from your
favorites">
<img hspace="3" alt="Favorites" src="/images/icons/LoadFav.png"
height="16" width="16"
onmousemove="this.style.cursor='pointer'"
:id="'bd_fav_image_' + items" title="Click to choose an item from
your favorites">
<select class="dropdown-content" v-if="showFav" name="BOMList"
:id="'bd_list_'+items" style="font-size:10pt;width: 100%" v-
model="selected" #change="selectingFav(items)">
<option value=""></option>
<option v-for="(fav,index) in favList" :id="index" v-
bind:value="fav" :key="fav" v-bind:index="index">{{fav}}
{{index}}</option>
</select>
</a>
</td>
<td valign="top" nowrap >
<input type="Text"
:id="'bd_qty_ '+ index"
value="" size="2"
inputmode="numeric"
maxlength="">
</td>
</tr>
favList--> this list holds a list of items , For eg:- A,B,C,D
When I select A it has to be shown in the input field.
selectingFav: function(value) {
console.log("Inside the selectingFav..." + this.selected + "value is ." +value);
setTheValue(value);
}
function setTheValue(val){
console.log("Inside the setThevlaue");
if (val === 1 ){
console.log("inside the if");
$j('#item_code_1').val(this.selected);
console.log("inside the if witht the value " + $j('#item_code_1').val());
}
Tried setting the value based on the id of the input field but nothing is showing up.
If I set the v-model to the input field then all the 3 fields will be showing up the same value.
Can someone please let me know what is the issue. Hope these details are sufficient.
a) v-model is internally implemented as:
<input v-model="myval">
<!-- is --!>
<input :model-value="myval" #update:model-value="v => myval = v">
so you can freely define your own function
<input v-for="obj, ind of whatever" #update:model-value="v => myfn(v, obj, ind)">
b) same as you have an array you v-for on you may make a parallel array
// you get the idea
data: () => ({ imputs: entries.map(e => 0) })
<div v-for="entry, ind of imputs">
<Whatever :entry="entry"/>
<imput v-model="imputs[ind]">
</div>
c) keep your imputs in objects, generally the best choice
// you get the idea
data: () => ({ imputs: entries.map(e => ({ entry: e, input: 0 })) })
// or
computed: {pairs(){ return this.entries.map(e => ({ entry: e, input: 0 })) }}
<div v-for="item of imputs">
<Whatever :entry="item.entry"/>
<imput v-model="item.input">
</div>
Here is how you can achieve that.
data() {
return {
itemList: [
{ id: 1 , value: '' },
{ id: 2, value: '' },
{ id: 3, value: '' }
]
}
},
methods:{
selectingFav: function(value) {
// value holds the index
if (value === 1 )
this.itemList[0].value = this.selected;
else if(value === 2 )
this.itemList[1].value = this.selected;
else
this.itemList[2].value = this.selected;
}
}
}
In HTML template section
<tr v-for="(items,i) in itemList">
<td valign="top"> {{items.id}} </td>
<td align="left" nowrap>
<input v-model="items.value" type="text" :id="'item_code_'+items"
#input="handleInput" size="20" maxlength="27" autocomplete="off">
<br/>
</td>

Vue 3 how to remove item from array with filter method?

I want to delete specific item from array with filter method. This is how I add new items to array first:
addNewInvoiceItem() {
this.invoiceItemList.push({
id: Date.now(),
itemName: "",
qty: "",
price: 0,
total: 0,
});
},
Template:
<tr
class="table-items flex"
v-for="item in invoiceItemList"
:key="item.id"
>
<td class="item-name">
<input type="text" v-model="item.itemName" />
</td>
<td class="qty"><input type="text" v-model="item.qty" /></td>
<td class="price">
<input type="text" v-model="item.price" />
</td>
<td class="total flex">
{{ (item.total = item.qty * item.price) }}
</td>
<img
src="#/assets/trash-bin.png"
alt="Delete icon"
#click="deleteInvoiceItem(item.id)"
/>
</tr>
And delete method:
deleteInvoiceItem(id) {
this.invoiceItemList = this.invoiceItemList.filter(
(item) => item.id == !id
);
},
There is second delete method, which is working correctly:
deleteInvoiceItem(item) {
this.invoiceItemList.splice(this.invoiceItemList.indexOf(item), 1);
},
But I want to know, why method with filter is not working?
I found solution. There was a mistake in this method:
deleteInvoiceItem(id) {
this.invoiceItemList = this.invoiceItemList.filter(
(item) => item.id == !id
);
},
it should be:
(item) => item.id !== id

Vuejs2- Avoid repetition of select field options using vee-validate

Iam using vee-validate plugin for validation. In my form, there is a select field in the table. Rows will be added dynamically in the table. I don't want to select the same select(Description column) option again and again Image. Hence I want to throw an error like "Selected description already exists in a table" this using vee-validate. Kindly help me to solve this.
Here is my code:
<template>
<div>
<b-card>
<div class="panel-body" id="app">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 20px;">No.</th>
<th style="width: 330px;">Description</th>
<th style="width: 130px;" class="text-right">Charges</th>
<th style="width: 130px;">Total</th>
<th style="width: 130px;"></th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in rows" :key="row.qty">
<td>
{{ index +1 }}
</td>
<td>
<select class="form-control" v-model="row.billChgDesc" v-validate="'required|check'" :name="'billChgDesc' + index" data-vv-as="Description" #change="checkRepetation">
<option v-for="option in billChgDescOpt" v-bind:value="option.value"
:key="option.value"> {{ option.text }}
</option>
</select>
<span v-show=" errors.has('billChgDesc' + index)" class="is-danger">{{ errors.first('billChgDesc' + index) }}</span>
</td>
<td>
<input class="form-control text-right" type="text" v-model="row.charges" data-type="currency" v-validate="'required'" :name="'charges' + index" data-vv-as="Charges" >
<span v-show=" errors.has('charges' + index)" class="is-danger">{{ errors.first('charges' + index) }}</span>
<td>
<input class="form-control text-right" :value="row.qty * row.charges" number readonly />
<input type="hidden" :value="row.qty * row.charges * row.tax / 100" number/>
</td>
<td>
<button class="btn btn-primary btn-sm" #click="addRow(index)"><i class="fa fa-plus"></i></button>
<button class="btn btn-danger btn-sm" #click="removeRow(index)"><i class="fa fa-minus"></i></button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right">TOTAL</td>
<td colspan="1" class="text-right"><input class="form-control text-right" v-model="delivery" number/></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</b-card>
</div>
</template>
<script>
import Vue from 'vue'
import accounting from 'accounting'
export default {
data: function () {
return {
billChgDescOpt: [
{ value: '', text: 'Select' },
{ value: 'M', text: 'Maintenance Fee'},
{ value: 'W', text: 'Water Charges'},
{ value: 'P', text: 'Penalty Fee'},
],
rows: [
{qty: 5, billChgDesc: '', charges: 55.20, tax: 10},
{qty: 19, billChgDesc: '', charges: 1255.20, tax: 20},
],
grandtotal: 0,
delivery: 40,
selectArr:[]
}
},
methods: {
addRow: function (index) {
try {
this.rows.splice(index + 1, 0, {});
} catch(e)
{
console.log(e);
}
},
removeRow: function (index) {
this.rows.splice(index, 1);
},
checkRepetation:function(){
this.$validator.extend('check', {
getMessage: field => '* Slected ' + field + ' already exists',
validate: function(value){
selectArr.push(value);
}
})
}
}
}
</script>
<style lang="scss" scoped>
.is-danger{
color: RED;
}
</style>
Thanks in advance.
You're on the right track, but a couple changes need to be made. When you call this.$validator.extend, that only needs to be done once - when your component is created. It attaches the check method to the validator, so then every time you have the attribute v-validate="'required|check'" in your HTML, it will run that check method.
In your check validator, you need to answer the question "is this value already selected". The answer is to go through the this.rows and see if any of them have the same billChgDesc property. Because this is in Vue, by the time the validator gets run, the row in question already does have that value, so you want to check if MORE than one row have that value. So, something like this:
mounted() {
var self = this;
this.$validator.extend('check', {
getMessage: field => '* Selected ' + field + ' already exists',
validate: function(value){
return (self.rows.filter(function(v){
return v.billChgDesc == value;
}).length <= 1);
}
});
}
This validator returns true if only one item has the given value. I'm using the built-in filter method of Array (see docs).
You can see an example of this all working here: https://jsfiddle.net/ryleyb/f9q50wx4/1/

How to edit particular row in a table by popping a form modal in vuejs?

I am using a modal form to add new details to the row of a table. After adding details, I’m just adding edit and delete buttons at the end of it. Now here delete button is working fine. How to edit a row of a table by popping replicate of form modal by clicking “edit” button in a row.
Here’s my code:
<div class="layout-padding">
<div
class="item item-link"
v-for="modal in types"
#click="$refs[modal.ref].open()"
>
<i class="item-primary">add</i>
<div class="item-content has-secondary">
<div>{{modal.label}}</div>
</div>
</div>
</div>
<q-modal ref="maximizedModal" class="minimized" :content-css="{padding: '50px'}">
<div class="main">
<label>Enter Your Name:</label>
<input id="name" name="name" type="text" placeholder="Enter your Name" v-model="YourName">
<br>
<label>I am:</label>
<input type="radio" id="Male" value="male" v-model="picked">
Male
<input type="radio" id="Female" value="female" v-model="picked">
Female
<br>
<div class="button">
<button class="red" #click="$refs.maximizedModal.close()">Close</button>
<button class="blue" v-on:click="sub" #click="$refs.maximizedModal.close()">Submit</button>
</div>
</div>
</q-modal>
<table>
<thead>
<th>Name</th>
<th>Gender</th> </thead>
<tbody class="result">
<tr v-for="(h, index) in final">
<td v-for="(value, key, index) in h">
{{ value }}
</td>
<td><button id="edit" class="green edit" v-for="modal in types"
#click="ed(h, index);$refs[modal.ref].open()" type="submit">EDIT</button></td>
<td><button id="delete" class="red delete" v-on:click="del(index)" type="submit">Delete</button></td>
</tr>
</tbody>
</table>
And my script is:
export default {
data () {
return {YourName: '',
details: [],
final: [],
types: [
{
label: 'Add Details',
ref: 'maximizedModal'
}
],
position: 'bottom'
}
},
methods: {
sub: function () {
this.details = {
'name': this.YourName,
'gender': this.picked,
}
this.ed()
if (index === '[object MouseEvent]') {
this.final.push(this.details)
}
if (index > -1) {
this.final.splice(index, 1, this.details)
}
else {
alert('else')
alert(JSON.stringify(this.details))
this.final.push(this.details)
}
},
del: function (index) {
this.$delete(this.final, index)
},
ed: function (details, index) {
return index
}
}
}
If edit button is clicked, the same row should be edited. I don’t know how to proceed further. Please, guide me.
Using the 'splice' can able to modify the given array of object.
Can simply include this inside an 'if' loop:
this.final.splice(this.indi, 1, this.details)

Vue 2 - update value of the array element after click event

Is it possible, when I fire updateCountry method (defined in country-list component), to update the value (if successful call) of that array element, so that the button will dynamically change text based on country.show value?
This is the Vue code I have at the moment:
Vue.component('country-list', {
template: `
<tbody>
<tr is="country" v-for="(country, index) in countries.data">
<td>
<input
type="text"
name="name"
class="form-control country-name"
:value="country.name"
>
</td>
<td>
<select name="show" class="form-control country-show" :value="country.show">
<option value="0">No</option>
<option value="1">Yes</option>
</select>
</td>
<td>
<input
type="text"
name="order"
class="form-control country-order"
:value="country.order"
>
</td>
<td>
<button class="btn btn-primary">
{{ country.show ? "Hide" : "Show" }}
</button>
<button class="btn btn-success"
#click="updateCountry"
:data-id="country.id">Update</button>
</td>
</tr>
</tbody>
`,
props: ['countries'],
methods: {
updateCountry(event) {
let self = this;
let countryID = event.target.dataset.id;
let parent = event.target.closest('.parent');
let countryName = parent.getElementsByClassName('country-name')[0].value;
let countryOrder = parent.getElementsByClassName('country-order')[0].value;
let countryShow = parent.getElementsByClassName('country-show')[0].value;
axios.post('/country/insert', {
id: countryID,
name: countryName,
order: countryOrder,
show: countryShow
})
.then(function (response) {
console.log(self);
})
.catch(function (error) {
console.log(error);
});
}
}
});
Vue.component('country', {
template: `<tr class=parent><slot></slot></tr>`
});
Vue.component('pagination-list', {
template: `
<tfoot>
<tr align="center">
<nav aria-label="Page navigation">
<ul class="pagination">
<li :class="countries.current_page == 1 ? 'disabled' : ''">
<a
:class="countries.current_page == 1 ? 'disabled' : ''"
:href="countries.current_page == 1 ? '#' : countries.prev_page_url"
#click.prevent="pagination(countries.current_page - 1)"
aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<li v-for="i in countries.last_page"
:class="countries.current_page == i ? 'active' : ''"
>
<a
:href="countries.current_page == i ? '#' : '/admin/countries?page='+i"
#click.prevent="pagination(i)"
>{{i}}</a>
</li>
<li>
<a
:href="countries.next_page_url"
#click.prevent="pagination(countries.current_page + 1)"
aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
</tr>
</tfoot>
`,
props: ['countries'],
methods: {
pagination(page) {
this.$parent.getCountries(page);
}
}
});
let App = new Vue({
el: '#app-container',
data: {
countries: []
},
created() {
this.getCountries()
},
methods: {
getCountries(page) {
let self = this;
let getParam = page ? '?page=' + page : '';
axios.get('/admin/countries' + getParam)
.then(function (response) {
self.countries = response.data;
})
.catch(function (error) {
console.log(error);
});
},
filterCountries(event) {
let name = event.target.value;
let self = this;
if(name.length > 2) {
axios.get('/country/search', {
params: {
name: name
}
})
.then(function (response) {
self.countries = response.data;
console.log(self.countries);
})
.catch(function (error) {
console.log(error);
});
}
if((event.keyCode === 8 && name.length === 2) || !name.length){
this.getCountries();
}
}
}
})
This code would be much more Vue like if you used v-model and it would cut down on some of the things you are having to do. For example if you update your country-list template like this:
<tbody>
<tr is="country" v-for="(country, index) in countries.data" :key="country">
<td>
<input
type="text"
name="name"
class="form-control country-name"
v-model="country.name"
>
</td>
<td>
<select name="show" class="form-control country-show" v-model="country.show">
<option value="0">No</option>
<option value="1">Yes</option>
</select>
</td>
<td>
<input
type="text"
name="order"
class="form-control country-order"
v-model="country.order"
>
</td>
<td>
<button class="btn btn-primary">
{{ country.show ? "Hide" : "Show" }}
</button>
<button class="btn btn-success"
#click="updateCountry(country)"
:data-id="country.id">Update</button>
</td>
</tr>
</tbody>
Then your updateCountry method could just be this
updateCountry(country) {
axios.post('/country/insert', country)
.catch(err => //do something on error)
}
Because using v-model, all the values are already updated locally, and you are just posting the values to the server. Since you are passing the actual country to the updateCountry method, there is no need to get the values from the inputs.
Note also, I added :key="country" to your v-for because a key is required when you iterate a component. If you have a country.id that would be even better as the key. But, again, I don't understand why you need the country component at all. It's completely unnecessary at this point.