Skip to content

Commit 8b2280e

Browse files
committed
closes #402
1 parent d7ceeba commit 8b2280e

File tree

6 files changed

+203
-158
lines changed

6 files changed

+203
-158
lines changed

docs/hyper-state/README.md

Lines changed: 110 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11

2-
<img align="left" width="100" height="100" style="margin-right: 20px" src="https://github.com/hyperstack-org/hyperstack/blob/edge/docs/wip.png?raw=true" /> The `Hyperstack::State::Observable` module allows you to build classes that share their state with Hyperstack Components, and have those components update when objects in those classes change state.
3-
4-
## This Page Under Construction
5-
6-
The `Hyperstack::State::Observable` module allows you to build classes that share their state with Hyperstack Components and have those components update when objects in those classes change state.
72

83
### Revisiting the Tic Tac Toe Game
94

105
The easiest way to understand HyperState is by example. If you you did not see the Tic-Tac-Toe example, then **[please review it now](client-dsl/interlude-tic-tac-toe.md)**, as we are going to use this to demonstrate how to use the `Hyperstack::State::Observable` module.
116

12-
In our original Tic Tac Toe implementation the state of the game was stored in the `DisplayGame` component. State was updated by
13-
"bubbling up" events from lower level components up to `DisplayGame` where the event handers updated the state.
7+
In our original Tic-Tac-Toe implementation the state of the game was stored in the `DisplayGame` component. State was updated by
8+
"bubbling up" events from lower level components up to `DisplayGame` where the event hander updated the state.
149

1510
This is a nice simple approach but suffers from two issues:
1611
+ Each level of lower level components must be responsible for bubbling up the events to the higher component.
@@ -19,11 +14,11 @@ This is a nice simple approach but suffers from two issues:
1914
As our applications become larger we will want a way to keep each component's interface isolated and not dependent on the overall
2015
architecture, and to insure good separation of concerns.
2116

22-
The `Hyperstack::State::Observable` module allows us to put the game's state into a separate class, which can be accessed from any
23-
component: No more need to bubble up events, and no more cluttering up our `DisplayGame` component with state management stuff
17+
The `Hyperstack::State::Observable` module allows us to put the game's state into a separate class which can be accessed from any
18+
component: No more need to bubble up events, and no more cluttering up our `DisplayGame` component with state management
2419
and details of the game's data structure.
2520

26-
Here is the game state moved out of the `DisplayGame` component into its own class:
21+
Here is the game state and associated methods moved out of the `DisplayGame` component into its own class:
2722

2823
```ruby
2924
class Game
@@ -73,9 +68,8 @@ class Game
7368
include Hyperstack::State::Observable
7469
```
7570

76-
Including `Hyperstack::State::Observable` gives us access to a number of methods that allows our class to become
77-
a *reactive store*, and interact with other stores and components so that when the `Game` state updates so will
78-
any components that directly or indirectly depend on its state.
71+
`Game` is now in its own class and includes `Hyperstack::State::Observable`. This adds a number of methods to `Game` that allows our class to become
72+
a *reactive store*. When `Game` interacts with other stores and components they will be updated as the state of `Game` changes.
7973

8074
```ruby
8175
def initialize
@@ -84,9 +78,9 @@ any components that directly or indirectly depend on its state.
8478
end
8579
```
8680

87-
In the original implementation we initialized the two state variables `@history` and `@step` in the `before_mount` callback.
88-
There is no "mounting" of a store, so instead we will initialize our game by creating a new instance in the `DisplayGame` before
89-
mount callback (see below.)
81+
In the original implementation we initialized the two state variables `@history` and `@step` in the `before_mount` callback. The same initialization
82+
is now in the `initialize` method which will be called when a new instance of the game is created. This will still be done in the `DisplayGame`
83+
`before_mount` callback (see below.)
9084

9185
```ruby
9286
observer :player do
@@ -108,8 +102,7 @@ been changed `observe` and `observer` indicate that state has been accessed outs
108102
attr_reader :history
109103
```
110104

