magicka 1.0.0 → 1.2.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +6 -9
  3. data/.github/copilot-instructions.md +229 -0
  4. data/.github/magicka-usage.md +394 -0
  5. data/.github/sinclair-usage.md +492 -0
  6. data/.gitignore +1 -1
  7. data/.rubocop.yml +19 -2
  8. data/.rubocop_todo.yml +12 -0
  9. data/Dockerfile +2 -2
  10. data/Gemfile +33 -0
  11. data/Makefile +21 -0
  12. data/README.md +6 -7
  13. data/Rakefile +3 -0
  14. data/config/check_specs.yml +0 -2
  15. data/config/yardstick.yml +8 -2
  16. data/lib/magicka/aggregator/class_methods.rb +3 -1
  17. data/lib/magicka/aggregator/method_builder.rb +35 -28
  18. data/lib/magicka/aggregator.rb +19 -9
  19. data/lib/magicka/display.rb +1 -1
  20. data/lib/magicka/element/class_methods.rb +5 -3
  21. data/lib/magicka/element.rb +4 -2
  22. data/lib/magicka/form_element.rb +1 -1
  23. data/lib/magicka/helper/aggregator_options.rb +64 -0
  24. data/lib/magicka/helper/class_methods.rb +10 -2
  25. data/lib/magicka/helper/method_builder.rb +10 -6
  26. data/lib/magicka/helper.rb +12 -6
  27. data/lib/magicka/version.rb +1 -1
  28. data/magicka.gemspec +3 -31
  29. data/magicka.jpg +0 -0
  30. data/spec/dummy/app/controllers/application_controller.rb +1 -0
  31. data/spec/dummy/app/controllers/documents_controller.rb +9 -0
  32. data/spec/dummy/app/models/document.rb +5 -0
  33. data/spec/dummy/app/models/magicka/data_entry.rb +7 -0
  34. data/spec/dummy/app/models/magicka/data_table.rb +6 -0
  35. data/spec/dummy/app/views/documents/show.html.erb +6 -0
  36. data/spec/dummy/app/views/templates/display/_data_entry.html.erb +2 -0
  37. data/spec/dummy/bin/setup +0 -4
  38. data/spec/dummy/bin/update +0 -4
  39. data/spec/dummy/config/environments/development.rb +2 -1
  40. data/spec/dummy/config/environments/production.rb +2 -2
  41. data/spec/dummy/config/initializers/magicka.rb +7 -0
  42. data/spec/dummy/config/puma.rb +3 -3
  43. data/spec/dummy/config/routes.rb +1 -0
  44. data/spec/dummy/db/schema.rb +5 -0
  45. data/spec/integration/yard/magicka/helper_spec.rb +33 -0
  46. data/spec/lib/magicka/aggregator/class_methods_spec.rb +352 -0
  47. data/spec/lib/magicka/aggregator/method_builder_spec.rb +29 -0
  48. data/spec/lib/magicka/aggregator_spec.rb +1 -222
  49. data/spec/lib/magicka/button_spec.rb +1 -1
  50. data/spec/lib/magicka/display_spec.rb +1 -1
  51. data/spec/lib/magicka/element/class_methods_spec.rb +1 -1
  52. data/spec/lib/magicka/element_spec.rb +1 -1
  53. data/spec/lib/magicka/form_element_spec.rb +1 -1
  54. data/spec/lib/magicka/form_spec.rb +1 -1
  55. data/spec/lib/magicka/helper/aggregator_options_spec.rb +99 -0
  56. data/spec/lib/magicka/helper/class_methods_spec.rb +101 -0
  57. data/spec/lib/magicka/helper_spec.rb +2 -18
  58. data/spec/lib/magicka/input_spec.rb +1 -1
  59. data/spec/lib/magicka/select_spec.rb +1 -1
  60. data/spec/lib/magicka/text_spec.rb +1 -1
  61. data/spec/spec_helper.rb +7 -1
  62. data/spec/support/factories/document.rb +7 -0
  63. data/spec/support/factory_bot.rb +7 -0
  64. data/spec/support/models/custom_aggregator.rb +12 -0
  65. data/spec/support/models/my_element.rb +6 -0
  66. metadata +29 -372
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 662d942d270879b97fb0ee3be36a41c4448db92ab11f2f3462fd4315a53b06d7
4
- data.tar.gz: b6dc02755727b9f84ab61dd66eb34d71a8ecac53da565594e49f69c1e632b784
3
+ metadata.gz: c084543fe6654f6bf8e182380dee9d814629d2aa43a4c322463488e1653b463c
4
+ data.tar.gz: 055c1153460da2568ab52b8ffb0c071a9ca72850d03def8f8cb1af05f88fbed1
5
5
  SHA512:
