blanks 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +427 -0
  5. data/Rakefile +8 -0
  6. data/examples/advanced_features_example.rb +81 -0
  7. data/examples/assign_from_model_example.rb +54 -0
  8. data/examples/model_name_example.rb +31 -0
  9. data/examples/normalization_example.rb +39 -0
  10. data/examples/post_form_example.rb +52 -0
  11. data/lib/blanks/association_proxy.rb +92 -0
  12. data/lib/blanks/associations.rb +58 -0
  13. data/lib/blanks/base.rb +256 -0
  14. data/lib/blanks/model_naming.rb +24 -0
  15. data/lib/blanks/nested_attributes.rb +131 -0
  16. data/lib/blanks/normalization.rb +38 -0
  17. data/lib/blanks/version.rb +3 -0
  18. data/lib/blanks.rb +18 -0
  19. data/spec/blanks/association_proxy_spec.rb +214 -0
  20. data/spec/blanks/associations_spec.rb +185 -0
  21. data/spec/blanks/attributes_extraction_spec.rb +138 -0
  22. data/spec/blanks/base_spec.rb +361 -0
  23. data/spec/blanks/callbacks_spec.rb +60 -0
  24. data/spec/blanks/custom_primary_key_spec.rb +168 -0
  25. data/spec/blanks/dirty_tracking_spec.rb +61 -0
  26. data/spec/blanks/i18n_spec.rb +33 -0
  27. data/spec/blanks/id_tracking_spec.rb +168 -0
  28. data/spec/blanks/inherit_attributes_from_spec.rb +148 -0
  29. data/spec/blanks/inherit_validations_from_spec.rb +260 -0
  30. data/spec/blanks/model_naming_spec.rb +82 -0
  31. data/spec/blanks/nested_attributes_spec.rb +378 -0
  32. data/spec/blanks/normalization_spec.rb +122 -0
  33. data/spec/dummy/Gemfile +10 -0
  34. data/spec/dummy/Gemfile.lock +242 -0
  35. data/spec/dummy/Rakefile +5 -0
  36. data/spec/dummy/app/controllers/application_controller.rb +4 -0
  37. data/spec/dummy/app/controllers/simple_form/articles_controller.rb +65 -0
  38. data/spec/dummy/app/controllers/simple_form/posts_controller.rb +59 -0
  39. data/spec/dummy/app/controllers/standard/articles_controller.rb +65 -0
  40. data/spec/dummy/app/controllers/standard/posts_controller.rb +59 -0
  41. data/spec/dummy/app/forms/article_form.rb +17 -0
  42. data/spec/dummy/app/forms/cover_image_form.rb +9 -0
  43. data/spec/dummy/app/forms/post_form.rb +10 -0
  44. data/spec/dummy/app/forms/tag_form.rb +12 -0
  45. data/spec/dummy/app/models/application_record.rb +5 -0
  46. data/spec/dummy/app/models/article.rb +11 -0
  47. data/spec/dummy/app/models/cover_image.rb +7 -0
  48. data/spec/dummy/app/models/post.rb +8 -0
  49. data/spec/dummy/app/models/tag.rb +7 -0
  50. data/spec/dummy/app/views/layouts/application.html.erb +43 -0
  51. data/spec/dummy/app/views/simple_form/articles/_form.html.erb +43 -0
  52. data/spec/dummy/app/views/simple_form/articles/edit.html.erb +5 -0
  53. data/spec/dummy/app/views/simple_form/articles/index.html.erb +29 -0
  54. data/spec/dummy/app/views/simple_form/articles/new.html.erb +5 -0
  55. data/spec/dummy/app/views/simple_form/articles/show.html.erb +29 -0
  56. data/spec/dummy/app/views/simple_form/posts/_form.html.erb +19 -0
  57. data/spec/dummy/app/views/simple_form/posts/edit.html.erb +5 -0
  58. data/spec/dummy/app/views/simple_form/posts/index.html.erb +27 -0
  59. data/spec/dummy/app/views/simple_form/posts/new.html.erb +5 -0
  60. data/spec/dummy/app/views/simple_form/posts/show.html.erb +12 -0
  61. data/spec/dummy/app/views/standard/articles/_form.html.erb +61 -0
  62. data/spec/dummy/app/views/standard/articles/edit.html.erb +5 -0
  63. data/spec/dummy/app/views/standard/articles/index.html.erb +29 -0
  64. data/spec/dummy/app/views/standard/articles/new.html.erb +5 -0
  65. data/spec/dummy/app/views/standard/articles/show.html.erb +29 -0
  66. data/spec/dummy/app/views/standard/posts/_form.html.erb +30 -0
  67. data/spec/dummy/app/views/standard/posts/edit.html.erb +5 -0
  68. data/spec/dummy/app/views/standard/posts/index.html.erb +27 -0
  69. data/spec/dummy/app/views/standard/posts/new.html.erb +5 -0
  70. data/spec/dummy/app/views/standard/posts/show.html.erb +12 -0
  71. data/spec/dummy/bin/rails +6 -0
  72. data/spec/dummy/config/application.rb +18 -0
  73. data/spec/dummy/config/boot.rb +5 -0
  74. data/spec/dummy/config/database.yml +12 -0
  75. data/spec/dummy/config/environment.rb +5 -0
  76. data/spec/dummy/config/environments/development.rb +9 -0
  77. data/spec/dummy/config/environments/test.rb +8 -0
  78. data/spec/dummy/config/initializers/simple_form.rb +21 -0
  79. data/spec/dummy/config/routes.rb +15 -0
  80. data/spec/dummy/config/storage.yml +3 -0
  81. data/spec/dummy/config.ru +5 -0
  82. data/spec/dummy/db/migrate/1_create_posts.rb +12 -0
  83. data/spec/dummy/db/migrate/2_create_articles.rb +12 -0
  84. data/spec/dummy/db/migrate/3_create_cover_images.rb +12 -0
  85. data/spec/dummy/db/migrate/4_create_tags.rb +12 -0
  86. data/spec/dummy/db/migrate/5_create_active_storage_tables.rb +36 -0
  87. data/spec/dummy/db/schema.rb +82 -0
  88. data/spec/dummy/spec/examples.txt +145 -0
  89. data/spec/dummy/tmp/local_secret.txt +1 -0
  90. data/spec/examples.txt +157 -0
  91. data/spec/spec_helper.rb +21 -0
  92. metadata +159 -0
