props_template 0.16.0 → 0.21.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: 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