quince 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/lib/quince/attributes_by_element.rb +1 -0
- data/lib/quince/callback.js.erb +30 -0
- data/lib/quince/callback.rb +17 -6
- data/lib/quince/component.rb +24 -8
- data/lib/quince/html_tag_components.rb +18 -15
- data/lib/quince/singleton_methods.rb +24 -5
- data/lib/quince/version.rb +1 -1
- data/scripts.js +73 -26
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0039bd9ca9658cfebba4048cc84fdbb095d8f2e656930501528f420c4dfa3bba'
|
4
|
+
data.tar.gz: a9720e7b11db54113fd56f1e09e43c980893297bbd5d3f46b1cdd0fc8ea2b61e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6117b88cd23184b2072adae7b3b99ca868197119ffac9344477e6048519c735c7daa498067a4b96a92b60d68ce8fb5e0cc5a40529734c5c01c3b49e58317002d
|
7
|
+
data.tar.gz: f7952f3eea1cee2ef4153cbe4be1a5c709d777130f7eaa3be3ec0f5fa728da3550b81713766fcea6003673d8a65dd8a92aaae4ee3b1fb599b773463a67c5f05d
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
quince (0.
|
4
|
+
quince (0.4.0)
|
5
5
|
oj (~> 3.13)
|
6
6
|
typed_struct (>= 0.1.4)
|
7
7
|
|
@@ -9,7 +9,7 @@ GEM
|
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
11
|
diff-lcs (1.4.4)
|
12
|
-
oj (3.13.
|
12
|
+
oj (3.13.7)
|
13
13
|
rake (13.0.6)
|
14
14
|
rbs (1.6.2)
|
15
15
|
rspec (3.10.0)
|
data/README.md
CHANGED
@@ -92,8 +92,8 @@ class Counter < Quince::Component
|
|
92
92
|
def render
|
93
93
|
div(
|
94
94
|
h2("count is #{state.val}"),
|
95
|
-
button(onclick:
|
96
|
-
button(onclick:
|
95
|
+
button(onclick: callback(:increment)) { "++" },
|
96
|
+
button(onclick: callback(:decrement)) { "--" }
|
97
97
|
)
|
98
98
|
end
|
99
99
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
const p = this.dataset[`quOn<%= key.to_s[2..-1] %>State`];
|
2
|
+
<% if value.debugger %>debugger;<% end %>
|
3
|
+
<% if value.if %>
|
4
|
+
if (<%= value.if %>) {
|
5
|
+
<% end %>
|
6
|
+
<% if value.debounce_ms %>
|
7
|
+
if (!window[`<%= fn_name %>`]) window[`<%= fn_name %>`] = Q.d((p) => {
|
8
|
+
<% end %>
|
9
|
+
Q.c(
|
10
|
+
`<%= endpoint %>`,
|
11
|
+
JSON.stringify(
|
12
|
+
{component: p, event: `<%= key.to_s[2..-1] %>`,stateContainer: `<%= state_container %>`,
|
13
|
+
<% if value.take_form_values %>
|
14
|
+
params: Q.f(this),
|
15
|
+
<% end %>
|
16
|
+
<% if rerender %>
|
17
|
+
rerender: <%= rerender.to_json %>,
|
18
|
+
<% end %>}),
|
19
|
+
`<%= rerender&.dig(:selector)&.to_s || selector %>`,
|
20
|
+
<% if mode = rerender&.dig(:mode) %>`<%= mode.to_s %>`<% end %>
|
21
|
+
);
|
22
|
+
<% if value.debounce_ms&.positive? %>
|
23
|
+
}, <%= value.debounce_ms %>); window[`<%= fn_name %>`](p)
|
24
|
+
<% end %>
|
25
|
+
<% if value.if %>
|
26
|
+
};
|
27
|
+
<% end %>
|
28
|
+
<% if value.prevent_default %>
|
29
|
+
;return false;
|
30
|
+
<% end %>
|
data/lib/quince/callback.rb
CHANGED
@@ -6,8 +6,10 @@ module Quince
|
|
6
6
|
DEFAULT_CALLBACK_OPTIONS = {
|
7
7
|
prevent_default: false,
|
8
8
|
take_form_values: false,
|
9
|
-
|
10
|
-
|
9
|
+
debounce_ms: nil,
|
10
|
+
if: nil,
|
11
|
+
debugger: false,
|
12
|
+
rerender: nil,
|
11
13
|
}.freeze
|
12
14
|
|
13
15
|
def callback(method_name, **opts)
|
@@ -22,15 +24,24 @@ module Quince
|
|
22
24
|
end
|
23
25
|
|
24
26
|
module Interface
|
25
|
-
attr_reader
|
27
|
+
attr_reader(
|
28
|
+
:receiver,
|
29
|
+
:method_name,
|
30
|
+
*ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
|
31
|
+
)
|
26
32
|
end
|
27
33
|
|
28
34
|
include Interface
|
29
35
|
|
30
|
-
def initialize(
|
36
|
+
def initialize(
|
37
|
+
receiver,
|
38
|
+
method_name,
|
39
|
+
**opts
|
40
|
+
)
|
31
41
|
@receiver, @method_name = receiver, method_name
|
32
|
-
|
33
|
-
|
42
|
+
ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.each_key do |opt|
|
43
|
+
instance_variable_set :"@#{opt}", opts.fetch(opt)
|
44
|
+
end
|
34
45
|
end
|
35
46
|
end
|
36
47
|
end
|
data/lib/quince/component.rb
CHANGED
@@ -10,7 +10,8 @@ module Quince
|
|
10
10
|
def Props(**kw)
|
11
11
|
self.const_set "Props", TypedStruct.new(
|
12
12
|
{ default: Quince::Types::Undefined },
|
13
|
-
Quince::Component::
|
13
|
+
Quince::Component::PARENT_SELECTOR_ATTR => String,
|
14
|
+
Quince::Component::SELF_SELECTOR => String,
|
14
15
|
**kw,
|
15
16
|
)
|
16
17
|
end
|
@@ -23,16 +24,24 @@ module Quince
|
|
23
24
|
self.const_set "State", st
|
24
25
|
end
|
25
26
|
|
26
|
-
def exposed(action,
|
27
|
+
def exposed(action, method: :POST)
|
27
28
|
@exposed_actions ||= Set.new
|
28
29
|
@exposed_actions.add action
|
29
30
|
route = "/api/#{self.name}/#{action}"
|
30
31
|
Quince.middleware.create_route_handler(
|
31
|
-
verb:
|
32
|
+
verb: method,
|
32
33
|
route: route,
|
33
34
|
) do |params|
|
34
35
|
instance = Quince::Serialiser.deserialise(CGI.unescapeHTML(params[:component]))
|
35
36
|
Quince::Component.class_variable_set :@@params, params
|
37
|
+
render_with = if params[:rerender]
|
38
|
+
instance.instance_variable_set :@state_container, params[:stateContainer]
|
39
|
+
params[:rerender][:method].to_sym
|
40
|
+
else
|
41
|
+
:render
|
42
|
+
end
|
43
|
+
instance.instance_variable_set :@render_with, render_with
|
44
|
+
instance.instance_variable_set :@callback_event, params[:event]
|
36
45
|
if @exposed_actions.member? action
|
37
46
|
instance.send action
|
38
47
|
instance
|
@@ -58,7 +67,9 @@ module Quince
|
|
58
67
|
private
|
59
68
|
|
60
69
|
def initialize_props(const, id, **props)
|
61
|
-
|
70
|
+
if const.const_defined?("Props")
|
71
|
+
const::Props.new(PARENT_SELECTOR_ATTR => id, **props, SELF_SELECTOR => id)
|
72
|
+
end
|
62
73
|
end
|
63
74
|
end
|
64
75
|
|
@@ -76,7 +87,7 @@ module Quince
|
|
76
87
|
protected
|
77
88
|
|
78
89
|
def to(route, via: :POST)
|
79
|
-
self.class.exposed route,
|
90
|
+
self.class.exposed route, method: via
|
80
91
|
end
|
81
92
|
|
82
93
|
def params
|
@@ -87,10 +98,15 @@ module Quince
|
|
87
98
|
|
88
99
|
attr_reader :__id
|
89
100
|
|
90
|
-
|
101
|
+
PARENT_SELECTOR_ATTR = :"data-quid-parent"
|
102
|
+
SELF_SELECTOR = :"data-quid"
|
103
|
+
|
104
|
+
def html_parent_selector
|
105
|
+
"[#{PARENT_SELECTOR_ATTR}='#{__id}']".freeze
|
106
|
+
end
|
91
107
|
|
92
|
-
def
|
93
|
-
"[#{
|
108
|
+
def html_self_selector
|
109
|
+
"[#{SELF_SELECTOR}='#{props[SELF_SELECTOR]}']".freeze
|
94
110
|
end
|
95
111
|
end
|
96
112
|
end
|
@@ -1,9 +1,13 @@
|
|
1
1
|
require "oj"
|
2
2
|
require_relative "attributes_by_element"
|
3
3
|
require_relative "serialiser"
|
4
|
+
require "erb"
|
4
5
|
|
5
6
|
module Quince
|
6
7
|
module HtmlTagComponents
|
8
|
+
CALLBACK_TEMPLATE = File.read(File.join(__dir__, "callback.js.erb")).delete!("\n").freeze
|
9
|
+
CALLBACK_ERB_INSTANCE = ERB.new(CALLBACK_TEMPLATE)
|
10
|
+
|
7
11
|
def self.define_html_tag_component(const_name, attrs, self_closing: false)
|
8
12
|
klass = Class.new(Quince::Component) do
|
9
13
|
Props(
|
@@ -17,9 +21,12 @@ module Quince
|
|
17
21
|
props.each_pair.map { |k, v| to_html_attr(k, v) }.compact.join(" ")
|
18
22
|
end
|
19
23
|
result = "<#{tag_name}"
|
20
|
-
|
21
|
-
|
22
|
-
|
24
|
+
if !attrs.empty?
|
25
|
+
result << " #{attrs}>"
|
26
|
+
return result if self_closing?
|
27
|
+
elsif attrs.empty? && self_closing?
|
28
|
+
return result << ">"
|
29
|
+
end
|
23
30
|
|
24
31
|
result << Quince.to_html(children)
|
25
32
|
result << "</#{tag_name}>"
|
@@ -37,22 +44,18 @@ module Quince
|
|
37
44
|
receiver = value.receiver
|
38
45
|
owner = receiver.class.name
|
39
46
|
name = value.method_name
|
40
|
-
|
47
|
+
endpoint = "/api/#{owner}/#{name}"
|
48
|
+
selector = receiver.send :html_parent_selector
|
41
49
|
internal = Quince::Serialiser.serialise receiver
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
payload_var_name
|
48
|
-
end
|
49
|
-
cb = %Q{const #{payload_var_name} = #{payload}; callRemoteEndpoint(`/api/#{owner}/#{name}`, JSON.stringify(#{stringify_payload}),`#{selector}`)}
|
50
|
-
cb += ";return false" if value.prevent_default
|
51
|
-
CGI.escape_html(cb)
|
50
|
+
fn_name = "_Q_#{key}_#{receiver.send(:__id)}"
|
51
|
+
rerender = value.rerender
|
52
|
+
state_container = html_self_selector
|
53
|
+
code = CALLBACK_ERB_INSTANCE.result(binding)
|
54
|
+
return %Q{#{key}="#{CGI.escape_html(code)}" data-qu-#{key}-state="#{CGI.escapeHTML(internal)}"}
|
52
55
|
when true
|
53
56
|
return key
|
54
57
|
when false, nil, Quince::Types::Undefined
|
55
|
-
return
|
58
|
+
return nil
|
56
59
|
else
|
57
60
|
raise "prop type not yet implemented #{value}"
|
58
61
|
end
|
@@ -16,6 +16,7 @@ module Quince
|
|
16
16
|
) do |params|
|
17
17
|
component = component.create if component.instance_of? Class
|
18
18
|
Quince::Component.class_variable_set :@@params, params
|
19
|
+
component.instance_variable_set :@render_with, :render
|
19
20
|
component
|
20
21
|
end
|
21
22
|
end
|
@@ -25,7 +26,7 @@ module Quince
|
|
25
26
|
def define_constructor(const, constructor_name = const.to_s)
|
26
27
|
HtmlTagComponents.instance_eval do
|
27
28
|
define_method(constructor_name) do |*children, **props, &block_children|
|
28
|
-
new_props = { **props, Quince::Component::
|
29
|
+
new_props = { **props, Quince::Component::PARENT_SELECTOR_ATTR => __id }
|
29
30
|
const.create(*children, **new_props, &block_children)
|
30
31
|
end
|
31
32
|
end
|
@@ -44,12 +45,30 @@ module Quince
|
|
44
45
|
output = to_html(output.call)
|
45
46
|
when NilClass
|
46
47
|
output = ""
|
47
|
-
|
48
|
+
when Component
|
48
49
|
tmp = output
|
49
|
-
|
50
|
-
|
51
|
-
|
50
|
+
render_with = output.instance_variable_get(:@render_with) || :render
|
51
|
+
output = output.send render_with
|
52
|
+
case render_with
|
53
|
+
when :render
|
54
|
+
if output.is_a?(Array)
|
55
|
+
raise "#render in #{tmp.class} should not return multiple elements. Consider wrapping it in a div"
|
56
|
+
end
|
57
|
+
else
|
58
|
+
internal = Quince::Serialiser.serialise tmp
|
59
|
+
updated_state = CGI.escapeHTML(internal).to_json
|
60
|
+
selector = tmp.instance_variable_get :@state_container
|
61
|
+
event = tmp.instance_variable_get :@callback_event
|
62
|
+
|
63
|
+
scr = to_html(HtmlTagComponents::Script.create(<<~JS, type: "text/javascript"))
|
64
|
+
var stateContainer = document.querySelector(`#{selector}`);
|
65
|
+
stateContainer.dataset.quOn#{event}State = #{updated_state};
|
66
|
+
JS
|
67
|
+
|
68
|
+
output += (output.is_a?(String) ? scr : [scr])
|
52
69
|
end
|
70
|
+
else
|
71
|
+
raise "don't know how to render #{output.class} (#{output.inspect})"
|
53
72
|
end
|
54
73
|
end
|
55
74
|
|
data/lib/quince/version.rb
CHANGED
data/scripts.js
CHANGED
@@ -1,28 +1,75 @@
|
|
1
|
-
const
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
1
|
+
const Q = {
|
2
|
+
c: (endpoint, payload, selector, mode = "replace") => {
|
3
|
+
return fetch(
|
4
|
+
endpoint,
|
5
|
+
{
|
6
|
+
method: `POST`,
|
7
|
+
headers: {
|
8
|
+
"Content-Type": `application/json;charset=utf-8`
|
9
|
+
},
|
10
|
+
body: payload,
|
11
|
+
}
|
12
|
+
).then(resp => resp.text()).then(html => {
|
13
|
+
const element = document.querySelector(selector);
|
14
|
+
if (!element) {
|
15
|
+
throw `element not found for ${selector}`;
|
16
|
+
}
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
switch (mode) {
|
19
|
+
case "append_diff":
|
20
|
+
const tmpElem = document.createElement(element.nodeName);
|
21
|
+
tmpElem.innerHTML = html;
|
22
|
+
const newNodes = Array.from(tmpElem.childNodes);
|
23
|
+
const script = newNodes.pop();
|
24
|
+
const existingChildren = element.childNodes;
|
25
|
+
// This comparison doesn't currently work because of each node's unique id (data-quid).
|
26
|
+
// maybe it would be possible to use regex replace to on the raw html, but it could also
|
27
|
+
// be overkill
|
28
|
+
// let c = 0;
|
29
|
+
// for (; c < existingChildren.length; c++) {
|
30
|
+
// if (existingChildren[c].isEqualNode(newNodes[c]))
|
31
|
+
// continue;
|
32
|
+
// else
|
33
|
+
// break;
|
34
|
+
// }
|
35
|
+
// for the time being, we can just assume that we can just take the extra items
|
36
|
+
let c = existingChildren.length;
|
37
|
+
for (const node of newNodes.slice(c)) {
|
38
|
+
element.appendChild(node);
|
39
|
+
}
|
40
|
+
|
41
|
+
const newScript = document.createElement("script");
|
42
|
+
newScript.dataset.quid = script.dataset.quid;
|
43
|
+
newScript.innerHTML = script.innerHTML;
|
44
|
+
document.head.appendChild(newScript);
|
45
|
+
break;
|
46
|
+
case "replace":
|
47
|
+
element.outerHTML = html;
|
48
|
+
break;
|
49
|
+
default:
|
50
|
+
throw `mode ${mode} is not valid`;
|
51
|
+
}
|
52
|
+
})
|
53
|
+
},
|
54
|
+
f: (elem) => {
|
55
|
+
let form = elem.localName === "form" ? elem : elem.form;
|
56
|
+
if (!form) {
|
57
|
+
throw `element ${elem} should belong to a form`;
|
58
|
+
}
|
59
|
+
const fd = new FormData(form);
|
60
|
+
return Object.fromEntries(fd.entries());
|
61
|
+
},
|
62
|
+
d: (func, wait_ms) => {
|
63
|
+
let timer = null;
|
20
64
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
}
|
65
|
+
return (...args) => {
|
66
|
+
clearTimeout(timer);
|
67
|
+
return new Promise((resolve) => {
|
68
|
+
timer = setTimeout(
|
69
|
+
() => resolve(func(...args)),
|
70
|
+
wait_ms,
|
71
|
+
);
|
72
|
+
});
|
73
|
+
};
|
74
|
+
},
|
75
|
+
};
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quince
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joseph Johansen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-09-
|
11
|
+
date: 2021-09-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: typed_struct
|
@@ -57,6 +57,7 @@ files:
|
|
57
57
|
- bin/setup
|
58
58
|
- lib/quince.rb
|
59
59
|
- lib/quince/attributes_by_element.rb
|
60
|
+
- lib/quince/callback.js.erb
|
60
61
|
- lib/quince/callback.rb
|
61
62
|
- lib/quince/component.rb
|
62
63
|
- lib/quince/html_tag_components.rb
|