111-
Just as we have `mutate`, `mutator`, and `state_writer`, we have `observe`, `observer`, and `state_reader`. The `state_accessor`
112-
method just combines the two together.
105+
Just as we have `mutate`, `mutator`, and `state_writer`, we have `observe`, `observer`, and `state_reader`.
113106

114107
```ruby
115108
WINNING_COMBOS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
@@ -178,7 +171,7 @@ end
178171

179172
The `DisplayGame` `before_mount` callback is still responsible for initializing the game, but it no longer needs to be aware of
180173
the internals of the game's state. It simply calls `Game.new` and stores the result in the `@game` instance variable. For the rest
181-
of the component call the appropriate method on `@game`.
174+
of the component's code we call the appropriate method on `@game`.
182175

183176
We will need to pass the entire game to `DisplayBoard` (we will see why shortly) so we will rename it to `DisplayCurrentBoard`.
184177

@@ -216,19 +209,20 @@ communicate back upwards via events. Instead we communicate through the central
216209

217210
Rather than sending params down to lower level components, and having the components bubble up events, we have created a *Flux Loop*.
218211
The `Game` store holds the state, the top level component reads the state and sends it down to lower level components, those
219-
components update the state, causing the top level component to re-rerender.
212+
components update the `Game` state causing the top level component to re-rerender.
220213

221214
This structure greatly simplifies the structure and understanding of our components, and keeps each component functionally isolated.
222215

223216
Furthermore algorithms such as `current_winner?` now are neatly abstracted out into their own class.
224217

225218
### Classes and Instances
226219

227-
If we are sure we will only want one Game board, we could define Game with class methods like this:
220+
If we are sure we will only want one game board, we could define `Game` with class methods like this:
228221

229222
```ruby
230223
class Game
231224
include Hyperstack::State::Observable
225+
232226
class << self
233227
def initialize
234228
@history = [[]]
@@ -286,7 +280,6 @@ class DisplayBoard < HyperComponent
286280
end
287281

288282
class DisplayGame < HyperComponent
289-
before_mount { Game.initialize }
290283
def moves
291284
return unless Game.history.length > 1
292285

@@ -316,33 +309,14 @@ class DisplayGame < HyperComponent
316309
end
317310
```
318311

319-
Note that with this approach we can go back to passing just the current board to `DisplayBoard` as `DisplayBoard` can
320-
directly access `Game.handle_click!` since there is only one game.
321-
322-
### The Boot Broadcast
323-
324-
Observable classes can also receive information from *broadcasts*. Hyperstack comes with one predefined broadcast: `Hyperstack::Application::Boot` which is sent when the application has finished loading but before the first component is mounted.
312+
Now instead of creating an instance and passing it around we
313+
call the class level methods on `Game` throughout.
325314

326-
We can hook into the Boot broadcast and replace the need for our top level component to call initialize:
315+
The `Hyperstack::State::Observable` module will call any class level `initialize` methods in the class or subclasses
316+
before the first component mounts.
327317

328-
```ruby
329-
class Game
330-
include Hyperstack::State::Observable
331-
332-
receives Hyperstack::Application::Boot do
333-
@history = [[]]
334-
@step = 0
335-
end
336-
337-
class << self
338-
...
339-
end
340-
end
341-
...
342-
```
343-
> Why is `receives` outside the class definitions? The receives method can be used to attach broadcasts to either class level or
344-
instances. In this case we want to attach the receiver to Game, not to Game's singleton class. More on receivers and broadcasting
345-
in the chapter on Operations.
318+
Note that with this approach we can go back to passing just the current board to `DisplayBoard` as `DisplayBoard` can
319+
directly access `Game.handle_click!` since there is only one game.
346320

347321
### Thinking About Stores
348322

@@ -359,11 +333,9 @@ If your store's methods access other stores, you do not need worry about their s
359333
that the built in Ruby Array and Hash classes are **not** stores, so when you modify or read an Array or a Hash its up to you to use
360334
the appropriate `mutate` or `observe` method.
361335

362-
> Why not make Arrays and Hashes stores? For efficiency. Its a comprimise solution.
363-
364336
### Stores and Parameters
365337

366-
Typically in a large system you will have one or more central stores, and what you end up passing as parameters are either instances of those stores, or some other kind of index into the store. Or if (as in the case of our Game) there is only one store, you
338+
Typically in a large system you will have one or more central stores, and what you end up passing as parameters are either instances of those stores, or some other kind of index into the store. If there is only one store (as in the case of our Game), you
367339
need not pass any parameters at all.
368340

369341
We can rewrite the last iteration of DisplayBoard to demonstrate this:
@@ -387,22 +359,22 @@ class DisplayBoard < HyperComponent
387359
end
388360
```
389361

