Prevent form rerendering in conrolled react inputs - html

I am new to React.js and recently I learned about controlled inputs in React.
Code:
Here's a sample implementation that I made:
import React, { useState } from 'react';
const MyForm = () => {
console.log('rendered'); // Line 5
const [text1, setText1] = useState('');
const [text2, setText2] = useState('');
const onSubmit = (evt) => {
evt.preventDefault();
console.log(text1, text2);
}
return (<form onSubmit={onSubmit}>
<input type="text" value={text1} onChange={ev => setText1(ev.target.value)} />
<input type="text" value={text2} onChange={ev => setText2(ev.target.value)} />
<input type="submit"/>
</form>);
};
Problem:
Performance.
Above implementation works correctly, but I noticed that every time one of the field changes, console.log('rendered'); at line 5 is called again, and the entire form seems to be re-rendered. I guess that this could cause some problem especially for more advanced forms with many input fields and heavy pre-processing, etc. Ideally only the field that has changed should be re-rendered.
So I was wondering if my understanding of controlled inputs and form is okay. If not what is more scalable way of implementing this?

Since the state changes, the component will re-render. this is normal. if you dont want that, you need to "export" your input fields to new components with their own state, but then you have to somehow ref these components back to your parent form component in order to get their current values when you are going to submit the form.
Check this link on how to use ref, but I think that the form should be way too heavy in order for you to consider such a senario of creating for each input its own state in order to avoid parent component re-rendering on every input change, or even change to uncontrolled component, which is not usually recommended.

Related

Autofill/autocomplete input Web Component shadowDom

TL;DR: Browser Autofill doesn't work as expected when inputs are in shadow DOMs, particularly noticed with the use of Web Components.
Clarification: The subject of this post is the HTML autocomplete attribute with a custom Web Component input. This is NOT referring to auto-completion of search terms.
Set up: First, let's suppose you want to create a vanilla HTML form to gather a user's name, address, and phone number. You would create a form element with a nested input element for each data point and a submit button. Straightforward and nothing unusual here.
Now, to improve the experience for your users you add the autocomplete attribute to each input with its associated value. I am sure you have seen and used this browser-supported feature before, and if you are like me, it is an expected convenience when filling out online forms for address, credit cards, and username/password.
Up to this point, we don't have any issues--everything is working as expected. With the autocomplete attributes added to the inputs, the browser recognizes that you are trying to fill out a form and a typical browser, such as Chrome, will use whatever user-provided data stored within the browser it can to help auto complete the inputs. In our case, granted you have information stored in your Chrome Preferences/Autofill/'Address and more', you will be given a pop-up list with your stored Address profiles to use to populate the form.
The Twist: If you change your native input to a Web Component with an open shadowDom--because perhaps you want a reusable input that has some validation and styling--the autocomplete no longer works.
Expected result:
I would expect the browser autocomplete feature to work as it normally does, such as, find, associate, and prefill inputs and not discriminate web component inputs that our in shadowDoms.
This is a known, lacking feature which is currently being worked on.
Follow https://groups.google.com/a/chromium.org/g/blink-dev/c/RY9leYMu5hI?pli=1 and https://bugs.chromium.org/p/chromium/issues/detail?id=649162
to stay up to date.
You can work around this by creating your input (and label) outside of the web component and including it via a slot.
const createInput = () => {
const input = document.createElement('input');
input.slot = 'input';
input.className = 'enterCodeInput';
input.name = 'code';
input.id = 'code';
input.autocomplete = 'one-time-code';
input.autocapitalize = 'none';
input.inputMode = 'numeric';
return input;
};
const createLabel = () => {
const label = document.createElement('label');
label.htmlFor = 'code';
label.className = 'enterCodeLabel';
label.innerHTML = `Enter Code`;
return label;
};
#customElement('foo')
class Foo extends LitElement {
#state()
protected _inputEl = createInput();
#state()
protected _labelEl = createLabel();
public connectedCallback() {
this._inputEl.addEventListener('input', this._handleCodeChange);
this.insertAdjacentElement('beforeend', this._labelEl);
this.insertAdjacentElement('beforeend', this._inputEl);
}
public disconnectedCallback() {
this._inputEl?.removeEventListener('input', this._handleCodeChange);
this._labelEl?.remove();
this._inputEl?.remove();
}
public render() {
return html`<form>
<slot name="label"></slot>
<slot name="input"></slot>
</form>`;
}
protected _handleCodeChange = (e: Event) => {
// Do something
};
}
You can style the input and label using the ::slotted pseudo-selector.
css`
::slotted(.enterCodeLabel) {}
::slotted(.enterCodeInput) {}
::slotted(.enterCodeInput:focus) {}
`

