quince 0.3.0 → 0.4.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 +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
|