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