390-
Here `DisplayBoard` no longer takes any parameter (and should be renamed again to `DisplayCurrentBoard`) and now
391-
`DisplaySquare` takes the id of the square to display, but the game or the current board are never passed as parameters;
362+
Here `DisplayBoard` no longer takes any parameter (and could be renamed again to `DisplayCurrentBoard`) and now a new component -
363+
`DisplaySquare` - takes the id of the square to display, but the game or the current board are never passed as parameters;
392364
there is no need to as they are implicit.
393365

394-
Whether to not pass store objects, an instance of a store, and index into the store is a design decision that depends on
366+
Whether to not pass a store class, an instance of a store, or some other index into the store is a design decision that depends on
395367
lots of factors, mainly how you see your application evolving over time.
396368

397369
### Summary of Methods
398370

399371
All the observable methods can be used either at the class or instance level.
400372

401-
#### Observing State
373+
#### Observing State: `observe, observer, state_reader`
402374

403375
The `observe` method takes any number of arguments and/or a block. The last argument evaluated or the value of the block is returned.
404376

405-
The arguments and block are evaluated then the objects state will be *observed*.
377+
The arguments and block are evaluated then the object's state will be *observed*.
406378

407379
If the block exits with a return or break, the state will **not** be observed.
408380

@@ -416,7 +388,7 @@ observe do
416388
end
417389
```
418390

419-
The `observer` method defines a new method with an implicit observer call:
391+
The `observer` method defines a new method with an implicit observe:
420392

421393
```ruby
422394
observer :foo do |x, y, z|
@@ -449,10 +421,87 @@ def baz
449421
end
450422
```
451423

452-
#### Mutating State
424+
#### Mutating State: `mutate, mutator, state_writer, toggle`
425+
426+
The `mutate` method takes any number of arguments and/or a block. The last argument evaluated or the value of the block is returned.
427+
428+
The arguments and block are evaluated then the object's state will be *mutated*.
429+
430+
If the block exits with a return or break, the state will **not** be mutated.
431+
432+
```ruby
433+
# evaluate and return a value
434+
mutate @history[@step]
435+
436+
# evaluate a block and return its value
437+
mutate do
438+
@history[@step]
439+
end
440+
```
441+
442+
The `mutator` method defines a new method with an implicit mutate:
443+
444+
```ruby
445+
mutator :foo do |x, y, z|
446+
...
447+
end
448+
```
449+
is equivilent to
450+
```ruby
451+
def foo(x, y, z)
452+
mutate do
453+
...
454+
end
455+
end
456+
```
457+
458+
Again if the block exits with a `return` or `break` the state will **not** be mutated.
459+
460+
The `state_writer` method declares one or more state accessors with an implicit state mutation:
461+
462+
```ruby
463+
state_reader :bar, :baz
464+
```
465+
is equivilent to
466+
```ruby
467+
def bar=(x)
468+
mutate @bar = x
469+
end
470+
def baz=(x)
471+
observe @baz = x
472+
end
473+
```
474+
475+
The `toggle` method reverses the polarity of a instance variable:
476+
477+
```ruby
478+
toggle(:foo)
479+
```
480+
is equivilent to
481+
```ruby
482+
mutate @foo = !@foo
483+
```
484+
485+
#### The `state_accessor` Method
486+
487+
Combines `state_reader` and `state_writer` methods.
488+
489+
```ruby
490+
state_accessor :foo, :bar
491+
```
492+
is equivilent to
493+
```ruby
494+
state_reader :foo, :bar
495+
state_writer :foo, :bar
496+
```
497+
498+
### Components and Stores
453499

500+
The standard `HyperComponent` base class includes `Hyperstack::State::Observable` so any `HyperComponent` has access to
501+
all of the above methods. A component also always **observes itself** so you never need to use `observe` within
502+
a component **unless** the state will be accessed outside the component. However once you start doing that you
503+
would be better off to move the state into a separate store.
454504

455-
receives
456-
mutate, mutator, state_writer
457-
state_accessor
458-
toggle
505+
> In addition components also act as the **Observers** in the system. What this means is
506+
that current component that is running its render method is recording all stores that call `observe`, when
507+
a store mutates, then all the components that recorded observations will be rerendered.

docs/specs/spec/hyper-state/tic_tac_toe_spec.rb

Lines changed: 0 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -707,102 +707,6 @@ def draw_square(id)
707707
end
708708

709709
class DisplayGame < HyperComponent
710-
before_mount { Game.initialize }
711-
def moves
712-
return unless Game.history.length > 1
713-
714-
Game.history.length.times do |move|
715-
LI(key: move) { move.zero? ? "Go to game start" : "Go to move ##{move}" }
716-
.on(:click) { Game.jump_to!(move) }
717-
end
718-
end
719-
720-
def status
721-
if (winner = Game.current_winner?)
722-
"Winner: #{winner}"
723-
else
724-
"Next player: #{Game.player}"
725-
end
726-
end
727-
728-
render(DIV, class: :game) do
729-
DIV(class: :game_board) do
730-
DisplayBoard(board: Game.current)
731-
end
732-
DIV(class: :game_info) do
733-
DIV { status }
734-
OL { moves }
735-
end
736-
end
737-
end
738-
end
739-
run_the_spec([])
740-
end
741-
742-
it "using the Boot broadcast to initialize the store" do
743-
insert_html "<style>\n#{CSS}</style>"
744-
mount "DisplayGame" do
745-
class Game
746-
include Hyperstack::State::Observable
747-
748-
receives Hyperstack::Application::Boot do
749-
@history = [[]]
750-
@step = 0
751-
end
752-
753-
class << self
754-
observer :player do
755-
@step.even? ? :X : :O
756-
end
757-
758-
observer :current do
759-
@history[@step]
760-
end
761-
762-
state_reader :history
763-
764-
WINNING_COMBOS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
765-
766-
def current_winner?
767-
WINNING_COMBOS.each do |a, b, c|
768-
return current[a] if current[a] && current[a] == current[b] && current[a] == current[c]
769-
end
770-
false
771-
end
772-
773-
mutator :handle_click! do |id|
774-
board = history[@step]
775-
return if current_winner? || board[id]
776-
777-
board = board.dup
778-
board[id] = player
779-
@history = history[0..@step] + [board]
780-
@step += 1
781-
end
782-
783-
mutator(:jump_to!) { |step| @step = step }
784-
end
785-
end
786-
787-
class DisplayBoard < HyperComponent
788-
param :board
789-
790-
def draw_square(id)
791-
BUTTON(class: :square, id: id) { board[id] }
792-
.on(:click) { Game.handle_click!(id) }
793-
end
794-
795-
render(DIV) do
796-
(0..6).step(3) do |row|
797-
DIV(class: :board_row) do
798-
(row..row + 2).each { |id| draw_square(id) }
799-
end
800-
end
801-
end
802-
end
803-
804-
class DisplayGame < HyperComponent
805-
before_mount { Game.initialize }
806710
def moves
807711
return unless Game.history.length > 1
808712

0 commit comments

Comments
 (0)