props_template 0.16.0 → 0.17.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: abee2de45e7fc076a1edad8c9d3edd9f13b3335e0946d6d227fcc4e5370a9e60
4
- data.tar.gz: 20bb87744d490b3cae705a1514a965b07ec04cfdc99c5f7deafb627c00fb7484
3
+ metadata.gz: 4d36bdd3007d7932d78d70eaa76a69e84caea142098f8bff3a1df72b6c57bba8
4
+ data.tar.gz: 40f592a3801bc47ad1fe620845813f476df1d25a4ab08d3ae611bff7024af363
5
5
  SHA512:
6
- metadata.gz: 84b19f30e105c344ea37f0db00524804f998ba01ab84e0cd2f5c47e7c9324061096fd9b089df310a9cfc7a25980b72b01cb2c397649d3ce6ba24e9ca1fc7fd94
7
- data.tar.gz: 07257d4609fb4ebbce6621dd804f87abe1b28ab336a41ea1d092611d973d3ea905134b5ad0d690ed9496c62758318825034b1f99b3668b09b3aa7046f49fc9c3
6
+ metadata.gz: f6ec9ff34ea8fa3a99b243d6ef7c7e87da1c5c6ad5c43c570b311cd73372e70905fa6b84a7a05fe09d53435768261afd7213695a4c508cbcc10cdf7929bb8e5d
7
+ data.tar.gz: 4a58228230f74446c5c2a449f2c3ce023c47b292a4ed9bb22bae4864bdba441365b3e49e1d02d7fe857497c3dc8534e23c89b19f9ddb5d326357b623c6062f93
data/README.md CHANGED
@@ -213,7 +213,7 @@ json.posts
213
213
  end
214
214
  ```
215
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 traverse by attribute `/posts?bzq=posts.id=1`. You may still need a delegate that implements `member_by`.
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
217
 
218
218
  ### json.deferred!
219
219
  Returns all deferred nodes used by the [#deferment](#deferment) option.
@@ -233,16 +233,6 @@ json.fragments json.fragments!
233
233
 
234
234
  This method is normally used in `application.json.props` when first generated by `rails breezy:install:web`
235
235
 
236
- ### json.fragment_digest!
237
-
238
- Returns the digest of the current partial name and the locals passed. Useful for [optimistic updates](#optimistic-updates).
239
-
240
- ```ruby
241
- # _some_partial.json.props
242
-
243
- json.digest json.fragment_digest!
244
- ```
245
-
246
236
  ## Options
247
237
  Functionality such as Partials, Deferements, and Caching can only be set on a block. It is normal to see empty blocks.
248
238
 
@@ -305,42 +295,6 @@ json.header partial: ["profile", fragment: 'me_header'] do
305
295
  end
306
296
  ```
307
297
 
308
- #### Optimisitc Updates
309
- 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.
310
-
311
- For example:
312
-
313
- ```ruby
314
- # _header.js.props
315
- json.fragment_digest json.fragment_digest!
316
- ```
317
-
318
- And in your reducer
319
-
320
- ```javacript
321
- import {updateFragments} from 'jho406/Breezy';
322
-
323
- switch(action.type) {
324
- case SOME_ACTION: {
325
- const {
326
- fragmentDigest,
327
- prevNode // <- the content of the _header.js.props
328
- } = action.payload
329
-
330
- const nextNode = {
331
- ...prevNode,
332
- foo: 'bar'
333
- }
334
-
335
- return updateFragments(state, {
336
- [fragmentDigest]: nextNode
337
- })
338
- }
339
- default:
340
- return state
341
- }
342
- ```
343
-
344
298
  ### Caching
345
299
  Caching is supported on any node.
346
300
 
