quince 0.3.0 → 0.5.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: 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