Browse Source

formtastic

main
pvincent 1 week ago
parent
commit
459cbf2951
  1. 1
      Gemfile
  2. 4
      Gemfile.lock
  3. 2
      Rakefile
  4. 289
      app/assets/stylesheets/formtastic.css
  5. 59
      app/controllers/posts_controller.rb
  6. 2
      app/helpers/posts_helper.rb
  7. 2
      app/models/post.rb
  8. 5
      app/views/posts/_form.html.erb
  9. 2
      app/views/posts/_post.html.erb
  10. 10
      app/views/posts/edit.html.erb
  11. 43
      app/views/posts/index.html.erb
  12. 9
      app/views/posts/new.html.erb
  13. 15
      app/views/posts/show.html.erb
  14. 118
      config/initializers/formtastic.rb
  15. 1
      config/routes.rb
  16. 11
      db/migrate/20260303141001_create_posts.rb
  17. 21
      db/schema.rb
  18. 48
      test/controllers/posts_controller_test.rb
  19. 11
      test/fixtures/posts.yml
  20. 7
      test/models/post_test.rb

1
Gemfile

@ -3,6 +3,7 @@ source 'https://rubygems.org'
gem 'bootsnap', require: false
gem 'dotenv-rails'
gem 'formtastic'
gem 'importmap-rails'
gem 'propshaft'
gem 'puma'

4
Gemfile.lock

@ -106,6 +106,8 @@ GEM
ffi (1.17.3-x86_64-darwin)
ffi (1.17.3-x86_64-linux-gnu)
ffi (1.17.3-x86_64-linux-musl)
formtastic (6.0.0)
actionpack (>= 7.2.0)
fugit (1.12.1)
et-orbi (~> 1.4)
raabro (~> 1.4)
@ -366,6 +368,7 @@ DEPENDENCIES
debug
dotenv-rails
error_highlight
formtastic
htmlbeautifier
importmap-rails
propshaft
@ -426,6 +429,7 @@ CHECKSUMS
ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
formtastic (6.0.0) sha256=c398906b65978fec3d045d6792f82cf9641f086ac9f17357b2b382f723126165
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
htmlbeautifier (1.4.3) sha256=b43d08f7e2aa6ae1b5a6f0607b4ed8954c8d4a8e85fd2336f975dda1e4db385b

2
Rakefile

@ -1,6 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative "config/application"
require_relative 'config/application'
Rails.application.load_tasks

289
app/assets/stylesheets/formtastic.css

