quince 0.3.0 → 0.4.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: '0039bd9ca9658cfebba4048cc84fdbb095d8f2e656930501528f420c4dfa3bba'
4
+ data.tar.gz: a9720e7b11db54113fd56f1e09e43c980893297bbd5d3f46b1cdd0fc8ea2b61e
5
5
  SHA512:
6
- metadata.gz: 923689df34d18c7357785812424acc62f38f89a3966c568bfd6e86f03c41eb1a9dd629eebdee4b1ae9321e42e1180d0be65af560613e31543496ebc312c40a30
7
- data.tar.gz: 2624224d31f066ef9562333264e6f1cc00275bf72ab35942a1b0b3097864a14c89f04a6d4ebeee7ffb07ce240f22bde8f07939b61ee64a8019017ceceead23c9
6
+ metadata.gz: 6117b88cd23184b2072adae7b3b99ca868197119ffac9344477e6048519c735c7daa498067a4b96a92b60d68ce8fb5e0cc5a40529734c5c01c3b49e58317002d
7
+ data.tar.gz: f7952f3eea1cee2ef4153cbe4be1a5c709d777130f7eaa3be3ec0f5fa728da3550b81713766fcea6003673d8a65dd8a92aaae4ee3b1fb599b773463a67c5f05d
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.4.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.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
@@ -475,6 +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
@@ -0,0 +1,30 @@
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
+ <% if value.debounce_ms&.positive? %>
23
+ }, <%= value.debounce_ms %>); window[`<%= fn_name %>`](p)
24
+ <% end %>
25
+ <% if value.if %>
26
+ };
27
+ <% end %>
28
+ <% if value.prevent_default %>
29
+ ;return false;
30
+ <% end %>
@@ -6,8 +6,10 @@ 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,
11
13
  }.freeze
12
14
 
13
15
  def callback(method_name, **opts)
@@ -22,15 +24,24 @@ module Quince
22
24
  end
23
25
 
24
26
  module Interface
25
- attr_reader :receiver, :method_name, :prevent_default, :take_form_values
27
+ attr_reader(
28
+ :receiver,
29
+ :method_name,
30
+ *ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
31
+ )
26
32
  end
27
33
 
28
34
  include Interface
29
35
 
30
- def initialize(receiver, method_name, prevent_default:, take_form_values:)
36
+ def initialize(
37
+ receiver,
38
+ method_name,
39
+ **opts
40
+ )
31
41
  @receiver, @method_name = receiver, method_name
32
- @prevent_default = prevent_default
33
- @take_form_values = take_form_values
42
+ ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.each_key do |opt|
43
+ instance_variable_set :"@#{opt}", opts.fetch(opt)
44
+ end
34
45
  end
35
46
  end
36
47
  end
@@ -10,7 +10,8 @@ module Quince
10
10
  def Props(**kw)
11
11
  self.const_set "Props", TypedStruct.new(
12
12
  { default: Quince::Types::Undefined },
13
- Quince::Component::HTML_SELECTOR_ATTR => String,
13
+ Quince::Component::PARENT_SELECTOR_ATTR => String,
14
+ Quince::Component::SELF_SELECTOR => String,
14
15
  **kw,
15
16
  )
16
17
  end
@@ -23,16 +24,24 @@ module Quince
23
24
  self.const_set "State", st
24
25
  end
25
26
 
26
- def exposed(action, meth0d: :POST)
27
+ def exposed(action, method: :POST)
27
28
  @exposed_actions ||= Set.new
28
29
  @exposed_actions.add action
29
30
  route = "/api/#{self.name}/#{action}"
30
31
  Quince.middleware.create_route_handler(
31
- verb: meth0d,
32
+ verb: method,
32
33
  route: route,
33
34
  ) do |params|
34
35
  instance = Quince::Serialiser.deserialise(CGI.unescapeHTML(params[:component]))
35
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]
36
45
  if @exposed_actions.member? action
37
46
  instance.send action
38
47
  instance
@@ -58,7 +67,9 @@ module Quince
58
67
  private
59
68
 
60
69
  def initialize_props(const, id, **props)
61
- 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
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,15 @@ 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
+ "[#{PARENT_SELECTOR_ATTR}='#{__id}']".freeze
106
+ end
91
107
 
92
- def html_element_selector
93
- "[#{HTML_SELECTOR_ATTR}='#{__id}']".freeze
108
+ def html_self_selector
109
+ "[#{SELF_SELECTOR}='#{props[SELF_SELECTOR]}']".freeze
94
110
  end
95
111
  end
96
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}>"
@@ -37,22 +44,18 @@ 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 = html_self_selector
53
+ code = CALLBACK_ERB_INSTANCE.result(binding)
54
+ return %Q{#{key}="#{CGI.escape_html(code)}" data-qu-#{key}-state="#{CGI.escapeHTML(internal)}"}
52
55
  when true
53
56
  return key
54
57
  when false, nil, Quince::Types::Undefined
55
- return ""
58
+ return nil
56
59
  else
57
60
  raise "prop type not yet implemented #{value}"
58
61
  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.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/scripts.js CHANGED
@@ -1,28 +1,75 @@
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 = (elem) => {
22
- let form = elem.localName === "form" ? elem : elem.form;
23
- if (!form) {
24
- throw `element ${elem} should belong to a form`;
25
- }
26
- const fd = new FormData(form);
27
- return Object.fromEntries(fd.entries());
28
- }
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
+ };
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.4.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-09-24 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