Today, I am going to explain how to implement search function on my own.
Purposes of this article creation
- To be able to create function on my own.
Way of thinking
- Create search API in back-end
- Create search bar in front-end
- search input column
- search model select(User, Task)
- search target select(perfect, partial)
- Call search API in front-end
- Return search results to front-end in back-end
- Display search results in front-end
Implementation
Prerequisite
- To use Docker and docker-compose.
- To create User and Task model.
※Since some other functions have already been created, we have omitted sections that are not relevant to the content of this article.
【docker-compose.yml】
version: '3'
services:
front:
build: .
volumes:
- .:/grow
- node_modules:/grow/node_modules
command: sh -c "yarn start"
ports:
- "3001:3000"
tty: true
volumes:
node_modules:
【Dockerfile】
FROM node:16-alpine
RUN mkdir /grow
ENV FRONT_ROOT /grow
WORKDIR $FRONT_ROOT
COPY ./package.json $FRONT_ROOT/package.json
COPY ./node_modules $FRONT_ROOT/node_modules
RUN npm install -g n && yarn install
RUN npm install -g create-react-app
RUN npm install --silent
RUN npm install --save dotenv
ADD . $FRONT_ROOT
【db/schema.rb】
ActiveRecord::Schema.define(version: 2022_06_01_132549) do
・・・Abbr.・・・
create_table "tasks", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.string "title", null: false
t.text "content"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "user_id"
t.integer "status"
t.string "start_date"
t.string "end_date"
end
create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.string "nickname"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "email"
t.string "firebase_id"
t.text "bio"
t.string "user_name"
t.index ["user_name"], name: "index_users_on_user_name", unique: true
end
・・・Abbr.・・・
end
【config/routes.rb】
Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
post "/users", to: "users#create"
get "/users/:id", to: "users#show"
put "/users/:id", to: "users#update"
post "/users/sign_in", to: "sessions#create"
resources :tasks do
・・・Abbr.・・・
end
resources :users, only: [:show] do
・・・Abbr.・・・
end
・・・Abbr.・・・
resources :searches, only: [:index]
end
Create search bar and call search API and display search results in front-end
【src/components/containers/pages/search/index.jsx】
import React from 'react';
import { SearchTemplate } from '../../templates/search';
export function SearchIndex() {
return <SearchTemplate />;
}
【src/components/containers/templates/search/index.jsx】
import React from 'react';
import styled from 'styled-components';
import { Header } from '../../organisms/header';
import { SearchList } from '../../organisms/search/searchList';
const Main = styled.main`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 30px 10px;
text-align: center;
background-color: #f8f7f3;
`;
export function SearchTemplate() {
return (
<>
<Header />
<Main>
<SearchList />
</Main>
</>
);
}
【src/components/containers/organisms/search/searchList.jsx】
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Title } from '../../../presentational/atoms/Title/title';
import { BackButton } from '../../../presentational/atoms/Button/backButton';
import { getSearches } from '../../../../infra/api';
const ListCover = styled.div`
position: relative;
min-width: 180px;
margin-top: 30px;
`;
const ListHeader = styled.div`
display: flex;
width: 100%;
> h2 {
width: 100%;
margin-right: 45px;
}
`;
const FormCover = styled.div`
min-width: 260px;
padding: 0 10px;
text-align: left;
`;
const List = styled.div`
display: flex;
align-items: center;
text-align: left;
padding-bottom: 10px;
border-bottom: 1px solid #ddd;
&:not(:first-of-type) {
margin-top: 10px;
}
`;
export function SearchList() {
const [load, setLoad] = useState(false);
const [searchResults, setSearchResults] = useState([]);
const handleSubmit = (e) => {
e.preventDefault();
setLoad(true);
const { contents } = e.target.elements;
const { model } = e.target.elements;
const { method } = e.target.elements;
const searchData = { contents: contents.value, model: model.value, method: method.value };
let isMounted = true;
getSearches(searchData)
.then((response) => {
if (isMounted) setSearchResults(response.data.results);
})
.catch();
// .catch(() => {
// });
setLoad(false);
return () => {
isMounted = false;
};
};
return (
<>
<ListHeader>
<BackButton />
<Title title="Search List" />
</ListHeader>
<div>
<FormCover>
<form onSubmit={handleSubmit}>
<label htmlFor="contents">
<input name="contents" type="contents" placeholder="Contents" />
</label>
<select name="model">
<option value="user">User</option>
<option value="task">Task</option>
</select>
<select name="method">
<option value="perfect">Perfect</option>
<option value="partial">Partial</option>
</select>
<button type="submit" disabled={load}>
Search
</button>
</form>
</FormCover>
<ListCover>
{searchResults.map((result) => (
<>
{result.nickname && (
<List>
nickname:
<Link to={`/users/${result.id}`}>{result.nickname}</Link>
</List>
)}
{result.title && (
<List>
title:
<Link to={`/users/${result.user_id}/tasks/${result.id}`}>{result.title}</Link>
<div>cotent:{result.content}</div>
</List>
)}
</>
))}
</ListCover>
</div>
</>
);
}
【src/infra/api.js】
import axios from 'axios';
・・・Abbr.・・・
// searches
export const getSearches = (params) =>
axios({
method: 'get',
url: `/searches`,
params,
});
【src/App.js】
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
・・・Abbr.・・・
// Search
import { SearchIndex } from './components/containers/pages/search';
・・・Abbr.・・・
export function App() {
const [loading, setLoading] = useState(true);
useEffect(() => {
// setTimeout(() => setLoading(false), 1000)
setLoading(false);
if (
localStorage.getItem('token') === '' ||
localStorage.getItem('token') === null ||
Date.now() >= jwtDecode(localStorage.getItem('token')).exp * 1000
) {
signOut(auth);
}
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<Wrapper>
<PageWrapper>
<AuthProvider>
<Router>
<Routes>
・・・Abbr.・・・
{/* Searches */}
<Route
exact
path="/search"
element={<PrivateRoute element={<SearchIndex />} />}
/>
</Routes>
</Router>
</AuthProvider>
</PageWrapper>
</Wrapper>
);
}
Create search API and return search results to front-end in back-end
【app/controllers/searches_controller.rb】
class SearchesController < ApplicationController
def index
@contents = params[:contents]
@model = params[:model]
@method = params[:method]
@results = search_for(@model, @contents, @method)
render json: { results: @results }
end
private
def search_for(model, contents, method)
if model == 'user'
if method == 'perfect'
User.where(name: contents)
else
User.where('name LIKE ?', '%'+contents+'%')
end
elsif model == 'task'
if method == 'perfect'
Task.where(title: contents)
Task.where(content: contents)
else
Task.where('title LIKE ?', '%'+contents+'%')
Task.where('content LIKE ?', '%'+contents+'%')
end
end
end
end
My code
References
- 「【Ruby on Rails】検索機能(モデル、方法選択式)」:https://qiita.com/japwork/items/e6ee225970b50ea5d796
Summary/What I learned this time
This time, I had a pretty good understanding of how to call APIs using React and RailsAPI. I would like to implement more and more functions on my own, from simple to difficult ones! I might be able to do implementations on my own than I thought! It gave me confidence!