quince 0.2.0 → 0.4.1

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: dcc2582bba48ffba3d17a6bd6efe972db91c70d3680348334342e01ad8506158
4
- data.tar.gz: af1dccbf8ea37419b4eb4936ea8f4fa20e60509d5c78a460d21df08ded15698d
3
+ metadata.gz: 7821a6a97b508f9ad109b4ae7e59d9ccd774b94b2e451a9809eed7fa941e5cba
4
+ data.tar.gz: 5d5b6137d4089497408776c24152239fb99ac4977d5c31778cfa9bfa21518ee4
5
5
  SHA512:
6
- metadata.gz: 7d16ca52523b06dddb32cd22aac945690836c0b094051248394f823d56d35933a0bc163b2250e57fb6ccbcbdbe956db12865da486e16b5024148d83566059d46
7
- data.tar.gz: 8550f7c684a827e2c3b714e6e00c6e2b37bfdb2f54f1daf7dfe6f3dde46e4a17859fe0f5a887d3bcd54877db400728acb08037156a0032561f327a42a9e3910f
6
+ metadata.gz: cb40eef0228b05bb3a7a05b7c32ec1b3bf47cd261d25a164f7df018e2dc94a5e720565ca7d2fd58edbb39899a481cbf7aacc50f01c97cd73bd815ca7dc95938b
7
+ data.tar.gz: a6fb31c6d7835f380670ee3be74952786653bf3a3b5f7712c0ac4dd9526b8e2f9564c127d71fe16c12cebbdccf0cd4df9cbcd8a3d243c327323572baa7ebde4a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quince (0.2.0)
4
+ quince (0.4.1)
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,8 +468,14 @@ module Quince
468
468
  }.freeze
469
469
 
470
470
  DOM_EVENTS = {
471
- onclick: opt_method,
472
- onsubmit: 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,
473
479
  }.freeze
474
480
  end
475
481
  end
@@ -0,0 +1,31 @@
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
+ <% if mode = rerender&.dig(:mode) %>`<%= mode.to_s %>`<% end %>
21
+ );
22
+ <% unless push_params_state == "null" %>Q.ps(<%= push_params_state %>);<% end %>
23
+ <% if value.debounce_ms&.positive? %>
24
+ }, <%= value.debounce_ms %>); window[`<%= fn_name %>`](p)
25
+ <% end %>
26
+ <% if value.if %>
27
+ };
28
+ <% end %>
29
+ <% if value.prevent_default %>
30
+ ;return false;
31
+ <% end %>
@@ -0,0 +1,48 @@
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
+ }.freeze
15
+
16
+ def callback(method_name, **opts)
17
+ unless self.class.instance_variable_get(:@exposed_actions).member?(method_name)
18
+ raise "The action you called is not exposed"
19
+ end
20
+
21
+ opts = DEFAULT_CALLBACK_OPTIONS.merge opts
22
+
23
+ Callback.new(self, method_name, **opts)
24
+ end
25
+ end
26
+
27
+ module Interface
28
+ attr_reader(
29
+ :receiver,
30
+ :method_name,
31
+ *ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
32
+ )
33
+ end
34
+
35
+ include Interface
36
+
37
+ def initialize(
38
+ receiver,
39
+ method_name,
40
+ **opts
41
+ )
42
+ @receiver, @method_name = receiver, method_name
43
+ ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.each_key do |opt|
44
+ instance_variable_set :"@#{opt}", opts.fetch(opt)
45
+ end
46
+ end
47
+ end
48
+ 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,27 +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
49
- CGI.escape_html(
50
- "const p = #{payload}; callRemoteEndpoint( `/api/#{owner}/#{name}`, JSON.stringify({...p, params: getFormValues(this)}), `#{selector}`); return false"
51
- )
52
- 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)}"}
53
56
  when true
54
57
  return key
55
58
  when false, nil, Quince::Types::Undefined
56
- return ""
59
+ return nil
57
60
  else
58
61
  raise "prop type not yet implemented #{value}"
59
62
  end
@@ -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.0"
4
+ VERSION = "0.4.1"
5
5
  end
data/scripts.js CHANGED
@@ -1,24 +1,83 @@
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 = "replace") => {
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 => resp.text()).then(html => {
13
+ const element = document.querySelector(selector);
14
+ if (!element) {
15
+ throw `element not found for ${selector}`;
16
+ }
16
17
 
17
- element.outerHTML = html
18
- })
19
- }
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);
39
+ }
40
+
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`;
51
+ }
52
+ })
53
+ },
54
+ f: (elem) => {
55
+ let form = elem.localName === "form" ? elem : elem.form;
56
+ if (!form) {
57
+ throw `element ${elem} should belong to a form`;
58
+ }
59
+ const fd = new FormData(form);
60
+ return Object.fromEntries(fd.entries());
61
+ },
62
+ d: (func, wait_ms) => {
63
+ let timer = null;
20
64
 
21
- const getFormValues = (form) => {
22
- const fd = new FormData(form);
23
- return Object.fromEntries(fd.entries());
24
- }
65
+ return (...args) => {
66
+ clearTimeout(timer);
67
+ return new Promise((resolve) => {
68
+ timer = setTimeout(
69
+ () => resolve(func(...args)),
70
+ wait_ms,
71
+ );
72
+ });
73
+ };
74
+ },
75
+ ps: (stateObj) => {
76
+ const base = location.origin + location.pathname;
77
+ const url = new URL(base);
78
+ for (const p in stateObj) {
79
+ url.searchParams.append(p, stateObj[p]);
80
+ };
81
+ window.history.pushState({}, document.title, url);
82
+ }
83
+ };
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.0
4
+ version: 0.4.1
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-13 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