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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2189200f4bee6758d6a981e3f4ba7b0d8dc558d0e0e2bb66fd421c11b404f954
4
- data.tar.gz: 4153ceb85c057cfe7c2e74852aee81b82dde74362aecec53647b485fea191b87
3
+ metadata.gz: 8a30e10c958d56fc7a73d2515d200cae1080d407e2a19e132827b2b77837b6bb
4
+ data.tar.gz: 2f43465799409bb6a7336879189d35bfcfcd259b8637a1eec546826317034436
5
5
  SHA512:
6
- metadata.gz: 923689df34d18c7357785812424acc62f38f89a3966c568bfd6e86f03c41eb1a9dd629eebdee4b1ae9321e42e1180d0be65af560613e31543496ebc312c40a30
7
- data.tar.gz: 2624224d31f066ef9562333264e6f1cc00275bf72ab35942a1b0b3097864a14c89f04a6d4ebeee7ffb07ce240f22bde8f07939b61ee64a8019017ceceead23c9
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.3.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.6)
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: method(:increment)) { "++" },
96
- button(onclick: method(:decrement)) { "--" }
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' | 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 = {
@@ -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,
@@ -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,
@@ -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 %>
@@ -6,8 +6,12 @@ module Quince
6
6
  DEFAULT_CALLBACK_OPTIONS = {
7
7
  prevent_default: false,
8
8
  take_form_values: false,
9
- # others could include:
10
- # debounce: false,
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 :receiver, :method_name, :prevent_default, :take_form_values
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(receiver, method_name, prevent_default:, take_form_values:)
38
+ def initialize(
39
+ receiver,
40
+ method_name,
41
+ **opts
42
+ )
31
43
  @receiver, @method_name = receiver, method_name
32
- @prevent_default = prevent_default
33
- @take_form_values = take_form_values
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
@@ -9,30 +9,35 @@ module Quince
9
9
 
10
10
  def Props(**kw)
11
11
  self.const_set "Props", TypedStruct.new(
12
- { default: Quince::Types::Undefined },
13
- Quince::Component::HTML_SELECTOR_ATTR => String,
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, meth0d: :POST)
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: meth0d,
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
- const::Props.new(HTML_SELECTOR_ATTR => id, **props) if const.const_defined?("Props")
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, meth0d: via
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
- HTML_SELECTOR_ATTR = :"data-quid"
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 html_element_selector
93
- "[#{HTML_SELECTOR_ATTR}='#{__id}']".freeze
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
- result << " #{attrs}>" unless attrs.empty?
21
-
22
- return result if self_closing?
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
- selector = receiver.send :html_element_selector
47
+ endpoint = "/api/#{owner}/#{name}"
48
+ selector = receiver.send :html_parent_selector
41
49
  internal = Quince::Serialiser.serialise receiver
42
- payload = { component: CGI.escapeHTML(internal) }.to_json
43
- payload_var_name = "p"
44
- stringify_payload = if value.take_form_values
45
- "{...#{payload_var_name}, params: getFormValues(this)}"
46
- else
47
- payload_var_name
48
- end
49
- cb = %Q{const #{payload_var_name} = #{payload}; callRemoteEndpoint(`/api/#{owner}/#{name}`, JSON.stringify(#{stringify_payload}),`#{selector}`)}
50
- cb += ";return false" if value.prevent_default
51
- CGI.escape_html(cb)
50
+ fn_name = "_Q_#{key}_#{receiver.send(:__id)}"
51
+ rerender = value.rerender
52
+ state_container = 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, Quince::Types::Undefined
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
- Quince::Component.include HtmlTagComponents
94
- 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
+ }
95
110
 
96
- # tmp hack
97
- class TypedStruct < Struct
98
- def to_json(*args)
99
- 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
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 = const.to_s)
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
- define_method(constructor_name) do |*children, **props, &block_children|
28
- new_props = { **props, Quince::Component::HTML_SELECTOR_ATTR => __id }
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
- else
66
+ when Component
48
67
  tmp = output
49
- output = output.render
50
- if output.is_a?(Array)
51
- raise "#render in #{tmp.class} should not return multiple elements. Consider wrapping it in a div"
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 | 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.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/scripts.js CHANGED
@@ -1,28 +1,133 @@
1
- const callRemoteEndpoint = (endpoint, payload, selector) => {
2
- fetch(
3
- endpoint,
4
- {
5
- method: `POST`,
6
- headers: {
7
- "Content-Type": `application/json;charset=utf-8`
8
- },
9
- body: payload,
10
- }
11
- ).then(resp => resp.text()).then(html => {
12
- const element = document.querySelector(selector);
13
- if (!element) {
14
- throw `element not found for ${selector}`;
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
- element.outerHTML = html
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
- const getFormValues = (elem) => {
22
- let form = elem.localName === "form" ? elem : elem.form;
23
- if (!form) {
24
- throw `element ${elem} should belong to a form`;
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
- const fd = new FormData(form);
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.3.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-09-15 00:00:00.000000000 Z
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