quince 0.4.1 โ 0.6.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 +26 -2
- data/README.md +13 -38
- data/lib/quince/attributes_by_element.rb +14 -16
- data/lib/quince/callback.js.erb +2 -1
- data/lib/quince/callback.rb +3 -1
- data/lib/quince/component.rb +26 -19
- data/lib/quince/html_tag_components.rb +29 -8
- data/lib/quince/sinatra.rb +70 -0
- data/lib/quince/singleton_methods.rb +27 -24
- data/lib/quince/types.rb +3 -8
- data/lib/quince/version.rb +1 -1
- data/lib/quince.rb +1 -0
- data/quince.gemspec +3 -0
- data/scripts.js +93 -37
- metadata +45 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f42655bc57603a51bd5231388dc7be387a51a8e912549f6a504fbaeb0e1dbb3e
|
4
|
+
data.tar.gz: ed87a57062ce7343b08409fd8126034a7dbf46e094b1741829fc63fdb9ab1f7d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c5358907ee716710a7c0f53c277ede1301ee16c31ab2304a63d33cdd91dff9f80a282f2832637d7b5ad14d4103d65a5f9ce16c4ed02cda14f9a148145263ee5
|
7
|
+
data.tar.gz: 0dc9bad86700382b48810bf70a2c0b0258c7e3383a4b73a37ea84fc46bf8ca174f2e20404c5070723512ffd2c1f0b1a61447bd9ce3da9eec6275e2dc8a003309
|
data/Gemfile.lock
CHANGED
@@ -1,15 +1,26 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
quince (0.
|
4
|
+
quince (0.6.0)
|
5
5
|
oj (~> 3.13)
|
6
|
+
rack-contrib (~> 2.3)
|
7
|
+
sinatra (~> 2.1)
|
8
|
+
sinatra-contrib (~> 2.1)
|
6
9
|
typed_struct (>= 0.1.4)
|
7
10
|
|
8
11
|
GEM
|
9
12
|
remote: https://rubygems.org/
|
10
13
|
specs:
|
11
14
|
diff-lcs (1.4.4)
|
12
|
-
|
15
|
+
multi_json (1.15.0)
|
16
|
+
mustermann (1.1.1)
|
17
|
+
ruby2_keywords (~> 0.0.1)
|
18
|
+
oj (3.13.9)
|
19
|
+
rack (2.2.3)
|
20
|
+
rack-contrib (2.3.0)
|
21
|
+
rack (~> 2.0)
|
22
|
+
rack-protection (2.1.0)
|
23
|
+
rack
|
13
24
|
rake (13.0.6)
|
14
25
|
rbs (1.6.2)
|
15
26
|
rspec (3.10.0)
|
@@ -25,6 +36,19 @@ GEM
|
|
25
36
|
diff-lcs (>= 1.2.0, < 2.0)
|
26
37
|
rspec-support (~> 3.10.0)
|
27
38
|
rspec-support (3.10.2)
|
39
|
+
ruby2_keywords (0.0.5)
|
40
|
+
sinatra (2.1.0)
|
41
|
+
mustermann (~> 1.0)
|
42
|
+
rack (~> 2.2)
|
43
|
+
rack-protection (= 2.1.0)
|
44
|
+
tilt (~> 2.0)
|
45
|
+
sinatra-contrib (2.1.0)
|
46
|
+
multi_json
|
47
|
+
mustermann (~> 1.0)
|
48
|
+
rack-protection (= 2.1.0)
|
49
|
+
sinatra (= 2.1.0)
|
50
|
+
tilt (~> 2.0)
|
51
|
+
tilt (2.0.10)
|
28
52
|
typed_struct (0.1.4)
|
29
53
|
rbs (~> 1.0)
|
30
54
|
|
data/README.md
CHANGED
@@ -10,7 +10,8 @@ React, Turbo, Hotwire amongst others
|
|
10
10
|
|
11
11
|
### Current status
|
12
12
|
|
13
|
-
|
13
|
+
Early, but [working in production](https://quince-rb.herokuapp.com/). Expect more features and
|
14
|
+
optimisations to come, but also potential for big changes between versions in the early stages.
|
14
15
|
|
15
16
|
### How it works
|
16
17
|
|
@@ -22,7 +23,7 @@ Proof of concept, but [working in production](https://quince-rb.herokuapp.com/),
|
|
22
23
|
|
23
24
|
```ruby
|
24
25
|
# app.rb
|
25
|
-
require "
|
26
|
+
require "quince"
|
26
27
|
|
27
28
|
class App < Quince::Component
|
28
29
|
def render
|
@@ -46,7 +47,7 @@ ruby app.rb
|
|
46
47
|
## More complex example
|
47
48
|
|
48
49
|
```ruby
|
49
|
-
require '
|
50
|
+
require 'quince'
|
50
51
|
|
51
52
|
class App < Quince::Component
|
52
53
|
def render
|
@@ -114,49 +115,23 @@ expose App, at: "/"
|
|
114
115
|
|
115
116
|
### Why?
|
116
117
|
|
117
|
-
- Lightweight ๐ชถ
|
118
|
-
- Very few dependencies
|
119
|
-
- Just a couple hundred lines of core logic
|
120
|
-
- Fewer than 30 lines (unminified) of JavaScript in the front end
|
121
|
-
- Plug and play into multiple back ends ๐
|
122
118
|
- Components > templates ๐งฉ
|
123
|
-
|
124
|
-
- no special syntax or compilation required
|
119
|
+
- Lightweight ๐ชถ
|
125
120
|
- Shallow learning curve if you are already familiar with React ๐
|
126
|
-
-
|
127
|
-
|
128
|
-
- routes
|
129
|
-
- controllers
|
130
|
-
- front end -> back end communication/APIs/Data transfer
|
131
|
-
- front end -> back end code sharing
|
132
|
-
- Quince handles these for you
|
133
|
-
- No node_modules ๐ฆ
|
134
|
-
- No yarn/npm
|
135
|
-
- Minimise bundle size concerns
|
136
|
-
- Manage your dependencies just using bundler & rubygems
|
137
|
-
- Make use of other pre-built Quince components via rubygems
|
121
|
+
- Focus on your core business logic, not routes/APIs/data transfer/code sharing ๐งช
|
122
|
+
- No compilation/node_modules/yarn/js bundle size concerns - just bundler ๐ฆ
|
138
123
|
- Get full use of Ruby's rich and comprehensive standard library ๐
|
139
124
|
- Take advantage of Ruby's ability to wrap native libraries (eg. gems using C) โก๏ธ
|
140
|
-
- Fully server-rendered responses ๐ก
|
141
|
-
|
142
|
-
|
143
|
-
- Know exactly what your user is seeing
|
144
|
-
- Tracking a user's activity on the front end has become a big deal, especially in heavily front-end driven apps/SPAs, in order to be able to see how a user is actually using the app (ie. to track how the state has been changing on the front end)
|
145
|
-
- This normally requires cookies and a premium third party service
|
146
|
-
- But if everything a user sees is generated server-side, it would be easy to reconstruct a user's journey and their state changes
|
125
|
+
- Fully server-rendered responses - single source of truth ๐ก
|
126
|
+
- Easy to recreate/rehydrate a pages state (almost nothing is stored in memory from JavaScript - all
|
127
|
+
the state is stored with the HTML document's markup)
|
147
128
|
|
148
129
|
## Installation
|
149
130
|
|
150
|
-
|
151
|
-
|
152
|
-
### Install it via adapters
|
153
|
-
|
154
|
-
- [Sinatra](https://github.com/johansenja/quince_sinatra)
|
155
|
-
|
156
|
-
Pick one, and add it to your application's Gemfile, eg:
|
131
|
+
Add this to your application's Gemfile:
|
157
132
|
|
158
133
|
```ruby
|
159
|
-
gem '
|
134
|
+
gem 'quince'
|
160
135
|
```
|
161
136
|
|
162
137
|
And then execute:
|
@@ -165,7 +140,7 @@ And then execute:
|
|
165
140
|
|
166
141
|
Or install it yourself as:
|
167
142
|
|
168
|
-
$ gem install
|
143
|
+
$ gem install quince
|
169
144
|
|
170
145
|
|
171
146
|
## Usage notes
|
@@ -4,19 +4,19 @@ 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 = {
|
18
18
|
"A" => {
|
19
|
-
|
19
|
+
download: opt_string_sym,
|
20
20
|
href: opt_string_sym,
|
21
21
|
hreflang: opt_string_sym,
|
22
22
|
media: opt_string_sym,
|
@@ -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,
|
@@ -337,7 +337,7 @@ module Quince
|
|
337
337
|
"Area" => {
|
338
338
|
alt: opt_string_sym,
|
339
339
|
coords: opt_string_sym,
|
340
|
-
|
340
|
+
download: opt_string_sym,
|
341
341
|
href: opt_string_sym,
|
342
342
|
hreflang: opt_string_sym,
|
343
343
|
media: 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,
|
@@ -479,5 +479,3 @@ module Quince
|
|
479
479
|
}.freeze
|
480
480
|
end
|
481
481
|
end
|
482
|
-
|
483
|
-
Undefined = Quince::Types::Undefined
|
data/lib/quince/callback.js.erb
CHANGED
@@ -17,7 +17,8 @@ params: Q.f(this),
|
|
17
17
|
rerender: <%= rerender.to_json %>,
|
18
18
|
<% end %>}),
|
19
19
|
`<%= rerender&.dig(:selector)&.to_s || selector %>`,
|
20
|
-
|
20
|
+
`<%= (mode = rerender&.dig(:mode)) ? mode.to_s : "replace" %>`,
|
21
|
+
<%= value.handle_errors.to_json %>,
|
21
22
|
);
|
22
23
|
<% unless push_params_state == "null" %>Q.ps(<%= push_params_state %>);<% end %>
|
23
24
|
<% if value.debounce_ms&.positive? %>
|
data/lib/quince/callback.rb
CHANGED
@@ -11,6 +11,8 @@ module Quince
|
|
11
11
|
debugger: false,
|
12
12
|
rerender: nil,
|
13
13
|
push_params_state: nil,
|
14
|
+
handle_errors: true,
|
15
|
+
download: false,
|
14
16
|
}.freeze
|
15
17
|
|
16
18
|
def callback(method_name, **opts)
|
@@ -28,7 +30,7 @@ module Quince
|
|
28
30
|
attr_reader(
|
29
31
|
:receiver,
|
30
32
|
:method_name,
|
31
|
-
|
33
|
+
*ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
|
32
34
|
)
|
33
35
|
end
|
34
36
|
|
data/lib/quince/component.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "forwardable"
|
1
2
|
require_relative "callback"
|
2
3
|
|
3
4
|
module Quince
|
@@ -9,7 +10,6 @@ module Quince
|
|
9
10
|
|
10
11
|
def Props(**kw)
|
11
12
|
self.const_set "Props", TypedStruct.new(
|
12
|
-
{ default: Quince::Types::Undefined },
|
13
13
|
Quince::Component::PARENT_SELECTOR_ATTR => String,
|
14
14
|
Quince::Component::SELF_SELECTOR => String,
|
15
15
|
**kw,
|
@@ -17,10 +17,7 @@ module Quince
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def State(**kw)
|
20
|
-
st = kw.empty? ? nil : TypedStruct.new(
|
21
|
-
{ default: Quince::Types::Undefined },
|
22
|
-
**kw,
|
23
|
-
)
|
20
|
+
st = kw.empty? ? nil : TypedStruct.new(**kw)
|
24
21
|
self.const_set "State", st
|
25
22
|
end
|
26
23
|
|
@@ -28,18 +25,20 @@ module Quince
|
|
28
25
|
@exposed_actions ||= Set.new
|
29
26
|
@exposed_actions.add action
|
30
27
|
route = "/api/#{self.name}/#{action}"
|
31
|
-
Quince.
|
28
|
+
Quince::SinatraApp.create_route_handler(
|
32
29
|
verb: method,
|
33
30
|
route: route,
|
34
|
-
) do |
|
31
|
+
) do |bind|
|
32
|
+
Thread.current[:request_binding] = bind
|
33
|
+
params = bind.receiver.params
|
34
|
+
Thread.current[:params] = params[:params] || {}
|
35
35
|
instance = Quince::Serialiser.deserialise(CGI.unescapeHTML(params[:component]))
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
36
|
+
if params[:rerender]
|
37
|
+
instance.instance_variable_set :@state_container, params[:stateContainer]
|
38
|
+
render_with = params[:rerender][:method].to_sym
|
39
|
+
else
|
40
|
+
render_with = :render
|
41
|
+
end
|
43
42
|
instance.instance_variable_set :@render_with, render_with
|
44
43
|
instance.instance_variable_set :@callback_event, params[:event]
|
45
44
|
if @exposed_actions.member? action
|
@@ -68,16 +67,18 @@ module Quince
|
|
68
67
|
|
69
68
|
def initialize_props(const, id, **props)
|
70
69
|
if const.const_defined?("Props")
|
71
|
-
const::Props.new(
|
70
|
+
const::Props.new(
|
71
|
+
PARENT_SELECTOR_ATTR => id,
|
72
|
+
**props,
|
73
|
+
SELF_SELECTOR => id,
|
74
|
+
)
|
72
75
|
end
|
73
76
|
end
|
74
77
|
end
|
75
78
|
|
79
|
+
extend Forwardable
|
76
80
|
include Callback::ComponentHelpers
|
77
81
|
|
78
|
-
# set default
|
79
|
-
@@params = {}
|
80
|
-
|
81
82
|
attr_reader :props, :state, :children
|
82
83
|
|
83
84
|
def render
|
@@ -91,9 +92,15 @@ module Quince
|
|
91
92
|
end
|
92
93
|
|
93
94
|
def params
|
94
|
-
|
95
|
+
Thread.current[:params]
|
96
|
+
end
|
97
|
+
|
98
|
+
def request_context
|
99
|
+
Thread.current[:request_binding].receiver
|
95
100
|
end
|
96
101
|
|
102
|
+
def_delegators :request_context, :attachment, :request, :response, :redirect, :halt
|
103
|
+
|
97
104
|
private
|
98
105
|
|
99
106
|
attr_reader :__id
|
@@ -55,7 +55,7 @@ module Quince
|
|
55
55
|
return %Q{#{key}="#{CGI.escape_html(code)}" data-qu-#{key}-state="#{CGI.escapeHTML(internal)}"}
|
56
56
|
when true
|
57
57
|
return key
|
58
|
-
when false, nil
|
58
|
+
when false, nil
|
59
59
|
return nil
|
60
60
|
else
|
61
61
|
raise "prop type not yet implemented #{value}"
|
@@ -92,14 +92,35 @@ module Quince
|
|
92
92
|
contents
|
93
93
|
}
|
94
94
|
end
|
95
|
-
end
|
96
95
|
|
97
|
-
|
98
|
-
|
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
|
+
}
|
99
110
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
104
123
|
end
|
124
|
+
|
125
|
+
Quince::Component.include HtmlTagComponents
|
105
126
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ENV["RACK_ENV"] ||= "development"
|
4
|
+
|
5
|
+
require "sinatra/base"
|
6
|
+
require "sinatra/reloader" if ENV["RACK_ENV"] == "development"
|
7
|
+
require "rack/contrib"
|
8
|
+
|
9
|
+
module Quince
|
10
|
+
class SinatraApp < Sinatra::Base
|
11
|
+
configure :development do
|
12
|
+
if Object.const_defined? "Sinatra::Reloader"
|
13
|
+
register Sinatra::Reloader
|
14
|
+
dont_reload __FILE__
|
15
|
+
also_reload $0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
use Rack::JSONBodyParser
|
19
|
+
use Rack::Deflater
|
20
|
+
set :public_folder, File.join(File.dirname(File.expand_path($0)), "public")
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def create_route_handler(verb:, route:, &blck)
|
24
|
+
meth = case verb
|
25
|
+
when :POST, :post
|
26
|
+
:post
|
27
|
+
when :GET, :get
|
28
|
+
:get
|
29
|
+
else
|
30
|
+
raise "invalid verb"
|
31
|
+
end
|
32
|
+
|
33
|
+
public_send meth, route do
|
34
|
+
Quince.to_html(blck.call(binding))
|
35
|
+
ensure
|
36
|
+
Thread.current[:request_binding] = nil
|
37
|
+
Thread.current[:params] = nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def expose(component, at:)
|
45
|
+
Quince::SinatraApp.get(at) do
|
46
|
+
Thread.current[:request_binding] = binding
|
47
|
+
Thread.current[:params] = binding.receiver.params
|
48
|
+
comp = component.instance_of?(Class) ? component.create : component
|
49
|
+
comp.instance_variable_set :@render_with, :render
|
50
|
+
Quince.to_html(comp)
|
51
|
+
ensure
|
52
|
+
Thread.current[:request_binding] = nil
|
53
|
+
Thread.current[:params] = nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
at_exit do
|
58
|
+
if $!.nil? || ($!.is_a?(SystemExit) && $!.success?)
|
59
|
+
if Object.const_defined? "Sinatra::Reloader"
|
60
|
+
app_dir = Pathname(File.expand_path($0)).dirname.to_s
|
61
|
+
$LOADED_FEATURES.each do |f|
|
62
|
+
next unless f.start_with? app_dir
|
63
|
+
|
64
|
+
Quince::SinatraApp.also_reload f
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
Quince::SinatraApp.run!
|
69
|
+
end
|
70
|
+
end
|
@@ -1,34 +1,29 @@
|
|
1
1
|
module Quince
|
2
2
|
class << self
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
9
|
-
|
10
|
-
def middleware=(middleware)
|
11
|
-
@middleware = middleware
|
12
|
-
Object.define_method(:expose) do |component, at:|
|
13
|
-
Quince.middleware.create_route_handler(
|
14
|
-
verb: :GET,
|
15
|
-
route: at,
|
16
|
-
) do |params|
|
17
|
-
component = component.create if component.instance_of? Class
|
18
|
-
Quince::Component.class_variable_set :@@params, params
|
19
|
-
component.instance_variable_set :@render_with, :render
|
20
|
-
component
|
21
|
-
end
|
3
|
+
def define_constructor(const, constructor_name = nil)
|
4
|
+
if const.name
|
5
|
+
parts = const.name.split("::")
|
6
|
+
parent_namespace = Object.const_get(parts[0...-1].join("::")) if parts.length > 1
|
7
|
+
constructor_name ||= parts.last
|
22
8
|
end
|
23
|
-
|
24
|
-
end
|
9
|
+
constructor_name ||= const.to_s
|
25
10
|
|
26
|
-
def define_constructor(const, constructor_name = const.to_s)
|
27
11
|
HtmlTagComponents.instance_eval do
|
28
|
-
|
29
|
-
new_props = {
|
12
|
+
mthd = lambda do |*children, **props, &block_children|
|
13
|
+
new_props = {
|
14
|
+
**props,
|
15
|
+
Quince::Component::PARENT_SELECTOR_ATTR => __id,
|
16
|
+
}
|
30
17
|
const.create(*children, **new_props, &block_children)
|
31
18
|
end
|
19
|
+
|
20
|
+
if parent_namespace
|
21
|
+
parent_namespace.instance_exec do
|
22
|
+
define_method(constructor_name, &mthd)
|
23
|
+
end
|
24
|
+
else
|
25
|
+
define_method(constructor_name, &mthd)
|
26
|
+
end
|
32
27
|
end
|
33
28
|
end
|
34
29
|
|
@@ -64,6 +59,7 @@ module Quince
|
|
64
59
|
var stateContainer = document.querySelector(`#{selector}`);
|
65
60
|
stateContainer.dataset.quOn#{event}State = #{updated_state};
|
66
61
|
JS
|
62
|
+
output = output.render if output.is_a?(Component)
|
67
63
|
|
68
64
|
output += (output.is_a?(String) ? scr : [scr])
|
69
65
|
end
|
@@ -76,3 +72,10 @@ module Quince
|
|
76
72
|
end
|
77
73
|
end
|
78
74
|
end
|
75
|
+
|
76
|
+
############## TODO #############
|
77
|
+
# I think you should be able to know when a component is the first to be called in a render method,
|
78
|
+
# so you should be able to attach some props to it behind the scenes. Then any consumers of this
|
79
|
+
# state just have to know the selector, so they can read from it before passing it to the back end.
|
80
|
+
#
|
81
|
+
# 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/lib/quince.rb
CHANGED
data/quince.gemspec
CHANGED
@@ -29,6 +29,9 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.require_paths = ["lib"]
|
30
30
|
|
31
31
|
# Uncomment to register a new dependency of your gem
|
32
|
+
spec.add_dependency "sinatra", "~> 2.1"
|
33
|
+
spec.add_dependency "sinatra-contrib", "~> 2.1"
|
34
|
+
spec.add_dependency "rack-contrib", "~> 2.3"
|
32
35
|
spec.add_dependency "typed_struct", ">= 0.1.4"
|
33
36
|
spec.add_dependency "oj", "~> 3.13"
|
34
37
|
|
data/scripts.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
const Q = {
|
2
|
-
c: (endpoint, payload, selector, mode
|
2
|
+
c: (endpoint, payload, selector, mode, handleErrors) => {
|
3
3
|
return fetch(
|
4
4
|
endpoint,
|
5
5
|
{
|
@@ -9,46 +9,78 @@ const Q = {
|
|
9
9
|
},
|
10
10
|
body: payload,
|
11
11
|
}
|
12
|
-
).then(resp =>
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
).then(resp => {
|
13
|
+
if (resp.status <= 299) {
|
14
|
+
const cd = resp.headers.get("Content-Disposition");
|
15
|
+
if (cd && cd.trim().startsWith("attachment")) {
|
16
|
+
return resp.blob();
|
17
|
+
} else {
|
18
|
+
return resp.text();
|
19
|
+
}
|
20
|
+
} else if (resp.status >= 500) {
|
21
|
+
throw Q.em["500"];
|
22
|
+
} else {
|
23
|
+
let msg = Q.em[`${resp.code}`];
|
24
|
+
|
25
|
+
if (!msg && resp.code >= 400) msg = Q.em.generic;
|
26
|
+
|
27
|
+
throw msg;
|
16
28
|
}
|
17
|
-
|
18
|
-
switch (
|
19
|
-
case
|
20
|
-
const
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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);
|
29
|
+
}).then(data => {
|
30
|
+
switch (true) {
|
31
|
+
case data instanceof Blob:
|
32
|
+
const url = URL.createObjectURL(data);
|
33
|
+
const a = document.createElement('a');
|
34
|
+
a.href = url;
|
35
|
+
a.download = "download";
|
36
|
+
document.body.appendChild(a);
|
37
|
+
a.click();
|
38
|
+
a.remove();
|
39
|
+
break;
|
40
|
+
default: // html
|
41
|
+
const element = document.querySelector(selector);
|
42
|
+
if (!element) {
|
43
|
+
throw `element not found for ${selector}`;
|
39
44
|
}
|
40
45
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
46
|
+
switch (mode) {
|
47
|
+
case "append_diff":
|
48
|
+
const tmpElem = document.createElement(element.nodeName);
|
49
|
+
tmpElem.innerHTML = data;
|
50
|
+
const newNodes = Array.from(tmpElem.childNodes);
|
51
|
+
const script = newNodes.pop();
|
52
|
+
const existingChildren = element.childNodes;
|
53
|
+
let c = existingChildren.length;
|
54
|
+
for (const node of newNodes.slice(c)) {
|
55
|
+
element.appendChild(node);
|
56
|
+
}
|
57
|
+
|
58
|
+
const newScript = document.createElement("script");
|
59
|
+
newScript.dataset.quid = script.dataset.quid;
|
60
|
+
newScript.innerHTML = script.innerHTML;
|
61
|
+
document.head.appendChild(newScript);
|
62
|
+
break;
|
63
|
+
case "replace":
|
64
|
+
element.outerHTML = data;
|
65
|
+
break;
|
66
|
+
default:
|
67
|
+
throw `mode ${mode} is not valid`;
|
68
|
+
}
|
51
69
|
}
|
70
|
+
}).catch(err => {
|
71
|
+
if (!handleErrors) throw err;
|
72
|
+
|
73
|
+
let msg;
|
74
|
+
if (typeof err === "string") {
|
75
|
+
msg = err;
|
76
|
+
} else if (err.message) {
|
77
|
+
if (err.message.startsWith("NetworkError")) {
|
78
|
+
msg = Q.em.network;
|
79
|
+
} else {
|
80
|
+
msg = err.message;
|
81
|
+
}
|
82
|
+
} else msg = Q.em.generic;
|
83
|
+
Q.e(msg, 2500);
|
52
84
|
})
|
53
85
|
},
|
54
86
|
f: (elem) => {
|
@@ -79,5 +111,29 @@ const Q = {
|
|
79
111
|
url.searchParams.append(p, stateObj[p]);
|
80
112
|
};
|
81
113
|
window.history.pushState({}, document.title, url);
|
114
|
+
},
|
115
|
+
e: (msg, durationMs) => {
|
116
|
+
const containerClassName = "quince-err-container";
|
117
|
+
document.querySelectorAll(`.${containerClassName}`).forEach(e => e.remove());
|
118
|
+
const container = document.createElement("div");
|
119
|
+
const strong = document.createElement("strong");
|
120
|
+
strong.innerText = msg;
|
121
|
+
container.className = containerClassName;
|
122
|
+
strong.className = "quince-err-msg";
|
123
|
+
container.appendChild(strong);
|
124
|
+
document.body.insertAdjacentElement("afterbegin", container);
|
125
|
+
setTimeout(() => container.remove(), durationMs);
|
126
|
+
},
|
127
|
+
em: {
|
128
|
+
400: "Bad request",
|
129
|
+
401: "Unauthorised",
|
130
|
+
402: "Payment required",
|
131
|
+
403: "Forbidden",
|
132
|
+
404: "Not found",
|
133
|
+
422: "Unprocessable entity",
|
134
|
+
429: "Too many requests",
|
135
|
+
500: "Internal server error",
|
136
|
+
generic: "An error occurred",
|
137
|
+
network: "Network error",
|
82
138
|
}
|
83
139
|
};
|
metadata
CHANGED
@@ -1,15 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quince
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.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-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sinatra
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sinatra-contrib
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rack-contrib
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.3'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.3'
|
13
55
|
- !ruby/object:Gem::Dependency
|
14
56
|
name: typed_struct
|
15
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -62,6 +104,7 @@ files:
|
|
62
104
|
- lib/quince/component.rb
|
63
105
|
- lib/quince/html_tag_components.rb
|
64
106
|
- lib/quince/serialiser.rb
|
107
|
+
- lib/quince/sinatra.rb
|
65
108
|
- lib/quince/singleton_methods.rb
|
66
109
|
- lib/quince/types.rb
|
67
110
|
- lib/quince/version.rb
|