quince 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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