quince 0.2.1 → 0.4.2

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: 1b5c0ef933d95f7026239bcfc5685e4aeb1582382056725b245cd37838540bf4
4
- data.tar.gz: c790f0bc40602888167f46bf9636c55a739d5191645fd43e3614c78e381c6d1f
3
+ metadata.gz: 896b010bef61ab4914a5cf09ad07c94d2840063788c6637422dfe6544c9bda3c
4
+ data.tar.gz: e3c39708bc888257d56ae5463619dca5bc65ace9070537af7a488e5457ffd9a4
5
5
  SHA512:
6
- metadata.gz: d7f561f03a959ed9fb221d0462f8901fe9f0cb20bda9e80ede4757a6a5cf89c979da5f80c135054454ef64ad5ddec09751a23088fe08762084b13c097c56db3c
7
- data.tar.gz: 47f28845aad32b4ba53f76acf4fab7695c903cb8d94197b6ac0096f16a2f048684f5677d35edeaa0c06554ae19d32152294f878e5ef5e8cbd9eccd581671389a
6
+ metadata.gz: 7693d8919c8bc2f7af200afade98c9300131bf054a9a2614c9c3c80fa0eb90d2c153a092246bb7cf5b23661da2f57fde9038a712970ddecea45938eca4e4bcd5
7
+ data.tar.gz: da44b43b26aeeab5fe75579877e6e233e4425bf1befc12685ccdcc39aac445c617e16d2f28d571d1623afd1adc960db52a10918f39fc1922587e38c2aa736147
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quince (0.2.1)
4
+ quince (0.4.2)
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.7)
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
@@ -11,7 +11,7 @@ module Quince
11
11
  t = Quince::Types
12
12
  opt_string_sym = Rbs("#{t::OptionalString} | Symbol")
13
13
  opt_bool = t::OptionalBoolean
14
- opt_method = Rbs("Method | Quince::Types::Undefined")
14
+ opt_callback = Rbs("Quince::Callback::Interface | Quince::Types::Undefined")
15
15
  value = opt_string_sym # for now
16
16
 
17
17
  ATTRIBUTES_BY_ELEMENT = {
@@ -468,13 +468,14 @@ module Quince
468
468
  }.freeze
469
469
 
470
470
  DOM_EVENTS = {
471
- onclick: opt_method,
472
- onsubmit: opt_method,
473
- onblur: opt_method,
474
- onchange: opt_method,
475
- onsearch: opt_method,
476
- onkeyup: opt_method,
477
- onselect: opt_method,
471
+ onclick: opt_callback,
472
+ onsubmit: opt_callback,
473
+ onblur: opt_callback,
474
+ onchange: opt_callback,
475
+ onsearch: opt_callback,
476
+ onkeyup: opt_callback,
477
+ onselect: opt_callback,
478
+ onscroll: opt_callback,
478
479
  }.freeze
479
480
  end
480
481
  end
@@ -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 %>
@@ -0,0 +1,49 @@
1
+ module Quince
2
+ class Callback
3
+ module ComponentHelpers
4
+ protected
5
+
6
+ DEFAULT_CALLBACK_OPTIONS = {
7
+ prevent_default: false,
8
+ take_form_values: false,
9
+ debounce_ms: nil,
10
+ if: nil,
11
+ debugger: false,
12
+ rerender: nil,
13
+ push_params_state: nil,
14
+ handle_errors: true,
15
+ }.freeze
16
+
17
+ def callback(method_name, **opts)
18
+ unless self.class.instance_variable_get(:@exposed_actions).member?(method_name)
19
+ raise "The action you called is not exposed"
20
+ end
21
+
22
+ opts = DEFAULT_CALLBACK_OPTIONS.merge opts
23
+
24
+ Callback.new(self, method_name, **opts)
25
+ end
26
+ end
27
+
28
+ module Interface
29
+ attr_reader(
30
+ :receiver,
31
+ :method_name,
32
+ *ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
33
+ )
34
+ end
35
+
36
+ include Interface
37
+
38
+ def initialize(
39
+ receiver,
40
+ method_name,
41
+ **opts
42
+ )
43
+ @receiver, @method_name = receiver, method_name
44
+ ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.each_key do |opt|
45
+ instance_variable_set :"@#{opt}", opts.fetch(opt)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,3 +1,5 @@
1
+ require_relative "callback"
2
+
1
3
  module Quince
2
4
  class Component
3
5
  class << self
@@ -8,7 +10,8 @@ module Quince
8
10
  def Props(**kw)
9
11
  self.const_set "Props", TypedStruct.new(
10
12
  { default: Quince::Types::Undefined },
11
- Quince::Component::HTML_SELECTOR_ATTR => String,
13
+ Quince::Component::PARENT_SELECTOR_ATTR => String,
14
+ Quince::Component::SELF_SELECTOR => String,
12
15
  **kw,
13
16
  )
14
17
  end
@@ -21,16 +24,24 @@ module Quince
21
24
  self.const_set "State", st
22
25
  end
23
26
 
24
- def exposed(action, meth0d: :POST)
27
+ def exposed(action, method: :POST)
25
28
  @exposed_actions ||= Set.new
