Asad Javed
Asad Javed

Reputation: 25

Fetch data from API when form's search button clicked and show data on another page in React JS

I am developing a React JS web application where I have a form with four select fields (Make, Model, Min price and Max price) and a Search button. The data for search results will be fetched from API according to the selection of options. I want to show that data on another page in a card (page route path: /search) when user clicked on search button. I am using react router. The API url/end point is https://mysterious-journey-51969.herokuapp.com/api/search-vehicle/?q=mercedes&m=sprinter&pf=0&pt=100000 where "q" field matches Vehicle Make, "m" field matches Model, "pf" field matches Min Price, "pt" field matches Max Price. How I can do that?

Here is my Form component code:

import React, { Component } from 'react';
import { Form, FormGroup, Input } from 'reactstrap';
import { veh_data } from '../shared/vehicle_make_and_models';

const defaultValues = [
    { value: 0, text: 0, key: 1 },
    { value: 500, text: 500, key: 2 },
    { value: 1000, text: 1000, key: 3 },
    { value: 1500, text: 1500, key: 4 },
    { value: 2000, text: 2000, key: 5 },
    { value: 2000, text: 2000, key: 6 }
];

const MIN_TITLE = { selected: true, disabled: true, text: 'Min Price' };
const MAX_TITLE = { selected: true, disabled: true, text: 'Max Price' };

class ImgAndForm extends Component {
    constructor(props) {
        super(props);

        this.handleSearch = this.handleSearch.bind(this);
        this.keyToOption = this.keyToOption.bind(this);
        this.renderOptions = this.renderOptions.bind(this);
        this.handleModelChange = this.handleModelChange.bind(this);

        this.state = {
            minData: [MIN_TITLE, ...defaultValues],
            maxData: [MAX_TITLE, ...defaultValues],

            minValue: null,
            maxValue: null,
            modelSelected: null
        };

    }

    renderOptions(data) {
        return data.map(datum => {
            // this allows us to indicate whether we are selecting or disabling
            const selected = datum.selected || false;
            const disabled = datum.disabled || false;

            return (
                <option key={datum.key} value={datum.value} selected={selected} disabled={disabled}>
                    {datum.text}
                </option>
            );
        });
    }

    handleModelChange(event) {
        console.log(event.target.value);
        this.setState({ modelSelected: event.target.value });
    }

    handleSearch(event) {
        alert("Search button clicked");

    }

    keyToOption(key) {
        return key.split("-")
            .map(word => word.slice(0, 1).toUpperCase() + word.slice(1))
            .join(" ");
    }

    handleMinSelect = event => {
        const value = event.target.value;
        const newMaxValues = [];

        defaultValues.forEach(datum => {
            if (datum.value >= Number.parseInt(value, 10)) {
                newMaxValues.push(datum);
            }
        });

        this.setState({
            maxData: [MAX_TITLE, ...newMaxValues],
            minValue: value
        });
    };

    handleMaxSelect = event => {
        const value = event.target.value;
        this.setState({ maxValue: value });
    };

