实现搜索栏按钮和文件树组件

时间:2018-06-14 14:40:31

标签: javascript reactjs

在这个应用程序中,我有一个搜索引擎,它应该在文件树中查找所需的内容。目的是,在搜索某些内容并单击搜索按钮时,文件树将过滤并显示您要查找的内容。

现在,我有一个console.log()(在onSubmitSearch(e)内检查<SearchEngine/>),告诉我在点击“搜索”按钮时要求搜索的内容。唯一缺少的是实际在文件树中查找的搜索。我该怎么做?

请检查附带的工作代码。

谢谢!

&#13;
&#13;
/**** 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"));
&#13;
* {
  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 **/
&#13;
<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>
&#13;
&#13;
&#13;

1 个答案:

答案 0 :(得分:2)

您的问题是数据传播。您将数据提供给FileTree组件,并且您的SearchEngine与FileTree组件处于同一级别,并且它无法访问必须过滤的数据。我将数据提升到App Component - SearchEngine和FileTree Component的父级,并将数据传播到FileTree。而不是将数据传播到SearchEngine - 我将onSubmitSearch事件处理程序提升到App Component并将其传播到SearchEngine,因为即使我将数据提供给SearcEngine,我也无法在FileTree组件上更新它(因为单向数据流)。

// 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>
    );
  }
}

请注意,App现在具有onSubmitSearch函数,然后传播到SearchEngine,并使用搜索输入值调用它:

/**** 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>
    );
  }
}

FileTree组件现在获取过滤数据(通过搜索输入值/搜索引擎),并且只关注渲染FileTree组件。

使用提交按钮查看过滤树结构的工作示例:https://codesandbox.io/s/3rnvv0kln6