1

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. ;)

Yangshun Tay
  • 49,270
  • 33
  • 114
  • 141
Ash
  • 3,030
  • 3
  • 15
  • 33
  • I think you're using `` in a wrong way. Refer to the answers to [this question](https://stackoverflow.com/questions/43230194/how-to-use-redirect-in-the-new-react-router-dom-of-reactjs) to get an idea. Alternatively, you can use `history.push()` (also given in the link). – Ajay Dabas Jan 10 '20 at 18:08
  • Also, you should only alter the `state` using `setState()` function as opposed to what you're doing in `this.state.results = siteSearch.slice(0, 5).map(searchKey => docs[searchKey.ref]);`. – Ajay Dabas Jan 10 '20 at 18:15
  • 1
    In fact, you should use `setState()` to update `this.state.results` array inside `toggleSearchPressEnter()` function and once done, your app will re-render. In render function, check `if (this.state.results.length > 0)` and if true, `return ;`(taken from the given link) – Ajay Dabas Jan 10 '20 at 18:19
  • Yes. I used it for `changeTerm()`, but forgot to use `setState` when setting the results. Comfort of intellisense. I will try adding the check to `render()` if I have followed you correctly. I tried it earlier, but I wasn't using `setState`. – Ash Jan 10 '20 at 18:30
  • As you said, you tried to add the check previously but you were not using setState. Well, without setState, your app will not re-render on changing the state(that's why you should always use setState instead of directly changing state) and hence, the check would never be performed. Try with setState now. – Ajay Dabas Jan 10 '20 at 18:38
  • Thanks, @AjayDabas, I have posted an answer that worked for me. This has been a good learning process, but I think I jumped in a little deeper than I needed to. For some reason `history.push()` would not work for me, but `` was fine. – Ash Jan 11 '20 at 19:43

1 Answers1

1

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;
Ash
  • 3,030
  • 3
  • 15
  • 33