React and Google Autocomplete with Typescript - reactjs

I have a google address autcomplete and a react-bootstrap form and I can't make the types match for the ref.
class ProfileForm extends React.Component<PropsFromRedux, ProfileFormState> {
private myRef = React.createRef<FormControl<any>>();
private autocomplete?: google.maps.places.Autocomplete;
...
}
My Form control is like this:
<Form.Control ref={this.myRef} type="text" placeholder="Enter address"
defaultValue={this.props.userProfile.location!.formatted_address}
/>
Here is how I create my autocomplete:
function forceCast<T>(input: any): T {
// #ts-ignore <-- forces TS compiler to compile this as-is
return input;
}
//#ts-ignore
this.autocomplete = new google.maps.places.Autocomplete(
forceCast<HTMLInputElement>(this.myRef.current),
options);
My question is:
How do I do this without the force cast?
The google autocomplete wants an HTMLInputElement (I've tried loads of other types and stuff) but the best I can get with react-bootstrap is the FormControl (I can't figure out how to "cast" this to anything any other way than what is shown).

Related

How to get the value of input tag onSubmit without using onChange in React js/Typescript?

I have no other special events onChange so I need to simplify my code by only handling events onSubmit specifically on the Form tag.
I'm creating a simple Form in Typescript in React and When I click Submit, I want to log the value of the input box in the console. But I get an error when I do this.
import React from "react";
const handleSubmit =(event:React.FormEvent<HTMLFormElement>) =>{
event.preventDefault();
console.log(event.currentTarget[0].value);
};
export const InputForm: React.FC = () =>{
return (
<form className='form-container' onSubmit={handleSubmit}>
<input className="input" placeholder="Enter value" required />
<button type="submit">Enter</button>
</form>
)
}
My reasoning for this line => console.log(event.currentTarget[0].value); is that the form tag has 2 elements, input and button, so event.currentTarget[0] alone returns the input tag in the console but it passes an error(Property 'value' does not exist on type 'Element'.ts(2339)) when i try to add .value
The usual thing here is to use a controlled component, but you've said you don't want to do that.
The problem is that event.currentTarget[0] is just of type Element, and Element doesn't have value (which is just on certain elements, like HTMLInputElement, HTMLSelectElement, etc.).
You know it's an Element subtype with value, but TypeScript doesn't, because not all form control elements have value (fieldset and object don't, for a start, but they're counted as form elements).
You can put a utility assertion function in your toolkit:
function assertIsFormFieldElement(element: Element): asserts element is HTMLInputElement | HTMLSelectElement | HTMLButtonElement {
// Customize this list as necessary −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
if (!("value" in element)) {
throw new Error(`Element is not a form field element`);
}
}
...then use it:
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) =>{
event.preventDefault();
const firstField = event.currentTarget[0];
assertIsFormFieldElement(firstField);
console.log(firstField.value);
};
Playground link
Side note: If you're going to do this, you might consider putting a name on the input and using that when looking up the element, rather than assuming the input will be the first element:
<input name="the-name" className="input" placeholder="Enter value" required />
Then you can use a utility function to get the named item from the form:
function getFormControl(form: HTMLFormElement, name: string): HTMLInputElement | HTMLSelectElement | HTMLButtonElement /* | ...*/ {
const control = form.elements.namedItem(name);
if (!control || control instanceof RadioNodeList || !("value" in control)) {
throw new Error(`Form control "${name}" not found or was a RadioNodeList`);
}
return control;
}
// ...
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) =>{
event.preventDefault();
const inputField = getFormControl(event.currentTarget, "the-name");
console.log(inputField.value);
};
Playground link

Is there a standard design pattern for viewing + editing a record in React / Material-UI?

