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
@@ -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
|