props_template 0.16.0 → 0.21.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abee2de45e7fc076a1edad8c9d3edd9f13b3335e0946d6d227fcc4e5370a9e60
4
- data.tar.gz: 20bb87744d490b3cae705a1514a965b07ec04cfdc99c5f7deafb627c00fb7484
3
+ metadata.gz: 23e014144b689057bd6e6cc96a007b17263a2eab2a84890dd822056c2a4e258d
4
+ data.tar.gz: bba7578e983913d2eeeb15fc12026bb66b25c6da5a8df52c4d2003e8615a2eba
5
5
  SHA512:
6
- metadata.gz: 84b19f30e105c344ea37f0db00524804f998ba01ab84e0cd2f5c47e7c9324061096fd9b089df310a9cfc7a25980b72b01cb2c397649d3ce6ba24e9ca1fc7fd94
7
- data.tar.gz: 07257d4609fb4ebbce6621dd804f87abe1b28ab336a41ea1d092611d973d3ea905134b5ad0d690ed9496c62758318825034b1f99b3668b09b3aa7046f49fc9c3
6
+ metadata.gz: d1adee8973b67e46c8c2169a7664ec92c0ee52e21451108b449888b8a8dd89221b46d2918da2ace75e016530a7654ce03a513f4f761cb2f6fa938dd76d4a18ed
7
+ data.tar.gz: 2bc87b68c92e21f4755bf4a41c8ea1290bbfcd94f570fa2cf98b86fc2eecaa9ae83eae0f58042c3ae18cfd9c9f0d0c54caa23b5cff4c92814dfd81b3e306baa0
data/README.md CHANGED
@@ -1,15 +1,29 @@
1
1
  # PropsTemplate
2
2
 
3
- PropsTemplate is a direct-to-Oj, JBuilder-like DSL for building JSON. It has support for Russian-Doll caching, layouts, and of course, its most unique feature: your templates are queryable.
3
+ PropsTemplate is a direct-to-Oj, JBuilder-like DSL for building JSON. It has
4
+ support for Russian-Doll caching, layouts, and can be queried by giving the
5
+ root a key path.
4
6
 
