props_template 0.13.0 → 0.17.1

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: 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
+ ```