quince 0.2.0 → 0.4.1

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: 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