null value in dynamic v-for with functional template refs - javascript

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>

Related

ngFor don`t show all items immediately after place_changed event from google place autocomplete

I need to add result to the list after place_changed event. I display the list below the input in which I find locations. Event works and result is pushed to array items. But the problem is that new added item don`t display immediately. It displayed after some time or when I click on form where this input is displayed.
.ts:
#ViewChild('locationInput', { static: true }) input: ElementRef;
autocomplete;
items = [];
ngOnInit() {
this.autocomplete = new google.maps.places.Autocomplete(this.input.nativeElement, this.localityOptions);
this.autocomplete.addListener('place_changed', () => {
this.addToListSelectedItem();
});
}
public addToListSelectedItem() {
if (this.input.nativeElement.value) {
this.items.push(this.input.nativeElement.value);
this.input.nativeElement.value = '';
}
}
.html:
<input
#locationInput
class="shadow-none form-control"
formControlName="locality"
placeholder=""
[attr.disabled]="locationForm.controls['region'].dirty ? null : true"
/>
<div *ngFor="let item of items; let index = index">
<div class="listOfLocation">
<div class="itemList">{{ item }}</div>
<img [src]="icons.cross" class="delete-button-img" alt="edit-icon" (click)="deleteTask(index)" />
</div>
</div>
Thanks for the help!
Probably your component Change Detection Strategy is OnPush or google autocomplete is running outside zone.js:
changeDetection: ChangeDetectionStrategy.OnPush
And since items is array and it is stored in memory by reference you need to run manually change detenction:
constructor(private cdr: ChangeDetectorRef)
public addToListSelectedItem() {
...
this.input.nativeElement.value = '';
this.cdr.detectChanges();
Even better would be to work with an RxJS Subject, an Observable for items$ and use the async pipe. The async pipe works like magic what concerns updating the template :-)!
#ViewChild('locationInput', { static: true }) input: ElementRef;
autocomplete;
itemsSubject$ = new Subject<any[]>();
items$ = this.itemsSubject$.asObservable();
// Use a separate array to hold the items locally:
existing = [];
ngOnInit() {
this.autocomplete = new google.maps.places.Autocomplete(this.input.nativeElement, this.localityOptions);
this.autocomplete.addListener('place_changed', () => {
this.addToListSelectedItem();
});
}
public addToListSelectedItem() {
if (this.input.nativeElement.value) {
// Use spread syntax to create a new array with the input value pushed at the end:
this.existing = [...this.existing, this.input.nativeElement.value];
// Send the newly created array to the Subject (this will update the items$ Observable since it is derived from this Subject):
this.itemsSubject$.next(this.existing);
this.input.nativeElement.value = '';
}
}
// I added the deleteTask implementation to show you how this works with the subject:
deleteTask(index: number) {
// The Array "filter" function creates a new array; here it filters out the index that is equally to the given one:
this.existing = this.existing.filter((x, i) => i !== index);
this.itemsSubject$.next(this.existing);
}
And in the template:
<input
#locationInput
class="shadow-none form-control"
formControlName="locality"
placeholder=""
[attr.disabled]="locationForm.controls['region'].dirty ? null : true"
/>
<!-- Only difference here is adding the async pipe and using the items$ Observable instead -->
<div *ngFor="let item of items$ | async; let index = index">
<div class="listOfLocation">
<div class="itemList">{{ item }}</div>
<img [src]="icons.cross" class="delete-button-img" alt="edit-icon" (click)="deleteTask(index)" />
</div>
</div>
For a working example of the RxJS Subject in this concept, see https://stackblitz.com/edit/angular-ivy-dxfsoq?file=src%2Fapp%2Fapp.component.ts.
Maybe beyond this question, but since you're using a reactive form, why not use this.locationForm.get('locality').setValue('...') to set the input instead of using a ViewChild for working with the input? More control this way then using the ViewChild.

how can I make div tags as much as number in state(react.js)