    render() {

        const vehicles = veh_data.reduce((acc, veh, i) => {
            let make = Object.keys(veh)[0],
                vehModels = veh[make];

            return {
                makes: [
                    ...acc.makes,
                    <option key={make + i} value={make}>{this.keyToOption(make)}</option>
                ],
                models: {
                    ...acc.models,
                    [make]: vehModels.map((model, i) => {
                        return (
                            <option key={make + model + i} value={model}>
                                {this.keyToOption(model)}
                            </option>
                        );
                    })
                }
            };
        }, { makes: [], models: [] });

        const selectedModels =
            this.state.modelSelected && this.state.modelSelected.length ? (
                vehicles.models[this.state.modelSelected]
            ) : (
                    <option value="">Model (select make first)</option>
                );

        return (
            <div>
                <header className="headerbg d-flex">
                    <div className="container my-auto">
                        <div className="row">
                            <div className="offset-1 col-10 offset-lg-0 col-lg-4">
                                <div id="search-form-div" className="container">
                                    <div className="row">
                                        <div className="col-12 my-4">
                                            <h3>Search</h3>
                                            <Form onSubmit={this.handleSearch}>
                                                <FormGroup>
                                                    <Input
                                                        onChange={e => this.handleModelChange(e)}
                                                        type="select"
                                                        name="q"
                                                        id="q"
                                                    >
                                                        <option value="">Make</option>
                                                        {vehicles.makes}
                                                    </Input>
                                                </FormGroup>
                                                <FormGroup>
                                                    <Input type="select" name="m" id="m">
                                                        {selectedModels}
                                                    </Input>
                                                </FormGroup>
                                                <FormGroup>
                                                    <Input type="select"
                                                        name="pf"
                                                        id="pf"
                                                        value={this.state.minValue}
                                                        onChange={this.handleMinSelect}>
                                                        {this.renderOptions(this.state.minData)}
                                                    </Input>
                                                </FormGroup>
                                                <FormGroup>
                                                    <Input
                                                        type="select"
                                                        name="pt"
                                                        id="pt"
                                                        value={this.state.maxValue}
                                                        onChange={this.handleMaxSelect}>
                                                        {this.renderOptions(this.state.maxData)}
                                                    </Input>
                                                </FormGroup>
                                                <FormGroup>
                                                    <Input type="submit" name="search" id="search" className="btn btn-primary" value="Search" />
                                                </FormGroup>
                                            </Form>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </header>

            </div>
        );
    }
}

export default ImgAndForm;

Here is my Search result component code:

import React, { Component } from 'react';
import Smallheader from './SmallHeader';
import { Card, CardImg, CardTitle, CardSubtitle } from 'reactstrap';

