props_template 0.13.0 → 0.14.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: 9d1594043556583c84e0d4406165c623183b5843200c5fcdcac84f86ef7c30cc
4
- data.tar.gz: 1b9cf4b6fa1accad65dee696ba8b3b998dc1f1687c1ef010980def63fceed882
3
+ metadata.gz: ad98bb076e592658c137fd423771b211760ba047a1f6f394efa7a531a25eedc5
4
+ data.tar.gz: f5495c4eca8b7015ac5d18714a10a16735769128fac62b5693824b1ef0c6e505
5
5
  SHA512:
6
- metadata.gz: 25f9e1f56cead08969fa97f7b010059bfc073e67977c20d0b71977ada79a8282b9c037d8a9f9b3f744cfe48f09ae2a91e2442e2738433a124977bd04eea6d5eb
7
- data.tar.gz: 0442331012ade9ae49c2adc2a34e2c4b2ac5d809030622334fb71ad98db221be1f8c1032e4c81d2874e519ab676fa467f333457124f92df24a710c4e0b5c1663
6
+ metadata.gz: 629b8dc24b25dc596ee6f2bb9d03abce69e5af292121d118a83e3d53b83f38ef7ccc155e4cc79d4469a6e588c2efa3231ac02b05447ed11aa6c4642fca833d6f
7
+ data.tar.gz: 83d0aed77e8cba7cf1730307f15a6fed2b972a181a1722c0dbba85a03489c12ca8b47f89b515e48f8a776564f3c35b58c6118be9396e0eb270c48cfbe21794c9
@@ -0,0 +1,517 @@
1
+ # PropsTemplate
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.
4
+
5
+ Example:
6
+
7
+ ```ruby
8
+ json.flash flash.to_h
9
+
10
+ json.menu do
11
+ # all keys will be formatted as camelCase
12
+
13
+ json.current_user do
14
+ json.email current_user.email
15
+ json.avatar current_user.avatar
16
+ json.inbox current_user.messages.count
17
+ end
18
+ end
19
+
20
+ json.dashboard(defer: :auto) do
21
+ sleep 5
22
+ json.complex_post_metric 500
23
+ end
24
+
25
+ json.posts do
26
+ page_num = params[:page_num]
27
+ paged_posts = @posts.page(page_num).per(20)
28
+
29
+ json.list do
30
+ json.array! paged_posts, key: :id do |post|
31
+ json.id post.id
32
+ json.description post.description
33
+ json.comments_count post.comments.count
34
+ json.edit_path edit_post_path(post)
35
+ end
36
+ end
37
+
38
+ json.pagination_path posts_path
39
+ json.current paged_posts.current_page
40
+ json.total @posts.count
41
+ end
42
+
43
+
44
+ json.footer partial: 'shared/footer' do
45
+ end
46
+ ```
47
+
48
+ ## API
49
+
50
+ ### json.set! or json.<your key here>
51
+ Defines the attribute or stucture. All keys are automatically camelized.
52
+
53
+ ```ruby
54
+ json.set! :author_details, {..options...} do
55
+ json.set! :first_name, 'David'
56
+ end
57
+
58
+ or
59
+
60
+ json.author_details, {..options...} do
61
+ json.first_name, 'David'
62
+ end
63
+
64
+
65
+ # => {"authorDetails": { "firstName": "David" }}
66
+ ```
67
+
68
+ The inline form defines key and value
69
+
70
+ | Parameter | Notes |
71
+ | :--- | :--- |
72
+ | key | A json object key|
73
+ | value | A value |
74
+
75
+ ```ruby
76
+ json.set! :first_name, 'David'
77
+
78
+ or
79
+
80
+ json.first_name 'David'
81
+
82
+ # => { "firstName": "David" }
83
+ ```
84
+
85
+ The block form defines key and structure
86
+
87
+ | Parameter | Notes |
88
+ | :--- | :--- |
89
+ | key | A json object key|
90
+ | options | Additional [options](#options)|
91
+ | block | Additional `json.set!`s or `json.array!`s|
92
+
93
+ ```ruby
94
+ json.set! :details do
95
+ ...
96
+ end
97
+
98
+ or
99
+
100
+ json.details do
101
+ ...
102
+ end
103
+ ```
104
+
105
+ 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.
108
+
109
+ ### json.array!
110
+ Generates an array of json objects.
111
+
112
+ ```ruby
113
+ collection = [
114
+ {name: 'john'},
115
+ {name: 'jim'}
116
+ ]
117
+
118
+ json.details do
119
+ json.array! collection, {....options...} do |person|
120
+ json.first_name person[:name]
121
+ end
122
+ end
123
+
124
+ # => {"details": [
125
+ {"firstName": 'john'},
126
+ {"firstName": 'jim'}
127
+ ]}
128
+ ```
129
+
130
+ | Parameter | Notes |
131
+ | :--- | :--- |
132
+ | collection | A collection that responds to `member_at` and `member_by` |
133
+ | options | Additional [options](#options)|
134
+
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)`.
136
+
137
+ For example, if you were using a delegate:
138
+
139
+ ```ruby
140
+ class ObjectCollection < SimpleDelegator
141
+ def member_at(index)
142
+ at(index)
143
+ end
144
+
145
+ def member_by(attr, val)
146
+ find do |ele|
147
+ ele[attr] == val
148
+ end
149
+ end
150
+ end
151
+ ```
152
+
153
+ Then in your template:
154
+
155
+ ```ruby
156
+ data = ObjectCollection.new([{id: 1, name: 'foo'}, {id: 2, name: 'bar'}])
157
+
158
+ json.array! data do
159
+ ...
160
+ end
161
+ ```
162
+
163
+ Similarly for ActiveRecord:
164
+
165
+ ```ruby
166
+ class ApplicationRecord < ActiveRecord::Base
167
+ def self.member_at(index)
168
+ offset(index).limit(1).first
169
+ end
170
+
171
+ def self.member_by(attr, value)
172
+ find_by(Hash[attr, val])
173
+ end
174
+ end
175
+ ```
176
+
177
+ Then in your template:
178
+
179
+ ```ruby
180
+ json.array! Post.all do
181
+ ...
182
+ end
183
+ ```
184
+
185
+ #### **Array core extension**
186
+
187
+ For convenience, PropsTemplate includes a core\_ext that adds these methods to `Array`. For example:
188
+
189
+ ```ruby
190
+ require 'props_template/core_ext'
191
+ data = [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}]
192
+
193
+ json.posts
194
+ json.array! data do
195
+ ...
196
+ end
197
+ end
198
+ ```
199
+
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`.
201
+
202
+ ### json.deferred!
203
+ Returns all deferred nodes used by the [#deferment](#deferment) option.
204
+
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`
210
+
211
+ ### json.fragments!
212
+ Returns all fragment nodes used by the [partial fragments](#partial-fragments) option.
213
+
214
+ ```ruby
215
+ json.fragments json.fragments!
216
+ ```
217
+
218
+ This method is normally used in `application.json.props` when first generated by `rails breezy:install:web`
219
+
220
+ ### json.fragment_digest!
221
+
222
+ Returns the digest of the current partial name and the locals passed. Useful for [optimistic updates](#optimistic-updates).
223
+
224
+ ```ruby
225
+ # _some_partial.json.props
226
+
227
+ json.digest json.fragment_digest!
228
+ ```
229
+
230
+ ## Options
231
+ Functionality such as Partials, Deferements, and Caching can only be set on a block. It is normal to see empty blocks.
232
+
233
+ ```ruby
234
+ json.post(partial: 'blog_post') do
235
+ end
236
+ ```
237
+
238
+ ### Partials
239
+
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.
241
+
242
+ ```ruby
243
+ json.one_post partial: ["posts/blog_post", locals: {post: @post}] do
244
+ end
245
+ ```
246
+
247
+ Usage with arrays:
248
+
249
+ ```ruby
250
+ # as an option on an array. The `as:` option is supported when using `array!`
251
+ json.posts do
252
+ json.array! @posts, partial: ["posts/blog_post", locals: {foo: 'bar'}, as: 'post'] do
253
+ end
254
+ end
255
+ ```
256
+
257
+ ### Partial Fragments
258
+
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`.
262
+
263
+ ```ruby
264
+ # index.json.props
265
+ json.header partial: ["profile", fragment: true] do
266
+ end
267
+
268
+ # _profile.json.props
269
+ json.profile do
270
+ json.address do
271
+ json.state "New York City"
272
+ end
273
+ end
274
+ ```
275
+
276
+ When using fragments with Arrays, the argument **MUST** be a lamda:
277
+
278
+ ```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:
285
+
286
+ ```ruby
287
+ # index.js.breezy
288
+ json.header partial: ["profile", fragment: 'me_header'] do
289
+ end
290
+ ```
291
+
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
+ ### Caching
329
+ Caching is supported on any node.
330
+
331
+ Usage:
332
+
333
+ ```ruby
334
+ json.author(cache: "some_cache_key") do
335
+ json.first_name "tommy"
336
+ end
337
+
338
+ #or
339
+
340
+ json.profile(cache: "cachekey", partial: ["profile", locals: {foo: 1}]) do
341
+ end
342
+
343
+ #or nest it
344
+
345
+ json.author(cache: "some_cache_key") do
346
+ json.address(cache: "some_other_cache_key") do
347
+ json.zip 11214
348
+ end
349
+ end
350
+ ```
351
+
352
+ When used with arrays, PropsTemplate will use `Rails.cache.read_multi`.
353
+
354
+ ```ruby
355
+ require 'props_template/core_ext' #See (lists)[#Lists]
356
+
357
+ opts = {
358
+ cache: ->(i){ ['a', i] }
359
+ }
360
+ json.array! [4,5], opts do |x|
361
+ json.top "hello" + x.to_s
362
+ end
363
+
364
+ #or on arrays with partials
365
+
366
+ opts = {
367
+ cache: (->(d){ ['a', d.id] }),
368
+ partial: ["blog_post", as: :blog_post]
369
+ }
370
+ json.array! @options, opts
371
+ ```
372
+
373
+ ### Deferment
374
+
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.
377
+
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
+
380
+ Usage:
381
+
382
+ ```ruby
383
+ json.dashboard(defer: :auto) do
384
+ sleep 10
385
+ json.some_fancy_metric 42
386
+ end
387
+ ```
388
+
389
+ A manual option is also available:
390
+
391
+ ```ruby
392
+ json.dashboard(defer: :manual) do
393
+ sleep 10
394
+ json.some_fancy_metric 42
395
+ end
396
+ ```
397
+
398
+ Finally in your `application.json.props`:
399
+
400
+ ```ruby
401
+ json.defers json.deferred!
402
+ ```
403
+
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
+ #### 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.
409
+
410
+ 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.
413
+
414
+ For example:
415
+
416
+ ```ruby
417
+ require 'props_template/core_ext' #See (lists)[#Lists]
418
+
419
+ data = [{id: 1, name: 'foo'}, {id: 2, name: 'bar'}]
420
+
421
+ json.posts
422
+ json.array! data, key: :some_id do |item|
423
+ json.contact(defer: :auto) do
424
+ json.address '123 example drive'
425
+ end
426
+
427
+ # json.some_id item.some_id will be appended automatically to the end of the block
428
+ end
429
+ end
430
+ ```
431
+
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)`.
433
+
434
+ # Traversing
435
+
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)
437
+
438
+ ```ruby
439
+ traversal_path = ['data', 'details', 'personal']
440
+
441
+ json.data(search: traversal_path) do
442
+ json.details do
443
+ json.employment do
444
+ ...more stuff...
445
+ end
446
+
447
+ json.personal do
448
+ json.name 'james'
449
+ json.zip_code 91210
450
+ end
451
+ end
452
+ end
453
+
454
+ json.footer do
455
+ ...
456
+ end
457
+ ```
458
+
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:
460
+
461
+ ```json
462
+ {
463
+ data: {
464
+ name: 'james',
465
+ zipCode: 91210
466
+ },
467
+ footer: {
468
+ ....
469
+ }
470
+ }
471
+ ```
472
+
473
+ Breezy's searching only works with blocks, and will NOT work with Scalars ("leaf" values). For example:
474
+
475
+ ```ruby
476
+ traversal_path = ['data', 'details', 'personal', 'name'] <- not found
477
+
478
+ json.data(search: traversal_path) do
479
+ json.details do
480
+ json.personal do
481
+ json.name 'james'
482
+ end
483
+ end
484
+ end
485
+
486
+ ```
487
+
488
+ ## Nodes that do not exist
489
+
490
+ Nodes that are not found will not define the key where search was enabled on.
491
+
492
+ ```ruby
493
+ traversal_path = ['data', 'details', 'does_not_exist']
494
+
495
+ json.data(search: traversal_path) do
496
+ json.details do
497
+ json.personal do
498
+ json.name 'james'
499
+ end
500
+ end
501
+ end
502
+
503
+ json.footer do
504
+ ...
505
+ end
506
+
507
+ ```
508
+
509
+ The above will render:
510
+
511
+ ```
512
+ {
513
+ footer: {
514
+ ...
515
+ }
516
+ }
517
+ ```
@@ -78,23 +78,15 @@ module Props
78
78
  end
