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 +4 -4
- data/README.md +487 -0
- data/lib/props_template.rb +2 -9
- data/lib/props_template/base.rb +33 -24
- data/lib/props_template/base_with_extensions.rb +31 -30
- data/lib/props_template/debug_writer.rb +55 -0
- data/lib/props_template/extension_manager.rb +33 -32
- data/lib/props_template/extensions/cache.rb +2 -21
- data/lib/props_template/extensions/deferment.rb +17 -3
- data/lib/props_template/extensions/fragment.rb +4 -28
- data/lib/props_template/extensions/partial_renderer.rb +79 -33
- data/lib/props_template/layout_patch.rb +2 -2
- data/lib/props_template/railtie.rb +0 -8
- data/lib/props_template/searcher.rb +0 -3
- data/spec/layout_spec.rb +18 -10
- metadata +8 -21
- data/lib/props_template/key_formatter.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1228a8c2c592c21c0cdf1197c48e22b7da9f93ebde0efcc0f2dd5fd69a3831c
|
4
|
+
data.tar.gz: f788e4ee20ce71e27c13fc22876f5d79a6fe3bbeeb156728509774f24fc007bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dbea606c10aeacb7255d954c27229763129aa94490c49e5b64935d83991ba2719a9c4adbd5c7c40d7c6cbd76ec7bc9f00e40ecb776fa8f01c8c315b1c74b9429
|
7
|
+
data.tar.gz: 8c815c348ef0aca567e8755fc2c24efe9334e044f0232cf106a25b5be792ad0f7925914ae4fc0be7ff7c3fc41fabb10b953de38895d1a8bbc8ead07674372531
|
data/README.md
ADDED
@@ -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
|
+
```
|