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