@ -0,0 +1,289 @@
/* -------------------------------------------------------------------------------------------------
It's *strongly* suggested that you don't modify this file. Instead, load a new stylesheet after
this one in your layouts (eg formtastic_changes.css) and override the styles to suit your needs.
This will allow you to update formtastic.css with new releases without clobbering your own changes.
This stylesheet forms part of the Formtastic Rails gem
(c) Justin French
--------------------------------------------------------------------------------------------------*/
/* NORMALIZE AND RESET - obviously inspired by Yahoo's reset.css, but scoped to just .formtastic
--------------------------------------------------------------------------------------------------*/
.formtastic,
.formtastic ul,
.formtastic ol,
.formtastic li,
.formtastic fieldset,
.formtastic legend,
.formtastic input,
.formtastic button,
.formtastic textarea,
.formtastic select,
.formtastic p {
margin:0;
padding:0;
}
.formtastic fieldset {
border:0;
}
.formtastic em,
.formtastic strong {
font-style:normal;
font-weight:normal;
}
.formtastic ol,
.formtastic ul {
list-style:none;
}
.formtastic abbr,
.formtastic acronym {
border:0;
font-variant:normal;
}
.formtastic input,
.formtastic button,
.formtastic textarea {
font-family:sans-serif;
font-size:inherit;
font-weight:inherit;
}
.formtastic input,
.formtastic textarea,
.formtastic select {
font-size:100%;
}
.formtastic legend {
white-space:normal;
color:#000;
}
/* SEMANTIC ERRORS
--------------------------------------------------------------------------------------------------*/
.formtastic .errors {
color:#cc0000;
margin:0.5em 0 1.5em 25%;
list-style:square;
}
.formtastic .errors li {
padding:0;
border:none;
display:list-item;
}
/* BUTTONS & ACTIONS
--------------------------------------------------------------------------------------------------*/
.formtastic .buttons,
.formtastic .actions {
overflow:hidden; /* clear containing floats */
padding-left:25%;
}
.formtastic .button,
.formtastic .action {
float:left;
padding-right:0.5em;
}
.formtastic .button_action button {
padding:3px 8px;
}
.formtastic .link_action a {
display:block;
padding:3px 0;
}
/* INPUTS
--------------------------------------------------------------------------------------------------*/
.formtastic .inputs {
overflow:hidden; /* clear containing floats */
}
.formtastic .input {
overflow:hidden; /* clear containing floats */
padding:0.5em 0; /* padding and negative margin juggling is for Firefox */
margin-top:-0.5em;
margin-bottom:1em;
}
/* LEFT ALIGNED LABELS
--------------------------------------------------------------------------------------------------*/
.formtastic .input .label {
display:block;
width:25%;
float:left;
padding-top:.2em;
}
.formtastic .fragments .label,
.formtastic .choices .label {
position:absolute;
width:95%;
left:0px;
}
.formtastic .fragments .label label,
.formtastic .choices .label label {
position:absolute;
}
/* NESTED FIELDSETS AND LEGENDS (radio, check boxes and date/time inputs use nested fieldsets)
--------------------------------------------------------------------------------------------------*/
.formtastic .choices {
position:relative;
}
.formtastic .choices-group {
float:left;
width:74%;
margin:0;
padding:0 0 0 25%;
}
.formtastic .choice {
padding:0;
border:0;
}
/* INLINE HINTS
--------------------------------------------------------------------------------------------------*/
.formtastic .input .inline-hints {
color:#666;
margin:0.5em 0 0 25%;
}
/* INLINE ERRORS
--------------------------------------------------------------------------------------------------*/
.formtastic .inline-errors {
color:#cc0000;
margin:0.5em 0 0 25%;
}
.formtastic .errors {
color:#cc0000;
margin:0.5em 0 0 25%;
list-style:square;
}
.formtastic .errors li {
padding:0;
border:none;
display:list-item;
}
/* STRING, NUMERIC, PASSWORD, EMAIL, URL, PHONE, SEARCH (ETC) OVERRIDES
--------------------------------------------------------------------------------------------------*/
.formtastic .stringish input {
width:72%;
}
.formtastic .stringish input[size],
.formtastic .stringish input[max] {
width:auto;
max-width:72%;
}
/* TEXTAREA OVERRIDES
--------------------------------------------------------------------------------------------------*/
.formtastic .text textarea {
width:72%;
}
.formtastic .text textarea[cols] {
width:auto;
max-width:72%;
}
/* HIDDEN OVERRIDES
--------------------------------------------------------------------------------------------------*/
.formtastic .hidden {
display:none;
}
/* BOOLEAN LABELS
--------------------------------------------------------------------------------------------------*/
.formtastic .boolean label {
margin-left:25%;
display:block;
}
/* CHOICE GROUPS
--------------------------------------------------------------------------------------------------*/
.formtastic .choices-group {
margin-bottom:-0.5em;
}
.formtastic .choice {
margin:0.1em 0 0.5em 0;
}
.formtastic .choice label {
float:none;
width:100%;
line-height:100%;
padding-top:0;
margin-bottom:0.6em;
}
/* ADJUSTMENTS FOR INPUTS INSIDE LABELS (boolean input, radio input, check_boxes input)
--------------------------------------------------------------------------------------------------*/
.formtastic .choice label input,
.formtastic .boolean label input {
margin:0 0.3em 0 0.1em;
line-height:100%;
}
/* FRAGMENTED INPUTS (DATE/TIME/DATETIME)
--------------------------------------------------------------------------------------------------*/
.formtastic .fragments {
position:relative;
}
.formtastic .fragments-group {
float:left;
width:74%;
margin:0;
padding:0 0 0 25%;
}
.formtastic .fragment {
float:left;
width:auto;
margin:0 .3em 0 0;
padding:0;
border:0;
}
.formtastic .fragment label {
display:none;
}
.formtastic .fragment label input {
display:inline;
margin:0;
padding:0;
}

