rbexy 2.0.0.beta7 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/patbenatar/rbexy.svg?branch=master)](https://travis-ci.org/patbenatar/rbexy)
4
4
 
5
+ * [Getting Started](#getting-started-with-rails)
6
+ * [Template Syntax](#template-syntax)
7
+ * [Components](#components)
8
+ * [`Rbexy::Component`](#rbexycomponent)
9
+ * [Usage with any component library](#usage-with-any-component-library)
10
+ * [Fragment caching in Rails](#fragment-caching-in-rails)
11
+ * [Advanced](#advanced)
12
+ * [Component resolution](#component-resolution)
13
+ * [AST Transforms](#ast-transforms)
14
+ * [Usage outside of Rails](#usage-outside-of-rails)
15
+
16
+ ## Manifesto
17
+
5
18
  Love JSX and component-based frontends, but sick of paying the costs of SPA development? Rbexy brings the elegance of JSX—operating on HTML elements and custom components with an interchangeable syntax—to the world of Rails server-rendered apps.
6
19
 
7
20
  Combine this with CSS Modules in your Webpacker PostCSS pipeline and you'll have a first-class frontend development experience while maintaining the development efficiency of Rails.
@@ -85,78 +98,25 @@ Add a controller, action, route, and `rbx` view like `app/views/hello_worlds/ind
85
98
  </HelloWorld>
86
99
  ```
87
100
 
88
- _Or you can render Rbexy components from ERB with `<%= HelloWorldComponent.new(self, name: "Nick").render %>`_
89
-
90
101
  Fire up `rails s`, navigate to your route, and you should see Rbexy in action!
91
102
 
92
103
  ## Template Syntax
93
104
 
94
- ### Text
95
-
96
- You can put arbitrary strings anywhere.
97
-
98
- At the root:
105
+ You can use Ruby code within brackets:
99
106
 
100
107
  ```jsx
101
- Hello world
108
+ <p class={@dynamic_class}>
109
+ Hello {"world".upcase}
110
+ </p>
102
111
  ```
103
112
 
104
- Inside tags:
105
-
106
- ```jsx
107
- <p>Hello world</p>
108
- ```
109
-
110
- As attributes:
111
-
112
- ```jsx
113
- <div class="myClass"></div>
114
- ```
115
-
116
- ### Comments
117
-
118
- Start a line with `#` to leave a comment:
119
-
120
- ```jsx
121
- # Comments can be at the root
122
- <div>
123
- # Or within tags
124
- # spanning multiple lines
125
- <h1>Hello world</h1>
126
- </div>
127
- ```
128
-
129
- ### Expressions
130
-
131
- You can put ruby code anywhere that you would put text, just wrap it in `{ ... }`
132
-
133
- At the root:
134
-
135
- ```jsx
136
- {"hello world".upcase}
137
- ```
138
-
139
- Inside a sentence:
140
-
141
- ```jsx
142
- Hello {"world".upcase}
143
- ```
144
-
145
- Inside tags:
146
-
147
- ```jsx
148
- <p>{"hello world".upcase}</p>
149
- ```
150
-
151
- As attributes:
113
+ You can splat a hash into attributes:
152
114
 
153
115
  ```jsx
154
- <p class={@dynamic_class}>Hello world</p>
116
+ <div {**{class: "myClass"}} {**@more_attrs}></div>
155
117
  ```
156
118
 
157
- #### Tags within expressions
158
-
159
- To conditionalize your template:
119
+ You can use HTML or component tags within expressions. e.g. to conditionalize a template:
160
120
 
161
121
  ```jsx
162
122
  <div>
@@ -165,7 +125,7 @@ To conditionalize your template:
165
125
  </div>
166
126
  ```
167
127
 
168
- Loops:
128
+ Or in loops:
169
129
 
170
130
  ```jsx
171
131
  <ul>
@@ -181,7 +141,7 @@ Blocks:
181
141
  end}
182
142
  ```
183
143
 
184
- As an attribute:
144
+ Pass a tag to a component as an attribute:
185
145
 
186
146
  ```jsx
187
147
  <Hero title={<h1>Hello World</h1>}>
@@ -189,7 +149,7 @@ As an attribute:
189
149
  </Hero>
190
150
  ```
191
151
 
192
- Pass a lambda to a prop, that when called returns a tag:
152
+ Or pass a lambda as an attribute, that when called returns a tag:
193
153
 
194
154
  ```jsx
195
155
  <Hero title={-> { <h1>Hello World</h1> }}>
@@ -207,70 +167,15 @@ _Note that when using tags inside blocks, the block must evaluate to a single ro
207
167
  -> { <i>Hello</i> World }
208
168
  ```
209
169
 
210
- ### Tags
211
-
212
- You can put standard HTML tags anywhere.
213
-
214
- At the root:
215
-
216
- ```jsx
217
- <h1>Hello world</h1>
218
- ```
219
-
220
- As children:
221
-
222
- ```jsx
223
- <div>
224
- <h1>Hello world</h1>
225
- </div>
226
- ```
227
-
228
- As siblings with other tags:
229
-
230
- ```jsx
231
- <div>
232
- <h1>Hello world</h1>
233
- <p>Welcome to rbexy</p>
234
- </div>
235
- ```
236
-
237
- As siblings with text and expressions:
238
-
239
- ```jsx
240
- <h1>Hello world</h1>
241
- {an_expression}
242
- Some arbitrary text
243
- ```
244
-
245
- Self-closing tags:
246
-
247
- ```jsx
248
- <input type="text" />
249
- ```
250
-
251
- #### Attributes
252
-
253
- Text and expressions can be provided as attributes:
254
-
255
- ```jsx
256
- <div class="myClass" id={dynamic_id}></div>
257
- ```
258
-
259
- Value-less attributes are allowed:
260
-
261
- ```jsx
262
- <input type="submit" disabled>
263
- ```
264
-
265
- You can splat a hash into attributes:
170
+ Start a line with `#` to leave a comment:
266
171
 
267
172
  ```jsx
268
- <div {**{ class: "myClass" }} {**@more_attrs}></div>
173
+ # Private note to self that won't be rendered in the final HTML
269
174
  ```
270
175
 
271
- ## Custom components
176
+ ## Components
272
177
 
273
- You can use custom components alongside standard HTML tags:
178
+ You can use Ruby classes as components alongside standard HTML tags:
274
179
 
275
180
  ```jsx
276
181
  <div>
@@ -281,6 +186,8 @@ You can use custom components alongside standard HTML tags:
281
186
  </div>
282
187
  ```
283
188
 
189
+ By default, Rbexy will resolve `PageHeader` to a Ruby class called `PageHeaderComponent` and render it with the view context, attributes, and its children: `PageHeaderComponent.new(self, title: "Welcome").render_in(self, &block)`. This behavior is customizable, see "Component resolution" below.
190
+
284
191
  ### `Rbexy::Component`
285
192
 
286
193
  We ship with a component superclass that integrates nicely with Rails' ActionView and the controller rendering context. You can use it to easily implement custom components in your Rails app:
@@ -301,13 +208,11 @@ By default, we'll look for a template file in the same directory as the class an
301
208
  <h1>{@title}</h1>
302
209
  ```
303
210
 
304
- You can call this component from another `.rbx` template file (`<PageHeader title="Hello" />`)—either one rendered by another component class or a Rails view file like `app/views/products/index.rbx`. Or you can call it from ERB (or any other template language) like `PageHeaderComponent.new(self, title: "Hello").render_in`.
305
-
306
- Your components and their templates run in the same context as traditional Rails views, so you have access to all of the view helpers you're used to as well as any custom helpers you've defined in `app/helpers/`.
211
+ Your components and their templates run in the same context as traditional Rails views, so you have access to all of the view helpers you're used to as well as any custom helpers you've defined in `app/helpers/` or via `helper_method` in your controller.
307
212
 
308
213
  #### Template-less components
309
214
 
310
- If you'd prefer to render your components entirely from Ruby, e.g. using Rails `tag` helpers, you can do so with `#call`:
215
+ If you'd prefer to render your components entirely from Ruby, you can do so by implementing `#call`:
311
216
 
312
217
  ```ruby
313
218
  class PageHeaderComponent < Rbexy::Component
@@ -361,57 +266,115 @@ class TextFieldComponent < Rbexy::Component
361
266
  end
362
267
  ```
363
268
 
364
- ### `ViewComponent`
269
+ #### Usage with ERB
365
270
 
366
- Using Github's view_component library? Rbexy ships with a provider that'll resolve your RBX tags like `<Button />` to their corresponding `ButtonComponent < ViewComponent::Base` components.
271
+ We recommend using `Rbexy::Component` with the rbx template language, but if you prefer ERB... a component's template can be `.html.erb` and you can render a component from ERB like so:
367
272
 
368
- ```ruby
369
- require "rbexy/component_providers/view_component_provider"
273
+ Rails 6.1:
274
+
275
+ ```erb
276
+ <%= render PageHeaderComponent.new(self, title: "Welcome") do %>
277
+ <p>Children...</p>
278
+ <% end >
279
+ ```
280
+
281
+ Rails 6.0 or earlier:
282
+
283
+ ```erb
284
+ <%= PageHeaderComponent.new(self, title: "Welcome").render_in(self) %>
285
+ ```
286
+
287
+ ### Usage with any component library
370
288
 
289
+ You can use the rbx template language with other component libraries like Github's view_component. You just need to tell Rbexy how to render the component:
290
+
291
+ ```ruby
292
+ # config/initializers/rbexy.rb
371
293
  Rbexy.configure do |config|
372
- config.component_provider = Rbexy::ComponentProviders::ViewComponentProvider.new
294
+ config.component_rendering_templates = {
295
+ children: "{capture{%{children}}}",
296
+ component: "::%{component_class}.new(%{view_context},%{kwargs}).render_in%{children_block}"
297
+ }
373
298
  end
374
299
  ```
375
300
 
376
- ### Other types of components
301
+ ## Fragment caching in Rails
377
302
 
378
- You just need to tell rbexy how to resolve your custom component classes as it encounters them while evaluating your template by implementing a ComponentProvider:
303
+ `.rbx` templates integrate with Rails fragment caching, automatically cachebusting when the template or its render dependencies change.
379
304
 
380
- ```ruby
381
- class MyComponentProvider
382
- def match?(name)
383
- # Return true if the given tag name matches one of your custom components
384
- end
305
+ If you're using `Rbexy::Component`, you can further benefit from component cachebusting where the fragment cache will be busted if any dependent component's template _or_ class definition changes.
385
306
 
386
- def render(context, name, **attrs, &block)
387
- # Instantiate and render your custom component for the given name, using
388
- # the render context as needed (e.g. ActionView in Rails)
389
- end
390
- end
307
+ And you can use `<Rbexy.Cache>`, a convenient wrapper for the Rails fragment cache:
308
+
309
+ ```rbx
310
+ <Rbexy.Cache key={...}>
311
+ <p>Fragment here...</p>
312
+ <MyButton />
313
+ </Rbexy.Cache>
314
+ ```
391
315
 
392
- # Register your component provider with Rbexy
316
+ ## Advanced
317
+
318
+ ### Component resolution
319
+
320
+ By default, Rbexy resolves component tags to Ruby classes named `#{tag}Component`, e.g.:
321
+
322
+ * `<PageHeader />` => `PageHeaderComponent`
323
+ * `<Admin.Button />` => `Admin::ButtonComponent`
324
+
325
+ You can customize this behavior by providing a custom resolver:
326
+
327
+ ```ruby
328
+ # config/initializers/rbexy.rb
393
329
  Rbexy.configure do |config|
394
- config.component_provider = MyComponentProvider.new
330
+ config.element_resolver = MyResolver.new
395
331
  end
396
332
  ```
397
333
 
398
- Or in Rails you can customize the component provider just for a controller:
334
+ Where `MyResolver` implements the following API:
335
+
336
+ * `component?(name: string, template: Rbexy::Template) => Boolean`
337
+ * `component_class(name: string, template: Rbexy::Template) => T`
338
+
339
+ See `lib/rbexy/component_resolver.rb` for an example.
340
+
341
+ #### Auto-namespacing
342
+
343
+ Want to namespace your components but sick of typing `Admin.` in front of every component call? Rbexy's default `ComponentResolver` implementation has an option for that:
399
344
 
400
345
  ```ruby
401
- class ThingsController < ApplicationController
402
- def rbexy_component_provider
403
- MyComponentProvider.new
404
- end
346
+ # config/initializers/rbexy.rb
347
+ Rbexy.configure do |config|
348
+ config.element_resolver.component_namespaces = {
349
+ Rails.root.join("app", "views", "admin") => %w[Admin],
350
+ Rails.root.join("app", "components", "admin") => %w[Admin]
351
+ }
405
352
  end
406
353
  ```
407
354
 
408
- See `lib/rbexy/component_providers/` for example implementations.
355
+ Now any calls to `<Button>` made from `.rbx` views within `app/views/admin/` or from component templates within `app/components/admin/` will first check for `Admin::ButtonComponent` before `ButtonComponent`.
409
356
 
410
- ## Usage outside of Rails
357
+ ### AST Transforms
358
+
359
+ You can hook into Rbexy's compilation process to mutate the abstract syntax tree. This is both useful and dangerous, so use with caution.
360
+
361
+ An example use case is automatically scoping CSS class names if you're using something like CSS Modules. Here's an oversimplified example of this:
362
+
363
+ ```ruby
364
+ # config/initializers/rbexy.rb
365
+ Rbexy.configure do |config|
366
+ config.transforms.register(Rbexy::Nodes::HTMLAttr) do |node, context|
367
+ if node.name == "class"
368
+ class_list = node.value.split(" ")
369
+ node.value.content = scope_names(class_list, scope: context.template.identifier)
370
+ end
371
+ end
372
+ end
373
+ ```
411
374
 
412
- Rbexy compiles your template into ruby code, which you can then execute in any context you like, so long as a tag builder is available at `#rbexy_tag`. We provide a built-in runtime leveraging ActionView's `tag` helper that you can extend from or build your own:
375
+ ### Usage outside of Rails
413
376
 
414
- Subclass to add methods and instance variables that you'd like to make available to your template.
377
+ Rbexy compiles your template into ruby code, which you can then execute in any context you like. Subclass `Rbexy::Runtime` to add methods and instance variables that you'd like to make available to your template.
415
378
 
416
379
  ```ruby
417
380
  class MyRuntime < Rbexy::Runtime
@@ -428,50 +391,42 @@ end
428
391
  Rbexy.evaluate("<p class={a_method}>{@an_ivar}</p>", MyRuntime.new)
429
392
  ```
430
393
 
431
- If you're using custom components, inject a ComponentProvider (see "Custom components" for an example implementation):
432
-
433
- ```ruby
434
- class MyRuntime < Rbexy::Runtime
435
- def initialize(component_provider)
436
- super(component_provider)
437
- @ivar_val = "ivar value"
438
- end
439
-
440
- def splat_attrs
441
- {
442
- key1: "val1",
443
- key2: "val2"
444
- }
445
- end
446
- end
394
+ ## Development
447
395
 
448
- Rbexy.evaluate(
449
- "<Forms.TextField /><Button prop1=\"val1\" prop2={true && \"val2\">Submit</Button>",
450
- MyRuntime.new(MyComponentProvider.new)
451
- )
396
+ ```
397
+ docker-compose build
398
+ docker-compose run rbexy bin/test
452
399
  ```
453
400
 
454
- Or implement your own runtime, so long as it conforms to the API:
401
+ Or auto-run tests with guard if you prefer:
455
402
 
456
- * `#rbexy_tag` that returns a tag builder conforming to the API of `ActionView::Helpers::TagHelpers::TagBuilder`
457
- * `#evaluate(code)` that evals the given string of ruby code
403
+ ```
404
+ docker-compose run rbexy guard
405
+ ```
458
406
 
459
- ## Development
407
+ If you want to run against the supported versions of Rails, use
408
+ Appraisal:
460
409
 
461
410
  ```
462
- docker-compose build
463
- docker-compose run rbexy rspec
411
+ docker-compose run rbexy appraisal bin/test
464
412
  ```
465
413
 
466
- Or auto-run tests with guard if you prefer:
414
+ ## Debugging TemplatePath methods being called
415
+ When a new version of Rails is released, we need to check what methods are being
416
+ called on `Rbexy::Component::TemplatePath` to make sure we always return
417
+ a TemplatePath, not a string due to how we handle `TemplatePath`s
418
+ internally.
419
+
420
+ To list all methods being called, enable `RBEXY_TEMPLATE_PATH_DEBUG` and
421
+ run tests:
467
422
 
468
423
  ```
469
- docker-compose run rbexy guard
424
+ docker-compose run -e RBEXY_TEMPLATE_PATH_DEBUG=1 rbexy appraisal bin/test
470
425
  ```
471
426
 
472
427
  ## Contributing
473
428
 
474
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rbexy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/rbexy/blob/master/CODE_OF_CONDUCT.md).
429
+ Bug reports and pull requests are welcome on GitHub at https://github.com/patbenatar/rbexy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/patbenatar/rbexy/blob/master/CODE_OF_CONDUCT.md).
475
430
 
476
431
  ## License
477
432
 
@@ -479,4 +434,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
479
434
 
480
435
  ## Code of Conduct
481
436
 
482
- Everyone interacting in the Rbexy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/rbexy/blob/master/CODE_OF_CONDUCT.md).
437
+ Everyone interacting in the Rbexy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/patbenatar/rbexy/blob/master/CODE_OF_CONDUCT.md).
data/bin/test ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bash
2
+
3
+ declare -i RESULT=0
4
+
5
+ echo "Running main suite..."
6
+
7
+ bundle exec rspec
8
+ RESULT+=$?
9
+
10
+ echo "Running initial caching specs to test cold cache behavior..."
11
+
12
+ bundle exec rspec spec/integration/caching/before_changes_spec.rb
13
+ RESULT+=$?
14
+
15
+ echo "Making template source changes to allow testing of cache-busting..."
16
+
17
+ templates=(
18
+ "spec/dummy/app/views/caching/inline.rbx"
19
+ "spec/dummy/app/components/cached_thing_component.rbx"
20
+ "spec/dummy/app/components/cached_class_thing_component.rb"
21
+ "spec/dummy/app/components/cached_thing_call_component.rb"
22
+ "spec/dummy/app/views/caching/_partial_render_partial.rbx"
23
+ )
24
+ for i in "${templates[@]}"
25
+ do
26
+ mv $i $i.original
27
+ mv $i.changed $i
28
+ done
29
+
30
+ echo "Running subsequent caching specs to test cache-busting of warm cache..."
31
+
32
+ bundle exec rspec spec/integration/caching/after_changes_spec.rb
33
+ RESULT+=$?
34
+
35
+ echo "Cleaning up..."
36
+
37
+ for i in "${templates[@]}"
38
+ do
39
+ mv $i $i.changed
40
+ mv $i.original $i
41
+ done
42
+
43
+ exit $RESULT
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 6.1.4"
6
+ gem "net-smtp", require: false
7
+
8
+ gemspec path: "../"