Conditionally make a page read-only using react

I want to create a React webpage that has both editable and read-only versions, the whole page not just a few elements on the page. A version is displayed to the user based on user id and other conditions. How do I do it?
The only straight forward way I know is to create 2 pages one editable and one read-only and based on the condition show the appropriate version (html page) to the user.
Is there a better and smarter way to do this? Like can I create just one page for both versions and toggle the mode based on the condition to the users?
Your question should have provided an example of some code you had tried but based on the description, very rough example below of one of many possible solutions.
Suppose EditView component is your page and you are able to pass a value for permission based on whatever credential you need to apply.
Then you have a component, ExampleField that takes the permission and displays either an input or static text. A collection of multiple of these fields is mapped from a theoretical array of data that you'll have to fetch from somewhere and the fields are returned by the main component.
const EditView = ({permission}) => {
const [editable, setEditable] = useState();
const [values, setValues] = useState([]);
useEffect(() => {
setEditable(permission);
}, [permission]);
useEffect(() => {
//maybe fetch your data from a back end or whatever and assign it to `values`
//on page load
}, [])
const ExampleField = ({permission, val, index}) => {
const handleChange = (e) => {
let vals = [...values];
vals[index] = val;
setValues(vals);
}
return(
<>
{permission
? <input name="example" type="text" defaultValue={val}
onChange={handleChange} />
: <span>{val}</span>}
</>
)
}
const fields = values.map((value, i) => {
return <ExampleField permission={permission} val={value} index={i}/>
})
return(
<>
{fields}
</>
)
}
Most likely, you'll want to break out various field components into their own file and, instead of using useState, you would probably want to explore useContext or useStore type functionality to lift up your state and do all the react things.
*Haven't tested or even compiled this code - for illustration purposes only.

How to change react component on someone else's onclick