59
app/controllers/posts_controller.rb

@ -0,0 +1,59 @@
class PostsController < ApplicationController
before_action :set_post, only: %i[show edit update destroy]
# GET /posts
def index
@posts = Post.all
end
# GET /posts/1
def show
end
# GET /posts/new
def new
@post = Post.new
end
# GET /posts/1/edit
def edit
end
# POST /posts
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: 'Post was successfully created.'
else
render :new, status: :unprocessable_content
end
end
# PATCH/PUT /posts/1
def update
if @post.update(post_params)
redirect_to @post, notice: 'Post was successfully updated.', status: :see_other
else
render :edit, status: :unprocessable_content
end
end
# DELETE /posts/1
def destroy
@post.destroy!
redirect_to posts_path, notice: 'Post was successfully destroyed.', status: :see_other
end
private
# Use callbacks to share common setup or constraints between actions.
def set_post
@post = Post.find(params.expect(:id))
end
# Only allow a list of trusted parameters through.
def post_params
params.require(:post).permit(:title, :quantity, :content)
end
end

2
app/helpers/posts_helper.rb

@ -0,0 +1,2 @@
module PostsHelper
end

2
app/models/post.rb

@ -0,0 +1,2 @@
class Post < ApplicationRecord
end

5
app/views/posts/_form.html.erb

@ -0,0 +1,5 @@
<%= semantic_form_for @post do |f| %>
<%= f.inputs %>
<%= f.semantic_errors :state %>
<%= f.actions %>
<% end %>

2
app/views/posts/_post.html.erb

@ -0,0 +1,2 @@
<div id="<%= dom_id post %>" class="w-full sm:w-auto my-5 space-y-5">
</div>

10
app/views/posts/edit.html.erb

@ -0,0 +1,10 @@
<% content_for :title, "Editing post" %>
<div class="md:w-2/3 w-full">
<h1 class="font-bold text-4xl">Editing post</h1>
<%= render "form", post: @post %>
<%= link_to "Show this post", @post, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= link_to "Back to posts", posts_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
</div>

43
app/views/posts/index.html.erb

@ -0,0 +1,43 @@
<% content_for :title, "Posts" %>
<div class="w-full">
<% if notice.present? %>
<p
class="
py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md
inline-block
"
id="notice"
>
<%= notice %>
</p>
<% end %>
<div class="flex justify-between items-center">
<h1 class="font-bold text-4xl">Posts</h1>
<%= link_to "New post", new_post_path, class: "rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white block font-medium" %>
</div>
<div id="posts" class="min-w-full divide-y divide-gray-200 space-y-5">
<% if @posts.any? %>
<% @posts.each do |post| %>
<div
class="
flex flex-col sm:flex-row justify-between items-center pb-5
sm:pb-0
"
>
<%= render post %>
<div class="w-full sm:w-auto flex flex-col sm:flex-row space-x-2 space-y-2">
<%= link_to "Show", post, class: "btn btn-primary" %>
<%= link_to "Edit", edit_post_path(post), class: "btn btn-secondary" %>
<%= button_to "Destroy", post, method: :delete, class: "btn btn-accent", data: { turbo_confirm: "Are you sure?" } %>
</div>
</div>
<% end %>
<% else %>
<p class="text-center my-10">No posts found.</p>
<% end %>
</div>
</div>

9
app/views/posts/new.html.erb

@ -0,0 +1,9 @@
<% content_for :title, "New post" %>
<div class="md:w-2/3 w-full">
<h1 class="font-bold text-4xl">New post</h1>
<%= render "form", post: @post %>
<%= link_to "Back to posts", posts_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
</div>

15
app/views/posts/show.html.erb

@ -0,0 +1,15 @@
<% content_for :title, "Showing post" %>
<div class="md:w-2/3 w-full">
<% if notice.present? %>
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%= notice %></p>
<% end %>
<h1 class="font-bold text-4xl">Showing post</h1>
<%= render @post %>
<%= link_to "Edit this post", edit_post_path(@post), class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= link_to "Back to posts", posts_path, class: "w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium" %>
<%= button_to "Destroy this post", @post, method: :delete, form_class: "sm:inline-block mt-2 sm:mt-0 sm:ml-2", class: "w-full rounded-md px-3.5 py-2.5 text-white bg-red-600 hover:bg-red-500 font-medium cursor-pointer", data: { turbo_confirm: "Are you sure?" } %>
</div>

