props_template 0.14.0 → 0.20.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: ad98bb076e592658c137fd423771b211760ba047a1f6f394efa7a531a25eedc5
4
- data.tar.gz: f5495c4eca8b7015ac5d18714a10a16735769128fac62b5693824b1ef0c6e505
3
+ metadata.gz: 4cf7de015844e77efdf93e02272159080835cad2e0f6022e610690727a05cfb5
4
+ data.tar.gz: 350d1a534d30c625b7adbe3c95d6ac33c2a831205c970c404bb2c9eeb22dfd69
5
5
  SHA512:
6
- metadata.gz: 629b8dc24b25dc596ee6f2bb9d03abce69e5af292121d118a83e3d53b83f38ef7ccc155e4cc79d4469a6e588c2efa3231ac02b05447ed11aa6c4642fca833d6f
7
- data.tar.gz: 83d0aed77e8cba7cf1730307f15a6fed2b972a181a1722c0dbba85a03489c12ca8b47f89b515e48f8a776564f3c35b58c6118be9396e0eb270c48cfbe21794c9
6
+ metadata.gz: 533ff82a0dac43e211d9a5800b71ef9ec59a4e8d34d16214a3799ea7398e9746a1d0b823db55bfd4b272ea9a48a9e685a3f49fc437f8b7a7aa1ed49ef2d531c8
7
+ data.tar.gz: 0fc9fc3e0b1d898c1870c286d14dcd59e8a6679d428c026dcf2c6107ef8e281f40d0e08838f625e1a39b321199510920fe2e3a1b9ff20a08990106220fffc949
data/README.md CHANGED
@@ -1,8 +1,29 @@
1
1
  # PropsTemplate
2
2
 
