phlexible 3.1.1 → 3.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 335d7a198a6dd2e58d52abe064552a739cfd77c9910f294022bd4a96adf4ef92
4
- data.tar.gz: ee505e0e79afba25172584437407b99bc9ab52609d0d7c02cf2e29edc91ac20f
3
+ metadata.gz: 4752fe1e276560bb8cca4c2115aabb11c36958588672cc5ec36d20bd3599d82e
4
+ data.tar.gz: 854eba7f90f061528db3de98a120bc457a679421103ef84eced0373457b1e3f1
5
5
  SHA512:
6
- metadata.gz: 58bd0f6dba980c3328127363960d501e9abbc2e84543b3264d418eb30a5e0a3fad8e25a7fa5a1f9a2b3aaa2b4f0e2bc4057813cdc963a63c7c276611052e6502
7
- data.tar.gz: 7c082ee6bdfc6ae5f46f680d872b16e6d125dadfd94771b7da5d87151655a4405faf4c5616ae4bb5d56277a3df8dcfa45cfd928b7b0d97f678bcf2543d65f84d
6
+ metadata.gz: 1ce11414a8a6a792e6a50b20ded0135037dc946267ee802f4ae9f9bb031b22a3689b5eadf821029ad4d29f1ce4599e6d9ffa96ac1b3c4d15285df7f1c37415f9
7
+ data.tar.gz: 3a825528fc039dffedbbe35a588c52bd9e3c5cdbd336e0e23793d316d1949cbad2dd090a69fb36fe911b57f2b69e46983d17c3022c10177aebc8704975c49f3a
@@ -0,0 +1,31 @@
1
+ You are a RuboCop style reviewer for the Phlexible Ruby gem.
2
+
3
+ ## Project Style Rules
4
+
5
+ This project enforces strict RuboCop rules. Review changed Ruby files and flag violations.
6
+
7
+ ### Disabled Syntax (hard errors)
8
+ - **No `unless`** — use `if !` or negated conditions instead
9
+ - **No `and` / `or` / `not`** — use `&&` / `||` / `!` instead
10
+ - **No numbered parameters** (`_1`, `_2`) — use named block parameters
11
+
12
+ ### Formatting
13
+ - **Max line length**: 100 characters
14
+ - **Private method indentation**: `indented_internal_methods` — private/protected methods are indented one extra level beneath the access modifier
15
+ - **String literals**: prefer double quotes
16
+
17
+ ### Enabled Plugins
18
+ Review against these RuboCop plugins:
19
+ - `rubocop-rails`
20
+ - `rubocop-minitest`
21
+ - `rubocop-performance`
22
+ - `rubocop-packaging`
23
+ - `rubocop-rake`
24
+
25
+ ## Instructions
26
+
27
+ 1. Read the files that were changed (use `jj diff --name-only` or check recent edits).
28
+ 2. For each `.rb` file, check for violations of the rules above.
29
+ 3. Run `bundle exec rubocop -P --fail-level C --force-exclusion <file>` to confirm.
30
+ 4. Report any issues found, grouped by file, with the specific rule violated and a suggested fix.
31
+ 5. If no issues are found, confirm the code passes review.
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ INPUT=$(cat)
3
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
4
+
5
+ # Block edits to lock files and generated gemfiles
6
+ if [[ "$FILE_PATH" == *.lock ]] || [[ "$FILE_PATH" == */gemfiles/*.gemfile ]]; then
7
+ echo "Do not edit lock files or generated Appraisal gemfiles directly." >&2
8
+ exit 2
9
+ fi
10
+
11
+ exit 0
@@ -0,0 +1,18 @@
1
+ #!/bin/bash
2
+ INPUT=$(cat)
3
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
4
+
5
+ # Only lint Ruby files
6
+ if [[ "$FILE_PATH" != *.rb ]]; then
7
+ exit 0
8
+ fi
9
+
10
+ RESULT=$(bundle exec rubocop -P --fail-level C --force-exclusion "$FILE_PATH" 2>&1)
11
+ EXIT_CODE=$?
12
+
13
+ if [ $EXIT_CODE -ne 0 ]; then
14
+ echo "$RESULT" >&2
15
+ exit 2
16
+ fi
17
+
18
+ exit 0
@@ -0,0 +1,27 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Edit|Write",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "PostToolUse": [
15
+ {
16
+ "matcher": "Edit|Write",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/rubocop-check.sh",
21
+ "timeout": 30
22
+ }
23
+ ]
24
+ }
25
+ ]
26
+ }
27
+ }
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: test
3
+ description: Run tests for the Phlexible gem across the Appraisal matrix
4
+ arguments:
5
+ - name: appraisal
6
+ description: "Appraisal to test: phlex1-rails7, phlex1-rails8, phlex2-rails7, phlex2-rails8, or 'all' (default: phlex2-rails8)"
7
+ default: "phlex2-rails8"
8
+ - name: file
9
+ description: "Optional test file path, or path:line for a single test"
10
+ disable-model-invocation: true
11
+ ---
12
+
13
+ Run Phlexible tests using the Appraisal gem for multi-version testing.
14
+
15
+ ## Instructions
16
+
17
+ 1. If `appraisal` is "all", run: `bundle exec appraisal rails test {file}`
18
+ 2. Otherwise, run: `bundle exec appraisal {appraisal} rails test {file}`
19
+ 3. If no `file` is given, omit it to run the full suite for that appraisal.
20
+ 4. Report pass/fail/skip counts from the test output.
21
+ 5. If tests fail, summarise which tests failed and why.
data/.rubocop.yml CHANGED
@@ -12,6 +12,7 @@ AllCops:
12
12
  SuggestExtensions: false
13
13
  Exclude:
14
14
  - "gemfiles/**/*"
15
+ - "vendor/**/*"
15
16
 
16
17
  Naming/FileName:
17
18
  Exclude:
data/CLAUDE.md ADDED
@@ -0,0 +1,80 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Overview
6
+
7
+ Phlexible is a Ruby gem providing helpers and extensions for [Phlex](https://phlex.fun) views in Rails applications. It supports Phlex 1.x and 2.x across Rails 7.2+ and Ruby >= 3.3.
8
+
9
+ ## Commands
10
+
11
+ ### Tests
12
+
13
+ Tests use Minitest (with maxitest, minitest-focus, minitest-spec-rails). The project uses [Appraisal](https://github.com/thoughtbot/appraisal) to test across multiple Phlex/Rails version combinations defined in `Appraisals`.
14
+
15
+ ```bash
16
+ # Run all tests across all appraisals (phlex1/rails7, phlex1/rails8, phlex2/rails7, phlex2/rails8)
17
+ bundle exec appraisal rails test
18
+
19
+ # Run tests for a specific appraisal
20
+ bundle exec appraisal phlex2/rails8 rails test
21
+
22
+ # Run a single test file
23
+ bundle exec appraisal phlex2/rails8 rails test test/phlexible/alias_view_test.rb
24
+
25
+ # Run a single test by line number
26
+ bundle exec appraisal phlex2/rails8 rails test test/phlexible/alias_view_test.rb:10
27
+
28
+ # Focus a single test (add `focus` before the test method, provided by minitest-focus)
29
+ ```
30
+
31
+ ### Lint
32
+
33
+ ```bash
34
+ bundle exec rubocop -P --fail-level C
35
+ ```
36
+
37
+ ### Setup
38
+
39
+ ```bash
40
+ bin/setup
41
+ bundle exec appraisal install
42
+ ```
43
+
44
+ ## Architecture
45
+
46
+ All source code lives under `lib/phlexible/`. Autoloading is handled by Zeitwerk (`Zeitwerk::Loader.for_gem` in `lib/phlexible.rb`).
47
+
48
+ ### Module Organization
49
+
50
+ Modules are split into two categories:
51
+
52
+ **Standalone modules** (no Rails dependency):
53
+ - `Phlexible::AliasView` — `extend` in a view to create shortcut methods for rendering other components
54
+ - `Phlexible::Callbacks` — `include` for ActiveSupport::Callbacks-based `before_template`/`after_template`/`around_template` hooks. Also provides `before_layout`/`after_layout`/`around_layout` when used with `AutoLayout`.
55
+ - `Phlexible::PageTitle` — `include` for hierarchical page title management across nested views
56
+ - `Phlexible::ProcessAttributes` — `extend` to intercept and modify HTML element attributes before rendering (Phlex 2.x only; Phlex 1.x has this built-in). Prepends wrappers onto all StandardElements, VoidElements, and custom `register_element` methods.
57
+
58
+ **Rails-specific modules** (`lib/phlexible/rails/`):
59
+ - `ActionController::ImplicitRender` — convention-based automatic Phlex view rendering (resolves `UsersController#index` to `Users::IndexView`)
60
+ - `ControllerVariables` — explicit interface to expose controller instance variables to Phlex views via `controller_variable` class method
61
+ - `AElement` — overrides `a` tag to pass `href` through Rails `url_for`
62
+ - `ButtonTo` — Phlex component replacing Rails `button_to` helper
63
+ - `Responder` — integration with the [Responders](https://github.com/heartcombo/responders) gem
64
+ - `AutoLayout` — automatic layout wrapping based on view namespace conventions (e.g., `Views::Admin::Index` resolves to `Views::Layouts::Admin`). Includes `ViewAssigns` and `Callbacks`. Layout resolution is cached per class in production.
65
+ - `MetaTags` / `MetaTagsComponent` — define meta tags in controllers, render in views
66
+
67
+ ### Key Patterns
68
+
69
+ - Modules use `extend` (AliasView, ProcessAttributes) or `include` (Callbacks, PageTitle, ControllerVariables) depending on whether they add class-level or instance-level behavior.
70
+ - `ProcessAttributes` uses `class_eval` with string interpolation to dynamically wrap every HTML element method — be careful when modifying this.
71
+ - `ControllerVariables` depends on both `ViewAssigns` and `Callbacks` internally.
72
+ - `AutoLayout` depends on both `ViewAssigns` and `Callbacks`. Layout resolution is cached in production via `resolved_layout` class method; use `reset_resolved_layout!` to clear.
73
+ - Tests use a Rails dummy app at `test/dummy/` for integration testing with real controllers/routes.
74
+
75
+ ## Style
76
+
77
+ - RuboCop enforced with `rubocop-rails`, `rubocop-minitest`, `rubocop-performance`, `rubocop-packaging`, `rubocop-rake`.
78
+ - `unless`, `and`/`or`/`not`, and numbered parameters are **disabled** via `rubocop-disable_syntax`.
79
+ - `indented_internal_methods` indentation style (private methods indented one extra level).
80
+ - Max line length: 100 characters.
data/README.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  A bunch of helpers and goodies intended to make life with [Phlex](https://phlex.fun) even easier!
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Usage](#usage)
9
+ - [AliasView](#aliasview)
10
+ - [Callbacks](#callbacks)
11
+ - [PageTitle](#pagetitle)
12
+ - [ProcessAttributes](#processattributes)
13
+ - [Rails::ActionController::ImplicitRender](#railsactioncontrollerimplicitrender)
14
+ - [Rails::ControllerVariables](#railscontrollervariables)
15
+ - [Rails::AutoLayout](#railsautolayout)
16
+ - [Rails::AElement](#railsaelement)
17
+ - [Rails::ButtonTo](#railsbuttonto)
18
+ - [Rails::MetaTags](#railsmetatags)
19
+ - [Rails::Responder](#railsresponder)
20
+ - [Development](#development)
21
+ - [Contributing](#contributing)
22
+ - [License](#license)
23
+ - [Code of Conduct](#code-of-conduct)
24
+
5
25
  ## Installation
6
26
 
7
27
  Install the gem and add to the application's Gemfile by executing:
@@ -14,49 +34,39 @@ If bundler is not being used to manage dependencies, install the gem by executin
14
34
 
15
35
  ## Usage
16
36
 
17
- ### Rails
37
+ ### `AliasView`
18
38
 
19
- #### `ActionController::ImplicitRender`
39
+ Create an alias at a given `element`, to the given view class.
20
40
 
21
- Adds support for default and `action_missing` rendering of Phlex views. So instead of this:
41
+ So instead of:
22
42
 
23
43
  ```ruby
24
- class UsersController
25
- def index
26
- render Views::Users::Index.new
44
+ class MyView < Phlex::HTML
45
+ def view_template
46
+ div do
47
+ render My::Awesome::Component.new
48
+ end
27
49
  end
28
50
  end
29
51
  ```
30
52
 
31
- You can do this:
32
-
33
- ```ruby
34
- class UsersController
35
- include Phlexible::Rails::ActionController::ImplicitRender
36
- end
37
- ```
38
-
39
- ##### View Resolution
40
-
41
- By default, views are resolved using the `phlex_view_path` method, which constructs a path based on the controller and action name. For example, `UsersController#index` will look for `Users::IndexView`.
42
-
43
- You can customize this behavior by overriding `phlex_view_path` in your controller:
53
+ You can instead do:
44
54
 
45
55
  ```ruby
46
- class UsersController
47
- include Phlexible::Rails::ActionController::ImplicitRender
56
+ class MyView < Phlex::HTML
57
+ extend Phlexible::AliasView
48
58
 
49
- private
59
+ alias_view :awesome, -> { My::Awesome::Component }
50
60
 
51
- def phlex_view_path(action_name)
52
- "views/#{controller_path}/#{action_name}"
61
+ def view_template
62
+ div do
63
+ awesome
53
64
  end
65
+ end
54
66
  end
55
67
  ```
56
68
 
57
- This would resolve `UsersController#index` to `Views::Users::Index` instead.
58
-
59
- #### `Callbacks`
69
+ ### `Callbacks`
60
70
 
61
71
  While Phlex does have `before_template`, `after_template`, and `around_template` hooks, they must be defined as regular Ruby methods, meaning you have to always remember to call `super` when redefining any hook method.
62
72
 
@@ -82,9 +92,115 @@ end
82
92
 
83
93
  You can still use the regular `before_template`, `after_template`, and `around_template` hooks as well, but I recommend that if you include this module, that you use callbacks instead.
84
94
 
85
- #### `ControllerVariables`
95
+ When used with `Rails::AutoLayout`, layout callbacks (`before_layout`, `after_layout`, `around_layout`) are also available. See the `Rails::AutoLayout` section below.
96
+
97
+ ### `PageTitle`
98
+
99
+ Helper to assist in defining page titles within Phlex views. Also includes support for nested views, where each desendent view class will have its title prepended to the page title. Simply include *Phlexible::PageTitle* module and assign the title to the `page_title` class variable:
100
+
101
+ ```ruby
102
+ class MyView
103
+ include Phlexible::PageTitle
104
+ self.page_title = 'My Title'
105
+ end
106
+ ```
107
+
108
+ Then call the `page_title` method in the `<head>` of your page.
109
+
110
+ ### `ProcessAttributes`
111
+
112
+ > This functionality is already built in to **Phlex >= 1**. This module is only needed for **Phlex >= 2**.
113
+
114
+ Allows you to intercept and modify HTML element attributes before they are rendered. This is useful for adding default attributes, transforming values, or conditionally modifying attributes based on other attributes.
115
+
116
+ Extend your view class with `Phlexible::ProcessAttributes` and define a `process_attributes` instance method that receives the attributes hash and returns the modified hash.
117
+
118
+ ```ruby
119
+ class MyView < Phlex::HTML
120
+ extend Phlexible::ProcessAttributes
121
+
122
+ def process_attributes(attrs)
123
+ # Add a default class to all elements
124
+ attrs[:class] ||= 'my-default-class'
125
+ attrs
126
+ end
127
+
128
+ def view_template
129
+ div(id: 'container') { 'Hello' }
130
+ end
131
+ end
132
+ ```
133
+
134
+ This will output:
135
+
136
+ ```html
137
+ <div id="container" class="my-default-class">Hello</div>
138
+ ```
139
+
140
+ The `process_attributes` method is called for all standard HTML elements and void elements, as well as any custom elements registered with `register_element`.
141
+
142
+ ```ruby
143
+ class MyView < Phlex::HTML
144
+ extend Phlexible::ProcessAttributes
145
+
146
+ register_element :my_custom_element
147
+
148
+ def process_attributes(attrs)
149
+ attrs[:data_processed] = true
150
+ attrs
151
+ end
152
+
153
+ def view_template
154
+ my_custom_element(name: 'test') { 'Custom content' }
155
+ end
156
+ end
157
+ ```
158
+
159
+ ### `Rails::ActionController::ImplicitRender`
160
+
161
+ Adds support for default and `action_missing` rendering of Phlex views. So instead of this:
162
+
163
+ ```ruby
164
+ class UsersController
165
+ def index
166
+ render Views::Users::Index.new
167
+ end
168
+
169
+ def show
170
+ render Views::Users::Show.new
171
+ end
172
+ end
173
+ ```
174
+
175
+ You can do this:
176
+
177
+ ```ruby
178
+ class UsersController
179
+ include Phlexible::Rails::ActionController::ImplicitRender
180
+ end
181
+ ```
182
+
183
+ #### View Resolution
184
+
185
+ By default, views are resolved using the `phlex_view_path` method, which constructs a path based on the controller and action name. For example, `UsersController#index` will look for `Views::Users::IndexView`.
186
+
187
+ You can customize this behavior by overriding `phlex_view_path` in your controller:
188
+
189
+ ```ruby
190
+ class UsersController
191
+ include Phlexible::Rails::ActionController::ImplicitRender
192
+
193
+ private
194
+
195
+ def phlex_view_path(action_name)
196
+ "views/#{controller_path}/#{action_name}"
197
+ end
198
+ end
199
+ ```
200
+
201
+ This would resolve `UsersController#index` to `Views::Users::Index` instead.
86
202
 
87
- > Available in **>= 1.0.0**
203
+ ### `Rails::ControllerVariables`
88
204
 
89
205
  > **NOTE:** Prior to **1.0.0**, this module was called `ControllerAttributes` with a very different API. This is no longer available since **1.0.0**.
90
206
 
@@ -102,7 +218,7 @@ class Views::Users::Index < Views::Base
102
218
  end
103
219
  ```
104
220
 
105
- ##### Options
221
+ #### Options
106
222
 
107
223
  `controller_variable` accepts one or many symbols, or a hash of symbols to options.
108
224
 
@@ -127,38 +243,103 @@ end
127
243
 
128
244
  Please note that defining a variable with the same name as an existing variable in the view will be overwritten.
129
245
 
130
- #### `Responder`
246
+ ### `Rails::AutoLayout`
131
247
 
132
- If you use [Responders](https://github.com/heartcombo/responders), Phlexible provides a responder to support implicit rendering similar to `ActionController::ImplicitRender` above. It will render the Phlex view using `respond_with` if one exists, and fall back to default rendering.
133
-
134
- Just include it in your ApplicationResponder:
248
+ Automatically wraps Phlex views in a layout component based on namespace conventions. Include this module in your view classes to enable automatic layout resolution.
135
249
 
136
250
  ```ruby
137
- class ApplicationResponder < ActionController::Responder
138
- include Phlexible::Rails::ActionController::ImplicitRender
139
- include Phlexible::Rails::Responder
251
+ class Views::Admin::Index < Phlex::HTML
252
+ include Phlexible::Rails::AutoLayout
253
+
254
+ def view_template
255
+ span { 'admin index' }
256
+ end
140
257
  end
141
258
  ```
142
259
 
143
- Then simply `respond_with` in your action method as normal:
260
+ #### Layout Resolution
261
+
262
+ Layouts are resolved by mapping the view's namespace to a layout class under `Views::Layouts::`. For example:
263
+
264
+ | View class | Layout resolved |
265
+ |---|---|
266
+ | `Views::Admin::Index` | `Views::Layouts::Admin` |
267
+ | `Views::Admin::Users::Show` | `Views::Layouts::Admin::Users` (falls back to `Views::Layouts::Admin` if not found) |
268
+ | `Views::Dashboard::Index` | `Views::Layouts::Application` (default fallback) |
269
+
270
+ Layout classes receive the view instance as a constructor argument and yield the view content:
144
271
 
145
272
  ```ruby
146
- class UsersController < ApplicationController
147
- def new
148
- respond_with User.new
273
+ class Views::Layouts::Admin < Phlex::HTML
274
+ def initialize(view)
275
+ @view = view
149
276
  end
150
277
 
278
+ def view_template(&block)
279
+ div(id: 'admin-layout', &block)
280
+ end
281
+ end
282
+ ```
283
+
284
+ #### Controller-Assigned Layouts
285
+
286
+ You can override automatic resolution by setting a `@layout` instance variable in your controller. The view must also include `ViewAssigns` (which `AutoLayout` includes automatically):
287
+
288
+ ```ruby
289
+ class AdminController < ApplicationController
151
290
  def index
152
- respond_with User.all
291
+ @layout = Views::Layouts::Custom
153
292
  end
154
293
  end
155
294
  ```
156
295
 
157
- This responder requires the use of `ActionController::ImplicitRender`, so don't forget to include that in your `ApplicationController`.
296
+ #### Configuration
297
+
298
+ Three class attributes control layout resolution:
158
299
 
159
- If you use `ControllerVariables` in your view, and define a `resource` attribute, the responder will pass that to your view.
300
+ | Attribute | Default | Description |
301
+ |---|---|---|
302
+ | `auto_layout_view_prefix` | `'Views::'` | Only views matching this prefix get auto-layout. Set to `nil` to match all view classes. |
303
+ | `auto_layout_namespace` | `'Views::Layouts::'` | Namespace where layout classes are looked up. |
304
+ | `auto_layout_default` | `'Views::Layouts::Application'` | Fallback layout when no namespace match is found. Set to `nil` to render without a layout. |
160
305
 
161
- #### `AElement`
306
+ ```ruby
307
+ class Views::Base < Phlex::HTML
308
+ include Phlexible::Rails::AutoLayout
309
+
310
+ self.auto_layout_view_prefix = 'Views::'
311
+ self.auto_layout_namespace = 'Views::Layouts::'
312
+ self.auto_layout_default = 'Views::Layouts::Application'
313
+ end
314
+ ```
315
+
316
+ #### Layout Callbacks
317
+
318
+ When `Rails::AutoLayout` is included, `before_layout`, `after_layout`, and `around_layout` callbacks become available (provided by the `Callbacks` module):
319
+
320
+ ```ruby
321
+ class Views::Admin::Index < Phlex::HTML
322
+ include Phlexible::Rails::AutoLayout
323
+
324
+ before_layout :log_render
325
+
326
+ def view_template
327
+ span { 'admin index' }
328
+ end
329
+
330
+ private
331
+
332
+ def log_render
333
+ Rails.logger.info "Rendering with layout"
334
+ end
335
+ end
336
+ ```
337
+
338
+ #### Caching
339
+
340
+ Layout resolution is cached per class in production. In development (when `Rails.configuration.enable_reloading` is enabled), layouts are resolved fresh on each render. You can manually clear the cache with `reset_resolved_layout!`.
341
+
342
+ ### `Rails::AElement`
162
343
 
163
344
  No need to call Rails `link_to` helper, when you can simply render an anchor tag directly with Phlex. But unfortunately that means you lose some of the magic that `link_to` provides. Especially the automatic resolution of URL's and Rails routes.
164
345
 
@@ -180,7 +361,13 @@ class MyView < Phlex::HTML
180
361
  end
181
362
  ```
182
363
 
183
- #### 'ButtonTo`
364
+ You can also pass `:back` as the `href` value, which will use the referrer URL if available, or fall back to `javascript:history.back()`:
365
+
366
+ ```ruby
367
+ a(href: :back) { 'Go back' }
368
+ ```
369
+
370
+ ### `Rails::ButtonTo`
184
371
 
185
372
  Generates a form containing a single button that submits to the URL created by the set of options.
186
373
 
@@ -198,7 +385,7 @@ The form submits a POST request by default. You can specify a different HTTP ver
198
385
  Phlexible::Rails::ButtonTo.new(:root, method: :patch) { 'My Button' }
199
386
  ```
200
387
 
201
- ##### Options
388
+ #### Options
202
389
 
203
390
  - `:class` - Specify the HTML class name of the button (not the form).
204
391
  - `:form_attributes` - Hash of HTML attributes for the form tag.
@@ -207,9 +394,7 @@ Phlexible::Rails::ButtonTo.new(:root, method: :patch) { 'My Button' }
207
394
  - `:method` - Symbol of the HTTP verb. Supported verbs are :post (default), :get, :delete, :patch,
208
395
  and :put.
209
396
 
210
- #### `MetaTags`
211
-
212
- > Available in **>= 1.0.0**
397
+ ### `Rails::MetaTags`
213
398
 
214
399
  A super simple way to define and render meta tags in your Phlex views. Just render the
215
400
  `Phlexible::Rails::MetaTagsComponent` component in the head element of your page, and define the
@@ -237,98 +422,36 @@ class MyView < Phlex::HTML
237
422
  end
238
423
  ```
239
424
 
240
- ### `AliasView`
241
-
242
- Create an alias at a given `element`, to the given view class.
425
+ ### `Rails::Responder`
243
426
 
244
- So instead of:
427
+ If you use [Responders](https://github.com/heartcombo/responders), Phlexible provides a responder to support implicit rendering similar to `Rails::ActionController::ImplicitRender` above. It will render the Phlex view using `respond_with` if one exists, and fall back to default rendering.
245
428
 
246
- ```ruby
247
- class MyView < Phlex::HTML
248
- def view_template
249
- div do
250
- render My::Awesome::Component.new
251
- end
252
- end
253
- end
254
- ```
255
-
256
- You can instead do:
257
-
258
- ```ruby
259
- class MyView < Phlex::HTML
260
- extend Phlexible::AliasView
261
-
262
- alias_view :awesome, -> { My::Awesome::Component }
263
-
264
- def view_template
265
- div do
266
- awesome
267
- end
268
- end
269
- end
270
- ```
271
-
272
- ### PageTitle
273
-
274
- Helper to assist in defining page titles within Phlex views. Also includes support for nested views, where each desendent view class will have its title prepended to the page title. Simply include *Phlexible::PageTitle* module and assign the title to the `page_title` class variable:
429
+ Just include it in your ApplicationResponder:
275
430
 
276
431
  ```ruby
277
- class MyView
278
- self.page_title = 'My Title'
432
+ class ApplicationResponder < ActionController::Responder
433
+ include Phlexible::Rails::ActionController::ImplicitRender
434
+ include Phlexible::Rails::Responder
279
435
  end
280
436
  ```
281
437
 
282
- Then call the `page_title` method in the `<head>` of your page.
283
-
284
- ### `ProcessAttributes`
285
-
286
- > This functionality is already built in to **Phlex >= 1**. This module is only needed for **Phlex >= 2**.
287
-
288
- Allows you to intercept and modify HTML element attributes before they are rendered. This is useful for adding default attributes, transforming values, or conditionally modifying attributes based on other attributes.
289
-
290
- Extend your view class with `Phlexible::ProcessAttributes` and define a `process_attributes` instance method that receives the attributes hash and returns the modified hash.
438
+ Then simply `respond_with` in your action method as normal:
291
439
 
292
440
  ```ruby
293
- class MyView < Phlex::HTML
294
- extend Phlexible::ProcessAttributes
295
-
296
- def process_attributes(attrs)
297
- # Add a default class to all elements
298
- attrs[:class] ||= 'my-default-class'
299
- attrs
441
+ class UsersController < ApplicationController
442
+ def new
443
+ respond_with User.new
300
444
  end
301
445
 
302
- def view_template
303
- div(id: 'container') { 'Hello' }
446
+ def index
447
+ respond_with User.all
304
448
  end
305
449
  end
306
450
  ```
307
451
 
308
- This will output:
309
-
310
- ```html
311
- <div id="container" class="my-default-class">Hello</div>
312
- ```
313
-
314
- The `process_attributes` method is called for all standard HTML elements and void elements, as well as any custom elements registered with `register_element`.
315
-
316
- ```ruby
317
- class MyView < Phlex::HTML
318
- extend Phlexible::ProcessAttributes
319
-
320
- register_element :my_custom_element
321
-
322
- def process_attributes(attrs)
323
- attrs[:data_processed] = true
324
- attrs
325
- end
452
+ This responder requires the use of `ActionController::ImplicitRender`, so don't forget to include that in your `ApplicationController`.
326
453
 
327
- def view_template
328
- my_custom_element(name: 'test') { 'Custom content' }
329
- end
330
- end
331
- ```
454
+ If you use `Rails::ControllerVariables` in your view, and define a `resource` attribute, the responder will pass that to your view.
332
455
 
333
456
  ## Development
334
457
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexible (3.0.0)
4
+ phlexible (3.3.0)
5
5
  phlex (>= 1.10.0, < 3.0.0)
6
6
  phlex-rails (>= 1.2.0, < 3.0.0)
7
7
  rails (>= 7.2.0, < 9.0.0)
@@ -416,7 +416,7 @@ CHECKSUMS
416
416
  parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
417
417
  phlex (1.11.0) sha256=979548e79a205c981612f1ab613addc8fa128c8092694d02f41aad4cea905e73
418
418
  phlex-rails (1.2.2) sha256=a20218449e71bc9fa5a71b672fbede8a654c6b32a58f1c4ea83ddc1682307a4c
419
- phlexible (3.0.0)
419
+ phlexible (3.3.0)
420
420
  pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
421
421
  pretty_please (0.2.0) sha256=1f00296f946ae8ffd53db25803ed3998d615b9cc07526517dc75fcca6af3a0d3
422
422
  prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexible (3.0.0)
4
+ phlexible (3.3.0)
5
5
  phlex (>= 1.10.0, < 3.0.0)
6
6
  phlex-rails (>= 1.2.0, < 3.0.0)
7
7
  rails (>= 7.2.0, < 9.0.0)
@@ -414,7 +414,7 @@ CHECKSUMS
414
414
  parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
415
415
  phlex (1.11.0) sha256=979548e79a205c981612f1ab613addc8fa128c8092694d02f41aad4cea905e73
416
416
  phlex-rails (1.2.2) sha256=a20218449e71bc9fa5a71b672fbede8a654c6b32a58f1c4ea83ddc1682307a4c
417
- phlexible (3.0.0)
417
+ phlexible (3.3.0)
418
418
  pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
419
419
  pretty_please (0.2.0) sha256=1f00296f946ae8ffd53db25803ed3998d615b9cc07526517dc75fcca6af3a0d3
420
420
  prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexible (3.0.0)
4
+ phlexible (3.3.0)
5
5
  phlex (>= 1.10.0, < 3.0.0)
6
6
  phlex-rails (>= 1.2.0, < 3.0.0)
7
7
  rails (>= 7.2.0, < 9.0.0)
@@ -422,7 +422,7 @@ CHECKSUMS
422
422
  parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
423
423
  phlex (2.4.0) sha256=8aad2f0cd792d7b1bd287f15a1f89474c9cacac28120dc33d2a81467ae934067
424
424
  phlex-rails (2.4.0) sha256=2bcddbd488681acb25753bab1887d3ac150e644244ff8ba307f2171a4d0195f5
425
- phlexible (3.0.0)
425
+ phlexible (3.3.0)
426
426
  pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
427
427
  pretty_please (0.2.0) sha256=1f00296f946ae8ffd53db25803ed3998d615b9cc07526517dc75fcca6af3a0d3
428
428
  prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- phlexible (3.0.0)
4
+ phlexible (3.3.0)
5
5
  phlex (>= 1.10.0, < 3.0.0)
6
6
  phlex-rails (>= 1.2.0, < 3.0.0)
7
7
  rails (>= 7.2.0, < 9.0.0)
@@ -420,7 +420,7 @@ CHECKSUMS
420
420
  parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
421
421
  phlex (2.4.0) sha256=8aad2f0cd792d7b1bd287f15a1f89474c9cacac28120dc33d2a81467ae934067
422
422
  phlex-rails (2.4.0) sha256=2bcddbd488681acb25753bab1887d3ac150e644244ff8ba307f2171a4d0195f5
423
- phlexible (3.0.0)
423
+ phlexible (3.3.0)
424
424
  pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
425
425
  pretty_please (0.2.0) sha256=1f00296f946ae8ffd53db25803ed3998d615b9cc07526517dc75fcca6af3a0d3
426
426
  prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
@@ -33,6 +33,18 @@ module Phlexible
33
33
  def after_template(*names, &block)
34
34
  set_callback(:template, :after, *names, &block)
35
35
  end
36
+
37
+ def before_layout(*names, &block)
38
+ set_callback(:layout, :before, *names, &block)
39
+ end
40
+
41
+ def after_layout(*names, &block)
42
+ set_callback(:layout, :after, *names, &block)
43
+ end
44
+
45
+ def around_layout(*names, &block)
46
+ set_callback(:layout, :around, *names, &block)
47
+ end
36
48
  end
37
49
 
38
50
  def around_template
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'phlex/version'
4
+
3
5
  module Phlexible
4
6
  module Rails
5
- # Calls `url_for` for the `href` attribute.
7
+ # Calls `url_for` for the `href` attribute, and supports a `:back` `href` value.
6
8
  module AElement
7
9
  extend ActiveSupport::Concern
8
10
 
@@ -10,9 +12,43 @@ module Phlexible
10
12
  include Phlex::Rails::Helpers::URLFor
11
13
  end
12
14
 
13
- def a(href:, **, &)
14
- super(href: url_for(href), **, &)
15
+ def a(href:, **attrs, &)
16
+ attrs[:href] = href == :back ? _back_url : url_for(href)
17
+
18
+ super(**attrs, &)
15
19
  end
20
+
21
+ private
22
+
23
+ JAVASCRIPT_BACK = 'javascript:history.back()'
24
+ private_constant :JAVASCRIPT_BACK
25
+
26
+ if Phlex::VERSION >= '2.0.0'
27
+ def _back_url
28
+ _filtered_referrer || safe(JAVASCRIPT_BACK)
29
+ end
30
+ else
31
+ # Wraps a value to bypass Phlex 1.x's javascript: href stripping.
32
+ SafeHref = Struct.new(:value) do
33
+ def to_phlex_attribute_value = value
34
+ def to_s = ''
35
+ end
36
+ private_constant :SafeHref
37
+
38
+ def _back_url
39
+ _filtered_referrer || SafeHref.new(JAVASCRIPT_BACK)
40
+ end
41
+ end
42
+
43
+ def _filtered_referrer # :nodoc:
44
+ controller = respond_to?(:view_context) ? view_context : helpers
45
+
46
+ if controller.respond_to?(:request)
47
+ referrer = controller.request.env['HTTP_REFERER']
48
+ referrer if referrer && URI(referrer).scheme != 'javascript'
49
+ end
50
+ rescue URI::InvalidURIError # rubocop:disable Lint/SuppressedException
51
+ end
16
52
  end
17
53
  end
18
54
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexible
4
+ module Rails
5
+ module AutoLayout
6
+ extend ActiveSupport::Concern
7
+ include Phlexible::Rails::ViewAssigns
8
+ include Phlexible::Callbacks
9
+
10
+ included do
11
+ define_callbacks :layout
12
+
13
+ class_attribute :auto_layout_view_prefix, instance_writer: false, default: 'Views::'
14
+ class_attribute :auto_layout_namespace, instance_writer: false, default: 'Views::Layouts::'
15
+ class_attribute :auto_layout_default, instance_writer: false, default: 'Views::Layouts::Application'
16
+ end
17
+
18
+ def around_template
19
+ layout_class = controller_assigned_layout || self.class.resolved_layout
20
+ if layout_class
21
+ run_callbacks :layout do
22
+ render(@layout = layout_class.new(self)) { super }
23
+ end
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ class_methods do
30
+ def resolved_layout
31
+ if ::Rails.configuration.enable_reloading
32
+ resolve_layout
33
+ else
34
+ return @resolved_layout if defined?(@resolved_layout)
35
+
36
+ @resolved_layout = resolve_layout
37
+ end
38
+ end
39
+
40
+ def reset_resolved_layout!
41
+ remove_instance_variable(:@resolved_layout) if defined?(@resolved_layout)
42
+ end
43
+
44
+ private
45
+
46
+ def resolve_layout
47
+ view_name = resolve_view_name
48
+ return nil if view_name.blank?
49
+
50
+ prefix = auto_layout_view_prefix
51
+ return nil if prefix && !view_name.start_with?(prefix)
52
+
53
+ resolve_layout_from_namespace(view_name, prefix) ||
54
+ auto_layout_default&.safe_constantize
55
+ end
56
+
57
+ def resolve_layout_from_namespace(view_name, prefix)
58
+ strip_prefix = prefix || infer_prefix_from_namespace || ''
59
+ segments = view_name.delete_prefix(strip_prefix).split('::')[..-2]
60
+
61
+ segments.length.downto(1) do |i|
62
+ klass = "#{auto_layout_namespace}#{segments.first(i).join('::')}".safe_constantize
63
+ return klass if klass.is_a?(Class)
64
+ end
65
+
66
+ nil
67
+ end
68
+
69
+ def infer_prefix_from_namespace
70
+ root = auto_layout_namespace&.split('::')&.first
71
+ "#{root}::" if root
72
+ end
73
+
74
+ def resolve_view_name
75
+ ancestors.each do |ancestor|
76
+ return ancestor.name if ancestor.is_a?(Class) && ancestor.name.present?
77
+ end
78
+ nil
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def controller_assigned_layout
85
+ return nil if !respond_to?(:view_assigns, true)
86
+
87
+ view_assigns['layout']
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phlexible
4
- VERSION = '3.1.1'
4
+ VERSION = '3.3.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlexible
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.1
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Moss
@@ -91,9 +91,15 @@ executables: []
91
91
  extensions: []
92
92
  extra_rdoc_files: []
93
93
  files:
94
+ - ".claude/agents/rubocop-reviewer.md"
95
+ - ".claude/hooks/protect-files.sh"
96
+ - ".claude/hooks/rubocop-check.sh"
97
+ - ".claude/settings.json"
98
+ - ".claude/skills/test/SKILL.md"
94
99
  - ".rubocop.yml"
95
100
  - ".ruby-version"
96
101
  - Appraisals
102
+ - CLAUDE.md
97
103
  - CODE_OF_CONDUCT.md
98
104
  - LICENSE.txt
99
105
  - README.md
@@ -115,6 +121,7 @@ files:
115
121
  - lib/phlexible/rails/a_element.rb
116
122
  - lib/phlexible/rails/action_controller/implicit_render.rb
117
123
  - lib/phlexible/rails/action_controller/meta_tags.rb
124
+ - lib/phlexible/rails/auto_layout.rb
118
125
  - lib/phlexible/rails/button_to.rb
119
126
  - lib/phlexible/rails/button_to_concerns.rb
120
127
  - lib/phlexible/rails/controller_variables.rb
@@ -144,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
151
  - !ruby/object:Gem::Version
145
152
  version: '0'
146
153
  requirements: []
147
- rubygems_version: 4.0.4
154
+ rubygems_version: 4.0.6
148
155
  specification_version: 4
149
156
  summary: A bunch of helpers and goodies intended to make life with Phlex even easier!
150
157
  test_files: []