pakyow-ui 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|