6
- metadata.gz: 7bad26b83421ea3d47a6d002aa521f754fea7df036ed89ebf457297b076ffc2118bbdaf3fe2465a8cff8e1b3f176ef02dddb0211b20e3b2ab9f0c010b1cf75f6
7
- data.tar.gz: ce04771c5649517ee7c17b54978b61137c897e0e23118aac1806b4035ab96aafba6aef857f1cc498048e4ff37b37d0eef433f0133fd871cb364b19cedab5a08a
6
+ metadata.gz: 6801f95aaddcac8a9d85820c5dd2725a9c6dc75f37317ec70324ffe00fedfba7630562e9918a2e25b43d511b0be6292b1dac1c392d4b2ab013741ef48cb773bb
7
+ data.tar.gz: f8d8689c93d72a1da5888d2c91667ffe6f0a831d13ec767239b764415658dfb53ce59a9185f45759886d563b0683a4b2da70089803a7129d35d9344208952fbe
data/.circleci/config.yml CHANGED
@@ -18,18 +18,15 @@ workflows:
18
18
  only: /\d+\.\d+\.\d+/
19
19
  branches:
20
20
  only:
21
- - master
21
+ - main
22
22
  jobs:
23
23
  test:
24
24
  docker:
25
- - image: darthjee/circleci_rails_gems:1.2.0
25
+ - image: darthjee/circleci_rails_gems:2.1.0
26
26
  environment:
27
27
  PROJECT: magicka
28
28
  steps:
29
29
  - checkout
30
- - run:
31
- name: Prepare Coverage Test Report
32
- command: cc-test-reporter before-build
33
30
  - run:
34
31
  name: Bundle Install
35
32
  command: bundle install
@@ -37,11 +34,11 @@ jobs:
37
34
  name: RSpec
38
35
  command: bundle exec rspec
39
36
  - run:
40
- name: Coverage Test Report
41
- command: cc-test-reporter after-build --exit-code $?
37
+ name: Upload coverage to Codacy
38
+ command: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r coverage/lcov/project.lcov
42
39
  checks:
43
40
  docker:
44
- - image: darthjee/circleci_rails_gems:1.2.0
41
+ - image: darthjee/circleci_rails_gems:2.1.0
45
42
  environment:
46
43
  PROJECT: magicka
47
44
  steps:
@@ -66,7 +63,7 @@ jobs:
66
63
  command: check_specs
67
64
  build-and-release:
68
65
  docker:
69
- - image: darthjee/circleci_rails_gems:1.2.0
66
+ - image: darthjee/circleci_rails_gems:2.1.0
70
67
  environment:
71
68
  PROJECT: magicka
72
69
  steps:
