props_template 0.13.0 → 0.14.0

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