quince 0.3.0 → 0.5.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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/lib/quince/attributes_by_element.rb +13 -14
- data/lib/quince/callback.js.erb +32 -0
- data/lib/quince/callback.rb +19 -6
- data/lib/quince/component.rb +31 -14
- data/lib/quince/html_tag_components.rb +48 -23
- data/lib/quince/singleton_methods.rb +53 -7
- data/lib/quince/types.rb +3 -8
- data/lib/quince/version.rb +1 -1
- data/scripts.js +130 -25
- metadata +3 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 8a30e10c958d56fc7a73d2515d200cae1080d407e2a19e132827b2b77837b6bb
         | 
| 4 | 
            +
              data.tar.gz: 2f43465799409bb6a7336879189d35bfcfcd259b8637a1eec546826317034436
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 86e5e960621d4e6ecde1ce2cf07486596b00e64aeb8ddecf0c82d7c2ac9c3818ee2f367256e1a1539af86a1eee534af572cd45c94d2ed8676d5bd4d9867543f9
         | 
| 7 | 
            +
              data.tar.gz: 0360b4ed00cba7ec1604c1bd42d79a8fce65b065b409a0dcb4539816980de92a33bf1c181d8f09f7542699edac24508684d01e2ad05cd08630b5c14c5e06f433
         | 
    
        data/Gemfile.lock
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            PATH
         | 
| 2 2 | 
             
              remote: .
         | 
| 3 3 | 
             
              specs:
         | 