I am building a React app where I render a family tree. For that, in each of the family tree component nodes, I have added a onclick which opens a modal (aka popup form) that allows the user to edit the info of that person. In that modal/popup, I have a submit button on the bottom. I want it so that when the submit button is clicked, the input fields in the form (ex: name, parents, etc..) are fetched and updated on the respective node in the tree. I tried this in my code:
submitbtn.onclick = () => {
alert("couple submit clicked!");
info.husband = document.getElementById("hname_inp").value;
info.wife = document.getElementById("wname_inp").value;
modal.style.display = 'none';
alert(info.husband + ' ' + info.wife)
};
return (
<li>
<div onClick={handleClick}>
<span className="male">{info.husband}</span>
<span className="spacer"></span>
<span className="female">{info.wife}</span>
</div>
<Children />
</li>
);
By default, the component shows the info passed through props. When the submit button is clicked, i want the data from the input fields to replace the data in the component. The onclick and the data is feteched fine, but the component is not updated. I am new to React so it might just be a silly mistake, please bare with me.
Finally, and this is a little of the topic, but when I click the submit button, the screen flickers for a second a html page with no formatting shows up then it goes back to normal. What might be the cause for that?
Edit (New Code):
import React from "react";
export default class Couple extends React.Component {
constructor(props) {
super(props);
this.state = {
husband: this.props.husband,
wife: this.props.wife,
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
const newState = this.state
const modal = document.getElementById('coupleModal');
modal.style.display = 'block';
const submitbtn = document.getElementById('couplesubmitbtn');
submitbtn.onClick = (event) => {
event.preventDefault()
modal.style.display = 'none'
newState.husband = document.getElementById('hname').value;
newState.wife = document.getElementById('wname').value;
}
this.setState(newState);
}
render() {
const children = this.props.children;
return (
<li>
<div onClick={this.handleClick}>
<span className="male">{this.state.husband}</span>
<span className="spacer"></span>
<span className="female">{this.state.wife}</span>
</div>
{children != null && children.length !== 0 ? <ul>{children}</ul> : ""}
</li>
);
}
}
I think you should use different onClick functions on every node.and plus you can change name of the husband using a modal.I have used prompt and saved the data in state for husband and wife
const [Husband, setHusband] = useState("Varun")
const [Wife, setWife] = useState("Alia")
const handleClick = (e) => {
e.preventDefault()
setHusband(prompt("Please enter your Husband Name:"))
};
const handleWife = (e)=>{
e.preventDefault()
setWife(prompt("Please enter your Wife Name:"))
}
return (
<li>
<div>
<span className="male" onClick={handleClick}>{Husband}</span>
<span className="spacer"></span>
<span className="female" onClick={handleWife}>{Wife}</span>
</div>
</li>
);
};
As mentioned in comments before it would be great if you could provide a fiddle etc to look at.
You mentioned that you are new to React so even at the risk of sounding stupid may I just ask are you using some sorf of state handling here? If not then it might be something to look into. If you're already familiar with React state this answer is pointless and should be ignored.
In reactjs.org there are great documentations about what is the difference between state and props?
setState() schedules an update to a component’s state object. When state changes, the component responds by re-rendering.
https://reactjs.org/docs/faq-state.html#what-is-the-difference-between-state-and-props
So in this case information about your family tree would be initialized to state and popup should then update the state via setState. The new input then gets update and UI components rerender.
If I'm right and the state handling will help you go forward I would also recommend to look up React Hooks. Hooks are a new addition in React 16.8 and when you grasp an idea of state using Hooks will be a easy and more elegant way to write your application
==================== Part 2 ====================
Here's the answer to your question you asked below in comments and some additional thoughts:
I assume the flickering is actually page refreshing on submit. So catching the user event and passing it on and calling preventDefault() is a way to go. I will an example below.
Looking at your code I'm more and more convinced that you are indeed lacking the state handling and it's the initial problem here. You could really benefit reading little bit more about it. At the same time it will help you understand better the logic of how React generally works.
Here's another link that might be worth checking out:
https://www.freecodecamp.org/news/get-pro-with-react-setstate-in-10-minutes-d38251d1c781/
And lastly here's the codeSnippet. Note that the wifes input element you're trying to target with getElementById should be document.getElementById("hname") instead of document.getElementById("hname_inp")
submitbtn.onclick = (event) => {
event.preventDefault();
console.log(props.wife);
modal.style.display = "none";
info.husband = document.getElementById("name").value;
info.wife = document.getElementById("hname").value;
alert(info.husband + " " + info.wife);
};
==================== Part 3 ====================
Nice to see that you took a closer look on state handling and have tried it out. I would continue building the knowledge with some additional reading. Here's a good post about Reacts Data handling.
https://towardsdatascience.com/passing-data-between-react-components-parent-children-siblings-a64f89e24ecf
So instead of using state handling separately in different components I would suggest that you move it to App.js as it is the obvious Parent component of others. There you should also think about the data structure. I assume this project is not going to be connected (at least for now) for any api or database and so it's something that would be handled here as well.
So defining some sort of baseline to App.js could look for example like this.
this.state = {
state = { family : [
[{ name: 'kari', gender: male }]
[
{ name: 'jasper', gender: male },
{ name: 'tove', gender: femmale }
],
]
}
}
Then I suggest that you move the handlers here as well. Then writing them here you don't maybe even need separate ones to couples and singles any more.
I'm sorry to hear your still seeing the flickering. My best guess for this is that modal isn't aware about the event.preventDefault. For clarity I would refactor this a bit as well. Generally it's not a good practice to try to modify things via getElements inside React. It's usually all state and props all the way. So I added a few lines of code here as an example of how you could continue on
import React from "react";
import SingleModal from "./Modals/SingleModal";
export default class Couple extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
};
this.popUpHandler = this.popUpHandler.bind(this);
}
popUpHandler(event) {
event.preventDefault()
this.setState({visible: !this.state.visible})
}
render(props) {
return (
<>
<SingleModal visible={this.state.visible} popUpHandler={this.popUpHandler }/>
<li>
<div onClick={this.popUpHandler}>
<span className={this.props.gender}>{this.props.name}</span>
</div>
</li>
</>
);
}
}
And similary in SingleModal getting rid of the form submit like this:
<input
type="submit"
value="Submit"
className="submit"
id="singlesubmitbtn"
onClick={(e) => {
e.preventDefault();
props.popUpHandler(e)
}}
/>
PS. I think this is going to be my last answer on this question here. The answer is getting too long and it's starting to drift off topic of the original question. Good luck with your project

