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
@@ -0,0 +1,99 @@
|
|
1
|
+
require_relative 'mutable_data'
|
2
|
+
|
3
|
+
# TODO: make it possible to register this as data instead of mutables
|
4
|
+
|
5
|
+
module Pakyow
|
6
|
+
module UI
|
7
|
+
# Mutables enable PakyowUI to automatically handle changes in application
|
8
|
+
# state by interacting with the data layer in a declarative manner.
|
9
|
+
#
|
10
|
+
# Wraps a data source (such as a model object) and provides a convenient
|
11
|
+
# interface for defining and executing queries and actions. Queries accept
|
12
|
+
# parameters and return data sets. Actions cause a state change in
|
13
|
+
# application state.
|
14
|
+
#
|
15
|
+
# Once defined, all interactions with the data layer should occur through
|
16
|
+
# Mutables via the `data` helper method. When an action is performed that
|
17
|
+
# changes the state of the application, Pakyow will propogate the change
|
18
|
+
# through to all other connected clients automatically.
|
19
|
+
#
|
20
|
+
# Mutables should be registered with the `Pakyow::App.mutable` helper. The
|
21
|
+
# defined block will be executed in context of a `Mutable` instance.
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
class Mutable
|
25
|
+
include Helpers
|
26
|
+
|
27
|
+
attr_reader :context
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
def initialize(context, scope, &block)
|
31
|
+
@context = context
|
32
|
+
@scope = scope
|
33
|
+
@actions = {}
|
34
|
+
@queries = {}
|
35
|
+
|
36
|
+
instance_exec(&block)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Sets the model object.
|
40
|
+
#
|
41
|
+
# @api public
|
42
|
+
def model(model_class, type: nil)
|
43
|
+
@model_class = model_class
|
44
|
+
|
45
|
+
return if type.nil?
|
46
|
+
@model_type = type
|
47
|
+
|
48
|
+
# TODO: load default actions / queries based on type
|
49
|
+
end
|
50
|
+
|
51
|
+
# Defines an action.
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
def action(name, mutation: true, &block)
|
55
|
+
@actions[name] = {
|
56
|
+
block: block,
|
57
|
+
mutation: mutation
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Defines a query.
|
62
|
+
#
|
63
|
+
# @api public
|
64
|
+
def query(name, &block)
|
65
|
+
@queries[name] = block
|
66
|
+
end
|
67
|
+
|
68
|
+
# Handles calling queries or actions. Enables convenience like:
|
69
|
+
#
|
70
|
+
# data(:some_data).{action or query}
|
71
|
+
#
|
72
|
+
# @api public
|
73
|
+
def method_missing(method, *args)
|
74
|
+
action = @actions[method]
|
75
|
+
query = @queries[method]
|
76
|
+
|
77
|
+
if action
|
78
|
+
call_action(action, *args)
|
79
|
+
elsif query
|
80
|
+
call_query(query, method, *args)
|
81
|
+
else
|
82
|
+
fail ArgumentError, "Could not find query or action named #{method}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def call_action(action, *args)
|
89
|
+
result = action[:block].call(*args)
|
90
|
+
@context.ui.mutated(@scope, result, @context) if action[:mutation]
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
def call_query(query, method, *args)
|
95
|
+
MutableData.new(query, method, args, @scope)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Pakyow
|
2
|
+
module UI
|
3
|
+
# Adds metadata to a dataset returned by a Mutable query.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
class MutableData
|
7
|
+
attr_reader :query_name, :query_args, :scope
|
8
|
+
|
9
|
+
def initialize(query, query_name, query_args, scope)
|
10
|
+
@query = query
|
11
|
+
@query_name = query_name
|
12
|
+
@query_args = query_args
|
13
|
+
@scope = scope
|
14
|
+
end
|
15
|
+
|
16
|
+
def data
|
17
|
+
@data ||= @query.call(*@query_args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_relative 'channel_builder'
|
2
|
+
|
3
|
+
module Pakyow
|
4
|
+
module UI
|
5
|
+
# Provides helper methods to perform in context of a mutation. For example:
|
6
|
+
#
|
7
|
+
# view.scope(:foo).mutate(:bar).subscribe
|
8
|
+
#
|
9
|
+
# In the above example `mutate` returns a MutateContext object on which
|
10
|
+
# `subscribe` is called.
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
class MutateContext
|
14
|
+
attr_reader :mutation, :view, :data
|
15
|
+
|
16
|
+
# Creates a new context. Intended to be created by a Mutator.
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
def initialize(mutation, view, data)
|
20
|
+
@mutation = mutation
|
21
|
+
@view = view
|
22
|
+
@data = data
|
23
|
+
end
|
24
|
+
|
25
|
+
# Subscribes a mutation with optional qualifications. Qualifications are
|
26
|
+
# used to control the scope of future mutations. For example:
|
27
|
+
#
|
28
|
+
# view.scope(:foo).mutate(:bar).subscribe(user_id: 1)
|
29
|
+
#
|
30
|
+
# In the above example, a subscription is created qualified by `user_id`.
|
31
|
+
# Only mutations occuring with the same qualifications will cause the
|
32
|
+
# mutation to be performed again, triggering a view refresh.
|
33
|
+
#
|
34
|
+
# ui.mutated(:foo, user_id: 1)
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
def subscribe(qualifications = {})
|
38
|
+
if data.is_a?(MutableData)
|
39
|
+
MutationStore.instance.register(self, data, qualifications)
|
40
|
+
end
|
41
|
+
|
42
|
+
channel = ChannelBuilder.build(
|
43
|
+
scope: view.scoped_as,
|
44
|
+
mutation: mutation[:name],
|
45
|
+
qualifiers: mutation[:qualifiers],
|
46
|
+
data: data,
|
47
|
+
qualifications: qualifications
|
48
|
+
)
|
49
|
+
|
50
|
+
# subscribe to the channel
|
51
|
+
view.context.socket.subscribe(channel)
|
52
|
+
|
53
|
+
# handle setting the channel on the view
|
54
|
+
if view.is_a?(Presenter::ViewContext)
|
55
|
+
working_view = view.instance_variable_get(:@view)
|
56
|
+
else
|
57
|
+
working_view = view
|
58
|
+
end
|
59
|
+
|
60
|
+
if working_view.is_a?(Presenter::ViewCollection)
|
61
|
+
# NOTE there's a special case here where if the collection is
|
62
|
+
# empty we insert an empty element in its place; this makes
|
63
|
+
# it possible to know what the data should be applied to when
|
64
|
+
# a mutation occurs in the future
|
65
|
+
|
66
|
+
unless working_view.exists?
|
67
|
+
# TODO: would rather this be an html comment, but they aren't
|
68
|
+
# supported by query selectors; need to finalize how we will
|
69
|
+
# handle this particular edge case
|
70
|
+
working_view.first.doc.append('<span data-channel="' + channel + '" data-version="empty"></span>')
|
71
|
+
return
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
working_view.attrs.send(:'data-channel=', channel)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Pakyow
|
2
|
+
module UI
|
3
|
+
# Stores mutations.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
class MutationSet
|
7
|
+
attr_reader :mutations
|
8
|
+
|
9
|
+
def initialize(&block)
|
10
|
+
@mutations = {}
|
11
|
+
instance_exec(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
# NOTE I do have some concerns about defining qualifiers in this way;
|
15
|
+
# mainly because it will lead to having lots of versions of the same
|
16
|
+
# mutator just so the proper channels will be created.
|
17
|
+
#
|
18
|
+
# It's could end up being better to pass qualifiers to `subscribe`;
|
19
|
+
# however it feels premature to make this decision since it'll lead
|
20
|
+
# to a large increase in complexity to add at this point.
|
21
|
+
def mutator(name, qualify: [], &block)
|
22
|
+
@mutations[name] = {
|
23
|
+
fn: block,
|
24
|
+
qualifiers: Array.ensure(qualify),
|
25
|
+
name: name
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def mutation(name)
|
30
|
+
@mutations.fetch(name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def each(&block)
|
34
|
+
@mutations.each(&block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Pakyow
|
2
|
+
module UI
|
3
|
+
# Stores mutations that have occurred in the configured registry.
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
class MutationStore
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@registry = Config.ui.registry.instance
|
11
|
+
end
|
12
|
+
|
13
|
+
def register(mutate_context, mutable_data, qualifications)
|
14
|
+
# TODO: decide how we'll clean these up as clients disconnect
|
15
|
+
@registry.register(
|
16
|
+
mutable_data.scope,
|
17
|
+
mutation: mutate_context.mutation[:name],
|
18
|
+
qualifiers: mutate_context.mutation[:qualifiers],
|
19
|
+
qualifications: qualifications,
|
20
|
+
query_name: mutable_data.query_name,
|
21
|
+
query_args: mutable_data.query_args
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def mutations(scope)
|
26
|
+
@registry.mutations(scope) || []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative 'mutation_set'
|
2
|
+
require_relative 'mutate_context'
|
3
|
+
|
4
|
+
module Pakyow
|
5
|
+
module UI
|
6
|
+
# Performs mutations on views.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class Mutator
|
10
|
+
include Singleton
|
11
|
+
|
12
|
+
attr_reader :sets
|
13
|
+
|
14
|
+
# @api private
|
15
|
+
def initialize
|
16
|
+
reset
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset
|
20
|
+
@sets = {}
|
21
|
+
@mutables = {}
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def set(scope, &block)
|
26
|
+
@sets[scope] = MutationSet.new(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def mutable(scope, context = nil, &block)
|
30
|
+
if block_given?
|
31
|
+
@mutables[scope] = block
|
32
|
+
else
|
33
|
+
# TODO: inefficient to have to execute the block each time
|
34
|
+
Mutable.new(context, scope, &@mutables[scope])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def mutation(scope, name)
|
39
|
+
if mutations = mutations_by_scope(scope)
|
40
|
+
mutations.mutation(name)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# TODO: rename to mutation_set_for_scope
|
45
|
+
def mutations_by_scope(scope)
|
46
|
+
@sets[scope]
|
47
|
+
end
|
48
|
+
|
49
|
+
def mutate(mutation_name, view, data)
|
50
|
+
if mutation = mutation(view.scoped_as, mutation_name)
|
51
|
+
if data.is_a?(MutableData)
|
52
|
+
working_data = data.data
|
53
|
+
else
|
54
|
+
working_data = data
|
55
|
+
end
|
56
|
+
|
57
|
+
result = mutation[:fn].call(view, working_data)
|
58
|
+
MutateContext.new(mutation, result, data)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative 'mock_mutation_eval'
|
2
|
+
|
3
|
+
module Pakyow
|
4
|
+
module Presenter
|
5
|
+
# Stands in for a real View object and makes any attempted transformation
|
6
|
+
# a no-op.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class NoOpView
|
10
|
+
include Helpers
|
11
|
+
VIEW_CLASSES = [ViewContext]
|
12
|
+
|
13
|
+
# The arities of misc view methods that switch the behavior from
|
14
|
+
# instance_exec to yield.
|
15
|
+
#
|
16
|
+
EXEC_ARITIES = { with: 0, for: 1, for_with_index: 2, repeat: 1,
|
17
|
+
repeat_with_index: 2, bind: 1, bind_with_index: 2,
|
18
|
+
apply: 1 }
|
19
|
+
|
20
|
+
def initialize(view, context)
|
21
|
+
@view = view
|
22
|
+
@context = context
|
23
|
+
end
|
24
|
+
|
25
|
+
def is_a?(klass)
|
26
|
+
@view.is_a?(klass)
|
27
|
+
end
|
28
|
+
|
29
|
+
# View methods that should be a no-op
|
30
|
+
#
|
31
|
+
%i(bind bind_with_index apply).each do |method|
|
32
|
+
define_method(method) do |_data, **_kargs, &_block|
|
33
|
+
self
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def mutate(mutator, with: nil, data: nil)
|
38
|
+
MockMutationEval.new(mutator, with || data, self)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Pass these through, handling the return value.
|
42
|
+
#
|
43
|
+
def method_missing(method, *args, &block)
|
44
|
+
ret = @view.send(method, *args, &wrap(method, &block))
|
45
|
+
handle_return_value(ret)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def view?(obj)
|
51
|
+
VIEW_CLASSES.include?(obj.class)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a new context for returned views, or the return value.
|
55
|
+
#
|
56
|
+
def handle_return_value(value)
|
57
|
+
return NoOpView.new(value, @context) if view?(value)
|
58
|
+
|
59
|
+
value
|
60
|
+
end
|
61
|
+
|
62
|
+
# Wrap the block, substituting the view with the current view context.
|
63
|
+
#
|
64
|
+
def wrap(method, &block)
|
65
|
+
return if block.nil?
|
66
|
+
|
67
|
+
proc do |*args|
|
68
|
+
ctx = args.map! { |arg|
|
69
|
+
view?(arg) ? NoOpView.new(arg, @context) : arg
|
70
|
+
}.find { |arg| arg.is_a?(ViewContext) }
|
71
|
+
|
72
|
+
case block.arity
|
73
|
+
when EXEC_ARITIES[method]
|
74
|
+
# Rejecting ViewContext handles the edge cases around the order of
|
75
|
+
# arguments from view methods (since view is not present in some
|
76
|
+
# situations and when it is present, is always the first arg).
|
77
|
+
ctx.instance_exec(*args.reject { |arg|
|
78
|
+
arg.is_a?(ViewContext)
|
79
|
+
}, &block)
|
80
|
+
else
|
81
|
+
block.call(*args)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Pakyow
|
4
|
+
module UI
|
5
|
+
# Manages mutations.
|
6
|
+
#
|
7
|
+
# This is the default registry in production systems and is required in
|
8
|
+
# deployments with more than one app instance.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class RedisMutationRegistry
|
12
|
+
include Singleton
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
end
|
16
|
+
|
17
|
+
def register(scope, mutation)
|
18
|
+
Pakyow::Realtime.redis.sadd(key(scope), mutation.to_json)
|
19
|
+
end
|
20
|
+
|
21
|
+
def mutations(scope)
|
22
|
+
Pakyow::Realtime.redis.smembers(key(scope)).map do |m|
|
23
|
+
Hash.strhash(JSON.parse(m))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def key(scope)
|
30
|
+
"pui-mutation-#{scope}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Pakyow
|
2
|
+
module UI
|
3
|
+
# Manages mutations.
|
4
|
+
#
|
5
|
+
# Intended only for use in development or single app-instance deployments.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class SimpleMutationRegistry
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
reset
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset
|
16
|
+
@mutations = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def register(scope, mutation)
|
20
|
+
@mutations[scope] ||= []
|
21
|
+
|
22
|
+
return if @mutations[scope].include?(mutation)
|
23
|
+
@mutations[scope] << mutation
|
24
|
+
end
|
25
|
+
|
26
|
+
def mutations(scope)
|
27
|
+
@mutations[scope]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require_relative 'mutator'
|
2
|
+
require_relative 'channel_builder'
|
3
|
+
require_relative 'ui_view'
|
4
|
+
|
5
|
+
module Pakyow
|
6
|
+
module UI
|
7
|
+
# The UI context available during routing.
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
class UI
|
11
|
+
attr_accessor :context
|
12
|
+
attr_reader :mutator
|
13
|
+
|
14
|
+
# Informs Pakyow that a mutation has occurred in application state,
|
15
|
+
# triggering all the necessary realtime view updates.
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
def mutated(scope, data = nil, context = nil)
|
19
|
+
context ||= @context
|
20
|
+
|
21
|
+
MutationStore.instance.mutations(scope).each do |mutation|
|
22
|
+
view = UIView.new(scope)
|
23
|
+
|
24
|
+
qualified = true
|
25
|
+
|
26
|
+
# qualifiers are defined with the mutation
|
27
|
+
unless mutation[:qualifiers].empty? || data.nil?
|
28
|
+
mutation[:qualifiers].each_with_index do |qualifier, i|
|
29
|
+
qualified = false unless data[qualifier] == mutation[:query_args][i]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
qualified = false if data.nil? && !mutation[:qualifications].empty?
|
34
|
+
|
35
|
+
# qualifications are set on the subscription
|
36
|
+
unless !qualified || mutation[:qualifications].empty? || data.nil?
|
37
|
+
mutation[:qualifications].each_pair do |key, value|
|
38
|
+
qualified = false unless data[key] == value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
next unless qualified
|
43
|
+
|
44
|
+
mutable_data = Mutator.instance.mutable(scope, context).send(mutation[:query_name], *mutation[:query_args]).data
|
45
|
+
Mutator.instance.mutate(mutation[:mutation].to_sym, view, mutable_data)
|
46
|
+
|
47
|
+
Pakyow.app.socket.push(
|
48
|
+
view.finalize,
|
49
|
+
|
50
|
+
ChannelBuilder.build(
|
51
|
+
scope: scope,
|
52
|
+
mutation: mutation[:mutation].to_sym,
|
53
|
+
qualifiers: mutation[:qualifiers],
|
54
|
+
data: mutable_data,
|
55
|
+
qualifications: mutation[:qualifications]
|
56
|
+
)
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Addresses a component rendered on the client-side.
|
62
|
+
#
|
63
|
+
# @api public
|
64
|
+
def component(name, qualifications = {})
|
65
|
+
UIComponent.new(name, qualifications)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @api private
|
69
|
+
def load(mutators, mutables)
|
70
|
+
# TODO: this is another pattern I see all over the place
|
71
|
+
@mutator = Mutator.instance.reset
|
72
|
+
|
73
|
+
mutators.each_pair do |scope, block|
|
74
|
+
@mutator.set(scope, &block)
|
75
|
+
end
|
76
|
+
|
77
|
+
mutables.each_pair do |scope, block|
|
78
|
+
@mutator.mutable(scope, &block)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'ui_instructable'
|
2
|
+
|
3
|
+
module Pakyow
|
4
|
+
module UI
|
5
|
+
# Builds up instructions for changing view attributes.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class UIAttrs
|
9
|
+
include Instructable
|
10
|
+
|
11
|
+
def nested_instruct_object(_method, _data, _scope)
|
12
|
+
UIAttrs.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(method, value)
|
16
|
+
nested_instruct(method, value)
|
17
|
+
end
|
18
|
+
|
19
|
+
def class
|
20
|
+
method_missing(:class, nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
def id
|
24
|
+
method_missing(:id, nil)
|
25
|
+
end
|
26
|
+
|
27
|
+
def <<(value)
|
28
|
+
method_missing(:insert, value)
|
29
|
+
end
|
30
|
+
|
31
|
+
def []=(method, value)
|
32
|
+
method_missing(method, value)
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](method)
|
36
|
+
method_missing(method, nil)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require_relative 'ui_instructable'
|
2
|
+
|
3
|
+
module Pakyow
|
4
|
+
module UI
|
5
|
+
# An object for interacting with components rendered in a browser. Custom
|
6
|
+
# messages can be pushed and will be handled by the event listener defined
|
7
|
+
# on the client-side component.
|
8
|
+
#
|
9
|
+
# It's also possible to perform view transformations in realtime. Components
|
10
|
+
# implement a subset of transformations, including `scope` and `append`.
|
11
|
+
# This allows for finer control over particular components, completely
|
12
|
+
# bypassing mutables and mutators.
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
class UIComponent
|
16
|
+
include Instructable
|
17
|
+
|
18
|
+
attr_reader :name, :view, :qualifications
|
19
|
+
|
20
|
+
# Intended to be created through the `ui.component` helper.
|
21
|
+
#
|
22
|
+
# @api private
|
23
|
+
def initialize(name, qualifications = {})
|
24
|
+
super()
|
25
|
+
@name = name
|
26
|
+
@qualifications = qualifications
|
27
|
+
end
|
28
|
+
|
29
|
+
# Pushes a message to the component.
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def push(payload = nil)
|
33
|
+
payload ||= { instruct: (root || self).finalize }
|
34
|
+
|
35
|
+
Pakyow.app.socket.push(
|
36
|
+
payload,
|
37
|
+
|
38
|
+
ChannelBuilder.build(
|
39
|
+
component: name,
|
40
|
+
qualifications: qualifications
|
41
|
+
)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Narrows the scope of component instructions.
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
def scope(name)
|
49
|
+
nested_instruct(:scope, name.to_s, name)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Other supported transformation methods.
|
53
|
+
#
|
54
|
+
# @api public
|
55
|
+
%i(append prepend).each do |method|
|
56
|
+
define_method method do |value|
|
57
|
+
instruct(method, value)
|
58
|
+
push
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# @api private
|
63
|
+
def nested_instruct_object(_method, _data, _scope)
|
64
|
+
UIComponent.new(name, qualifications)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|