I'm developing with React.js and below is a simplified version of my component. As you can see, when I click the button state(number) would get a number. And here is what I want, make div tags as much as a number in state(number) in the form tag(which its class name is 'b').
Could you tell me how to make this possible? Thanks for reading. Your help will be appreciated.
//state
const[number,setNumber] = useState('')
//function
const appendChilddiv =(e)=>{
e.preventDefault()
setNumber('')
}
<div>
<form className="a" onSubmit={appendChilddiv}>
<input
value={number}
onChange={(e)=>setNumber(e.target.value)}
type="number"/>
<button type="submit">submit</button>
</form>
<div>
<form className="b">
</form>
</div>
</div>
I've created a codesandbox which includes the following code, previously you were storing the value as a string, it'd be best to store it as number so you can use that to map out, which I do via the Array() constructor (this creates an array of a fixed length, in this case the size of the divCount state - when we update the state by changing the input value this creates a re-render and thats why the mapping is updated with the new value)
import "./styles.css";
import * as React from "react";
export default function App() {
//state
const [divCount, setDivCount] = React.useState(0);
//function
const appendChilddiv = (e) => {
e.preventDefault();
// Submit your form info etc.
setDivCount(0);
};
return (
<div>
<form className="a" onSubmit={appendChilddiv}>
<input
value={divCount}
onChange={(e) => setDivCount(Number(e.target.value))}
type="number"
/>
<button type="submit">submit</button>
</form>
<div>
<form className="b">
{Array(divCount)
.fill(0)
.map((x, idx) => (
<div key={idx}>Div: {idx + 1}</div>
))}
</form>
</div>
</div>
);
}
You can map over an array whose length is same as that entered in input & create divs (if number entered is valid):
{!isNaN(number) &&
parseInt(number, 10) > 0 &&
Array(parseInt(number, 10))
.fill(0)
.map((_, idx) => <div key={idx}>Hey!</div>)
}
isNaN checks is number is valid
parseInt(number, 10) converts string into number,
Array(n) creates a new array with n elements (all empty) (try console logging Array(5)) - so you need to fill it
.fill(n) fill the array (make each element n)
and map is used to render different elements from existing things
So, in this way, you can achieve the mentioned result.
Here's a link to working Code Sandbox for your reference
You can do the following.
First thing it does is creates a new array of some length number.
Next, it fills that array with undefined, because creating new arrays like this doesn't really create an array of that length.
Lastly, we map over this array, we use the index as our key.
We return an empty div for each item in the array.
Note, using an index as a key isn't the best idea. In general it should be something as unique as possible. If you have data you can use that is unique, then you should use that as a key.
return new Array(number).fill(undefined).map((_, key) => <div key={key}></div>);
You can even do it without 'Submit' button. See the codesandbox link and the code snippet below:
import "./styles.css";
import * as React from 'react';
import { useState } from 'react';
export default function App() {
const [divTags, setDivTags] = useState([])
const appendChilddiv = (e) => {
const numberAsString = e.target.value;
let numberDivTags;
if (numberAsString.length === 0) {
numberDivTags = 0
} else {
numberDivTags = parseInt(numberAsString, 10)
}
setDivTags([...Array(numberDivTags)])
console.log(divTags)
}
return (
<>
<form className="a">
<input type="number" onChange={appendChilddiv}/>
</form>
<form className="b">
{divTags.map((e, i) => {
return <p key={i}>Div number {i}</p>
})}
</form>
</>
);
}

How to store the return value of a function inside a variable in reactJS

my code:
import "./styles.css";
export default function App() {
getUserValue = (e) => {
let value = e.target.value;
console.log(value)
return value
}
let userInputValue = getUserValue
return (
<div>
<div>
<h4>Sign Up</h4>
</div>
<div>
<div>
<p>Username</p>
<input onChange = {getUserValue}/>
</div>
<div >
<p>Password</p>
<input/>
</div>
</div>
<div>
<button onClick = {console.log(userInputValue)}>Submit</button>
</div>
<div>
<button>
Close
</button>
</div>
</div>
);
}
code sandbox: https://codesandbox.io/s/dazzling-sea-my5qm?file=/src/App.js:0-720
I'm trying to store the returned value of "getUserValue" function to "userInputValue" variable so I can log the input the user made and use it in different functions. I can't get it to work though, when I console log the variable hoping to get the returned result after I made an input I don't get anything, as if the button doesn't work.
I'm trying to store the returned value of "getUserValue" function to "userInputValue" variable so I can log the input the user made and use it in different functions.
You'd do that by making the input state in your component. In a function component like yours, that means using the useState hook (or various libraries like Redux that have alternative ways of doing it).
Here's a simple example, but you can find lots of more complex ones by searching:
const { useState } = React;
function Example() {
// Call the hook to get the current value and to
// get a setter function to change it. The default
// value ("" in this example) is only used the first
// time you call the hook in this component's lifecycle
const [userInput, setUserInput] = useState("");
// Handle changes in the input by setting state.
// Note that this function is recreated every time your
// component function is called to update. That's mostly
// fine, but there are times you might want to optimize
// that.
const onChange = (event) => {
setUserInput(event.currentTarget.value);
};
// Handle clicks on the button that show' the current input.
const onClick = () => {
console.log(`The current userInput is "${userInput}"`);
};
// Return the rendering information for React
return <div>
{/* Provide the value and hook the "change" (really "input") event */}
<input type="text" value={userInput} onChange={onChange} />
<input type="button" onClick={onClick} value="Show Current" />
</div>;
}
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>

Angular 4 nested form update