118
config/initializers/formtastic.rb

@ -0,0 +1,118 @@
# frozen_string_literal: true
# Set the default text field size when input is a string. Default is nil.
# Formtastic::FormBuilder.default_text_field_size = 50
# Set the default text area height when input is a text. Default is 20.
# Formtastic::FormBuilder.default_text_area_height = 5
# Set the default text area width when input is a text. Default is nil.
# Formtastic::FormBuilder.default_text_area_width = 50
# Should all fields be considered "required" by default?
# Defaults to true.
# Formtastic::FormBuilder.all_fields_required_by_default = true
# Should select fields have a blank option/prompt by default?
# Defaults to true.
# Formtastic::FormBuilder.include_blank_for_select_by_default = true
# Set the string that will be appended to the labels/fieldsets which are required.
# It accepts string or procs and the default is a localized version of
# '<abbr title="required">*</abbr>'. In other words, if you configure formtastic.required
# in your locale, it will replace the abbr title properly. But if you don't want to use
# abbr tag, you can simply give a string as below.
# Formtastic::FormBuilder.required_string = "(required)"
# Set the string that will be appended to the labels/fieldsets which are optional.
# Defaults to an empty string ("") and also accepts procs (see required_string above).
# Formtastic::FormBuilder.optional_string = "(optional)"
# Set the way inline errors will be displayed.
# Defaults to :sentence, valid options are :sentence, :list, :first and :none
# Formtastic::FormBuilder.inline_errors = :sentence
# Formtastic uses the following classes as default for hints, inline_errors and error list
# If you override the class here, please ensure to override it in your stylesheets as well.
# Formtastic::FormBuilder.default_hint_class = "inline-hints"
# Formtastic::FormBuilder.default_inline_error_class = "inline-errors"
# Formtastic::FormBuilder.default_error_list_class = "errors"
# Set the method to call on label text to transform or format it for human-friendly
# reading when formtastic is used without object. Defaults to :humanize.
# Formtastic::FormBuilder.label_str_method = :humanize
# Set the array of methods to try calling on parent objects in :select and :radio inputs
# for the text inside each @<option>@ tag or alongside each radio @<input>@. The first method
# that is found on the object will be used.
# Defaults to ["to_label", "display_name", "full_name", "name", "title", "username", "login", "value", "to_s"]
# Formtastic::FormBuilder.collection_label_methods = [
# "to_label", "display_name", "full_name", "name", "title", "username", "login", "value", "to_s"]
# Specifies if labels/hints for input fields automatically be looked up using I18n.
# Default value: true. Overridden for specific fields by setting value to true,
# i.e. :label => true, or :hint => true (or opposite depending on initialized value)
# Formtastic::FormBuilder.i18n_lookups_by_default = false
# Specifies if I18n lookups of the default I18n Localizer should be cached to improve performance.
# Defaults to true.
# Formtastic::FormBuilder.i18n_cache_lookups = false
# Specifies the class to use for localization lookups. You can create your own
# class and use it instead by subclassing Formtastic::Localizer (which is the default).
# Formtastic::FormBuilder.i18n_localizer = MyOwnLocalizer
# You can add custom inputs or override parts of Formtastic by subclassing Formtastic::FormBuilder and
# specifying that class here. Defaults to Formtastic::FormBuilder.
# Formtastic::Helpers::FormHelper.builder = MyCustomBuilder
# All formtastic forms have a class that indicates that they are just that. You
# can change it to any class you want.
# Formtastic::Helpers::FormHelper.default_form_class = 'formtastic'
# Formtastic will infer a class name from the model, array, string or symbol you pass to the
# form builder. You can customize the way that class is presented by overriding
# this proc.
# Formtastic::Helpers::FormHelper.default_form_model_class_proc = proc { |model_class_name| model_class_name }
# Allows to set a custom field_error_proc wrapper. By default this wrapper
# is disabled since `formtastic` already adds an error class to the LI tag
# containing the input.
# Formtastic::Helpers::FormHelper.formtastic_field_error_proc = proc { |html_tag, instance_tag| html_tag }
# You can opt-in to Formtastic's use of the HTML5 `required` attribute on `<input>`, `<select>`
# and `<textarea>` tags by setting this to true (defaults to false).
# Formtastic::FormBuilder.use_required_attribute = false
# You can opt-in to new HTML5 browser validations (for things like email and url inputs) by setting
# this to true. Doing so will omit the `novalidate` attribute from the `<form>` tag.
# See http://diveintohtml5.org/forms.html#validation for more info.
# Formtastic::FormBuilder.perform_browser_validations = true
# By creating custom input class finder, you can change how input classes are looked up.
# For example you can make it to search for TextInputFilter instead of TextInput.
# See https://github.com/formtastic/formtastic/wiki/Custom-Class-Finders
# Formtastic::FormBuilder.input_class_finder = Formtastic::InputClassFinder
# Define custom namespaces in which to look up your Input classes. Default is
# to look up in the global scope and in Formtastic::Inputs.
# Formtastic::FormBuilder.input_namespaces = [ ::Object, ::MyInputsModule, ::Formtastic::Inputs ]
# By creating custom action class finder, you can change how action classes are looked up.
# For example you can make it to search for MyButtonAction instead of ButtonAction.
# See https://github.com/formtastic/formtastic/wiki/Custom-Class-Finders
# Formtastic::FormBuilder.action_class_finder = Formtastic::ActionClassFinder
# Define custom namespaces in which to look up your Action classes. Default is
# to look up in the global scope and in Formtastic::Actions.
# Formtastic::FormBuilder.action_namespaces = [ ::Object, ::MyActionsModule, ::Formtastic::Actions ]
# Which columns to skip when automatically rendering a form without any fields specified.
# Formtastic::FormBuilder.skipped_columns = [:created_at, :updated_at, :created_on, :updated_on, :lock_version, :version]
# You can opt-in to accessibility features for the `semantic_errors` helper by setting
# this to true. Doing so will render the attributes in the error summary list
# as `<li> <a>` links to the inputs that have errors. the inline error sentence's id is added to
# the errored input's aria-describedby. This ensures that the errored input is read out with
# the inline error sentence's error explanation, aria-invalid is set to true for errored inputs
# Formtastic::FormBuilder.semantic_errors_link_to_inputs = true