How to trigger a change event on a textarea when setting the value via ngModel Binding

I have a <textarea> within a template driven form of an Angular 7 project.
When editing an object, the form is prefilled with the current values. I want to automatically resize the <textarea> when the content has changed via the [(ngModel)]="property" binding by modifying the element-style.
area.style.overflow = 'hidden';
area.style.height = '0';
area.style.height = area.scrollHeight + 'px';
The code generally is working, but I cannot find a suitable event to trigger it.
Subscribing to the change event of the <textarea> is only working on keyboard input. Using (ngModelChange)="adjustTextAreaSize($event)" has the same behavior.
I tried to execute my resizing code at the end of the ngOnInit() function, but the actual html-control seems to not have any content yet at this point.
Does anyone have an idea which event could do the trick here?
Seemed a rather easy task in the beginning, but I'm breaking my had over this for over an hour now... can not be such a difficult task, can it?
Yes there is a very simple solution for this.
Wrap your textarea inside a form and try the code below:-
HTML
<form #form="ngForm">
<textarea>....</textarea>
</form>
TS
#ViewChild('form') ngForm: NgForm;
ngOnInit() {
this.subscription = this.ngForm.form.valueChanges.subscribe(resp =>
{
console.log(resp); // You get your event here
}
)
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
trigger a change event on a textarea when setting the value via ngModel Binding
This will cause infinite triggering if you do so.
If you don't want to monitor the input model change in a more reactive way, a quicker solution (but a bit hacky) will be simply wrap your code inside setTimeout in ngOnInit() or ngAfterViewInit() where you mentioned it was not working.
setTimeout(() => {
updateSize();
});

How to dynamically add text to textarea while recording via webkitspeech with Angular 2?

Currently, I record my voice with this simple code in Angular Component
speechToText() {
const {webkitSpeechRecognition}: IWindow = <IWindow>window;
const recognition = new webkitSpeechRecognition();
recognition.lang = 'en-US';
recognition.continuous = true;
recognition.interimResults = true;
recognition.onresult = event => {
for (let i = event.resultIndex; i < event.results.length; ++i) {
this.interim_transcript = event.results[i][0].transcript;
}
};
recognition.onerror = event => {
console.log('Error occured', event);
};
recognition.start();
}
}
And in my HTML I have the value bind to the interim result
<textarea #description mdInput rows="5" placeholder="Short Story" name="description" [value]="interim_transcript"></textarea>
The problem, however, is that I can see the text being put into the textarea only after I click on the textarea or outside of it to trigger dom update. How to make it update textarea as soon as I begin saying words giving this live text update, same way as here https://www.google.com/intl/en/chrome/demos/speech.html
This happens because Angular is not aware of the update to interim_transcript since it happens outside of what the Zone is aware of.
I see two immediate ways to fix it:
Run the interim_transcript update in a zone.run call. See NgZone in the docs.
Make interim_transcript an Observable. Actually a Subject, but the point is that it needs to be observable.
I'd recommend the latter, and it basically involves this:
When you define interim_transcript, define it like this: interim_transcript: new Subject<string>()
When you update it in the onresult callback, replace
this.interim_transcript = event.results[i][0].transcript;
with
this.interim_transcript.next(event.results[i][0].transcript);
Change the value binding in your template, replace:
[value]="interim_transcript"
with
[value]="interim_transcript | async"
Observables are an incredibly powerful concept that can make your code more easy to reason about (even though it seems very odd at first). It can boost your performance significantly when you start using the OnPush change detection mechanism. Finally, however cheesy it sounds, can change the way you think about your programs, to a data stream mind model instead of state updates. This will likely sound confusing and weird, but I strongly recommend looking into it, I'm sure it will pay off.
Here are a few good resources to get started:
Using Observable from Rangle.io.
Understand and Utilize the Async Pipe in Angular 2 from Brian Troncone