quince 0.3.0 → 0.5.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 +13 -14
- data/lib/quince/callback.js.erb +32 -0
- data/lib/quince/callback.rb +19 -6
- data/lib/quince/component.rb +31 -14
- data/lib/quince/html_tag_components.rb +48 -23
- data/lib/quince/singleton_methods.rb +53 -7
- data/lib/quince/types.rb +3 -8
- data/lib/quince/version.rb +1 -1
- data/scripts.js +130 -25
- 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: 8a30e10c958d56fc7a73d2515d200cae1080d407e2a19e132827b2b77837b6bb
|
4
|
+
data.tar.gz: 2f43465799409bb6a7336879189d35bfcfcd259b8637a1eec546826317034436
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86e5e960621d4e6ecde1ce2cf07486596b00e64aeb8ddecf0c82d7c2ac9c3818ee2f367256e1a1539af86a1eee534af572cd45c94d2ed8676d5bd4d9867543f9
|
7
|
+
data.tar.gz: 0360b4ed00cba7ec1604c1bd42d79a8fce65b065b409a0dcb4539816980de92a33bf1c181d8f09f7542699edac24508684d01e2ad05cd08630b5c14c5e06f433
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
quince (0.
|
4
|
+
quince (0.5.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.9)
|
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
|
@@ -4,14 +4,14 @@ require_relative "types"
|
|
4
4
|
|
5
5
|
module Quince
|
6
6
|
module HtmlTagComponents
|
7
|
-
referrer_policy = Rbs("'' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url' |
|
7
|
+
referrer_policy = Rbs("'' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url' | nil")
|
8
8
|
form_method = Rbs(
|
9
|
-
'"get" | "post" | "GET" | "POST" | :GET | :POST | :get | :post |
|
9
|
+
'"get" | "post" | "GET" | "POST" | :GET | :POST | :get | :post | nil'
|
10
10
|
)
|
11
11
|
t = Quince::Types
|
12
12
|
opt_string_sym = Rbs("#{t::OptionalString} | Symbol")
|
13
13
|
opt_bool = t::OptionalBoolean
|
14
|
-
opt_callback = Rbs("Quince::Callback::Interface |
|
14
|
+
opt_callback = Rbs("Quince::Callback::Interface | nil")
|
15
15
|
value = opt_string_sym # for now
|
16
16
|
|
17
17
|
ATTRIBUTES_BY_ELEMENT = {
|
@@ -58,7 +58,7 @@ module Quince
|
|
58
58
|
formnovalidate: opt_bool,
|
59
59
|
formtarget: opt_string_sym,
|
60
60
|
name: opt_string_sym,
|
61
|
-
type: Rbs("'submit' | 'reset' | 'button' |
|
61
|
+
type: Rbs("'submit' | 'reset' | 'button' | nil"),
|
62
62
|
value: value,
|
63
63
|
}.freeze,
|
64
64
|
"Canvas" => {
|
@@ -128,7 +128,7 @@ module Quince
|
|
128
128
|
allowfullscreen: opt_bool,
|
129
129
|
allowtransparency: opt_bool,
|
130
130
|
height: opt_string_sym,
|
131
|
-
loading: Rbs('"eager" | "lazy" |
|
131
|
+
loading: Rbs('"eager" | "lazy" | nil'),
|
132
132
|
name: opt_string_sym,
|
133
133
|
referrerpolicy: referrer_policy,
|
134
134
|
sandbox: opt_string_sym,
|
@@ -192,7 +192,7 @@ module Quince
|
|
192
192
|
"Ol" => {
|
193
193
|
reversed: opt_bool,
|
194
194
|
start: opt_string_sym,
|
195
|
-
type: Rbs("'1' | 'a' | 'A' | 'i' | 'I' |
|
195
|
+
type: Rbs("'1' | 'a' | 'A' | 'i' | 'I' | nil"),
|
196
196
|
}.freeze,
|
197
197
|
"Optgroup" => {
|
198
198
|
disabled: opt_bool,
|
@@ -270,7 +270,7 @@ module Quince
|
|
270
270
|
}.freeze,
|
271
271
|
"Tbody" => {}.freeze,
|
272
272
|
"Td" => {
|
273
|
-
align: Rbs('"left" | "center" | "right" | "justify" | "char" |
|
273
|
+
align: Rbs('"left" | "center" | "right" | "justify" | "char" | nil'),
|
274
274
|
colspan: opt_string_sym,
|
275
275
|
headers: opt_string_sym,
|
276
276
|
rowspan: opt_string_sym,
|
@@ -278,7 +278,7 @@ module Quince
|
|
278
278
|
abbr: opt_string_sym,
|
279
279
|
height: opt_string_sym,
|
280
280
|
width: opt_string_sym,
|
281
|
-
valign: Rbs('"top" | "middle" | "bottom" | "baseline" |
|
281
|
+
valign: Rbs('"top" | "middle" | "bottom" | "baseline" | nil'),
|
282
282
|
}.freeze,
|
283
283
|
"Textarea" => {
|
284
284
|
autocomplete: opt_string_sym,
|
@@ -300,7 +300,7 @@ module Quince
|
|
300
300
|
}.freeze,
|
301
301
|
"Tfoot" => {}.freeze,
|
302
302
|
"Th" => {
|
303
|
-
align: Rbs('"left" | "center" | "right" | "justify" | "char" |
|
303
|
+
align: Rbs('"left" | "center" | "right" | "justify" | "char" | nil'),
|
304
304
|
colspan: opt_string_sym,
|
305
305
|
headers: opt_string_sym,
|
306
306
|
rowspan: opt_string_sym,
|
@@ -364,10 +364,10 @@ module Quince
|
|
364
364
|
"Hr" => {}.freeze,
|
365
365
|
"Img" => {
|
366
366
|
alt: opt_string_sym,
|
367
|
-
crossorigin: Rbs('"anonymous" | "use-credentials" | "" |
|
368
|
-
decoding: Rbs('"async" | "auto" | "sync" |
|
367
|
+
crossorigin: Rbs('"anonymous" | "use-credentials" | "" | nil'),
|
368
|
+
decoding: Rbs('"async" | "auto" | "sync" | nil'),
|
369
369
|
height: Rbs("#{opt_string_sym} | Integer"),
|
370
|
-
loading: Rbs('"eager" | "lazy" |
|
370
|
+
loading: Rbs('"eager" | "lazy" | nil'),
|
371
371
|
referrerpolicy: referrer_policy,
|
372
372
|
sizes: opt_string_sym,
|
373
373
|
src: opt_string_sym,
|
@@ -475,8 +475,7 @@ module Quince
|
|
475
475
|
onsearch: opt_callback,
|
476
476
|
onkeyup: opt_callback,
|
477
477
|
onselect: opt_callback,
|
478
|
+
onscroll: opt_callback,
|
478
479
|
}.freeze
|
479
480
|
end
|
480
481
|
end
|
481
|
-
|
482
|
-
Undefined = Quince::Types::Undefined
|
@@ -0,0 +1,32 @@
|
|
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
|
+
`<%= (mode = rerender&.dig(:mode)) ? mode.to_s : "replace" %>`,
|
21
|
+
<%= value.handle_errors.to_json %>,
|
22
|
+
);
|
23
|
+
<% unless push_params_state == "null" %>Q.ps(<%= push_params_state %>);<% end %>
|
24
|
+
<% if value.debounce_ms&.positive? %>
|
25
|
+
}, <%= value.debounce_ms %>); window[`<%= fn_name %>`](p)
|
26
|
+
<% end %>
|
27
|
+
<% if value.if %>
|
28
|
+
};
|
29
|
+
<% end %>
|
30
|
+
<% if value.prevent_default %>
|
31
|
+
;return false;
|
32
|
+
<% end %>
|
data/lib/quince/callback.rb
CHANGED
@@ -6,8 +6,12 @@ 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,
|
13
|
+
push_params_state: nil,
|
14
|
+
handle_errors: true,
|
11
15
|
}.freeze
|
12
16
|
|
13
17
|
def callback(method_name, **opts)
|
@@ -22,15 +26,24 @@ module Quince
|
|
22
26
|
end
|
23
27
|
|
24
28
|
module Interface
|
25
|
-
attr_reader
|
29
|
+
attr_reader(
|
30
|
+
:receiver,
|
31
|
+
:method_name,
|
32
|
+
*ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
|
33
|
+
)
|
26
34
|
end
|
27
35
|
|
28
36
|
include Interface
|
29
37
|
|
30
|
-
def initialize(
|
38
|
+
def initialize(
|
39
|
+
receiver,
|
40
|
+
method_name,
|
41
|
+
**opts
|
42
|
+
)
|
31
43
|
@receiver, @method_name = receiver, method_name
|
32
|
-
|
33
|
-
|
44
|
+
ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.each_key do |opt|
|
45
|
+
instance_variable_set :"@#{opt}", opts.fetch(opt)
|
46
|
+
end
|
34
47
|
end
|
35
48
|
end
|
36
49
|
end
|
data/lib/quince/component.rb
CHANGED
@@ -9,30 +9,35 @@ module Quince
|
|
9
9
|
|
10
10
|
def Props(**kw)
|
11
11
|
self.const_set "Props", TypedStruct.new(
|
12
|
-
|
13
|
-
Quince::Component::
|
12
|
+
Quince::Component::PARENT_SELECTOR_ATTR => String,
|
13
|
+
Quince::Component::SELF_SELECTOR => String,
|
14
14
|
**kw,
|
15
15
|
)
|
16
16
|
end
|
17
17
|
|
18
18
|
def State(**kw)
|
19
|
-
st = kw.empty? ? nil : TypedStruct.new(
|
20
|
-
{ default: Quince::Types::Undefined },
|
21
|
-
**kw,
|
22
|
-
)
|
19
|
+
st = kw.empty? ? nil : TypedStruct.new(**kw)
|
23
20
|
self.const_set "State", st
|
24
21
|
end
|
25
22
|
|
26
|
-
def exposed(action,
|
23
|
+
def exposed(action, method: :POST)
|
27
24
|
@exposed_actions ||= Set.new
|
28
25
|
@exposed_actions.add action
|
29
26
|
route = "/api/#{self.name}/#{action}"
|
30
27
|
Quince.middleware.create_route_handler(
|
31
|
-
verb:
|
28
|
+
verb: method,
|
32
29
|
route: route,
|
33
30
|
) do |params|
|
34
31
|
instance = Quince::Serialiser.deserialise(CGI.unescapeHTML(params[:component]))
|
35
|
-
Quince::Component.class_variable_set :@@params, params
|
32
|
+
Quince::Component.class_variable_set :@@params, params[:params]
|
33
|
+
render_with = if params[:rerender]
|
34
|
+
instance.instance_variable_set :@state_container, params[:stateContainer]
|
35
|
+
params[:rerender][:method].to_sym
|
36
|
+
else
|
37
|
+
:render
|
38
|
+
end
|
39
|
+
instance.instance_variable_set :@render_with, render_with
|
40
|
+
instance.instance_variable_set :@callback_event, params[:event]
|
36
41
|
if @exposed_actions.member? action
|
37
42
|
instance.send action
|
38
43
|
instance
|
@@ -58,7 +63,13 @@ module Quince
|
|
58
63
|
private
|
59
64
|
|
60
65
|
def initialize_props(const, id, **props)
|
61
|
-
|
66
|
+
if const.const_defined?("Props")
|
67
|
+
const::Props.new(
|
68
|
+
PARENT_SELECTOR_ATTR => id,
|
69
|
+
**props,
|
70
|
+
SELF_SELECTOR => id
|
71
|
+
)
|
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,16 @@ 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
|
+
id = props ? props[SELF_SELECTOR] : __id
|
106
|
+
"[#{PARENT_SELECTOR_ATTR}='#{id}']".freeze
|
107
|
+
end
|
91
108
|
|
92
|
-
def
|
93
|
-
"[#{
|
109
|
+
def html_self_selector
|
110
|
+
"[#{SELF_SELECTOR}='#{__id}']".freeze
|
94
111
|
end
|
95
112
|
end
|
96
113
|
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,19 @@ 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
|
-
|
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 = selector
|
53
|
+
push_params_state = value.push_params_state.to_json
|
54
|
+
code = CALLBACK_ERB_INSTANCE.result(binding)
|
55
|
+
return %Q{#{key}="#{CGI.escape_html(code)}" data-qu-#{key}-state="#{CGI.escapeHTML(internal)}"}
|
52
56
|
when true
|
53
57
|
return key
|
54
|
-
when false, nil
|
55
|
-
return
|
58
|
+
when false, nil
|
59
|
+
return nil
|
56
60
|
else
|
57
61
|
raise "prop type not yet implemented #{value}"
|
58
62
|
end
|
@@ -88,14 +92,35 @@ module Quince
|
|
88
92
|
contents
|
89
93
|
}
|
90
94
|
end
|
91
|
-
end
|
92
95
|
|
93
|
-
|
94
|
-
|
96
|
+
ERROR_HANDLING_STYLES = <<~CSS.freeze
|
97
|
+
.quince-err-container {
|
98
|
+
position: absolute;
|
99
|
+
bottom: 32px;
|
100
|
+
width: 300px;
|
101
|
+
height: 50px;
|
102
|
+
background-color: white;
|
103
|
+
border-radius: 4px;
|
104
|
+
border: 1px solid #bbb;
|
105
|
+
z-index: 999;
|
106
|
+
box-shadow: 2px 2px 8px rgba(0,0,0,0.3);
|
107
|
+
padding: 0 24px;
|
108
|
+
left: calc(50vw - 50px);
|
109
|
+
}
|
95
110
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
111
|
+
.quince-err-msg {
|
112
|
+
font-size: 1.2em;
|
113
|
+
text-align: center;
|
114
|
+
margin: auto;
|
115
|
+
color: chocolate;
|
116
|
+
}
|
117
|
+
CSS
|
118
|
+
private_constant :ERROR_HANDLING_STYLES
|
119
|
+
|
120
|
+
def error_message_styles
|
121
|
+
style(ERROR_HANDLING_STYLES)
|
122
|
+
end
|
100
123
|
end
|
124
|
+
|
125
|
+
Quince::Component.include HtmlTagComponents
|
101
126
|
end
|
@@ -16,18 +16,37 @@ 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
|
22
23
|
Object.send :private, :expose
|
23
24
|
end
|
24
25
|
|
25
|
-
def define_constructor(const, constructor_name =
|
26
|
+
def define_constructor(const, constructor_name = nil)
|
27
|
+
if const.name
|
28
|
+
parts = const.name.split("::")
|
29
|
+
parent_namespace = Object.const_get(parts[0...-1].join("::")) if parts.length > 1
|
30
|
+
constructor_name ||= parts.last
|
31
|
+
end
|
32
|
+
constructor_name ||= const.to_s
|
33
|
+
|
26
34
|
HtmlTagComponents.instance_eval do
|
27
|
-
|
28
|
-
new_props = {
|
35
|
+
mthd = lambda do |*children, **props, &block_children|
|
36
|
+
new_props = {
|
37
|
+
**props,
|
38
|
+
Quince::Component::PARENT_SELECTOR_ATTR => __id,
|
39
|
+
}
|
29
40
|
const.create(*children, **new_props, &block_children)
|
30
41
|
end
|
42
|
+
|
43
|
+
if parent_namespace
|
44
|
+
parent_namespace.instance_exec do
|
45
|
+
define_method(constructor_name, &mthd)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
define_method(constructor_name, &mthd)
|
49
|
+
end
|
31
50
|
end
|
32
51
|
end
|
33
52
|
|
@@ -44,12 +63,32 @@ module Quince
|
|
44
63
|
output = to_html(output.call)
|
45
64
|
when NilClass
|
46
65
|
output = ""
|
47
|
-
|
66
|
+
when Component
|
48
67
|
tmp = output
|
49
|
-
|
50
|
-
|
51
|
-
|
68
|
+
render_with = output.instance_variable_get(:@render_with) || :render
|
69
|
+
output = output.send render_with
|
70
|
+
case render_with
|
71
|
+
when :render
|
72
|
+
if output.is_a?(Array)
|
73
|
+
raise "#render in #{tmp.class} should not return multiple elements. Consider wrapping it in a div"
|
74
|
+
end
|
75
|
+
else
|
76
|
+
internal = Quince::Serialiser.serialise tmp
|
77
|
+
updated_state = CGI.escapeHTML(internal).to_json
|
78
|
+
selector = tmp.instance_variable_get :@state_container
|
79
|
+
event = tmp.instance_variable_get :@callback_event
|
80
|
+
|
81
|
+
scr = to_html(HtmlTagComponents::Script.create(<<~JS, type: "text/javascript"))
|
82
|
+
var stateContainer = document.querySelector(`#{selector}`);
|
83
|
+
console.log('yes');
|
84
|
+
stateContainer.dataset.quOn#{event}State = #{updated_state};
|
85
|
+
JS
|
86
|
+
output = output.render if output.is_a?(Component)
|
87
|
+
|
88
|
+
output += (output.is_a?(String) ? scr : [scr])
|
52
89
|
end
|
90
|
+
else
|
91
|
+
raise "don't know how to render #{output.class} (#{output.inspect})"
|
53
92
|
end
|
54
93
|
end
|
55
94
|
|
@@ -57,3 +96,10 @@ module Quince
|
|
57
96
|
end
|
58
97
|
end
|
59
98
|
end
|
99
|
+
|
100
|
+
############## TODO #############
|
101
|
+
# I think you should be able to know when a component is the first to be called in a render method,
|
102
|
+
# so you should be able to attach some props to it behind the scenes. Then any consumers of this
|
103
|
+
# state just have to know the selector, so they can read from it before passing it to the back end.
|
104
|
+
#
|
105
|
+
# Also, the front end needs to be updated such that script tags from the back end are always read
|
data/lib/quince/types.rb
CHANGED
@@ -1,13 +1,8 @@
|
|
1
1
|
module Quince
|
2
2
|
module Types
|
3
|
-
class Base; end
|
4
|
-
|
5
3
|
# precompiled helper types
|
6
|
-
OptionalString = Rbs("String
|
7
|
-
OptionalBoolean = Rbs("true | false |
|
8
|
-
|
9
|
-
# no functional value for now, other than constants
|
10
|
-
Undefined = Class.new(Base).new.freeze
|
11
|
-
Any = Class.new(Base).new.freeze
|
4
|
+
OptionalString = Rbs("String?").freeze
|
5
|
+
OptionalBoolean = Rbs("true | false | nil").freeze
|
6
|
+
Any = Rbs("untyped").freeze
|
12
7
|
end
|
13
8
|
end
|
data/lib/quince/version.rb
CHANGED
data/scripts.js
CHANGED
@@ -1,28 +1,133 @@
|
|
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, handleErrors) => {
|
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 => {
|
13
|
+
if (resp.status <= 299) {
|
14
|
+
return resp.text()
|
15
|
+
} else if (resp.status >= 500) {
|
16
|
+
throw Q.em["500"];
|
17
|
+
} else {
|
18
|
+
let msg = Q.em[`${resp.code}`];
|
19
|
+
|
20
|
+
if (!msg && resp.code >= 400) msg = Q.em.generic;
|
21
|
+
|
22
|
+
throw msg;
|
23
|
+
}
|
24
|
+
}).then(html => {
|
25
|
+
const element = document.querySelector(selector);
|
26
|
+
if (!element) {
|
27
|
+
throw `element not found for ${selector}`;
|
28
|
+
}
|
29
|
+
|
30
|
+
switch (mode) {
|
31
|
+
case "append_diff":
|
32
|
+
const tmpElem = document.createElement(element.nodeName);
|
33
|
+
tmpElem.innerHTML = html;
|
34
|
+
const newNodes = Array.from(tmpElem.childNodes);
|
35
|
+
const script = newNodes.pop();
|
36
|
+
const existingChildren = element.childNodes;
|
37
|
+
// This comparison doesn't currently work because of each node's unique id (data-quid).
|
38
|
+
// maybe it would be possible to use regex replace to on the raw html, but it could also
|
39
|
+
// be overkill
|
40
|
+
// let c = 0;
|
41
|
+
// for (; c < existingChildren.length; c++) {
|
42
|
+
// if (existingChildren[c].isEqualNode(newNodes[c]))
|
43
|
+
// continue;
|
44
|
+
// else
|
45
|
+
// break;
|
46
|
+
// }
|
47
|
+
// for the time being, we can just assume that we can just take the extra items
|
48
|
+
let c = existingChildren.length;
|
49
|
+
for (const node of newNodes.slice(c)) {
|
50
|
+
element.appendChild(node);
|
51
|
+
}
|
16
52
|
|
17
|
-
|
18
|
-
|
19
|
-
|
53
|
+
const newScript = document.createElement("script");
|
54
|
+
newScript.dataset.quid = script.dataset.quid;
|
55
|
+
newScript.innerHTML = script.innerHTML;
|
56
|
+
document.head.appendChild(newScript);
|
57
|
+
break;
|
58
|
+
case "replace":
|
59
|
+
element.outerHTML = html;
|
60
|
+
break;
|
61
|
+
default:
|
62
|
+
throw `mode ${mode} is not valid`;
|
63
|
+
}
|
64
|
+
}).catch(err => {
|
65
|
+
if (!handleErrors) throw err;
|
66
|
+
|
67
|
+
let msg;
|
68
|
+
if (typeof err === "string") {
|
69
|
+
msg = err;
|
70
|
+
} else if (err.message) {
|
71
|
+
if (err.message.startsWith("NetworkError")) {
|
72
|
+
msg = Q.em.network;
|
73
|
+
} else {
|
74
|
+
msg = err.message;
|
75
|
+
}
|
76
|
+
} else msg = Q.em.generic;
|
77
|
+
Q.e(msg, 2500);
|
78
|
+
})
|
79
|
+
},
|
80
|
+
f: (elem) => {
|
81
|
+
let form = elem.localName === "form" ? elem : elem.form;
|
82
|
+
if (!form) {
|
83
|
+
throw `element ${elem} should belong to a form`;
|
84
|
+
}
|
85
|
+
const fd = new FormData(form);
|
86
|
+
return Object.fromEntries(fd.entries());
|
87
|
+
},
|
88
|
+
d: (func, wait_ms) => {
|
89
|
+
let timer = null;
|
20
90
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
91
|
+
return (...args) => {
|
92
|
+
clearTimeout(timer);
|
93
|
+
return new Promise((resolve) => {
|
94
|
+
timer = setTimeout(
|
95
|
+
() => resolve(func(...args)),
|
96
|
+
wait_ms,
|
97
|
+
);
|
98
|
+
});
|
99
|
+
};
|
100
|
+
},
|
101
|
+
ps: (stateObj) => {
|
102
|
+
const base = location.origin + location.pathname;
|
103
|
+
const url = new URL(base);
|
104
|
+
for (const p in stateObj) {
|
105
|
+
url.searchParams.append(p, stateObj[p]);
|
106
|
+
};
|
107
|
+
window.history.pushState({}, document.title, url);
|
108
|
+
},
|
109
|
+
e: (msg, durationMs) => {
|
110
|
+
const containerClassName = "quince-err-container";
|
111
|
+
document.querySelectorAll(`.${containerClassName}`).forEach(e => e.remove());
|
112
|
+
const container = document.createElement("div");
|
113
|
+
const strong = document.createElement("strong");
|
114
|
+
strong.innerText = msg;
|
115
|
+
container.className = containerClassName;
|
116
|
+
strong.className = "quince-err-msg";
|
117
|
+
container.appendChild(strong);
|
118
|
+
document.body.insertAdjacentElement("afterbegin", container);
|
119
|
+
setTimeout(() => container.remove(), durationMs);
|
120
|
+
},
|
121
|
+
em: {
|
122
|
+
400: "Bad request",
|
123
|
+
401: "Unauthorised",
|
124
|
+
402: "Payment required",
|
125
|
+
403: "Forbidden",
|
126
|
+
404: "Not found",
|
127
|
+
422: "Unprocessable entity",
|
128
|
+
429: "Too many requests",
|
129
|
+
500: "Internal server error",
|
130
|
+
generic: "An error occurred",
|
131
|
+
network: "Network error",
|
25
132
|
}
|
26
|
-
|
27
|
-
return Object.fromEntries(fd.entries());
|
28
|
-
}
|
133
|
+
};
|
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.5.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-
|
11
|
+
date: 2021-10-15 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
|