quince 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cb965782c941f40df31a058388f1c7e30864c5a1cf708f2840340bf014a3b16
4
- data.tar.gz: 235e3aa8b1a759799f996f2d2170582f628ac32a80a69bd4fab2838d91dd3a03
3
+ metadata.gz: 1b5c0ef933d95f7026239bcfc5685e4aeb1582382056725b245cd37838540bf4
4
+ data.tar.gz: c790f0bc40602888167f46bf9636c55a739d5191645fd43e3614c78e381c6d1f
5
5
  SHA512:
6
- metadata.gz: 8de7ca420355f54e90e5dc16517f670a6009ebf0d02f6fa45bbeddf2b92fb8eb9e05f8b1e1f9eec584aa6e283b9063146b8980b4b6c2b1463b4b104f6c967865
7
- data.tar.gz: 0ac305e40cab122d9865e030572c704897a8daa5f2fa1dbb6222976834690be58efe1c4a5e1049b3f1e86846af584795ff41c2eb547490783d626544db677100
6
+ metadata.gz: d7f561f03a959ed9fb221d0462f8901fe9f0cb20bda9e80ede4757a6a5cf89c979da5f80c135054454ef64ad5ddec09751a23088fe08762084b13c097c56db3c
7
+ data.tar.gz: 47f28845aad32b4ba53f76acf4fab7695c903cb8d94197b6ac0096f16a2f048684f5677d35edeaa0c06554ae19d32152294f878e5ef5e8cbd9eccd581671389a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quince (0.1.0)
4
+ quince (0.2.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.5)
12
+ oj (3.13.6)
13
13
  rake (13.0.6)
14
14
  rbs (1.6.2)
15
15
  rspec (3.10.0)
data/README.md CHANGED
@@ -1,15 +1,162 @@
1
1
  # Quince
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/quince`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ ### What is Quince?
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ Quince is an opinionated framework for building dynamic yet fully server-rendered web apps, with little to no JavaScript.
6
+
7
+ ### Inspired by
8
+
9
+ React, Turbo, Hotwire amongst others
10
+
11
+ ### Current status
12
+
13
+ Proof of concept, but [working in production](https://quince-rb.herokuapp.com/), and with decent performance despite few optimisations at this stage
14
+
15
+ ### How it works
16
+
17
+ - Define some components and `expose` them at certain routes
18
+ - Define some interactions that can take place, which can change the state of the components, and are handled with ruby methods
19
+ - The front end will swap out the updated components with new HTML re-rendered by the back end
20
+
21
+ ## Minimal 'hello world' example
22
+
23
+ ```ruby
24
+ # app.rb
25
+ require "quince_sinatra"
26
+
27
+ class App < Quince::Component
28
+ def render
29
+ html(
30
+ head,
31
+ body("hello world")
32
+ )
33
+ end
34
+ end
35
+
36
+ expose App, at: "/"
37
+ ```
38
+
39
+ - Run it via
40
+ ```sh
41
+ ruby app.rb
42
+ ```
43
+
44
+ - Visit `localhost:4567/`!
45
+
46
+ ## More complex example
47
+
48
+ ```ruby
49
+ require 'quince_sinatra'
50
+
51
+ class App < Quince::Component
52
+ def render
53
+ Layout(title: "First app") {[
54
+ Counter()
55
+ ]}
56
+ end
57
+ end
58
+
59
+ class Layout < Quince::Component
60
+ Props(title: String)
61
+
62
+ def render
63
+ html(
64
+ head(
65
+ internal_scripts
66
+ ),
67
+ body(
68
+ h1(props.title),
69
+ children
70
+ )
71
+ )
72
+ end
73
+ end
74
+
75
+ class Counter < Quince::Component
76
+ State(val: Integer)
77
+
78
+ def initialize
79
+ @state = State.new(
80
+ val: params.fetch(:val, 0),
81
+ )
82
+ end
83
+
84
+ exposed def increment
85
+ state.val += 1
86
+ end
87
+
88
+ exposed def decrement
89
+ state.val -= 1
90
+ end
91
+
92
+ def render
93
+ div(
94
+ h2("count is #{state.val}"),
95
+ button(onclick: method(:increment)) { "++" },
96
+ button(onclick: method(:decrement)) { "--" }
97
+ )
98
+ end
99
+ end
100
+
101
+ expose App, at: "/"
102
+ ```
103
+
104
+ #### See https://github.com/johansenja/quince-demo and https://quince-rb.herokuapp.com/ for more
105
+
106
+ ## Why Quince?
107
+
108
+ ### Why not?
109
+
110
+ - You have pre-existing APIs which you want to integrate a front end with
111
+ - You want to share the back end API with a different service
112
+ - You want more offline functionality
113
+ - You need a super complex/custom front end
114
+
115
+ ### Why?
116
+
117
+ - Lightweight 🪶
118
+ - Very few dependencies
119
+ - Just a couple hundred lines of core logic
120
+ - Fewer than 30 lines (unminified) of JavaScript in the front end
121
+ - Plug and play into multiple back ends 🔌
122
+ - Components > templates 🧩
123
+ - Write html-like elements, but with strong typo resistence
124
+ - no special syntax or compilation required
125
+ - Shallow learning curve if you are already familiar with React 📈
126
+ - Just worry about your core business logic and how the app looks 🧪
127
+ - No need to worry about
128
+ - routes
129
+ - controllers
130
+ - front end -> back end communication/APIs/Data transfer
131
+ - front end -> back end code sharing
132
+ - Quince handles these for you
133
+ - No node_modules 📦
134
+ - No yarn/npm
135
+ - Minimise bundle size concerns
136
+ - Manage your dependencies just using bundler & rubygems
137
+ - Make use of other pre-built Quince components via rubygems
138
+ - Get full use of Ruby's rich and comprehensive standard library 💎
139
+ - Take advantage of Ruby's ability to wrap native libraries (eg. gems using C) ⚡️
140
+ - Fully server-rendered responses 📡
141
+ - Single source of truth for your app's code (no code-sharing needed)
142
+ - Better SEO out the box
143
+ - Know exactly what your user is seeing
144
+ - Tracking a user's activity on the front end has become a big deal, especially in heavily front-end driven apps/SPAs, in order to be able to see how a user is actually using the app (ie. to track how the state has been changing on the front end)
145
+ - This normally requires cookies and a premium third party service
146
+ - But if everything a user sees is generated server-side, it would be easy to reconstruct a user's journey and their state changes
6
147
 
7
148
  ## Installation
8
149
 
9
- Add this line to your application's Gemfile:
150
+ Quince itself is framework agnostic, so you should use an adaptor which plugs it into an existing framework for handling basic server needs
151
+
152
+ ### Install it via adapters
153
+
154
+ - [Sinatra](https://github.com/johansenja/quince_sinatra)
155
+
156
+ Pick one, and add it to your application's Gemfile, eg:
10
157
 
11
158
  ```ruby
