What I want to do is to change the border on the input to red if the input value doesn't match any movie in the API call.
The user types in the input field and the call to the API shows the matching result.
If we don't have any result I would like the border on the input to be red.
But I can't see how I should make that happen.
The component Input is at the end of the code snippet.
CSS
.input-style {
padding: 7px;
border-radius: 5px;
border: 1px solid #cccccc;
font-family: Courier New, Courier, monospace;
transition: background-color 0.3s ease-in-out;
outline: none;
}
.input-style:focus {
border: 1px solid turquoise;
}
APP.js
class App extends Component {
constructor() {
super();
this.state = {
value: '',
items: [],
isLoading: false,
searchResult: null,
error: false,
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
// To handle search
handleChange(e) {
this.setState({ value: e.target.value });
}
handleSubmit(e) {
let searchResult = [];
for (var i = 0; i < this.state.items.length; i++) {
if (
this.state.items[i].name
.toLowerCase()
.indexOf(this.state.value.toLowerCase()) !== -1
) {
searchResult.push(this.state.items[i]);
} else {
console.log('No matches on your search, try again');
}
}
e.preventDefault();
// If we have something in the object searchResult
if (searchResult.length > 0) {
this.setState({
error: false,
value: '',
searchResult: searchResult,
});
} else {
this.setState({
error: true,
value: '',
searchResult: [],
});
}
}
// call to the API
componentDidMount() {
this.setState({ isLoading: !this.state.isLoading });
fetch('https://api.tvmaze.com/shows')
.then(response => response.json())
.then(data => {
this.setState({
items: data,
error: false,
});
this.setState({ isLoading: !this.state.isLoading });
})
.catch(console.error);
}
render() {
return (
<div className="App">
<Header />
<Loader isLoading={this.state.isLoading} />
<Input
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
value={this.state.value}
/>
{this.state.error ? (
<p className="errorMsg">No match on the search, try again!</p>
) : null}
<Search search={this.state.searchResult} />
</div>
);
}
}
export default App;
Input.js
function Input(props) {
return (
<div>
<form onSubmit={props.handleSubmit}>
<input
type="text"
className="input-style"
placeholder="Sök efter film.."
value={props.value}
onChange={props.handleChange}
/>
<button id="bold" className="button-style" type="submit">
<i className="fa fa-search" />
</button>
</form>
</div>
);
}
export default Input;
You can pass the error into the Input component from the App
<Input
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
value={this.state.value}
error={this.state.error)
/>
and in your Input component:
<input
type="text"
className={props.error ? 'error-input-style' : 'input-style'}
placeholder="Sök efter film.."
value={props.value}
onChange={props.handleChange}
/>
alternative you can also set an inline styling for the error condition:
<input
type="text"
className="input-style"
placeholder="Sök efter film.."
value={props.value}
onChange={props.handleChange}
style={{ border: props.error ? '1px solid red' : '' }}
/>
You can easily do this. Have a flag, say resultFound, in the state of App.js, with an initial value of false. Then, in the function where you make the API call, update this resultFound depending on whether any result was obtained.
And, in the render(), before returning, assign inputClassName dynamically based on the this.state.resultFound, like so,
let inputClassName = '';
if (this.state.resultFound === false) {
inputClassName = 'input-style-error'; // new CSS class for errors
} else {
inputClassName = 'input-style';
}
Then, you can pass the inputClassName as a prop to Input and use it as <input>'s className, like so,
// in your App.js render() method's return
// ... existing components
<Input customStyle={inputClassName} ... />
// ...
<!-- in your Input.js render() method -->
<input type="text" className={props.customStyle} ... />
Whenever the API call happens, your state will change causing a re-render of the DOM (render() is called). During each call, we dynamically set the inputClassName based on the state's resultFound. And, accordingly, the right className will be applied to the <input>.
I will give bad names for classes and variables, just to make it super clear. You should use more generic ones.
The trick here is to give your Input a dynamic class via props, and if that expression turns true and the class is appended to the element, you can style it with css.
__CSS__
.input-style {
padding: 7px;
border-radius: 5px;
border: 1px solid #cccccc;
font-family: Courier New, Courier, monospace;
transition: background-color 0.3s ease-in-out;
outline: none;
}
.input-style:focus {
border: 1px solid turquoise;
}
.input-style.red-border {
border: 1px solid red;
}
__APP.js__
class App extends Component {
constructor() {
super();
this.state = {
value: '',
items: [],
isLoading: false,
searchResult: null,
error: false,
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
// To handle search
handleChange(e) {
this.setState({ value: e.target.value });
}
handleSubmit(e) {
let searchResult = [];
for (var i = 0; i < this.state.items.length; i++) {
if (
this.state.items[i].name
.toLowerCase()
.indexOf(this.state.value.toLowerCase()) !== -1
) {
searchResult.push(this.state.items[i]);
} else {
console.log('No matches on your search, try again');
}
}
e.preventDefault();
// If we have something in the object searchResult
if (searchResult.length > 0) {
this.setState({
error: false,
value: '',
searchResult: searchResult,
});
} else {
this.setState({
error: true,
value: '',
searchResult: [],
});
}
}
// call to the API
componentDidMount() {
this.setState({ isLoading: !this.state.isLoading });
fetch('https://api.tvmaze.com/shows')
.then(response => response.json())
.then(data => {
this.setState({
items: data,
error: false,
});
this.setState({ isLoading: !this.state.isLoading });
})
.catch(console.error);
}
render() {
return (
<div className="App">
<Header />
<Loader isLoading={this.state.isLoading} />
<Input
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
value={this.state.value}
showRedBorder={this.state.error === true} // or what ever your logic
/>
{this.state.error ? (
<p className="errorMsg">No match on the search, try again!</p>
) : null}
<Search search={this.state.searchResult} />
</div>
);
}
}
export default App;
__Input.js__
function Input(props) {
return (
<div>
<form onSubmit={props.handleSubmit}>
<input
type="text"
className={`input-style${props.showRedBorder ? ' red-border' : ''}`}
placeholder="Sök efter film.."
value={props.value}
onChange={props.handleChange}
/>
<button id="bold" className="button-style" type="submit">
<i className="fa fa-search" />
</button>
</form>
</div>
);
}
export default Input;
Related
I'm pretty much ready to rip my hair out. So my final project in my Javascript class is an experimental thing with learning React.js, where you do a basic todo list. I got all that done and working, and I can have it add things properly. But my final hurdle is making it so that onclicking the printed paragraph from the button will cause them to give the printed paragraphs the strikethrough property, which can be undone by clicking on it again.
I've looked up everywhere, I've tried other examples from here, and nothing I can think of gets the strikethrough to take place. I tried a basic Javascript function that would do what I wanted if this was an HTML/non-react file, but it breaks the react page when I try to plop it in. So I spent a stupidly long time on a tutorial trying to figure out what to do, and I maybe figured out the step in the right direction? But I still can't get anything to happen and I don't know how to establish an onclick to this.
import React from 'react';
import './App.css';
class App extends React.Component {
setCurrentToDoItem = (toDoItem) => {
console.log("toDoItem", toDoItem);
this.setState({
currentToDoItem: toDoItem
});
};
saveToDoListItem = (toDoItem) => {
this.setState({
toDoList: [...this.state.toDoList,
toDoItem]
});
};
constructor(props) {
super(props);
this.state = {
currentToDoItem: null,
toDoList: [],
strikeThrough: []
};
}
render() {
return (
<div>
<h1>To Do List</h1>
<label>To Do Item: </label>
<input
onChange={(event) => this.setCurrentToDoItem(event.target.value)}>
</input>
<button onClick={() => this.saveToDoListItem(this.state.currentToDoItem)}>
<p>Add Item</p>
</button>
<p>{this.state.currentToDoItem}</p>
<div>
<p>To Do Items</p>
{
this.state.toDoList.map((item, index) => <p key={index}>{item} </p>)
}
</div>
</div>
);
}
}
export default App;
This is my App.js code. As you can see, everything else should work fine, but I have no clue how to add a strikethrough effect to what would result from the this.state.toDoList.map((item, index) => <p key={index}>{item} </p>) bit like I would with a function in normal javascript. How do I make the printed lines strikethrough via onclick, and then how do I undo that by clicking on it again? (I assume with a second onclick) I really just need to know how to get a working strikethrough with this, as everything else is pretty much working.
One of the most comfortable ways to do that is as advised in comments. A really quick way to implement this is to toggle class list. In the code bellow, I added a function crossLine which toggles class name "crossed-line" and adds event listener on mapped to-dos (in render function). Then in your App.css add a line
.crossed-line {
text-decoration: line-through;
}
Here's your edited component code.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
currentToDoItem: null,
toDoList: [],
strikeThrough: []
};
}
setCurrentToDoItem = toDoItem => {
this.setState({
currentToDoItem: toDoItem
});
};
saveToDoListItem = toDoItem => {
this.setState({
toDoList: [...this.state.toDoList, toDoItem]
});
};
crossLine = event => {
const element = event.target;
element.classList.toggle("crossed-line");
};
render() {
return (
<div>
<h1>To Do List</h1>
<label>To Do Item: </label>
<input
onChange={event =>
this.setCurrentToDoItem(event.target.value)
}
/>
<button
onClick={() =>
this.saveToDoListItem(this.state.currentToDoItem)
}
>
<p>Add Item</p>
</button>
<p>{this.state.currentToDoItem}</p>
<div>
<p>To Do Items</p>
{this.state.toDoList.map((item, index) => {
return (
<p onClick={this.crossLine} key={index}>
{item}{" "}
</p>
);
})}
</div>
</div>
);
}
}
As commented, you will have to keep a handle click and add class to add strikethrough using CSS.
For this I have updated your JSX to:
<p onClick={ () => this.handleClick(index) } className={ item.isComplete ? 'completed' : '' } key={index}>{item.value} </p>
and the signature of toDoItem from string to an object:
{
value: string;
isComplete: boolean
}
and based on this flag, I'm adding class.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
currentToDoItem: null,
toDoList: [],
strikeThrough: []
};
this.setCurrentToDoItem = this.setCurrentToDoItem.bind(this);
this.saveToDoListItem = this.saveToDoListItem.bind(this);
this.handleClick = this.handleClick.bind(this);
}
setCurrentToDoItem(toDoItem) {
this.setState({
currentToDoItem: toDoItem
});
}
saveToDoListItem(toDoItem) {
this.setState({
toDoList: [...this.state.toDoList, {
value: toDoItem,
isComplete: false
}]
});
}
handleClick(index) {
const {
toDoList
} = this.state;
toDoList[index].isComplete = !toDoList[index].isComplete;
this.setState({
toDoList
});
}
render() {
return (
<div>
<h1>To Do List</h1>
<label>To Do Item: </label>
<input
onChange={(event) => this.setCurrentToDoItem(event.target.value)}>
</input>
<button onClick={() => this.saveToDoListItem(this.state.currentToDoItem)}>
<p>Add Item</p>
</button>
<p>{this.state.currentToDoItem}</p>
<div>
<p>To Do Items</p>
{
this.state.toDoList.map((item, index) =>
<p onClick={ () => this.handleClick(index) } className={ item.isComplete ? 'completed' : '' } key={index}>{item.value} </p>)
}
</div>
</div>
);
}
}
ReactDOM.render( < App / > , document.querySelector("#app"))
body {
background: #20262E;
padding: 20px;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
.completed {
text-decoration: line-through;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Check out this solution https://codesandbox.io/s/crazy-kare-go2vf
I have modified your code to achieve the required functionality.
This code does exactly what you want.
Update : Created a TODO fiddler code using React Hooks for modern code approach.
const initialState = {
items: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn React", done: false },
{ text: "Play around in JSFiddle", done: true },
{ text: "Build something awesome", done: true }
]
};
function appReducer(state, action) {
switch (action.type) {
case 'ITEM_STATUS_CHANGE':{
let affected = state.items.slice();
affected[action.index].done = !affected[action.index].done;
return Object.assign({}, state, { items: affected });
}
case 'ADD_ITEM_TO_LIST':{
let affected = state.items.slice();
affected.push({ text: action.data, done : false})
return Object.assign({}, state, { items: affected });
}
default:
throw new Error();
}
}
function TodoApp(props){
const [state, dispatch] = React.useReducer(appReducer, initialState);
return (
<div>
<h2>Todos:
<input type="text" id="todoTextItem"/>
<button
onClick={()=>{
dispatch({
type: 'ADD_ITEM_TO_LIST',
data: window.todoTextItem.value
})
}}
>Add Item</button>
</h2>
<ol>
{state.items.map((item, index) => (
<li key={index}>
<label>
<input
type="checkbox"
checked={item.done}
onChange={()=>{
dispatch({
type: 'ITEM_STATUS_CHANGE',
index: index,
})
}}
/>
<span className={item.done ? "done" : ""}>{item.text}</span>
</label>
</li>
))}
</ol>
</div>
);
}
ReactDOM.render(<TodoApp />, document.querySelector("#app"))
and in CSS
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
.done {
color: rgba(0, 0, 0, 0.3);
text-decoration: line-through;
}
input {
margin-right: 5px;
}
Explanation:
Basically i am creating a list with done boolean flag which is false by default, which helps to identify if the TODO items added to the list is finished or not using reducers. With that logic class .done is toggled. You can change the code according to your need by segregating TODO list from done list items while setting state
This is a Unit testable code by creating Jest snapshot. Never to manipulate DOM directly, which will defeat the purpose of React's snapshot testing.
Old fiddle code using Class Component.
Use this to compare and learn modern hooks concepts from class based.
I have an array of objects and when the user inputs a zipcode and click I want to loop through the array and if the users zipcode matches a zip code in the array output results if the zipcode dose not match output another result
I have attempted to use map and forEach on the array and each allow me to find the zipcode and provide out put Im running into trouble when the zipcodes do not match
class PricingTool extends Component {
state = {
zipCode: "",
finalZip: "",
routine: "",
rush: "",
sameDay: "",
city: "",
match: false
};
handleKeypress = e => {
const { zipCode } = this.state;
if (e.which === 13 && zipCode.length === 5) {
console.log("enterrerre");
this.zipcodeHandler();
}
};
zipcodeHandler = () => {
const { zipCode } = this.state;
this.setState({
finalZip: zipCode,
});
};
changeHandler = e => {
e.preventDefault();
if (e.target.value.length <= 5 && !isNaN(e.target.value)) {
this.setState({
[e.target.name]: e.target.value
});
}
};
render() {
const { finalZip, zipCode, match } = this.state;
let searchResult;
if(finalZip){
searchResult = zipCodes.map(cur => {
if (finalZip && finalZip == cur.zip) {
return (
<div className="pricing__searchResult">
<h1 className="pricing__searchResult-1">
We do serve in {cur.city}, Indiana
</h1>
<div className="pricing__searchResult-2">Same-Day</div>
<div className="pricing__searchResult-3">{cur.fees.sameDay}</div>
<div className="pricing__searchResult-4">Rush</div>
<div className="pricing__searchResult-5">{cur.fees.rush}</div>
<div className="pricing__searchResult-6">Routine</div>
<div className="pricing__searchResult-7">{cur.fees.routine}</div>
<div className="pricing__searchResult-8">
Please call us or email info#ccprocess.com to order
</div>
</div>
);
}
});
}
I would like it to find the user inputed zip code if it is in the data array and if it is not then render another message
Instead of using a Array map method, which would map each value of the array to something else (in your case, it would only map the zipcode found), you can (and should) use a better method for the job. The find method will find the first item that meet your criteria and return it, in your case, it could be finalZip && (finalZip == cur.zip). If no item is found for the expression given, undefined is returned.
render() {
const { finalZip, zipCode, match } = this.state;
let searchResult;
if(finalZip){
searchResult = zipCodes.find(cur => finalZip && (finalZip == cur.zip));
if(searchResult) {
// do something for when the zip code is found
}
else {
// do something when no zip code is found
}
}
}
Array find method documentation: MDN
Being not sure what all components you used or are currently in your application, I created a sample app which kind of simulates your requirement and possibly delivers the correct output.
So here, I had no idea of where the input will be taken from the user and how your onKeyPress listener would work, so I created another component that renders your data based on the input and check if that zipcode exist or not.
Like shown below, your ZipCode related information will be render by another component ZipTool and your input box is handled by PricingTool
Also, here's the jsfiddle if you want to play around with the code - https://jsfiddle.net/hf9Ly6o7/3/
I hope you find this useful.
class ZipTool extends React.Component {
constructor(props) {
super(props);
}
render() {
let { data } = this.props;
return (
<div>
<h1 className="pricing__searchResult-1">We do serve in { data.city }, Indiana</h1>
<div className="pricing__searchResult-2">Same-Day</div>
<div className="pricing__searchResult-3">{ data.fees.sameDay }</div>
<div className="pricing__searchResult-4">Rush</div>
<div className="pricing__searchResult-5">{ data.fees.rush }</div>
<div className="pricing__searchResult-6">Routine</div>
<div className="pricing__searchResult-7">{ data.fees.routine }</div>
</div>
);
}
}
class PricingTool extends React.Component {
constructor(props) {
super(props)
this.state = {
zipCodes: [
{
'zipcode': '12345',
'city': 'abc',
'fees': {
'sameDay': '43',
'rush': '90',
'routine': '20'
}
},
{
'zipcode': '54321',
'city': 'xyz',
'fees': {
'sameDay': '25',
'rush': '35',
'routine': '10'
}
}
],
zipCode: "",
finalZip: "",
routine: "",
rush: "",
sameDay: "",
city: "",
match: false
}
}
changeHandler(e) {
this.setState({
zipCode: e.target.value,
});
for (let code of this.state.zipCodes) {
if( code.zipcode === e.target.value ){
this.setState({match: true, finalZip: code})
break;
}
else {
this.setState({match: false, finalZip: null})
}
}
};
render() {
return (
<div>
<input type="text" onChange={this.changeHandler.bind(this)} onKeyPress={this.handleKeyPress} name={this.state.zipCode}></input>
<div className="pricing__searchResult">
{ this.state.finalZip ? <ZipTool data={this.state.finalZip} /> : <div>Not Found</div> }
<div className="pricing__searchResult-8">
Please call us or email info#ccprocess.com to order
</div>
</div>
</div>
)
}
}
ReactDOM.render(<PricingTool />, document.querySelector("#app"))
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
.done {
color: rgba(0, 0, 0, 0.3);
text-decoration: line-through;
}
input {
margin-right: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I want to add a red border only if an input is empty. I couldn't find a way to "addClass" in React so I'm using state. Right now the code will add red border to all inputs, even if it has text.
State:
this.state = {
inputBorderError: false,
};
HTML/JSX:
<label>Name</label>
<input className={
this.state.inputBorderError ? 'form-input form-input-fail' : 'form-input'
} />
<label>Email</label>
<input className={
this.state.inputBorderError ? 'form-input form-input-fail' : 'form-input'
} />
<label>Message</label>
<textarea className={
this.state.inputBorderError ? 'form-input form-input-fail' : 'form-input'
} />
CSS:
.form-input-fail {
border: 1px solid red;
}
JS:
let inputFields = document.getElementsByClassName('form-input');
for (var i = 0; i < inputFields.length; i++) {
if (inputFields[i].value === '') {
this.setState({
inputBorderError: true,
});
}
}
I see the error in my code as it's basically setting the state to true anytime it finds an empty input. I think I may be approaching this incorrectly as there's only one state. Is there a solution based on my state approach, or is there another solution?
Right now, you have single state-value that affects all inputs, you should consider having one for each input. Also, your inputs are not controlled, it will be harder to record and track their values for error-handling.
It is good practice to give each input tag a name property. Making it easier to dynamically update their corresponding state-value.
Try something like the following, start typing into each input, then remove your text: https://codesandbox.io/s/nervous-feynman-vfmh5
class App extends React.Component {
state = {
inputs: {
name: "",
email: "",
message: ""
},
errors: {
name: false,
email: false,
message: false
}
};
handleOnChange = event => {
this.setState({
inputs: {
...this.state.inputs,
[event.target.name]: event.target.value
},
errors: {
...this.state.errors,
[event.target.name]: false
}
});
};
handleOnBlur = event => {
const { inputs } = this.state;
if (inputs[event.target.name].length === 0) {
this.setState({
errors: {
...this.state.errors,
[event.target.name]: true
}
});
}
};
handleOnSubmit = event => {
event.preventDefault();
const { inputs, errors } = this.state;
//create new errors object
let newErrorsObj = Object.entries(inputs)
.filter(([key, value]) => {
return value.length === 0;
})
.reduce((obj, [key, value]) => {
if (value.length === 0) {
obj[key] = true;
} else {
obj[key] = false;
}
return obj;
}, {});
if (Object.keys(newErrorsObj).length > 0) {
this.setState({
errors: newErrorsObj
});
}
};
render() {
const { inputs, errors } = this.state;
return (
<div>
<form onSubmit={this.handleOnSubmit}>
<label>Name</label>
<input
className={
errors.name ? "form-input form-input-fail" : "form-input"
}
name="name"
value={inputs.name}
onChange={this.handleOnChange}
onBlur={this.handleOnBlur}
/>
<label>Email</label>
<input
className={
errors.email ? "form-input form-input-fail" : "form-input"
}
name="email"
value={inputs.email}
onChange={this.handleOnChange}
onBlur={this.handleOnBlur}
/>
<label>Message</label>
<textarea
className={
errors.message ? "form-input form-input-fail" : "form-input"
}
name="message"
value={inputs.message}
onChange={this.handleOnChange}
onBlur={this.handleOnBlur}
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
}
You are correct that there is only one state.
What you need to do is store a separate error for each input. one way to do this is with a set or array on state like state = {errors: []} and then check
<label>Name</label>
<input className={
this.state.errors.includes('name') ? 'form-input form-input-fail' : 'form-input'
} />
<label>Email</label>
<input className={
this.state.errors.includes('email') ? 'form-input form-input-fail' : 'form-input'
} />
} />
You should keep track of the input value in the state instead of checking for borderStyling state only.
Base on your code, you could refactor it to something like this:
// keep track of your input changes
this.state = {
inputs: {
email: '',
name: '',
comment: '',
},
errors: {
email: false,
name: false,
comment: false,
}
};
// event handler for input changes
handleChange = ({ target: { name, value } }) => {
const inputChanges = {
...state.inputs,
[name]: value
}
const inputErrors = {
...state.errors,
[name]: value == ""
}
setState({
inputs: inputChanges,
errors: inputErrors,
});
}
HTML/JSX
// the name attribut for your input
<label>Name</label>
<input name="name" onChange={handleChange} className={
this.errors.name == "" ? 'form-input form-input-fail' : 'form-input'
} />
<label>Email</label>
<input name="email" onChange={handleChange} className={
this.errors.email == "" ? 'form-input form-input-fail' : 'form-input'
} />
<label>Message</label>
<textarea name="comment" onChange={handleChange} className={
this.errors.comment == "" ? 'form-input form-input-fail' : 'form-input'
} />
And if you are probably looking at implementing it with CSS and js, you can try this article matching-an-empty-input-box-using-css-and-js
But try and learn to make your component reusable and Dry, because that is the beginning of enjoying react app.
[Revised]
In this app I have a search engine that is supposed to look inside the file tree for desired content. The purpose is, when searching for some content and clicking the Search button, the file tree will filter and show what you are looking for.
For now, I have a console.log() (check onSubmitSearch(e) inside <SearchEngine/>) telling me what content I am asking to search when I click the Search button. The only thing missing is the search actually looking inside the file tree. How do I do that?
Please check the working snippet attached.
Thank you!
/**** TEXT BOX COMPONENT ****/
class TextBox extends React.Component {
constructor(props) {
super(props);
this.state = { content: "Select A Node To See Its Data Structure Here..." };
this.changeContent = this.changeContent.bind(this);
}
changeContent(newContent) {
this.setState({ content: newContent });
}
componentWillReceiveProps(nextProps) {
this.setState({
content: nextProps.content
});
}
render() {
return (
<div className="padd_top">
<div className="content_box">
{this.state.content}
</div>
</div>
);
}
}
/**** SEARCH COMPONENT ****/
class SearchEngine extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
this.onInputChange = this.onInputChange.bind(this);
this.onSubmitSearch = this.onSubmitSearch.bind(this);
}
onInputChange(e) {
const content = e.target.value;
this.setState({value: content});
console.log(content);
}
onSubmitSearch(e) { // CONSOLE LOG IS HERE
e.preventDefault();
console.log('A node was submitted: ' + this.state.value);
}
render() {
return (
<div>
<form onSubmit={this.onSubmitSearch}>
<input
className="form-control"
value={this.state.value}
type="text"
onChange={this.onInputChange}
/>
<p>{this.state.value}</p>
<SearchButton />
</form>
</div>
);
}
}
/**** SEARCH BUTTON ****/
class SearchButton extends React.Component {
render() {
return (
<button
type="submit"
value="submit"
bsStyle="danger"> Search
</button>
);
}
}
/**** FILE TREE COMPONENT ****/
let data = [
{
type: "directory",
name: ".",
contents: [
{
type: "directory",
name: "./bin",
contents: [{ type: "file", name: "./bin/greet" }]
},
{
type: "directory",
name: "./lib",
contents: [{ type: "file", name: "./lib/greeting.rb" }]
},
{
type: "directory",
name: "./spec",
contents: [
{ type: "file", name: "./spec/01_greeting_spec.rb" },
{ type: "file", name: "./spec/02_cli_spec.rb" },
{ type: "file", name: "./spec/spec_helper.rb" }
]
},
{ type: "file", name: "./CONTRIBUTING.md" },
{ type: "file", name: "./Gemfile" },
{ type: "file", name: "./Gemfile.lock" },
{ type: "file", name: "./LICENSE.md" },
{ type: "file", name: "./README.md" }
]
}
];
// Icon file image for 'FileTree'
const FileIcon = () => {
return (
<div className="svg-icon">
<svg
id="icon-file-text2"
className="icon"
viewBox="0 0 32 32"
fill="currentColor"
width="1em"
height="1em"
>
<path d="M28.681 7.159c-0.694-0.947-1.662-2.053-2.724-3.116s-2.169-2.030-3.116-2.724c-1.612-1.182-2.393-1.319-2.841-1.319h-15.5c-1.378 0-2.5 1.121-2.5 2.5v27c0 1.378 1.122 2.5 2.5 2.5h23c1.378 0 2.5-1.122 2.5-2.5v-19.5c0-0.448-0.137-1.23-1.319-2.841zM24.543 5.457c0.959 0.959 1.712 1.825 2.268 2.543h-4.811v-4.811c0.718 0.556 1.584 1.309 2.543 2.268zM28 29.5c0 0.271-0.229 0.5-0.5 0.5h-23c-0.271 0-0.5-0.229-0.5-0.5v-27c0-0.271 0.229-0.5 0.5-0.5 0 0 15.499-0 15.5 0v7c0 0.552 0.448 1 1 1h7v19.5z" />
<path d="M23 26h-14c-0.552 0-1-0.448-1-1s0.448-1 1-1h14c0.552 0 1 0.448 1 1s-0.448 1-1 1z" />
<path d="M23 22h-14c-0.552 0-1-0.448-1-1s0.448-1 1-1h14c0.552 0 1 0.448 1 1s-0.448 1-1 1z" />
<path d="M23 18h-14c-0.552 0-1-0.448-1-1s0.448-1 1-1h14c0.552 0 1 0.448 1 1s-0.448 1-1 1z" />
</svg>
</div>
);
};
// Icon folder image for 'FileTree'
const FolderIcon = () => {
return (
<div className="svg-icon">
<svg
id="icon-folder"
className="icon"
viewBox="0 0 32 32"
fill="currentColor"
height="1em"
width="1em"
>
<path d="M14 4l4 4h14v22h-32v-26z" />
</svg>
</div>
);
};
// Icon arrow image for 'FileTree'
const TriangleDown = () => {
return (
<div className="svg-icon">
<svg
id="svg__icon--triangle-down"
viewBox="0 0 9 4.5"
fill="currentColor"
height="1em"
width="1em"
>
<path d="M0,0,4.5,4.5,9,0Z" />
</svg>
</div>
);
};
// Filters file 'name' and adds '/'
const formatName = name => {
return name.substr(name.lastIndexOf("/") + 1);
};
// Dummy data set
var root = data[0];
// Construction of FileTree
class FileTree extends React.Component {
constructor(props) {
super(props);
this.state = {
activeNode: null
};
this.setActiveNode = this.setActiveNode.bind(this);
}
setActiveNode(name) {
this.setState({ activeNode: name });
this.props.liftStateUp(name);
}
componentWillReceiveProps({ searchTerm }) {
this.setState({ searchTerm });
}
render() {
return (
<div className="padd_top">
{renderTree(
this.props.root || root,
this.setActiveNode,
this.state.activeNode,
null,
this.state.searchTerm
)}
</div>
);
}
}
/**** DIRECTORY ****/
class Directory extends React.Component {
constructor(props) {
super(props);
this.state = { expanded: true };
this.toggleDirectory = this.toggleDirectory.bind(this);
}
toggleDirectory() {
this.setState({ expanded: !this.state.expanded });
}
hasMatchingNodes() {
const searchTerm = this.props.searchTerm.toLowerCase();
const matchNode = node =>
node.contents
? node.contents.filter(matchNode).length !== 0
: node.name.toLowerCase().indexOf(searchTerm) !== -1;
return matchNode(this.props.node);
}
render() {
let node = this.props.node;
const rotate = this.state;
if (this.props.searchTerm && !this.hasMatchingNodes()) return null;
return (
<div className="directory-container">
<div className="directory">
<div
className={`directory__toggle ${
this.state.expanded ? "expanded" : ""
}`}
>
<div onClick={this.toggleDirectory}>
<TriangleDown onClick={() => this.setState({ rotate: true })}
className={rotate ? "rotate" : ""} />
</div>
</div>
<div className="directory__icon" onClick={this.toggleDirectory}>
<FolderIcon />
</div>
<div className="directory__name" onClick={this.toggleDirectory}>
<div>{formatName(node.name)}</div>
</div>
</div>
{this.state.expanded
? node.contents.map((content, index) =>
renderTree(
content,
this.props.setActiveNode,
this.props.activeNode,
index,
this.props.searchTerm
)
)
: ""}
</div>
);
}
}
// Set class Active to selected file
const File = ({ name, setActiveNode, activeNode, searchTerm }) => {
if (searchTerm && name.toLowerCase().indexOf(searchTerm.toLowerCase()) < 0)
return null;
let isActive = activeNode === name;
let className = isActive ? "active" : "";
return (
<div className={className + " file"} onClick={() => setActiveNode(name)}>
<div className="file__icon">
<FileIcon />
</div>
<div className="file__name">{formatName(name)}</div>
{isActive && <div className="file__options">...</div>}
</div>
);
};
var renderTree = (node, setActiveNode, activeNode, index, searchTerm) => {
if (node.type === "file") {
return (
<File
key={index}
name={node.name}
setActiveNode={setActiveNode}
activeNode={activeNode}
searchTerm={searchTerm}
/>
);
} else if (node.type === "directory") {
return (
<Directory
key={index}
node={node}
setActiveNode={setActiveNode}
activeNode={activeNode}
searchTerm={searchTerm}
/>
);
} else {
return null;
}
};
/**** APP ****/
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
activeNode: ""
};
this.onChange = this.onChange.bind(this);
}
liftStateUp = (data) => {
this.setState({ activeNode: data });
};
onChange(data) {
this.setState({ searchTerm: data });
}
render() {
return (
<div>
<div className="col-md-12">
<SearchEngine className="form-control" onChange={this.onChange} />
</div>
<div className="col-md-6">
<FileTree
liftStateUp={this.liftStateUp}
searchTerm={this.state.searchTerm}
/>
</div>
<div className="col-md-6">
<TextBox content={this.state.activeNode}/>
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("app"));
* {
font-family: Helvetica;
color: #333333 !important;
}
/** DIRECTORY CSS **/
.directory {
padding-left: 10px;
padding-top: 1px;
padding-bottom: 1px;
display: flex;
flex-direction: row;
align-items: center;
}
.directory__toggle {
padding-left: 10px;
transform: rotate(-90deg)
}
.directory__icon {
padding-left: 10px;
}
.directory__icon {
padding-left: 10px;
}
.directory__name {
padding-left: 10px;
}
.directory-container {
padding-left: 10px;
}
/** FILE CSS **/
.file {
padding-left: 50px;
padding-top: 1px;
padding-bottom: 1px;
display: flex;
}
.file__icon {
padding-left: 10px;
}
.file__name {
padding-left: 10px;
}
.file__options {
align-self: flex-end;
}
.icon {
display: inline-block;
width: 1em;
height: 1em;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
}
.svg-icon {
width: 1em;
height: 1em;
}
.expanded {
transform: rotate(0deg)
}
/** CONTENT BOX **/
.padd_top {
padding-top: 20px;
}
.btn-danger {
color: #fff !important;
}
.content_box {
font-size: 12px;
white-space: pre-wrap;
border: solid 1px black;
padding: 20px;
color: #9da5ab;
min-height: 250px;
width: 100%;
}
.text_color {
color: #21252b !important;
}
/** arrow animation **/
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<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>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<title>React App</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
Your problem is data propagation. You gave data to FileTree Component and your SearchEngine is on the same level as the FileTree Component, and it cannot access the data which has to be filtered. I lifted up data to App Component - parent of SearchEngine and FileTree Component and propagated data to FileTree. Instead of propagating data to SearchEngine - I lifted onSubmitSearch event handler to App Component and propagated it to SearchEngine, because even if I gave data to SearcEngine I couldn't update it on FileTree Component (because of unidirectional data flow).
// Dummy data set
var root = data[0];
/**** APP ****/
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
activeNode: "",
root: root
};
this.onChange = this.onChange.bind(this);
}
liftStateUp = data => {
this.setState({ activeNode: data });
};
onSubmitSearch = (e, search) => {
let tree = JSON.stringify(root); // always search full data tree
tree = JSON.parse(tree); // JSON.stringify and then JSON.parse are
if (!search || search === "") {
// if search is undefined, null or empty, set root to full data tree
this.setState({ root: tree }); // state.root is filtered tree passed to the FileTree component
return;
}
/*uncoment if you need to filter already filtered tree*/
// tree = JSON.stringify(this.state.root);
// tree = JSON.parse(tree);
/**/
// else filter tree
this.setState({
root: this.filterTree(tree, search.toLowerCase())
});
};
filterTree = (data, search) => {
let children = data.contents;
if (!children || !children.length) {
if (!data.name.toLowerCase().includes(search)) {
data.remove = true;
}
} else {
for (let i = children.length - 1; i >= 0; i--) {
this.filterTree(children[i], search);
if (children[i].remove) {
children.splice(i, 1);
}
}
if (!children.length) {
data.remove = true;
}
}
return data;
};
onChange(data) {
this.setState({ searchTerm: data });
}
render() {
return (
<div>
<div className="col-md-12">
<SearchEngine
className="form-control"
onChange={this.onChange}
onSubmitSearch={this.onSubmitSearch}
/>
</div>
<div className="col-md-6">
<FileTree
root={this.state.root}
liftStateUp={this.liftStateUp}
searchTerm={this.state.searchTerm}
/>
</div>
<div className="col-md-6">
<TextBox content={this.state.activeNode} />
</div>
</div>
);
}
}
Note that App now has onSubmitSearch function which is then propagated to SearchEngine where it is called with the search input value:
/**** SEARCH COMPONENT ****/
class SearchEngine extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
this.onInputChange = this.onInputChange.bind(this);
}
onInputChange(e) {
const content = e.target.value;
this.setState({ value: content });
}
render() {
const { onSubmitSearch } = this.props;
const { value } = this.state;
return (
<div>
<form onSubmit={e => onSubmitSearch(e, value)}>
<input
className="form-control"
value={this.state.value}
type="text"
onChange={this.onInputChange}
/>
<p>{this.state.value}</p>
<SearchButton />
</form>
</div>
);
}
}
And the FileTree Component now gets filtered data (by search input value / search engine) and takes care about rendering FileTree Component only.
Take a look at the working example of filtering tree structure with submit button here: https://codesandbox.io/s/3rnvv0kln6
I have a form that should update my applications state as a user types in the input field and in the textarea. The input and textarea call their event handlers through onChange={event handler}. For some reason when I type in either field the event handlers don't get called at all. The input and textarea fields seem to work fine outside of a form though.
import React, { Component, PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import LogoutHeader from './LogoutHeader';
import { fetchTodo, updateTodo, deleteTodo } from '../actions/index';
import { Link } from 'react-router';
class ShowTodo extends Component {
static contextTypes = {
router: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
descriptionChanged: false,
newDescription: '',
newTitle: '',
done: false,
id: 0
};
this.handleDescriptionChange = this.handleDescriptionChange.bind(this);
this.handleDeleteClick = this.handleDeleteClick.bind(this);
this.changeButtons = this.changeButtons.bind(this);
this.handleSaveClick = this.handleSaveClick.bind(this);
this.handleUndoClick = this.handleUndoClick.bind(this);
this.handleTitleChange = this.handleTitleChange.bind(this);
this.handleDoneChange = this.handleDoneChange.bind(this);
}
componentWillMount() {
this.props.fetchTodo(this.props.params.id).then(() => {
this.setState({
newDescription: this.props.todo.description,
newTitle: this.props.todo.title,
done: this.props.todo.completed,
id: this.props.todo.id
});
});
}
render() {
const { todo } = this.props;
const { fields: {title, description}, handleSubmit } = this.props;
console.log("Fields: description: ", this.props.fields.description.value); //These values change as expected
console.log("Fields: title: ", this.props.fields.title.value);
if (!todo) {
return (
<h3>Loading...</h3>
);
}
return (
<div id="showTodo">
<Link id="btnBack" className="btn btn-custom" role="button" to="/todos_index"><span className="glyphicon glyphicon-arrow-left"></span></Link>
<LogoutHeader></LogoutHeader>
<div className="row">
<div className="col-md-6 col-md-offset-3">
<form onSubmit={handleSubmit(this.handleSaveClick)}>
<h3>Edit Todo</h3>
<div className={`form-group ${title.touched && title.invalid ? 'has-danger' : ''}`}>
<label>Title</label>
<input
type="text"
className="form-control"
value={this.state.newTitle}
onChange={this.handleTitleChange}
{...title} />
</div>
<div className="text-help">
{description.touched ? description.error : ''}
</div>
<div className={`form-group ${description.touched && description.invalid ? 'has-danger' : ''}`}>
<label>Description</label>
<textarea
className="form-control"
value={this.state.newDescription}
onChange={this.handleDescriptionChange}
{...description} >
</textarea>
</div>
<div className="text-help">
{description.touched ? description.error : ''}
</div>
<span className="input-group-btn">
{this.changeButtons()}
<button id="editTodoDelete" className="btn btn-custom" onClick={this.handleDeleteClick}><span className="glyphicon glyphicon-trash"></span></button>
</span>
</form>
</div>
</div>
</div>
);
}
changeButtons() { //This does not get called when there are changed in the input or textareas.
if (!this.state.descriptionChanged) {
return null;
} else {
return [
<button
type="submit"
id="editTodoSave"
className="btn btn-custom"
><span className="glyphicon glyphicon-floppy-save"></span></button>,
<button
id="editTodoRefresh"
className="btn btn-custom"
onClick={this.handleUndoClick}
><span className="glyphicon glyphicon-refresh"></span></button>
];
}
}
handleDescriptionChange(event) { //This does not get called when there is a change in the textarea
this.setState({
descriptionChanged: true,
newDescription: this.props.fields.description.value
});
}
handleTitleChange(event) { //This does not get called when there is a changed in the input field.
this.setState({
descriptionChanged: true,
newTitle: this.props.fields.title.value
});
}
handleDoneChange() {
this.setState({
done: !this.state.done
});
var props = {
completed: this.state.done
};
this.props.updateTodo(this.state.id, JSON.stringify(props));
}
handleDeleteClick() {
this.props.deleteTodo(this.state.id).then(() => {
this.context.router.push('/todos_index');
});
}
handleSaveClick(props) {
this.props.updateTodo(this.state.id, JSON.stringify(props)).then(() => {
alert("Todo updates should have been recieved in database");
this.context.router.push('/todos_index');
});
}
handleUndoClick() {
this.setState({
descriptionChanged: false,
newTitle: this.props.todo.title,
newDescription: this.props.todo.description,
errors: {
title: '',
description: ''
}
});
}
}
function validate(values) {
const errors = {};
if (!values.title) {
errors.title = 'Please enter a title';
}
if (values.title) {
if (values.title.length > 25){
errors.title = 'You exceeded 25 characters';
}
}
if (!values.description) {
errors.description = 'Please enter your description';
}
if (values.description) {
if (values.description.length > 500) {
errors.description = "You exceeded 500 characters";
}
}
return errors;
}
function mapStateToProps(state) {
return { todo: state.todos.todo };
}
export default reduxForm({
form: 'ShowTodoForm',
fields: ['title', 'description'],
validate //These configurations will be added to the application state, so reduxForm is very similar to the connect function.
//connect: first argument is mapStateToProps, second is mapDispatchToProps
//reduxForm: 1st is form configuration, 2nd is mapStateToProps, 3rd is mapDispatchToProps
}, mapStateToProps, { fetchTodo, updateTodo, deleteTodo })(ShowTodo);