I've got the following 'triple level' nested form:
FormGroup->ArrayOfFormGroups->FormGroup
Top level (myForm):
this.fb.group({
name: '',
description: '',
questions: this.fb.array([])
});
Nested form array element for 'questions':
this.fb.group({
priority: ['1'],
params: this.fb.group({parameter: ['']})
});
Nested form group element for 'params' is a key:value object of random length.
I'm using the following ngFor to go through elements:
<tr *ngFor="let questionConfigForm of myForm.controls.questions.controls; let i=index" [formGroupName]="i">
...
<div *ngFor="let param of objectKeys(questionConfigForm.controls.params.controls)" formGroupName="params">
<input type="text" [formControlName]="param">
I've got the following behavior:
When I'm updating any of the fields on first two form levels I could instantly see changes in corresponding form controls values with {{myForm.value | json}}.
But if I input something in one of 'params' controls I couldn't see any changes in myForm values, but the form data for 'params' controls will be updated if I will make any changes in corresponding 'questions' form.
For me it looks like 'param' form control receives input data, but doesn't trigger some update event, and I don't know how to fix that, except writing my own function to react on (change) and patchValue in form..
So my question is how to make 'params' controls update myForm without that strange behavior?
UPD:
initQuestionConfig() {
return this.fb.group({
priority: ['1'],
params: this.fb.group({parameter: ['']}),
});
}
addQuestionConfig() {
const control = <FormArray>this.myForm.controls['questions'];
const newQuestionCfg = this.initQuestionConfig();
control.push(newQuestionCfg);
}
Finally the problem is solved.
The root of this issue was the way I've cleaned up already existing 'params'.
To remove all parameters from 'questions' I used the following code:
const control = <FormArray>this.myForm.controls['questions'];
control.controls[index]['controls'].params = this.fb.group([]);
And the reason of those glitches was this new 'fb.group' instance.
Now I'm removing params one by one, keeping original formGroup instance and it works as expected:
const control = <FormArray>this.myForm.controls['questions'];
const paramNames = Object.keys(control.controls[index]['controls'].params.controls);
for (let i = 0; i < paramNames.length; i++) {
control.controls[index]['controls'].params.removeControl(paramNames[i]);
}
#MilanRaval thanks for your time again :)
Try this: Give formArrayName and formGroupName like below...
<div formArrayName="testGroup">
<div *ngFor="let test of testGroup.controls; let i=index">
<div [formGroupName]="i">
<div class="well well-sm">
<label>
<input type="checkbox" formControlName="controlName" />
</div>
</div>
</div>
</div>

creating elements in React

I don't understand how elements are created in React.
I have some code below where the goal is to create elements on a form submit using a value from a refs - so for every submit in a form, it creates a new <h1> tag with the content of the textbox inside of it. A sample of what I'm trying to do looks like:
...
addHeader(e) {
e.preventDefault();
const newHeader = this.refs.post.value;
var newpost = React.createElement("h1", {
type: "text",
value: newHeader
});
}
...
render() {
return (
<div className="form-section">
{ newPost }
<form onSubmit={this.addHeader.bind(this)}>
<input id="input-post" type="text" placeholder="Post Here" ref="post" />
<input type="submit" value="Submit" />
</form>
<button className="form-section__submit" onClick={this.clearFields.bind(this)}>Clear All</button>
</div>
);
}
Basically my thinking is in my addHeader() function I'm assigning a variable of newPost to the method and calling it within my component. This code is causing 2 errors:
33:9 warning 'newpost' is assigned a value but never used no-unused-vars
49:13 error 'newPost' is not defined no-undef
What I don't understand, is (from what I can see) I am assigning a value to that variable and also using it in the component that I am rendering... along with that, I don't understand this error message. How can something be assigned a value but be undefined at the same time...? Is it because it's in the wrong scope? How do I declare where the new element is rendered specifically in the component?
I read the documentation but it doesn't give a clear answer as to how to control where in the component the new element is rendered.
Made some changes to your code. You're going to want to initialize component state in your constructor. In your addHeader method you will use this.setState to update the state of the component with a new posts value including the value of this.input. I changed your ref on the input an actual ref. You take the element and store on this. Every time you add a new post you will get a new <h1> with the value of the textarea.
...
addHeader(e) {
e.preventDefault();
this.setState((prevState, props) => {
return { posts: [ ...prevState.posts, this.input.value ] };
});
}
...
render() {
const { posts } = this.state;
return (
<div className="form-section">
{ posts.map( text => <h1>{ text }</h1> ) }
<form onSubmit={this.addHeader.bind(this)}>
<input id="input-post" type="text" placeholder="Post Here" ref={ el => this.input = ref } />
<input type="submit" value="Submit" />
</form>
<button className="form-section__submit" onClick={this.clearFields.bind(this)}>Clear All</button>
</div>
);
}
As an aside: Binding functions in the render method of react components will cause a performance hit. There is no need to re-bind the this context of the function on every render. this.clearFields.bind(this) should become this.clearFields and you will need to add this.clearFields = this.clearFields.bind(this) to your constructor. You do not need to bind functions that are not used as callbacks.
You're going to want to do the same thing for this.addHeader.bind(this).

Categories

Resources