magicka 1.1.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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +6 -9
- data/.github/copilot-instructions.md +229 -0
- data/.github/magicka-usage.md +394 -0
- data/.github/sinclair-usage.md +492 -0
- data/.gitignore +1 -1
- data/.rubocop.yml +12 -2
- data/.rubocop_todo.yml +12 -0
- data/Dockerfile +2 -2
- data/Gemfile +33 -0
- data/Makefile +21 -0
- data/README.md +6 -7
- data/Rakefile +3 -0
- data/lib/magicka/aggregator/class_methods.rb +3 -1
- data/lib/magicka/aggregator.rb +3 -1
- data/lib/magicka/element/class_methods.rb +3 -3
- data/lib/magicka/form_element.rb +1 -1
- data/lib/magicka/version.rb +1 -1
- data/magicka.gemspec +3 -31
- data/magicka.jpg +0 -0
- data/spec/dummy/app/models/document.rb +1 -1
- data/spec/dummy/bin/setup +0 -4
- data/spec/dummy/bin/update +0 -4
- data/spec/dummy/config/environments/development.rb +1 -1
- data/spec/dummy/config/environments/production.rb +2 -2
- data/spec/dummy/config/puma.rb +3 -3
- data/spec/integration/yard/magicka/helper_spec.rb +1 -1
- data/spec/lib/magicka/aggregator/class_methods_spec.rb +1 -1
- data/spec/lib/magicka/aggregator_spec.rb +1 -1
- data/spec/lib/magicka/button_spec.rb +1 -1
- data/spec/lib/magicka/display_spec.rb +1 -1
- data/spec/lib/magicka/element/class_methods_spec.rb +1 -1
- data/spec/lib/magicka/element_spec.rb +1 -1
- data/spec/lib/magicka/form_element_spec.rb +1 -1
- data/spec/lib/magicka/form_spec.rb +1 -1
- data/spec/lib/magicka/input_spec.rb +1 -1
- data/spec/lib/magicka/select_spec.rb +1 -1
- data/spec/lib/magicka/text_spec.rb +1 -1
- data/spec/spec_helper.rb +7 -1
- metadata +12 -372
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c084543fe6654f6bf8e182380dee9d814629d2aa43a4c322463488e1653b463c
|
|
4
|
+
data.tar.gz: 055c1153460da2568ab52b8ffb0c071a9ca72850d03def8f8cb1af05f88fbed1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
21
|
+
- main
|
|
22
22
|
jobs:
|
|
23
23
|
test:
|
|
24
24
|
docker:
|
|
25
|
-
- image: darthjee/circleci_rails_gems:1.
|
|
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:
|
|
41
|
-
command:
|
|
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.
|
|
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.
|
|
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.
|