@@ -0,0 +1,242 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ blanks (0.1.0)
5
+ activemodel (>= 6.0)
6
+ activesupport (>= 6.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ action_text-trix (2.1.15)
12
+ railties
13
+ actioncable (8.1.1)
14
+ actionpack (= 8.1.1)
15
+ activesupport (= 8.1.1)
16
+ nio4r (~> 2.0)
17
+ websocket-driver (>= 0.6.1)
18
+ zeitwerk (~> 2.6)
19
+ actionmailbox (8.1.1)
20
+ actionpack (= 8.1.1)
21
+ activejob (= 8.1.1)
22
+ activerecord (= 8.1.1)
23
+ activestorage (= 8.1.1)
24
+ activesupport (= 8.1.1)
25
+ mail (>= 2.8.0)
26
+ actionmailer (8.1.1)
27
+ actionpack (= 8.1.1)
28
+ actionview (= 8.1.1)
29
+ activejob (= 8.1.1)
30
+ activesupport (= 8.1.1)
31
+ mail (>= 2.8.0)
32
+ rails-dom-testing (~> 2.2)
33
+ actionpack (8.1.1)
34
+ actionview (= 8.1.1)
35
+ activesupport (= 8.1.1)
36
+ nokogiri (>= 1.8.5)
37
+ rack (>= 2.2.4)
38
+ rack-session (>= 1.0.1)
39
+ rack-test (>= 0.6.3)
40
+ rails-dom-testing (~> 2.2)
41
+ rails-html-sanitizer (~> 1.6)
42
+ useragent (~> 0.16)
43
+ actiontext (8.1.1)
44
+ action_text-trix (~> 2.1.15)
45
+ actionpack (= 8.1.1)
46
+ activerecord (= 8.1.1)
47
+ activestorage (= 8.1.1)
48
+ activesupport (= 8.1.1)
49
+ globalid (>= 0.6.0)
50
+ nokogiri (>= 1.8.5)
51
+ actionview (8.1.1)
52
+ activesupport (= 8.1.1)
53
+ builder (~> 3.1)
54
+ erubi (~> 1.11)
55
+ rails-dom-testing (~> 2.2)
56
+ rails-html-sanitizer (~> 1.6)
57
+ activejob (8.1.1)
58
+ activesupport (= 8.1.1)
59
+ globalid (>= 0.3.6)
60
+ activemodel (8.1.1)
61
+ activesupport (= 8.1.1)
62
+ activerecord (8.1.1)
63
+ activemodel (= 8.1.1)
64
+ activesupport (= 8.1.1)
65
+ timeout (>= 0.4.0)
66
+ activestorage (8.1.1)
67
+ actionpack (= 8.1.1)
68
+ activejob (= 8.1.1)
69
+ activerecord (= 8.1.1)
70
+ activesupport (= 8.1.1)
71
+ marcel (~> 1.0)
72
+ activesupport (8.1.1)
73
+ base64
74
+ bigdecimal
75
+ concurrent-ruby (~> 1.0, >= 1.3.1)
76
+ connection_pool (>= 2.2.5)
77
+ drb
78
+ i18n (>= 1.6, < 2)
79
+ json
80
+ logger (>= 1.4.2)
81
+ minitest (>= 5.1)
82
+ securerandom (>= 0.3)
83
+ tzinfo (~> 2.0, >= 2.0.5)
84
+ uri (>= 0.13.1)
85
+ base64 (0.3.0)
86
+ bigdecimal (4.0.0)
87
+ builder (3.3.0)
88
+ concurrent-ruby (1.3.6)
89
+ connection_pool (3.0.2)
90
+ crass (1.0.6)
91
+ date (3.5.1)
92
+ drb (2.2.3)
93
+ erb (6.0.1)
94
+ erubi (1.13.1)
95
+ globalid (1.3.0)
96
+ activesupport (>= 6.1)
97
+ i18n (1.14.7)
98
+ concurrent-ruby (~> 1.0)
99
+ io-console (0.8.2)
100
+ irb (1.16.0)
101
+ pp (>= 0.6.0)
102
+ rdoc (>= 4.0.0)
103
+ reline (>= 0.4.2)
104
+ json (2.18.0)
105
+ logger (1.7.0)
106
+ loofah (2.25.0)
107
+ crass (~> 1.0.2)
108
+ nokogiri (>= 1.12.0)
109
+ mail (2.9.0)
110
+ logger
111
+ mini_mime (>= 0.1.1)
112
+ net-imap
113
+ net-pop
114
+ net-smtp
115
+ marcel (1.1.0)
116
+ mini_mime (1.1.5)
117
+ minitest (5.27.0)
118
+ net-imap (0.6.1)
119
+ date
120
+ net-protocol
121
+ net-pop (0.1.2)
122
+ net-protocol
123
+ net-protocol (0.2.2)
124
+ timeout
125
+ net-smtp (0.5.1)
126
+ net-protocol
127
+ nio4r (2.7.5)
128
+ nokogiri (1.18.10-aarch64-linux-gnu)
129
+ racc (~> 1.4)
130
+ nokogiri (1.18.10-aarch64-linux-musl)
131
+ racc (~> 1.4)
132
+ nokogiri (1.18.10-arm-linux-gnu)
133
+ racc (~> 1.4)
134
+ nokogiri (1.18.10-arm-linux-musl)
135
+ racc (~> 1.4)
136
+ nokogiri (1.18.10-arm64-darwin)
137
+ racc (~> 1.4)
138
+ nokogiri (1.18.10-x86_64-darwin)
139
+ racc (~> 1.4)
140
+ nokogiri (1.18.10-x86_64-linux-gnu)
141
+ racc (~> 1.4)
142
+ nokogiri (1.18.10-x86_64-linux-musl)
143
+ racc (~> 1.4)
144
+ pp (0.6.3)
145
+ prettyprint
146
+ prettyprint (0.2.0)
147
+ psych (5.3.1)
148
+ date
149
+ stringio
150
+ puma (7.1.0)
151
+ nio4r (~> 2.0)
152
+ racc (1.8.1)
153
+ rack (3.2.4)
154
+ rack-session (2.1.1)
155
+ base64 (>= 0.1.0)
156
+ rack (>= 3.0.0)
157
+ rack-test (2.2.0)
158
+ rack (>= 1.3)
159
+ rackup (2.3.1)
160
+ rack (>= 3)
161
+ rails (8.1.1)
162
+ actioncable (= 8.1.1)
163
+ actionmailbox (= 8.1.1)
164
+ actionmailer (= 8.1.1)
165
+ actionpack (= 8.1.1)
166
+ actiontext (= 8.1.1)
167
+ actionview (= 8.1.1)
168
+ activejob (= 8.1.1)
169
+ activemodel (= 8.1.1)
170
+ activerecord (= 8.1.1)
171
+ activestorage (= 8.1.1)
172
+ activesupport (= 8.1.1)
173
+ bundler (>= 1.15.0)
174
+ railties (= 8.1.1)
175
+ rails-dom-testing (2.3.0)
176
+ activesupport (>= 5.0.0)
177
+ minitest
178
+ nokogiri (>= 1.6)
179
+ rails-html-sanitizer (1.6.2)
180
+ loofah (~> 2.21)
181
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
182
+ railties (8.1.1)
183
+ actionpack (= 8.1.1)
184
+ activesupport (= 8.1.1)
185
+ irb (~> 1.13)
186
+ rackup (>= 1.0.0)
187
+ rake (>= 12.2)
188
+ thor (~> 1.0, >= 1.2.2)
189
+ tsort (>= 0.2)
190
+ zeitwerk (~> 2.6)
191
+ rake (13.3.1)
192
+ rdoc (6.17.0)
193
+ erb
194
+ psych (>= 4.0.0)
195
+ tsort
196
+ reline (0.6.3)
197
+ io-console (~> 0.5)
198
+ securerandom (0.4.1)
199
+ simple_form (5.4.0)
200
+ actionpack (>= 7.0)
201
+ activemodel (>= 7.0)
202
+ sqlite3 (2.8.1-aarch64-linux-gnu)
203
+ sqlite3 (2.8.1-aarch64-linux-musl)
204
+ sqlite3 (2.8.1-arm-linux-gnu)
205
+ sqlite3 (2.8.1-arm-linux-musl)
206
+ sqlite3 (2.8.1-arm64-darwin)
207
+ sqlite3 (2.8.1-x86_64-darwin)
208
+ sqlite3 (2.8.1-x86_64-linux-gnu)
209
+ sqlite3 (2.8.1-x86_64-linux-musl)
210
+ stringio (3.2.0)
211
+ thor (1.4.0)
212
+ timeout (0.6.0)
213
+ tsort (0.2.0)
214
+ tzinfo (2.0.6)
215
+ concurrent-ruby (~> 1.0)
216
+ uri (1.1.1)
217
+ useragent (0.16.11)
218
+ websocket-driver (0.8.0)
219
+ base64
220
+ websocket-extensions (>= 0.1.0)
221
+ websocket-extensions (0.1.5)
222
+ zeitwerk (2.7.4)
223
+
224
+ PLATFORMS
225
+ aarch64-linux-gnu
226
+ aarch64-linux-musl
227
+ arm-linux-gnu
228
+ arm-linux-musl
229
+ arm64-darwin
230
+ x86_64-darwin
231
+ x86_64-linux-gnu
232
+ x86_64-linux-musl
233
+
234
+ DEPENDENCIES
235
+ blanks!
236
+ puma (~> 7.1)
237
+ rails (>= 7.0)
238
+ simple_form
239
+ sqlite3
240
+
241
+ BUNDLED WITH
242
+ 2.7.2
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config/application"
4
+
5
+ Rails.application.load_tasks
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationController < ActionController::Base
4
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleForm
4
+ class ArticlesController < ApplicationController
5
+ before_action :set_article, only: [:show, :edit, :update, :destroy]
6
+
7
+ def index
8
+ @articles = Article.includes(:cover_image, :tags).order(created_at: :desc)
9
+ end
10
+
11
+ def show
12
+ end
13
+
14
+ def new
15
+ @form = ArticleForm.new
16
+ @form.build_cover_image
17
+ 3.times { @form.tags.new }
18
+ end
19
+
20
+ def create
21
+ @form = ArticleForm.new(article_params)
22
+
23
+ if @form.valid?
24
+ @article = Article.create!(@form.assignable_attributes)
25
+ redirect_to simple_form_article_path(@article), notice: "article created"
26
+ else
27
+ render :new, status: :unprocessable_entity
28
+ end
29
+ end
30
+
31
+ def edit
32
+ @form = ArticleForm.from_model(@article)
33
+ end
34
+
35
+ def update
36
+ @form = ArticleForm.new(article_params)
37
+
38
+ if @form.valid?
39
+ @article.update!(@form.assignable_attributes)
40
+ redirect_to simple_form_article_path(@article), notice: "article updated"
41
+ else
42
+ render :edit, status: :unprocessable_entity
43
+ end
44
+ end
45
+
46
+ def destroy
47
+ @article.destroy!
48
+ redirect_to simple_form_articles_path, notice: "article deleted"
49
+ end
50
+
51
+ private
52
+
53
+ def set_article
54
+ @article = Article.find(params[:id])
55
+ end
56
+
57
+ def article_params
58
+ params.require(:article).permit(
59
+ :title, :body, :author_name,
60
+ cover_image_attributes: [:id, :url, :alt_text, :_destroy],
61
+ tags_attributes: [:id, :name, :color, :_destroy]
62
+ )
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleForm
4
+ class PostsController < ApplicationController
5
+ before_action :set_post, only: [:show, :edit, :update, :destroy]
6
+
7
+ def index
8
+ @posts = Post.all.order(created_at: :desc)
9
+ end
10
+
11
+ def show
12
+ end
13
+
14
+ def new
15
+ @form = PostForm.new
16
+ end
17
+
18
+ def create
19
+ @form = PostForm.new(post_params)
20
+
21
+ if @form.valid?
22
+ @post = Post.create!(@form.assignable_attributes)
23
+ redirect_to simple_form_post_path(@post), notice: "post created"
24
+ else
25
+ render :new, status: :unprocessable_entity
26
+ end
27
+ end
28
+
29
+ def edit
30
+ @form = PostForm.from_model(@post)
31
+ end
32
+
33
+ def update
34
+ @form = PostForm.new(post_params)
35
+
36
+ if @form.valid?
37
+ @post.update!(@form.assignable_attributes)
38
+ redirect_to simple_form_post_path(@post), notice: "post updated"
39
+ else
40
+ render :edit, status: :unprocessable_entity
41
+ end
42
+ end
43
+
44
+ def destroy
45
+ @post.destroy!
46
+ redirect_to simple_form_posts_path, notice: "post deleted"
47
+ end
48
+
49
+ private
50
+
51
+ def set_post
52
+ @post = Post.find(params[:id])
53
+ end
54
+
55
+ def post_params
56
+ params.require(:post).permit(:title, :content, :published)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Standard
4
+ class ArticlesController < ApplicationController
5
+ before_action :set_article, only: [:show, :edit, :update, :destroy]
6
+
7
+ def index
8
+ @articles = Article.includes(:cover_image, :tags).order(created_at: :desc)
9
+ end
10
+
11
+ def show
12
+ end
13
+
14
+ def new
15
+ @form = ArticleForm.new
16
+ @form.build_cover_image
17
+ 3.times { @form.tags.new }
18
+ end
19
+
20
+ def create
21
+ @form = ArticleForm.new(article_params)
22
+
23
+ if @form.valid?
24
+ @article = Article.create!(@form.assignable_attributes)
25
+ redirect_to standard_article_path(@article), notice: "article created"
26
+ else
27
+ render :new, status: :unprocessable_entity
28
+ end
29
+ end
30
+
31
+ def edit
32
+ @form = ArticleForm.from_model(@article)
33
+ end
34
+
35
+ def update
36
+ @form = ArticleForm.new(article_params)
37
+
38
+ if @form.valid?
39
+ @article.update!(@form.assignable_attributes)
40
+ redirect_to standard_article_path(@article), notice: "article updated"
41
+ else
42
+ render :edit, status: :unprocessable_entity
43
+ end
44
+ end
45
+
46
+ def destroy
47
+ @article.destroy!
48
+ redirect_to standard_articles_path, notice: "article deleted"
49
+ end
50
+
51
+ private
52
+
53
+ def set_article
54
+ @article = Article.find(params[:id])
55
+ end
56
+
57
+ def article_params
58
+ params.require(:article).permit(
59
+ :title, :body, :author_name,
60
+ cover_image_attributes: [:id, :url, :alt_text, :_destroy],
61
+ tags_attributes: [:id, :name, :color, :_destroy]
62
+ )
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Standard
4
+ class PostsController < ApplicationController
5
+ before_action :set_post, only: [:show, :edit, :update, :destroy]
6
+
7
+ def index
8
+ @posts = Post.all.order(created_at: :desc)
9
+ end
10
+
11
+ def show
12
+ end
13
+
14
+ def new
15
+ @form = PostForm.new
16
+ end
17
+
18
+ def create
19
+ @form = PostForm.new(post_params)
20
+
21
+ if @form.valid?
22
+ @post = Post.create!(@form.assignable_attributes)
23
+ redirect_to standard_post_path(@post), notice: "post created"
24
+ else
25
+ render :new, status: :unprocessable_entity
26
+ end
27
+ end
28
+
29
+ def edit
30
+ @form = PostForm.from_model(@post)
31
+ end
32
+
33
+ def update
34
+ @form = PostForm.new(post_params)
35
+
36
+ if @form.valid?
37
+ @post.update!(@form.assignable_attributes)
38
+ redirect_to standard_post_path(@post), notice: "post updated"
39
+ else
40
+ render :edit, status: :unprocessable_entity
41
+ end
42
+ end
43
+
44
+ def destroy
45
+ @post.destroy!
46
+ redirect_to standard_posts_path, notice: "post deleted"
47
+ end
48
+
49
+ private
50
+
51
+ def set_post
52
+ @post = Post.find(params[:id])
53
+ end
54
+
55
+ def post_params
56
+ params.require(:post).permit(:title, :content, :published)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ArticleForm < Blanks::Base
4
+ attribute :id, :integer
5
+ attribute :title, :string
6
+ attribute :body, :string
7
+ attribute :author_name, :string
8
+
9
+ has_one :cover_image
10
+ has_many :tags, allow_destroy: true
11
+
12
+ normalizes :title, with: ->(title) { title&.strip }
13
+ normalizes :author_name, with: ->(name) { name&.strip }
14
+
15
+ validates :title, presence: true
16
+ validates :body, presence: true
17
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CoverImageForm < Blanks::Base
4
+ attribute :id, :integer
5
+ attribute :url, :string
6
+ attribute :alt_text, :string
7
+
8
+ validates :url, presence: true
9
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PostForm < Blanks::Base
4
+ inherit_attributes_from Post, only: [:id, :title, :content, :published]
5
+ inherit_validations_from Post, only: [:title]
6
+
7
+ normalizes :title, with: ->(title) { title.strip }
8
+
9
+ validates :content, presence: true
10
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TagForm < Blanks::Base
4
+ attribute :id, :integer
5
+ attribute :name, :string
6
+ attribute :color, :string
7
+
8
+ normalizes :color, with: ->(color) { color&.downcase }
9
+ normalizes :name, with: ->(name) { name&.strip }
10
+
11
+ validates :name, presence: true
12
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Article < ApplicationRecord
4
+ has_one :cover_image, dependent: :destroy
5
+ has_many :tags, dependent: :destroy
6
+
7
+ accepts_nested_attributes_for :cover_image, allow_destroy: true
8
+ accepts_nested_attributes_for :tags, allow_destroy: true
9
+
10
+ validates :title, presence: true
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CoverImage < ApplicationRecord
4
+ belongs_to :article
5
+
6
+ validates :url, presence: true
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Post < ApplicationRecord
4
+ has_one_attached :featured_image
5
+
6
+ validates :title, presence: true, length: { minimum: 3 }
7
+ validates :content, presence: true
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tag < ApplicationRecord
4
+ belongs_to :article
5
+
6
+ validates :name, presence: true
7
+ end
@@ -0,0 +1,43 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Blanks Demo</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
8
+ nav { margin-bottom: 20px; padding: 10px 0; border-bottom: 1px solid #ddd; }
9
+ nav a { margin-right: 15px; }
10
+ .flash { padding: 10px; margin-bottom: 15px; background: #d4edda; border: 1px solid #c3e6cb; }
11
+ .error { color: #dc3545; font-size: 14px; }
12
+ .field { margin-bottom: 15px; }
13
+ .field label { display: block; margin-bottom: 5px; font-weight: 600; }
14
+ .field input, .field textarea, .field select { width: 100%; padding: 8px; border: 1px solid #ccc; box-sizing: border-box; }
15
+ .field input[type="checkbox"] { width: auto; }
16
+ .nested { border: 1px solid #eee; padding: 15px; margin: 10px 0; background: #fafafa; }
17
+ .nested h4 { margin-top: 0; }
18
+ .actions { margin-top: 20px; }
19
+ .btn { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
20
+ .btn-danger { background: #dc3545; }
21
+ table { width: 100%; border-collapse: collapse; }
22
+ th, td { padding: 10px; border: 1px solid #ddd; text-align: left; }
23
+ th { background: #f5f5f5; }
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <nav>
28
+ <strong>SimpleForm:</strong>
29
+ <%= link_to "Posts", simple_form_posts_path %>
30
+ <%= link_to "Articles", simple_form_articles_path %>
31
+ |
32
+ <strong>Standard:</strong>
33
+ <%= link_to "Posts", standard_posts_path %>
34
+ <%= link_to "Articles", standard_articles_path %>
35
+ </nav>
36
+
37
+ <% if notice %>
38
+ <div class="flash"><%= notice %></div>
39
+ <% end %>
40
+
41
+ <%= yield %>
42
+ </body>
43
+ </html>
@@ -0,0 +1,43 @@
1
+ <%= simple_form_for form, url: form.persisted? ? simple_form_article_path(form) : simple_form_articles_path do |f| %>
2
+ <% if form.errors.any? %>
3
+ <div class="error">
4
+ <ul>
5
+ <% form.errors.full_messages.each do |msg| %>
6
+ <li><%= msg %></li>
7
+ <% end %>
8
+ </ul>
9
+ </div>
10
+ <% end %>
11
+
12
+ <%= f.input :title %>
13
+ <%= f.input :body, as: :text %>
14
+ <%= f.input :author_name %>
15
+
16
+ <div class="nested">
17
+ <h4>Cover Image</h4>
18
+ <%= f.simple_fields_for :cover_image do |ci| %>
19
+ <%= ci.input :url, placeholder: "https://example.com/image.jpg" %>
20
+ <%= ci.input :alt_text %>
21
+ <% end %>
22
+ </div>
23
+
24
+ <div class="nested">
25
+ <h4>Tags</h4>
26
+ <%= f.simple_fields_for :tags do |tag| %>
27
+ <div style="display: flex; gap: 10px; margin-bottom: 10px;">
28
+ <% if tag.object.persisted? %>
29
+ <%= tag.hidden_field :id %>
30
+ <% end %>
31
+ <%= tag.input :name, wrapper: false, label: false, placeholder: "Tag name" %>
32
+ <%= tag.input :color, wrapper: false, label: false, placeholder: "#ff0000" %>
33
+ <% if tag.object.persisted? %>
34
+ <%= tag.input :_destroy, as: :boolean, wrapper: false, label: "Delete" %>
35
+ <% end %>
36
+ </div>
37
+ <% end %>
38
+ </div>
39
+
40
+ <div class="actions">
41
+ <%= f.button :submit, class: "btn" %>
42
+ </div>
43
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <h1>Edit Article (SimpleForm with nested)</h1>
2
+
3
+ <%= render "form", form: @form %>
4
+
5
+ <p><%= link_to "Back", simple_form_articles_path %></p>