I have a class component which renders an input and a tooltip and controls state. The <SelectInputType> components job is to take a single prop of 'type' and render either a text input, a textarea, a select input or a checkbox group component. There are a load of props that need to be passed through this SelectInputType component, some of which are relevant to all 4 of the input components (placeholderText and required for example) and some of which are specific to a particular input (options for example is only relevant to checkboxes and select inputs).
render() {
const inputProps = {};
inputProps.placeholderText = this.props.placeholderText;
inputProps.required = this.props.required;
inputProps.class = this.props.class;
inputProps.value = this.props.value;
inputProps.options = this.props.options;
inputProps.updateText = this.handleInput;
inputProps.handleFocus = this.focusIn;
inputProps.handleBlur = this.focusOut;
inputProps.handleCheckboxChange = e => this.addCheckboxToList(e);
inputProps.closeCheckboxSelector = this.closeCheckboxSelector;
inputProps.readableSelectedCheckboxes = this.state.readableSelectedCheckboxes;
const inputClass = classNames('input-with-tooltip', {
focus: this.state.focus,
'step-complete': this.state.completed,
});
return (
<div className={inputClass}>
<InputTooltip
tooltipText={this.props.tooltipText}
completed={this.state.completed}
/>
<div className="drill-creation-input">
<SelectInputType type={this.props.type} {...inputProps} />
</div>
</div>
);
}
My SelectInputType component looks like this...
const SelectInputType = (props) => {
let component;
if (props.type === 'title') {
component = <TextInput />;
} else if (props.type === 'text') {
component = <TextareaInput />;
} else if (props.type === 'checkbox') {
component = <CheckboxInput />;
} else if (props.type === 'select') {
component = <SelectInput />;
}
return (
<div>
{component}
// Need to pass props through to this?
</div>
);
};
I am using the JSX spread attribute to pass the props down to the SelectInputType component, but I don't know how to then pass these on to the 4 input components (and if I do pass them on will I have errors with certain props not being valid on particular components?)
You could alternatively save the component type in the variable, not the component itself:
const SelectInputType = (props) => {
let ComponentType;
if (props.type === 'title') {
ComponentType = TextInput;
} else if (props.type === 'text') {
ComponentType = TextareaInput;
} else if (props.type === 'checkbox') {
ComponentType = CheckboxInput;
} else if (props.type === 'select') {
ComponentType = SelectInput;
}
return (
<div>
<ComponentType {..props} />
</div>
);
};
You might get errors. If so you could create utility functions to extract the props you need per input type:
extractTextInputProps(props) {
const { value, className, required } = props
return { value, className, required }
}
extractSelectInputProps(props) {
const { value, handleBlur, updateText } = props
return { value, handleBlur, updateText }
}
You could probably extract a more generic function out of this so you don't have to repeat the property name twice.
Then use them when creating your components with the spread operator:
let component;
if (props.type === 'title') {
component = <TextInput { ...extractTextInputProps(props) } />;
} else if (props.type === 'text') {
component = <TextareaInput />;
} else if (props.type === 'checkbox') {
component = <CheckboxInput />;
} else if (props.type === 'select') {
component = <SelectInput { ...extractSelectInputProps(props) } />;
}
Related
I've set condition for when a user enters numbers into a text box depending on what the numbers entered start with on screen it should display the name of the card.I seem to be changing the variable name fine but am having issues actually displaying the variable name. I'm wondering whether instead of having the if statements in the CardCheck.js they should maybe be in App.js or CardTypeDisplay.js. Apologies first week on React.
App.js
import "./App.css";
import React from "react";
import CardCheck from "./CardCheck";
import CardTypeDisplay from "./CardTypeDisplay";
class App extends React.Component {
state = {
cardNumber: "",
cardType: "",
};
handleChange = (event) => {
this.setState({ cardNumber: event.target.value });
};
handleClick = () => {
let { cardNumber } = this.state;
let { cardType } = this.state;
this.setState({
cardType: cardType,
cardNumber: "",
});
};
render() {
let { cardNumber } = this.state;
let { cardType } = this.state;
return (
<div className="App">
<h1>Taken Yo Money</h1>
<CardCheck
cardNumber={cardNumber}
handleChange={this.handleChange}
handleClick={this.handleClick}
/>
<CardTypeDisplay cardType={cardType} />
</div>
);
}
}
export default App;
CardCheck.js
function CardCheck(props) {
let { cardNumber, handleChange, handleClick } = props;
let cardType = props;
if (cardNumber.match(/^34/) || cardNumber.match(/^38/)) {
cardType = "AMEX";
console.log(cardType);
} else if (cardNumber.match(/^6011/)) {
cardType = "Discover";
console.log(cardType);
} else if (
cardNumber.match(/^51/) ||
cardNumber.match(/^52/) ||
cardNumber.match(/^53/) ||
cardNumber.match(/^54/) ||
cardNumber.match(/^55/)
) {
cardType = "MasterCard";
console.log(cardType);
} else if (cardNumber.match(/^4/)) {
cardType = "Visa";
console.log(cardType);
}
return (
<div className="TweetInput">
<div className="bar-wrapper"></div>
<textarea onChange={handleChange} value={cardNumber}></textarea>
<footer>
<button onClick={handleClick} value={cardType}>
Enter Card Details
</button>
</footer>
</div>
);
}
export default CardCheck;
CardTypeDisplay.js
function CardTypeDisplay(props) {
let { cardType } = props;
return (
<div className="cardType">
<p> {cardType} </p>
</div>
);
}
export default CardTypeDisplay;
You could use the hook useEffect in CardCheck to perform an action each time that cardNumber changes
Like this
useEffect(() => {
if (cardNumber.match(/^34/) || cardNumber.match(/^38/)) {
cardType = "AMEX";
console.log(cardType);
} else if (cardNumber.match(/^6011/)) {
cardType = "Discover";
console.log(cardType);
} else if (
cardNumber.match(/^51/) ||
cardNumber.match(/^52/) ||
cardNumber.match(/^53/) ||
cardNumber.match(/^54/) ||
cardNumber.match(/^55/)
) {
cardType = "MasterCard";
console.log(cardType);
} else if (cardNumber.match(/^4/)) {
cardType = "Visa";
console.log(cardType);
}
},{cardNumber})
Also you are defining cartType in CardCheck and CardCheck doesn't know that variable. What you could do is pass this value as parameter in handleClick
<button onClick={(e) => handleClick(e,cartType)} >
Enter Card Details
</button>
And in receive it and change the state
handleChange = (event, type) => {
this.setState({ cardNumber: event.target.value, cartType:type });
};
I am trying to configure some props to children.
In this dummy example I am testing the function in order to specifically target any child, nested or not, with the following prop : swipeMe.
It works very well if inside my div on the render function if it contains just a single child, like this:
<SwapableItems>
<div>
{/*the p element will get the red color as defined on childrenHandler*/}
<p swipeMe>Add Props</p>
</div>
</SwapableItems>
Yet, if I add more children into my div, somehow I guess that my ternary operation on childrenHandler is not working well and I do not know why...
If it has children, clone this element and call childrenHandler passing its children.
If it has my desired prop, clone de element and do something with it.
If none of the above, just clone the element.
return childHasChildren
? React.cloneElement(child, {}, childHasChildren)
: child.props.swipeMe
? React.cloneElement(child, { ...swipeMeProps })
: React.cloneElement(child, {});
Below is the full script.
You can also check it out on Codesandbox
import React from "react";
import ReactDOM from "react-dom";
function App() {
return (
<SwapableItems>
<div>
<p swipeMe>Add Props</p>
<div>Don't Add Props</div>
</div>
</SwapableItems>
);
}
function SwapableItems({ children }) {
const content = childrenHandler(children, { style: { color: "red" } });
return content;
}
const childrenHandler = (children, swipeMeProps) => {
const childEls = React.Children.toArray(children).map((child) => {
const childHasChildren =
child.props.children && React.isValidElement(child.props.children)
? childrenHandler(child.props.children, swipeMeProps)
: undefined;
return childHasChildren
? React.cloneElement(child, {}, childHasChildren)
: child.props.swipeMe
? React.cloneElement(child, { ...swipeMeProps })
: React.cloneElement(child, {});
});
return childEls;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I tested this work.
I wrapped nested children by SwapableItems if has children.
function SwapableItems({ children }) {
const props = { style: { color: "red" } };
return Children.map(children, (child) => {
let nestedChild = child.props.children;
const hasNestedChild = nestedChild && typeof nestedChild !== "string"
if (hasNestedChild) {
nestedChild = <SwapableItems>{nestedChild}</SwapableItems>;
}
return child.props?.swipeMe || hasNestedChild
? cloneElement(child, child.props?.swipeMe ? props : {}, [nestedChild])
: child;
});
}
https://codesandbox.io/s/kind-snow-8zm6u?file=/src/App.js:345-751
child.props.children is an array, so React.isValidElement(child.props.children) is always falsy, to fix it try using React.cloneElement on it:
React.isValidElement(React.cloneElement(child.props.children)); // true
A style reset example:
const styleA = {
color: "blue"
};
const InlineReset = ({ children }) => {
return React.Children.map(children, child => {
console.log(child.props);
return React.cloneElement(child.props.children, { style: null });
});
};
export default function App() {
return (
<InlineReset>
<div>
<h1 style={styleA}>Hello </h1>
</div>
</InlineReset>
);
}
This is the solution:
I have initialized a variable which will hold the recursion outside of the logic and undefined.
let childHasChildren;
I have encapsulated the code inside an if statement with some adaptions:
"in case child has children, either an array or object, and if within the passed children there is/are valid React elements"
const deeperChildren = child.props.children;
const arrayOfChildren = Array.isArray(deeperChildren);
const singleChildren =
typeof deeperChildren === "object" && deeperChildren !== null;
if (
(arrayOfChildren &&
deeperChildren.some((c) => React.isValidElement(c))) ||
(singleChildren && React.isValidElement(deeperChildren))
) {
In order to pass the recursion without errors, in case the code within the if statement is called, I have cloned a filter/single object of which children would be React valid elements, then I pass just these/this into the recursion:
const validChildren = arrayOfChildren
? deeperChildren.filter((c) => React.isValidElement(c))
: deeperChildren;
Here is the result
Now it accepts, one item with children, or an Array. This can be configured in order to dynamically pass props, with default ones and other props that could be passed from outside of the component, although the latter is not my case. This solution was desired in order to achieve more complex stuff without using those solutions, such as render props, props contracts, HOCs, etc.
const childrenHandler = (children, swipeMeProps) => {
const childEls = React.Children.toArray(children).map((child) => {
let childHasChildren;
const deeperChildren = child.props.children;
const arrayOfChildren = Array.isArray(deeperChildren);
const singleChildren =
typeof deeperChildren === "object" && deeperChildren !== null;
if (
(arrayOfChildren &&
deeperChildren.some((c) => React.isValidElement(c))) ||
(singleChildren && React.isValidElement(deeperChildren))
) {
const validChildren = arrayOfChildren
? deeperChildren.filter((c) => React.isValidElement(c))
: deeperChildren;
childHasChildren = childrenHandler(validChildren, swipeMeProps);
}
return childHasChildren
? React.cloneElement(child, {}, childHasChildren)
: child.props.swipeMe
? React.cloneElement(child, { ...swipeMeProps })
: React.cloneElement(child, {});
});
return childEls;
};
In my component did update, I console.log this.state.checkedTeams and this.state.checked. These values are determined through the map in componentDidUpdate. They read as they should read.
However, when I console log the values in side the render, I get empty arrays.
When I try to set the state in componentDidUpdate, I exceed the maximum depth and throw an error.
The goal of this to be able to adjust this.state.checked based off of the state of a foreign component.
import React, { Component } from 'react'
import { Text, View } from 'react-native'
import { connect } from 'react-redux'
import { loadTeams, loadLeagues } from '../actions'
import Check from './CheckBox'
class TeamSelect extends Component {
constructor(props) {
super(props)
this.state = {
checked: [],
checkedTeams: [],
setOnce: 0
}
}
componentDidUpdate() {
this.state.checked.length = 0
this.props.team.map(
(v, i) => {
if(this.props.checkedLeagues.includes(v.league.acronym)){
this.state.checked.push(true)
this.state.checkedTeams.push(v.team_name)
} else{
this.state.checked.push(false)
}
}
)
console.log('checkedTeams', this.state.checkedTeams)
console.log('checked', this.state.checked)
}
// componentDidUpdate(){
// if(this.state.checked.length === 0) {
// this.props.team.map(
// (v, i) => {
// if(this.props.checkedLeagues.includes(v.league.acronym)){
// this.state.checked.push(true)
// this.state.checkedTeams.push(v.team_name)
// } else{
// this.state.checked.push(false)
// }
// }
// )
// }
// console.log('checked league', this.props.checkedLeagues)
// }
changeCheck = (index, name) => {
firstString = []
if(!this.state.checkedTeams.includes(name)) {
firstString.push(name)
} else {
firstString.filter(v => { return v !== name})
}
this.state.checkedTeams.map(
(v, i) => {
if(v !== name) {
firstString.push(v)
}
}
)
if(name === this.state.checkedTeams[0] && firstString.length === 0) {
this.setState({ checkMessage: `Don't you want something to look at?` })
} else {
if(!this.state.checkedTeams.includes(name)){
this.state.checkedTeams.push(name)
} else {
this.setState({checkedTeams: this.state.checkedTeams.filter(v => { return v !== name})})
}
this.state.checked[index] = !this.state.checked[index]
this.setState({ checked: this.state.checked })
// queryString = []
// firstString.map(
// (v, i) => {
// if (queryString.length < 1) {
// queryString.push(`?league=${v}`)
// } else if (queryString.length >= 1 ) {
// queryString.push(`&league=${v}`)
// }
// }
// )
// axios.get(`http://localhost:4000/reports${queryString.join('')}`)
// .then(response => {
// this.props.loadCards(response.data)
// })
}
// console.log('first string', firstString)
// console.log('in function', this.state.checkedTeams)
}
render() {console.log('in render - checkedTeams', this.state.checkedTeams)
console.log('in render - checked', this.state.checked)
return(
<View>
{
this.props.team === null ?'' : this.props.team.map(
(v, i) => {
return(
<View key={i}>
<Check
checked={this.state.checked[i]}
index={i}
value={v.team_name}
changeCheck={this.changeCheck}
/>
{ v.team_name === undefined ? null :
<Text>{v.team_name}</Text>}
</View>
)
}
)
}
</View>
)
}
}
function mapStateToProps(state) {
return {
team: state.team.team,
checkedLeagues: state.league.checkedLeagues
}
}
export default connect(mapStateToProps)(TeamSelect)
You are mutating state directly in several places (very bad). You have to use this.setState(new_state) or react looses track of what is changing. In order to change the state of one component based on the value of another, you need to have the dependent component as a child of the control component so it can receive the value as a prop or you need to pass a state changing function to the controlling element that is bound to the dependent element.
Read the official react docs on how state works and lifting state for more info.
https://reactjs.org/docs/faq-state.html
https://reactjs.org/docs/lifting-state-up.html
i have an array of projects, which can be filtered by three select dropdowns. once the user selects an option the option will be passed in the components state through the store. i just dont know how to handle the cases where one of the filter options is empty and an empty filter option is passed. also i would like to have the option to show all and ignore the filter. I just get the or logic to work like to be able to only filter for years and if i filter places or types the years already set getting ignored but i would also like to filter all three.
export class Projects extends React.Component {
constructor(props) {
super(props);
autoBind(this);
this.state = {
initialItems: props.data.projects.edges,
items: null,
filterPlace: '',
filterYear: '',
filterType: '',
};
this.placesOptions = new Set();
this.yearsOptions = new Set();
this.typesOptions = new Set();
}
componentWillMount(){
const { initialItems } = this.state
this.setState({items: initialItems})
initialItems.map((item) => {
this.placesOptions.add(item.node.categories_names[0].name)
this.yearsOptions.add(item.node.categories_names[1].name)
this.typesOptions.add(item.node.categories_names[2].name)
})
}
// TODO: FIX BUG ON RELOAD ALL EMPTY
componentDidUpdate(prevProps) {
if (prevProps !== this.props) {
this.filterProjects()
}
}
filterProjects(){
const { filterPlace, filterYear, filterType } = this.props;
let updatedList = this.state.initialItems;
updatedList = updatedList.filter((item) => {
const itemFilterCategory = item.node.categories_names
const placeQueryString = itemFilterCategory[0].name.toString().toLowerCase()
const yearQueryString = itemFilterCategory[1].name.toString().toLowerCase()
const typeQueryString = itemFilterCategory[2].name.toString().toLowerCase()
return (
filterPlace !== "" && placeQueryString.search( filterPlace.toLowerCase()) !== -1 ||
filterYear !== "" && yearQueryString.search( filterYear.toLowerCase()) !== -1 ||
filterType !== "" && typeQueryString.search( filterType.toLowerCase()) !== -1
)
});
this.setState({items: updatedList});
}
render() {
const { location, data } = this.props;
const { items } = this.state;
const { page } = data;
return (
<MainLayout location={location}>
<TopNavigation />
<Header
siteBrand={config.siteBrand}
siteSubtitle={page.title}
isProjectArchive
/>
<ConnectedProjectFilter
changeHandlerSearch={(e) => this.searchProjects(e)}
placesOptions={this.placesOptions}
yearsOptions={this.yearsOptions}
typesOptions={this.typesOptions}
/>
<ProjectArchiveListing projects={items} />
</MainLayout>
)
}
}
const mapStateToProps = state => ({
filterPlace: state.filterPlace,
filterYear: state.filterYear,
filterType: state.filterType,
});
const mapDispatchToProps = dispatch => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Projects)
If you want any empty input to match (pass through) every item then I believe this is the condition you want:
(filterPlace === "" ||Â placeQueryString.search( filterPlace.toLowerCase()) !== -1) &&
(filterYear === "" || yearQueryString.search( filterYear.toLowerCase()) !== -1) &&
(filterType === "" || typeQueryString.search( filterType.toLowerCase()) !== -1)
To describe it in words, you want each passing item to be valid with respect to every filter. To validate with a filter the filter must allow all (be empty) or the item must match the filter.
How do I render an indeterminate checkbox via JSX?
Here's what I've tried:
function ICB({what}) {
return <input type="checkbox"
checked={what === "checked"}
indeterminate={what === "indeterminate"} />;
}
However, indeterminate is not an attribute on the HTMLElement, but a property. How do I set properties from React / JSX?
Solution:
As most of the answers below use findDOMNode or string refs, both of which are no longer considered good practice in React, I've written a more modern implementation:
function ICB() {
const [state, setState] = React.useState(0);
const indetSetter = React.useCallback(el => {
if (el && state === 2) {
el.indeterminate = true;
}
}, [state]);
const advance = () => setState(prev => (prev + 1) % 3);
return <input type="checkbox"
checked={state === 1}
ref={indetSetter}
onClick={advance} />;
}
ReactDOM.render(<ICB />, document.getElementById("out"));
<div id="out"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
You can also use the ref function directly:
ReactDOM.render(
<label>
<input
type="checkbox"
ref={input => {
if (input) {
input.indeterminate = true;
}
}}
/>
{' '}
Un test
</label>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I would probably create a composite component that encapsulates the necessary hooks to set or unset the checkbox's indeterminate property. It looks like you're using ES2015 syntax, so I'll use some of those features here.
class IndeterminateCheckbox extends React.Component {
componentDidMount() {
if (this.props.indeterminate === true) {
this._setIndeterminate(true);
}
}
componentDidUpdate(previousProps) {
if (previousProps.indeterminate !== this.props.indeterminate) {
this._setIndeterminate(this.props.indeterminate);
}
}
_setIndeterminate(indeterminate) {
const node = React.findDOMNode(this);
node.indeterminate = indeterminate;
}
render() {
const { indeterminate, type, ...props } = this.props;
return <input type="checkbox" {...props} />;
}
}
// elsewhere
render() {
return <IndeterminateCheckbox
checked={this.props.state === "checked"}
indeterminate={this.props.state === "indeterminate"} />
}
Working example: https://jsbin.com/hudemu/edit?js,output
You can use the componentDidMount step (which is invoked after the initial rendering) to set that property:
componentDidMount() {
React.findDOMNode(this).indeterminate = this.props.state === "indeterminate";
}
If you want that property to be updated with subsequent renders, do the same thing in componentDidUpdate also.
I'd suggest creating a simple component (code ported from coffeescript so mind you, might have some simple typos):
const React = require('react');
module.exports = class IndeterminateCheckbox extends React.Component {
componentDidMount() {
this.refs.box.indeterminate = this.props.indeterminate;
}
componentDidUpdate(prevProps, prevState) {
if(prevProps.indeterminate !== this.props.indeterminate) {
this.refs.box.indeterminate = this.props.indeterminate;
}
}
render() {
return <input {...this.props} ref="box" type="checkbox"/>;
}
}
Now you have a simple component that behaves exactly like a checkbox, that supports the indeterminate prop. Note there's plenty of room for improvements here, namely setting propTypes and proper defaults for some props, and of course implementing componentShouldUpdate to only do something when needed.
An alternative would be to use a ref attribute with a callback to set the property on the DOM node. For example:
render: function() {
return (
<input
type="checkbox"
ref={function(input) {
if (input != null) {
React.findDOMNode(input).indeterminate = this.props.indeterminate;
}}
{...this.props} />
)
}
Dont use React.findDOMNode(this).
It is risky.
export class SelectAll extends Component {
constructor(props) {
super(props);
this.state = {
checked: false
};
this.myRef = React.createRef();
this.onChange = this.onChange.bind(this);
this.update = this.update.bind(this);
}
onChange(e) {
const checked = e.target.checked;
this.setState({
checked: checked
});
this.selectAllNode.indeterminate = false;
}
update(state: {
checked: Boolean,
indeterminate: Boolean
}) {
this.setState({
checked: state.checked
});
this.myRef.current.indeterminate = state.indeterminate;
}
render() {
return ( <
input type = "checkbox"
name = "selectAll"
checked = {
this.state.checked
}
onChange = {
this.onChange
}
ref = {
this.myRef
}
/>
);
}
}
React v15 implementation:
import React from 'react';
export default class Checkbox extends React.Component {
componentDidMount() {
this.el.indeterminate = this.props.indeterminate;
}
componentDidUpdate(prevProps, prevState) {
if(prevProps.indeterminate !== this.props.indeterminate) {
this.el.indeterminate = this.props.indeterminate;
}
}
render() {
const {indeterminate, ...attrs} = this.props;
return <input ref={el => {this.el = el}} type="checkbox" {...attrs}/>;
}
}
Taken from my tutorial which shows how this works with the recent React features. I hope this helps someone who stumbles upon this older question:
const App = () => {
const [checked, setChecked] = React.useState(CHECKBOX_STATES.Empty);
const handleChange = () => {
let updatedChecked;
if (checked === CHECKBOX_STATES.Checked) {
updatedChecked = CHECKBOX_STATES.Empty;
} else if (checked === CHECKBOX_STATES.Empty) {
updatedChecked = CHECKBOX_STATES.Indeterminate;
} else if (checked === CHECKBOX_STATES.Indeterminate) {
updatedChecked = CHECKBOX_STATES.Checked;
}
setChecked(updatedChecked);
};
return (
<div>
<Checkbox
label="Value"
value={checked}
onChange={handleChange}
/>
<p>Is checked? {checked}</p>
</div>
);
};
const Checkbox = ({ label, value, onChange }) => {
const checkboxRef = React.useRef();
React.useEffect(() => {
if (value === CHECKBOX_STATES.Checked) {
checkboxRef.current.checked = true;
checkboxRef.current.indeterminate = false;
} else if (value === CHECKBOX_STATES.Empty) {
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = false;
} else if (value === CHECKBOX_STATES.Indeterminate) {
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = true;
}
}, [value]);
return (
<label>
<input ref={checkboxRef} type="checkbox" onChange={onChange} />
{label}
</label>
);
};