quince 0.4.1 โ†’ 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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