props_template 0.13.0 → 0.17.1

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: 9d1594043556583c84e0d4406165c623183b5843200c5fcdcac84f86ef7c30cc
4
- data.tar.gz: 1b9cf4b6fa1accad65dee696ba8b3b998dc1f1687c1ef010980def63fceed882
3
+ metadata.gz: c1228a8c2c592c21c0cdf1197c48e22b7da9f93ebde0efcc0f2dd5fd69a3831c
4
+ data.tar.gz: f788e4ee20ce71e27c13fc22876f5d79a6fe3bbeeb156728509774f24fc007bc
5
5
  SHA512:
6
- metadata.gz: 25f9e1f56cead08969fa97f7b010059bfc073e67977c20d0b71977ada79a8282b9c037d8a9f9b3f744cfe48f09ae2a91e2442e2738433a124977bd04eea6d5eb
7
- data.tar.gz: 0442331012ade9ae49c2adc2a34e2c4b2ac5d809030622334fb71ad98db221be1f8c1032e4c81d2874e519ab676fa467f333457124f92df24a710c4e0b5c1663
6
+ metadata.gz: dbea606c10aeacb7255d954c27229763129aa94490c49e5b64935d83991ba2719a9c4adbd5c7c40d7c6cbd76ec7bc9f00e40ecb776fa8f01c8c315b1c74b9429
7
+ data.tar.gz: 8c815c348ef0aca567e8755fc2c24efe9334e044f0232cf106a25b5be792ad0f7925914ae4fc0be7ff7c3fc41fabb10b953de38895d1a8bbc8ead07674372531
@@ -0,0 +1,487 @@
1
+ # PropsTemplate
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.
4
+
5
+ PropsTemplate is fast!
6
+
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.
8
+
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).
10
+
11
+
12
+ Example:
13
+
14
+ ```ruby
15
+ json.flash flash.to_h
16
+
17
+ json.menu do
18
+ # all keys will be formatted as camelCase
19
+
20
+ json.current_user do
21
+ json.email current_user.email
22
+ json.avatar current_user.avatar
23
+ json.inbox current_user.messages.count
24
+ end
25
+ end
26
+
27
+ json.dashboard(defer: :auto) do
28
+ sleep 5
29
+ json.complex_post_metric 500
30
+ end
31
+
32
+ json.posts do
33
+ page_num = params[:page_num]
34
+ paged_posts = @posts.page(page_num).per(20)
35
+
36
+ json.list do
37
+ json.array! paged_posts, key: :id do |post|
38
+ json.id post.id
39
+ json.description post.description
40
+ json.comments_count post.comments.count
41
+ json.edit_path edit_post_path(post)
42
+ end
43
+ end
44
+
45
+ json.pagination_path posts_path
46
+ json.current paged_posts.current_page
47
+ json.total @posts.count
48
+ end
49
+
50
+
51
+ json.footer partial: 'shared/footer' do
52
+ end
53
+ ```
54
+
55
+ ## Installation
56
+ If you plan to use PropsTemplate alone just add it to your Gemfile.
57
+
58
+ ```
59
+ gem 'props_template'
60
+ ```
61
+
62
+ and run `bundle`
63
+
64
+ ## API
65
+
66
+ ### json.set! or json.<your key here>
67
+ Defines the attribute or stucture. All keys are automatically camelized lower.
68
+
69
+ ```ruby
70
+ json.set! :author_details, {..options...} do
71
+ json.set! :first_name, 'David'
72
+ end
73
+
74
+ or
75
+
76
+ json.author_details, {..options...} do
77
+ json.first_name, 'David'
78
+ end
79
+
80
+
81
+ # => {"authorDetails": { "firstName": "David" }}
82
+ ```
83
+
84
+ The inline form defines key and value
85
+
86
+ | Parameter | Notes |
87
+ | :--- | :--- |
88
+ | key | A json object key|
89
+ | value | A value |
90
+
91
+ ```ruby
92
+ json.set! :first_name, 'David'
93
+
94
+ or
95
+
96
+ json.first_name 'David'
97
+
98
+ # => { "firstName": "David" }
99
+ ```
100
+
101
+ The block form defines key and structure
102
+
103
+ | Parameter | Notes |
104
+ | :--- | :--- |
105
+ | key | A json object key|
106
+ | options | Additional [options](#options)|
107
+ | block | Additional `json.set!`s or `json.array!`s|
108
+
109
+ ```ruby
110
+ json.set! :details do
111
+ ...
112
+ end
113
+
114
+ or
115
+
116
+ json.details do
117
+ ...
118
+ end
119
+ ```
120
+
121
+ 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.
124
+
125
+ ### json.array!
126
+ Generates an array of json objects.
127
+
128
+ ```ruby
129
+ collection = [
130
+ {name: 'john'},
131
+ {name: 'jim'}
132
+ ]
133
+
134
+ json.details do
135
+ json.array! collection, {....options...} do |person|
136
+ json.first_name person[:name]
137
+ end
138
+ end
139
+
140
+ # => {"details": [
141
+ {"firstName": 'john'},
142
+ {"firstName": 'jim'}
143
+ ]}
144
+ ```
145
+
146
+ | Parameter | Notes |
147
+ | :--- | :--- |
148
+ | collection | A collection that responds to `member_at` and `member_by` |
149
+ | options | Additional [options](#options)|
150
+
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)`.
152
+
153
+ For example, if you were using a delegate:
154
+
155
+ ```ruby
156
+ class ObjectCollection < SimpleDelegator
157
+ def member_at(index)
158
+ at(index)
159
+ end
160
+
161
+ def member_by(attr, val)
162
+ find do |ele|
163
+ ele[attr] == val
164
+ end
165
+ end
166
+ end
167
+ ```
168
+
169
+ Then in your template:
170
+
171
+ ```ruby
172
+ data = ObjectCollection.new([{id: 1, name: 'foo'}, {id: 2, name: 'bar'}])
173
+
174
+ json.array! data do
175
+ ...
176
+ end
177
+ ```
178
+
179
+ Similarly for ActiveRecord:
180
+
181
+ ```ruby
182
+ class ApplicationRecord < ActiveRecord::Base
183
+ def self.member_at(index)
184
+ offset(index).limit(1).first
185
+ end
186
+
187
+ def self.member_by(attr, value)
188
+ find_by(Hash[attr, val])
189
+ end
190
+ end
191
+ ```
192
+
193
+ Then in your template:
194
+
195
+ ```ruby
196
+ json.array! Post.all do
197
+ ...
198
+ end
199
+ ```
200
+
201
+ #### **Array core extension**
202
+
203
+ For convenience, PropsTemplate includes a core\_ext that adds these methods to `Array`. For example:
204
+
205
+ ```ruby
206
+ require 'props_template/core_ext'
207
+ data = [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}]
208
+
209
+ json.posts
210
+ json.array! data do
211
+ ...
212
+ end
213
+ end
214
+ ```
215
+
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 query by attribute `/posts?bzq=posts.id=1`. You may still need a delegate that implements `member_by`.
217
+
218
+ ### json.deferred!
219
+ Returns all deferred nodes used by the [#deferment](#deferment) option.
220
+
221
+ ```ruby
222
+ json.deferred json.deferred!
223
+ ```
224
+
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
+
230
+ ```ruby
231
+ json.fragments json.fragments!
232
+ ```
233
+
234
+ This method is normally used in `application.json.props` when first generated by `rails breezy:install:web`
235
+
236
+ ## Options
237
+ Functionality such as Partials, Deferements, and Caching can only be set on a block. It is normal to see empty blocks.
238
+
239
+ ```ruby
240
+ json.post(partial: 'blog_post') do
241
+ end
242
+ ```
243
+
244
+ ### Partials
245
+
246
+ 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.
247
+
248
+ ```ruby
249
+ json.one_post partial: ["posts/blog_post", locals: {post: @post}] do
250
+ end
251
+ ```
252
+
253
+ Usage with arrays:
254
+
255
+ ```ruby
256
+ # as an option on an array. The `as:` option is supported when using `array!`
257
+ json.posts do
258
+ json.array! @posts, partial: ["posts/blog_post", locals: {foo: 'bar'}, as: 'post'] do
259
+ end
260
+ end
261
+ ```
262
+
263
+ ### Partial Fragments
264
+
265
+ 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.
266
+
267
+ You would need use partials and add the option `fragment: true`.
268
+
269
+ ```ruby
270
+ # index.json.props
271
+ json.header partial: ["profile", fragment: true] do
272
+ end
273
+
274
+ # _profile.json.props
275
+ json.profile do
276
+ json.address do
277
+ json.state "New York City"
278
+ end
279
+ end
280
+ ```
281
+
282
+ When using fragments with Arrays, the argument **MUST** be a lamda:
283
+
284
+ ```ruby
285
+ require 'props_template/core_ext' #See (lists)[#Lists]
286
+
287
+ json.array! ['foo', 'bar'], partial: ["footer", fragment: ->(x){ x == 'foo'}]
288
+ ```
289
+
290
+ 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:
291
+
292
+ ```ruby
293
+ # index.js.breezy
294
+ json.header partial: ["profile", fragment: 'me_header'] do
295
+ end
296
+ ```
297
+
298
+ ### Caching
299
+ Caching is supported on any node.
300
+
301
+ Usage:
302
+
303
+ ```ruby
304
+ json.author(cache: "some_cache_key") do
305
+ json.first_name "tommy"
306
+ end
307
+
308
+ #or
309
+
310
+ json.profile(cache: "cachekey", partial: ["profile", locals: {foo: 1}]) do
311
+ end
312
+
313
+ #or nest it
314
+
315
+ json.author(cache: "some_cache_key") do
316
+ json.address(cache: "some_other_cache_key") do
317
+ json.zip 11214
318
+ end
319
+ end
320
+ ```
321
+
322
+ When used with arrays, PropsTemplate will use `Rails.cache.read_multi`.
323
+
324
+ ```ruby
325
+ require 'props_template/core_ext' #See (lists)[#Lists]
326
+
327
+ opts = {
328
+ cache: ->(i){ ['a', i] }
329
+ }
330
+ json.array! [4,5], opts do |x|
331
+ json.top "hello" + x.to_s
332
+ end
333
+
334
+ #or on arrays with partials
335
+
336
+ opts = {
337
+ cache: (->(d){ ['a', d.id] }),
338
+ partial: ["blog_post", as: :blog_post]
339
+ }
340
+ json.array! @options, opts
341
+ ```
342
+
343
+ ### Deferment
344
+
345
+ 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.
346
+ 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.
347
+
348
+ You can access what was deferred with `json.deferred!`. If you use the generators, this will be set up in `application.json.props`.
349
+
350
+ Usage:
351
+
352
+ ```ruby
353
+ json.dashboard(defer: :auto) do
354
+ sleep 10
355
+ json.some_fancy_metric 42
356
+ end
357
+ ```
358
+
359
+ A manual option is also available:
360
+
361
+ ```ruby
362
+ json.dashboard(defer: :manual) do
363
+ sleep 10
364
+ json.some_fancy_metric 42
365
+ end
366
+ ```
367
+
368
+ Finally in your `application.json.props`:
369
+
370
+ ```ruby
371
+ json.defers json.deferred!
372
+ ```
373
+
374
+
375
+ If `:manual` is used, PropsTemplate will no-op the block and will not populate `json.deferred!`. Its up to you to [query](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.
376
+
377
+ #### Working with arrays
378
+ 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.
379
+
380
+ If you wish to use an attribute to identify the element. You must:
381
+ 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`
382
+ 2. Implement `member_at`, and `member_key` on the collection to allow for BreezyJS to traverse the tree based on key value attributes.
383
+
384
+ For example:
385
+
386
+ ```ruby
387
+ require 'props_template/core_ext' #See (lists)[#Lists]
388
+
389
+ data = [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}]
390
+
391
+ json.posts
392
+ json.array! data, key: :some_id do |item|
393
+ json.contact(defer: :auto) do
394
+ json.address '123 example drive'
395
+ end
396
+
397
+ # json.some_id item.some_id will be appended automatically to the end of the block
398
+ end
399
+ end
400
+ ```
401
+
402
+ 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)`.
403
+
404
+ # Traversing
405
+
406
+ 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)
407
+
408
+ ```ruby
409
+ traversal_path = ['data', 'details', 'personal']
410
+
411
+ json.data(search: traversal_path) do
412
+ json.details do
413
+ json.employment do
414
+ ...more stuff...
415
+ end
416
+
417
+ json.personal do
418
+ json.name 'james'
419
+ json.zip_code 91210
420
+ end
421
+ end
422
+ end
423
+
424
+ json.footer do
425
+ ...
426
+ end
427
+ ```
428
+
429
+ 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:
430
+
431
+ ```json
432
+ {
433
+ data: {
434
+ name: 'james',
435
+ zipCode: 91210
436
+ },
437
+ footer: {
438
+ ....
439
+ }
440
+ }
441
+ ```
442
+
443
+ Breezy's searching only works with blocks, and will NOT work with Scalars ("leaf" values). For example:
444
+
445
+ ```ruby
446
+ traversal_path = ['data', 'details', 'personal', 'name'] <- not found
447
+
448
+ json.data(search: traversal_path) do
449
+ json.details do
450
+ json.personal do
451
+ json.name 'james'
452
+ end
453
+ end
454
+ end
455
+
456
+ ```
457
+
458
+ ## Nodes that do not exist
459
+
460
+ Nodes that are not found will not define the key where search was enabled on.
461
+
462
+ ```ruby
463
+ traversal_path = ['data', 'details', 'does_not_exist']
464
+
465
+ json.data(search: traversal_path) do
466
+ json.details do
467
+ json.personal do
468
+ json.name 'james'
469
+ end
470
+ end
471
+ end
472
+
473
+ json.footer do
474
+ ...
475
+ end
476
+
477
+ ```
478
+
479
+ The above will render:
480
+
481
+ ```
482
+ {
483
+ footer: {
484
+ ...
485
+ }
486
+ }
487
+ ```