pakyow-ui 0.10.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 +7 -0
- data/pakyow-ui/CHANGELOG.md +3 -0
- data/pakyow-ui/LICENSE +20 -0
- data/pakyow-ui/README.md +325 -0
- data/pakyow-ui/lib/pakyow-ui/base.rb +35 -0
- data/pakyow-ui/lib/pakyow-ui/channel_builder.rb +54 -0
- data/pakyow-ui/lib/pakyow-ui/config.rb +13 -0
- data/pakyow-ui/lib/pakyow-ui/ext/app.rb +50 -0
- data/pakyow-ui/lib/pakyow-ui/ext/app_context.rb +5 -0
- data/pakyow-ui/lib/pakyow-ui/ext/view_context.rb +30 -0
- data/pakyow-ui/lib/pakyow-ui/fetch_view_handler.rb +67 -0
- data/pakyow-ui/lib/pakyow-ui/helpers.rb +11 -0
- data/pakyow-ui/lib/pakyow-ui/mock_mutation_eval.rb +25 -0
- data/pakyow-ui/lib/pakyow-ui/mutable.rb +99 -0
- data/pakyow-ui/lib/pakyow-ui/mutable_data.rb +21 -0
- data/pakyow-ui/lib/pakyow-ui/mutate_context.rb +79 -0
- data/pakyow-ui/lib/pakyow-ui/mutation_set.rb +38 -0
- data/pakyow-ui/lib/pakyow-ui/mutation_store.rb +30 -0
- data/pakyow-ui/lib/pakyow-ui/mutator.rb +63 -0
- data/pakyow-ui/lib/pakyow-ui/no_op_view.rb +87 -0
- data/pakyow-ui/lib/pakyow-ui/registries/redis_mutation_registry.rb +34 -0
- data/pakyow-ui/lib/pakyow-ui/registries/simple_mutation_registry.rb +31 -0
- data/pakyow-ui/lib/pakyow-ui/ui.rb +83 -0
- data/pakyow-ui/lib/pakyow-ui/ui_attrs.rb +40 -0
- data/pakyow-ui/lib/pakyow-ui/ui_component.rb +68 -0
- data/pakyow-ui/lib/pakyow-ui/ui_instructable.rb +112 -0
- data/pakyow-ui/lib/pakyow-ui/ui_view.rb +179 -0
- data/pakyow-ui/lib/pakyow-ui.rb +1 -0
- metadata +154 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: efeadfe62f8a75592def0245762f51a0ed6810d0
|
4
|
+
data.tar.gz: 9cc0f94cdeac4337d04dae90c43172d50dca1c81
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 456f564c2ce44a45f96e1374db0441a6ae764ef5dbaef49506f4a6dd9a16fdc7fac9f61b07929d274ade2ee3c44b91d22d112572be419d8a054896e9f4c03797
|
7
|
+
data.tar.gz: 6ab2b517de51cc4b2310b4f6517c39ad7ba0cdb60497be90dfbf638b6cdb6dcf0f8ad9a09cee3af87c30e5a9e75feb5e3342220830cb65acdb2d246df06e6812
|
data/pakyow-ui/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2015 Bryan Powell
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/pakyow-ui/README.md
ADDED
@@ -0,0 +1,325 @@
|
|
1
|
+
# PakyowUI
|
2
|
+
|
3
|
+
Auto-Updating UIs for Pakyow, without moving to the client.
|
4
|
+
|
5
|
+
## Rationale
|
6
|
+
|
7
|
+
We wanted a way to build modern apps in a traditional backend-driven
|
8
|
+
manner while still reaping the benefits of modern, live updating
|
9
|
+
UIs. The existing approaches to building live UIs tend to *replace*
|
10
|
+
backend-driven architecture rather than *extend* it.
|
11
|
+
|
12
|
+
Instead of replacing traditional architecture, PakyowUI adds a layer
|
13
|
+
on top of it. This allows Pakyow apps to have live UIs out of the box
|
14
|
+
without any additional work by the developer, while remaining accessible
|
15
|
+
and fully usable in the absence of WebSockets, JavaScript, etc.
|
16
|
+
|
17
|
+
The PakyowUI approach stays true to the fundamental nature of the web.
|
18
|
+
It aims to be the real-time web expressed as progressive enhancement.
|
19
|
+
It allows for any aspect of a website or web app to be updated in
|
20
|
+
real-time, without any additional work by the developer.
|
21
|
+
|
22
|
+
## Overview
|
23
|
+
|
24
|
+
At a high level, PakyowUI keeps rendered views (meaning views that
|
25
|
+
are currently rendered by a client in the browser) in sync with the
|
26
|
+
current state of the data.
|
27
|
+
|
28
|
+
During the initial request/response cycle, Pakyow keeps track of what
|
29
|
+
view is rendered and sent back to the client, along with the underlying
|
30
|
+
data used to render those views.
|
31
|
+
|
32
|
+
When the data changes in the future, a set of transformation
|
33
|
+
instructions are sent to the client(s) who possess views with that
|
34
|
+
data. The transformations are then applied to the existing views by a
|
35
|
+
JavaScript client library so that the view not reflects the current
|
36
|
+
app state. The app *does not* push re-rendered views back down to the
|
37
|
+
client, nor does any JavaScript transcompilation occur.
|
38
|
+
|
39
|
+
---
|
40
|
+
|
41
|
+
Let's look at an example. Say during the initial request/response cycle
|
42
|
+
Pakyow rendered a view that presents a user's name:
|
43
|
+
|
44
|
+
```html
|
45
|
+
<div data-scope="user" data-id="1">
|
46
|
+
<h3 data-prop="name">
|
47
|
+
Bob Dylan
|
48
|
+
</h3>
|
49
|
+
</div>
|
50
|
+
```
|
51
|
+
|
52
|
+
When the name changes, PakyowUI builds up an instruction like this:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
[[:bind, { name: 'Thelonius Monk' }]]
|
56
|
+
```
|
57
|
+
|
58
|
+
This instruction is routed to the client(s) that present a view
|
59
|
+
containing data for user 1. Once received, the intructions are applied.
|
60
|
+
|
61
|
+
Predictably, the updated view reflects the new state:
|
62
|
+
|
63
|
+
```html
|
64
|
+
<div data-scope="user" data-id="1">
|
65
|
+
<h3 data-prop="name">
|
66
|
+
Thelonius Monk
|
67
|
+
</h3>
|
68
|
+
</div>
|
69
|
+
```
|
70
|
+
|
71
|
+
PakyowUI builds on the [pakyow-realtime
|
72
|
+
library](https://github.com/pakyow/pakyow/tree/master/pakyow-realtime),
|
73
|
+
so all of the communication between client and server occurs over a
|
74
|
+
WebSocket. If WebSockets aren't supported by the client (or for some
|
75
|
+
reason aren't working) the app will continue to work, just without live
|
76
|
+
updates. You get this graceful degradation for free without developing
|
77
|
+
with progressive enhancement in mind.
|
78
|
+
|
79
|
+
---
|
80
|
+
|
81
|
+
By expressing view transformations as data, they can be applied to
|
82
|
+
any view by any interpreter; be it on the server or the client. To
|
83
|
+
accomplish this, view rendering must be expressed separately from
|
84
|
+
the view and in context of the data being presented by the view. The
|
85
|
+
rendering itself also is expressed independently of how to fetch
|
86
|
+
the data necessary to perform the render, giving PakyowUI all the
|
87
|
+
information it needs to automatically perform the rendering again at
|
88
|
+
some point in the future.
|
89
|
+
|
90
|
+
## Data - Mutables
|
91
|
+
|
92
|
+
PakyowUI introduces a concept called **mutables**. A mutable wraps a
|
93
|
+
data model and defines two things:
|
94
|
+
|
95
|
+
1. Actions that can occur on the model that cause state mutations.
|
96
|
+
2. Queries that define how particular data is to be fetched.
|
97
|
+
|
98
|
+
Here's how a mutable is defined:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class User < Sequel::Model; end
|
102
|
+
|
103
|
+
Pakyow::App.mutable :user do
|
104
|
+
model User
|
105
|
+
|
106
|
+
action :create do |object|
|
107
|
+
User.create(object)
|
108
|
+
end
|
109
|
+
|
110
|
+
query :all do
|
111
|
+
User.all
|
112
|
+
end
|
113
|
+
|
114
|
+
query :find do |id|
|
115
|
+
User[id]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
From a route, we can use the mutable to query for data:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
# get all the users
|
124
|
+
data(:user).all
|
125
|
+
|
126
|
+
# get a specific user
|
127
|
+
data(:user).find(1)
|
128
|
+
```
|
129
|
+
|
130
|
+
We can also change data through the mutable:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
data(:user).create(params[:user])
|
134
|
+
```
|
135
|
+
|
136
|
+
Mutables are the first step in making the route declarative (what)
|
137
|
+
rather than imperative (how). All of the *how* is wrapped up in the
|
138
|
+
mutable itself, letting us express only *what* should happen from the
|
139
|
+
route. This is important.
|
140
|
+
|
141
|
+
## View - Mutators
|
142
|
+
|
143
|
+
The second concept introduced by PakyowUI is **mutators**. A mutator
|
144
|
+
describes *how* to render a particular view with some particular data.
|
145
|
+
|
146
|
+
Here's a mutator for rendering a list of users:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
Pakyow::Mutators :user do
|
150
|
+
mutator :list do |view, users|
|
151
|
+
view.apply(users)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
From a route, you could invoke the mutator on a view like this:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
view.scope(:user).mutate(:list, with: data(:user).all)
|
160
|
+
```
|
161
|
+
|
162
|
+
Notice that we're mutating with the data from our mutable user. Pakyow
|
163
|
+
will fetch the data using the `all` query and pass it to the `list`
|
164
|
+
mutation where the view is rendered.
|
165
|
+
|
166
|
+
***NOTE:*** One important caveat here is that the individual data
|
167
|
+
passed to the mutate method needs to respond to the `to_hash` method,
|
168
|
+
which should return a hash of all relevant attributes. E.g. For
|
169
|
+
ActiveRecord models could define `to_hash` like this:
|
170
|
+
|
171
|
+
```
|
172
|
+
class User < ActiveRecord::Base
|
173
|
+
def to_hash
|
174
|
+
attributes
|
175
|
+
end
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
At this point we've effectively turned view rendering into a declarative
|
180
|
+
action from the route's point of view. We only have to describe *what*
|
181
|
+
we want to happen and Pakyow takes care of the rest.
|
182
|
+
|
183
|
+
This becomes useful when you want to subscribe the mutation to future
|
184
|
+
changes in state. You can do this by calling `subscribe`:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
view.scope(:user).mutate(:list, with: data(:user).all).subscribe
|
188
|
+
```
|
189
|
+
|
190
|
+
The view is rendered in the intial request/response cycle exactly like
|
191
|
+
it was before, but now it's also subscribed to any future state change
|
192
|
+
that affects the rendered view.
|
193
|
+
|
194
|
+
Let's mutate our state:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
data(:user).create(params[:user])
|
198
|
+
```
|
199
|
+
|
200
|
+
Pakyow knows that we've mutated our user state; it also knows what
|
201
|
+
clients are currently rendering the mutated user state. It automatically
|
202
|
+
pushes down instructions over a WebSocket describing how the client
|
203
|
+
should update their view to match the current state.
|
204
|
+
|
205
|
+
Our rendered views now keep themselves up to date with the current state
|
206
|
+
of the application; and we as the developer don't have to do anything
|
207
|
+
but write the initial rendering code! We don't have to move any part of
|
208
|
+
our app to the client. Our app retains a backend-driven architecture
|
209
|
+
while still behaving like a modern app with live updates.
|
210
|
+
|
211
|
+
## Ring - Client Library
|
212
|
+
|
213
|
+
PakyowUI ships with a client library called Ring, effectively
|
214
|
+
bringing Pakyow's view transformation API to the client. In addition
|
215
|
+
to applying view transformations, Pakyow.js also ships with several
|
216
|
+
components, including:
|
217
|
+
|
218
|
+
- Mutation Detection: Watches user-interaction with the rendered view and can
|
219
|
+
interpret which interactions cause a mutation in state (e.g. submitting a
|
220
|
+
form). Once detected, the mutation is sent to the server by calling the REST
|
221
|
+
endpoint through the open WebSocket. The mutation is then validated by the
|
222
|
+
server, persisted (if necessary), and broadcast to all other clients.
|
223
|
+
|
224
|
+
You can use Ring to build custom front-end components that emit
|
225
|
+
their own mutations or otherwise communicate with your app's HTTP routes
|
226
|
+
over a WebSocket.
|
227
|
+
|
228
|
+
Ring is available here:
|
229
|
+
|
230
|
+
- http://github.com/pakyow/ring).
|
231
|
+
|
232
|
+
## Channels
|
233
|
+
|
234
|
+
Pakyow keeps track of what clients should receive what state mutations
|
235
|
+
with channels. Here's how a channel is structured:
|
236
|
+
|
237
|
+
```
|
238
|
+
scope:{name};mutation{name}::{qualifiers}
|
239
|
+
```
|
240
|
+
|
241
|
+
In the example from the Mutators section, the subscribed channel name is:
|
242
|
+
|
243
|
+
```
|
244
|
+
scope:user;mutation:list
|
245
|
+
```
|
246
|
+
|
247
|
+
This means that any client who rendered any user data with the `list`
|
248
|
+
mutation will receive future updates in user state. Read the next
|
249
|
+
section to understand how to better control this.
|
250
|
+
|
251
|
+
## Qualifiers
|
252
|
+
|
253
|
+
You might be curious about how to exercise fine-grained control over
|
254
|
+
clients and the mutations they receive. PakyowUI handles this with
|
255
|
+
*qualifiers*.
|
256
|
+
|
257
|
+
For example, you can subscribe a view to only update with the current
|
258
|
+
user's data:
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
view.scope(:user).mutate(:list, with:
|
262
|
+
data(:user).for_user(current_user)).subscribe({
|
263
|
+
user_id: current_user.id
|
264
|
+
})
|
265
|
+
```
|
266
|
+
|
267
|
+
The `user_id` qualifier is added to the channel name, so when future
|
268
|
+
mutations occur, the result will only be pushed down to that particular
|
269
|
+
client. Here's the subscribed channel name:
|
270
|
+
|
271
|
+
scope:user;mutation:present::user_id:1
|
272
|
+
|
273
|
+
You can also qualify mutators. Here's how you would express that you
|
274
|
+
want a particular user's mutations to be sent only to clients that
|
275
|
+
render that state:
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
Pakyow::Mutators :user do
|
279
|
+
mutator :present, qualify: [:id] do |view, user|
|
280
|
+
view.bind(user)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
view.scope(:user).mutate(:present, with: data(:user).find(1)).subscribe
|
285
|
+
```
|
286
|
+
|
287
|
+
The value for the qualifier will be pulled from the user's id and added to the
|
288
|
+
channel name. Now only client's who currently render the user with id of 1 will
|
289
|
+
receive future state changes about that user. Here's the subscribed channel
|
290
|
+
name:
|
291
|
+
|
292
|
+
```
|
293
|
+
scope:user;mutation:present::id:1
|
294
|
+
```
|
295
|
+
|
296
|
+
# Download
|
297
|
+
|
298
|
+
The latest version of Pakyow UI can be installed with RubyGems:
|
299
|
+
|
300
|
+
```
|
301
|
+
gem install pakyow-ui
|
302
|
+
```
|
303
|
+
|
304
|
+
Source code can be downloaded as part of the Pakyow project on Github:
|
305
|
+
|
306
|
+
- https://github.com/pakyow/pakyow/tree/master/pakyow-ui
|
307
|
+
|
308
|
+
# License
|
309
|
+
|
310
|
+
Pakyow UI is released free and open-source under the [MIT
|
311
|
+
License](http://opensource.org/licenses/MIT).
|
312
|
+
|
313
|
+
# Support
|
314
|
+
|
315
|
+
Documentation is available here:
|
316
|
+
|
317
|
+
- http://pakyow.org/docs/live-views
|
318
|
+
|
319
|
+
Found a bug? Tell us about it here:
|
320
|
+
|
321
|
+
- https://github.com/pakyow/pakyow/issues
|
322
|
+
|
323
|
+
We'd love to have you in the community:
|
324
|
+
|
325
|
+
- http://pakyow.org/get-involved
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'helpers'
|
2
|
+
require_relative 'ui'
|
3
|
+
require_relative 'mutator'
|
4
|
+
require_relative 'mutation_set'
|
5
|
+
require_relative 'mutable'
|
6
|
+
require_relative 'mutate_context'
|
7
|
+
require_relative 'ui_view'
|
8
|
+
require_relative 'channel_builder'
|
9
|
+
require_relative 'fetch_view_handler'
|
10
|
+
require_relative 'mutation_store'
|
11
|
+
require_relative 'registries/simple_mutation_registry'
|
12
|
+
require_relative 'registries/redis_mutation_registry'
|
13
|
+
require_relative 'config'
|
14
|
+
require_relative 'ui_component'
|
15
|
+
require_relative 'ui_instructable'
|
16
|
+
|
17
|
+
require_relative 'ext/app'
|
18
|
+
require_relative 'ext/app_context'
|
19
|
+
require_relative 'ext/view_context'
|
20
|
+
|
21
|
+
Pakyow::App.before :init do
|
22
|
+
@ui = Pakyow::UI::UI.new
|
23
|
+
end
|
24
|
+
|
25
|
+
Pakyow::App.after :load do
|
26
|
+
@ui.load(mutators, mutables)
|
27
|
+
end
|
28
|
+
|
29
|
+
Pakyow::App.before :route do
|
30
|
+
# setup a new ui context to work in
|
31
|
+
#
|
32
|
+
ui_dup = @ui.dup
|
33
|
+
ui_dup.context = self
|
34
|
+
@context.ui = ui_dup
|
35
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Pakyow
|
2
|
+
module UI
|
3
|
+
# Helpers for building channel names.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
module ChannelBuilder
|
7
|
+
PARTS = [:scope, :mutation, :component]
|
8
|
+
|
9
|
+
def self.build(qualifiers: [], data: [], qualifications: {}, **args)
|
10
|
+
channel = []
|
11
|
+
channel_extras = []
|
12
|
+
|
13
|
+
PARTS.each do |part|
|
14
|
+
add_part(part, args[part], channel)
|
15
|
+
end
|
16
|
+
|
17
|
+
add_qualifiers(qualifiers, data, channel_extras)
|
18
|
+
add_qualifications(qualifications, channel_extras)
|
19
|
+
|
20
|
+
channel = channel.join(';')
|
21
|
+
|
22
|
+
return channel if channel_extras.empty?
|
23
|
+
channel << "::#{channel_extras.join(';')}"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def self.add_part(part, value, channel)
|
29
|
+
return if value.nil?
|
30
|
+
channel << "#{part}:#{value}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.add_qualifiers(qualifiers, data, channel_extras)
|
34
|
+
qualifiers = Array.ensure(qualifiers)
|
35
|
+
|
36
|
+
data = data.data if data.is_a?(Pakyow::UI::MutableData)
|
37
|
+
return if qualifiers.empty? || data.empty?
|
38
|
+
|
39
|
+
datum = Array.ensure(data).first
|
40
|
+
|
41
|
+
qualifiers.each do |qualifier|
|
42
|
+
channel_extras << "#{qualifier}:#{datum[qualifier]}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.add_qualifications(qualifications, channel_extras)
|
47
|
+
qualifications.each do |name, value|
|
48
|
+
next if value.nil?
|
49
|
+
channel_extras << "#{name}:#{value}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'registries/simple_mutation_registry'
|
2
|
+
require_relative 'registries/redis_mutation_registry'
|
3
|
+
|
4
|
+
Pakyow::Config.register(:ui) { |config|
|
5
|
+
# The registry to use when keeping up with connections.
|
6
|
+
config.opt :registry, Pakyow::UI::SimpleMutationRegistry
|
7
|
+
}.env(:development) { |opts|
|
8
|
+
opts.registry = Pakyow::UI::SimpleMutationRegistry
|
9
|
+
}.env(:staging) { |opts|
|
10
|
+
opts.registry = Pakyow::UI::RedisMutationRegistry
|
11
|
+
}.env(:production) { |opts|
|
12
|
+
opts.registry = Pakyow::UI::RedisMutationRegistry
|
13
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Pakyow
|
2
|
+
class App
|
3
|
+
class << self
|
4
|
+
# Defines mutators for a scope.
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
def mutators(scope = nil, &block)
|
8
|
+
@mutators ||= {}
|
9
|
+
|
10
|
+
if scope && block
|
11
|
+
@mutators[scope] = block
|
12
|
+
else
|
13
|
+
@mutators || {}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Defines a mutable object.
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def mutable(scope, &block)
|
21
|
+
@mutables ||= {}
|
22
|
+
@mutables[scope] = block
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
def mutables
|
27
|
+
@mutables || {}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Convenience method for defining mutators on an app instance.
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def mutators(scope = nil, &block)
|
35
|
+
self.class.mutators(scope, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Convenience method for defining a mutable on an app instance.
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
def mutable(scope, &block)
|
42
|
+
self.class.mutable(scope, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @api private
|
46
|
+
def mutables
|
47
|
+
self.class.mutables
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Pakyow
|
2
|
+
module Presenter
|
3
|
+
class ViewContext
|
4
|
+
MSG_NONCOMPONENT = 'Cannot subscribe a non-component view'
|
5
|
+
|
6
|
+
# Mutates a view with a registered mutator.
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
def mutate(mutator, data: nil, with: nil)
|
10
|
+
Pakyow::UI::Mutator.instance.mutate(mutator, self, data || with || [])
|
11
|
+
end
|
12
|
+
|
13
|
+
# Subscribes a view and sets the `data-channel` attribute.
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
def subscribe(qualifications = {})
|
17
|
+
fail ArgumentError, MSG_NONCOMPONENT unless component?
|
18
|
+
|
19
|
+
channel = Pakyow::UI::ChannelBuilder.build(
|
20
|
+
component: component_name,
|
21
|
+
qualifications: qualifications
|
22
|
+
)
|
23
|
+
|
24
|
+
context.socket.subscribe(channel)
|
25
|
+
attrs.send(:'data-channel=', channel)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative 'no_op_view'
|
2
|
+
|
3
|
+
# Makes it possible to fetch a particular part of a view for a path. Calls a
|
4
|
+
# route with all view actions becoming no-ops. Then a query is run against the
|
5
|
+
# final view, pulling out the part that was requested.
|
6
|
+
#
|
7
|
+
# Expects the following in the message:
|
8
|
+
#
|
9
|
+
# - uri: the route to call
|
10
|
+
# - lookup: the view query
|
11
|
+
#
|
12
|
+
# Lookup currently supports the following keys:
|
13
|
+
#
|
14
|
+
# - channel
|
15
|
+
# - version
|
16
|
+
# - container
|
17
|
+
# - partial
|
18
|
+
# - scope
|
19
|
+
# - prop
|
20
|
+
#
|
21
|
+
Pakyow::Realtime.handler :'fetch-view' do |message, session, response|
|
22
|
+
env = Rack::MockRequest.env_for(message['uri'])
|
23
|
+
env['rack.session'] = session
|
24
|
+
|
25
|
+
app = Pakyow.app.dup
|
26
|
+
|
27
|
+
def app.view
|
28
|
+
Pakyow::Presenter::NoOpView.new(
|
29
|
+
Pakyow::Presenter::ViewContext.new(@presenter.view, self),
|
30
|
+
self
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
app_response = app.process(env)
|
35
|
+
|
36
|
+
body = ''
|
37
|
+
lookup = message['lookup']
|
38
|
+
view = app.presenter.view
|
39
|
+
|
40
|
+
channel = lookup['channel']
|
41
|
+
|
42
|
+
if channel
|
43
|
+
unqualified_channel = channel.split('::')[0]
|
44
|
+
view_for_channel = view.composed.doc.channel(unqualified_channel)
|
45
|
+
|
46
|
+
if view_for_channel
|
47
|
+
view_for_channel.set_attribute(:'data-channel', channel)
|
48
|
+
body = view_for_channel.to_html
|
49
|
+
end
|
50
|
+
else
|
51
|
+
lookup.each_pair do |key, value|
|
52
|
+
next if key == 'version'
|
53
|
+
view = view.send(key.to_sym, value.to_sym)
|
54
|
+
end
|
55
|
+
|
56
|
+
if view.is_a?(Pakyow::Presenter::ViewVersion)
|
57
|
+
body = view.use(lookup['version'] || :default).to_html
|
58
|
+
else
|
59
|
+
body = view.to_html
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
response[:status] = app_response[0]
|
64
|
+
response[:headers] = app_response[1]
|
65
|
+
response[:body] = body
|
66
|
+
response
|
67
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Pakyow
|
2
|
+
module Presenter
|
3
|
+
# Used by NoOpView to perform mutations in a no-op manner.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
class MockMutationEval
|
7
|
+
def initialize(mutation_name, relation_name, view)
|
8
|
+
@mutation_name = mutation_name
|
9
|
+
@relation_name = relation_name
|
10
|
+
@view = view
|
11
|
+
end
|
12
|
+
|
13
|
+
# NOTE we don't care about qualifiers here since we're just getting
|
14
|
+
# the proper view template; not actually setting it up with data
|
15
|
+
def subscribe(*_args)
|
16
|
+
channel = Pakyow::UI::ChannelBuilder.build(
|
17
|
+
scope: @view.scoped_as,
|
18
|
+
mutation: @mutation_name
|
19
|
+
)
|
20
|
+
|
21
|
+
@view.attrs.send(:'data-channel=', channel)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|