12
- gem 'quince'
159
+ gem 'quince_sinatra'
13
160
  ```
14
161
 
15
162
  And then execute:
@@ -18,11 +165,30 @@ And then execute:
18
165
 
19
166
  Or install it yourself as:
20
167
 
21
- $ gem install quince
22
-
23
- ## Usage
24
-
25
- TODO: Write usage instructions here
168
+ $ gem install quince_sinatra
169
+
170
+
171
+ ## Usage notes
172
+
173
+ - All HTML tags are available via a method of the same name, eg. `div()`, `section()`, `span()` - **with the exception of `para` standing in for `p` to avoid clashes with Ruby's common `Kernel#p` method**
174
+ - All HTML attributes are available, and are the same as they would be in a regular html document, eg. `onclick` rather than `onClick` - **with the exception of a `Class`, `Max`, `Min`, `Method`** - which start with capital letters to avoid clashes with some internal methods.
175
+ - Type checking is available at runtime for a component's `State` and `Props`, and is done in accordance with [Typed Struct](https://github.com/johansenja/typed_struct)
176
+ - Children can be specified in one of two places, depending on what you would prefer:
177
+ - as a block argument, to maintain similar readability with real html elements, where attributes come first
178
+ ```ruby
179
+ div(id: :my_div, style: "color: red") { h1("Single child") }
180
+ div(id: "div2", style: "color: green") {[
181
+ h2("multiple"),
182
+ h3("children")
183
+ ]}
184
+ ```
185
+ - as positional arguments (for convenience and cleanliness when no props are passed)
186
+ ```ruby
187
+ div(
188
+ h1("hello world")
189
+ )
190
+ ```
191
+ - A component's `render` method should always return a single top level element, ie. if you wanted to return 2 elements you should wrap them in a `div`
26
192
 
27
193
  ## Development
28
194
 
@@ -32,7 +198,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
198
 
33
199
  ## Contributing
34
200
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/quince.
201
+ Bug reports and pull requests are welcome on GitHub at https://github.com/johansenja/quince.
36
202
 
37
203
  ## License
38
204
 
@@ -16,6 +16,7 @@ module Quince
16
16
 