Frequently I have a record of data to display to the user, and I want to display an Edit button allowing that data to be modified:
Create Pizza:
sauce: Margherita
cheese: Gorgonzola
radius: 12
[EDIT] [ORDER]
Is there a standard pattern for this in React / Material-UI? It seems silly to implement the same view twice, once with (readonly) <div>s or whatever and once with <input>s. Is there a better way? (Perhaps there's a Material-UI component with an "editable" prop, for instance.)
Take a look at api for text field. You'll see that you have a couple of ways of doing what you are looking.
You could set disabled={true}
You also get inputProps, as an example
<TextField id="time" type="time" inputProps={{ readOnly:true }} />
All the other elements should also have one or the other or both.
I usually have a wrapper component that accepts props such as ({isReadOnly: boolean}) & within that is the "native" React MUI component & then I send back editable or a read only component back to the caller.
EDIT:
interface MyTextFieldProps extends StandardTextFieldProps {
isEditable?: boolean;
// send in any other props necessary to render your non editable
// portion
}
const MyTextField: FC<MyTextFieldProps> = (props) => {
const { isEditable, ...other } = props;
return <>{props.isEditable ? <TextField {...other}></TextField> : <span>text:{props.value}</span>}</>;
};

React Native TextInput has no focus method

I've been digging around and I can't seem to find a proper way to focus the next TextInput in a form. I'm using React Native 0.61.5 & React 16.9.0 and have set-up the project with a Typescript template. Most of what I've found online uses earlier versions of React Native or React and I can't downgrade because it's a company project and what versions we use are up to the dev leads.
React Native's documentation https://facebook.github.io/react-native/docs/textinput#__docusaurus doesn't show a .focus() method to exist on TextInput. There's onFocus, but that occurs after the TextInput has been focused, it doesn't help us set the focus to a particular TextInput on the screen after hitting the return key.
Using refs is a sound idea:
inputRef = React.createRef<'TextInput>(); // (comma after < because it kept hiding the code)
Intellisense for hovering over inputRef:
(property) LoginForm.inputRef: React.RefObject<'TextInput> // (comma after < because it kept hiding the code)
I'm using this ref like this:
this.inputRef?.current?.focus();
but I keep getting a typescript error saying:
Property 'focus' does not exist on type 'TextInput'.
Which, given the documentation, makes sense since I couldn't find it as a property there.
Intellisense for TextInput when hovering over ref attribute:
(JSX attribute) React.ClassAttributes.ref?: string | ((instance: TextInput | null) => void) | React.RefObject | null | undefined
I want to be able to tap the return key on the android/ios virtual keyboard and have the focus shift to the next TextInput so that I can submit the form.
The object create with React.createRef() have the following shape
{ current: TextInput }
You can access the text input element by doing:
inputRef.current.focus();
<TextInput
placeholder = "FirstTextInput"
returnKeyType = { "next" }
onSubmitEditing={() => { this.secondTextInput.focus(); }}
blurOnSubmit={false}
/>
<TextInput
ref={input => { this.secondTextInput = input; }}
placeholder = "secondTextInput"
/>

Inheritance in react component

Please consider a following case:
I have created a "baseUser" component which is a form having three fields username, password and name. Now I want this form in three different applications : Application1, Application2 and Application3.
In Application1, User component should use this baseUser component but want only two fields (username, password) from baseUser state and should also have two additional fields which is first-name and last-name.
In Application2 and Application3, User component should work same as the baseUser.
Also all the actions, events, states should be able to work alone and be able to overridden.
The render method should also be overridden as their can be different UIs for different applications.
How can we achieve this functionality using react components? Does inheriting "baseUser" in applications using "extends" cause any issue (Or is it correct way to do it)?
React does not use Inheritance. You can't extend a react component. React basically work on the Composition. Composition means, creating small parts and combine them together to make a complete part.
Now, in your situation. There are only 3 input fields into your baseUsr component and you want to use only two of them into your application1. Simply you can't do this. You have to render complete baseUsr component.
You are getting the concept of components wrong. Think about components like a function or part of UI that is abstract or can be used in standalone.
For example, you can create a Header component because it can be used in isolation and can be used on multiple pages. But an input field in a form can not be used in isolation.
This confusion is created because you create components just like classes in javascript but they are the basic building block of UI.
For more information read Composition VS inheritance on React docs
You can write your baseUser this way:
class BaseUserForm extends Component {
handleChange = ({ currentTarget: input }) => {
const data = { ...this.state.data };
data[input.name] = input.value;
this.setState({ data }); //state is in inheriated form.
};
handleSubmit = e => {
e.preventDefault();
//Form validation
if (!errors) this.doSubmit();
};
renderInput = (name, label, type = "text") => {
return (
<Input
name={name}
label={label}
type={type}
error={this.state.errors[name]}
value={this.state.data[name]}
onChange={this.handleChange}
/>
);
};
renderButton = (label, type = "submit") => {
return (
<button
type={type}
className="btn btn-primary"
// disabled={this.validate()}
>
{label}
</button>
);
};
}
export default BaseUserForm;
Then depending on your Application, you can add/remove input fields as needed.
class AppForm extends BaseUserForm{
state={
data:{username:"",
password:""}
};
doSubmit = () => {
//Do your submit here.
};
render(){
return(
<form onSubmit={this.handleSubmit}>
{this.renderInput("username", "User Name")}
{this.renderInput("password", "Password")}
//Add more field as needed.
</form>
);
}
}

Enzyme change another input field

I have a simple React component which has one email input field and a checkbox like this:
interface MyProps {
onSubmit?: (form: any) => void;
}
class Preferences extends React.Component<MyProps> {
state = {
primaryEmailCheckbox: false,
primaryEmail: "",
};
onPrimaryEmailChange = e => {
this.setState({ primaryEmail: e.target.value });
let checkbox = document.getElementById("primaryEmailCheckId") as HTMLInputElement;
checkbox.disabled = false; //<<< checkbox is null. lets say this is line 18
}
}
render() {
return (
<StaticContent />
<h3>Email Address</h3>
<div className="ui mini icon input">
<input type="email" value={this.state.primaryEmail} placeholder="Enter email..." onChange={this.onPrimaryEmailChange} />
</div>
<div className="ui checkbox">
<input type="checkbox" disabled={true} id="primaryEmailCheckId" onChange={e => this.setState({ primaryEmailCheckbox: e.target.checked })} /><label> Continue to receive further email communications from Us </label>
</div>
);
}
}
export default Preferences;
When anyone enters any thing on the email field, the checkbox becomes visible for user to check it or keep it unchecked.
When I run the application, it works as expected. But when I test it, it says checkbox is null (at line 18), so you cannot set disabled on that.
This is a test to test Preferences Component:
import * as React from "react";
import { shallow } from "enzyme";
import Preferences from "../../components/Preferences";
test("Preferences shows email and checkbox", () => {
const wrapper = shallow(<Preferences onSubmit={() => { }} />);
wrapper.find("input").at(0).simulate("change", {
target: {
value: "a#b.c",
}
});
expect(wrapper.find("input").state().value).toEqual("a#b.c");
});
This throws Null exception at line 18. The thing is, the value a#b.c is passed correctly and I verified it by placing log statements.
But, when I try to change the value of input type email, it calls a onChange method which tries to access (and change) the value of another input field.
I don't know how to change the value of 2nd input type which is a checkbox. How I can I make it work? Any help is appreciated.
This is because the shallow(...) rendering method provides a limited set of interaction patterns and document.getElementById(...) is not one of them. You should be able to get what you want using the following:
const wrapper = mount(<Preferences />, { attachTo: document.body });
(Docs for the above code. You can swap out document.body for the relevant equivalent if you're using something like JSDOM or similar).
That said... using document.getElementById at all is a huge red-flag in React development. Because React lets you interact with a virtual DOM and handles the application of that to the real DOM, fiddling with the real one yourself is a great way to end up with all sorts of bugs. A much better option would be to use refs to access the checkbox in the "React way", or just make checkboxEnabled: boolean part of your state and update it inside your onPrimaryEmailChange() method.

Resources