O autor selecionou a Electronic Frontier Foundation para receber uma doação como parte do programa Write for DOnations.
Introdução
O Ruby on Rails é um popular framework de aplicativos web no lado do servidor. Ele impulsiona muitas aplicações populares que existem na web hoje, como GitHub, Basecamp, SoundCloud, Airbnb e Twitch. Com ênfase na experiência do programador e na comunidade apaixonada construída ao seu redor, o Ruby on Rails fornecerá as ferramentas necessárias para construir e manter sua aplicação web moderna.
O React é uma biblioteca JavaScript usada para criar interfaces de usuário no front-end. Apoiado pelo Facebook, é uma das bibliotecas front-end mais populares usadas na web hoje. O React oferece recursos como um modelo de objeto de documento virtual (DOM), arquitetura de componentes e gerenciamento de estado, que tornam o processo de desenvolvimento front-end mais organizado e eficiente.
Com o frontend da web movendo-se em direção a frameworks separados do código do lado do servidor, combinar a elegância do Rails com a eficiência do React permitirá que você construa aplicativos poderosos e modernos informados pelas tendências atuais. Ao usar o React para renderizar componentes de dentro de uma visualização do Rails (em vez do motor de templates do Rails), seu aplicativo se beneficiará dos últimos avanços em JavaScript e desenvolvimento frontend, enquanto aproveita a expressividade do Ruby on Rails.
Neste tutorial, você criará uma aplicação Ruby on Rails que armazena suas receitas favoritas e depois as exibe com um frontend React. Quando terminar, você poderá criar, visualizar e excluir receitas usando uma interface React estilizada com Bootstrap:
Pré-requisitos
Para seguir este tutorial, você precisa:
-
Node.js e npm instalados na sua máquina de desenvolvimento. Este tutorial utiliza a versão 16.14.0 do Node.js e a versão 8.3.1 do npm. Node.js é um ambiente de tempo de execução JavaScript que permite executar seu código fora do navegador. Ele vem com um Gerenciador de Pacotes pré-instalado chamado npm, que permite instalar e atualizar pacotes. Para instalar esses no Ubuntu 20.04 ou macOS, siga a seção “Instalando Usando um PPA” de Como Instalar o Node.js no Ubuntu 20.04 ou os passos em Como Instalar o Node.js e Criar um Ambiente de Desenvolvimento Local no macOS.
-
O Gerenciador de Pacotes Yarn instalado na sua máquina de desenvolvimento, que permitirá baixar o framework React. Este tutorial foi testado na versão 1.22.10; para instalar essa dependência, siga o guia de instalação oficial do Yarn.
-
O Ruby on Rails está instalado. Para conseguir isso, siga nosso guia em Como Instalar o Ruby on Rails com rbenv no Ubuntu 20.04. Se você deseja desenvolver esta aplicação no macOS, pode utilizar Como Instalar o Ruby on Rails com rbenv no macOS. Este tutorial foi testado na versão 3.1.2 do Ruby e na versão 7.0.4 do Rails, portanto, certifique-se de especificar essas versões durante o processo de instalação.
Observação: A versão 7 do Rails não é compatível com versões anteriores. Se estiver usando a versão 5 do Rails, por favor, visite o tutorial Como Configurar um Projeto Ruby on Rails v5 com um Frontend React no Ubuntu 18.04.
- O PostgreSQL está instalado, conforme descrito nos Passos 1 e 2 Como usar o PostgreSQL com sua aplicação Ruby on Rails no Ubuntu 20.04 ou Como usar o PostgreSQL com sua aplicação Ruby on Rails no macOS. Para seguir este tutorial, você pode usar a versão 12 ou superior do PostgreSQL. Se você deseja desenvolver esta aplicação em uma distribuição diferente do Linux ou outro sistema operacional, consulte a página oficial de downloads do PostgreSQL. Para obter mais informações sobre como usar o PostgreSQL, consulte Como instalar e usar o PostgreSQL.
Passo 1 — Criando uma Nova Aplicação Rails
Você vai construir sua aplicação de receitas no framework de aplicação Rails nesta etapa. Primeiro, você irá criar uma nova aplicação Rails, que será configurada para funcionar com React.
O Rails fornece vários scripts chamados geradores que criam tudo o que é necessário para construir uma aplicação web moderna. Para revisar uma lista completa desses comandos e o que eles fazem, execute o seguinte comando em seu terminal:
- rails -h
Este comando irá gerar uma lista abrangente de opções, permitindo que você defina os parâmetros da sua aplicação. Um dos comandos listados é o comando new
, que cria uma nova aplicação Rails.
Agora, você irá criar uma nova aplicação Rails usando o gerador new
. Execute o seguinte comando no seu terminal:
- rails new rails_react_recipe -d postgresql -j esbuild -c bootstrap -T
O comando anterior cria uma nova aplicação Rails em um diretório chamado rails_react_recipe
, instala as dependências Ruby e JavaScript necessárias, e configura o Webpack. As flags associadas a este comando do gerador new
incluem o seguinte:
- A flag
-d
especifica o motor de banco de dados preferido, que neste caso é o PostgreSQL. - A flag
-j
especifica a abordagem JavaScript da aplicação. O Rails oferece algumas maneiras diferentes de lidar com o código JavaScript em aplicações Rails. A opçãoesbuild
passada para a flag-j
instrui o Rails a pré-configurar esbuild como o empacotador JavaScript preferido. - A flag
-c
especifica o processador de CSS da aplicação. Bootstrap é a opção preferida neste caso. - A flag
-T
instrui o Rails a pular a geração de arquivos de teste, já que você não estará escrevendo testes para este tutorial. Este comando também é sugerido se você deseja usar uma ferramenta de teste Ruby diferente daquela que o Rails fornece.
Assim que o comando terminar, vá para o diretório rails_react_recipe
, que é o diretório raiz da sua aplicação:
- cd rails_react_recipe
Em seguida, liste o conteúdo do diretório:
- ls
Os conteúdos serão impressos de forma semelhante a isto:
OutputGemfile README.md bin db node_modules storage yarn.lock
Gemfile.lock Rakefile config lib package.json tmp
Procfile.dev app config.ru log public vendor
Este diretório raiz possui vários arquivos e pastas gerados automaticamente que compõem a estrutura de uma aplicação Rails, incluindo um arquivo package.json
contendo dependências para uma aplicação React.
Agora que você criou com sucesso uma nova aplicação Rails, você irá conectá-la a um banco de dados no próximo passo.
Passo 2 — Configurando o Banco de Dados
Antes de executar sua nova aplicação Rails, é necessário conectá-la a um banco de dados. Neste passo, você irá conectar a aplicação Rails recém-criada a um banco de dados PostgreSQL para que os dados da receita possam ser armazenados e recuperados conforme necessário.
O arquivo database.yml
encontrado em config/database.yml
contém detalhes do banco de dados, como nomes de banco de dados para diferentes ambientes de desenvolvimento. O Rails especifica um nome de banco de dados para os vários ambientes de desenvolvimento, acrescentando um sublinhado (_
) seguido pelo nome do ambiente. Neste tutorial, você usará os valores padrão de configuração do banco de dados, mas pode alterar esses valores de configuração, se necessário.
Nota: Neste ponto, você pode alterar config/database.yml
para definir qual papel do PostgreSQL você gostaria que o Rails usasse para criar seu banco de dados. Durante os pré-requisitos, você criou um papel que está protegido por uma senha no tutorial Como Usar o PostgreSQL com Sua Aplicação Ruby on Rails. Se você ainda não definiu o usuário, agora pode seguir as instruções para o Passo 4 — Configurando e Criando Seu Banco de Dados no mesmo tutorial de pré-requisitos.
O Rails oferece muitos comandos que facilitam o desenvolvimento de aplicações web, incluindo comandos para trabalhar com bancos de dados como create
, drop
e reset
. Para criar um banco de dados para sua aplicação, execute o seguinte comando em seu terminal:
- rails db:create
Este comando cria um banco de dados development
e test
, produzindo a seguinte saída:
OutputCreated database 'rails_react_recipe_development'
Created database 'rails_react_recipe_test'
Agora que a aplicação está conectada a um banco de dados, inicie a aplicação executando o seguinte comando:
- bin/dev
O Rails fornece um script alternativo bin/dev
que inicia uma aplicação Rails executando os comandos no arquivo Procfile.dev
no diretório raiz do aplicativo usando a gema Foreman.
Assim que você executar este comando, seu prompt de comando desaparecerá e a seguinte saída será exibida em seu lugar:
Outputstarted with pid 70099
started with pid 70100
started with pid 70101
yarn run v1.22.10
yarn run v1.22.10
$ esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --watch
$ sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules --watch
=> Booting Puma
=> Rails 7.0.4 application starting in development
=> Run `bin/rails server --help` for more startup options
[watch] build finished, watching for changes...
Puma starting in single mode...
* Puma version: 5.6.5 (ruby 3.1.2-p20) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 70099
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
Sass is watching for changes. Press Ctrl-C to stop.
Para acessar sua aplicação, abra uma janela do navegador e navegue até http://localhost:3000
. A página de boas-vindas padrão do Rails será carregada, o que significa que você configurou corretamente sua aplicação Rails:
Para parar o servidor web, pressione CTRL+C
no terminal onde o servidor está em execução. Você receberá uma mensagem de despedida do Puma:
Output^C SIGINT received, starting shutdown
- Gracefully stopping, waiting for requests to finish
=== puma shutdown: 2019-07-31 14:21:24 -0400 ===
- Goodbye!
Exiting
sending SIGTERM to all processes
terminated by SIGINT
terminated by SIGINT
exited with code 0
Seu prompt do terminal então reaparecerá.
Você configurou com sucesso um banco de dados para sua aplicação de receitas de comida. No próximo passo, você irá instalar as dependências do JavaScript necessárias para montar sua interface de usuário React.
Passo 3 — Instalando Dependências de Frontend
Neste passo, você irá instalar as dependências do JavaScript necessárias no frontend de sua aplicação de receitas de comida. Elas incluem:
- React para construir interfaces de usuário.
- React DOM para permitir que o React interaja com o DOM do navegador.
- React Router para lidar com a navegação em uma aplicação React.
Execute o seguinte comando para instalar esses pacotes com o gerenciador de pacotes Yarn:
- yarn add react react-dom react-router-dom
Este comando utiliza o Yarn para instalar os pacotes especificados e adicioná-los ao arquivo package.json
. Para verificar isso, abra o arquivo package.json
localizado no diretório raiz do projeto:
- nano package.json
Os pacotes instalados serão listados sob a chave dependencies
:
{
"name": "app",
"private": "true",
"dependencies": {
"@hotwired/stimulus": "^3.1.0",
"@hotwired/turbo-rails": "^7.1.3",
"@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.1",
"bootstrap-icons": "^1.9.1",
"esbuild": "^0.15.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"sass": "^1.54.9"
},
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
"build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
}
}
Feche o arquivo pressionando CTRL+X
.
Você instalou algumas dependências front-end para sua aplicação. Em seguida, você irá configurar uma página inicial para sua aplicação de receitas culinárias.
Passo 4 — Configurando a Página Inicial
Com as dependências necessárias instaladas, agora você criará uma página inicial para a aplicação servir como página inicial quando os usuários visitarem a aplicação pela primeira vez.
O Rails segue o padrão arquitetônico Model-View-Controller para aplicações. No padrão MVC, o objetivo de um controlador é receber solicitações específicas e encaminhá-las para o modelo ou visualização apropriados. Atualmente, a aplicação exibe a página de boas-vindas do Rails quando a URL raiz é carregada no navegador. Para alterar isso, você criará um controlador e uma visualização para a página inicial e, em seguida, a associará a uma rota.
O Rails fornece um gerador de controller
para criar um controller. O gerador de controller
recebe um nome de controller e uma ação correspondente. Para mais informações sobre isso, você pode revisar a documentação do Rails.
Este tutorial chamará o controller de Página Inicial
. Execute o seguinte comando para criar um controller Página Inicial
com uma ação index
:
- rails g controller Homepage index
Nota:
No Linux, o erro FATAL: Listen error: unable to monitor directories for changes.
pode resultar de um limite do sistema no número de arquivos que sua máquina pode monitorar para alterações. Execute o seguinte comando para corrigir isso:
- echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
Este comando aumentará permanentemente o número de diretórios que você pode monitorar com Listen
para 524288
. Você pode alterar isso novamente executando o mesmo comando e substituindo 524288
pelo número desejado.
A execução do comando controller
gera os seguintes arquivos:
- A
homepage_controller.rb
file for receiving all homepage-related requests. This file contains theindex
action you specified in the command. - A
homepage_helper.rb
file for adding helper methods related to theHomepage
controller. - Um arquivo
index.html.erb
como a página de visualização para renderizar qualquer coisa relacionada à página inicial.
Além dessas novas páginas criadas pela execução do comando Rails, o Rails também atualiza o arquivo de rotas localizado em config/routes.rb
, adicionando uma rota get
para sua página inicial, que você modificará como sua rota raiz.
A root route in Rails specifies what will show up when users visit the root URL of your application. In this case, you want your users to see your homepage. Open the routes file located at config/routes.rb
in your favorite editor:
- nano config/routes.rb
Neste arquivo, substitua get 'homepage/index'
por root 'homepage#index'
para que o arquivo corresponda ao seguinte:
Rails.application.routes.draw do
root 'homepage#index'
# Para obter detalhes sobre o DSL disponível neste arquivo, consulte http://guides.rubyonrails.org/routing.html
end
Esta modificação instrui o Rails a mapear solicitações para a raiz da aplicação para a ação index
do controlador Homepage
, que por sua vez renderiza no navegador o que estiver no arquivo index.html.erb
localizado em app/views/homepage/index.html.erb
.
Salve e feche o arquivo.
Para verificar se isso está funcionando, inicie sua aplicação:
- bin/dev
Ao abrir ou atualizar a aplicação no navegador, uma nova página inicial para sua aplicação será carregada:
Depois de verificar se sua aplicação está funcionando, pressione CTRL+C
para parar o servidor.
Em seguida, abra o arquivo ~/rails_react_recipe/app/views/homepage/index.html.erb
:
- nano ~/rails_react_recipe/app/views/homepage/index.html.erb
Remova o código dentro do arquivo e, em seguida, salve-o como vazio. Fazendo isso, você garante que o conteúdo de index.html.erb
não interfira na renderização do seu frontend em React.
Agora que você configurou a página inicial para sua aplicação, pode prosseguir para a próxima seção, onde configurará o frontend da sua aplicação para usar React.
Passo 5 — Configurando o React como o Frontend do seu Rails
Neste passo, você configurará o Rails para usar o React no frontend da aplicação, em vez do seu mecanismo de modelo. Esta nova configuração permitirá que você crie uma página inicial mais visualmente atraente com React.
Com a ajuda da opção esbuild
especificada ao gerar a aplicação Rails, a maioria das configurações necessárias para permitir que o JavaScript funcione perfeitamente com o Rails já está em vigor. Tudo o que resta é carregar o ponto de entrada da aplicação React no ponto de entrada do esbuild
para arquivos JavaScript. Para fazer isso, comece criando um diretório de componentes no diretório app/javascript
:
- mkdir ~/rails_react_recipe/app/javascript/components
O diretório components
abrigará o componente para a página inicial, juntamente com outros componentes React na aplicação, incluindo o arquivo de entrada na aplicação React.
Em seguida, abra o arquivo application.js
localizado em app/javascript/application.js
:
- nano ~/rails_react_recipe/app/javascript/application.js
Adicione a linha de código destacada ao arquivo:
// Ponto de entrada para o script de construção no seu package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./components"
A linha de código adicionada ao arquivo application.js
importará o código no arquivo de entrada index.jsx
, tornando-o disponível para esbuild
para empacotamento. Com o diretório /components
importado no ponto de entrada JavaScript da aplicação Rails, você pode criar um componente React para sua página inicial. A página inicial conterá alguns textos e um botão de chamada para ação para visualizar todas as receitas.
Salve e feche o arquivo.
Em seguida, crie um arquivo Home.jsx
no diretório components
:
- nano ~/rails_react_recipe/app/javascript/components/Home.jsx
Adicione o seguinte código ao arquivo:
import React from "react";
import { Link } from "react-router-dom";
export default () => (
<div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">
<div className="jumbotron jumbotron-fluid bg-transparent">
<div className="container secondary-color">
<h1 className="display-4">Food Recipes</h1>
<p className="lead">
A curated list of recipes for the best homemade meal and delicacies.
</p>
<hr className="my-4" />
<Link
to="/recipes"
className="btn btn-lg custom-button"
role="button"
>
View Recipes
</Link>
</div>
</div>
</div>
);
Neste código, você importa o React e o componente Link
do React Router. O componente Link
cria um hiperlink para navegar de uma página para outra. Em seguida, você cria e exporta um componente funcional contendo alguma linguagem de marcação para a sua página inicial, estilizada com classes Bootstrap.
Salve e feche o arquivo.
Com o seu componente Home
configurado, você irá agora configurar o roteamento usando o React Router. Crie um diretório routes
no diretório app/javascript
:
- mkdir ~/rails_react_recipe/app/javascript/routes
O diretório routes
conterá algumas rotas com seus respectivos componentes. Sempre que uma rota específica for carregada, ela renderizará seu componente correspondente no navegador.
No diretório routes
, crie um arquivo index.jsx
:
- nano ~/rails_react_recipe/app/javascript/routes/index.jsx
Adicione o seguinte código a ele:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
export default (
<Router>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Router>
);
Neste arquivo de rota index.jsx
, você importa os seguintes módulos: o módulo React
que permite o uso do React, bem como os módulos BrowserRouter
, Routes
e Route
do React Router, que ajudam na navegação de uma rota para outra. Por último, você importa o seu componente Home
, que será renderizado sempre que uma solicitação corresponder à rota raiz (/
). Quando quiser adicionar mais páginas à sua aplicação, você pode declarar uma rota neste arquivo e associá-la ao componente que deseja renderizar para essa página.
Salve e saia do arquivo.
Agora você configurou o roteamento usando o React Router. Para o React reconhecer as rotas disponíveis e utilizá-las, as rotas devem estar disponíveis no ponto de entrada da aplicação. Para conseguir isso, você vai renderizar suas rotas em um componente que o React vai renderizar no seu arquivo de entrada.
Crie um arquivo App.jsx
no diretório app/javascript/components
:
- nano ~/rails_react_recipe/app/javascript/components/App.jsx
Adicione o seguinte código ao arquivo App.jsx
:
import React from "react";
import Routes from "../routes";
export default props => <>{Routes}</>;
No arquivo App.jsx
, você importa o React e os arquivos de rota que acabou de criar. Então, exporta um componente para renderizar as rotas dentro de fragmentos. Esse componente será renderizado no ponto de entrada da aplicação, tornando as rotas disponíveis sempre que a aplicação for carregada.
Salve e feche o arquivo.
Agora que você configurou o seu App.jsx
, pode renderizá-lo no seu arquivo de entrada. Crie um arquivo index.jsx
no diretório components
:
- nano ~/rails_react_recipe/app/javascript/components/index.jsx
Adicione o seguinte código ao arquivo index.js
:
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
document.addEventListener("turbo:load", () => {
const root = createRoot(
document.body.appendChild(document.createElement("div"))
);
root.render(<App />);
});
Nas linhas de import
, você importa a biblioteca React, a função createRoot
do ReactDOM e seu componente App
. Usando a função createRoot
do ReactDOM, você cria um elemento raiz como um elemento div
anexado à página e renderiza seu componente App
nele. Quando a aplicação for carregada, o React renderizará o conteúdo do componente App
dentro do elemento div
na página.
Salve e saia do arquivo.
Por fim, você vai adicionar alguns estilos CSS à sua página inicial.
Abra o arquivo application.bootstrap.scss
no diretório ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
:
- nano ~/rails_react_recipe/app/assets/stylesheets/application.bootstrap.scss
Em seguida, substitua o conteúdo do arquivo application.bootstrap.scss
pelo seguinte código:
@import 'bootstrap/scss/bootstrap';
@import 'bootstrap-icons/font/bootstrap-icons';
.bg_primary-color {
background-color: #FFFFFF;
}
.primary-color {
background-color: #FFFFFF;
}
.bg_secondary-color {
background-color: #293241;
}
.secondary-color {
color: #293241;
}
.custom-button.btn {
background-color: #293241;
color: #FFF;
border: none;
}
.hero {
width: 100vw;
height: 50vh;
}
.hero img {
object-fit: cover;
object-position: top;
height: 100%;
width: 100%;
}
.overlay {
height: 100%;
width: 100%;
opacity: 0.4;
}
Você definiu algumas cores personalizadas para a página. A seção .hero
criará o esqueleto para uma imagem de destaque ou um grande banner na página inicial do seu site, que você adicionará posteriormente. Além disso, o estilo custom-button.btn
formata o botão que o usuário usará para entrar na aplicação.
Com seus estilos CSS no lugar, salve e saia do arquivo.
Em seguida, reinicie o servidor web da sua aplicação:
- bin/dev
Depois, recarregue a aplicação no seu navegador. Uma nova página inicial será carregada:
Pare o servidor web com CTRL+C
.
Você configurou sua aplicação para usar o React como frontend nesta etapa. Na próxima etapa, você criará modelos e controladores que permitirão criar, ler, atualizar e excluir receitas.
Etapa 6 — Criando o Controlador e Modelo de Receitas
Agora que você configurou um frontend React para sua aplicação, você criará um modelo e controlador de Receita. O modelo de receita representará a tabela do banco de dados contendo informações sobre as receitas do usuário, enquanto o controlador receberá e lidará com solicitações para criar, ler, atualizar ou excluir receitas. Quando um usuário solicita uma receita, o controlador de receitas recebe essa solicitação e a passa para o modelo de receita, que recupera os dados solicitados do banco de dados. O modelo então retorna os dados da receita como resposta para o controlador. Finalmente, essas informações são exibidas no navegador.
Comece criando um modelo `Recipe` usando o subcomando `generate model` fornecido pelo Rails e especificando o nome do modelo junto com suas colunas e tipos de dados. Execute o seguinte comando:
- rails generate model Recipe name:string ingredients:text instruction:text image:string
O comando anterior instrui o Rails a criar um modelo `Recipe` juntamente com uma coluna `name` do tipo `string`, colunas `ingredients` e `instruction` do tipo `text`, e uma coluna `image` do tipo `string`. Este tutorial nomeou o modelo `Recipe`, porque os modelos no Rails usam um nome singular enquanto suas tabelas de banco de dados correspondentes usam um nome plural.
Executar o comando `generate model` cria dois arquivos e imprime a seguinte saída:
Output invoke active_record
create db/migrate/20221017220817_create_recipes.rb
create app/models/recipe.rb
Os dois arquivos criados são:
- A
recipe.rb
file that holds all the model-related logic. - A
20221017220817_create_recipes.rb
file (the number at the beginning of the file may differ depending on the date when you run the command). This migration file contains the instruction for creating the database structure.
Em seguida, você editará o arquivo do modelo de receita para garantir que apenas dados válidos sejam salvos no banco de dados. Você pode conseguir isso adicionando algumas validações de banco de dados ao seu modelo.
Abra o modelo de receita localizado em `app/models/recipe.rb`:
- nano ~/rails_react_recipe/app/models/recipe.rb
Adicione as seguintes linhas de código destacadas ao arquivo:
class Recipe < ApplicationRecord
validates :name, presence: true
validates :ingredients, presence: true
validates :instruction, presence: true
end
Neste código, você adiciona a validação do modelo, que verifica a presença dos campos name
, ingredients
e instruction
. Sem esses três campos, uma receita é inválida e não será salva no banco de dados.
Salve e feche o arquivo.
Para o Rails criar a tabela recipes
no seu banco de dados, você precisa executar uma migração, que é uma maneira de fazer alterações no seu banco de dados programaticamente. Para garantir que a migração funcione com o banco de dados que você configurou, é necessário fazer alterações no arquivo 20221017220817_create_recipes.rb
.
Abra este arquivo no seu editor:
- nano ~/rails_react_recipe/db/migrate/20221017220817_create_recipes.rb
Adicione os materiais destacados para que seu arquivo corresponda ao seguinte:
class CreateRecipes < ActiveRecord::Migration[5.2]
def change
create_table :recipes do |t|
t.string :name, null: false
t.text :ingredients, null: false
t.text :instruction, null: false
t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'
t.timestamps
end
end
end
Este arquivo de migração contém uma classe Ruby com um método change
e um comando para criar uma tabela chamada recipes
juntamente com as colunas e seus tipos de dados. Você também atualiza 20221017220817_create_recipes.rb
com uma restrição NOT NULL
nas colunas name
, ingredients
e instruction
adicionando null: false
, garantindo que essas colunas tenham um valor antes de alterar o banco de dados. Finalmente, você adiciona uma URL de imagem padrão para sua coluna de imagem; esta pode ser outra URL se desejar usar uma imagem diferente.
Com essas alterações, salve e saia do arquivo. Agora você está pronto para executar sua migração e criar sua tabela. No terminal, execute o seguinte comando:
- rails db:migrate
Você usa o comando de migração de banco de dados para executar as instruções no seu arquivo de migração. Uma vez que o comando seja executado com sucesso, você receberá uma saída semelhante à seguinte:
Output== 20190407161357 CreateRecipes: migrating ====================================
-- create_table(:recipes)
-> 0.0140s
== 20190407161357 CreateRecipes: migrated (0.0141s) ===========================
Com seu modelo de receitas no lugar, você irá criar o controlador de receitas para adicionar a lógica de criação, leitura e exclusão de receitas. Execute o seguinte comando:
- rails generate controller api/v1/Recipes index create show destroy --skip-template-engine --no-helper
Neste comando, você cria um controlador Recipes
em um diretório api/v1
com ações index
, create
, show
e destroy
. A ação index
lidará com a obtenção de todas as suas receitas; a ação create
será responsável por criar novas receitas; a ação show
buscará uma única receita, e a ação destroy
terá a lógica para excluir uma receita.
Você também passa algumas flags para tornar o controlador mais leve, incluindo:
--skip-template-engine
, que instrui o Rails a pular a geração de arquivos de visualização do Rails, já que o React cuida das suas necessidades de front-end.--no-helper
, que instrui o Rails a pular a geração de um arquivo de helper para o seu controlador.
A execução do comando também atualiza o arquivo de rotas com uma rota para cada ação no controlador Recipes
.
Quando o comando é executado, ele imprimirá uma saída como esta:
Output create app/controllers/api/v1/recipes_controller.rb
route namespace :api do
namespace :v1 do
get 'recipes/index'
get 'recipes/create'
get 'recipes/show'
get 'recipes/destroy'
end
end
Para usar essas rotas, você fará alterações no seu arquivo config/routes.rb
. Abra o arquivo routes.rb
no seu editor de texto:
- nano ~/rails_react_recipe/config/routes.rb
Atualize este arquivo para se parecer com o seguinte código, alterando ou adicionando as linhas destacadas:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
get 'recipes/index'
post 'recipes/create'
get '/show/:id', to: 'recipes#show'
delete '/destroy/:id', to: 'recipes#destroy'
end
end
root 'homepage#index'
get '/*path' => 'homepage#index'
# Defina as rotas da sua aplicação conforme o DSL em https://guides.rubyonrails.org/routing.html
# Define a rota do caminho raiz ("/")
# root "articles#index"
end
No arquivo de rotas, modifique o verbo HTTP das rotas create
e destroy
para que possam post
e delete
dados. Modifique também as rotas para as ações show
e destroy
adicionando um parâmetro :id
à rota. :id
irá conter o número de identificação da receita que você deseja ler ou excluir.
Adicione uma rota catch-all com get '/*path'
que direcionará qualquer outra solicitação que não corresponda às rotas existentes para a ação index
do controlador homepage
. O roteamento no front-end lidará com solicitações não relacionadas à criação, leitura ou exclusão de receitas.
Salve e saia do arquivo.
Para avaliar a lista de rotas disponíveis em sua aplicação, execute o seguinte comando:
- rails routes
A execução deste comando exibe uma extensa lista de padrões de URI, verbos e controladores ou ações correspondentes para o seu projeto.
Em seguida, você adicionará a lógica para obter todas as receitas de uma vez. O Rails utiliza a biblioteca ActiveRecord para lidar com tarefas relacionadas ao banco de dados, como esta. O ActiveRecord conecta classes a tabelas de banco de dados relacionais e fornece uma API rica para trabalhar com elas.
Para obter todas as receitas, você utilizará o ActiveRecord para consultar a tabela de receitas e buscar todas as receitas no banco de dados.
Abra o arquivo recipes_controller.rb
com o seguinte comando:
- nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb
Adicione as linhas destacadas ao controlador de receitas:
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
end
def show
end
def destroy
end
end
Na sua ação index
, você utiliza o método all
do ActiveRecord para obter todas as receitas no seu banco de dados. Utilizando o método order
, você as ordena em ordem decrescente pela data de criação, o que colocará as receitas mais recentes primeiro. Por fim, você envia a lista de receitas como uma resposta JSON com render
.
Em seguida, você adicionará a lógica para criar novas receitas. Assim como ao buscar todas as receitas, você dependerá do ActiveRecord para validar e salvar os detalhes da receita fornecidos. Atualize o seu controlador de receitas com as seguintes linhas de código destacadas:
class Api::V1::RecipesController < ApplicationController
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
recipe = Recipe.create!(recipe_params)
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def show
end
def destroy
end
private
def recipe_params
params.permit(:name, :image, :ingredients, :instruction)
end
end
No método create
, você utiliza o método create
do ActiveRecord para criar uma nova receita. O método create
pode atribuir todos os parâmetros do controlador ao modelo de uma vez. Esse método facilita a criação de registros, mas abre a possibilidade de uso malicioso. O uso malicioso pode ser evitado ao usar a funcionalidade parâmetros fortes fornecida pelo Rails. Dessa forma, os parâmetros não podem ser atribuídos a menos que tenham sido permitidos. Você passa um parâmetro recipe_params
para o método create
em seu código. O recipe_params
é um método private
onde você permite que os parâmetros do controlador evitem que conteúdo incorreto ou malicioso entre no seu banco de dados. Neste caso, você permite um parâmetro name
, image
, ingredients
e instruction
para o uso válido do método create
.
Seu controlador de receitas agora pode ler e criar receitas. Tudo o que resta é a lógica para ler e excluir uma única receita. Atualize seu controlador de receitas com o código destacado:
class Api::V1::RecipesController < ApplicationController
before_action :set_recipe, only: %i[show destroy]
def index
recipe = Recipe.all.order(created_at: :desc)
render json: recipe
end
def create
recipe = Recipe.create!(recipe_params)
if recipe
render json: recipe
else
render json: recipe.errors
end
end
def show
render json: @recipe
end
def destroy
@recipe&.destroy
render json: { message: 'Recipe deleted!' }
end
private
def recipe_params
params.permit(:name, :image, :ingredients, :instruction)
end
def set_recipe
@recipe = Recipe.find(params[:id])
end
end
Nas novas linhas de código, você cria um método set_recipe
privado chamado por um before_action
apenas quando as ações show
e delete
correspondem a uma solicitação. O método set_recipe
utiliza o método find
do ActiveRecord para encontrar uma receita cujo id
corresponde ao id
fornecido nos params
e atribui a uma variável de instância @recipe
. Na ação show
, você retorna o objeto @recipe
definido pelo método set_recipe
como uma resposta JSON.
Na ação destroy
, você fez algo semelhante usando o operador de navegação segura do Ruby &.
, que evita erros nil
ao chamar um método. Essa adição permite que você exclua uma receita apenas se ela existir e, em seguida, envie uma mensagem como resposta.
Depois de fazer essas alterações no recipes_controller.rb
, salve e feche o arquivo.
Neste passo, você criou um modelo e um controlador para suas receitas. Você escreveu toda a lógica necessária para trabalhar com receitas no backend. Na próxima seção, você criará componentes para visualizar suas receitas.
Passo 7 — Visualizando Receitas
Nesta seção, você irá criar componentes para visualizar receitas. Você criará duas páginas: uma para visualizar todas as receitas existentes e outra para visualizar receitas individuais.
Você começará criando uma página para visualizar todas as receitas. Antes de criar a página, você precisa de receitas para trabalhar, já que seu banco de dados está vazio no momento. O Rails oferece uma maneira de criar dados iniciais para a sua aplicação.
Abra o arquivo de semente chamado seeds.rb
para edição:
- nano ~/rails_react_recipe/db/seeds.rb
Substitua o conteúdo inicial do arquivo de semente com o seguinte código:
9.times do |i|
Recipe.create(
name: "Recipe #{i + 1}",
ingredients: '227g tub clotted cream, 25g butter, 1 tsp cornflour,100g parmesan, grated nutmeg, 250g fresh fettuccine or tagliatelle, snipped chives or chopped parsley to serve (optional)',
instruction: 'In a medium saucepan, stir the clotted cream, butter, and cornflour over a low-ish heat and bring to a low simmer. Turn off the heat and keep warm.'
)
end
Neste código, você usa um loop que instrui o Rails a criar nove receitas com seções para nome
, ingredientes
e instrução
. Salve e saia do arquivo.
Para popular o banco de dados com esses dados, execute o seguinte comando no seu terminal:
- rails db:seed
Executando este comando, serão adicionadas nove receitas ao seu banco de dados. Agora você pode buscá-las e renderizá-las no frontend.
O componente para visualizar todas as receitas fará uma requisição HTTP para a ação index
no RecipesController
para obter uma lista de todas as receitas. Essas receitas serão então exibidas em cartões na página.
Crie um arquivo Recipes.jsx
no diretório app/javascript/components
:
- nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx
Assim que o arquivo estiver aberto, importe os módulos React
, useState
, useEffect
, Link
e useNavigate
adicionando as seguintes linhas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
Em seguida, adicione as linhas destacadas para criar e exportar um componente funcional React chamado Recipes
:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
};
export default Recipes;
Dentro do componente Recipe
, a API de navegação do React Router irá chamar o hook useNavigate. O hook useState do React irá inicializar o estado recipes
, que é um array vazio ([]
), e uma função setRecipes
para atualizar o estado recipes
.
Em seguida, em um hook useEffect
, você fará uma requisição HTTP para buscar todas as suas receitas. Para fazer isso, adicione as linhas destacadas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
useEffect(() => {
const url = "/api/v1/recipes/index";
fetch(url)
.then((res) => {
if (res.ok) {
return res.json();
}
throw new Error("Network response was not ok.");
})
.then((res) => setRecipes(res))
.catch(() => navigate("/"));
}, []);
};
export default Recipes;
No seu hook useEffect
, você faz uma chamada HTTP para buscar todas as receitas usando a Fetch API. Se a resposta for bem-sucedida, a aplicação salva o array de receitas no estado recipes
. Se ocorrer um erro, ela redirecionará o usuário para a página inicial.
Por fim, retorne a marcação dos elementos que serão avaliados e exibidos na página do navegador quando o componente for renderizado. Neste caso, o componente irá renderizar um card de receitas a partir do estado recipes
. Adicione as linhas destacadas em Recipes.jsx
:
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
const Recipes = () => {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
useEffect(() => {
const url = "/api/v1/recipes/index";
fetch(url)
.then((res) => {
if (res.ok) {
return res.json();
}
throw new Error("Network response was not ok.");
})
.then((res) => setRecipes(res))
.catch(() => navigate("/"));
}, []);
const allRecipes = recipes.map((recipe, index) => (
<div key={index} className="col-md-6 col-lg-4">
<div className="card mb-4">
<img
src={recipe.image}
className="card-img-top"
alt={`${recipe.name} image`}
/>
<div className="card-body">
<h5 className="card-title">{recipe.name}</h5>
<Link to={`/recipe/${recipe.id}`} className="btn custom-button">
View Recipe
</Link>
</div>
</div>
</div>
));
const noRecipe = (
<div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
<h4>
No recipes yet. Why not <Link to="/new_recipe">create one</Link>
</h4>
</div>
);
return (
<>
<section className="jumbotron jumbotron-fluid text-center">
<div className="container py-5">
<h1 className="display-4">Recipes for every occasion</h1>
<p className="lead text-muted">
We’ve pulled together our most popular recipes, our latest
additions, and our editor’s picks, so there’s sure to be something
tempting for you to try.
</p>
</div>
</section>
<div className="py-5">
<main className="container">
<div className="text-end mb-3">
<Link to="/recipe" className="btn custom-button">
Create New Recipe
</Link>
</div>
<div className="row">
{recipes.length > 0 ? allRecipes : noRecipe}
</div>
<Link to="/" className="btn btn-link">
Home
</Link>
</main>
</div>
</>
);
};
export default Recipes;
Salve e saia de Recipes.jsx
.
Agora que você criou um componente para exibir todas as receitas, você irá criar uma rota para ele. Abra o arquivo de rota do front-end app/javascript/routes/index.jsx
:
- nano app/javascript/routes/index.jsx
Adicione as linhas destacadas no arquivo:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" element={<Recipes />} />
</Routes>
</Router>
);
Salve e saia do arquivo.
Neste ponto, é uma boa ideia verificar se o seu código está funcionando conforme o esperado. Como feito anteriormente, use o seguinte comando para iniciar o seu servidor:
- bin/dev
Em seguida, abra o aplicativo em seu navegador. Pressione o botão Ver Receita na página inicial para acessar uma página de exibição com suas receitas:
Utilize CTRL+C
no seu terminal para parar o servidor e retornar ao prompt.
Agora que você pode visualizar todas as receitas em seu aplicativo, é hora de criar um segundo componente para visualizar receitas individuais. Crie um arquivo Recipe.jsx
no diretório app/javascript/components
:
- nano app/javascript/components/Recipe.jsx
Assim como o componente Recipes
, importe os módulos React
, useState
, useEffect
, Link
, useNavigate
, e useParam
adicionando as seguintes linhas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
Em seguida, adicione as linhas destacadas para criar e exportar um componente funcional React chamado Recipe
:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
};
export default Recipe;
Assim como o componente Recipes
, você inicializa a navegação do React Router com o gancho useNavigate
. Um estado recipe
e uma função setRecipe
irão atualizar o estado com o gancho useState
. Além disso, você chama o gancho useParams
, que retorna um objeto cujos pares chave/valor são parâmetros de URL.
Para encontrar uma receita específica, seu aplicativo precisa saber o id
da receita, o que significa que o seu componente Recipe
espera um param
id
na URL. Você pode acessar isso por meio do objeto params
que contém o valor de retorno do gancho useParams
.
Em seguida, declare um gancho useEffect
onde você acessará o id
param
do objeto params
. Assim que você obtiver o id
do parâmetro da receita, você fará uma solicitação HTTP para buscar a receita. Adicione as linhas destacadas ao seu arquivo:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
};
export default Recipe;
No gancho useEffect
, você usa o valor params.id
para fazer uma solicitação HTTP GET para buscar a receita que possui o id
e então salvá-la no estado do componente usando a função setRecipe
. O aplicativo redireciona o usuário para a página de receitas se a receita não existir.
Em seguida, adicione uma função addHtmlEntities
, que será usada para substituir entidades de caracteres por entidades HTML no componente. A função addHtmlEntities
receberá uma string e substituirá todos os colchetes de abertura e fechamento escapados por suas entidades HTML. Esta função ajudará você a converter qualquer caractere escapado que tenha sido salvo nas instruções da sua receita. Adicione as linhas destacadas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
};
export default Recipe;
Por fim, retorne a marcação para renderizar a receita no estado do componente na página, adicionando as linhas destacadas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
};
export default Recipe;
Com uma função ingredientList
, você divide seus ingredientes de receita separados por vírgulas em uma matriz e mapeia sobre eles para criar uma lista de ingredientes. Se não houver ingredientes, o aplicativo exibe uma mensagem que diz Nenhum ingrediente disponível. Você também substitui todos os colchetes de abertura e fechamento nas instruções da receita ao passá-los pela função addHtmlEntities
. Por fim, o código exibe a imagem da receita como uma imagem principal, adiciona um botão Excluir Receita ao lado da instrução da receita e adiciona um botão que retorna à página de receitas.
Observação: Usar o atributo dangerouslySetInnerHTML
do React é arriscado, pois expõe seu aplicativo a ataques de cross-site scripting. Esse risco é reduzido garantindo que caracteres especiais inseridos ao criar receitas sejam substituídos usando a função stripHtmlEntities
declarada no componente NewRecipe
.
Salve e saia do arquivo.
Para visualizar o componente Recipe
em uma página, você o adicionará ao seu arquivo de rotas. Abra seu arquivo de rotas para edição:
- nano app/javascript/routes/index.jsx
Adicione as seguintes linhas destacadas ao arquivo:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
<Route path="/recipe/:id" element={<Recipe />} />
</Routes>
</Router>
);
Você importa seu componente Recipe
neste arquivo de rota e adiciona uma rota. Sua rota possui um param
:id
que será substituído pelo id
da receita que você deseja visualizar.
Salve e feche o arquivo.
Utilize o script bin/dev
para iniciar seu servidor novamente e, em seguida, visite http://localhost:3000
em seu navegador. Clique no botão Ver Receitas para navegar até a página de receitas. Na página de receitas, acesse qualquer receita clicando no botão Ver Receita. Você será recebido com uma página preenchida com os dados do seu banco de dados:
Você pode parar o servidor com CTRL+C
.
Neste passo, você adicionou nove receitas ao seu banco de dados e criou componentes para visualizar essas receitas, tanto individualmente quanto em coleção. No próximo passo, você adicionará um componente para criar receitas.
Passo 8 — Criando Receitas
O próximo passo para ter um aplicativo de receitas de alimentos utilizável é a capacidade de criar novas receitas. Neste passo, você criará um componente para essa funcionalidade. O componente conterá um formulário para coletar os detalhes da receita necessários do usuário e, em seguida, fará uma solicitação para a ação create
no controlador Recipe
para salvar os dados da receita.
Crie um arquivo NewRecipe.jsx
no diretório app/javascript/components
:
- nano app/javascript/components/NewRecipe.jsx
No novo arquivo, importe os módulos React
, useState
, Link
e useNavigate
que você usou em outros componentes:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
Em seguida, crie e exporte um componente funcional NewRecipe
adicionando as linhas destacadas:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
};
export default NewRecipe;
Como nos componentes anteriores, você inicializa a navegação do React router com o gancho useNavigate
e, em seguida, utiliza o gancho useState
para inicializar os estados name
, ingredients
e instruction
, cada um com suas respectivas funções de atualização. Esses são os campos necessários para criar uma receita válida.
Em seguida, crie uma função stripHtmlEntities
que converterá caracteres especiais (como <
) em seus valores escapados/encodificados (como <
), respectivamente. Para fazer isso, adicione as linhas destacadas ao componente NewRecipe
:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
};
export default NewRecipe;
Na função stripHtmlEntities
, substitua os caracteres <
e >
por seus valores escapados. Dessa forma, você não armazenará HTML puro no seu banco de dados.
Em seguida, adicione as linhas destacadas para incluir as funções onChange
e onSubmit
ao componente NewRecipe
para lidar com a edição e o envio do formulário:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
const onChange = (event, setFunction) => {
setFunction(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
const url = "/api/v1/recipes/create";
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: stripHtmlEntities(instruction),
};
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) => navigate(`/recipe/${response.id}`))
.catch((error) => console.log(error.message));
};
};
export default NewRecipe;
O código da função onChange
aceita a entrada do usuário event
e a função do definidor de estado. Em seguida, ele atualiza o estado com o valor de entrada do usuário. Na função onSubmit
, verifica-se que nenhum dos inputs obrigatórios está vazio. Em seguida, é construído um objeto contendo os parâmetros necessários para criar uma nova receita. Utilizando a função stripHtmlEntities
, substituímos os caracteres <
e >
na instrução da receita pelo seu valor escapado e substituímos cada caractere de nova linha por uma quebra de linha, mantendo assim o formato de texto inserido pelo usuário. Por fim, é feita uma solicitação HTTP POST para criar a nova receita e redirecionar para sua página em caso de uma resposta bem-sucedida.
Para se proteger contra ataques de falsificação de solicitação entre sites (CSRF), o Rails anexa um token de segurança CSRF ao documento HTML. Esse token é necessário sempre que uma solicitação não-GET
é feita. Com a constante token
no código anterior, sua aplicação verifica o token no servidor e lança uma exceção se o token de segurança não corresponder ao esperado. Na função onSubmit
, a aplicação recupera o token CSRF incorporado em seu documento HTML pelo Rails e então faz uma solicitação HTTP com uma string JSON. Se a receita for criada com sucesso, a aplicação redireciona o usuário para a página da receita, onde eles podem ver a receita recém-criada.
Por fim, retorne a marcação que renderiza um formulário para o usuário inserir os detalhes da receita que desejam criar. Adicione as linhas destacadas:
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const NewRecipe = () => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [ingredients, setIngredients] = useState("");
const [instruction, setInstruction] = useState("");
const stripHtmlEntities = (str) => {
return String(str)
.replace(/\n/g, "<br> <br>")
.replace(/</g, "<")
.replace(/>/g, ">");
};
const onChange = (event, setFunction) => {
setFunction(event.target.value);
};
const onSubmit = (event) => {
event.preventDefault();
const url = "/api/v1/recipes/create";
if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
return;
const body = {
name,
ingredients,
instruction: stripHtmlEntities(instruction),
};
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) => navigate(`/recipe/${response.id}`))
.catch((error) => console.log(error.message));
};
return (
<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 recipe to our awesome recipe collection.
</h1>
<form onSubmit={onSubmit}>
<div className="form-group">
<label htmlFor="recipeName">Recipe name</label>
<input
type="text"
name="name"
id="recipeName"
className="form-control"
required
onChange={(event) => onChange(event, setName)}
/>
</div>
<div className="form-group">
<label htmlFor="recipeIngredients">Ingredients</label>
<input
type="text"
name="ingredients"
id="recipeIngredients"
className="form-control"
required
onChange={(event) => onChange(event, setIngredients)}
/>
<small id="ingredientsHelp" className="form-text text-muted">
Separate each ingredient with a comma.
</small>
</div>
<label htmlFor="instruction">Preparation Instructions</label>
<textarea
className="form-control"
id="instruction"
name="instruction"
rows="5"
required
onChange={(event) => onChange(event, setInstruction)}
/>
<button type="submit" className="btn custom-button mt-3">
Create Recipe
</button>
<Link to="/recipes" className="btn btn-link mt-3">
Back to recipes
</Link>
</form>
</div>
</div>
</div>
);
};
export default NewRecipe;
A marcação retornada inclui um formulário que contém três campos de entrada; um para cada recipeName
, recipeIngredients
e instruction
. Cada campo de entrada tem um manipulador de evento onChange
que chama a função onChange
. Um manipulador de evento onSubmit
também é anexado ao botão de enviar e chama a função onSubmit
que envia os dados do formulário.
Salve e saia do arquivo.
Para acessar este componente no navegador, atualize seu arquivo de rota com sua rota:
- nano app/javascript/routes/index.jsx
Atualize seu arquivo de rota para incluir estas linhas destacadas:
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
import NewRecipe from "../components/NewRecipe";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" exact component={Recipes} />
<Route path="/recipe/:id" exact component={Recipe} />
<Route path="/recipe" element={<NewRecipe />} />
</Routes>
</Router>
);
Com a rota no lugar, salve e saia do seu arquivo.
Reinicie seu servidor de desenvolvimento e visite http://localhost:3000
no seu navegador. Navegue até a página de receitas e clique no botão Criar Nova Receita. Você encontrará uma página com um formulário para adicionar receitas ao seu banco de dados:
Insira os detalhes da receita necessários e clique no botão Criar Receita. A receita recém-criada aparecerá então na página. Quando estiver pronto, feche o servidor.
Neste passo, você adicionou a capacidade de criar receitas à sua aplicação de receitas culinárias. No próximo passo, você adicionará a funcionalidade para deletar receitas.
Passo 9 — Deletando Receitas
Nesta seção, você irá modificar seu componente de Receita para incluir uma opção de exclusão de receitas. Ao clicar no botão de exclusão na página da receita, a aplicação enviará uma solicitação para deletar a receita do banco de dados.
Primeiro, abra seu arquivo Recipe.jsx
para edição:
- nano app/javascript/components/Recipe.jsx
No componente Recipe
, adicione uma função deleteRecipe
com as linhas destacadas:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const deleteRecipe = () => {
const url = `/api/v1/destroy/${params.id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => navigate("/recipes"))
.catch((error) => console.log(error.message));
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
...
Na função deleteRecipe
, você obtém o id
da receita a ser excluída, em seguida, constrói sua URL e obtém o token CSRF. Em seguida, você faz uma solicitação DELETE
para o controlador Recipes
para excluir a receita. A aplicação redireciona o usuário para a página de receitas se a receita for excluída com sucesso.
Para executar o código na função deleteRecipe
sempre que o botão de exclusão for clicado, passe-a como manipulador de evento de clique para o botão. Adicione um evento onClick
ao elemento do botão de exclusão no componente:
...
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
onClick={deleteRecipe}
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
...
Neste ponto do tutorial, seu arquivo completo Recipe.jsx
deve corresponder a este arquivo:
import React, { useState, useEffect } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
const Recipe = () => {
const params = useParams();
const navigate = useNavigate();
const [recipe, setRecipe] = useState({ ingredients: "" });
useEffect(() => {
const url = `/api/v1/show/${params.id}`;
fetch(url)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then((response) => setRecipe(response))
.catch(() => navigate("/recipes"));
}, [params.id]);
const addHtmlEntities = (str) => {
return String(str).replace(/</g, "<").replace(/>/g, ">");
};
const deleteRecipe = () => {
const url = `/api/v1/destroy/${params.id}`;
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch(url, {
method: "DELETE",
headers: {
"X-CSRF-Token": token,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
})
.then(() => navigate("/recipes"))
.catch((error) => console.log(error.message));
};
const ingredientList = () => {
let ingredientList = "No ingredients available";
if (recipe.ingredients.length > 0) {
ingredientList = recipe.ingredients
.split(",")
.map((ingredient, index) => (
<li key={index} className="list-group-item">
{ingredient}
</li>
));
}
return ingredientList;
};
const recipeInstruction = addHtmlEntities(recipe.instruction);
return (
<div className="">
<div className="hero position-relative d-flex align-items-center justify-content-center">
<img
src={recipe.image}
alt={`${recipe.name} image`}
className="img-fluid position-absolute"
/>
<div className="overlay bg-dark position-absolute" />
<h1 className="display-4 position-relative text-white">
{recipe.name}
</h1>
</div>
<div className="container py-5">
<div className="row">
<div className="col-sm-12 col-lg-3">
<ul className="list-group">
<h5 className="mb-2">Ingredients</h5>
{ingredientList()}
</ul>
</div>
<div className="col-sm-12 col-lg-7">
<h5 className="mb-2">Preparation Instructions</h5>
<div
dangerouslySetInnerHTML={{
__html: `${recipeInstruction}`,
}}
/>
</div>
<div className="col-sm-12 col-lg-2">
<button
type="button"
className="btn btn-danger"
onClick={deleteRecipe}
>
Delete Recipe
</button>
</div>
</div>
<Link to="/recipes" className="btn btn-link">
Back to recipes
</Link>
</div>
</div>
);
};
export default Recipe;
Salve e saia do arquivo.
Reinicie o servidor da aplicação e navegue até a página inicial. Clique no botão Ver Receitas para acessar todas as receitas existentes, em seguida, abra qualquer receita específica e clique no botão Excluir Receita na página para deletar a receita. Você será redirecionado para a página de receitas, e a receita excluída não existirá mais.
Com o botão de exclusão funcionando, agora você tem um aplicativo de receitas totalmente funcional!
Conclusão
Neste tutorial, você criou uma aplicação de receitas de comida com Ruby on Rails e uma interface React, utilizando PostgreSQL como seu banco de dados e Bootstrap para estilização. Se você deseja continuar construindo com Ruby on Rails, considere seguir nosso tutorial Como Segurar Comunicações em uma Aplicação Rails de Três Camadas Usando Túneis SSH ou visite nossa série Como Codificar em Ruby para atualizar suas habilidades em Ruby. Para se aprofundar no React, experimente o tutorial Como Exibir Dados da API da DigitalOcean com React.