class SearchResult extends Component {
constructor(props) {
    super(props);

    this.state = {
    };
}


render() {

    return (
        <div>
            <Smallheader />
            <div className="my-5">
                <div className="container text-center" id="contactContainer">
                    <div className="row">
                        <div className="col-lg-12 mx-auto">
                            <h2 className="text-center">Search Results</h2>
                            <hr className="my-4 thick-hr" />
                        </div>
                    </div>
                    <div className="row">
                        <div className="col-6 col-lg-3 mt-4">
                            <Card>
                                <a href="#">
                                    <CardImg src="" className="img-fluid" />
                                    <CardTitle>Title Here</CardTitle>
                                    <CardSubtitle>Price Here</CardSubtitle>
                                </a>
                            </Card>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}
}

export default SearchResult;

Upvotes: 0

Views: 1988

Answers (2)

cantuket
cantuket

Reputation: 1592

Here is a working solution...

https://codesandbox.io/s/lrv2w3qxlq?moduleview=1

I've imported your SearchResults component and put it directly below your ImgAndForm, but you can move it anywhere in that render function.

For this specific situation you would need a way to render this on a new 'page' you would need a way to manage shared application state, like Redux or at least a container component as @MikeZinn mentioned, but to do that properly would require as significant amount of work to implement the routing and re-architect your entire program. (If you want I can show you a small hack to produce the same result without that for now, but I'd advise looking into a more permanent solution.)

Since the SearchResults component can be 'stateless' I removed the constructor function, but I left it as a class for now because this component will likely need state eventually.

I added the axios library to fetch the data from the API, but any other XHR module already used in your program will do.

NOTE: Since the specific API endpoints that your form is currently able to query are unavailable, I've hard coded the 'mercedes' example you provided, but the program will log both 'realQuery' and 'dummyQuery' so you see that it is producing the correct query structure for whenever you fix that.

Form Component

import React, { Component } from "react";
import { Form, FormGroup, Input } from "reactstrap";
// import { veh_data } from '../shared/vehicle_make_and_models';
import SearchResult from "./result";
import axios from "axios";

const veh_data = [
  { "alfa-romeo": ["145", "90", "Alfa 6", "Alfasud"] },
  { "aston-martin": ["15", "2-Litre", "AM Vantage", "Atom", "Cygnet", "DB2"] },
  { audi: ["100", "200", "A1", "A2", "A3", "A4", "A5", "A6", "A7"] }
];
const defaultValues = [
  { value: 0, text: 0, key: 1 },
  { value: 500, text: 500, key: 2 },
  { value: 1000, text: 1000, key: 3 },
  { value: 1500, text: 1500, key: 4 },
  { value: 2000, text: 2000, key: 5 },
  { value: 2000, text: 2000, key: 6 }
];

const MIN_TITLE = { selected: true, disabled: true, text: "Min Price" };
const MAX_TITLE = { selected: true, disabled: true, text: "Max Price" };

class ImgAndForm extends Component {
  constructor(props) {
    super(props);

    this.handleSearch = this.handleSearch.bind(this);
    this.keyToOption = this.keyToOption.bind(this);
    this.renderOptions = this.renderOptions.bind(this);
    this.handleModelChange = this.handleModelChange.bind(this);

    this.state = {
      minData: [MIN_TITLE, ...defaultValues],
      maxData: [MAX_TITLE, ...defaultValues],

      minValue: "",
      maxValue: "",
      modelSelected: "",
      makeSelected: "",
      searchResults: ""
    };
  }

  renderOptions(data) {
    return data.map(datum => {
      // this allows us to indicate whether we are selecting or disabling
      const selected = datum.selected || false;
      const disabled = datum.disabled || false;

      return (
        <option
          key={datum.key}
          value={datum.value}
          selected={selected}
          disabled={disabled}
        >
          {datum.text}
        </option>
      );
    });
  }

  handleModelChange(event) {
    console.log(event.target.value);
    this.setState({ modelSelected: event.target.value });
  }

  handleMakeChange(event) {
    console.log(event.target.value);
    this.setState({ makeSelected: event.target.value });
  }

  async handleSearch(event) {
    event.preventDefault();

    alert("Search button clicked");
    let { makeSelected, modelSelected, minValue, maxValue } = this.state;
    let realQuery =
      "https://mysterious-journey-51969.herokuapp.com/api/search-vehicle/?" +
      `q=${makeSelected.split("-").join("")}` +
      `&m=${modelSelected.split("-").join("")}` +
      `&pf=${minValue}` +
      `&pt=${maxValue}`;
    let dummyQuery =
      "https://mysterious-journey-51969.herokuapp.com/api/search-vehicle/?q=mercedes&m=sprinter&pf=0&pt=100000";
    console.log("realQuery (was not run)", realQuery);
    console.log("dummyQuery (was run)", dummyQuery);
    let res = await axios.get(dummyQuery).catch(err => console.log(err));
    console.log("res", res.data);
    if (res && res.data) {
      this.setState(prevState => {
        return {
          ...prevState,
          searchResults: res.data
        };
      });
    }
  }

  keyToOption(key) {
    return key
      .split("-")
      .map(word => word.slice(0, 1).toUpperCase() + word.slice(1))
      .join(" ");
  }

  handleMinSelect = event => {
    const value = event.target.value;
    const newMaxValues = [];

    defaultValues.forEach(datum => {
      if (datum.value >= Number.parseInt(value, 10)) {
        newMaxValues.push(datum);
      }
    });

    this.setState({
      maxData: [MAX_TITLE, ...newMaxValues],
      minValue: value
    });
  };

  handleMaxSelect = event => {
    const value = event.target.value;
    this.setState({ maxValue: value });
  };

  render() {
    const vehicles = veh_data.reduce(
      (acc, veh, i) => {
        let make = Object.keys(veh)[0],
          vehModels = veh[make];

        return {
          makes: [
            ...acc.makes,
            <option key={make + i} value={make}>
              {this.keyToOption(make)}
            </option>
          ],
          models: {
            ...acc.models,
            [make]: vehModels.map((model, i) => {
              return (
                <option key={make + model + i} value={model}>
                  {this.keyToOption(model)}
                </option>
              );
            })
          }
        };
      },
      { makes: [], models: [] }
    );

    const selectedModels =
      this.state.makeSelected && this.state.makeSelected.length ? (
        vehicles.models[this.state.makeSelected]
      ) : (
        <option value="">Model (select make first)</option>
      );

    return (
      <div>
        <header className="headerbg d-flex">
          <div className="container my-auto">
            <div className="row">
              <div className="offset-1 col-10 offset-lg-0 col-lg-4">
                <div id="search-form-div" className="container">
                  <div className="row">
                    <div className="col-12 my-4">
                      <h3>Search</h3>
                      <Form onSubmit={this.handleSearch}>
                        <FormGroup key={1}>
                          <Input
                            onChange={e => this.handleMakeChange(e)}
                            type="select"
                            name="q"
                            id="q"
                          >
                            <option value="">Make</option>
                            {vehicles.makes}
                          </Input>
                        </FormGroup>
                        <FormGroup key={2}>
                          <Input
                            onChange={e => this.handleModelChange(e)}
                            type="select"
                            name="m"
                            id="m"
                          >
                            {selectedModels}
                          </Input>
                        </FormGroup>
                        <FormGroup key={3}>
                          <Input
                            type="select"
                            name="pf"
                            id="pf"
                            value={this.state.minValue}
                            onChange={this.handleMinSelect}
                          >
                            {this.renderOptions(this.state.minData)}
                          </Input>
                        </FormGroup>
                        <FormGroup key={4}>
                          <Input
                            type="select"
                            name="pt"
                            id="pt"
                            value={this.state.maxValue}
                            onChange={this.handleMaxSelect}
                          >
                            {this.renderOptions(this.state.maxData)}
                          </Input>
                        </FormGroup>
                        <FormGroup key={5}>
                          <Input
                            type="submit"
                            name="search"
                            id="search"
                            className="btn btn-primary"
                            value="Search"
                          />
                        </FormGroup>
                      </Form>
                      <SearchResult results={this.state.searchResults} />
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </header>
      </div>
    );
  }
}

export default ImgAndForm;

Results Component

import React, { Component } from "react";
// import Smallheader from './SmallHeader';
import { Card, CardImg, CardTitle, CardSubtitle } from "reactstrap";

class SearchResult extends Component {
  renderResults() {
    let { results } = this.props;
    console.log("results", results);
    if (results && results.length) {
      return results.map(({ price, text, title, remote_image }, i) => {
        return (
          <Card key={"card-" + i}>
            <a href="#">
              <CardImg src={remote_image} className="img-fluid" />
              <CardTitle>{title}</CardTitle>
              <CardSubtitle>{price}</CardSubtitle>
            </a>
          </Card>
        );
      });
    }
  }

  render() {
    return (
      <div>
        {/* <Smallheader /> */}
        <div className="my-5">
          <div className="container text-center" id="contactContainer">
            <div className="row">
              <div className="col-lg-12 mx-auto">
                <h2 className="text-center">Search Results</h2>
                <hr className="my-4 thick-hr" />
              </div>
            </div>
            <div className="row">
              <div className="col-6 col-lg-3 mt-4">{this.renderResults()}</div>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default SearchResult;

Upvotes: 1

Mike
Mike

Reputation: 617

This is exactly the type of problem Redux Solves without using Redux you will need to store the state on a shared parent component. For example,

class Search extends Component {
  state = {
    searchResult: null,
  };

  handleSearch = searchResult => {
    this.setState({
      searchResult,
    });
  }

  render(){
    const { searchResult, } = this.state;
    if(searchResult === null){
      return (
        <ImgAndForm handleSearch={this.handleSearch} />
      );
    }

    return (
      <SearchResult searchResult={searchResult} />
    );
  }
}

Upvotes: 0

Related Questions