使用 ReactJS 向 Rails 后端提交表单

Submit a form to Rails backend with ReactJS

这是我第一次在 Rails 解决方案中使用 ReactJS,任务是紧急请求,没有给我足够的时间在开始冲刺之前学习 React。后端是一个 JSON (API) 解决方案,我已经能够编写一个 ReactJS 来呈现 GET 端点以列出数据库中的任务。但是,当我到了为 POST 端点提交带有 ReactJS 的表单时,什么也没有发生。我的输入没有保存到数据库中,并且表单没有消失到任务列表端点。我不知道我做错了什么。但是,下面是我的解决方案的代码片段。

controller/api/v1/tasks_controller.rb

# frozen_string_literal: true

module Api
  module V1
    class TasksController < ApiController
      include StrongParameters

      def index
        @task = Task.all.load
        render json: @task
      end

      def create
        @task = Task.create!(create_action_params)
        if @task
          render json: @task
        else
          render json: @task.errors
        end
      end

      private

      def create_action_params
        params.require(:task).permit(permitted_task_attributes)
      end
    end
  end
end

config/route.rb

# frozen_string_literal: true

Rails.application.routes.draw do
  # For React
  root 'homepage#index'
  get '/new_task' => 'homepage#index'

  # For API Backend
  namespace :api do
    namespace :v1 do
      get 'tasks/index'
      post 'tasks/create'
    end
  end
end

model/task.rb

# frozen_string_literal: true

class Task < ApplicationRecord
  validates :avatar_url, presence: true
  validates :description, presence: true
end

views/homepage/index.htnl.erb

这是空文件

views/layout/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>Title</title>
    <meta name="viewport" content="width=device-width,initial-scale=1, shrink-to-fit=no">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
        <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'Index' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

app/javascript/components/App.jsx

import React from "react";
import Routes from "../routes/Index";

export default props => <>{Routes}</>;

app/javascript/components/NewTask.jsx

import React from "react";
import {Link} from "react-router-dom";

class NewTask extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            avatar_url: props.post.avatar_url,
            description: props.post.description
        };

        this.onChange = this.onChange.bind(this);
        this.onSubmit = this.onSubmit.bind(this);
        this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
    }

    stripHtmlEntities(str) {
        return String(str)
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
    }

    onChange(event) {
        this.setState({ [event.target.avatar_url]: event.target.value });
    }

    onSubmit(event) {
        event.preventDefault();
        const url = "api/v1/tasks/create";
        const { avatar_url, description } = this.state;

        if (avatar_url.length === 0 || description.length === 0)
            return;

        const body = {
            avatar_url,
            description: description.replace(/\n/g, "<br> <br>")
        };

        const token = document.querySelector('meta[name="csrf-token"]').content;
        fetch(url, {
            method: "POST",
            headers: {
                "X-CSRF-Token": token,
                "Content-Type": "application/json"
            },
            body: JSON.stringify(body)
        })
            .then(response => {
                if (response.ok) {
                    return response.json();
                }
                throw new Error("Network response was not ok.");
            })
            .then(response => this.props.history.push(`/new_task/${response.id}`))
            .catch(error => console.log(error.message));
    }

    render() {
        return (
            <>
                <nav className="navbar navbar-expand-lg navbar-dark task-navbar-color">
                    <div className="container">
                        <div className="navbar-header">
                            <p className="navbar-brand">Add Task</p>
                        </div>
                        <div>
                            <ul className="nav navbar-nav navbar-right">
                                <li>
                                    <Link to="/">
                                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
                                             fill="currentColor" className="bi bi-arrow-left text-white" viewBox="0 0 16 16">
                                            <path fill-rule="evenodd"
                                                  d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/>
                                        </svg>
                                    </Link>
                                </li>
                            </ul>
                        </div>
                    </div>
                </nav>
                <div className="container mt-5">
                    <div className="row">
                        <div className="col-sm-12 col-lg-6 offset-lg-3">
                            <h1 className="font-weight-normal mb-5">
                                Add a new task to our awesome task collection.
                            </h1>
                            <form onSubmit={this.onSubmit}>
                                <div className="form-group">
                                    <label htmlFor="taskAvatar">Avatar URL</label>
                                    <input
                                        type="url"
                                        name="task[avatar_url]"
                                        value={this.state.value}
                                        className="form-control"
                                        required
                                        onChange={this.onChange}
                                    />
                                </div>
                                <label htmlFor="description">Task Description</label>
                                <textarea
                                    className="form-control"
                                    name="task[description]"
                                    value={this.state.value}
                                    rows="5"
                                    required
                                    onChange={this.onChange}
                                />
                                <button type="submit" value="Save" className="btn btn-primary mt-3">
                                    Add Task
                                </button>
                                <Link to="/" className="btn btn-link mt-3">
                                    Back to tasks
                                </Link>
                            </form>
                        </div>
                    </div>
                </div>
            </>
        );
    }
}

export default NewTask;

app/javascript/packs/Index.jsx

import React from "react";
import { render } from "react-dom";
import 'bootstrap/dist/css/bootstrap.min.css';
import $ from 'jquery';
import Popper from 'popper.js';
import 'bootstrap/dist/js/bootstrap.bundle.min';
import App from "../components/App";

document.addEventListener("DOMContentLoaded", () => {
  render(
    <App />,
    document.body.appendChild(document.createElement("div"))
  );
});

app/javascript/routes/Index.jsx

import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Task from "../components/Task";
import NewTask from "../components/NewTask";

export default <Router>
  <Switch>
    <Route path="/" exact component={Task} />
    <Route path="/new_task" exact component={NewTask} />
  </Switch>
</Router>;

当我在 Postman 和 Insomnia 上调用 API 端点时,它们按预期运行。但是当我从 ReactJS 表单提交输入时(即 NewTask.jsx),没有任何内容保存到数据库中。

我做错了什么。我希望有人能帮助我解决这个问题。

显然,问题在于 onSubmit 事件方法,因为它针对 avatar_url 而不是名为 name 的表单输入属性。所以这就是我用来让它工作的方法。

  1. 将 onChange 事件更改为:

    onChange(event) {
        this.setState({ [event.target.name]: event.target.value });
    }
    
  2. 将表单输入属性更正为:

    <form onSubmit={this.onSubmit}>
                            <div className="form-group">
                                <label htmlFor="avatar_url">Avatar URL</label>
                                <input
                                    type="url"
                                    name="avatar_url"
                                    value={this.state.value}
                                    className="form-control"
                                    required
                                    onChange={this.onChange}
                                />
                            </div>
                            <label htmlFor="description">Task Description</label>
                            <textarea
                                className="form-control"
                                name="description"
                                value={this.state.value}
                                rows="5"
                                required
                                onChange={this.onChange}
                            />
                            <button type="submit" value="Save" className="btn btn-primary mt-3">
                                Add Task
                            </button>
                            <Link to="/" className="btn btn-link mt-3">
                                Back to tasks
                            </Link>
                        </form>