79
79
 
80
80
  def handle_collection_item(collection, item, index, options)
81
- if collection.respond_to? :member_key
82
- member_key = collection.member_key
83
- end
84
-
85
- if !member_key
81
+ if !options[:key]
86
82
  @traveled_path.push(index)
87
83
  else
88
- id = if item.respond_to? member_key
89
- item.send(member_key)
90
- elsif item.is_a? Hash
91
- item[member_key] || item[member_key.to_sym]
92
- end
84
+ id, val = options[:key]
93
85
 
94
86
  if id.nil?
95
87
  @traveled_path.push(index)
96
88
  else
97
- @traveled_path.push("#{member_key.to_s}=#{id}")
89
+ @traveled_path.push("#{id.to_s}=#{val}")
98
90
  end
99
91
  end
100
92
 
@@ -110,6 +102,16 @@ module Props
110
102
 
111
103
 
112
104
  def refine_item_options(item, options)
105
+ if key = options[:key]
106
+ val = if item.respond_to? key
107
+ item.send(key)
108
+ elsif item.is_a? Hash
109
+ item[key] || item[key.to_sym]
110
+ end
111
+
112
+ options[:key] = [options[:key], val]
113
+ end
114
+
113
115
  @em.refine_options(options, item)
114
116
  end
115
117
  end
