Vue 3 v-model not properly updating on in Andoid's Chrome? - javascript

I have a component, which is essentially an input/select hybrid field, which allows users to type in the input field, and select items from the dropdown, based on their query.
It works perfectly fine on most devices I've tried, i.e. as the user types something into the input field, the list of items updates and only shows those items which contain that piece of string.
Except the Chrome browser on my Android device - as you type, the list doesn't seem to update, unless I press the "space bar". Very strange. Anyone have any ideas why this might be?
Here is the code in <script setup>:
const props = defineProps([ 'items', 'searchBy' ])
const searchTerm = ref('')
const itemsToShow = computed(() => {
if (props.items) {
if (searchTerm.value) {
return props.items.filter(el => {
if (props.searchBy) {
return el[props.searchBy].toUpperCase().indexOf(searchTerm.value.toUpperCase()) > -1
}
return el.toUpperCase().indexOf(searchTerm.value.toUpperCase()) > -1
})
} else {
return props.items
}
} else {
return []
}
})
And the HTML:
<input
type="text"
class="input"
v-model="searchTerm"
placeholder=" "
/>
<div class="items-list">
<div
v-for="item in itemsToShow"
:key="item"
#click="() => handleAdd(item)"
class="item text"
>
{{ item }}
</div>
<div
v-if="!itemsToShow.length"
class="text"
>
No items match searched term
</div>
</div>
UPDATE:
I've investigated a little, and it seems the searchTerm ref, isn't updating properly, even though its bound using v-model... Still no idea why though.

I've ran into this issue before.
It seems that on certain devices, the v-model waits for a change event, instead of an input one.
Apparently, it's to do with the input method editor (IME) for the specific device.
You can check a discussion about this at https://github.com/vuejs/core/issues/5580
The workaround is to simply bind the input field with value and listen for the input event manually, e.g.
<input
type="text"
class="input"
:value="searchTerm"
#input="(e) => searchTerm = e.target.value"
placeholder=" "
/>

Related

null value in dynamic v-for with functional template refs

Situation
I am building a custom filtering component. This allows the user to apply n filters that are displayed with a v-for in the template. The user can update any value in the input fields or remove any of the filters afterwards.
Problem
After removing one of the filters, my array itemRefs got a null value as the last item.
Code (simplified)
<script setup>
const filtersScope = $ref([])
const itemRefs = $ref([])
function addFilter () {
filtersScope.push({ value: '' })
}
function removeFilter (idx) {
filtersScope.splice(idx, 1)
itemRefs.pop() // <- necessary? has no effect
// validate and emit stuff
console.log(itemRefs)
// itemRefs got at least one null item
// itemRefs = [null]
}
// assign the values from the input fields to work with it later on
function updateValue() {
itemRefs.forEach((input, idx) => filtersScope[idx].value = input.value)
}
</script>
<template>
<div v-for="(filter, idx) in filtersScope" :key="filter.id">
<input
type="text"
#keyup="updateValue"
:ref="(input) => { itemRefs[idx] = input }"
:value="filter.value"
>
<button #click="removeFilter(idx)" v-text="'x'" />
</div>
<button #click="addFilter()" v-text="'add filter +'" />
</template>
>>> Working demo
to reproduce:
add two filters
itemRefs got now the template refs as a reference, like: [input, input]
remove one filter, itemRefs now looks: [input, null]
remove the last filter, itemRefs now looks like: [null]
Question
Without the itemRefs.pop() I got the following error, after removing and applying new filters:
Uncaught TypeError: input is null
With the pop() method I prevent a console error, but the null-value in itemRefs still remains.
How do I clean my template refs cleanly?
I don't know what's up with using $refs inside $refs but it's clearly not working as one would expect.
However, you should never need nested $refs. When mutating data, mutate the outer $refs. Use $computed to get a simplified/focused angle/slice of that data.
Here's a working example.
<script setup>
const filtersScope = $ref([])
const values = $computed(() => filtersScope.map(e => e.value))
function addFilter() {
filtersScope.push({ value: '' })
}
function removeFilter(idx) {
filtersScope.splice(idx, 1);
console.log(values)
}
</script>
<template>
<div v-for="(filter, idx) in filtersScope" :key="idx">
<input type="text"
v-model="filtersScope[idx].value">
<button #click="removeFilter(idx)" v-text="'x'" />
</div>
<button #click="addFilter()" v-text="'add filter +'" />
</template>