@@ -0,0 +1,229 @@
1
+ # GitHub Copilot Instructions for Magicka
2
+
3
+ ## Project Overview
4
+
5
+ Magicka is a Ruby gem that facilitates the creation of HTML templates for forms and
6
+ displaying data, especially when working with JS applications like AngularJS. Its main
7
+ feature is providing a unified way to create form inputs and data display elements using
8
+ the same partial templates, avoiding HTML repetition by defining templates once and
9
+ reusing them for both forms and display views.
10
+
11
+ ### How It Works
12
+
13
+ - Uses aggregators like `magicka_form` and `magicka_display` that render the same
14
+ elements differently
15
+ - A single partial can be used for both creating forms (`new.html.erb`) and displaying
16
+ data (`show.html.erb`)
17
+ - Each element is paired with an element class, a template, and a method in an aggregator
18
+ - Supports conditional rendering (e.g., `form.only(:form)` for form-specific content)
19
+
20
+ ### Real-World Usage
21
+
22
+ The gem is used in several projects that demonstrate practical implementation patterns
23
+ and best practices:
24
+
25
+ - darthjee/oak
26
+ - darthjee/plague_inc
27
+ - darthjee/paperboy
28
+
29
+ ---
30
+
31
+ ## Language Requirements
32
+
33
+ - All pull requests, comments, documentation, and code must be written in **English**
34
+ - Maintain consistency in terminology and naming conventions across the codebase
35
+
36
+ ---
37
+
38
+ ## Testing Requirements
39
+
40
+ - Tests are **mandatory** for all code changes
41
+ - Files without tests should be included in `check_specs.yml`
42
+ - Ensure comprehensive test coverage for new features and changes
43
+ - Test both form and display rendering scenarios
44
+ - Test template generation and element rendering
45
+
46
+ ---
47
+
48
+ ## Documentation Requirements
49
+
50
+ - Use **YARD** format for all documentation
51
+ - Document all public methods, classes, and modules
52
+ - Include examples showing both `magicka_form` and `magicka_display` usage
53
+ - Document template creation and customization options
54
+ - Provide clear examples of element classes and aggregators
55
+
56
+ ---
57
+
58
+ ## Code Style and Design Principles
59
+
60
+ - Follow Sandi Metz principles from *99 Bottles of OOP*
61
+ - Keep classes and methods focused with **single responsibilities**
62
+ - Avoid violations of the **Law of Demeter**
63
+ - Prefer small, well-defined methods over large, complex ones
64
+ - Aim for **high cohesion and low coupling**
65
+ - Separate concerns: element classes, templates, and aggregators should have distinct
66
+ responsibilities
67
+
68
+ ---
69
+
70
+ ## Project-Specific Guidelines
71
+
72
+ ### Template Design
73
+
74
+ - Keep templates **DRY** (Don't Repeat Yourself)
75
+ - A single partial should work for both form and display contexts
76
+ - Use aggregator methods to handle context-specific rendering
77
+ - Minimize HTML duplication by leveraging the template system
78
+
79
+ ### Element Classes
80
+
81
+ - Each form element should have a corresponding element class
82
+ - Element classes should encapsulate rendering logic
83
+ - Keep element classes small and focused
84
+
85
+ ### Aggregators
86
+
87
+ - Different aggregators (form vs display) should share method signatures
88
+ - Aggregators should handle the context (form vs display) transparently
89
+ - Support conditional rendering for context-specific content
90
+
91
+ ### Best Practices
92
+
93
+ - When adding new element types, ensure they work in both form and display contexts
94
+ - Test template rendering in multiple scenarios
95
+ - Consider AngularJS integration patterns when applicable
96
+ - Follow Rails conventions and best practices
97
+ - Maintain backward compatibility when making changes
98
+
99
+ ---
100
+
101
+ ## Implementation Pattern
102
+
103
+ When implementing new features, follow this pattern:
104
+
105
+ ```ruby
106
+ # 1. Element class with single responsibility
107
+ module Magicka
108
+ class MyElement < Magicka::Element
109
+ with_attribute_locals :label, :field, :id
110
+ end
111
+ end
112
+
113
+ # 2. Register the element with both Form and Display aggregators
114
+ Magicka::Form.with_element(Magicka::MyElement)
115
+ Magicka::Display.with_element(Magicka::MyElement)
116
+ ```
117
+
118
+ ```erb
119
+ <%# 3. Template for rendering (templates/form/_my_element.html.erb) %>
120
+ <div>
121
+ <label for="<%= field %>"><%= label %></label>
122
+ <input type="text" id="<%= id %>" name="<%= field %>" />
123
+ </div>
124
+ ```
125
+
126
+ ```ruby
127
+ # 4. Tests for both form and display contexts
128
+ RSpec.describe Magicka::MyElement do
129
+ it 'renders in form context' do
130
+ # ...
131
+ end
132
+
133
+ it 'renders in display context' do
134
+ # ...
135
+ end
136
+ end
137
+ ```
138
+
139
+ ```ruby
140
+ # 5. YARD documentation with examples
141
+ # @example Using in a form partial
142
+ # <%= form.my_element(:field_name) %>
143
+ #
144
+ # @example Using in a display partial
145
+ # <%= display.my_element(:field_name) %>
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Usage Examples
151
+
152
+ ### Basic Setup
153
+
154
+ ```ruby
155
+ # app/controllers/application_controller.rb
156
+ class ApplicationController < ActionController::Base
157
+ helper Magicka::Helper
158
+ end
159
+ ```
160
+
161
+ ### Shared Partial
162
+
163
+ ```erb
164
+ <%# app/views/people/_person_form.html.erb %>
165
+ <%= form.input(:first_name) %>
166
+ <%= form.input(:last_name) %>
167
+ <%= form.input(:age) %>
168
+ <%= form.select(:gender, options: %w[MALE FEMALE]) %>
169
+
170
+ <%= form.only(:form) do %>
171
+ <%# This block only appears in a form context %>
172
+ <%= form.button(ng_click: 'controller.save', text: 'Save') %>
173
+ <% end %>
174
+ ```
175
+
176
+ ### Form View
177
+
178
+ ```erb
179
+ <%# app/views/people/new.html.erb %>
180
+ <% magicka_form('controller.person') do |form| %>
181
+ <%= render partial: 'person_form', locals: { form: form } %>
182
+ <% end %>
183
+ ```
184
+
185
+ ### Display View
186
+
187
+ ```erb
188
+ <%# app/views/people/show.html.erb %>
189
+ <% magicka_display('controller.person') do |form| %>
190
+ <%= render partial: 'person_form', locals: { form: form } %>
191
+ <% end %>
192
+ ```
193
+
194
+ ### Custom Element
195
+
196
+ ```ruby
197
+ # config/initializers/magicka.rb
198
+ module Magicka
199
+ class MyTextInput < Magicka::Element
200
+ with_attribute_locals :label, :field, :id
201
+ end
202
+ end
203
+
204
+ Magicka::Form.with_element(Magicka::MyTextInput)
205
+ ```
206
+
207
+ ```erb
208
+ <%# templates/form/_my_text_input.html.erb %>
209
+ <div>
210
+ <label for="<%= field %>"><%= label %></label>
211
+ <input type="text" id="<%= id %>" name="<%= field %>" />
212
+ </div>
213
+ ```
214
+
215
+ ## Sinclair Usage
216
+
217
+ Magicka uses the **sinclair** gem extensively. Refer to [.github/sinclair-usage.md](.github/sinclair-usage.md) for the full usage guide.
218
+
219
+ Key features used in this project:
220
+
221
+ - **`Sinclair`** – Dynamically add instance/class methods to existing classes via builders
222
+ - **`Sinclair::Model`** – Quick plain-Ruby models with keyword initializers and equality support
223
+ - **`Sinclair::Options`** – Validated option/parameter objects with defaults
224
+ - **`Sinclair::Configurable`** – Read-only application configuration with defaults
225
+ - **`Sinclair::Comparable`** – Attribute-based `==` for models
226
+ - **`Sinclair::Matchers`** – RSpec matchers to test builder behaviour (`add_method`, `add_class_method`, `change_method`)
227
+
228
+ When building new features, prefer sinclair patterns for dynamic method generation, option handling, and plain-Ruby models over raw `attr_accessor` / `define_method` approaches.
229
+
@@ -0,0 +1,394 @@
1
+ # Magicka Usage Guide
2
+
3
+ Magicka is a Ruby gem that enables template reuse for both forms and data display in
4
+ Rails applications. Its main benefit is eliminating HTML duplication by allowing a
5
+ single partial to work in both form creation and data display contexts.
6
+
7
+ ## Basic Setup
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem 'magicka'
12
+ ```
13
+
14
+ ```bash
15
+ bundle install
16
+ ```
17
+
18
+ ```ruby
19
+ # app/controllers/application_controller.rb
20
+ class ApplicationController < ActionController::Base
21
+ helper Magicka::Helper
22
+ end
23
+ ```
24
+
25
+ ## Required: HTML Templates
26
+
27
+ > **Important:** Magicka does not ship any HTML templates. You must create all template
28
+ > files yourself — including those for the built-in elements (`input`, `select`,
29
+ > `button`, and the display-context `text`). No element will render until its
30
+ > corresponding template exists in your application.
31
+
32
+ ### Template Paths for Built-In Elements
33
+
34
+ Each element resolves its template path from a folder configured on the element class.
35
+ All paths below are relative to `app/views/`.
36
+
37
+ | Element method | Context | Template file | Available locals |
38
+ |----------------|---------|---------------|------------------|
39
+ | `form.input` | form | `templates/forms/_input.html.erb` | `field`, `label`, `ng_model`, `ng_errors`, `placeholder` |
40
+ | `form.input` | display | `templates/display/_text.html.erb` | `field`, `label`, `ng_model`, `ng_errors` |
41
+ | `form.select` | form | `templates/forms/_select.html.erb` | `field`, `label`, `ng_model`, `ng_errors`, `options` |
42
+ | `form.select` | display | `templates/display/_text.html.erb` | `field`, `label`, `ng_model`, `ng_errors` |
43
+ | `form.button` | form | `templates/forms/_button.html.erb` | `text`, `ng_click`, `ng_disabled`, `classes` |
44
+
45
+ `ng_model` and `ng_errors` are AngularJS expression strings (e.g.,
46
+ `"controller.person.first_name"` and `"controller.person.errors.first_name"`).
47
+
48
+ ### Minimal Starter Templates
49
+
50
+ These examples show the minimal markup needed to get each element working. Adapt them
51
+ to match your application's HTML structure and CSS framework.
52
+
53
+ **`app/views/templates/forms/_input.html.erb`**
54
+ ```erb
55
+ <div>
56
+ <label for="<%= field %>"><%= label %></label>
57
+ <input type="text"
58
+ id="<%= field %>"
59
+ ng-model="<%= ng_model %>"
60
+ placeholder="<%= placeholder %>" />
61
+ <span ng-show="<%= ng_errors %>">Invalid value</span>
62
+ </div>
63
+ ```
64
+
65
+ **`app/views/templates/forms/_select.html.erb`**
66
+ ```erb
67
+ <div>
68
+ <label for="<%= field %>"><%= label %></label>
69
+ <select id="<%= field %>" ng-model="<%= ng_model %>">
70
+ <% options.each do |option| %>
71
+ <option value="<%= option %>"><%= option %></option>
72
+ <% end %>
73
+ </select>
74
+ <span ng-show="<%= ng_errors %>">Invalid selection</span>
75
+ </div>
76
+ ```
77
+
78
+ **`app/views/templates/forms/_button.html.erb`**
79
+ ```erb
80
+ <button class="<%= classes %>"
81
+ ng-click="<%= ng_click %>"
82
+ ng-disabled="<%= ng_disabled %>">
83
+ <%= text %>
84
+ </button>
85
+ ```
86
+
87
+ **`app/views/templates/display/_text.html.erb`** (used by both `input` and `select` in
88
+ display context)
89
+ ```erb
90
+ <div>
91
+ <label><%= label %></label>
92
+ <span>{{ <%= ng_model %> }}</span>
93
+ </div>
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Core Concepts
99
+
100
+ ### Aggregators
101
+
102
+ Magicka provides two aggregators that share the same interface but render elements
103
+ differently based on context:
104
+
105
+ - **`magicka_form`** — renders elements as interactive form inputs
106
+ - **`magicka_display`** — renders elements as read-only display values
107
+
108
+ Both aggregators expose the same methods (`input`, `select`, `button`, `only`,
109
+ `except`, `with_model`), so a single partial can be passed to either without
110
+ modification.
111
+
112
+ ## Basic Usage Pattern
113
+
114
+ Define one shared partial that works for both contexts:
115
+
116
+ **Form view (`new.html.erb` or `edit.html.erb`):**
117
+ ```erb
118
+ <% magicka_form('controller.person') do |form| %>
119
+ <%= render partial: 'person_form', locals: { form: form } %>
120
+ <% end %>
121
+ ```
122
+
123
+ **Display view (`show.html.erb`):**
124
+ ```erb
125
+ <% magicka_display('controller.person') do |form| %>
126
+ <%= render partial: 'person_form', locals: { form: form } %>
127
+ <% end %>
128
+ ```
129
+
130
+ **Shared partial (`_person_form.html.erb`):**
131
+ ```erb
132
+ <%= form.input(:first_name) %>
133
+ <%= form.input(:last_name) %>
134
+ <%= form.input(:age) %>
135
+ <%= form.select(:gender, options: %w[MALE FEMALE]) %>
136
+
137
+ <%= form.only(:form) do %>
138
+ <!-- This block only appears in a form, not in display mode -->
139
+ <%= form.button(ng_click: 'controller.save', text: 'Save') %>
140
+ <% end %>
141
+ ```
142
+
143
+ ## Available Methods
144
+
145
+ ### Input Fields
146
+
147
+ ```erb
148
+ <%# Renders a text input (form) or the field's display value (display) %>
149
+ <%= form.input(:field_name) %>
150
+
151
+ <%# With a custom label %>
152
+ <%= form.input(:field_name, label: 'Custom Label') %>
153
+
154
+ <%# With a placeholder %>
155
+ <%= form.input(:field_name, placeholder: 'Enter value...') %>
156
+ ```
157
+
158
+ ### Select / Dropdown
159
+
160
+ ```erb
161
+ <%# Renders a <select> element (form) or the selected value (display) %>
162
+ <%= form.select(:field_name, options: %w[option_a option_b option_c]) %>
163
+
164
+ <%# With a custom label %>
165
+ <%= form.select(:role, label: 'User Role', options: %w[admin user guest]) %>
166
+ ```
167
+
168
+ ### Button
169
+
170
+ ```erb
171
+ <%# Renders a button (form only — becomes a noop in display context) %>
172
+ <%= form.button(text: 'Save', ng_click: 'controller.save') %>
173
+
174
+ <%# With a disabled condition %>
175
+ <%= form.button(text: 'Save', ng_click: 'controller.save', ng_disabled: 'controller.saving') %>
176
+ ```
177
+
178
+ `form.button` is automatically a no-op in display context, so it is safe to call it
179
+ outside of an `only(:form)` block when you do not need other conditional logic around
180
+ it.
181
+
182
+ ### Nested Model Scope
183
+
184
+ ```erb
185
+ <%# Scope a block of fields to a nested model %>
186
+ <%= form.with_model(:address) do |address_form| %>
187
+ <%= address_form.input(:street) %>
188
+ <%= address_form.input(:city) %>
189
+ <%= address_form.input(:zip_code) %>
190
+ <% end %>
191
+ ```
192
+
193
+ ## Conditional Rendering
194
+
195
+ Use `only` and `except` to render content selectively based on the current context.
196
+
197
+ ```erb
198
+ <%# Only in form context %>
199
+ <%= form.only(:form) do %>
200
+ <%= form.button(ng_click: 'controller.save', text: 'Save') %>
201
+ <% end %>
202
+ ```
203
+
204
+ ```erb
205
+ <%# Only in display context %>
206
+ <%= form.only(:display) do %>
207
+ <div class="timestamps">
208
+ Created at: <%= @person.created_at %>
209
+ </div>
210
+ <% end %>
211
+ ```
212
+
213
+ ```erb
214
+ <%# Everything except form context (equivalent to only(:display) when there are two contexts) %>
215
+ <%= form.except(:form) do %>
216
+ <p class="read-only-note">This record is read-only.</p>
217
+ <% end %>
218
+ ```
219
+
220
+ ## Best Practices
221
+
222
+ - **Single Partial Pattern**: Always create one partial that works for both form and
223
+ display contexts. Name it descriptively (e.g., `_person_form.html.erb`).
224
+ - **Context Parameter Naming**: Use `form` as the local variable name even in display
225
+ contexts — this keeps partial signatures consistent.
226
+ - **Template Organization**: Keep partials DRY by relying on Magicka's dual-context
227
+ rendering rather than duplicating markup.
228
+ - **Conditional Content**: Use `form.only(:form)` for submit buttons and other
229
+ form-only elements; use `form.only(:display)` for read-only annotations.
230
+ - **Model Binding**: Pass the AngularJS model path (e.g., `'controller.person'`) as
231
+ the first argument to `magicka_form` and `magicka_display`.
232
+ - **Labels**: Labels default to the capitalized, underscore-stripped field name
233
+ (e.g., `:first_name` → `'First name'`). Override with `label:` when needed.
234
+
235
+ ## Common Patterns
236
+
237
+ ### Simple CRUD Form
238
+
239
+ ```erb
240
+ <%# _user_form.html.erb %>
241
+ <%= form.input(:name) %>
242
+ <%= form.input(:email) %>
243
+ <%= form.input(:phone, label: 'Phone Number') %>
244
+ <%= form.select(:role, options: %w[admin user guest]) %>
245
+
246
+ <%= form.only(:form) do %>
247
+ <%= form.button(text: 'Save User', ng_click: 'controller.save') %>
248
+ <% end %>
249
+ ```
250
+
251
+ ### Nested Attributes
252
+
253
+ ```erb
254
+ <%# _company_form.html.erb %>
255
+ <%= form.input(:name, label: 'Company Name') %>
256
+
257
+ <%= form.with_model(:address) do |address_form| %>
258
+ <%= address_form.input(:street) %>
259
+ <%= address_form.input(:city) %>
260
+ <%= address_form.input(:country) %>
261
+ <% end %>
262
+ ```
263
+
264
+ ### Combining Conditional Blocks
265
+
266
+ ```erb
267
+ <%= form.input(:title) %>
268
+ <%= form.input(:body) %>
269
+
270
+ <%= form.only(:form) do %>
271
+ <%= form.select(:status, options: %w[draft published archived]) %>
272
+ <%= form.button(text: 'Publish', ng_click: 'controller.publish') %>
273
+ <% end %>
274
+
275
+ <%= form.only(:display) do %>
276
+ <p>Status: <%= @article.status %></p>
277
+ <p>Last updated: <%= @article.updated_at %></p>
278
+ <% end %>
279
+ ```
280
+
281
+ ## Integration with AngularJS
282
+
283
+ Magicka was designed with AngularJS in mind. The `ng_model` and `ng_errors` locals are
284
+ generated automatically for each form element based on the aggregator's model path and
285
+ the field name.
286
+
287
+ ### Automatic AngularJS Bindings
288
+
289
+ Given `magicka_form('controller.person')` and `form.input(:first_name)`, Magicka
290
+ automatically makes these locals available to the element template:
291
+
292
+ | Local | Generated value |
293
+ |-------|----------------|
294
+ | `ng_model` | `"controller.person.first_name"` |
295
+ | `ng_errors` | `"controller.person.errors.first_name"` |
296
+
297
+ ### Button Attributes
298
+
299
+ ```erb
300
+ <%= form.button(
301
+ text: 'Save',
302
+ ng_click: 'controller.save()',
303
+ ng_disabled: 'controller.form.$invalid'
304
+ ) %>
305
+ ```
306
+
307
+ ### Example AngularJS Controller Pattern
308
+
309
+ ```erb
310
+ <%# new.html.erb %>
311
+ <div ng-controller="PersonController as controller">
312
+ <% magicka_form('controller.person') do |form| %>
313
+ <%= render partial: 'person_form', locals: { form: form } %>
314
+ <% end %>
315
+ </div>
316
+ ```
317
+
318
+ ```erb
319
+ <%# show.html.erb %>
320
+ <div ng-controller="PersonController as controller">
321
+ <% magicka_display('controller.person') do |form| %>
322
+ <%= render partial: 'person_form', locals: { form: form } %>
323
+ <% end %>
324
+ </div>
325
+ ```
326
+
327
+ ## Custom Elements
328
+
329
+ You can extend Magicka with custom element types that integrate with both aggregators.
330
+
331
+ ```ruby
332
+ # config/initializers/magicka.rb
333
+ module Magicka
334
+ class DatePicker < Magicka::Element
335
+ with_attribute_locals :label, :field, :min_date, :max_date
336
+ template_folder 'templates/form'
337
+ end
338
+ end
339
+
340
+ Magicka::Form.with_element(Magicka::DatePicker)
341
+ Magicka::Display.with_element(Magicka::DatePicker)
342
+ ```
343
+
344
+ ```erb
345
+ <%# templates/form/_date_picker.html.erb %>
346
+ <div class="date-picker">
347
+ <label for="<%= field %>"><%= label %></label>
348
+ <input type="date"
349
+ id="<%= field %>"
350
+ name="<%= field %>"
351
+ min="<%= min_date %>"
352
+ max="<%= max_date %>" />
353
+ </div>
354
+ ```
355
+
356
+ ```erb
357
+ <%# Use in a shared partial %>
358
+ <%= form.date_picker(:birth_date, min_date: '1900-01-01', max_date: '2099-12-31') %>
359
+ ```
360
+
361
+ ## Real-World Examples
362
+
363
+ The following projects use Magicka and demonstrate practical integration patterns:
364
+
365
+ - **[darthjee/oak](https://github.com/darthjee/oak)** — authentication and
366
+ authorization patterns with Magicka forms
367
+ - **[darthjee/plague_inc](https://github.com/darthjee/plague_inc)** — game-specific
368
+ forms and display views
369
+ - **[darthjee/paperboy](https://github.com/darthjee/paperboy)** — content management
370
+ forms with edit/view modes
371
+
372
+ ## Common Use Cases
373
+
374
+ - **User registration and profile display** — same partial for sign-up form and profile page
375
+ - **Admin panels** — identical partial switches between edit mode and read-only view
376
+ - **Data entry with preview** — form and display side by side using the same partial
377
+ - **Multi-step forms with review** — final step renders the same partial in display mode
378
+ - **API-backed forms** — display context shows persisted values; form context allows editing
379
+
380
+ ## Tips for GitHub Copilot
381
+
382
+ - When creating a form, always plan the display view at the same time and write a single
383
+ shared partial.
384
+ - Create the shared partial (`_<resource>_form.html.erb`) before writing the form and
385
+ display views.
386
+ - Always name the block variable `form` in both `magicka_form` and `magicka_display`
387
+ for consistency across partials.
388
+ - Use `form.only(:form)` to wrap submit buttons so they do not appear in display mode.
389
+ - Prefer `form.button(...)` over raw `<button>` tags — it automatically becomes a
390
+ no-op in display context.
391
+ - Use `form.with_model(:nested_model)` instead of changing the parent aggregator's
392
+ model for nested resource sections.
393
+ - Lean on automatic label derivation (`:first_name` → `'First name'`) and only supply
394
+ an explicit `label:` when the default is not descriptive enough.