5
- PropsTemplate is fast!
7
+ [![Build
8
+ Status](https://circleci.com/gh/thoughtbot/props_template.svg?style=shield)](https://circleci.com/gh/thoughtbot/props_template)
6
9
 
7
- Most libraries would build a hash before feeding it to your serializer of choice, typically Oj. PropsTemplate writes directly to Oj using `Oj::StringWriter` as its rendering your template and skips the need for an intermediate data structure.
10
+ It's fast.
8
11
 
9
- PropsTemplate also improves caching. While other libraries spend time unmarshaling, merging, and then serializing to JSON; PropsTemplate simply takes the cached string and [push_json](http://www.ohler.com/oj/doc/Oj/StringWriter.html#push_json-instance_method).
12
+ PropsTemplate bypasses the steps of hash building and serializing
13
+ that other libraries perform by using Oj's `StringWriter` in `rails` mode.
10
14
 
15
+ ![benchmarks](docs/benchmarks.png)
11
16
 
12
- Example:
17
+ Caching is fast too.
18
+
19
+ While other libraries spend time unmarshaling,
20
+ merging hashes, and serializing to JSON; PropsTemplate simply takes
21
+ the cached string and uses Oj's [push_json](http://www.ohler.com/oj/doc/Oj/StringWriter.html#push_json-instance_method).
22
+
23
+ ## Example:
24
+
25
+ PropsTemplate is very similar to JBuilder, and selectively retains some
26
+ conveniences and magic.
13
27
 
14
28
  ```ruby
15
29
  json.flash flash.to_h
@@ -47,34 +61,50 @@ json.posts do
47
61
  json.total @posts.count
48
62
  end
49
63
 
50
-
51
64
  json.footer partial: 'shared/footer' do
52
65
  end
53
66
  ```
54
67
 
55
68
  ## Installation
56
- If you plan to use PropsTemplate alone just add it to your Gemfile.
57
69
 
58
70
  ```
59
71
  gem 'props_template'
60
72
  ```
61
73
 
62
- and run `bundle`
74
+ and run `bundle`.
75
+
76
+ Add the [core ext](#array-core-extension) to an initializer.
77
+
78
+ ```ruby
79
+ require 'props_template/core_ext'
80
+ ```
81
+
82
+
83
+ And create a file in your `app/views` folder like so:
84
+
85
+ ```ruby
86
+ # app/views/posts/index.json.props
87
+
88
+ json.greetings "hello world"
89
+ ```
90
+
91
+ You can also add a [layout](#layouts).
63
92
 
64
93
  ## API
65
94
 
66
- ### json.set! or json.<your key here>
67
- Defines the attribute or stucture. All keys are automatically camelized lower.
95
+ ### json.set! or json.\<your key here\>
96
+
97
+ Defines the attribute or structure. All keys are automatically camelized lower.
68
98
 
69
99
  ```ruby
70
- json.set! :author_details, {..options...} do
100
+ json.set! :author_details, {...options} do
71
101
  json.set! :first_name, 'David'
72
102
  end
73
103
 
74
104
  or
75
105
 
76
- json.author_details, {..options...} do
77
- json.first_name, 'David'
106
+ json.author_details, {...options} do
107
+ json.first_name 'David'
78
108
  end
79
109
 
80
110
 
@@ -89,6 +119,7 @@ The inline form defines key and value
89
119
  | value | A value |
90
120
 
91
121
  ```ruby
122
+
92
123
  json.set! :first_name, 'David'
93
124
 
94
125
  or
@@ -108,39 +139,36 @@ The block form defines key and structure
108
139
 
109
140
  ```ruby
110
141
  json.set! :details do
111
- ...
142
+ ...
112
143
  end
113
144
 
114
145
  or
115
146
 
116
147
  json.details do
117
- ...
148
+ ...
118
149
  end
119
150
  ```
120
151
 
121
152
  The difference between the block form and inline form is
122
- 1. The block form is an internal node. Partials, Deferement and other [options](#options) are only available on the block form.
123
- 2. The inline form is considered a leaf node, and you can only [search](#traversing) for internal nodes.
153
+ 1. The block form is an internal node. Functionality such as Partials,
154
+ Deferment and other [options](#options) are only available on the
155
+ block form.
156
+ 2. The inline form is considered a leaf node, and you can only [search](#traversing)
157
+ for internal nodes.
124
158
 
125
159
  ### json.array!
126
160
  Generates an array of json objects.
127
161
 
128
162
  ```ruby
129
- collection = [
130
- {name: 'john'},
131
- {name: 'jim'}
132
- ]
163
+ collection = [ {name: 'john'}, {name: 'jim'} ]
133
164
 
134
165
  json.details do
135
- json.array! collection, {....options...} do |person|
166
+ json.array! collection, {...options} do |person|
136
167
  json.first_name person[:name]
137
168
  end
138
169
  end
139
170
 
140
- # => {"details": [
141
- {"firstName": 'john'},
142
- {"firstName": 'jim'}
143
- ]}
171
+ # => {"details": [{"firstName": 'john'}, {"firstName": 'jim'} ]}
144
172
  ```
145
173
 
146
174
  | Parameter | Notes |
@@ -148,7 +176,8 @@ end
148
176
  | collection | A collection that responds to `member_at` and `member_by` |
149
177
  | options | Additional [options](#options)|
150
178
 
151
- To support [traversing nodes](react-redux.md#traversing-nodes), any list passed to `array!` MUST implement `member_at(index)` and `member_by(attr, value)`.
179
+ To support [traversing nodes](#traversing), any list passed
180
+ to `array!` MUST implement `member_at(index)` and `member_by(attr, value)`.
152
181
 
153
182
  For example, if you were using a delegate:
154
183
 
@@ -169,7 +198,10 @@ end
169
198
  Then in your template:
170
199
 
171
200
  ```ruby
172
- data = ObjectCollection.new([{id: 1, name: 'foo'}, {id: 2, name: 'bar'}])
201
+ data = ObjectCollection.new([
202
+ {id: 1, name: 'foo'},
203
+ {id: 2, name: 'bar'}
204
+ ])
173
205
 
174
206
  json.array! data do
175
207
  ...
@@ -200,11 +232,15 @@ end
200
232
 
201
233
  #### **Array core extension**
202
234
 
203
- For convenience, PropsTemplate includes a core\_ext that adds these methods to `Array`. For example:
235
+ For convenience, PropsTemplate includes a core\_ext that adds these methods to
236
+ `Array`. For example:
204
237
 
205
238
  ```ruby
206
239
  require 'props_template/core_ext'
207
- data = [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}]
240
+ data = [
241
+ {id: 1, name: 'foo'},
242
+ {id: 2, name: 'bar'}
243
+ ]
208
244
 
209
245
  json.posts
210
246
  json.array! data do
@@ -213,38 +249,39 @@ json.posts
213
249
  end
214
250
  ```
215
251
 
216
- PropsTemplate does not know what the elements are in your collection. The example above will be fine for [traversing](props-template.md#traversing_nodes) by index `\posts?bzq=posts.0`, but will raise a `NotImplementedError` if you traverse by attribute `/posts?bzq=posts.id=1`. You may still need a delegate that implements `member_by`.
252
+ PropsTemplate does not know what the elements are in your collection. The
253
+ example above will be fine for [traversing](#traversing)
254
+ by index, but will raise a `NotImplementedError` if you query by attribute. You
255
+ may still need to implement `member_by`.
217
256
 
218
257
  ### json.deferred!
219
- Returns all deferred nodes used by the [#deferment](#deferment) option.
258
+ Returns all deferred nodes used by the [deferment](#deferment) option.
220
259
 
221
- ```ruby
222
- json.deferred json.deferred!
223
- ```
260
+ **Note** This is a [BreezyJS][1] specific functionality and is used in
261
+ `application.json.props` when first running `rails breezy:install:web`
224
262
 
225
- This method is normally used in `application.json.props` when first generated by `rails breezy:install:web`
226
-
227
- ### json.fragments!
228
- Returns all fragment nodes used by the [partial fragments](#partial-fragments) option.
229
263
 
230
264
  ```ruby
231
- json.fragments json.fragments!
232
- ```
265
+ json.deferred json.deferred!
233
266
 
234
- This method is normally used in `application.json.props` when first generated by `rails breezy:install:web`
267
+ # => [{url: '/some_url?props_at=outer.inner', path: 'outer.inner', type: 'auto'}]
268
+ ```
235
269
 
236
- ### json.fragment_digest!
270
+ This method provides metadata about deferred nodes to the frontend ([BreezyJS][1])
271
+ to fetch missing data in a second round trip.
237
272
 
238
- Returns the digest of the current partial name and the locals passed. Useful for [optimistic updates](#optimistic-updates).
273
+ ### json.fragments!
274
+ Returns all fragment nodes used by the [partial fragments](#partial-fragments)
275
+ option.
239
276
 
240
- ```ruby
241
- # _some_partial.json.props
277
+ ```ruby json.fragments json.fragments! ```
242
278
 
243
- json.digest json.fragment_digest!
244
- ```
279
+ **Note** This is a [BreezyJS][1] specific functionality and is used in
280
+ `application.json.props` when first running `rails breezy:install:web`
245
281
 
246
282
  ## Options
247
- Functionality such as Partials, Deferements, and Caching can only be set on a block. It is normal to see empty blocks.
283
+ Options Functionality such as Partials, Deferements, and Caching can only be
284
+ set on a block. It is normal to see empty blocks.
248
285
 
249
286
  ```ruby
250
287
  json.post(partial: 'blog_post') do
@@ -253,7 +290,9 @@ end
253
290
 
254
291
  ### Partials
255
292
 
256
- Partials are supported. The following will render the file `views/posts/_blog_posts.json.props`, and set a local variable `foo` assigned with @post, which you can use inside the partial.
293
+ Partials are supported. The following will render the file
294
+ `views/posts/_blog_posts.json.props`, and set a local variable `foo` assigned
295
+ with @post, which you can use inside the partial.
257
296
 
258
297
  ```ruby
259
298
  json.one_post partial: ["posts/blog_post", locals: {post: @post}] do
@@ -263,7 +302,8 @@ end
263
302
  Usage with arrays:
264
303
 
265
304
  ```ruby
266
- # as an option on an array. The `as:` option is supported when using `array!`
305
+ # The `as:` option is supported when using `array!`
306
+
267
307
  json.posts do
268
308
  json.array! @posts, partial: ["posts/blog_post", locals: {foo: 'bar'}, as: 'post'] do
269
309
  end
@@ -271,14 +311,14 @@ end
271
311
  ```
272
312
 
273
313
  ### Partial Fragments
314
+ **Note** This is a [BreezyJS][1] specific functionality.
274
315
 
275
- A fragment uses a digest to identify a rendered partial across your page state in Redux. When BreezyJS recieves a payload with a fragment, it will update every fragment with the same digest in your Redux store.
276
-
277
- You would need use partials and add the option `fragment: true`.
316
+ A fragment identifies a partial output across multiple pages. It can be used to
317
+ update cross cutting concerns like a header bar.
278
318
 
279
319
  ```ruby
280
320
  # index.json.props
281
- json.header partial: ["profile", fragment: true] do
321
+ json.header partial: ["profile", fragment: "header"] do
282
322
  end
283
323
 
284
324
  # _profile.json.props
@@ -292,57 +332,16 @@ end
292
332
  When using fragments with Arrays, the argument **MUST** be a lamda:
293
333
 
294
334
  ```ruby
295
- require 'props_template/core_ext' #See (lists)[#Lists]
296
-
297
- json.array! ['foo', 'bar'], partial: ["footer", fragment: ->(x){ x == 'foo'}]
298
- ```
299
-
300
- PropsTemplate creates a name for the partial using a digest of your locals, partial name, and globalId (to_json as fallback if there is no globalId) on objects that you pass. You may override this behavior and use a custom identifier:
335
+ require 'props_template/core_ext'
301
336
 
302
- ```ruby
303
- # index.js.breezy
304
- json.header partial: ["profile", fragment: 'me_header'] do
337
+ json.array! ['foo', 'bar'], partial: ["footer", fragment: ->(x){ x == 'foo'}] do
305
338
  end
306
339
  ```
307
340
 
308
- #### Optimisitc Updates
309
- Breezy uses the digest generated by `fragment: true` to uniquely identify a partial across the redux state. If you need to optimistically update a fragment, use `json.fragment_digest!` to obtain the identifier in your partial, and `updateFragments` from BreezyJS.
310
-
311
- For example:
312
-
313
- ```ruby
314
- # _header.js.props
315
- json.fragment_digest json.fragment_digest!
316
- ```
317
-
318
- And in your reducer
319
-
320
- ```javacript
321
- import {updateFragments} from 'jho406/Breezy';
322
-
323
- switch(action.type) {
324
- case SOME_ACTION: {
325
- const {
326
- fragmentDigest,
327
- prevNode // <- the content of the _header.js.props
328
- } = action.payload
329
-
330
- const nextNode = {
331
- ...prevNode,
332
- foo: 'bar'
333
- }
334
-
335
- return updateFragments(state, {
336
- [fragmentDigest]: nextNode
337
- })
338
- }
339
- default:
340
- return state
341
- }
342
- ```
343
-
344
341
  ### Caching
345
- Caching is supported on any node.
342
+ Caching is supported on internal nodes only. This limitation is what makes it
343
+ possible to for props_template to forgo marshalling/unmarshalling and simply
344
+ use [push_json](http://www.ohler.com/oj/doc/Oj/StringWriter.html#push_json-instance_method).
346
345
 
347
346
  Usage:
348
347
 
@@ -368,44 +367,60 @@ end
368
367
  When used with arrays, PropsTemplate will use `Rails.cache.read_multi`.
369
368
 
370
369
  ```ruby
371
- require 'props_template/core_ext' #See (lists)[#Lists]
370
+ require 'props_template/core_ext'
371
+
372
+ opts = { cache: ->(i){ ['a', i] } }
372
373
 
373
- opts = {
374
- cache: ->(i){ ['a', i] }
375
- }
376
374
  json.array! [4,5], opts do |x|
377
375
  json.top "hello" + x.to_s
378
376
  end
379
377
 
380
378
  #or on arrays with partials
381
379
 
382
- opts = {
383
- cache: (->(d){ ['a', d.id] }),
384
- partial: ["blog_post", as: :blog_post]
385
- }
386
- json.array! @options, opts
380
+ opts = { cache: (->(d){ ['a', d.id] }), partial: ["blog_post", as: :blog_post] }
381
+
382
+ json.array! @options, opts do
383
+ end
387
384
  ```
388
385
 
389
386
  ### Deferment
390
387
 
391
- You can defer rendering of expensive nodes in your content tree using the `defer: :auto` option. Behind the scenes PropsTemplates will no-op the block entirely, replace the value with `{}` as a placeholder.
392
- When the client recieves the payload, BreezyJS will use the meta data to issue a `remote` dispatch to fetch the missing node and immutibly graft it at the appropriate keypath in your Redux store.
388
+ You can defer rendering of expensive nodes in your content tree using the
389
+ `defer: :manual` option. Behind the scenes PropsTemplates will no-op the block
390
+ entirely and replace the value with a placeholder. A common use case would be
391
+ tabbed content that does not load until you click the tab.
392
+
393
+ When your client receives the payload, you may issue a second request to the
394
+ same endpoint to fetch any missing nodes. See [traversing nodes](#traversing)
393
395
 
394
- You can access what was deferred with `json.deferred!`. If you use the generators, this will be set up in `application.json.props`.
396
+ There is also an `defer: :auto` option that you can use with [BreezyJS][1]. [BreezyJS][1]
397
+ will use the metadata from `json.deferred!` to issue a `remote` dispatch to fetch
398
+ the missing node and immutably graft it at the appropriate keypath in your Redux
399
+ store.
395
400
 
396
401
  Usage:
397
402
 
398
403
  ```ruby
399
- json.dashboard(defer: :auto) do
404
+ json.dashboard(defer: :manual) do
405
+ sleep 10
406
+ json.some_fancy_metric 42
407
+ end
408
+
409
+
410
+ # or you can explicitly pass a placeholder
411
+
412
+ json.dashboard(defer: [:manual, placeholder: {}]) do
400
413
  sleep 10
401
414
  json.some_fancy_metric 42
402
415
  end
403
416
  ```
404
417
 
405
- A manual option is also available:
418
+ A auto option is available:
419
+
420
+ **Note** This is a [BreezyJS][1] specific functionality.
406
421
 
407
422
  ```ruby
408
- json.dashboard(defer: :manual) do
423
+ json.dashboard(defer: :auto) do
409
424
  sleep 10
410
425
  json.some_fancy_metric 42
411
426
  end
@@ -417,39 +432,51 @@ Finally in your `application.json.props`:
417
432
  json.defers json.deferred!
418
433
  ```
419
434
 
420
-
421
- If `:manual` is used, PropsTemplate will no-op the block and will not populate `json.deferred!`. Its up to you to [traverse](props-template.md#traversing_nodes) to fetch the node seperately. A common usecase would be tab content that does not load until you click the tab.
422
-
423
435
  #### Working with arrays
424
- The default behavior for deferements is to use the index of the collection to identify an element. PropsTemplate will generate `?_bzq=a.b.c.0.title` in its metadata.
436
+ The default behavior for deferements is to use the index of the collection to
437
+ identify an element.
438
+
439
+ **Note** If you are using this library with [BreezyJS][1], the `:auto` options will
440
+ generate `?props_at=a.b.c.0.title` for `json.deferred!`.
425
441
 
426
442
  If you wish to use an attribute to identify the element. You must:
427
- 1. Implement `:key` to specify which attribute you want to use to uniquely identify the element in the collection. PropsTemplate will generate `?_bzq=a.b.c.some_id=some_value.title`
428
- 2. Implement `member_at`, and `member_key` on the collection to allow for BreezyJS to traverse the tree based on key value attributes.
443
+
444
+ 1. Use the `:key` option on `json.array!`. This key refers to an attribute on
445
+ your collection item, and is used for `defer: :auto` to generate a keypath for
446
+ [BreezyJS][1]. If you are NOT using BreezyJS, you do not need to do this.
447
+
448
+ 2. Implement `member_at`, on the [collection](#jsonarray). This will be called
449
+ by PropsTemplate to when [searching nodes](#traversing)
429
450
 
430
451
  For example:
431
452
 
432
453
  ```ruby
433
- require 'props_template/core_ext' #See (lists)[#Lists]
434
-
435
- data = [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}]
454
+ require 'props_template/core_ext'
455
+ data = [
456
+ {id: 1, name: 'foo'},
457
+ {id: 2, name: 'bar'}
458
+ ]
436
459
 
437
460
  json.posts
438
461
  json.array! data, key: :some_id do |item|
462
+ # By using :key, props_template will append `json.some_id item.some_id`
463
+ # automatically
464
+
439
465
  json.contact(defer: :auto) do
440
466
  json.address '123 example drive'
441
467
  end
442
-
443
- # json.some_id item.some_id will be appended automatically to the end of the block
444
468
  end
445
469
  end
446
470
  ```
447
471
 
448
- When BreezyJS receives the response, it will automatically kick off `remote(?bzq=posts.some_id=1.contact)` and `remote(?bzq=posts.some_id=2.contact)`.
472
+ If you are using [BreezyJS][1], BreezyJS will, it will automatically kick off
473
+ `remote(?props_at=posts.some_id=1.contact)` and `remote(?props_at=posts.some_id=2.contact)`.
449
474
 
450
- # Traversing
475
+ ## Traversing
451
476
 
452
- PropsTemplate has the ability to walk the tree you build, skipping execution of untargeted nodes. This feature is useful for partial updating your frontend state. See [traversing nodes](react-redux.md#traversing-nodes)
477
+ PropsTemplate has the ability to walk the tree you build, skipping execution of
478
+ untargeted nodes. This feature is useful for selectively updating your frontend
479
+ state.
453
480
 
454
481
  ```ruby
455
482
  traversal_path = ['data', 'details', 'personal']
@@ -457,7 +484,7 @@ traversal_path = ['data', 'details', 'personal']
457
484
  json.data(search: traversal_path) do
458
485
  json.details do
459
486
  json.employment do
460
- ...more stuff...
487
+ ...more stuff
461
488
  end
462
489
 
463
490
  json.personal do
@@ -468,25 +495,28 @@ json.data(search: traversal_path) do
468
495
  end
469
496
 
470
497
  json.footer do
471
- ...
498
+ ...
472
499
  end
473
500
  ```
474
501
 
475
- PropsTemplate will will walk breath first, finds the matching key, executes the associated block, then repeats until it the node is found. The above will output the below:
502
+ PropsTemplate will walk depth first, walking only when it finds a matching key,
503
+ then executes the associated block, and repeats until it the node is found.
504
+ The above will output:
476
505
 
477
506
  ```json
478
507
  {
479
- data: {
480
- name: 'james',
481
- zipCode: 91210
508
+ "data": {
509
+ "name": 'james',
510
+ "zipCode": 91210
482
511
  },
483
- footer: {
484
- ....
512
+ "footer": {
513
+ ...
485
514
  }
486
515
  }
487
516
  ```
488
517
 
489
- Breezy's searching only works with blocks, and will NOT work with Scalars ("leaf" values). For example:
518
+ Searching only works with blocks, and will NOT work with Scalars
519
+ ("leaf" values). For example:
490
520
 
491
521
  ```ruby
492
522
  traversal_path = ['data', 'details', 'personal', 'name'] <- not found
@@ -498,12 +528,11 @@ json.data(search: traversal_path) do
498
528
  end
499
529
  end
500
530
  end
501
-
502
531
  ```
503
532
 
504
533
  ## Nodes that do not exist
505
534
 
506
- Nodes that are not found will not define the key where search was enabled on.
535
+ Nodes that are not found will remove the branch where search was enabled on.
507
536
 
508
537
  ```ruby
509
538
  traversal_path = ['data', 'details', 'does_not_exist']
@@ -517,17 +546,54 @@ json.data(search: traversal_path) do
517
546
  end
518
547
 
519
548
  json.footer do
520
- ...
549
+ ...
521
550
  end
522
-
523
551
  ```
524
552
 
525
553
  The above will render:
526
554
 
527
- ```
555
+ ```json
528
556
  {
529
- footer: {
557
+ "footer": {
530
558
  ...
531
559
  }
532
560
  }
533
561
  ```
562
+
563
+ ## Layouts
564
+ A single layout is supported. To use, create an `application.json.props` in
565
+ `app/views/layouts`. Here's an example:
566
+
567
+ ```ruby
568
+ json.data do
569
+ # template runs here.
570
+ yield json
571
+ end
572
+
573
+ json.header do
574
+ json.greeting "Hello"
575
+ end
576
+
577
+ json.footer do
578
+ json.greeting "Hello"
579
+ end
580
+
581
+ json.flash flash.to_h
582
+ ```
583
+
584
+ **NOTE** PropsTemplate inverts the usual Rails rendering flow. PropsTemplate
585
+ will render Layout first, then the template when `yield json` is used.
586
+
587
+ ## Contributing
588
+
589
+ See the [CONTRIBUTING] document. Thank you, [contributors]!
590
+
591
+ [CONTRIBUTING]: CONTRIBUTING.md
592
+ [contributors]: https://github.com/thoughtbot/props_template/graphs/contributors
593
+
594
+ ## Special Thanks
595
+
596
+ Thanks to [turbostreamer](https://github.com/malomalo/turbostreamer) for the
597
+ inspiration.
598
+
599
+ [1]: https://github.com/thoughtbot/breezy
@@ -28,10 +28,6 @@ module Props
28
28
  @em.fragments
29
29
  end
30
30
 
31
- def fragment_digest!
32
- @em.fragment_digest
33
- end
34
-
35
31
  def set_block_content!(options = {})
36
32
  return super if !@em.has_extensions(options)
37
33
 
@@ -7,7 +7,7 @@ module Props
7
7
  class ExtensionManager
8
8
  attr_reader :base, :builder, :context
9
9
 
10
- def initialize(base, defered=[], fragments={})
10
+ def initialize(base, defered=[], fragments=[])
11
11
  @base = base
12
12
  @context = base.context
13
13
  @builder = base.builder
@@ -36,10 +36,6 @@ module Props
36
36
  @deferment.deferred
37
37
  end
38
38
 
39
- def fragment_digest
40
- @fragment.name
41
- end
42
-
43
39
  def fragments
44
40
  @fragment.fragments
45
41
  end
@@ -59,10 +55,8 @@ module Props
59
55
  handle_cache(options) do
60
56
  base.set_block_content! do
61
57
  if options[:partial]
62
- current_digest = @fragment.name
63
58
  @fragment.handle(options)
64
59
  @partialer.handle(options)
65
- @fragment.name = current_digest
66
60
  else
67
61
  yield
68
62
  end
@@ -104,14 +98,7 @@ module Props
104
98
  next_deferred, next_fragments = Oj.load(meta)
105
99
  base.stream.push_json(raw_json)
106
100
  deferred.push(*next_deferred)
107
-
108
- next_fragments.each do |k, v|
109
- if fragments[k]
110
- fragments[k].push(*v)
111
- else
112
- fragments[k] = v
113
- end
114
- end
101
+ fragments.push(*next_fragments)
115
102
  end
116
103
  else
117
104
  yield
@@ -25,12 +25,6 @@ module Props
25
25
  @context
26
26
  end
27
27
 
28
- def instrument(name, **options)
29
- ActiveSupport::Notifications.instrument(name, options) do |payload|
30
- yield payload
31
- end
32
- end
33
-
34
28
  def multi_fetch(keys, options = {})
35
29
  result = {}
36
30
  key_to_ckey = {}
@@ -49,7 +43,7 @@ module Props
49
43
 
50
44
  read_caches = {}
51
45
 
52
- instrument('read_multi_fragments.action_view', payload) do |payload|
46
+ ActiveSupport::Notifications.instrument('read_multi_fragments.action_view', payload) do |payload|
53
47
  read_caches = ::Rails.cache.read_multi(*ckeys, options)
54
48
  payload[:read_caches] = read_caches
55
49
  end
@@ -122,7 +116,7 @@ module Props
122
116
 
123
117
  def cache_key(key, options)
124
118
  name_options = options.slice(:skip_digest, :virtual_path)
125
- key = fragment_name_with_digest(key, name_options)
119
+ key = @context.cache_fragment_name(key, **name_options)
126
120
 
127
121
  if @context.respond_to?(:combined_fragment_cache_key)
128
122
  key = @context.combined_fragment_cache_key(key)
@@ -132,19 +126,6 @@ module Props
132
126
 
133
127
  ::ActiveSupport::Cache.expand_cache_key(key, :props)
134
128
  end
135
-
136
- def fragment_name_with_digest(key, options)
137
- if @context.respond_to?(:cache_fragment_name)
138
- # Current compatibility, fragment_name_with_digest is private again and cache_fragment_name
139
- # should be used instead.
140
- @context.cache_fragment_name(key, options)
141
- elsif @context.respond_to?(:fragment_name_with_digest)
142
- # Backwards compatibility for period of time when fragment_name_with_digest was made public.
143
- @context.fragment_name_with_digest(key)
144
- else
145
- key
146
- end
147
- end
148
129
  end
149
130
  end
150
131
 
@@ -34,6 +34,8 @@ module Props
34
34
 
35
35
  type, rest = options[:defer]
36
36
  placeholder = rest[:placeholder]
37
+ success_action = rest[:success_action]
38
+ fail_action = rest[:fail_action]
37
39
 
38
40
  if type.to_sym == :auto && options[:key]
39
41
  key, val = options[:key]
@@ -45,16 +47,22 @@ module Props
45
47
  path = @base.traveled_path.join('.')
46
48
  uri = ::URI.parse(request_path)
47
49
  qry = ::URI.decode_www_form(uri.query || '')
48
- .reject{|x| x[0] == 'bzq' }
49
- .push(["bzq", path])
50
+ .reject{|x| x[0] == 'props_at' }
51
+ .push(["props_at", path])
50
52
 
51
53
  uri.query = ::URI.encode_www_form(qry)
52
54
 
53
- @deferred.push(
55
+ deferral = {
54
56
  url: uri.to_s,
55
57
  path: path,
56
- type: type.to_s
57
- )
58
+ type: type.to_s,
59
+ }
60
+
61
+ # camelize for JS land
62
+ deferral[:successAction] = success_action if success_action
63
+ deferral[:failAction] = fail_action if fail_action
64
+
65
+ @deferred.push(deferral)
58
66
 
59
67
  placeholder
60
68
  end
@@ -1,14 +1,10 @@
1
- require 'digest'
2
-
3
1
  module Props
4
2
  class Fragment
5
3
  attr_reader :fragments
6
- attr_accessor :name
7
4
 
8
- def initialize(base, fragments={})
5
+ def initialize(base, fragments=[])
9
6
  @base = base
10
7
  @fragments = fragments
11
- @digest = Digest::SHA2.new(256)
12
8
  end
13
9
 
14
10
  def handle(options)
@@ -20,30 +16,10 @@ module Props
20
16
  fragment_name = fragment.to_s
21
17
  path = @base.traveled_path.join('.')
22
18
  @name = fragment_name
23
- @fragments[fragment_name] ||= []
24
- @fragments[fragment_name].push(path)
25
- end
26
-
27
- if fragment == true
28
- locals = partial_opts[:locals]
29
19
 
30
- identity = {}
31
- locals
32
- .clone
33
- .tap{|h| h.delete(:json)}
34
- .each do |key, value|
35
- if value.respond_to?(:to_global_id)
36
- identity[key] = value.to_global_id.to_s
37
- else
38
- identity[key] = value
39
- end
40
- end
41
-
42
- path = @base.traveled_path.join('.')
43
- fragment_name = @digest.hexdigest("#{partial_name}#{identity.to_json}")
44
- @name = fragment_name
45
- @fragments[fragment_name] ||= []
46
- @fragments[fragment_name].push(path)
20
+ @fragments.push(
21
+ { type: fragment_name, partial: partial_name, path: path }
22
+ )
47
23
  end
48
24
  end
49
25
  end
@@ -1,22 +1,56 @@
1
1
  require 'action_view'
2
2
 
3
3
  module Props
4
+ class RenderedTemplate
5
+ attr_reader :body, :layout, :template
6
+
7
+ def initialize(body, layout, template)
8
+ @body = body
9
+ @layout = layout
10
+ @template = template
11
+ end
12
+
13
+ def format
14
+ template.format
15
+ end
16
+ end
17
+
4
18
  class Partialer
19
+ INVALID_PARTIAL_MESSAGE = "The partial name must be a string, but received (%s)."
20
+
5
21
  def initialize(base, context, builder)
6
22
  @context = context
7
23
  @builder = builder
8
24
  @base = base
9
25
  end
10
26
 
27
+ def extract_details(options) # :doc:
28
+ @context.lookup_context.registered_details.each_with_object({}) do |key, details|
29
+ value = options[key]
30
+
31
+ details[key] = Array(value) if value
32
+ end
33
+ end
34
+
11
35
  def find_and_add_template(all_options)
12
36
  first_opts = all_options[0]
13
37
 
14
38
  if first_opts[:partial]
15
39
  partial_opts = block_opts_to_render_opts(@builder, first_opts)
16
- renderer = PartialRenderer.new(@context, partial_opts)
40
+ .merge(formats: [:json])
41
+ partial_opts.delete(:handlers)
42
+ partial = partial_opts[:partial]
43
+
44
+ if !(String === partial)
45
+ raise ArgumentError.new(INVALID_PARTIAL_MESSAGE % (partial.inspect))
46
+ end
47
+
48
+ template_keys = retrieve_template_keys(partial_opts)
49
+ details = extract_details(partial_opts)
50
+ template = find_template(partial, template_keys, details)
17
51
 
18
52
  all_options.map do |opts|
19
- opts[:_template] = renderer.template
53
+ opts[:_template] = template
20
54
  opts
21
55
  end
22
56
  else
@@ -24,6 +58,17 @@ module Props
24
58
  end
25
59
  end
26
60
 
61
+ def find_template(path, locals, details)
62
+ prefixes = path.include?(?/) ? [] : @context.lookup_context.prefixes
63
+ @context.lookup_context.find_template(path, prefixes, true, locals, details)
64
+ end
65
+
66
+ def retrieve_template_keys(options)
67
+ template_keys = options[:locals].keys
68
+ template_keys << options[:as] if options[:as]
69
+ template_keys
70
+ end
71
+
27
72
  def block_opts_to_render_opts(builder, options)
28
73
  partial, pass_opts = [*options[:partial]]
29
74
  pass_opts ||= {}
@@ -45,9 +90,20 @@ module Props
45
90
 
46
91
  renderer.render(template, pass_opts)
47
92
  end
93
+
94
+ def render(template, options)
95
+ view = @context
96
+ instrument(:partial, identifier: template.identifier) do |payload|
97
+ locals = options[:locals]
98
+ content = template.render(view, locals)
99
+
100
+ payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
101
+ build_rendered_template(content, template)
102
+ end
103
+ end
48
104
  end
49
105
 
50
- class PartialRenderer < ActionView::AbstractRenderer
106
+ class PartialRenderer
51
107
  OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
52
108
  "make sure it starts with lowercase letter, " \
53
109
  "and is followed by any combination of letters, numbers and underscores."
@@ -56,21 +112,6 @@ module Props
56
112
 
57
113
  INVALID_PARTIAL_MESSAGE = "The partial name must be a string, but received (%s)."
58
114
 
59
- def self.find_and_add_template(builder, context, all_options)
60
- first_opts = all_options[0]
61
-
62
- if first_opts[:partial]
63
- partial_opts = block_opts_to_render_opts(builder, first_opts)
64
- renderer = new(context, partial_opts)
65
-
66
- all_options.map do |opts|
67
- opts[:_template] = renderer.template
68
- opts
69
- end
70
- else
71
- all_options
72
- end
73
- end
74
115
 
75
116
  def self.raise_invalid_option_as(as)
76
117
  raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
@@ -106,8 +147,7 @@ module Props
106
147
  locals[as] = item
107
148
 
108
149
  if fragment_name = rest[:fragment]
109
- fragment_name = Proc === fragment_name ? fragment_name.call(item) : fragment_name.to_s
110
- rest[:fragment] = fragment_name
150
+ rest[:fragment] = fragment_name.to_s
111
151
  end
112
152
  end
113
153
 
@@ -121,7 +161,6 @@ module Props
121
161
 
122
162
  def initialize(context, options)
123
163
  @context = context
124
- super(@context.lookup_context)
125
164
  @options = options.merge(formats: [:json])
126
165
  @options.delete(:handlers)
127
166
  @details = extract_details(@options)
@@ -133,7 +172,7 @@ module Props
133
172
  end
134
173
 
135
174
  @path = partial
136
- @context_prefix = @lookup_context.prefixes.first
175
+
137
176
  template_keys = retrieve_template_keys(@options)
138
177
  @template = find_template(@path, template_keys)
139
178
  end
@@ -145,6 +184,19 @@ module Props
145
184
  end
146
185
 
147
186
  private
187
+ def extract_details(options) # :doc:
188
+ @context.lookup_context.registered_details.each_with_object({}) do |key, details|
189
+ value = options[key]
190
+
191
+ details[key] = Array(value) if value
192
+ end
193
+ end
194
+
195
+ def instrument(name, **options) # :doc:
196
+ ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
197
+ yield payload
198
+ end
199
+ end
148
200
 
149
201
  def render_partial(template, view, options)
150
202
  template ||= @template
@@ -159,17 +211,13 @@ module Props
159
211
  end
160
212
  end
161
213
 
162
- # Sets up instance variables needed for rendering a partial. This method
163
- # finds the options and details and extracts them. The method also contains
164
- # logic that handles the type of object passed in as the partial.
165
- #
166
- # If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
167
- # set to that string. Otherwise, the +options[:partial]+ object must
168
- # respond to +to_partial_path+ in order to setup the path.
214
+ def build_rendered_template(content, template, layout = nil)
215
+ RenderedTemplate.new content, layout, template
216
+ end
169
217
 
170
218
  def find_template(path, locals)
171
- prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
172
- @lookup_context.find_template(path, prefixes, true, locals, @details)
219
+ prefixes = path.include?(?/) ? [] : @context.lookup_context.prefixes
220
+ @context.lookup_context.find_template(path, prefixes, true, locals, @details)
173
221
  end
174
222
 
175
223
  def retrieve_template_keys(options)
@@ -23,9 +23,6 @@ module Props
23
23
  []
24
24
  end
25
25
 
26
- def fragment_digest!
27
- end
28
-
29
26
  def found!
30
27
  pass_opts = @found_options.clone || {}
31
28
  pass_opts.delete(:defer)
@@ -0,0 +1,3 @@
1
+ module Props
2
+ VERSION = "0.21.0".freeze
3
+ end
@@ -1,6 +1,7 @@
1
1
  require 'props_template/base_with_extensions'
2
2
  require 'props_template/searcher'
3
3
  require 'props_template/handler'
4
+ require 'props_template/version'
4
5
 
5
6
  require 'active_support'
6
7
 
@@ -16,7 +17,6 @@ module Props
16
17
  :deferred!,
17
18
  :fragments!,
18
19
  :set_block_content!,
19
- :fragment_digest!,
20
20
  to: :builder!
21
21
 
22
22
  def initialize(context = nil, options = {})
data/spec/layout_spec.rb CHANGED
@@ -1,16 +1,24 @@
1
- require_relative './support/helper'
2
- require_relative './support/rails_helper'
3
- require 'props_template/layout_patch'
1
+ require_relative "./support/helper"
2
+ require_relative "./support/rails_helper"
3
+ require "props_template/layout_patch"
4
+ require "action_controller"
4
5
 
5
- RSpec.describe 'Props::Template' do
6
- it 'uses a layout to render' do
7
- view_path = File.join(File.dirname(__FILE__),'./fixtures')
8
- controller = ActionView::TestCase::TestController.new
6
+ RSpec.describe "Props::Template" do
7
+ class TestController < ActionController::Base
8
+ protect_from_forgery
9
+
10
+ def self.controller_path
11
+ ""
12
+ end
13
+ end
14
+
15
+ it "uses a layout to render" do
16
+ view_path = File.join(File.dirname(__FILE__), "./fixtures")
17
+ controller = TestController.new
9
18
  controller.prepend_view_path(view_path)
10
- controller.response.headers['Content-Type']='application/json'
11
- controller.request.path = '/some_url'
12
19
 
13
- json = controller.render('200', layout: 'application')
20
+ json = controller.render_to_string("200", layout: "application")
21
+
14
22
  expect(json.strip).to eql('{"data":{"success":"ok"}}')
15
23
  end
16
24
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: props_template
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johny Ho
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-25 00:00:00.000000000 Z
11
+ date: 2022-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,8 +52,10 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.9'
55
- description: A JSON builder for your React props
56
- email: jho406@gmail.com
55
+ description: PropsTemplate is a direct-to-Oj, JBuilder-like DSL for building JSON.
56
+ It has support for Russian-Doll caching, layouts, and can be queried by giving the
57
+ root a key path.
58
+ email: johny@thoughtbot.com
57
59
  executables: []
58
60
  extensions: []
59
61
  extra_rdoc_files: []
@@ -74,14 +76,15 @@ files:
74
76
  - lib/props_template/layout_patch.rb
75
77
  - lib/props_template/railtie.rb
76
78
  - lib/props_template/searcher.rb
79
+ - lib/props_template/version.rb
77
80
  - spec/layout_spec.rb
78
81
  - spec/props_template_spec.rb
79
82
  - spec/searcher_spec.rb
80
- homepage: https://github.com/jho406/breezy/
83
+ homepage: https://github.com/thoughtbot/props_template/
81
84
  licenses:
82
85
  - MIT
83
86
  metadata: {}
84
- post_install_message:
87
+ post_install_message:
85
88
  rdoc_options: []
86
89
  require_paths:
87
90
  - lib
@@ -89,17 +92,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
89
92
  requirements:
90
93
  - - ">="
91
94
  - !ruby/object:Gem::Version
92
- version: '2.3'
95
+ version: '2.5'
93
96
  required_rubygems_version: !ruby/object:Gem::Requirement
94
97
  requirements:
95
98
  - - ">="
96
99
  - !ruby/object:Gem::Version
97
100
  version: '0'
98
101
  requirements: []
99
- rubygems_version: 3.0.3
100
- signing_key:
102
+ rubygems_version: 3.1.4
103
+ signing_key:
101
104
  specification_version: 4
102
- summary: A JSON builder for your React props
105
+ summary: A fast JSON builder
103
106
  test_files:
104
107
  - spec/searcher_spec.rb
105
108
  - spec/layout_spec.rb