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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7821a6a97b508f9ad109b4ae7e59d9ccd774b94b2e451a9809eed7fa941e5cba
4
- data.tar.gz: 5d5b6137d4089497408776c24152239fb99ac4977d5c31778cfa9bfa21518ee4
3
+ metadata.gz: f42655bc57603a51bd5231388dc7be387a51a8e912549f6a504fbaeb0e1dbb3e
4
+ data.tar.gz: ed87a57062ce7343b08409fd8126034a7dbf46e094b1741829fc63fdb9ab1f7d
5
5
  SHA512:
6
- metadata.gz: cb40eef0228b05bb3a7a05b7c32ec1b3bf47cd261d25a164f7df018e2dc94a5e720565ca7d2fd58edbb39899a481cbf7aacc50f01c97cd73bd815ca7dc95938b
7
- data.tar.gz: a6fb31c6d7835f380670ee3be74952786653bf3a3b5f7712c0ac4dd9526b8e2f9564c127d71fe16c12cebbdccf0cd4df9cbcd8a3d243c327323572baa7ebde4a
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.1)
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
- oj (3.13.7)
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
- Proof of concept, but [working in production](https://quince-rb.herokuapp.com/), and with decent performance despite few optimisations at this stage
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 "quince_sinatra"
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 'quince_sinatra'
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
- - Write html-like elements, but with strong typo resistence
124
- - no special syntax or compilation required
119
+ - Lightweight ๐Ÿชถ
125
120
  - Shallow learning curve if you are already familiar with React ๐Ÿ“ˆ
126
- - Just worry about your core business logic and how the app looks ๐Ÿงช
127
- - No need to worry about
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
- - Single source of truth for your app's code (no code-sharing needed)
142
- - Better SEO out the box
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
- Quince itself is framework agnostic, so you should use an adaptor which plugs it into an existing framework for handling basic server needs
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 'quince_sinatra'
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 quince_sinatra
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' | Quince::Types::Undefined")
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 | Quince::Types::Undefined'
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 | Quince::Types::Undefined")
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
- # download: t::Any,
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' | Quince::Types::Undefined"),
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" | Quince::Types::Undefined'),
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' | Quince::Types::Undefined"),
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" | Quince::Types::Undefined'),
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" | Quince::Types::Undefined'),
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" | Quince::Types::Undefined'),
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
- # download: t::Any,
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" | "" | Quince::Types::Undefined'),
368
- decoding: Rbs('"async" | "auto" | "sync" | Quince::Types::Undefined'),
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" | Quince::Types::Undefined'),
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
@@ -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
- <% if mode = rerender&.dig(:mode) %>`<%= mode.to_s %>`<% end %>
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? %>
@@ -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
- *ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
33
+ *ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
32
34
  )
33
35
  end
34
36
 
@@ -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.middleware.create_route_handler(
28
+ Quince::SinatraApp.create_route_handler(
32
29
  verb: method,
33
30
  route: route,
34
- ) do |params|
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
- 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
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(PARENT_SELECTOR_ATTR => id, **props, SELF_SELECTOR => id)
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
- @@params
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, Quince::Types::Undefined
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
- Quince::Component.include HtmlTagComponents
98
- end
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
- # tmp hack
101
- class TypedStruct < Struct
102
- def to_json(*args)
103
- to_h.to_json(*args)
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
- attr_reader :middleware
4
- attr_accessor :underlying_app
5
-
6
- def optional_string
7
- @optional_string ||= Rbs("String?")
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
- Object.send :private, :expose
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
- define_method(constructor_name) do |*children, **props, &block_children|
29
- new_props = { **props, Quince::Component::PARENT_SELECTOR_ATTR => __id }
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 | Quince::Types::Undefined").freeze
7
- OptionalBoolean = Rbs("true | false | Quince::Types::Undefined").freeze
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quince
4
- VERSION = "0.4.1"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/quince.rb CHANGED
@@ -5,3 +5,4 @@ require_relative "quince/singleton_methods"
5
5
  require_relative "quince/component"
6
6
  require_relative "quince/html_tag_components"
7
7
  require_relative "quince/version"
8
+ require_relative "quince/sinatra"
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 = "replace") => {
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 => resp.text()).then(html => {
13
- const element = document.querySelector(selector);
14
- if (!element) {
15
- throw `element not found for ${selector}`;
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 (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);
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
- 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`;
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.1
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-09-24 00:00:00.000000000 Z
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