26
29
  @exposed_actions.add action
27
30
  route = "/api/#{self.name}/#{action}"
28
31
  Quince.middleware.create_route_handler(
29
- verb: meth0d,
32
+ verb: method,
30
33
  route: route,
31
34
  ) do |params|
32
- instance = Quince::Serialiser.deserialise params[:component]
35
+ instance = Quince::Serialiser.deserialise(CGI.unescapeHTML(params[:component]))
33
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
43
+ instance.instance_variable_set :@render_with, render_with
44
+ instance.instance_variable_set :@callback_event, params[:event]
34
45
  if @exposed_actions.member? action
35
46
  instance.send action
36
47
  instance
@@ -47,7 +58,8 @@ module Quince
47
58
  id = SecureRandom.alphanumeric 6
48
59
  instance.instance_variable_set :@__id, id
49
60
  instance.instance_variable_set :@props, initialize_props(self, id, **props)
50
- instance.instance_variable_set(:@children, block_children || children)
61
+ kids = block_children ? block_children.call : children
62
+ instance.instance_variable_set(:@children, kids)
51
63
  instance.send :initialize
52
64
  end
53
65
  end
@@ -55,10 +67,14 @@ module Quince
55
67
  private
56
68
 
57
69
  def initialize_props(const, id, **props)
58
- const::Props.new(HTML_SELECTOR_ATTR => id, **props) if const.const_defined?("Props")
70
+ if const.const_defined?("Props")
71
+ const::Props.new(PARENT_SELECTOR_ATTR => id, **props, SELF_SELECTOR => id)
72
+ end
59
73
  end
60
74
  end
61
75
 
76
+ include Callback::ComponentHelpers
77
+
62
78
  # set default
63
79
  @@params = {}
64
80
 
@@ -71,7 +87,7 @@ module Quince
71
87
  protected
72
88
 
73
89
  def to(route, via: :POST)
74
- self.class.exposed route, meth0d: via
90
+ self.class.exposed route, method: via
75
91
  end
76
92
 
77
93
  def params
@@ -82,10 +98,15 @@ module Quince
82
98
 
83
99
  attr_reader :__id
84
100
 
85
- HTML_SELECTOR_ATTR = :"data-respid"
101
+ PARENT_SELECTOR_ATTR = :"data-quid-parent"
102
+ SELF_SELECTOR = :"data-quid"
103
+
104
+ def html_parent_selector
105
+ "[#{PARENT_SELECTOR_ATTR}='#{__id}']".freeze
106
+ end
86
107
 
87
- def html_element_selector
88
- "[#{HTML_SELECTOR_ATTR}='#{__id}']".freeze
108
+ def html_self_selector
109
+ "[#{SELF_SELECTOR}='#{props[SELF_SELECTOR]}']".freeze
89
110
  end
90
111
  end
91
112
  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}>"
@@ -33,30 +40,23 @@ module Quince
33
40
  attrib = case value
34
41
  when String, Integer, Float, Symbol
35
42
  value.to_s
36
- when Method
37
- owner = value.owner
43
+ when Callback::Interface
38
44
  receiver = value.receiver
39
- name = value.name
40
- selector = receiver.send :html_element_selector
45
+ owner = receiver.class.name
46
+ name = value.method_name
47
+ endpoint = "/api/#{owner}/#{name}"
48
+ selector = receiver.send :html_parent_selector
41
49
  internal = Quince::Serialiser.serialise receiver
42
- payload = { component: internal }.to_json
43
- case key
44
- when :onclick
45
- CGI.escape_html(
46
- "callRemoteEndpoint(`/api/#{owner}/#{name}`,`#{payload}`,`#{selector}`)"
47
- )
48
- when :onsubmit, :onchange, :onblur, :onsearch, :onkeyup, :onselect
49
- ev = "const p = #{payload}; callRemoteEndpoint( `/api/#{owner}/#{name}`, JSON.stringify({...p, params: getFormValues(this)}), `#{selector}`)"
50
- case key
51
- when :onsubmit
52
- ev += "; return false"
53
- end
54
- CGI.escape_html(ev)
55
- end
50
+ fn_name = "_Q_#{key}_#{receiver.send(:__id)}"
51
+ rerender = value.rerender
52
+ state_container = html_self_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)}"}
56
56
  when true
57
57
  return key
58
58
  when false, nil, Quince::Types::Undefined
59
- return ""
59
+ return nil
60
60
  else
61
61
  raise "prop type not yet implemented #{value}"
62
62
  end
@@ -92,6 +92,34 @@ module Quince
92
92
  contents
93
93
  }
94
94
  end
95
+
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
+ }
110
+
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
95
123
  end
96
124
 
97
125
  Quince::Component.include HtmlTagComponents
@@ -2,98 +2,11 @@ module Quince
2
2
  class Serialiser
3
3
  class << self
4
4
  def serialise(obj)
