hibiki_phlex 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4c2497270017a9a8000df2a65a725d71a330698a5bd1ecba91b7e17d535ad625
4
+ data.tar.gz: ab335047166028c98ea49b12d784660f3667ceff5c5b980a1bfdd4c9402f971e
5
+ SHA512:
6
+ metadata.gz: 01cb9954a404539ed1c78c9c55542f2b2fa4b2c28d8828e04ae0ceafa0d0352db2664f7519f95f53cb6fdcae538250d8b5d7a471332e1bb8077ad3d868b65af2
7
+ data.tar.gz: 81a1c655c2e8a4b29e922dd2f397015972a148d2a68ae73150eaed3551a54c2fe0fa77c5afb5705475da70f5f61abd5a07779763ad292b629d65fdd4ea3ca736
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # hibiki_phlex
2
+
3
+ Phlex glue for [hibiki](https://github.com/planetaska/hibiki): reactive
4
+ components. A Phlex component is a plain Ruby object, so
5
+ `Hibiki::Reactive` gives it per-instance signals read as ordinary method
6
+ calls — no `.value` anywhere in `view_template` — and a **render effect**
7
+ re-renders it whenever one of those signals changes.
8
+
9
+ ```
10
+ signal write → render effect re-runs → component re-renders
11
+ (same instance) → fresh HTML handed to your block
12
+ ```
13
+
14
+ Transport-agnostic on purpose: the gem depends only on hibiki and phlex.
15
+ The block receiving the HTML decides where it goes — broadcast it over
16
+ Turbo Streams (see [hibiki_rails](../hibiki_rails)), write it to a file,
17
+ diff it in a test.
18
+
19
+ Incubating inside the hibiki repo while the core gem is pre-release; will
20
+ be extracted to its own repository once hibiki 0.1.0 ships and this API
21
+ stabilizes. Phlex >= 2.4, < 2.5 (see the version-pin rationale below),
22
+ Ruby >= 3.4.
23
+
24
+ ## Usage
25
+
26
+ ```ruby
27
+ class TodoList < Phlex::HTML
28
+ include Hibiki::Reactive
29
+ include Hibiki::Phlex::Rerenderable
30
+
31
+ state(:items) { [] }
32
+ derived(:remaining) { items.count { |item| !item[:done] } }
33
+
34
+ def view_template
35
+ div(id: "todos") do
36
+ h2 { "Todos — #{remaining} remaining" }
37
+ ul { items.each { |item| li { item[:title] } } }
38
+ end
39
+ end
40
+
41
+ # Signals compare with ==; build new values instead of mutating in place.
42
+ def add(title) = self.items = items + [{ title:, done: false }]
43
+ end
44
+ ```
45
+
46
+ ```ruby
47
+ list = TodoList.new
48
+
49
+ effect = Hibiki::Phlex.render_effect(list) do |html|
50
+ # your transport — e.g. inside a Hibiki::Rails::Channel:
51
+ # broadcast_replace target: "todos", html:
52
+ end
53
+
54
+ list.add("write docs") # → the block runs again with fresh HTML
55
+ effect.dispose # or let an enclosing Hibiki.root own teardown
56
+ ```
57
+
58
+ - The effect's **first run is the dependency-collecting initial render**:
59
+ signals read inside `view_template` subscribe it through plain method
60
+ calls, and that first HTML is yielded too.
61
+ - Re-renders happen **on the same instance** — signal identity lives in
62
+ the instance, so a fresh instance per render would reset every signal.
63
+ That's what `Rerenderable` exists for.
64
+ - `render_effect(component, scheduler:)` passes the scheduler through to
65
+ `Hibiki::Effect`, so reruns can be deferred or debounced (e.g.
66
+ `Hibiki::Rails::Debounce`).
67
+ - Returns the `Hibiki::Effect`. Created inside `Hibiki.root` (or another
68
+ effect), the owner tree disposes it automatically; a bare caller calls
69
+ `#dispose`.
70
+
71
+ ## With hibiki_rails
72
+
73
+ The combination is the LiveView-ish loop — one signal graph per cable
74
+ connection, one render effect per component:
75
+
76
+ ```ruby
77
+ class TodosChannel < ApplicationCable::Channel
78
+ include Hibiki::Rails::Channel
79
+
80
+ def build_graph
81
+ @list = TodoList.new
82
+ Hibiki::Phlex.render_effect(@list) do |html|
83
+ broadcast_replace target: "todos", html:
84
+ end
85
+ end
86
+
87
+ def add(data) = @list.add(data["title"])
88
+ end
89
+ ```
90
+
91
+ Granularity is inherent and worth knowing: the ERB style in hibiki_rails
92
+ is one effect **per partial** (fine-grained), a Phlex render effect is one
93
+ effect **per component** (component-grained). Both are correct; pick per
94
+ page. The initial-state pattern (server-rendered placeholder + subscribe
95
+ after the Turbo stream confirms) is transport-side — see the hibiki_rails
96
+ README.
97
+
98
+ ## Why the strict Phlex pin
99
+
100
+ Phlex components are one-shot: an instance renders once, and a second
101
+ `call` raises `Phlex::DoubleRenderError`. Phlex 2 tracks "spent" purely
102
+ through the private `@_state` ivar — everything else about a render is
103
+ per-call — so `Rerenderable#rerender` clears it and calls again. That is
104
+ the entire adapter, but it leans on a private implementation detail, so:
105
+
106
+ - the gemspec allows only verified Phlex minors (`>= 2.4, < 2.5`), and
107
+ - `spec/phlex_contract_spec.rb` pins the upstream contract itself, so
108
+ bumping the bound fails loudly in CI if the internals moved.
109
+
110
+ ## Development
111
+
112
+ ```
113
+ bundle exec rake # specs + rubocop (same as CI)
114
+ ```
115
+
116
+ Plain RSpec — no Rails; the live end-to-end proof app is `spike/` in the
117
+ parent repo (the `/phlex` page).
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hibiki
4
+ module Phlex
5
+ # Lets the same component instance render more than once — required for
6
+ # render_effect, where signal identity lives in the instance (a fresh
7
+ # instance per render would reset every signal).
8
+ #
9
+ # Phlex 2 marks an instance as spent purely through @_state —
10
+ # SGML#internal_call raises DoubleRenderError when it is set, and it is
11
+ # deliberately never cleared after a render (see `rendering?`).
12
+ # Everything else (output buffer, capture state, the thread-local
13
+ # current component) is per-call, so clearing @_state before calling
14
+ # again is the ENTIRE adapter. It leans on a private ivar, which is why
15
+ # this gem pins Phlex minors and spec/phlex_contract_spec.rb pins the
16
+ # contract itself.
17
+ module Rerenderable
18
+ def rerender(...)
19
+ @_state = nil
20
+ call(...)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hibiki
4
+ module Phlex
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # NOTE: for everything under lib/hibiki/phlex/ — inside this namespace a bare
4
+ # `Phlex` constant resolves to Hibiki::Phlex, not the framework. Always
5
+ # write `::Phlex.` when you mean the framework.
6
+ module Hibiki
7
+ # Phlex glue for hibiki: a component is a plain Ruby object, so
8
+ # Hibiki::Reactive gives it signals read as ordinary method calls, and a
9
+ # render effect re-renders the same instance (signal identity lives in
10
+ # the instance) whenever one of them changes.
11
+ module Phlex
12
+ # Wrap a component's render in an effect. The effect's first run is the
13
+ # dependency-collecting initial render — signals read inside
14
+ # view_template subscribe it through plain method calls — and its HTML
15
+ # is yielded like every rerun's. On each signal change the component
16
+ # re-renders ON THE SAME INSTANCE and the fresh HTML goes to the block;
17
+ # the block owns the transport (broadcast it, print it, diff it).
18
+ #
19
+ # Hibiki::Phlex.render_effect(@list) do |html|
20
+ # broadcast_replace target: "todos", html:
21
+ # end
22
+ #
23
+ # Component-grained by design: one effect per component (the ERB style
24
+ # in hibiki_rails is one effect per partial — both are correct).
25
+ # `scheduler:` passes through to Hibiki::Effect, so reruns can be
26
+ # deferred/debounced (e.g. Hibiki::Rails::Debounce). Returns the Effect;
27
+ # inside a Hibiki.root or another effect the owner tree adopts it, a
28
+ # bare caller disposes it by hand.
29
+ def self.render_effect(component, scheduler: nil, &sink)
30
+ unless component.respond_to?(:rerender)
31
+ raise ArgumentError,
32
+ "#{component.class} has no #rerender — include Hibiki::Phlex::Rerenderable"
33
+ end
34
+
35
+ Effect.new(scheduler:) { sink.call(component.rerender) }
36
+ end
37
+ end
38
+ end
39
+
40
+ require_relative "phlex/version"
41
+ require_relative "phlex/rerenderable"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hibiki"
4
+ require "phlex"
5
+ require "hibiki/phlex"
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hibiki_phlex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - planetaska
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: hibiki
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: phlex
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.4'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '2.5'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '2.4'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '2.5'
46
+ description: 'Make a Phlex component reactive: include Hibiki::Reactive for signals
47
+ read as plain method calls, Hibiki::Phlex::Rerenderable so the same instance can
48
+ render again, and wrap it in Hibiki::Phlex.render_effect to get fresh HTML on every
49
+ signal change — transport-agnostic, the block decides where the HTML goes.'
50
+ email:
51
+ - planetaska@gmail.com
52
+ executables: []
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - README.md
57
+ - lib/hibiki/phlex.rb
58
+ - lib/hibiki/phlex/rerenderable.rb
59
+ - lib/hibiki/phlex/version.rb
60
+ - lib/hibiki_phlex.rb
61
+ homepage: https://github.com/planetaska/hibiki-phlex
62
+ licenses:
63
+ - MIT
64
+ metadata:
65
+ homepage_uri: https://github.com/planetaska/hibiki-phlex
66
+ source_code_uri: https://github.com/planetaska/hibiki-phlex
67
+ rubygems_mfa_required: 'true'
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 4.0.16
83
+ specification_version: 4
84
+ summary: 'Phlex glue for hibiki: reactive components re-rendered by a render effect.'
85
+ test_files: []