quince 0.1.2 → 0.2.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: 057efcbe62deb405120d389ac7186e485961d9b5b258c5ba3934d10dc88d4998
4
- data.tar.gz: 7e14d9297e3820d8d501bfd415e5f0891bc5e92b518609c895e1de45d8388605
3
+ metadata.gz: dcc2582bba48ffba3d17a6bd6efe972db91c70d3680348334342e01ad8506158
4
+ data.tar.gz: af1dccbf8ea37419b4eb4936ea8f4fa20e60509d5c78a460d21df08ded15698d
5
5
  SHA512:
6
- metadata.gz: fc197781d667ea003854e622327175332d8ba958fcf87c2385b80f635590aa95f525ed9843c7c496aa6734b2f84d07ce31638d998160e00131a7635ad79fe0e6
7
- data.tar.gz: 19678d0fabbe803917ce453e85aed1e6622c6c5729941d0a324323f0cc0e8502bf94d5194755d85fdd3d768ab31011fb4e57c58e39a57611f442569589d7b19e
6
+ metadata.gz: 7d16ca52523b06dddb32cd22aac945690836c0b094051248394f823d56d35933a0bc163b2250e57fb6ccbcbdbe956db12865da486e16b5024148d83566059d46
7
+ data.tar.gz: 8550f7c684a827e2c3b714e6e00c6e2b37bfdb2f54f1daf7dfe6f3dde46e4a17859fe0f5a887d3bcd54877db400728acb08037156a0032561f327a42a9e3910f
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quince (0.1.2)
4
+ quince (0.2.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.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,
@@ -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
@@ -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.2"
4
+ VERSION = "0.2.0"
5
5
  end
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.2
4
+ version: 0.2.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-09 00:00:00.000000000 Z
11
+ date: 2021-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typed_struct