@@ -43,7 +43,7 @@ module Props
43
43
  end
44
44
 
45
45
  def has_extensions(options)
46
- options[:defer] || options[:cache] || options[:partial]
46
+ options[:defer] || options[:cache] || options[:partial] || options[:key]
47
47
  end
48
48
 
49
49
  def handle(commands, options)
@@ -64,6 +64,11 @@ module Props
64
64
  else
65
65
  yield
66
66
  end
67
+
68
+ if options[:key]
69
+ id, val = options[:key]
70
+ base.set!(id, val)
71
+ end
67
72
  end
68
73
  end
69
74
  end
@@ -35,6 +35,12 @@ module Props
35
35
  type, rest = options[:defer]
36
36
  placeholder = rest[:placeholder]
37
37
 
38
+ if type.to_sym == :auto && options[:key]
39
+ key, val = options[:key]
40
+ placeholder = {}
41
+ placeholder[key] = val
42
+ end
43
+
38
44
  request_path = @base.context.controller.request.fullpath
39
45
  path = @base.traveled_path.join('.')
40
46
  uri = ::URI.parse(request_path)
@@ -18,9 +18,9 @@ module Props
18
18
  def render_props_template(view, template, path, locals)
19
19
  layout_locals = locals.dup
20
20
  layout_locals.delete(:json)
21
+ layout_locals[:virtual_path_of_template] = template.virtual_path
21
22
 
22
- layout = resolve_props_layout(path, layout_locals, [formats.first])
23
-
23
+ layout = resolve_props_layout(path, layout_locals.keys, [formats.first])
24
24
  body = layout.render(view, layout_locals) do |json|
25
25
  locals[:json] = json
26
26
  template.render(view, locals)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: props_template
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johny Ho
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-20 00:00:00.000000000 Z
11
+ date: 2020-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -72,6 +72,7 @@ executables: []
72
72
  extensions: []
73
73
  extra_rdoc_files: []
74
74
  files:
75
+ - README.md
75
76
  - lib/props_template.rb
76
77
  - lib/props_template/base.rb
77
78
  - lib/props_template/base_with_extensions.rb