3
- PropsTemplate is a queryable JSON templating library inspired by JBuilder. It has support for layouts, partials, russian-doll caching, multi-fetch and can selectively render nodes in your tree without executing others.
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
- Example:
7
+ [![Build
8
+ Status](https://circleci.com/gh/thoughtbot/props_template.svg?style=shield)](https://circleci.com/gh/thoughtbot/props_template)
9
+
10
+ It's fast.
11
+
12
+ PropsTemplate bypasses the steps of hash building and serializing
13
+ that other libraries perform by using Oj's `StringWriter` in `rails` mode.
14
+
15
+ ![benchmarks](docs/benchmarks.png)
16
+
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.
6
27
 
7
28
  ```ruby
8
29
  json.flash flash.to_h
@@ -40,25 +61,33 @@ json.posts do
40
61
  json.total @posts.count
41
62
  end
42
63
 
43
-
44
64
  json.footer partial: 'shared/footer' do
45
65
  end
46
66
  ```
47
67
 
68
+ ## Installation
69
+
70
+ ```
71
+ gem 'props_template'
72
+ ```
73
+
74
+ and run `bundle`
75
+
48
76
  ## API
49
77
 
50
- ### json.set! or json.<your key here>
51
- Defines the attribute or stucture. All keys are automatically camelized.
78
+ ### json.set! or json.\<your key here\>
79
+
80
+ Defines the attribute or structure. All keys are automatically camelized lower.
52
81
 
53
82
  ```ruby
54
- json.set! :author_details, {..options...} do
83
+ json.set! :author_details, {...options} do
55
84
  json.set! :first_name, 'David'
56
85
  end
57
86
 
58
87
  or
59
88
 
60
- json.author_details, {..options...} do
61
- json.first_name, 'David'
89
+ json.author_details, {...options} do
90
+ json.first_name 'David'
62
91
  end
63
92
 
64
93
 
@@ -73,6 +102,7 @@ The inline form defines key and value
73
102
  | value | A value |
74
103
 
75
104
  ```ruby
105
+
76
106
  json.set! :first_name, 'David'
77
107
 
78
108
  or
@@ -92,39 +122,36 @@ The block form defines key and structure
92
122
 
93
123
  ```ruby
94
124
  json.set! :details do
95
- ...
125
+ ...
96
126
  end
97
127
 
98
128
  or
99
129
 
100
130
  json.details do
101
- ...
131
+ ...
102
132
  end
103
133
  ```
104
134
 
105
135
  The difference between the block form and inline form is
106
- 1. The block form is an internal node. Partials, Deferement and other [options](#options) are only available on the block form.
107
- 2. The inline form is considered a leaf node, and you can only [search](#traversing) for internal nodes.
136
+ 1. The block form is an internal node. Functionality such as Partials,
137
+ Deferment and other [options](#options) are only available on the
138
+ block form.
139
+ 2. The inline form is considered a leaf node, and you can only [search](#traversing)
140
+ for internal nodes.
108
141
 
109
142
  ### json.array!
110
143
  Generates an array of json objects.
111
144
 
112
145
  ```ruby
113
- collection = [
114
- {name: 'john'},
115
- {name: 'jim'}
116
- ]
146
+ collection = [ {name: 'john'}, {name: 'jim'} ]
117
147
 
118
148
  json.details do
119
- json.array! collection, {....options...} do |person|
149
+ json.array! collection, {...options} do |person|
120
150
  json.first_name person[:name]
121
151
  end
122
152
  end
123
153
 
124
- # => {"details": [
125
- {"firstName": 'john'},
126
- {"firstName": 'jim'}
127
- ]}
154
+ # => {"details": [{"firstName": 'john'}, {"firstName": 'jim'} ]}
128
155
  ```
129
156
 
130
157
  | Parameter | Notes |
@@ -132,7 +159,8 @@ end
132
159
  | collection | A collection that responds to `member_at` and `member_by` |
133
160
  | options | Additional [options](#options)|
134
161
 
135
- To support [traversing nodes](react-redux.md#traversing-nodes), any list passed to `array!` MUST implement `member_at(index)` and `member_by(attr, value)`.
162
+ To support [traversing nodes](#traversing), any list passed
163
+ to `array!` MUST implement `member_at(index)` and `member_by(attr, value)`.
136
164
 
137
165
  For example, if you were using a delegate:
138
166
 
@@ -153,7 +181,10 @@ end
153
181
  Then in your template:
154
182
 
155
183
  ```ruby
156
- data = ObjectCollection.new([{id: 1, name: 'foo'}, {id: 2, name: 'bar'}])
184
+ data = ObjectCollection.new([
185
+ {id: 1, name: 'foo'},
186
+ {id: 2, name: 'bar'}
187
+ ])
157
188
 
158
189
  json.array! data do
159
190
  ...
@@ -184,11 +215,15 @@ end
184
215
 
185
216
  #### **Array core extension**
186
217
 
187
- For convenience, PropsTemplate includes a core\_ext that adds these methods to `Array`. For example:
218
+ For convenience, PropsTemplate includes a core\_ext that adds these methods to
219
+ `Array`. For example:
188
220
 
189
221
  ```ruby
190
222
  require 'props_template/core_ext'
191
- data = [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}]
223
+ data = [
224
+ {id: 1, name: 'foo'},
225
+ {id: 2, name: 'bar'}
226
+ ]
192
227
 
193
228
  json.posts
194
229
  json.array! data do
@@ -197,38 +232,39 @@ json.posts
197
232
  end
198
233
  ```
199
234
 
200
- 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`.
235
+ PropsTemplate does not know what the elements are in your collection. The
236
+ example above will be fine for [traversing](#traversing)
237
+ by index, but will raise a `NotImplementedError` if you query by attribute. You
238
+ may still need to implement `member_by`.
201
239
 
202
240
  ### json.deferred!
203
- Returns all deferred nodes used by the [#deferment](#deferment) option.
241
+ Returns all deferred nodes used by the [deferment](#deferment) option.
204
242
 
205
- ```ruby
206
- json.deferred json.deferred!
207
- ```
208
-
209
- This method is normally used in `application.json.props` when first generated by `rails breezy:install:web`
243
+ **Note** This is a [BreezyJS][1] specific functionality and is used in
244
+ `application.json.props` when first running `rails breezy:install:web`
210
245
 
211
- ### json.fragments!
212
- Returns all fragment nodes used by the [partial fragments](#partial-fragments) option.
213
246
 
214
247
  ```ruby
215
- json.fragments json.fragments!
216
- ```
248
+ json.deferred json.deferred!
217
249
 
218
- This method is normally used in `application.json.props` when first generated by `rails breezy:install:web`
250
+ # => [{url: '/some_url?bzq=outer.inner', path: 'outer.inner', type: 'auto'}]
251
+ ```
219
252
 
220
- ### json.fragment_digest!
253
+ This method provides metadata about deferred nodes to the frontend ([BreezyJS][1])
254
+ to fetch missing data in a second round trip.
221
255
 
222
- Returns the digest of the current partial name and the locals passed. Useful for [optimistic updates](#optimistic-updates).
256
+ ### json.fragments!
257
+ Returns all fragment nodes used by the [partial fragments](#partial-fragments)
258
+ option.
223
259
 
224
- ```ruby
225
- # _some_partial.json.props
260
+ ```ruby json.fragments json.fragments! ```
226
261
 
227
- json.digest json.fragment_digest!
228
- ```
262
+ **Note** This is a [BreezyJS][1] specific functionality and is used in
263
+ `application.json.props` when first running `rails breezy:install:web`
229
264
 
230
265
  ## Options
231
- Functionality such as Partials, Deferements, and Caching can only be set on a block. It is normal to see empty blocks.
266
+ Options Functionality such as Partials, Deferements, and Caching can only be
267
+ set on a block. It is normal to see empty blocks.
232
268
 
233
269
  ```ruby
234
270
  json.post(partial: 'blog_post') do
@@ -237,7 +273,9 @@ end
237
273
 
238
274
  ### Partials
239
275
 
240
- 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.
276
+ Partials are supported. The following will render the file
277
+ `views/posts/_blog_posts.json.props`, and set a local variable `foo` assigned
278
+ with @post, which you can use inside the partial.
241
279
 
242
280
  ```ruby
243
281
  json.one_post partial: ["posts/blog_post", locals: {post: @post}] do
@@ -247,7 +285,8 @@ end
247
285
  Usage with arrays:
248
286
 
249
287
  ```ruby
250
- # as an option on an array. The `as:` option is supported when using `array!`
288
+ # The `as:` option is supported when using `array!`
289
+
251
290
  json.posts do
252
291
  json.array! @posts, partial: ["posts/blog_post", locals: {foo: 'bar'}, as: 'post'] do
253
292
  end
@@ -255,14 +294,14 @@ end
255
294
  ```
256
295
 
257
296
  ### Partial Fragments
297
+ **Note** This is a [BreezyJS][1] specific functionality.
258
298
 
259
- 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.
260
-
261
- You would need use partials and add the option `fragment: true`.
299
+ A fragment identifies a partial output across multiple pages. It can be used to
300
+ update cross cutting concerns like a header bar.
262
301
 
263
302
  ```ruby
264
303
  # index.json.props
265
- json.header partial: ["profile", fragment: true] do
304
+ json.header partial: ["profile", fragment: "header"] do
266
305
  end
267
306
 
268
307
  # _profile.json.props
@@ -276,57 +315,16 @@ end
276
315
  When using fragments with Arrays, the argument **MUST** be a lamda:
277
316
 
278
317
  ```ruby
279
- require 'props_template/core_ext' #See (lists)[#Lists]
280
-
281
- json.array! ['foo', 'bar'], partial: ["footer", fragment: ->(x){ x == 'foo'}]
282
- ```
283
-
284
- 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:
318
+ require 'props_template/core_ext'
285
319
 
286
- ```ruby
287
- # index.js.breezy
288
- json.header partial: ["profile", fragment: 'me_header'] do
320
+ json.array! ['foo', 'bar'], partial: ["footer", fragment: ->(x){ x == 'foo'}] do
289
321
  end
290
322
  ```
291
323
 
292
- #### Optimisitc Updates
293
- 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.
294
-
295
- For example:
296
-
297
- ```ruby
298
- # _header.js.props
299
- json.fragment_digest json.fragment_digest!
300
- ```
301
-
302
- And in your reducer
303
-
304
- ```javacript
305
- import {updateFragments} from 'jho406/Breezy';
306
-
307
- switch(action.type) {
308
- case SOME_ACTION: {
309
- const {
310
- fragmentDigest,
311
- prevNode // <- the content of the _header.js.props
312
- } = action.payload
313
-
314
- const nextNode = {
315
- ...prevNode,
316
- foo: 'bar'
317
- }
318
-
319
- return updateFragments(state, {
320
- [fragmentDigest]: nextNode
321
- })
322
- }
323
- default:
324
- return state
325
- }
326
- ```
327
-
328
324
  ### Caching
329
- Caching is supported on any node.
325
+ Caching is supported on internal nodes only. This limitation is what makes it
326
+ possible to for props_template to forgo marshalling/unmarshalling and simply
327
+ use [push_json](http://www.ohler.com/oj/doc/Oj/StringWriter.html#push_json-instance_method).
330
328
 
331
329
  Usage:
332
330
 
@@ -352,44 +350,60 @@ end
352
350
  When used with arrays, PropsTemplate will use `Rails.cache.read_multi`.
353
351
 
354
352
  ```ruby
355
- require 'props_template/core_ext' #See (lists)[#Lists]
353
+ require 'props_template/core_ext'
354
+
355
+ opts = { cache: ->(i){ ['a', i] } }
356
356
 
357
- opts = {
358
- cache: ->(i){ ['a', i] }
359
- }
360
357
  json.array! [4,5], opts do |x|
361
358
  json.top "hello" + x.to_s
362
359
  end
363
360
 
364
361
  #or on arrays with partials
365
362
 
366
- opts = {
367
- cache: (->(d){ ['a', d.id] }),
368
- partial: ["blog_post", as: :blog_post]
369
- }
370
- json.array! @options, opts
363
+ opts = { cache: (->(d){ ['a', d.id] }), partial: ["blog_post", as: :blog_post] }
364
+
365
+ json.array! @options, opts do
366
+ end
371
367
  ```
372
368
 
373
369
  ### Deferment
374
370
 
375
- 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.
376
- 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.
371
+ You can defer rendering of expensive nodes in your content tree using the
372
+ `defer: :manual` option. Behind the scenes PropsTemplates will no-op the block
373
+ entirely and replace the value with a placeholder. A common use case would be
374
+ tabbed content that does not load until you click the tab.
375
+
376
+ When your client receives the payload, you may issue a second request to the
377
+ same endpoint to fetch any missing nodes. See [traversing nodes](#traversing)
377
378
 
378
- You can access what was deferred with `json.deferred!`. If you use the generators, this will be set up in `application.json.props`.
379
+ There is also an `defer: :auto` option that you can use with [BreezyJS][1]. [BreezyJS][1]
380
+ will use the metadata from `json.deferred!` to issue a `remote` dispatch to fetch
381
+ the missing node and immutably graft it at the appropriate keypath in your Redux
382
+ store.
379
383
 
380
384
  Usage:
381
385
 
382
386
  ```ruby
383
- json.dashboard(defer: :auto) do
387
+ json.dashboard(defer: :manual) do
388
+ sleep 10
389
+ json.some_fancy_metric 42
390
+ end
391
+
392
+
393
+ # or you can explicitly pass a placeholder
394
+
395
+ json.dashboard(defer: [:manual, placeholder: {}]) do
384
396
  sleep 10
385
397
  json.some_fancy_metric 42
386
398
  end
387
399
  ```
388
400
 
389
- A manual option is also available:
401
+ A auto option is available:
402
+
403
+ **Note** This is a [BreezyJS][1] specific functionality.
390
404
 
391
405
  ```ruby
392
- json.dashboard(defer: :manual) do
406
+ json.dashboard(defer: :auto) do
393
407
  sleep 10
394
408
  json.some_fancy_metric 42
395
409
  end
@@ -401,39 +415,51 @@ Finally in your `application.json.props`:
401
415
  json.defers json.deferred!
402
416
  ```
403
417
 
404
-
405
- 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.
406
-
407
418
  #### Working with arrays
408
- 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.
419
+ The default behavior for deferements is to use the index of the collection to
420
+ identify an element.
421
+
422
+ **Note** If you are using this library with [BreezyJS][1], the `:auto` options will
423
+ generate `?_bzq=a.b.c.0.title` for `json.deferred!`.
409
424
 
410
425
  If you wish to use an attribute to identify the element. You must:
411
- 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`
412
- 2. Implement `member_at`, and `member_key` on the collection to allow for BreezyJS to traverse the tree based on key value attributes.
426
+
427
+ 1. Use the `:key` option on `json.array!`. This key refers to an attribute on
428
+ your collection item, and is used for `defer: :auto` to generate a keypath for
429
+ [BreezyJS][1]. If you are NOT using BreezyJS, you do not need to do this.
430
+
431
+ 2. Implement `member_at`, on the [collection](#jsonarray). This will be called
432
+ by PropsTemplate to when [searching nodes](#traversing)
413
433
 
414
434
  For example:
415
435
 
416
436
  ```ruby
417
- require 'props_template/core_ext' #See (lists)[#Lists]
418
-
419
- data = [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}]
437
+ require 'props_template/core_ext'
438
+ data = [
439
+ {id: 1, name: 'foo'},
440
+ {id: 2, name: 'bar'}
441
+ ]
420
442
 
421
443
  json.posts
422
444
  json.array! data, key: :some_id do |item|
445
+ # By using :key, props_template will append `json.some_id item.some_id`
446
+ # automatically
447
+
423
448
  json.contact(defer: :auto) do
424
449
  json.address '123 example drive'
425
450
  end
426
-
427
- # json.some_id item.some_id will be appended automatically to the end of the block
428
451
  end
429
452
  end
430
453
  ```
431
454
 
432
- 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)`.
455
+ If you are using [BreezyJS][1], BreezyJS will, it will automatically kick off
456
+ `remote(?bzq=posts.some_id=1.contact)` and `remote(?bzq=posts.some_id=2.contact)`.
433
457
 
434
- # Traversing
458
+ ## Traversing
435
459
 
436
- 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)
460
+ PropsTemplate has the ability to walk the tree you build, skipping execution of
461
+ untargeted nodes. This feature is useful for selectively updating your frontend
462
+ state.
437
463
 
438
464
  ```ruby
439
465
  traversal_path = ['data', 'details', 'personal']
@@ -441,7 +467,7 @@ traversal_path = ['data', 'details', 'personal']
441
467
  json.data(search: traversal_path) do
442
468
  json.details do
443
469
  json.employment do
444
- ...more stuff...
470
+ ...more stuff
445
471
  end
446
472
 
447
473
  json.personal do
@@ -452,25 +478,28 @@ json.data(search: traversal_path) do
452
478
  end
453
479
 
454
480
  json.footer do
455
- ...
481
+ ...
456
482
  end
457
483
  ```
458
484
 
459
- 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:
485
+ PropsTemplate will walk depth first, walking only when it finds a matching key,
486
+ then executes the associated block, and repeats until it the node is found.
487
+ The above will output:
460
488
 
461
489
  ```json
462
490
  {
463
- data: {
464
- name: 'james',
465
- zipCode: 91210
491
+ "data": {
492
+ "name": 'james',
493
+ "zipCode": 91210
466
494
  },
467
- footer: {
468
- ....
495
+ "footer": {
496
+ ...
469
497
  }
470
498
  }
471
499
  ```
472
500
 
473
- Breezy's searching only works with blocks, and will NOT work with Scalars ("leaf" values). For example:
501
+ Searching only works with blocks, and will NOT work with Scalars
502
+ ("leaf" values). For example:
474
503
 
475
504
  ```ruby
476
505
  traversal_path = ['data', 'details', 'personal', 'name'] <- not found
@@ -482,12 +511,11 @@ json.data(search: traversal_path) do
482
511
  end
483
512
  end
484
513
  end
485
-
486
514
  ```
487
515
 
488
516
  ## Nodes that do not exist
489
517
 
490
- Nodes that are not found will not define the key where search was enabled on.
518
+ Nodes that are not found will remove the branch where search was enabled on.
491
519
 
492
520
  ```ruby
493
521
  traversal_path = ['data', 'details', 'does_not_exist']
@@ -501,17 +529,54 @@ json.data(search: traversal_path) do
501
529
  end
502
530
 
503
531
  json.footer do
504
- ...
532
+ ...
505
533
  end
506
-
507
534
  ```
508
535
 
509
536
  The above will render:
510
537
 
511
- ```
538
+ ```json
512
539
  {
513
- footer: {
540
+ "footer": {
514
541
  ...
515
542
  }
516
543
  }
517
544
  ```
545
+
546
+ ## Layouts
547
+ A single layout is supported. To use, create an `application.json.props` in
548
+ `app/views/layouts`. Here's an example:
549
+
550
+ ```ruby
551
+ json.data do
552
+ # template runs here.
553
+ yield json
554
+ end
555
+
556
+ json.header do
557
+ json.greeting "Hello"
558
+ end
559
+
560
+ json.footer do
561
+ json.greeting "Hello"
562
+ end
563
+
564
+ json.flash flash.to_h
565
+ ```
566
+
567
+ **NOTE** PropsTemplate inverts the usual Rails rendering flow. PropsTemplate
568
+ will render Layout first, then the template when `yield json` is used.
569
+
570
+ ## Contributing
571
+
572
+ See the [CONTRIBUTING] document. Thank you, [contributors]!
573
+
574
+ [CONTRIBUTING]: CONTRIBUTING.md
575
+ [contributors]: https://github.com/thoughtbot/props_template/graphs/contributors
576
+
577
+ ## Special Thanks
578
+
579
+ Thanks to [turbostreamer](https://github.com/malomalo/turbostreamer) for the
580
+ inspiration.
581
+
582
+ [1]: https://github.com/thoughtbot/breezy