props_template 0.14.0 → 0.20.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: 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