1
config/routes.rb

@ -1,4 +1,5 @@
Rails.application.routes.draw do
resources :posts
root to: 'edge#index'
get 'edge/index'
post 'edge/turbo_edit'

11
db/migrate/20260303141001_create_posts.rb

@ -0,0 +1,11 @@
class CreatePosts < ActiveRecord::Migration[8.1]
def change
create_table :posts do |t|
t.string :title
t.integer :quantity
t.string :content
t.timestamps
end
end
end

21
db/schema.rb

@ -0,0 +1,21 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2026_03_03_141001) do
create_table "posts", force: :cascade do |t|
t.string "content"
t.datetime "created_at", null: false
t.integer "quantity"
t.string "title"
t.datetime "updated_at", null: false
end
end

48
test/controllers/posts_controller_test.rb

@ -0,0 +1,48 @@
require "test_helper"
class PostsControllerTest < ActionDispatch::IntegrationTest
setup do
@post = posts(:one)
end
test "should get index" do
get posts_url
assert_response :success
end
test "should get new" do
get new_post_url
assert_response :success
end
test "should create post" do
assert_difference("Post.count") do
post posts_url, params: { post: {} }
end
assert_redirected_to post_url(Post.last)
end
test "should show post" do
get post_url(@post)
assert_response :success
end
test "should get edit" do
get edit_post_url(@post)
assert_response :success
end
test "should update post" do
patch post_url(@post), params: { post: {} }
assert_redirected_to post_url(@post)
end
test "should destroy post" do
assert_difference("Post.count", -1) do
delete post_url(@post)
end
assert_redirected_to posts_url
end
end

11
test/fixtures/posts.yml

@ -0,0 +1,11 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
title: MyString
quantity: 1
content: MyString
two:
title: MyString
quantity: 1
content: MyString

7
test/models/post_test.rb

@ -0,0 +1,7 @@
require "test_helper"
class PostTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
Loading…
Cancel
Save