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 aplicação web no lado do servidor. Ele alimenta muitas aplicações populares que existem na web hoje, como GitHub, Basecamp, SoundCloud, Airbnb e Twitch. Com seu foco 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.
React é uma biblioteca JavaScript usada para criar interfaces de usuário no front-end. Apoiado pelo Facebook, é uma das bibliotecas de front-end mais populares usadas na web hoje. O React oferece recursos como um Modelo de Objeto de Documento (DOM) virtual, arquitetura de componentes e gerenciamento de estado, que tornam o processo de desenvolvimento de front-end mais organizado e eficiente.
Com a mudança do frontend da web em direção a frameworks separados do código 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 mecanismo de template do Rails), seu aplicativo se beneficiará dos avanços mais recentes em JavaScript e desenvolvimento frontend, ao mesmo tempo em que aproveita a expressividade do Ruby on Rails.
Neste tutorial, você criará uma aplicação Ruby on Rails que armazena suas receitas favoritas e as exibe com um frontend React. Ao finalizar, você será capaz de 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. O Node.js é um ambiente de execução JavaScript que permite que você execute seu código fora do navegador. Ele vem com um Gerenciador de Pacotes pré-instalado chamado npm, que permite que você instale e atualize 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.
-
Ruby on Rails 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 seguir 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, então certifique-se de especificar essas versões durante o processo de instalação.
Nota: 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 o PostgreSQL versão 12 ou superior. Se você deseja desenvolver esta aplicação em uma distribuição Linux diferente ou outro sistema operacional, consulte a página oficial de downloads do PostgreSQL. Para mais informações sobre como usar o PostgreSQL, veja Como Instalar e Usar o PostgreSQL.
Passo 1 — Criando uma Nova Aplicação Rails
Você irá construir sua aplicação de receitas no framework de aplicativos Rails neste passo. 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 no seu terminal:
- rails -h
Este comando fornecerá 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 gerador new
incluem o seguinte:
- A flag
-d
especifica o mecanismo de banco de dados preferido, que neste caso é o PostgreSQL. - A flag
-j
especifica a abordagem de 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 o 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ê quiser 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, você deve 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 anexando um sublinhado (_
) seguido pelo nome do ambiente. Neste tutorial, você usará os valores de configuração padrão do banco de dados, mas pode alterar seus valores de configuração se necessário.
Observação: 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 no 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 vá para 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. No próximo passo, você instalará as dependências JavaScript necessárias para montar seu frontend React.
Passo 3 — Instalando Dependências de Frontend
Neste passo, você instalará as dependências JavaScript necessárias no frontend de sua aplicação de receitas. 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 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 de front-end para sua aplicação. Em seguida, você irá configurar uma página inicial para a sua aplicação de receitas de comida.
Passo 4 — Configurando a Página Inicial
Com as dependências necessárias instaladas, agora você irá criar uma página inicial para a aplicação servir como a página de entrada quando os usuários visitarem pela primeira vez.
O Rails segue o padrão arquitetural Model-View-Controller para aplicações. No padrão MVC, o propósito de um controlador é receber solicitações específicas e encaminhá-las para o modelo ou visualização apropriados. A aplicação atualmente exibe a página de boas-vindas do Rails quando a URL raiz é carregada no navegador. Para mudar isso, você irá criar um controlador e visualização para a página inicial e então associá-lo a uma rota.
O Rails fornece um gerador de controller
para criar um controlador. O gerador de controller
recebe um nome de controlador e uma ação correspondente. Para mais informações sobre isso, você pode revisar a documentação do Rails.
Este tutorial irá chamar o controlador de PáginaInicial
. Execute o seguinte comando para criar um controlador PáginaInicial
com uma ação index
:
- rails g controller Homepage index
Nota:
No Linux, o erro FATAL: Erro de escuta: impossível monitorar diretórios para mudanças.
pode resultar de um limite do sistema no número de arquivos que sua máquina pode monitorar para mudanças. Execute o seguinte comando para corrigi-lo:
- 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 ao executar o comando Rails, o Rails também atualiza seu 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 detalhes sobre a DSL disponível dentro deste 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 PáginaInicial
, 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 que 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. Ao fazer isso, você garante que o conteúdo de index.html.erb
não interfira com a renderização do React no seu frontend.
Agora que você configurou sua página inicial para sua aplicação, você pode passar para a próxima seção, onde configurará o frontend de sua aplicação para usar o React.
Passo 5 — Configurando o React como Seu Frontend do Rails
Nesta etapa, você irá configurar o Rails para usar o React na interface do aplicativo, em vez de seu mecanismo de modelo. Essa nova configuração permitirá que você crie uma página inicial mais visualmente atraente com o React.
Com a ajuda da opção esbuild
especificada ao gerar o aplicativo Rails, a maior parte da configuração necessária para permitir que o JavaScript funcione perfeitamente com o Rails já está em vigor. Tudo o que resta é carregar o ponto de entrada do aplicativo 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 no aplicativo, incluindo o arquivo de entrada no aplicativo 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 compilação em 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 o esbuild
para empacotamento. Com o diretório /components
importado no ponto de entrada JavaScript do aplicativo 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 hyperlink para navegar de uma página para outra. Em seguida, você cria e exporta um componente funcional contendo alguma linguagem de marcação para sua página inicial, estilizada com classes do Bootstrap.
Salve e feche o arquivo.
Com seu componente Home
configurado, agora você irá 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 componentes correspondentes. Sempre que qualquer rota especificada for carregada, ela renderizará seu componente correspondente para o 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 usar o React, bem como os módulos BrowserRouter
, Routes
e Route
do React Router, que juntos ajudam a navegar de uma rota para outra. Por fim, você importa seu componente Home
, que será renderizado sempre que uma solicitação corresponder à rota raiz (/
). Quando você quiser adicionar mais páginas à sua aplicação, pode declarar uma rota neste arquivo e combiná-la com o componente que deseja renderizar para essa página.
Salve e saia do arquivo.
Agora você configurou o roteamento usando o React Router. Para que o React esteja ciente das rotas disponíveis e as utilize, as rotas devem estar disponíveis no ponto de entrada da aplicação. Para conseguir isso, você irá renderizar suas rotas em um componente que o React 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. Em seguida, exporta um componente para renderizar as rotas dentro de fragmentos. Este 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 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 é 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ê 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 esboço para uma imagem de destaque, ou um grande banner da web 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 acessar a 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. No próximo passo, você criará modelos e controladores que permitirão criar, ler, atualizar e excluir receitas.
Passo 6 — Criando o Controlador e Modelo de Receitas
Agora que você configurou um frontend React para sua aplicação, você criará um modelo e um 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 receita 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 ao controlador. Finalmente, estas informações são exibidas no navegador.
Comece criando um modelo de Receita
usando o subcomando generate model
fornecido pelo Rails e especificando o nome do modelo juntamente 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 de Receita
juntamente com uma coluna name
do tipo string
, uma coluna ingredients
e instruction
do tipo text
, e uma coluna image
do tipo string
. Este tutorial nomeou o modelo de Receita
, porque os modelos no Rails usam um nome singular enquanto suas tabelas de banco de dados correspondentes usam um nome plural.
A execução do 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ê irá editar o arquivo do modelo de receita para garantir que apenas dados válidos sejam salvos no banco de dados. Você pode fazer isso adicionando algumas validações de banco de dados ao seu modelo.
Abra seu modelo de receita localizado em app/models/recipe.rb
:
- nano ~/rails_react_recipe/app/models/recipe.rb
Adicione as seguintes linhas de código ao arquivo:
class Recipe < ApplicationRecord
validates :name, presence: true
validates :ingredients, presence: true
validates :instruction, presence: true
end
Neste código, você adiciona validação de 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, você deve 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. Por fim, você adiciona uma URL de imagem padrão para sua coluna de imagem; esta poderia ser outra URL se você quiser usar uma imagem diferente.
Com essas alterações, salve e saia do arquivo. Você está pronto para executar sua migração e criar sua tabela. No terminal, execute o seguinte comando:
- rails db:migrate
Você utiliza 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 é 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 o seu modelo de receita 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 busca de todas as suas receitas; a ação create
será responsável pela criação de novas receitas; a ação show
buscará uma única receita, e a ação destroy
conterá 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 suas rotas de aplicativo conforme o DSL em https://guides.rubyonrails.org/routing.html
# Define a rota do caminho raiz ("/")
# root "articles#index"
end
Neste arquivo de rota, você modifica o verbo HTTP das rotas create
e destroy
para que possam post
e delete
dados. Você também modifica as rotas para as ações show
e destroy
adicionando um parâmetro :id
à rota. :id
vai conter o número de identificação da receita que você deseja ler ou excluir.
Você adiciona uma rota de captura geral 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 de front-end lidará com solicitações não relacionadas à criação, leitura ou exclusão de receitas.
Salve e saia do arquivo.
Para avaliar uma lista de rotas disponíveis em sua aplicação, execute o seguinte comando:
- rails routes
Executar este comando exibe uma lista extensa 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 só vez. O Rails usa a biblioteca ActiveRecord para lidar com tarefas relacionadas a bancos de dados como essa. 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ê usará 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ê usa 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 último, 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 fornecida. Atualize o 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
Na ação 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 prevenido pelo uso da 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
no qual você permite os parâmetros do controlador para evitar 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
.
Agora, seu controlador de receitas 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
corresponda ao id
fornecido nos params
e atribui-a 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 de 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.
Após fazer essas alterações no arquivo 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ê 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 sua aplicação.
Abra o arquivo de sementes chamado seeds.rb
para edição:
- nano ~/rails_react_recipe/db/seeds.rb
Substitua o conteúdo inicial do arquivo de sementes pelo 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 alimentar o banco de dados com esses dados, execute o seguinte comando no seu terminal:
- rails db:seed
Executar este comando adiciona nove receitas ao seu banco de dados. Agora você pode buscá-las e exibi-las no frontend.
O componente para visualizar todas as receitas fará uma solicitaçã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, ele redirecionará o usuário para a página inicial.
Por fim, retorne a marcação para os 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 do estado recipes
. Adicione as linhas destacadas ao arquivo 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 do arquivo Recipes.jsx
.
Agora que você criou um componente para exibir todas as receitas, você criará uma rota para ele. Abra o arquivo de rotas do front-end app/javascript/routes/index.jsx
:
- nano app/javascript/routes/index.jsx
Adicione as 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";
export default (
<Router>
<Routes>
<Route path="/" exact component={Home} />
<Route path="/recipes" element={<Recipes />} />
</Routes>
</Router>
);
Salve e saia do arquivo.
Neste momento, é uma boa ideia verificar se o seu código está funcionando conforme o esperado. Como fez antes, use o seguinte comando para iniciar 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:
Use CTRL+C
no seu terminal para parar o servidor e retornar ao seu prompt.
Agora que você pode visualizar todas as receitas em sua aplicação, é 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 hook useNavigate
. Um estado recipe
e uma função setRecipe
atualizarão o estado com o hook useState
. Além disso, você chama o hook useParams
, que retorna um objeto cujos pares chave/valor são parâmetros de URL.
Para encontrar uma receita específica, sua aplicação precisa saber o id
da receita, o que significa que seu componente Recipe
espera um id param
na URL. Você pode acessar isso através do objeto params
que mantém o valor de retorno do hook useParams
.
Próximo, declare um gancho useEffect
onde você acessará o id
param
do objeto params
. Assim que você obter o id
param 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ê utiliza o valor params.id
para fazer uma solicitação HTTP GET para buscar a receita que possui o id
e depois 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. Essa função ajudará 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;
Finalmente, 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írgula em uma matriz e mapeia sobre ela 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 passando-as pela função addHtmlEntities
. Por último, o código exibe a imagem da receita como uma imagem hero, adiciona um botão Excluir Receita ao lado das instruções da receita e adiciona um botão que volta para a 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 os 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 tem um param
:id
que será substituído pelo id
da receita que você deseja visualizar.
Salve e feche o arquivo.
Use o script bin/dev
para iniciar seu servidor novamente, 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 correspondente. Você será recebido com uma página populada 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 uma 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 alimentares utilizável é a capacidade de criar novas receitas. Neste passo, você criará um componente para esta 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;
Assim como nos componentes anteriores, você inicializa a navegação do React router com o gancho useNavigate
e então usa o gancho useState
para inicializar um estado name
, ingredients
e instruction
, cada um com suas respectivas funções de atualização. Estes são os campos que você precisará criar para 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 >
pelos seus valores escapados. Dessa forma, você não armazenará HTML bruto no seu banco de dados.
Em seguida, adicione as linhas destacadas para adicionar as funções onChange
e onSubmit
ao componente NewRecipe
para lidar com a edição e 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;
A função onChange
aceita a entrada do usuário event
e a função de definição do estado, em seguida, ela atualiza o estado com o valor de entrada do usuário. Na função onSubmit
, verifica-se que nenhum dos campos 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 quebra de linha por uma tag de quebra, 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 uma resposta bem-sucedida.
Para proteção contra ataques de falsificação de solicitação entre sites (CSRF), o Rails anexa um token de segurança CSRF ao documento HTML. Este 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 no 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 podem visualizar a nova receita criada.
Por fim, retorne a marcação que renderiza um formulário para que o usuário insira os detalhes da receita que deseja 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;
O marcado retornado inclui um formulário que contém três campos de entrada; um para cada um dos campos 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 está vinculado ao botão de envio 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
em 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 então aparecerá na página. Quando estiver pronto, feche o servidor.
Neste passo, você adicionou a capacidade de criar receitas ao seu aplicativo de receitas. No próximo passo, você adicionará a funcionalidade para excluir receitas.
Passo 9 — Excluindo Receitas
Nesta seção, você modificará seu componente de Receita para incluir uma opção para excluir receitas. Quando você clicar no botão de exclusão na página da receita, o aplicativo enviará uma solicitação para excluir uma 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. O aplicativo 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 Recipe.jsx
completo 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 de aplicativos 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 excluir o artigo. 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, você agora tem um aplicativo de receitas totalmente funcional!
Conclusão
Neste tutorial, você criou um aplicativo de receitas de alimentos com Ruby on Rails e uma interface React, utilizando o PostgreSQL como seu banco de dados e o Bootstrap para estilização. Se desejar continuar desenvolvendo 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 Programar em Ruby para revisar suas habilidades em Ruby. Para se aprofundar no React, experimente o tutorial Como Exibir Dados da API da DigitalOcean com React.