17
17
  ATTRIBUTES_BY_ELEMENT = {
18
18
  "A" => {
19
+ # download: t::Any,
19
20
  href: opt_string_sym,
20
21
  hreflang: opt_string_sym,
21
22
  media: opt_string_sym,
@@ -336,7 +337,7 @@ module Quince
336
337
  "Area" => {
337
338
  alt: opt_string_sym,
338
339
  coords: opt_string_sym,
339
- download: t::Any,
340
+ # download: t::Any,
340
341
  href: opt_string_sym,
341
342
  hreflang: opt_string_sym,
342
343
  media: opt_string_sym,
@@ -469,6 +470,11 @@ module Quince
469
470
  DOM_EVENTS = {
470
471
  onclick: opt_method,
471
472
  onsubmit: opt_method,
473
+ onblur: opt_method,
474
+ onchange: opt_method,
475
+ onsearch: opt_method,
476
+ onkeyup: opt_method,
477
+ onselect: opt_method,
472
478
  }.freeze
473
479
  end
474
480
  end
@@ -30,12 +30,9 @@ module Quince
30
30
  route: route,
31
31
  ) do |params|
32
32
  instance = Quince::Serialiser.deserialise params[:component]
33
+ Quince::Component.class_variable_set :@@params, params
33
34
  if @exposed_actions.member? action
34
- if instance.method(action).arity.zero?
35
- instance.send action
36
- else
37
- instance.send action, params[:params]
38
- end
35
+ instance.send action
39
36
  instance
40
37
  else
41
38
  raise "The action you called is not exposed"
@@ -45,12 +42,16 @@ module Quince
45
42
  route
46
43
  end
47
44
 
48
- def initial_state=(attrs)
49
- @initial_state ||= self::State.new(**attrs)
45
+ def create(*children, **props, &block_children)
46
+ allocate.tap do |instance|
47
+ id = SecureRandom.alphanumeric 6
48
+ instance.instance_variable_set :@__id, id
49
+ instance.instance_variable_set :@props, initialize_props(self, id, **props)
50
+ instance.instance_variable_set(:@children, block_children || children)
51
+ instance.send :initialize
52
+ end
50
53
  end
51
54
 
52
- attr_reader :initial_state
53
-
54
55
  private
55
56
 
56
57
  def initialize_props(const, id, **props)
@@ -58,14 +59,10 @@ module Quince
58
59
  end
59
60
  end
60
61
 
61
- attr_reader :props, :state, :children
62
+ # set default
63
+ @@params = {}
62
64
 
63
- def initialize(*children, **props, &block_children)
64
- @__id = SecureRandom.alphanumeric(6)
65
- @props = self.class.send :initialize_props, self.class, @__id, **props
66
- @state = self.class.initial_state
67
- @children = block_children || children
68
- end
65
+ attr_reader :props, :state, :children
69
66
 
70
67
  def render
71
68
  raise "not implemented"
@@ -77,6 +74,10 @@ module Quince
77
74
  self.class.exposed route, meth0d: via
78
75
  end
79
76
 
77
+ def params
78
+ @@params
79
+ end
80
+
80
81
  private
81
82
 
82
83
  attr_reader :__id
@@ -45,10 +45,13 @@ module Quince
45
45
  CGI.escape_html(
46
46
  "callRemoteEndpoint(`/api/#{owner}/#{name}`,`#{payload}`,`#{selector}`)"
47
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
- )
48
+ when :onsubmit, :onchange, :onblur, :onsearch, :onkeyup, :onselect
49
+ ev = "const p = #{payload}; callRemoteEndpoint( `/api/#{owner}/#{name}`, JSON.stringify({...p, params: getFormValues(this)}), `#{selector}`)"
50
+ case key
51
+ when :onsubmit
52
+ ev += "; return false"
53
+ end
54
+ CGI.escape_html(ev)
52
55
  end
53
56
  when true
54
57
  return key
@@ -32,9 +32,7 @@ module Quince
32
32
  obj = obj.call # is there a more efficient way of doing this?
33
33
  serialise(obj)
34
34
  when String
35
- res = obj.gsub "
36
- ", "
37
- "
35
+ res = obj.gsub "\n", '\n'
38
36
  res.gsub! ?", '\"'
39
37
  res
40
38
  else
@@ -10,21 +10,23 @@ module Quince
10
10
  def middleware=(middleware)
11
11
  @middleware = middleware
12
12
  Object.define_method(:expose) do |component, at:|
13
- component = component.new if component.instance_of? Class
14
-
15
13
  Quince.middleware.create_route_handler(
16
14
  verb: :GET,
17
15
  route: at,
18
- component: component,
19
- )
16
+ ) do |params|
17
+ component = component.create if component.instance_of? Class
18
+ Quince::Component.class_variable_set :@@params, params
19
+ component
20
+ end
20
21
  end
22
+ Object.send :private, :expose
21
23
  end
22
24
 
23
25
  def define_constructor(const, constructor_name = const.to_s)
24
26
  HtmlTagComponents.instance_eval do
25
27
  define_method(constructor_name) do |*children, **props, &block_children|
26
28
  new_props = { **props, Quince::Component::HTML_SELECTOR_ATTR => __id }
27
- const.new(*children, **new_props, &block_children)
29
+ const.create(*children, **new_props, &block_children)
28
30
  end
29
31
  end
30
32
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quince
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/quince.rb CHANGED
@@ -4,3 +4,4 @@ require "cgi"
4
4
  require_relative "quince/singleton_methods"
5
5
  require_relative "quince/component"
6
6
  require_relative "quince/html_tag_components"
7
+ require_relative "quince/version"
data/scripts.js ADDED
@@ -0,0 +1,28 @@
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
+ }
16
+
17
+ element.outerHTML = html
18
+ })
19
+ }
20
+
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
+ }
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.1.0
4
+ version: 0.2.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-09 00:00:00.000000000 Z
11
+ date: 2021-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typed_struct
@@ -64,6 +64,7 @@ files:
64
64
  - lib/quince/types.rb
65
65
  - lib/quince/version.rb
66
66
  - quince.gemspec
67
+ - scripts.js
67
68
  homepage: https://github.com/johansenja/quince
68
69
  licenses:
69
70
  - MIT