I'm building a help page with Gatsby and have a search bar (Searchbar.js) where I'm trying to pass the user's input in the field (search bar is always present within the page--think like Evernote's help page) to a component that conducts the search (search.js), which then passes that output to the actual results page (SearchResults.js).
When I do gatsby develop everything works as it should, but when I do a gatsby build I get an error where it says it cant read the property "query" because its undefined (line 63 of search.js: var search = location.state.query.trim()). Why is this failing on build?
Searchbar.js
import React from 'react'
import { navigate } from 'gatsby'
import { FaSearch } from 'react-icons/fa'
import searchbarStyles from "./searchbar.module.css"
export default class Search extends React.Component {
constructor(props) {
super(props)
this.state = {
search: ''
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleChange = this.handleChange.bind(this)
}
handleSubmit(event) {
event.preventDefault()
var query = this.state.search
navigate(
"/search/",
{
state: { query },
}
)
}
handleChange(event) {
this.setState({
search: event.target.value
})
}
render(){
return (
<div className={searchbarStyles.global_search}>
<div className={searchbarStyles.row}>
<form className={searchbarStyles.search} onSubmit={this.handleSubmit}>
<input
type='text'
id='globalSearchInput'
className=''
placeholder='Search Help & Training'
autoComplete='off'
value={this.state.search}
onChange={this.handleChange}
/>
<button
type='submit'
disabled={!this.state.search}
><FaSearch className={searchbarStyles.searchIcon}/></button>
</form>
</div>
</div>
)
}
}
search.js
import React, { useMemo } from 'react'
import Layout from '../components/Layout'
import SearchResults from '../components/SearchResults'
import { graphql } from 'gatsby'
import Fuse from 'fuse.js'
const matchThreshold = .65
//options for the fuzzy search
var fuseOptions = {
shouldSort: true,
threshold: matchThreshold,
location: 0,
distance: 99999999999,
minMatchCharLength: 1,
includeMatches: true,
includeScore: true,
keys: [
{name: "title", weight: 0.3 },
{name: "content", weight: 0.7}
]
};
function cleanString(string) {
const re = /( |<([^>]+)>)/ig
return string.replace(re,'')
}
function FuzzySearch (query, data) {
fuseOptions.minMatchCharLength = query.length
var dataPrepped = data.map(function(element) {
return {
"title": element.node.frontmatter.title,
"content": cleanString(element.node.html),
"slug": element.node.fields.slug,
}
})
var fuse = useMemo(() => new Fuse(dataPrepped, fuseOptions), [])
var results = fuse.search(query)
//customize the results to only return matches within desired threshold
return results.filter(function(match) {
if(match.score <= matchThreshold) {
return true
}
return false
}).map(function(match) {
return {
"title": match.item.title,
"slug": match.item.slug,
"matches": match.matches
}
})
}
export default ({ location, data }) => {
console.log("SERACH.JS\n")
console.log(JSON.stringify(location))
var search = location.state.query.trim()
var results = []
if(search.length) results = FuzzySearch(search, data.allMarkdownRemark.edges)
return (
<Layout>
<SearchResults FoundItems={results} SearchedTerm={search}> </SearchResults>
</Layout>
)
}
export const query = graphql `
query MyQuery {
allMarkdownRemark {
edges {
node {
fields {
slug
}
frontmatter {
title
date
doctype
}
html
}
}
}
}
`
SearchResults.js
import React from 'react'
import { Link } from 'gatsby'
import searchResultStyles from "./searchresults.module.css"
function resultsPage(resultsBlurb, results) {
return(
<div className={searchResultStyles.content}>
<h1>Search Results</h1>
<p className={searchResultStyles.resultBlurb}>{resultsBlurb}</p>
<ol>
{results.map((match) => (
<li>
<div className={searchResultStyles.resultContent}>
<Link to={match.slug} className={searchResultStyles.resultTitle}>{match.title}</Link>
{match.matches.map(function(instanceOfMatch) {
if(instanceOfMatch.key === "content") {
let startIndex = instanceOfMatch.indices[0][0]
return(
<p className={searchResultStyles.resultExcerpt}>{`...${instanceOfMatch.value.substring(startIndex, startIndex + 100)}...`}</p>
)
}
})}
</div>
</li>
))}
</ol>
</div>
)
}
class SearchResults extends React.Component {
constructor(props) {
super(props)
this.state = {
results: props.FoundItems,
term: props.SearchedTerm,
}
this.updateBlurb = this.updateBlurb.bind(this)
}
updateBlurb() {
let resultsBlurb = `Sorry--could not find any results for "${this.state.term}"`
if (this.state.results.length) {
resultsBlurb = (this.state.results.length === 1) ? `Only 1 item found for "${this.state.term}"` : `Found ${this.state.results.length} items for "${this.state.term}"`
}
return resultsBlurb
}
componentDidUpdate(prevProps) {
if(prevProps!== this.props) {
this.setState ({
results: this.props.FoundItems,
term: this.props.SearchedTerm
})
}
return(resultsPage(this.updateBlurb(), this.props.FoundItems))
}
render() {
return(resultsPage(this.updateBlurb(), this.state.results))
}
}
export default SearchResults
SOLUTION
(within search.js)
export default ({ location, data }) => {
var search = ''
var results = []
if(typeof window !== "undefiend") search = location.state.query.trim()
if(search.length) results = ...
location is short for window.location, but at build-time your code is running in Node.js which does not have a window. Instead consider testing for the existence of window (typeof window !== "undefined") before running your location.state.query.trim call, and fall back to a default value in the case that window does not exist.
Related
I'm trying to create a web-app with the front end in React and backend in Flask. I have a dropdown which gets populated by the flask JSON which is basically a list of companies. All total there are around 5 components, the first one is the App.js, second is the CompanySelection.js, the third one is the Chart.js where I want to return my graphs and all.
So in theCompanySelection.js when I change the dropdown selection the updated company name does not go into Charts.js, i.e. the other component. I guess when this part gets solved similarly I can pass the values from one component to the other easily.
These are my three code files:
App.js
import React from "react";
import { CompanyContextProvider } from "./context";
import Dropdown from 'react-dropdown';
import 'react-dropdown/style.css';
import Header from "./Header";
import CompanySelection from "./CompanySelection/CompanySelection.js";
import Charts from "./Charts/Chart.js";
import axios from 'axios';
class App extends React.Component{
state = {
companies: [],
firstCompany: {},
firstCompanyName: ''
};
componentDidMount() {
fetch('http://127.0.0.1:5001/algo/loc')
.then(res => res.json())
.then(data => {
this.setState({companies: data,
firstCompany: data[0],
firstCompanyName: data[0].value}, () =>
console.log(this.state.companies, this.state.firstCompany, this.state.firstCompanyName));
console.log('')
}).catch(function (error) {
console.log(error);
});
}
selectedValueHandler = (selectedValue) => {
this.setState({
firstCompanyName: selectedValue
})
}
render() {
const { selectedValue } = this.state.firstCompanyName;
console.log('change value',selectedValue)
return (
<div className="app">
<Header/>
<CompanySelection companies= {this.state.companies} selectedCompany={this.state.firstCompany} setSelectedCompany={this.state.firstCompanyName} selectedValueHandler = {this.selectedValueHandler}/>
<Charts companies= {this.state.companies} selectedCompany={this.state.firstCompany} setSelectedCompany={selectedValue}/>
</div>
);
}
} ;
export default App;
CompanySelection.js
import { h, render, Component} from 'preact';
import style from './style.css';
import { useContext } from "preact/hooks";
import { CompanyContext } from "../context";
class CompanySelection extends Component {
constructor(props)
{
super(props);
}
render(_, { value }) {
const companies = this.props.companies;
const selectedCompany = this.props.selectedCompany;
const setSelectedCompany = this.props.setSelectedCompany;
var onChange = (e) =>{
console.log("In on change");
this.setState({ value: e.target.value });
const setSelectedCompany = e.target.value;
console.log("Selected", e.target.value);
const companies = this.props.companies;
const selectedCompany = this.props.selectedCompany;
this.props.selectedValueHandler(e.target.value);
}
if (typeof companies !== 'undefined')
{
var options = companies.map((comp) =>
<option
key={comp.label}
value={comp.value}
>
{comp.label}
</option>
);
}
else {
var options = [{value: 'A', label: 'B'}].map((comp) =>
<option
key={comp.label}
value={comp.value}
>
{comp.label}
</option>
);
}
return (
<fragment class={style.fragment}>
<label class={style.label}> Company </label>
<select value={value} onChange={onChange} class={style.dropdown}>
{options}
</select>
</fragment>
);
}
}
render(<CompanySelection />, document.body);
export default CompanySelection;
Chart.js
import { h, render, Component } from 'preact';
import style from './style.css';
import { VictoryChart, VictoryLine, VictoryScatter, VictoryLabel} from 'victory';
import { useContext } from "preact/hooks";
import { CompanyContext } from "../context";
class Charts extends Component {
constructor(props)
{
super(props);
}
render(_, { value }) {
const companies = this.props.companies;
const selectedCompany = this.props.selectedCompany;
const setSelectedCompany = this.props.setSelectedCompany;
console.log('list of companies chart', companies)
console.log('chart input', setSelectedCompany)
if (typeof selectedCompany !== 'undefined') {
var comp = selectedCompany;
}
else {
var comp = '';
}
console.log("comp", comp);
return (
<fragment>
<div class={style.chart}>
<VictoryChart domain={[0, 10]}>
<VictoryLabel text={comp} x={225} y={30} textAnchor="middle"/>
<VictoryLine
style={{ data: { stroke: "blue", strokeWidth: 3 } }}
y={(d) => d.x}
/>
<VictoryScatter
symbol="star"
size={8}
style={{ data: { fill: "red" }}}
data={[{ x: 5, y: 5 }]}
/>
<VictoryScatter
symbol="circle"
size={8}
style={{ data: { fill: "red" }}}
data={[{ x: 7, y: 7 }]}
/>
</VictoryChart>
</div>
</fragment>
);
}
}
render(<Charts />, document.body);
export default Charts;
I have taken reference of this code from this stackoverflow post: How to pass data from one component to another component in onchange using React js
However when I see the output of this line of code: const { selectedValue } = this.state.firstCompanyName;
console.log('change value',selectedValue)
I get that change value undefined, which means the values are not getting passed on. I'm very new to react and haven't been able to solve this yet. Any help is much appreciated.
P.S. The components pass on well to the <CompanySelection..../>
However when I see the output of this line of code: const { selectedValue } = this.state.firstCompanyName;
console.log('change value',selectedValue)
It will be undefined, here you are trying to destructure selectedValue from a string firstCompanyName, as long as this.state.firstCompanyName is not an object with a key named selectedValue it will be undefined. Instead do this.
const selectedValue = this.state.firstCompanyName;
console.log('change value',selectedValue)`
I will preface this with stating this is my fourth day working on Node or React.js, so please bear with me.
I am building a custom, offline search function for Docusaurus 2. I have built a JSON index and created a function to search it with elasticlunr. I want to redirect to a separate results page, however I am having issues with the redirect despite trying to follow multiple examples. Here is my index.js for the SearchBar.
import React, {Component} from 'react';
import {Redirect} from 'react-router-dom';
import classnames from 'classnames';
import elasticlunr from 'elasticlunr';
let siteIndex = require('./siteIndex.json');
class Search extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
term: '',
index: elasticlunr(function () {
this.setRef('id');
this.addField('title');
this.addField('body');
this.addField('url');
})
};
this.toggleSearchPressEnter = this.toggleSearchPressEnter.bind(this);
this.changeTerm = this.changeTerm.bind(this);
}
init() {
let siteIndexKeys = Object.keys(siteIndex);
siteIndexKeys.forEach(key => {
this.state.index.addDoc(siteIndex[key]);
});
}
changeTerm(e) {
this.setState({term: e.target.value});
}
toggleSearchPressEnter(e) {
if (e.key === "Enter") {
this.init();
let siteSearch = this.state.index.search(e.target.value, {}); // Empty dictionary here fixes the warning about using the default configuration because you didn't supply one!
let docs = this.state.index.documentStore.docs;
this.state.results = siteSearch.slice(0, 5).map(searchKey => docs[searchKey.ref]);
if (this.state.results.length > 0) {
this.renderRedirect();
}
}
}
renderRedirect() {
console.log("Go home!");
console.log(this.state.results.length);
console.log(this.state.results);
// window.location = "/"
<Redirect
to={{
pathname: '/',
state: { results: this.state.results }
}}
/>
}
render() {
return (
<div className="navbar__search" key="search-box">
<span
aria-label="expand searchbar"
role="button"
className={classnames('search-icon', {
'search-icon-hidden': this.props.isSearchBarExpanded,
})}
tabIndex={0}
/>
<input
id="search_input_react"
type="search"
placeholder="Search"
aria-label="Search"
className={classnames(
'navbar__search-input',
{'search-bar-expanded': this.props.isSearchBarExpanded},
{'search-bar': !this.props.isSearchBarExpanded},
)}
onKeyPress={this.toggleSearchPressEnter}
/>
</div>
);
}
}
export default Search;
Because we had issues redirecting to the results page with the results, I wanted to see if I could just go to the home page. I see the message "Go home!" in the browser console when the user hits enter on the search bar, but no redirect occurs. I have commented out the javascript redirect that does work if I comment out Redirect from renderRedirect().
I have tried adding a return() around the Redirect, but it does not seem to make any difference.
If you would like to reproduce the issue
npx #docusaurus/init#next init docs classic
npm run swizzle #docusaurus/theme-search-algolia SearchBar
Replace the contents of src/theme/SearchBar/index.js with the code that is the problem above.
To generate the JSON index:
generate-index.js
const fs = require('fs-extra');
const path = require('path');
const removeMd = require('remove-markdown');
let searchId = 0;
const searchDoc = {};
async function readAllFilesAndFolders(folder) {
try {
const topFilesAndFolders = fs.readdirSync(folder);
for (let i = 0; i < topFilesAndFolders.length; i++) {
const file = topFilesAndFolders[i];
const fileOrFolderPath = `${folder}/${file}`;
const stat = fs.lstatSync(fileOrFolderPath);
if (stat.isFile() && path.extname(fileOrFolderPath) === '.md') {
console.log(`Got Markdown File ${file}`);
fs.readFile(fileOrFolderPath, (err, data) => {
if (err) throw err;
const regex = /title: .*\n/g;
let search = data.toString().match(regex);
let docTitle = search[0].toString().replace("title: ", "");
console.log("doctitle: ", docTitle);
if (!docTitle) {
docTitle = file.replace('.md', '');
generateSearchIndexes(fileOrFolderPath, file, docTitle);
}
else {
generateSearchIndexes(fileOrFolderPath, file, docTitle);
}
});
} else if (stat.isDirectory()) {
console.log(`Got Directory ${file}, Started Looking into it`);
readAllFilesAndFolders(fileOrFolderPath, file);
}
}
} catch (error) {
console.log(error);
}
}
function generateSearchIndexes(fileOrFolderPath, file, docTitle) {
try {
let fileContent = fs.readFileSync(fileOrFolderPath, 'utf-8');
let body = removeMd(fileContent).replace(/^\s*$(?:\r\n?|\n)/gm, '');
let title = docTitle.trim();
let url = fileOrFolderPath
.replace('.md', '')
.trim();
searchDoc[file.replace('.md', '').toLowerCase()] = { id: searchId, title, body, url };
fs.writeFileSync('src/theme/SearchBar/siteIndex.json', JSON.stringify(searchDoc), 'utf-8');
searchId = searchId + 1;
} catch (error) {
console.log('Failed to generate fail:', error);
}
}
readAllFilesAndFolders('docs');
Once the JSON index is built from the default docs, the search can be attempted. I haven't made any other changes.
I've probably done something stupid and hopefully it is easily fixable, so please be merciful. I really did try. ;)
Using some guidance from Ajay, and playing around a little, I have a working solution.
import React, {Component} from 'react';
import {Redirect} from 'react-router';
import classnames from 'classnames';
import elasticlunr from 'elasticlunr';
let siteIndex = require('./siteIndex.json');
class Search extends Component {
constructor(props) {
super(props);
this.state = {
results: [],
term: '',
search: '',
index: elasticlunr(function () {
this.setRef('id');
this.addField('title');
this.addField('body');
this.addField('url');
})
};
this.toggleSearchPressEnter = this.toggleSearchPressEnter.bind(this);
this.changeTerm = this.changeTerm.bind(this);
}
init() {
let siteIndexKeys = Object.keys(siteIndex);
siteIndexKeys.forEach(key => {
this.state.index.addDoc(siteIndex[key]);
});
}
changeTerm(e) {
this.setState({term: e.target.value});
}
toggleSearchPressEnter(e) {
if (e.key === "Enter") {
this.init();
let searchTerm = e.target.value;
let siteSearch = this.state.index.search(searchTerm, {}); // Empty dictionary here fixes the warning about using the default configuration because you didn't supply one!
let docs = this.state.index.documentStore.docs;
let searchResults = siteSearch.slice(0, 5).map(searchKey => docs[searchKey.ref]);
this.setState({
results: searchResults,
search: searchTerm,
});
}
}
render() {
if (this.state.results.length >= 1) {
return <Redirect to={{
pathname: '/results',
state: {
results: this.state.results,
search: this.state.search
}
}} />
}
return (
<div className="navbar__search" key="search-box">
<span
aria-label="expand searchbar"
role="button"
className={classnames('search-icon', {
'search-icon-hidden': this.props.isSearchBarExpanded,
})}
tabIndex={0}
/>
<input
id="search_input_react"
type="search"
placeholder="Search"
aria-label="Search"
className={classnames(
'navbar__search-input',
{'search-bar-expanded': this.props.isSearchBarExpanded},
{'search-bar': !this.props.isSearchBarExpanded},
)}
onKeyPress={this.toggleSearchPressEnter}
/>
</div>
);
}
}
export default Search;
I am trying to make live search for name in table but i can't make live search i don't know how to do this i wrote my code like this as i mentioned please help me how to make live search on name field foe table and in Search Page i used onSubmit={this.props.loaddata like this thanks
import React, { Component } from "react";
import Search from "../../views/Cars/Search";
class Search1 extends Component {
constructor(props) {
super(props);
this.state = {
query: []
};
}
// Get Data from filter date
getData = async e => {
try {
const search = e.target.elements.search.value;
e.preventDefault();
const res = await fetch(`https://swapi.co/api/people/?search=${search}`);
const query = await res.json();
console.log(query);
this.setState({
query: query.results
});
} catch (e) {
console.log(e);
}
};
async componentDidMount() {
// let authToken = localStorage.getItem("Token");
try {
const res = await fetch(`https://swapi.co/api/people/`);
const query = await res.json();
// console.log(movie);
this.setState({
query: query.results
});
} catch (e) {
console.log(e);
}
}
render() {
const options = this.state.query.map(r => <li key={r.id}>{r.name}</li>);
return (
<div>
<Search loaddata={this.getData} />
{options}
</div>
);
}
}
export default Search1;
Genrally You can try React-Search
import Search from 'react-search'
import ReactDOM from 'react-dom'
import React, { Component, PropTypes } from 'react'
class TestComponent extends Component {
HiItems(items) {
console.log(items)
}
render () {
let items = [
{ id: 0, value: 'ruby' },
{ id: 1, value: 'javascript' },
{ id: 2, value: 'lua' },
{ id: 3, value: 'go' },
{ id: 4, value: 'julia' }
]
return (
<div>
<Search items={items} />
<Search items={items}
placeholder='Pick your language'
maxSelected={3}
multiple={true}
onItemsChanged={this.HiItems.bind(this)} />
</div>
)
}
}
Made few changes to your component. Send e.target.value from your child component
class Search1 extends Component {
constructor(props) {
super(props);
this.state = {
query: []
};
}
// Get Data from filter date
getData = search => {
const url = `https://swapi.co/api/people${search ? `/?search=${search}` : ``}`;
// e.preventDefault();
fetch(url)
.then(res => res.json())
.then(data =>
this.setState({
query: data.results || []
})).catch(e => console.log(e));
};
async componentDidMount() {
// let authToken = localStorage.getItem("Token");
this.getData();
}
render() {
const options = this.state.query.map(r => <li key={r.id}>{r.name}</li>);
return (
<div>
<Search loaddata={this.getData} />
{options}
</div>
);
}
}
export default Search1;
For Gettind Data from Api you can follow this code of react-search
import Search from 'react-search'
import ReactDOM from 'react-dom'
import React, { Component, PropTypes } from 'react'
class TestComponent extends Component {
constructor (props) {
super(props)
this.state = { repos: [] }
}
getItemsAsync(searchValue, cb) {
let url = `https://api.github.com/search/repositories?q=${searchValue}&language=javascript`
fetch(url).then( (response) => {
return response.json();
}).then((results) => {
if(results.items != undefined){
let items = results.items.map( (res, i) => { return { id: i, value: res.full_name } })
this.setState({ repos: items })
cb(searchValue)
}
});
}
render () {
return (
<div>
<Search items={this.state.repos}
multiple={true}
getItemsAsync={this.getItemsAsync.bind(this)}
onItemsChanged={this.HiItems.bind(this)} />
</div>
)
}
I am near the end of creating my application.
So it is for banks accounts where they ask you to give the first letter of your password, then for example fourth, etc.
I'm tired of counting on my own so I created this app.
But there is the last bug that I don't know how to fix.
So when I press "1" I get "1 - H", and then when I press "4" I want to get:
"1 - H" (clicked before)
"4 - X" (clicked just now)
but instead, I get:
"4 - X" (clicked just now)
"4 - X" (clicked just now)
So it is caused by the way handleResults() function works inside my Input component, but for now it is my only concept how to approach this...
import React, { Component } from 'react';
import TextField from 'material-ui/TextField';
import './style.css';
import Buttons from '../Buttons';
import Results from '../Results';
class Input extends Component {
constructor(props) {
super(props);
this.state = {
password: 'Hh9Xzke2ayzcEUPHuIfS',
selectedButtons: [],
};
this.handleButtonSelectTwo = this.handleButtonSelectTwo.bind(this);
}
handleInputChange(pass) {
this.setState({ password: pass });
}
handleButtonSelectTwo(selected) {
this.setState({
selectedButtons: [...this.state.selectedButtons, selected],
});
}
handleResults() {
return this.state.selectedButtons.map(el => (
<Results key={el} appState={this.state} />
));
}
render() {
return (
<div>
<div className="Input-textfield">
<TextField
hintText="Paste your password here to begin"
value={this.state.password}
onChange={event => this.handleInputChange(event.target.value)}
/>
</div>
<div>
<Buttons
handleButtonSelectOne={this.handleButtonSelectTwo}
array={this.state.password.length}
/>
{this.handleResults()}
</div>
</div>
);
}
}
export default Input;
and here is Results component code:
import React, { Component } from 'react';
import _ from 'lodash';
import Avatar from 'material-ui/Avatar';
import List from 'material-ui/List/List';
import ListItem from 'material-ui/List/ListItem';
import './style.css';
const style = {
avatarList: {
position: 'relative',
left: -40,
},
avatarSecond: {
position: 'relative',
top: -40,
left: 40,
},
};
class Results extends Component {
resultsEngine(arg) {
const { selectedButtons, password } = this.props.appState;
const passwordArray = password.split('').map(el => el);
const lastSelectedButton = _.last(selectedButtons);
const passwordString = passwordArray[_.last(selectedButtons) - 1];
if (arg === 0) {
return lastSelectedButton;
}
if (arg === 1) {
return passwordString;
}
return null;
}
render() {
if (this.props.appState.selectedButtons.length > 0) {
return (
<div className="test">
<List style={style.avatarList}>
<ListItem
disabled
leftAvatar={<Avatar>{this.resultsEngine(0)}</Avatar>}
/>
<ListItem
style={style.avatarSecond}
disabled
leftAvatar={<Avatar>{this.resultsEngine(1)}</Avatar>}
/>
</List>
</div>
);
}
return <div />;
}
}
export default Results;
Anyone has an idea how should I change my code inside handleResults() function to achieve my goal? Any help with solving that problem will be much appreciated.
Buttons component code:
import React from 'react';
import OneButton from '../OneButton';
const Buttons = props => {
const arrayFromInput = props.array;
const buttonsArray = [];
for (let i = 1; i <= arrayFromInput; i++) {
buttonsArray.push(i);
}
const handleButtonSelectZero = props.handleButtonSelectOne;
const allButtons = buttonsArray.map(el => (
<OneButton key={el} el={el} onClick={handleButtonSelectZero} />
));
if (arrayFromInput > 0) {
return <div>{allButtons}</div>;
}
return <div />;
};
export default Buttons;
And OneButton code:
import React, { Component } from 'react';
import RaisedButton from 'material-ui/RaisedButton';
const style = {
button: {
margin: 2,
padding: 0,
minWidth: 1,
},
};
class OneButton extends Component {
constructor() {
super();
this.state = { disabled: false };
}
handleClick() {
this.setState({ disabled: !this.state.disabled });
this.props.onClick(this.props.el);
}
render() {
return (
<RaisedButton
disabled={this.state.disabled}
key={this.props.el}
label={this.props.el}
style={style.button}
onClick={() => this.handleClick()}
/>
);
}
}
export default OneButton;
In your resultsEngine function in the Results component you are specifying that you always want the _.last(selectedButtons) to be used. This is what it is doing, hence you always see the last button clicked. What you actually want is the index of that iteration to show.
const lastSelectedButton = selectedButtons[this.props.index];
const passwordString = passwordArray[selectedButtons[this.props.index]];
To get an index you have to create and pass one in, so create it when you map over the selected Buttons in the handleResults function in your Input component.
handleResults() {
return this.state.selectedButtons.map((el, index) => (
<Results key={el} appState={this.state} index={index} />
));
}
I don't want to fire requests as long as the user is typing. My code should throttle requests so that when the user types quickly, it will fire one request with the latest input value instead of many.
For now when I'm typing "test" it fires 4 different requests:
"t"
"te"
"tes"
"test"
So I found lodash _.debounce and _.throttle ( [https://lodash.com/docs/4.17.4#debounce] ) but don't really understand how I can inplement it to my code. Can anyone help me?
My code:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import './style.css';
import { search } from '../../actions/';
class SearchBar extends Component {
constructor(props) {
super(props);
this.state = { searchTerm: '' };
}
startSearch(query) {
const storedTerms = this.props.searchedTerm;
let foundDuplicate = false;
if (storedTerms.length === 0 && query) {
return this.props.search(query);
}
if (storedTerms.length !== 0 && query) {
const testDuplicate = storedTerms.map(term => term === query);
foundDuplicate = testDuplicate.some(element => element);
}
if (storedTerms.length !== 0 && !query) {
return false;
}
if (foundDuplicate) {
return false;
}
return this.props.search(query);
}
handleInputChange(term) {
this.setState({ searchTerm: term });
this.startSearch(term);
}
render() {
return (
<div className="Search-bar">
<input
value={this.state.searchTerm}
onChange={event => this.handleInputChange(event.target.value)}
/>
</div>
);
}
function mapStateToProps(state) {
return {
searchedTerm: state.searchedTerm,
savedData: state.savedData,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ search }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(SearchBar);
EDIT:
Thx to Sagiv b.g, I'm adding some explanation:
ok, so the user should type more than 2 letters && also my app should wait minimum 2 seconds before starting ajax request
EDIT2:
Thx to Sagiv b.g, for great solution!
I've changed my code like so:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import _ from 'lodash';
import './style.css';
import { search } from '../../actions/';
class SearchBar extends Component {
constructor(props) {
super(props);
this.state = { inputValue: '' };
this.startSearch = _.debounce(this.startSearch, 2000);
}
startSearch(query) {
const storedTerms = this.props.searchedTerm;
let foundDuplicate = false;
if (storedTerms.length === 0 && query) {
return this.props.search(query);
}
if (storedTerms.length !== 0 && query) {
const testDuplicate = storedTerms.map(term => term === query);
foundDuplicate = testDuplicate.some(element => element);
}
if (storedTerms.length !== 0 && !query) {
return false;
}
if (foundDuplicate) {
return false;
}
return this.props.search(query);
}
onChange = ({ target: { value } }) => {
this.setState({ inputValue: value });
if (value.length > 2) {
this.startSearch(value);
}
};
render() {
return (
<div className="Search-bar">
<input
placeholder="Type something to search GitHub"
value={this.state.inputValue}
onChange={this.onChange}
/>
</div>
);
}
}
function mapStateToProps(state) {
return {
searchedTerm: state.searchedTerm,
savedData: state.savedData,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ search }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(SearchBar);
Last Bug to deal with
But it has one last bug, that I don't know how to get rid off. When the user wants to change search query and uses backspace to erase search field, my app always fires unexpectedly another API request.
Here is an example:
https://youtu.be/uPEt0hHDOAI
Any ideas how I can get rid of that behavior?
Well this is easy with lodash _.debounce.
You wrap your method with it and pass the milliseconds you want to wait.
As for the minimum length of the input, just invoke the new method only if the length is above 2.
Here is a small running example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
message: '',
inputValue: ''
};
this.updateMessage = _.debounce(this.updateMessage, 2000);
}
onChange = ({ target: { value } }) => {
this.setState({ inputValue: value });
if (value.length > 2) {
this.updateMessage(value);
}
}
updateMessage = message => this.setState({ message });
render() {
const { message, inputValue } = this.state;
return (
<div>
<input placeholder="type something..." value={inputValue} onChange={this.onChange} />
<hr/>
<div>server call >> wait 2 seconds & min length of 2</div>
<p>{message}</p>
</div>
);
}
}
ReactDOM.render(<App />, 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>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.compat.js"></script>
<div id="root"></div>