Get Input event to fire off on Android (using Nuxt)

I have an input field with a function that filters a list each time a key is pressed, and outputs the results of the filtered list in the browser.
On desktop it works great, however the list only gets displayed on Android mobiles after pressing space or the enter key. To make it stranger, after pressing space or the "enter" arrow on the Android keyboard, the list behaves like it should; filtering and displaying the list as you type. Is there a way to get the behavior I am looking for? I have tried #keydown, #keyup and #keypress.
Input field with v-model
<input
v-model="searchValue"
type="text"
#input="filterStories"
/>
Filter Function and Data Properties
data() {
return {
searchValue: '',
filteredStories: []
}
},
methods: {
filterStories() {
if (this.stories.length) {
let filtered = this.stories.filter(
(story) =>
story.name.toLowerCase().includes(this.searchValue.toLowerCase())
)
if (!this.searchValue) {
this.filteredStories = []
} else {
this.filteredStories = filtered
}
}
}
Output list in browser
<li
v-for="(story, i) in filteredStories"
v-else-if="
stories.length && filteredStories.length && searchValue
"
:key="i"
>
<NuxtLink
:to="'/' + story.full_slug"
>
{{ story.name }}
</NuxtLink>
</li>
I hope this provides enough information. Thank you!
Reference to vuejs docs:
For languages that require an IME (Chinese, Japanese, Korean, etc.), you’ll notice that v-model doesn’t get updated during IME composition. If you want to cater to these updates as well, use the input event instead.
Maybe it's because the type of your keyboard. But if it does not work in all keyboard, try using value and #input instead of v-model:
<input
:value="searchValue"
type="text"
#input="filterStories"
/>
And in filterStory method:
filterStories(e) {
this.searchValue = e.target.value
if (this.stories.length) {
let filtered = this.stories.filter(
(story) =>
story.name.toLowerCase().includes(this.searchValue.toLowerCase())
)
if (!this.searchValue) {
this.filteredStories = []
} else {
this.filteredStories = filtered
}
}
}

Remove the selected option from select box

I am making angular application with angular form.
Here i have given a form with input fields first name and last name which will always showing..
After that i am having children which will be displayed upon clicking the add button and the children will get removed on click remove button.
As of now everything works fine.
Here i am making patching of data to the inputs on click option from select box.. The neccessary inputs gets patched..
HTML:
<div>
<form (ngSubmit)="onSubmit()" [formGroup]="form">
<div *ngFor="let question of questions" class="form-row">
<ng-container *ngIf="question.children">
<div [formArrayName]="question.key">
<div *ngFor="let item of form.get(question.key).controls; let i=index" [formGroupName]="i">
<div *ngFor="let item of question.children">
<app-question [question]="item" [form]="form.get(question.key).at(i)"></app-question>
</div>
</div>
<select multiple (change)="changeEvent($event)">
<option *ngFor="let opt of persons" [value]="opt.key">{{opt.value}}</option>
</select>
</div>
</ng-container>
<ng-container *ngIf="!question.children">
<app-question [question]="question" [form]="form"></app-question>
</ng-container>
</div>
<div class="form-row">
<!-- <button type="submit" [disabled]="!form.valid">Save</button> -->
</div>
</form> <br>
<!-- Need to have add and remove button.. <br><br> -->
<button (click)="addControls('myArray')"> Add </button>
<button (click)="removeControls('myArray')"> Remove </button><br/><br/>
<pre>
{{form?.value|json}}
</pre>
</div>
TS:
changeEvent(e) {
if (e.target.value == 1) {
let personOneChild = [
{ property_name : "Property one" },
{ property_name : "Property two" },
]
for (let i = 0; i < personOneChild.length; i++) {
this.addControls('myArray')
}
this.form.patchValue({
'myArray': personOneChild
});
}
if (e.target.value == 2) {
let personTwoChild = [
{ property_name : "Property three" },
{ property_name : "Property four" },
{ property_name : "Property five" },
]
for (let i = 0; i < personTwoChild.length; i++) {
this.addControls('myArray')
}
this.form.patchValue({
'myArray': personTwoChild
});
}
}
addControls(control: string) {
let question: any = this.questions.find(q => q.key == control);
let children = question ? question.children : null;
if (children)
(this.form.get(control) as FormArray).push(this.qcs.toFormGroup(children))
}
removeControls(control: string) {
let array = this.form.get(control) as FormArray;
array.removeAt(array.length - 1);
}
Clear working stackblitz: https://stackblitz.com/edit/angular-x4a5b6-fnclvf
You can work around in the above link that if you select the person one option then the value named property one and property two gets binded to the inputs and in select box the property one is highlighted as selected..
The thing i am in need is actually from here,
I am having a remove button, you can see in demo.. If i click the remove button, one at last will be got removed and again click the last gets removed..
Here i am having two property one and two, if i remove both the inputs with remove button, the the highlighted value person one in select box needs to get not highlighted.
This is actually my requirement.. If i remove either one property then it should be still in highlighted state.. Whereas completely removing the both properties it should not be highlighted..
Hope you got my point of explanation.. If any needed i am ready to provide.
Note: I use ng-select for it as i am unable implement that library, i am making it with html 5 select box.. In ng-select library it will be like adding and removing the option.. Any solution with ng-select library also appreciable..
Kindly help me to achieve the result please..
Real time i am having in application like this:
Selected three templates and each has one property with one,two,three respectively:
If choose a dropdown then the property values for the respective will get added as children.
Here you can see i have deleted the property name three for which the parent is template three and the template three still shows in select box even though i removed its children
Firstly, get a reference to the select, like so:
HTML:
<select multiple (change)="changeEvent($event)" #mySelect>
<option *ngFor="let opt of persons" [value]="opt.key">{{opt.value}}</option>
</select>
TS:
import { ViewChild } from '#angular/core';
// ...
#ViewChild('mySelect') select;
Then, in your remove function, check if all elements have been removed, and if they have, set the value of the select to null
if (array.length === 0) {
this.select.nativeElement.value = null;
}
Here is a fork of the StackBlitz

ngFor inputs copy values of eachother

I'm using Angular5 and would like to create a form where user can insert new records with a button. Each record has many controls in it and should be changed independently from other records. I have done this many times but now I'm getting weird results.
<form #newRequestForm="ngForm">
<a class="list-group-item" *ngFor="let detail of currentRequest.details; let index$ = index">
.
.
.
<ng-select [items]="products" [searchFn]="searchProduct" (change)="productChanged($event, detail)">
<ng-template ng-label-tmp let-item="item">
{{item.code}} - {{item.name1}}
</ng-template>
</ng-select>
<input class="form-control" name="productname1" type="text" [ngModel]="detail.product.name1" />
<input class="form-control" name="productname2" type="text" [ngModel]="detail.product.name2" />
<input class="form-control" name="productname3" type="text" [ngModel]="detail.product.name3" />
<input class="form-control" type="text" name="description" [(ngModel)]="detail.description" />
The problem is that the method called by ng-select onchange productChanged sets the product names of the current selected product. (Products has 3 seperate name fields in erp.) And when this happens all 3 name fields of all records in the form changes to the name fields of the currently selected product. No matter which ng-select I use, all has changed. All the other fields working seperately eg: description.
So I suppose the bug is in the method but it looks like this:
productChanged($event, detail) {
detail.product = $event;
console.log('-----------------------------------------');
this.currentRequest.details.forEach((d, i) => {
console.log(i, d.product !== null ? d.product.name1 : '');
});
}
Yes, I have tried to debug with the good old console.log and it says that the content of each of the detail.products are perfect, according to the last selection of that record's ng-select.
The input controls are still rewritten by whatever select I make. Why?
Because you didn't specify a track by function, so Angular doesn't really know how to keep track of your inputs.
<a class="list-group-item" *ngFor="let detail of currentRequest.details; let index$ = index; trackBy: customTB">
customTB(index, item) { return index + '-' + item.product.name1; }

Traversing children's children and adding function to all inputs while keep other children untouched

I have been trying to get this to work for a while now and not sure how to do the following. My form component has children that contain regular html markup as well a inputs. If the child is a Input I want to add the attachToForm and detachFromForm functions. If it is not an input I want to continue traversing the children to make sure that the element does not have a child input field. Wether or not the element is an input I still want it to appear on my page, I just want to add the functions to the inputs.
The problem is I can only get my function to return only the inputs, removing the labels and title. I know that is because Im only adding elements with inputs to newChildren, but if I push the other elements in the else if section I get duplicates and i can think of another way of doing this. Im not sure if im not understanding basic JS or having a brain gap.
React.Children.forEach(children, function(child) {
var current = child;
if (child.props && child.props.name) {
this.newChildren.push(React.cloneElement(child, {
detachFromForm: this.detachFromForm,
attachToForm: this.attachToForm,
key: child.props.name
}));
} else if (child.props && child.props.children){
this.newChildren.push(child);
this.registerInputs(child.props.children);
} else {
*need to keep track of parent elements and elements that do not have inputs
}
}.bind(this));
Edit: Not sure if needed but this is and example form im traversing
return (
<Modal className="_common-edit-team-settings" title={`Edit ${this.props.team.name}`} isOpen={this.props.modalIsOpen && this.props.editTeamModal} onCancel={this.props.toggleEditTeamModal} backdropClosesModal>
<Form onSubmit={this.saveChanges}>
<FormSection className="edit-team-details" sectionHeader="Team Details">
<FormField label="Name">
<Input name="name" value={this.state.values.name} onChange={this.handleInputChange} type="text" placeholder={this.props.team.name}/>
</FormField>
<FormField label="Mission">
<Input name="mission" value={this.state.values.mission} onChange={this.handleInputChange} type="text" placeholder={this.props.team.kitMission || 'Kit Mission'} multiline />
</FormField>
</FormSection>
<FormSection className="privacy-settings" sectionHeader="Privacy Settings">
<FormField label="Included in global search results" >
<SlideToggle name="globalSearch" defaultChecked={this.state.values.globalSearch} onChange={this.handleCheckedChange} type="checkbox" />
</FormField>
<FormField label="Accessible by anyone" >
<SlideToggle name="public" defaultChecked={this.state.values.public} onChange={this.handleCheckedChange} type="checkbox" />
</FormField>
<FormField label="Secured with WitCrypt" >
<SlideToggle name="witcryptSecured" defaultChecked={this.state.values.witcryptSecured} onChange={this.handleCheckedChange} type="checkbox" />
</FormField>
</FormSection>
<FormSection sectionHeader="Participants">
{participantsList}
<div id="add-participant" className="participant" onClick={this.toggleAddParticipantModal}>
<span className="participant-avatar" style={{backgroundImage:'url(/img/blue_add.svg)'}}></span>
<span>Add a Participant</span>
<span className="add-action roll"><a></a></span>
</div>
</FormSection>
<Button type="hollow-primary" size="md" className="single-modal-btn" block submit>Save</Button>
</Form>
<AddParticipant people={this.props.people} toggleAddParticipantModal={this.props.toggleAddParticipantModal} modalIsOpen={this.props.modalIsOpen} toggleAddParticipantModal={this.toggleAddParticipantModal} addParticipantModal={this.state.addParticipantModal} />
</Modal>
);
As an aside I started out a lot simpler wanting to do the following but get:
"Can't add property attachToForm, object is not extensible"
If anyone knows why please let me know.
registerInputs: function (children) {
React.Children.forEach(children, function (child) {
if (child.props.name) {
child.props.attachToForm = this.attachToForm;
child.props.detachFromForm = this.detachFromForm;
}
if (child.props.children) {
this.registerInputs(child.props.children);
}
}.bind(this));
}
Judging of an error message, you have a problem with immutable prop object. Starting from React 0.14 the prop is "frozen":
The props object is now frozen, so mutating props after creating a component element is no longer supported. In most cases, React.cloneElement should be used instead. This change makes your components easier to reason about and enables the compiler optimizations mentioned above.
Blog post on this
So somewhere in your code you try to extend a prop object causing an error.
You could wrap different parts of your prop interactions with try..catch construction which will point you the exact problem place.

Categories

Resources