| 4 | 
            -
                quince (0. | 
| 4 | 
            +
                quince (0.5.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. | 
| 12 | 
            +
                oj (3.13.9)
         | 
| 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:  | 
| 96 | 
            -
                        button(onclick:  | 
| 95 | 
            +
                        button(onclick: callback(:increment)) { "++" },
         | 
| 96 | 
            +
                        button(onclick: callback(:decrement)) { "--" }
         | 
| 97 97 | 
             
                    )
         | 
| 98 98 | 
             
                end
         | 
| 99 99 | 
             
            end
         | 
| @@ -4,14 +4,14 @@ require_relative "types" | |
| 4 4 |  | 
| 5 5 | 
             
            module Quince
         | 
| 6 6 | 
             
              module HtmlTagComponents
         | 
| 7 | 
            -
                referrer_policy = Rbs("'' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url' |  | 
| 7 | 
            +
                referrer_policy = Rbs("'' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url' | nil")
         | 
| 8 8 | 
             
                form_method = Rbs(
         | 
| 9 | 
            -
                  '"get" | "post" | "GET" | "POST" | :GET | :POST | :get | :post |  | 
| 9 | 
            +
                  '"get" | "post" | "GET" | "POST" | :GET | :POST | :get | :post | nil'
         | 
| 10 10 | 
             
                )
         | 
| 11 11 | 
             
                t = Quince::Types
         | 
| 12 12 | 
             
                opt_string_sym = Rbs("#{t::OptionalString} | Symbol")
         | 
| 13 13 | 
             
                opt_bool = t::OptionalBoolean
         | 
| 14 | 
            -
                opt_callback = Rbs("Quince::Callback::Interface |  | 
| 14 | 
            +
                opt_callback = Rbs("Quince::Callback::Interface | nil")
         | 
| 15 15 | 
             
                value = opt_string_sym # for now
         | 
| 16 16 |  | 
| 17 17 | 
             
                ATTRIBUTES_BY_ELEMENT = {
         | 
| @@ -58,7 +58,7 @@ module Quince | |
| 58 58 | 
             
                    formnovalidate: opt_bool,
         | 
| 59 59 | 
             
                    formtarget: opt_string_sym,
         | 
| 60 60 | 
             
                    name: opt_string_sym,
         | 
| 61 | 
            -
                    type: Rbs("'submit' | 'reset' | 'button' |  | 
| 61 | 
            +
                    type: Rbs("'submit' | 'reset' | 'button' | nil"),
         | 
| 62 62 | 
             
                    value: value,
         | 
| 63 63 | 
             
                  }.freeze,
         | 
| 64 64 | 
             
                  "Canvas" => {
         | 
| @@ -128,7 +128,7 @@ module Quince | |
| 128 128 | 
             
                    allowfullscreen: opt_bool,
         | 
| 129 129 | 
             
                    allowtransparency: opt_bool,
         | 
| 130 130 | 
             
                    height: opt_string_sym,
         | 
| 131 | 
            -
                    loading: Rbs('"eager" | "lazy" |  | 
| 131 | 
            +
                    loading: Rbs('"eager" | "lazy" | nil'),
         | 
| 132 132 | 
             
                    name: opt_string_sym,
         | 
| 133 133 | 
             
                    referrerpolicy: referrer_policy,
         | 
| 134 134 | 
             
                    sandbox: opt_string_sym,
         | 
| @@ -192,7 +192,7 @@ module Quince | |
| 192 192 | 
             
                  "Ol" => {
         | 
| 193 193 | 
             
                    reversed: opt_bool,
         | 
| 194 194 | 
             
                    start: opt_string_sym,
         | 
| 195 | 
            -
                    type: Rbs("'1' | 'a' | 'A' | 'i' | 'I' |  | 
| 195 | 
            +
                    type: Rbs("'1' | 'a' | 'A' | 'i' | 'I' | nil"),
         | 
| 196 196 | 
             
                  }.freeze,
         | 
| 197 197 | 
             
                  "Optgroup" => {
         | 
| 198 198 | 
             
                    disabled: opt_bool,
         | 
| @@ -270,7 +270,7 @@ module Quince | |
| 270 270 | 
             
                  }.freeze,
         | 
| 271 271 | 
             
                  "Tbody" => {}.freeze,
         | 
| 272 272 | 
             
                  "Td" => {
         | 
| 273 | 
            -
                    align: Rbs('"left" | "center" | "right" | "justify" | "char" |  | 
| 273 | 
            +
                    align: Rbs('"left" | "center" | "right" | "justify" | "char" | nil'),
         | 
| 274 274 | 
             
                    colspan: opt_string_sym,
         | 
| 275 275 | 
             
                    headers: opt_string_sym,
         | 
| 276 276 | 
             
                    rowspan: opt_string_sym,
         | 
| @@ -278,7 +278,7 @@ module Quince | |
| 278 278 | 
             
                    abbr: opt_string_sym,
         | 
| 279 279 | 
             
                    height: opt_string_sym,
         | 
| 280 280 | 
             
                    width: opt_string_sym,
         | 
| 281 | 
            -
                    valign: Rbs('"top" | "middle" | "bottom" | "baseline" |  | 
| 281 | 
            +
                    valign: Rbs('"top" | "middle" | "bottom" | "baseline" | nil'),
         | 
| 282 282 | 
             
                  }.freeze,
         | 
| 283 283 | 
             
                  "Textarea" => {
         | 
| 284 284 | 
             
                    autocomplete: opt_string_sym,
         | 
| @@ -300,7 +300,7 @@ module Quince | |
| 300 300 | 
             
                  }.freeze,
         | 
| 301 301 | 
             
                  "Tfoot" => {}.freeze,
         | 
| 302 302 | 
             
                  "Th" => {
         | 
| 303 | 
            -
                    align: Rbs('"left" | "center" | "right" | "justify" | "char" |  | 
| 303 | 
            +
                    align: Rbs('"left" | "center" | "right" | "justify" | "char" | nil'),
         | 
| 304 304 | 
             
                    colspan: opt_string_sym,
         | 
| 305 305 | 
             
                    headers: opt_string_sym,
         | 
| 306 306 | 
             
                    rowspan: opt_string_sym,
         | 
| @@ -364,10 +364,10 @@ module Quince | |
| 364 364 | 
             
                  "Hr" => {}.freeze,
         | 
| 365 365 | 
             
                  "Img" => {
         | 
| 366 366 | 
             
                    alt: opt_string_sym,
         | 
| 367 | 
            -
                    crossorigin: Rbs('"anonymous" | "use-credentials" | "" |  | 
| 368 | 
            -
                    decoding: Rbs('"async" | "auto" | "sync" |  | 
| 367 | 
            +
                    crossorigin: Rbs('"anonymous" | "use-credentials" | "" | nil'),
         | 
| 368 | 
            +
                    decoding: Rbs('"async" | "auto" | "sync" | nil'),
         | 
| 369 369 | 
             
                    height: Rbs("#{opt_string_sym} | Integer"),
         | 
| 370 | 
            -
                    loading: Rbs('"eager" | "lazy" |  | 
| 370 | 
            +
                    loading: Rbs('"eager" | "lazy" | nil'),
         | 
| 371 371 | 
             
                    referrerpolicy: referrer_policy,
         | 
| 372 372 | 
             
                    sizes: opt_string_sym,
         | 
| 373 373 | 
             
                    src: opt_string_sym,
         | 
| @@ -475,8 +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
         | 
| 481 | 
            -
             | 
| 482 | 
            -
            Undefined = Quince::Types::Undefined
         | 
| @@ -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 %>
         | 
    
        data/lib/quince/callback.rb
    CHANGED
    
    | @@ -6,8 +6,12 @@ module Quince | |
| 6 6 | 
             
                  DEFAULT_CALLBACK_OPTIONS = {
         | 
| 7 7 | 
             
                    prevent_default: false,
         | 
| 8 8 | 
             
                    take_form_values: false,
         | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 9 | 
            +
                    debounce_ms: nil,
         | 
| 10 | 
            +
                    if: nil,
         | 
| 11 | 
            +
                    debugger: false,
         | 
| 12 | 
            +
                    rerender: nil,
         | 
| 13 | 
            +
                    push_params_state: nil,
         | 
| 14 | 
            +
                    handle_errors: true,
         | 
| 11 15 | 
             
                  }.freeze
         | 
| 12 16 |  | 
| 13 17 | 
             
                  def callback(method_name, **opts)
         | 
| @@ -22,15 +26,24 @@ module Quince | |
| 22 26 | 
             
                end
         | 
| 23 27 |  | 
| 24 28 | 
             
                module Interface
         | 
| 25 | 
            -
                  attr_reader | 
| 29 | 
            +
                  attr_reader(
         | 
| 30 | 
            +
                    :receiver,
         | 
| 31 | 
            +
                    :method_name,
         | 
| 32 | 
            +
                    *ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
         | 
| 33 | 
            +
                  )
         | 
| 26 34 | 
             
                end
         | 
| 27 35 |  | 
| 28 36 | 
             
                include Interface
         | 
| 29 37 |  | 
| 30 | 
            -
                def initialize( | 
| 38 | 
            +
                def initialize(
         | 
| 39 | 
            +
                  receiver,
         | 
| 40 | 
            +
                  method_name,
         | 
| 41 | 
            +
                  **opts
         | 
| 42 | 
            +
                )
         | 
| 31 43 | 
             
                  @receiver, @method_name = receiver, method_name
         | 
| 32 | 
            -
                   | 
| 33 | 
            -
             | 
| 44 | 
            +
                  ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.each_key do |opt|
         | 
| 45 | 
            +
                    instance_variable_set :"@#{opt}", opts.fetch(opt)
         | 
| 46 | 
            +
                  end
         | 
| 34 47 | 
             
                end
         | 
| 35 48 | 
             
              end
         | 
| 36 49 | 
             
            end
         | 
    
        data/lib/quince/component.rb
    CHANGED
    
    | @@ -9,30 +9,35 @@ module Quince | |
| 9 9 |  | 
| 10 10 | 
             
                  def Props(**kw)
         | 
| 11 11 | 
             
                    self.const_set "Props", TypedStruct.new(
         | 
| 12 | 
            -
                       | 
| 13 | 
            -
                      Quince::Component:: | 
| 12 | 
            +
                      Quince::Component::PARENT_SELECTOR_ATTR => String,
         | 
| 13 | 
            +
                      Quince::Component::SELF_SELECTOR => String,
         | 
| 14 14 | 
             
                      **kw,
         | 
| 15 15 | 
             
                    )
         | 
| 16 16 | 
             
                  end
         | 
| 17 17 |  | 
| 18 18 | 
             
                  def State(**kw)
         | 
| 19 | 
            -
                    st = kw.empty? ? nil : TypedStruct.new(
         | 
| 20 | 
            -
                      { default: Quince::Types::Undefined },
         | 
| 21 | 
            -
                      **kw,
         | 
| 22 | 
            -
                    )
         | 
| 19 | 
            +
                    st = kw.empty? ? nil : TypedStruct.new(**kw)
         | 
| 23 20 | 
             
                    self.const_set "State", st
         | 
| 24 21 | 
             
                  end
         | 
| 25 22 |  | 
| 26 | 
            -
                  def exposed(action,  | 
| 23 | 
            +
                  def exposed(action, method: :POST)
         | 
| 27 24 | 
             
                    @exposed_actions ||= Set.new
         | 
| 28 25 | 
             
                    @exposed_actions.add action
         | 
| 29 26 | 
             
                    route = "/api/#{self.name}/#{action}"
         | 
| 30 27 | 
             
                    Quince.middleware.create_route_handler(
         | 
| 31 | 
            -
                      verb:  | 
| 28 | 
            +
                      verb: method,
         | 
| 32 29 | 
             
                      route: route,
         | 
| 33 30 | 
             
                    ) do |params|
         | 
| 34 31 | 
             
                      instance = Quince::Serialiser.deserialise(CGI.unescapeHTML(params[:component]))
         | 
| 35 | 
            -
                      Quince::Component.class_variable_set :@@params, params
         | 
| 32 | 
            +
                      Quince::Component.class_variable_set :@@params, params[:params]
         | 
| 33 | 
            +
                      render_with = if params[:rerender]
         | 
| 34 | 
            +
                          instance.instance_variable_set :@state_container, params[:stateContainer]
         | 
| 35 | 
            +
                          params[:rerender][:method].to_sym
         | 
| 36 | 
            +
                        else
         | 
| 37 | 
            +
                          :render
         | 
| 38 | 
            +
                        end
         | 
| 39 | 
            +
                      instance.instance_variable_set :@render_with, render_with
         | 
| 40 | 
            +
                      instance.instance_variable_set :@callback_event, params[:event]
         | 
| 36 41 | 
             
                      if @exposed_actions.member? action
         | 
| 37 42 | 
             
                        instance.send action
         | 
| 38 43 | 
             
                        instance
         | 
| @@ -58,7 +63,13 @@ module Quince | |
| 58 63 | 
             
                  private
         | 
| 59 64 |  | 
| 60 65 | 
             
                  def initialize_props(const, id, **props)
         | 
| 61 | 
            -
                     | 
| 66 | 
            +
                    if const.const_defined?("Props")
         | 
| 67 | 
            +
                      const::Props.new(
         | 
| 68 | 
            +
                        PARENT_SELECTOR_ATTR => id,
         | 
| 69 | 
            +
                        **props,
         | 
| 70 | 
            +
                        SELF_SELECTOR => id
         | 
| 71 | 
            +
                      )
         | 
| 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,  | 
| 90 | 
            +
                  self.class.exposed route, method: via
         | 
| 80 91 | 
             
                end
         | 
| 81 92 |  | 
| 82 93 | 
             
                def params
         | 
| @@ -87,10 +98,16 @@ module Quince | |
| 87 98 |  | 
| 88 99 | 
             
                attr_reader :__id
         | 
| 89 100 |  | 
| 90 | 
            -
                 | 
| 101 | 
            +
                PARENT_SELECTOR_ATTR = :"data-quid-parent"
         | 
| 102 | 
            +
                SELF_SELECTOR = :"data-quid"
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                def html_parent_selector
         | 
| 105 | 
            +
                  id = props ? props[SELF_SELECTOR] : __id
         | 
| 106 | 
            +
                  "[#{PARENT_SELECTOR_ATTR}='#{id}']".freeze
         | 
| 107 | 
            +
                end
         | 
| 91 108 |  | 
| 92 | 
            -
                def  | 
| 93 | 
            -
                  "[#{ | 
| 109 | 
            +
                def html_self_selector
         | 
| 110 | 
            +
                  "[#{SELF_SELECTOR}='#{__id}']".freeze
         | 
| 94 111 | 
             
                end
         | 
| 95 112 | 
             
              end
         | 
| 96 113 | 
             
            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 | 
            -
                       | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 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,19 @@ module Quince | |
| 37 44 | 
             
                          receiver = value.receiver
         | 
| 38 45 | 
             
                          owner = receiver.class.name
         | 
| 39 46 | 
             
                          name = value.method_name
         | 
| 40 | 
            -
                           | 
| 47 | 
            +
                          endpoint = "/api/#{owner}/#{name}"
         | 
| 48 | 
            +
                          selector = receiver.send :html_parent_selector
         | 
| 41 49 | 
             
                          internal = Quince::Serialiser.serialise receiver
         | 
| 42 | 
            -
                           | 
| 43 | 
            -
                           | 
| 44 | 
            -
                           | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 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 = 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)}"}
         | 
| 52 56 | 
             
                        when true
         | 
| 53 57 | 
             
                          return key
         | 
| 54 | 
            -
                        when false, nil | 
| 55 | 
            -
                          return  | 
| 58 | 
            +
                        when false, nil
         | 
| 59 | 
            +
                          return nil
         | 
| 56 60 | 
             
                        else
         | 
| 57 61 | 
             
                          raise "prop type not yet implemented #{value}"
         | 
| 58 62 | 
             
                        end
         | 
| @@ -88,14 +92,35 @@ module Quince | |
| 88 92 | 
             
                    contents
         | 
| 89 93 | 
             
                  }
         | 
| 90 94 | 
             
                end
         | 
| 91 | 
            -
              end
         | 
| 92 95 |  | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 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 | 
            +
                  }
         | 
| 95 110 |  | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 99 | 
            -
             | 
| 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
         | 
| 100 123 | 
             
              end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
              Quince::Component.include HtmlTagComponents
         | 
| 101 126 | 
             
            end
         | 
| @@ -16,18 +16,37 @@ 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
         | 
| 22 23 | 
             
                  Object.send :private, :expose
         | 
| 23 24 | 
             
                end
         | 
| 24 25 |  | 
| 25 | 
            -
                def define_constructor(const, constructor_name =  | 
| 26 | 
            +
                def define_constructor(const, constructor_name = nil)
         | 
| 27 | 
            +
                  if const.name
         | 
| 28 | 
            +
                    parts = const.name.split("::")
         | 
| 29 | 
            +
                    parent_namespace = Object.const_get(parts[0...-1].join("::")) if parts.length > 1
         | 
| 30 | 
            +
                    constructor_name ||= parts.last
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                  constructor_name ||= const.to_s
         | 
| 33 | 
            +
             | 
| 26 34 | 
             
                  HtmlTagComponents.instance_eval do
         | 
| 27 | 
            -
                     | 
| 28 | 
            -
                      new_props = { | 
| 35 | 
            +
                    mthd = lambda do |*children, **props, &block_children|
         | 
| 36 | 
            +
                      new_props = {
         | 
| 37 | 
            +
                        **props,
         | 
| 38 | 
            +
                        Quince::Component::PARENT_SELECTOR_ATTR => __id,
         | 
| 39 | 
            +
                      }
         | 
| 29 40 | 
             
                      const.create(*children, **new_props, &block_children)
         | 
| 30 41 | 
             
                    end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    if parent_namespace
         | 
| 44 | 
            +
                      parent_namespace.instance_exec do
         | 
| 45 | 
            +
                        define_method(constructor_name, &mthd)
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
                    else
         | 
| 48 | 
            +
                      define_method(constructor_name, &mthd)
         | 
| 49 | 
            +
                    end
         | 
| 31 50 | 
             
                  end
         | 
| 32 51 | 
             
                end
         | 
| 33 52 |  | 
| @@ -44,12 +63,32 @@ module Quince | |
| 44 63 | 
             
                      output = to_html(output.call)
         | 
| 45 64 | 
             
                    when NilClass
         | 
| 46 65 | 
             
                      output = ""
         | 
| 47 | 
            -
                     | 
| 66 | 
            +
                    when Component
         | 
| 48 67 | 
             
                      tmp = output
         | 
| 49 | 
            -
                       | 
| 50 | 
            -
                       | 
| 51 | 
            -
             | 
| 68 | 
            +
                      render_with = output.instance_variable_get(:@render_with) || :render
         | 
| 69 | 
            +
                      output = output.send render_with
         | 
| 70 | 
            +
                      case render_with
         | 
| 71 | 
            +
                      when :render
         | 
| 72 | 
            +
                        if output.is_a?(Array)
         | 
| 73 | 
            +
                          raise "#render in #{tmp.class} should not return multiple elements. Consider wrapping it in a div"
         | 
| 74 | 
            +
                        end
         | 
| 75 | 
            +
                      else
         | 
| 76 | 
            +
                        internal = Quince::Serialiser.serialise tmp
         | 
| 77 | 
            +
                        updated_state = CGI.escapeHTML(internal).to_json
         | 
| 78 | 
            +
                        selector = tmp.instance_variable_get :@state_container
         | 
| 79 | 
            +
                        event = tmp.instance_variable_get :@callback_event
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                        scr = to_html(HtmlTagComponents::Script.create(<<~JS, type: "text/javascript"))
         | 
| 82 | 
            +
                          var stateContainer = document.querySelector(`#{selector}`);
         | 
| 83 | 
            +
                          console.log('yes');
         | 
| 84 | 
            +
                          stateContainer.dataset.quOn#{event}State = #{updated_state};
         | 
| 85 | 
            +
                        JS
         | 
| 86 | 
            +
                        output = output.render if output.is_a?(Component)
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                        output += (output.is_a?(String) ? scr : [scr])
         | 
| 52 89 | 
             
                      end
         | 
| 90 | 
            +
                    else
         | 
| 91 | 
            +
                      raise "don't know how to render #{output.class} (#{output.inspect})"
         | 
| 53 92 | 
             
                    end
         | 
| 54 93 | 
             
                  end
         | 
| 55 94 |  | 
| @@ -57,3 +96,10 @@ module Quince | |
| 57 96 | 
             
                end
         | 
| 58 97 | 
             
              end
         | 
| 59 98 | 
             
            end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
            ############## TODO #############
         | 
| 101 | 
            +
            # I think you should be able to know when a component is the first to be called in a render method,
         | 
| 102 | 
            +
            # so you should be able to attach some props to it behind the scenes. Then any consumers of this
         | 
| 103 | 
            +
            # state just have to know the selector, so they can read from it before passing it to the back end.
         | 
| 104 | 
            +
            #
         | 
| 105 | 
            +
            # Also, the front end needs to be updated such that script tags from the back end are always read
         | 
    
        data/lib/quince/types.rb
    CHANGED
    
    | @@ -1,13 +1,8 @@ | |
| 1 1 | 
             
            module Quince
         | 
| 2 2 | 
             
              module Types
         | 
| 3 | 
            -
                class Base; end
         | 
| 4 | 
            -
             | 
| 5 3 | 
             
                # precompiled helper types
         | 
| 6 | 
            -
                OptionalString = Rbs("String | 
| 7 | 
            -
                OptionalBoolean = Rbs("true | false |  | 
| 8 | 
            -
             | 
| 9 | 
            -
                # no functional value for now, other than constants
         | 
| 10 | 
            -
                Undefined = Class.new(Base).new.freeze
         | 
| 11 | 
            -
                Any = Class.new(Base).new.freeze
         | 
| 4 | 
            +
                OptionalString = Rbs("String?").freeze
         | 
| 5 | 
            +
                OptionalBoolean = Rbs("true | false | nil").freeze
         | 
| 6 | 
            +
                Any = Rbs("untyped").freeze
         | 
| 12 7 | 
             
              end
         | 
| 13 8 | 
             
            end
         | 
    
        data/lib/quince/version.rb
    CHANGED
    
    
    
        data/scripts.js
    CHANGED
    
    | @@ -1,28 +1,133 @@ | |
| 1 | 
            -
            const  | 
| 2 | 
            -
               | 
| 3 | 
            -
                 | 
| 4 | 
            -
             | 
| 5 | 
            -
                   | 
| 6 | 
            -
             | 
| 7 | 
            -
                     | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
                 | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 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 | 
            -
             | 
| 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 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 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 | 
            -
             | 
| 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. | 
| 4 | 
            +
              version: 0.5.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- | 
| 11 | 
            +
            date: 2021-10-15 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
         |