pakyow-ui 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ # 0.10.0 (to be released)
2
+
3
+ * Initial release
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.
@@ -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,5 @@
1
+ module Pakyow
2
+ class AppContext
3
+ attr_accessor :ui
4
+ end
5
+ 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,11 @@
1
+ module Pakyow
2
+ module Helpers
3
+ def ui
4
+ context.ui
5
+ end
6
+
7
+ def data(scope)
8
+ ui.mutator.mutable(scope, self)
9
+ end
10
+ end
11
+ 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