34
loading...
This website collects cookies to deliver better user experience
rails new crud-rails-react
sqlite3
gem and change it to the following:gem 'sqlite3', '~> 1.3.10'
rails g scaffold Beer brand:string style:string country:string quantity:integer & rake db:migrate
Beer.create(brand: 'Double Stout', style: 'Stout', country: 'England', quantity: 54)
Beer.create(brand: 'Spaten', style: 'Helles', country: 'Germany', quantity: 3)
Beer.create(brand: 'Newcastle', style: 'Brown ale', country: 'UK', quantity: 12)
rake db:seed
gem 'webpacker', '~> 4.3.x'
bundle install
bundle exec rake webpacker:install
bundle exec rake webpacker:install:react
node_modules
and the package.json
) will also be created.rails s
command, this will be the result:yarn add antd react-router-dom
react
library since react-router-dom
will do it.{
"dependencies": {
"@babel/preset-react": "^7.12.1",
"@rails/webpacker": "4.3.0",
"antd": "^4.7.2",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"prop-types": "^15.7.2",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-router-dom": "^5.2.0"
},
"devDependencies": {
"webpack-dev-server": "^3.11.0"
}
}
BeersController
.class BeersController < ApplicationController
def index
end
end
index
method.Rails.application.routes.draw do
root 'beers#index'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
root
config. Yes, we’re mapping the root’s endpoint to the beers
index method.BeersController
but with some slight changes. To create it, run the following command:rails generate controller api/v1/Beers
class Api::V1::BeersController < ApplicationController
before_action :set_beer, only: [:show, :edit, :update, :destroy]
# GET /beers
# GET /beers.json
def index
@beers = Beer.all.order(brand: :asc)
render json: @beers
end
# GET /beers/1
# GET /beers/1.json
def show
if @beer
render json: @beer
else
render json: @beer.errors
end
end
# GET /beers/new
def new
@beer = Beer.new
end
# GET /beers/1/edit
def edit
end
# POST /beers
# POST /beers.json
def create
@beer = Beer.new(beer_params)
if @beer.save
render json: @beer
else
render json: @beer.errors
end
end
# PATCH/PUT /beers/1
# PATCH/PUT /beers/1.json
def update
end
# DELETE /beers/1
# DELETE /beers/1.json
def destroy
@beer.destroy
render json: { notice: 'Beer was successfully removed.' }
end
private
# Use callbacks to share common setup or constraints between actions.
def set_beer
@beer = Beer.find(params[:id])
end
# Only allow a list of trusted parameters through.
def beer_params
params.permit(:brand, :style, :country, :quantity)
end
end
before_action
snippet will take care of recovering the proper beer object according to the id
parameter within the requests. Only the operations placed into the array after the :only
clause will need this auto-recover feature.Rails.application.routes.draw do
namespace :api do
namespace :v1 do
get 'beers/index'
post 'beers/create'
delete 'beers/:id', to: 'beers#destroy'
end
end
root 'beers#index'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
destroy
method if it’s not explicitly set in the path.<Layout>
<Header />
<Content>...</Content>
<Footer>Honeybadger ©2020.</Footer>
</Layout>
class Beers extends React.Component {}
React.Component
class, have a lifecycle, and provide utilization methods to execute code before initialization, rendering, and destroying phases.render()
, which is called every time the component is updated.Beer
component above would be represented in a function as follows:const Beers = () => <div>My Beers</div>;
<head>
tag of your app/views/layouts/application.html.erb file:<%= javascript_pack_tag 'index' %>
import React from "react";
import { render } from "react-dom";
import App from "../components/App";
document.addEventListener("DOMContentLoaded", () => {
render(<App />, document.body.appendChild(document.createElement("div")));
});
render
function.App
tag maps the top component of our hierarchy. So, let’s create it as index.jsx under the javascript/components folder (create the folders manually in case they don’t exist yet) and place the following code into it:import React from "react";
import Routes from "../routes/index";
import "antd/dist/antd.css";
export default () => <>{Routes}</>;
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";
export default (
<Router>
<Switch>
<Route path="/" exact component={Home} />
</Switch>
</Router>
);
Route
tag. The path
parameter must match the URI of each route endpoint, while the component
param indicates the component to which the React Router should redirect the request.Home
component here, which doesn’t exist yet. So, let’s create it as Home.jsx under the components folder. Then, add the following code to it:import { Layout } from "antd";
import React from "react";
import Beers from "./Beers";
import Header from "./Header";
const { Content, Footer } = Layout;
export default () => (
<Layout className="layout">
<Header />
<Content style={{ padding: "0 50px" }}>
<div className="site-layout-content" style={{ margin: "100px auto" }}>
<h1>Beer Catalog</h1>
<Beers />
</div>
</Content>
<Footer style={{ textAlign: "center" }}>Honeybadger ©2020.</Footer>
</Layout>
);
Layout
, Header
, Content,
and Footer
.import React from "react";
import { Layout, Menu } from "antd";
const { Header } = Layout;
export default () => (
<Header>
<div className="logo" />
<Menu theme="dark" mode="horizontal" defaultSelectedKeys={["1"]}>
<Menu.Item key="1">Home</Menu.Item>
<Menu.Item key="2">Our Services</Menu.Item>
<Menu.Item key="3">Contact</Menu.Item>
</Menu>
</Header>
);
defaultSelectedKeys
, an array that tells the menu which items are active.Beers
component.state
object. This object acts as a store directly attached to the given component. Each component has its own state
object, and every time you change the state, the component gets re-rendered.Beers
component is to display the listing on a table. For this purpose, we’ll need to hold this list in an array:state = {
beers: [],
};
loadBeers = () => {
const url = "api/v1/beers/index";
fetch(url)
.then((data) => {
if (data.ok) {
return data.json();
}
throw new Error("Network error.");
})
.then((data) => {
data.forEach((beer) => {
const newEl = {
key: beer.id,
id: beer.id,
brand: beer.brand,
style: beer.style,
country: beer.country,
quantity: beer.quantity,
};
this.setState((prevState) => ({
beers: [...prevState.beers, newEl],
}));
});
})
.catch((err) => message.error("Error: " + err));
};
then
checks if the response status equals OK.Error
.Then
, we iterate over the array of results to compose our own beer object and add to the state’s beers
array.catch
block will capture the exception and exhibit it as a message alert.columns = [
{
title: "Brand",
dataIndex: "brand",
key: "brand",
},
...{
title: "",
key: "action",
render: (_text, record) => (
<Popconfirm title="Are you sure to delete this beer?" onConfirm={() => this.deleteBeer(record.id)} okText="Yes" cancelText="No">
<a href="#" type="danger">
Delete{" "}
</a>
</Popconfirm>
),
},
];
title
attribute receives the name of the column, while the dataIndex
name is how it’ll be known within React components, and the key
is its unique identifier.deleteBeer = (id) => {
const url = `api/v1/beers/${id}`;
fetch(url, {
method: "delete",
})
.then((data) => {
if (data.ok) {
this.reloadBeers();
return data.json();
}
throw new Error("Network error.");
})
.catch((err) => message.error("Error: " + err));
};
method
passed as the second parameter of the fetch
method. Plus, within the then
clause, we call the reloadBeers
function, which will re-fetch all the beers from the back-end once again.reloadBeers = () => {
this.setState({ beers: [] });
this.loadBeers();
};
beers
array and calling the load function again.import { Table, message, Popconfirm } from "antd";
import React from "react";
import AddBeerModal from "./AddBeerModal";
class Beers extends React.Component {
columns = [
{
title: "Brand",
dataIndex: "brand",
key: "brand",
},
{
title: "Style",
dataIndex: "style",
key: "style",
},
{
title: "Country",
dataIndex: "country",
key: "country",
},
{
title: "Quantity",
dataIndex: "quantity",
key: "quantity",
},
{
title: "",
key: "action",
render: (_text, record) => (
<Popconfirm title="Are you sure to delete this beer?" onConfirm={() => this.deleteBeer(record.id)} okText="Yes" cancelText="No">
<a href="#" type="danger">
Delete{" "}
</a>
</Popconfirm>
),
},
];
state = {
beers: [],
};
componentDidMount() {
this.loadBeers();
}
loadBeers = () => {
const url = "api/v1/beers/index";
fetch(url)
.then((data) => {
if (data.ok) {
return data.json();
}
throw new Error("Network error.");
})
.then((data) => {
data.forEach((beer) => {
const newEl = {
key: beer.id,
id: beer.id,
brand: beer.brand,
style: beer.style,
country: beer.country,
quantity: beer.quantity,
};
this.setState((prevState) => ({
beers: [...prevState.beers, newEl],
}));
});
})
.catch((err) => message.error("Error: " + err));
};
reloadBeers = () => {
this.setState({ beers: [] });
this.loadBeers();
};
deleteBeer = (id) => {
const url = `api/v1/beers/${id}`;
fetch(url, {
method: "delete",
})
.then((data) => {
if (data.ok) {
this.reloadBeers();
return data.json();
}
throw new Error("Network error.");
})
.catch((err) => message.error("Error: " + err));
};
render() {
return (
<>
<Table className="table-striped-rows" dataSource={this.state.beers} columns={this.columns} pagination={{ pageSize: 5 }} />
<AddBeerModal reloadBeers={this.reloadBeers} />
</>
);
}
}
export default Beers;
Table
component and AddBeerModal
(the modal form we’ll create in a few minutes).pagination
object. The only property we’re adding here is the size of each page (5 results per page).dataSource
attribute receives the list of beers we’ve mounted from the back-end, and the columns
attribute receives the metadata we’ve already built.showModal
and handleCancel
deal with the modal’s opening and closing.onFinish
is triggered when we submit the form.state = {
visible: false,
};
this.setState({
visible: true,
});
onFinish = (values) => {
const url = "api/v1/beers/";
fetch(url, {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
})
.then((data) => {
if (data.ok) {
this.handleCancel();
return data.json();
}
throw new Error("Network error.");
})
.then(() => {
this.props.reloadBeers();
})
.catch((err) => console.error("Error: " + err));
};
headers
attribute must be informed.import { Button, Form, Input, Modal, Select } from "antd";
import React from "react";
const { Option } = Select;
class AddBeerModal extends React.Component {
formRef = React.createRef();
state = {
visible: false,
};
onFinish = (values) => {
const url = "api/v1/beers/";
fetch(url, {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(values),
})
.then((data) => {
if (data.ok) {
this.handleCancel();
return data.json();
}
throw new Error("Network error.");
})
.then(() => {
this.props.reloadBeers();
})
.catch((err) => console.error("Error: " + err));
};
showModal = () => {
this.setState({
visible: true,
});
};
handleCancel = () => {
this.setState({
visible: false,
});
};
render() {
return (
<>
<Button type="primary" onClick={this.showModal}>
Create New +
</Button>
<Modal title="Add New Beer ..." visible={this.state.visible} onCancel={this.handleCancel} footer={null}>
<Form ref={this.formRef} layout="vertical" onFinish={this.onFinish}>
<Form.Item name="brand" label="Brand" rules={[{ required: true, message: "Please input your beer brand!" }]}>
<Input placeholder="Input your beer brand" />
</Form.Item>
<Form.Item name="style" label="Style" rules={[{ required: true, message: "Please input your beer style!" }]}>
<Input placeholder="Input your beer style" />
</Form.Item>
<Form.Item
name="country"
label="Country"
rules={[
{
required: true,
message: "Please input the country of the beer!",
},
]}
>
<Select showSearch placeholder="Select your beer country" optionFilterProp="children" style={{ width: "100%" }}>
<Option value="Finland">Finland</Option>
<Option value="Germany">Germany</Option>
<Option value="Netherlands">Netherlands</Option>
<Option value="UK">UK</Option>
<Option value="USA">USA</Option>
<Option value="Other">Other</Option>
</Select>
</Form.Item>
<Form.Item name="quantity" label="Quantity" rules={[{ required: true, message: "Please input the quantity!" }]}>
<Input type="number" placeholder="How many beers you desire?" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</Modal>
</>
);
}
}
export default AddBeerModal;
rules
attribute. You can customize the message it’ll display in case the user submits the form without filling it properly:Select
component, which translates a combo box. See how easy it is to create complex components by just providing the right attributes. For example, if you want to make your select searchable, just put the showSearch
property, there and it’s done:.site-layout-content {
background: #fff;
padding: 24px;
min-height: 380px;
}
.logo {
width: 200px;
min-height: 31px;
margin: 16px 24px 16px 0;
float: left;
background-image: url(https://www.honeybadger.io/images/navbar_logo.svg?1602785015);
background-repeat: no-repeat;
}
.table-striped-rows th,
.table-striped-rows td {
border-bottom: 1px solid #dedddd !important;
}
.table-striped-rows tr:nth-child(2n) td {
background-color: #fbfbfb;
}
.table-striped-rows thead {
background-color: #f1f1f1;
}
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
end
rails s
command, access the http://localhost:3000/ address, and play around with the CRUD.edit
method at the API controller to receive the updated beer info and perform the update to the database. For the view, another modal would suit very well to accommodate the edit’s form.