@@ -418,7 +372,7 @@ json.defers json.deferred!
418
372
  ```
419
373
 
420
374
 
421
- 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.
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.
422
376
 
423
377
  #### Working with arrays
424
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.
@@ -16,7 +16,6 @@ module Props
16
16
  :deferred!,
17
17
  :fragments!,
18
18
  :set_block_content!,
19
- :fragment_digest!,
20
19
  to: :builder!
21
20
 
22
21
  def initialize(context = nil, options = {})
@@ -28,10 +28,6 @@ module Props
28
28
  @em.fragments
29
29
  end
30
30
 
31
- def fragment_digest!
32
- @em.fragment_digest
33
- end
34
-
35
31
  def set_block_content!(options = {})
36
32
  return super if !@em.has_extensions(options)
37
33
 
@@ -7,7 +7,7 @@ module Props
7
7
  class ExtensionManager
8
8
  attr_reader :base, :builder, :context
9
9
 
10
- def initialize(base, defered=[], fragments={})
10
+ def initialize(base, defered=[], fragments=[])
11
11
  @base = base
12
12
  @context = base.context
13
13
  @builder = base.builder
@@ -36,10 +36,6 @@ module Props
36
36
  @deferment.deferred
37
37
  end
38
38
 
39
- def fragment_digest
40
- @fragment.name
41
- end
42
-
43
39
  def fragments
44
40
  @fragment.fragments
45
41
  end
@@ -59,10 +55,8 @@ module Props
59
55
  handle_cache(options) do
60
56
  base.set_block_content! do
61
57
  if options[:partial]
62
- current_digest = @fragment.name
63
58
  @fragment.handle(options)
64
59
  @partialer.handle(options)
65
- @fragment.name = current_digest
66
60
  else
67
61
  yield
68
62
  end
@@ -104,14 +98,7 @@ module Props
104
98
  next_deferred, next_fragments = Oj.load(meta)
105
99
  base.stream.push_json(raw_json)
106
100
  deferred.push(*next_deferred)
107
-
108
- next_fragments.each do |k, v|
109
- if fragments[k]
110
- fragments[k].push(*v)
111
- else
112
- fragments[k] = v
113
- end
114
- end
101
+ fragments.push(*next_fragments)
115
102
  end
116
103
  else
117
104
  yield
@@ -25,12 +25,6 @@ module Props
25
25
  @context
26
26
  end
27
27
 
28
- def instrument(name, **options)
29
- ActiveSupport::Notifications.instrument(name, options) do |payload|
30
- yield payload
31
- end
32
- end
33
-
34
28
  def multi_fetch(keys, options = {})
35
29
  result = {}
36
30
  key_to_ckey = {}
@@ -49,7 +43,7 @@ module Props
49
43
 
50
44
  read_caches = {}
51
45
 
52
- instrument('read_multi_fragments.action_view', payload) do |payload|
46
+ ActiveSupport::Notifications.instrument('read_multi_fragments.action_view', payload) do |payload|
53
47
  read_caches = ::Rails.cache.read_multi(*ckeys, options)
54
48
  payload[:read_caches] = read_caches
55
49
  end
@@ -122,7 +116,7 @@ module Props
122
116
 
123
117
  def cache_key(key, options)
124
118
  name_options = options.slice(:skip_digest, :virtual_path)
125
- key = fragment_name_with_digest(key, name_options)
119
+ key = @context.cache_fragment_name(key, **name_options)
126
120
 
127
121
  if @context.respond_to?(:combined_fragment_cache_key)
128
122
  key = @context.combined_fragment_cache_key(key)
@@ -132,19 +126,6 @@ module Props
132
126
 
133
127
  ::ActiveSupport::Cache.expand_cache_key(key, :props)
134
128
  end
135
-
136
- def fragment_name_with_digest(key, options)
137
- if @context.respond_to?(:cache_fragment_name)
138
- # Current compatibility, fragment_name_with_digest is private again and cache_fragment_name
139
- # should be used instead.
140
- @context.cache_fragment_name(key, options)
141
- elsif @context.respond_to?(:fragment_name_with_digest)
142
- # Backwards compatibility for period of time when fragment_name_with_digest was made public.
143
- @context.fragment_name_with_digest(key)
144
- else
145
- key
146
- end
147
- end
148
129
  end
149
130
  end
150
131
 
@@ -34,6 +34,8 @@ module Props
34
34
 
35
35
  type, rest = options[:defer]
36
36
  placeholder = rest[:placeholder]
37
+ success_action = rest[:success_action]
38
+ fail_action = rest[:fail_action]
37
39
 
38
40
  if type.to_sym == :auto && options[:key]
39
41
  key, val = options[:key]
@@ -50,11 +52,17 @@ module Props
50
52
 
51
53
  uri.query = ::URI.encode_www_form(qry)
52
54
 
53
- @deferred.push(
55
+ deferral = {
54
56
  url: uri.to_s,
55
57
  path: path,
56
- type: type.to_s
57
- )
58
+ type: type.to_s,
59
+ }
60
+
61
+ # camelize for JS land
62
+ deferral[:successAction] = success_action if success_action
63
+ deferral[:failAction] = fail_action if fail_action
64
+
65
+ @deferred.push(deferral)
58
66
 
59
67
  placeholder
60
68
  end
@@ -1,14 +1,10 @@
1
- require 'digest'
2
-
3
1
  module Props
4
2
  class Fragment
5
3
  attr_reader :fragments
6
- attr_accessor :name
7
4
 
8
- def initialize(base, fragments={})
5
+ def initialize(base, fragments=[])
9
6
  @base = base
10
7
  @fragments = fragments
11
- @digest = Digest::SHA2.new(256)
12
8
  end
13
9
 
14
10
  def handle(options)
@@ -20,30 +16,10 @@ module Props
20
16
  fragment_name = fragment.to_s
21
17
  path = @base.traveled_path.join('.')
22
18
  @name = fragment_name
23
- @fragments[fragment_name] ||= []
24
- @fragments[fragment_name].push(path)
25
- end
26
-
27
- if fragment == true
28
- locals = partial_opts[:locals]
29
19
 
30
- identity = {}
31
- locals
32
- .clone
33
- .tap{|h| h.delete(:json)}
34
- .each do |key, value|
35
- if value.respond_to?(:to_global_id)
36
- identity[key] = value.to_global_id.to_s
37
- else
38
- identity[key] = value
39
- end
40
- end
41
-
42
- path = @base.traveled_path.join('.')
43
- fragment_name = @digest.hexdigest("#{partial_name}#{identity.to_json}")
44
- @name = fragment_name
45
- @fragments[fragment_name] ||= []
46
- @fragments[fragment_name].push(path)
20
+ @fragments.push(
21
+ { type: fragment_name, partial: partial_name, path: path }
22
+ )
47
23
  end
48
24
  end
49
25
  end
@@ -1,22 +1,56 @@
1
1
  require 'action_view'
2
2
 
3
3
  module Props
4
+ class RenderedTemplate
5
+ attr_reader :body, :layout, :template
6
+
7
+ def initialize(body, layout, template)
8
+ @body = body
9
+ @layout = layout
10
+ @template = template
11
+ end
12
+
13
+ def format
14
+ template.format
15
+ end
16
+ end
17
+
4
18
  class Partialer
19
+ INVALID_PARTIAL_MESSAGE = "The partial name must be a string, but received (%s)."
20
+
5
21
  def initialize(base, context, builder)
6
22
  @context = context
7
23
  @builder = builder
8
24
  @base = base
9
25
  end
10
26
 
27
+ def extract_details(options) # :doc:
28
+ @context.lookup_context.registered_details.each_with_object({}) do |key, details|
29
+ value = options[key]
30
+
31
+ details[key] = Array(value) if value
32
+ end
33
+ end
34
+
11
35
  def find_and_add_template(all_options)
12
36
  first_opts = all_options[0]
13
37
 
14
38
  if first_opts[:partial]
15
39
  partial_opts = block_opts_to_render_opts(@builder, first_opts)
16
- renderer = PartialRenderer.new(@context, partial_opts)
40
+ .merge(formats: [:json])
41
+ partial_opts.delete(:handlers)
42
+ partial = partial_opts[:partial]
43
+
44
+ if !(String === partial)
45
+ raise ArgumentError.new(INVALID_PARTIAL_MESSAGE % (partial.inspect))
46
+ end
47
+
48
+ template_keys = retrieve_template_keys(partial_opts)
49
+ details = extract_details(partial_opts)
50
+ template = find_template(partial, template_keys, details)
17
51
 
18
52
  all_options.map do |opts|
19
- opts[:_template] = renderer.template
53
+ opts[:_template] = template
20
54
  opts
21
55
  end
22
56
  else
@@ -24,6 +58,17 @@ module Props
24
58
  end
25
59
  end
26
60
 
61
+ def find_template(path, locals, details)
62
+ prefixes = path.include?(?/) ? [] : @context.lookup_context.prefixes
63
+ @context.lookup_context.find_template(path, prefixes, true, locals, details)
64
+ end
65
+
66
+ def retrieve_template_keys(options)
67
+ template_keys = options[:locals].keys
68
+ template_keys << options[:as] if options[:as]
69
+ template_keys
70
+ end
71
+
27
72
  def block_opts_to_render_opts(builder, options)
28
73
  partial, pass_opts = [*options[:partial]]
29
74
  pass_opts ||= {}
@@ -45,9 +90,20 @@ module Props
45
90
 
46
91
  renderer.render(template, pass_opts)
47
92
  end
93
+
94
+ def render(template, options)
95
+ view = @context
96
+ instrument(:partial, identifier: template.identifier) do |payload|
97
+ locals = options[:locals]
98
+ content = template.render(view, locals)
99
+
100
+ payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
101
+ build_rendered_template(content, template)
102
+ end
103
+ end
48
104
  end
49
105
 
50
- class PartialRenderer < ActionView::AbstractRenderer
106
+ class PartialRenderer
51
107
  OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
52
108
  "make sure it starts with lowercase letter, " \
53
109
  "and is followed by any combination of letters, numbers and underscores."
@@ -56,21 +112,6 @@ module Props
56
112
 
57
113
  INVALID_PARTIAL_MESSAGE = "The partial name must be a string, but received (%s)."
58
114
 
59
- def self.find_and_add_template(builder, context, all_options)
60
- first_opts = all_options[0]
61
-
62
- if first_opts[:partial]
63
- partial_opts = block_opts_to_render_opts(builder, first_opts)
64
- renderer = new(context, partial_opts)
65
-
66
- all_options.map do |opts|
67
- opts[:_template] = renderer.template
68
- opts
69
- end
70
- else
71
- all_options
72
- end
73
- end
74
115
 
75
116
  def self.raise_invalid_option_as(as)
76
117
  raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
@@ -106,8 +147,7 @@ module Props
106
147
  locals[as] = item
107
148
 
108
149
  if fragment_name = rest[:fragment]
109
- fragment_name = Proc === fragment_name ? fragment_name.call(item) : fragment_name.to_s
110
- rest[:fragment] = fragment_name
150
+ rest[:fragment] = fragment_name.to_s
111
151
  end
112
152
  end
113
153
 
@@ -121,7 +161,6 @@ module Props
121
161
 
122
162
  def initialize(context, options)
123
163
  @context = context
124
- super(@context.lookup_context)
125
164
  @options = options.merge(formats: [:json])
126
165
  @options.delete(:handlers)
127
166
  @details = extract_details(@options)
@@ -133,7 +172,7 @@ module Props
133
172
  end
134
173
 
135
174
  @path = partial
136
- @context_prefix = @lookup_context.prefixes.first
175
+
137
176
  template_keys = retrieve_template_keys(@options)
138
177
  @template = find_template(@path, template_keys)
139
178
  end
@@ -145,6 +184,19 @@ module Props
145
184
  end
146
185
 
147
186
  private
187
+ def extract_details(options) # :doc:
188
+ @context.lookup_context.registered_details.each_with_object({}) do |key, details|
189
+ value = options[key]
190
+
191
+ details[key] = Array(value) if value
192
+ end
193
+ end
194
+
195
+ def instrument(name, **options) # :doc:
196
+ ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
197
+ yield payload
198
+ end
199
+ end
148
200
 
149
201
  def render_partial(template, view, options)
150
202
  template ||= @template
@@ -159,17 +211,13 @@ module Props
159
211
  end
160
212
  end
161
213
 
162
- # Sets up instance variables needed for rendering a partial. This method
163
- # finds the options and details and extracts them. The method also contains
164
- # logic that handles the type of object passed in as the partial.
165
- #
166
- # If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
167
- # set to that string. Otherwise, the +options[:partial]+ object must
168
- # respond to +to_partial_path+ in order to setup the path.
214
+ def build_rendered_template(content, template, layout = nil)
215
+ RenderedTemplate.new content, layout, template
216
+ end
169
217
 
170
218
  def find_template(path, locals)
171
- prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
172
- @lookup_context.find_template(path, prefixes, true, locals, @details)
219
+ prefixes = path.include?(?/) ? [] : @context.lookup_context.prefixes
220
+ @context.lookup_context.find_template(path, prefixes, true, locals, @details)
173
221
  end
174
222
 
175
223
  def retrieve_template_keys(options)
@@ -23,9 +23,6 @@ module Props
23
23
  []
24
24
  end
25
25
 
26
- def fragment_digest!
27
- end
28
-
29
26
  def found!
30
27
  pass_opts = @found_options.clone || {}
31
28
  pass_opts.delete(:defer)
@@ -1,16 +1,24 @@
1
- require_relative './support/helper'
2
- require_relative './support/rails_helper'
3
- require 'props_template/layout_patch'
1
+ require_relative "./support/helper"
2
+ require_relative "./support/rails_helper"
3
+ require "props_template/layout_patch"
4
+ require "action_controller"
4
5
 
5
- RSpec.describe 'Props::Template' do
6
- it 'uses a layout to render' do
7
- view_path = File.join(File.dirname(__FILE__),'./fixtures')
8
- controller = ActionView::TestCase::TestController.new
6
+ RSpec.describe "Props::Template" do
7
+ class TestController < ActionController::Base
8
+ protect_from_forgery
9
+
10
+ def self.controller_path
11
+ ""
12
+ end
13
+ end
14
+
15
+ it "uses a layout to render" do
16
+ view_path = File.join(File.dirname(__FILE__), "./fixtures")
17
+ controller = TestController.new
9
18
  controller.prepend_view_path(view_path)
10
- controller.response.headers['Content-Type']='application/json'
11
- controller.request.path = '/some_url'
12
19
 
13
- json = controller.render('200', layout: 'application')
20
+ json = controller.render_to_string("200", layout: "application")
21
+
14
22
  expect(json.strip).to eql('{"data":{"success":"ok"}}')
15
23
  end
16
24
  end
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.16.0
4
+ version: 0.17.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-07-25 00:00:00.000000000 Z
11
+ date: 2021-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -96,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
98
  requirements: []
99
- rubygems_version: 3.0.3
99
+ rubygems_version: 3.1.2
100
100
  signing_key:
101
101
  specification_version: 4
102
102
  summary: A JSON builder for your React props