5
- val = case obj
6
- when Quince::Component
7
- {
8
- id: serialise(obj.send(:__id)),
9
- props: serialise(obj.props),
10
- state: serialise(obj.state),
11
- children: serialise(obj.children),
12
- html_element_selector: serialise(obj.send(:html_element_selector)),
13
- }
14
- when Array
15
- obj.map { |e| serialise e }
16
- when TypedStruct, Struct, OpenStruct, Hash
17
- result = obj.each_pair.each_with_object({}) do |(k, v), ob|
18
- case v
19
- when Undefined
20
- next
21
- else
22
- ob[k] = serialise(v)
23
- end
24
- end
25
- if result.empty? && !obj.is_a?(Hash)
26
- obj = nil
27
- nil
28
- else
29
- result
30
- end
31
- when Proc
32
- obj = obj.call # is there a more efficient way of doing this?
33
- serialise(obj)
34
- when String
35
- res = obj.gsub "\n", '\n'
36
- res.gsub! ?", '\"'
37
- res
38
- else
39
- obj
40
- end
41
-
42
- { t: obj.class&.name, v: val }
5
+ Oj.dump(obj)
43
6
  end
44
7
 
45
8
  def deserialise(json)
46
- case json[:t]
47
- when "String", "Integer", "Float", "NilClass", "TrueClass", "FalseClass"
48
- json[:v]
49
- when "Symbol"
50
- json[:v].to_sym
51
- when "Array"
52
- json[:v].map { |e| deserialise e }
53
- when "Hash"
54
- transform_hash json[:v]
55
- when "OpenStruct"
56
- OpenStruct.new(**transform_hash(props))
57
- when nil
58
- nil
59
- else
60
- klass = Object.const_get(json[:t])
61
- if klass < TypedStruct
62
- transform_hash_for_struct(json[:v]) || {}
63
- elsif klass < Quince::Component
64
- instance = klass.allocate
65
- val = json[:v]
66
- id = deserialise val[:id]
67
-
68
- instance.instance_variable_set :@__id, id
69
- instance.instance_variable_set(
70
- :@props,
71
- klass.send(
72
- :initialize_props,
73
- klass,
74
- id,
75
- **(deserialise(val[:props]) || {}),
76
- ),
77
- )
78
- st = deserialise(val[:state])
79
- instance.instance_variable_set :@state, klass::State.new(**st) if st
80
- instance.instance_variable_set :@children, deserialise(val[:children])
81
- instance
82
- else
83
- klass = Object.const_get(json[:t])
84
- klass.new(deserialise(json[:v]))
85
- end
86
- end
87
- end
88
-
89
- private
90
-
91
- def transform_hash(hsh)
92
- hsh.transform_values! { |v| deserialise v }
93
- end
94
-
95
- def transform_hash_for_struct(hsh)
96
- hsh.to_h { |k, v| [k.to_sym, deserialise(v)] }
9
+ Oj.load(json)
97
10
  end
98
11
  end
99
12
  end
@@ -16,6 +16,7 @@ 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
@@ -25,7 +26,7 @@ module Quince
25
26
  def define_constructor(const, constructor_name = const.to_s)
26
27
  HtmlTagComponents.instance_eval do
27
28
  define_method(constructor_name) do |*children, **props, &block_children|
28
- new_props = { **props, Quince::Component::HTML_SELECTOR_ATTR => __id }
29
+ new_props = { **props, Quince::Component::PARENT_SELECTOR_ATTR => __id }
29
30
  const.create(*children, **new_props, &block_children)
30
31
  end
31
32
  end
@@ -44,12 +45,30 @@ module Quince
44
45
  output = to_html(output.call)
45
46
  when NilClass
46
47
  output = ""
47
- else
48
+ when Component
48
49
  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"
50
+ render_with = output.instance_variable_get(:@render_with) || :render
51
+ output = output.send render_with
52
+ case render_with
53
+ when :render
54
+ if output.is_a?(Array)
55
+ raise "#render in #{tmp.class} should not return multiple elements. Consider wrapping it in a div"
56
+ end
57
+ else
58
+ internal = Quince::Serialiser.serialise tmp
59
+ updated_state = CGI.escapeHTML(internal).to_json
60
+ selector = tmp.instance_variable_get :@state_container
61
+ event = tmp.instance_variable_get :@callback_event
62
+
63
+ scr = to_html(HtmlTagComponents::Script.create(<<~JS, type: "text/javascript"))
64
+ var stateContainer = document.querySelector(`#{selector}`);
65
+ stateContainer.dataset.quOn#{event}State = #{updated_state};
66
+ JS
67
+
68
+ output += (output.is_a?(String) ? scr : [scr])
52
69
  end
70
+ else
71
+ raise "don't know how to render #{output.class} (#{output.inspect})"
53
72
  end
54
73
  end
55
74
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quince
4
- VERSION = "0.2.1"
4
+ VERSION = "0.4.2"
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.2.1
4
+ version: 0.4.2
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-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typed_struct
@@ -57,6 +57,8 @@ files:
57
57
  - bin/setup
58
58
  - lib/quince.rb
59
59
  - lib/quince/attributes_by_element.rb
60
+ - lib/quince/callback.js.erb
61
+ - lib/quince/callback.rb
60
62
  - lib/quince/component.rb
61
63
  - lib/quince/html_tag_components.rb
62
64
  - lib/quince/serialiser.rb