hyper-component 1.0.alpha1.1 → 1.0.alpha1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/Gemfile +5 -0
  4. data/hyper-component.gemspec +6 -9
  5. data/lib/hyper-component.rb +10 -1
  6. data/lib/hyperstack/component.rb +78 -10
  7. data/lib/hyperstack/component/children.rb +1 -1
  8. data/lib/hyperstack/component/element.rb +93 -21
  9. data/lib/hyperstack/component/event.rb +1 -1
  10. data/lib/hyperstack/component/free_render.rb +21 -0
  11. data/lib/hyperstack/component/isomorphic_helpers.rb +13 -8
  12. data/lib/hyperstack/component/version.rb +1 -1
  13. data/lib/hyperstack/component/while_loading.rb +27 -0
  14. data/lib/hyperstack/ext/component/array.rb +20 -0
  15. data/lib/hyperstack/ext/component/element.rb +22 -8
  16. data/lib/hyperstack/ext/component/enumerable.rb +18 -0
  17. data/lib/hyperstack/ext/component/kernel.rb +7 -0
  18. data/lib/hyperstack/ext/component/string.rb +1 -1
  19. data/lib/hyperstack/internal/component.rb +7 -1
  20. data/lib/hyperstack/internal/component/class_methods.rb +84 -8
  21. data/lib/hyperstack/internal/component/haml.rb +3 -12
  22. data/lib/hyperstack/internal/component/instance_methods.rb +38 -5
  23. data/lib/hyperstack/internal/component/props_wrapper.rb +29 -9
  24. data/lib/hyperstack/internal/component/rails/component_mount.rb +12 -0
  25. data/lib/hyperstack/internal/component/rails/controller_helper.rb +20 -1
  26. data/lib/hyperstack/internal/component/rails/railtie.rb +3 -2
  27. data/lib/hyperstack/internal/component/rails/server_rendering/contextual_renderer.rb +5 -1
  28. data/lib/hyperstack/internal/component/rails/server_rendering/hyper_asset_container.rb +4 -2
  29. data/lib/hyperstack/internal/component/react_wrapper.rb +79 -62
  30. data/lib/hyperstack/internal/component/rendering_context.rb +95 -45
  31. data/lib/hyperstack/internal/component/rescue_wrapper.rb +40 -0
  32. data/lib/hyperstack/internal/component/should_component_update.rb +1 -1
  33. data/lib/hyperstack/internal/component/tags.rb +12 -3
  34. data/lib/hyperstack/internal/component/top_level_rails_component.rb +4 -0
  35. data/lib/hyperstack/internal/component/while_loading_wrapper.rb +29 -0
  36. data/lib/react/react-source.rb +2 -2
  37. metadata +50 -81
  38. data/Gemfile.lock +0 -363
@@ -43,6 +43,9 @@ module Hyperstack
43
43
  end
44
44
 
45
45
  def footers
46
+ return if @hyperstack_footers_rendered
47
+
48
+ @hyperstack_footers_rendered = true
46
49
  Hyperstack::Component::IsomorphicHelpers.prerender_footers(controller)
47
50
  end
48
51
  end
@@ -50,3 +53,12 @@ module Hyperstack
50
53
  end
51
54
  end
52
55
  end
56
+
57
+
58
+ module React
59
+ module Rails
60
+ module ViewHelper
61
+ alias mount_component react_component
62
+ end
63
+ end
64
+ end
@@ -1,5 +1,19 @@
1
1
  require 'action_controller'
2
2
 
3
+ module Hyperstack
4
+ module Internal
5
+ module Component
6
+ class Redirect < StandardError
7
+ attr_reader :url
8
+ def initialize(url)
9
+ @url = url
10
+ super("redirect to #{url}")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
3
17
  module ActionController
4
18
  # adds render_component helper to ActionControllers
5
19
  class Base
@@ -7,8 +21,13 @@ module ActionController
7
21
  @component_name = (args[0].is_a? Hash) || args.empty? ? params[:action].camelize : args.shift
8
22
  @render_params = args.shift || {}
9
23
  options = args[0] || {}
10
- render inline: '<%= react_component @component_name, @render_params %>',
24
+ return if performed?
25
+ render inline: '<%= mount_component @component_name, @render_params %>',
11
26
  layout: options.key?(:layout) ? options[:layout].to_s : :default
27
+ rescue Exception => e
28
+ m = /^RuntimeError: Hyperstack::Internal::Component::Redirect (.+) status: (.+)$/.match(e.message)
29
+ raise e unless m
30
+ redirect_to m[1], status: m[2]
12
31
  end
13
32
  end
14
33
  end
@@ -12,8 +12,9 @@ module Hyperstack
12
12
  end
13
13
  config.after_initialize do
14
14
  class ::HyperstackController < ::ApplicationController
15
- def action_missing(_name)
16
- render_component
15
+ def action_missing(*)
16
+ parts = params[:action].split('__')
17
+ render_component (parts[0..-2]+[parts.last.camelize]).join('::')
17
18
  end
18
19
  end
19
20
  end
@@ -13,7 +13,11 @@ module Hyperstack
13
13
 
14
14
  class ContextualRenderer < React::ServerRendering::BundleRenderer
15
15
  def initialize(options = {})
16
- super(options)
16
+ unless v8_runtime?
17
+ raise "Hyperstack prerendering only works with MiniRacer. Add 'mini_racer' to your Gemfile"
18
+ end
19
+
20
+ super({ files: ['hyperstack-prerender-loader.js'] }.merge(options))
17
21
  ComponentLoader.new(v8_context).load
18
22
  end
19
23
 
@@ -9,7 +9,9 @@ module Hyperstack
9
9
  module ServerRendering
10
10
  class HyperTestAssetContainer
11
11
  def find_asset(logical_path)
12
- ::Rails.cache.read(logical_path)
12
+ # we skip the container if it raises an error so we
13
+ # don't care if we are running under hyperspec or not
14
+ HyperSpec::Internal::Controller.cache_read(logical_path)
13
15
  end
14
16
  end
15
17
 
@@ -24,7 +26,7 @@ module Hyperstack
24
26
  if React::ServerRendering::WebpackerManifestContainer.compatible?
25
27
  @ass_containers << React::ServerRendering::WebpackerManifestContainer.new
26
28
  end
27
- @ass_containers << HyperTestAssetContainer.new if ::Rails.env.test?
29
+ @ass_containers << HyperTestAssetContainer.new
28
30
  end
29
31
 
30
32
  def find_asset(logical_path)
@@ -18,6 +18,10 @@ module Hyperstack
18
18
  class ReactWrapper
19
19
  @@component_classes = {}
20
20
 
21
+ def self.stateless?(ncc)
22
+ `typeof #{ncc} === 'symbol' || (typeof #{ncc} === 'function' && !(#{ncc}.prototype && #{ncc}.prototype.isReactComponent))`
23
+ end
24
+
21
25
  def self.import_native_component(opal_class, native_class)
22
26
  opal_class.instance_variable_set("@native_import", true)
23
27
  @@component_classes[opal_class] = native_class
@@ -26,12 +30,13 @@ module Hyperstack
26
30
  def self.eval_native_react_component(name)
27
31
  component = `eval(name)`
28
32
  raise "#{name} is not defined" if `#{component} === undefined`
33
+
34
+ component = `component.default` if `component.__esModule`
29
35
  is_component_class = `#{component}.prototype !== undefined` &&
30
36
  (`!!#{component}.prototype.isReactComponent` ||
31
37
  `!!#{component}.prototype.render`)
32
- is_functional_component = `typeof #{component} === "function"`
33
38
  has_render_method = `typeof #{component}.render === "function"`
34
- unless is_component_class || is_functional_component || has_render_method
39
+ unless is_component_class || stateless?(component) || has_render_method
35
40
  raise 'does not appear to be a native react component'
36
41
  end
37
42
  component
@@ -39,9 +44,11 @@ module Hyperstack
39
44
 
40
45
  def self.native_react_component?(name = nil)
41
46
  return false unless name
47
+
42
48
  eval_native_react_component(name)
43
49
  true
44
- rescue
50
+ # Exception to be compatible with all versions of opal
51
+ rescue Exception # rubocop:disable Lint/RescueException
45
52
  false
46
53
  end
47
54
 
@@ -60,100 +67,88 @@ module Hyperstack
60
67
  end
61
68
 
62
69
  def self.create_native_react_class(type)
70
+ raise "createReactClass is undefined. Add the 'react-create-class' npm module, and import it as 'createReactClass'" if `typeof(createReactClass)=='undefined'`
63
71
  raise "Provided class should define `render` method" if !(type.method_defined? :render)
64
- render_fn = (type.method_defined? :_render_wrapper) ? :_render_wrapper : :render
72
+ old_school = !type.method_defined?(:_render_wrapper)
73
+ render_fn = old_school ? :render : :_render_wrapper
65
74
  # this was hashing type.to_s, not sure why but .to_s does not work as it Foo::Bar::View.to_s just returns "View"
66
-
67
75
  @@component_classes[type] ||= begin
68
76
  comp = %x{
69
- class extends React.Component {
70
- constructor(props) {
71
- super(props);
77
+ createReactClass({
78
+ getInitialState: function() {
72
79
  this.mixins = #{type.respond_to?(:native_mixins) ? type.native_mixins : `[]`};
73
80
  this.statics = #{type.respond_to?(:static_call_backs) ? type.static_call_backs.to_n : `{}`};
74
- this.state = {};
75
81
  this.__opalInstanceInitializedState = false;
76
82
  this.__opalInstanceSyncSetState = true;
77
83
  this.__opalInstance = #{type.new(`this`)};
78
84
  this.__opalInstanceInitializedState = true;
79
85
  this.__opalInstanceSyncSetState = false;
80
86
  this.__name = #{type.name};
81
- }
82
- static get displayName() {
83
- if (typeof this.__name != "undefined") {
84
- return this.__name;
85
- } else {
86
- return #{type.name};
87
- }
88
- }
89
- static set displayName(name) {
90
- this.__name = name;
91
- }
92
- static get defaultProps() {
87
+ return {}
88
+ },
89
+ displayName: #{type.name},
90
+ getDefaultProps: function() {
93
91
  return #{type.respond_to?(:default_props) ? type.default_props.to_n : `{}`};
94
- }
95
- static get propTypes() {
96
- return #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`};
97
- }
98
- componentWillMount() {
92
+ },
93
+ propTypes: #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`},
94
+ componentWillMount: old_school && function() {
99
95
  if (#{type.method_defined? :component_will_mount}) {
100
96
  this.__opalInstanceSyncSetState = true;
101
97
  this.__opalInstance.$component_will_mount();
102
98
  this.__opalInstanceSyncSetState = false;
103
99
  }
104
- }
105
- componentDidMount() {
100
+ },
101
+ componentDidMount: function() {
106
102
  this.__opalInstance.__hyperstack_component_is_mounted = true
107
103
  if (#{type.method_defined? :component_did_mount}) {
108
104
  this.__opalInstanceSyncSetState = false;
109
105
  this.__opalInstance.$component_did_mount();
110
106
  }
111
- }
112
- componentWillReceiveProps(next_props) {
107
+ },
108
+ UNSAFE_componentWillReceiveProps: function(next_props) {
113
109
  if (#{type.method_defined? :component_will_receive_props}) {
114
110
  this.__opalInstanceSyncSetState = true;
115
111
  this.__opalInstance.$component_will_receive_props(Opal.Hash.$new(next_props));
116
112
  this.__opalInstanceSyncSetState = false;
117
113
  }
118
- }
119
- shouldComponentUpdate(next_props, next_state) {
114
+ },
115
+ shouldComponentUpdate: function(next_props, next_state) {
120
116
  if (#{type.method_defined? :should_component_update?}) {
121
117
  this.__opalInstanceSyncSetState = false;
122
118
  return this.__opalInstance["$should_component_update?"](Opal.Hash.$new(next_props), Opal.Hash.$new(next_state));
123
119
  } else { return true; }
124
- }
125
- componentWillUpdate(next_props, next_state) {
120
+ },
121
+ UNSAFE_componentWillUpdate: function(next_props, next_state) {
126
122
  if (#{type.method_defined? :component_will_update}) {
127
123
  this.__opalInstanceSyncSetState = false;
128
124
  this.__opalInstance.$component_will_update(Opal.Hash.$new(next_props), Opal.Hash.$new(next_state));
129
125
  }
130
- }
131
- componentDidUpdate(prev_props, prev_state) {
126
+ },
127
+ componentDidUpdate: function(prev_props, prev_state) {
132
128
  if (#{type.method_defined? :component_did_update}) {
133
129
  this.__opalInstanceSyncSetState = false;
134
130
  this.__opalInstance.$component_did_update(Opal.Hash.$new(prev_props), Opal.Hash.$new(prev_state));
135
131
  }
136
- }
137
- componentWillUnmount() {
132
+ },
133
+ componentWillUnmount: function() {
138
134
  if (#{type.method_defined? :component_will_unmount}) {
139
135
  this.__opalInstanceSyncSetState = false;
140
136
  this.__opalInstance.$component_will_unmount();
141
137
  }
142
138
  this.__opalInstance.__hyperstack_component_is_mounted = false;
143
- }
144
-
145
- render() {
139
+ },
140
+ render: function() {
146
141
  this.__opalInstanceSyncSetState = false;
147
142
  return this.__opalInstance.$send(render_fn).$to_n();
148
143
  }
149
- }
144
+ })
150
145
  }
151
146
  # check to see if there is an after_error callback. If there is add a
152
147
  # componentDidCatch handler. Because legacy behavior is to allow any object
153
148
  # that responds to render to act as a component we have to make sure that
154
149
  # we have a callbacks_for method. This all becomes much easier once issue
155
150
  # #270 is resolved.
156
- if type.respond_to?(:callbacks_for) && type.callbacks_for(:after_error) != []
151
+ if type.respond_to?(:callbacks?) && type.callbacks?(:after_error)
157
152
  add_after_error_hook_to_native comp
158
153
  end
159
154
  comp
@@ -178,7 +173,9 @@ module Hyperstack
178
173
  end
179
174
 
180
175
  # Convert Passed in properties
181
- properties = convert_props(args)
176
+ ele = nil # create nil var for the ref to use
177
+ ref = ->(ref) { ele._update_ref(ref) } unless stateless?(ncc)
178
+ properties = convert_props(type, { ref: ref }, *args)
182
179
  params << properties.shallow_to_n
183
180
 
184
181
  # Children Nodes
@@ -190,14 +187,17 @@ module Hyperstack
190
187
  }
191
188
  }
192
189
  end
193
- Hyperstack::Component::Element.new(`React.createElement.apply(null, #{params})`, type, properties, block)
190
+ # assign to ele so that the ref callback can use it
191
+ ele = Hyperstack::Component::Element.new(
192
+ `React.createElement.apply(null, #{params})`, type, properties, block
193
+ )
194
194
  end
195
195
 
196
196
  def self.clear_component_class_cache
197
197
  @@component_classes = {}
198
198
  end
199
199
 
200
- def self.convert_props(args)
200
+ def self.convert_props(type, *args)
201
201
  # merge args together into a single properties hash
202
202
  properties = {}
203
203
  args.each do |arg|
@@ -234,34 +234,51 @@ module Hyperstack
234
234
  # process properties according to react rules
235
235
  props = {}
236
236
  properties.each do |key, value|
237
- if ["style", "dangerously_set_inner_HTML"].include? key
237
+ if %w[style dangerously_set_inner_HTML].include? key
238
238
  props[lower_camelize(key)] = value.to_n
239
239
 
240
- elsif key == "className"
240
+ elsif key == :className
241
241
  props[key] = value.join(' ')
242
242
 
243
- elsif key == "key"
244
- props["key"] = value.to_key
243
+ elsif key == :key
244
+ props[:key] = value.to_key
245
+
246
+ elsif key == :init
247
+ if %w[select textarea].include? type
248
+ key = :defaultValue
249
+ elsif type == :input
250
+ key = if %w[radio checkbox].include? properties[:type]
251
+ :defaultChecked
252
+ else
253
+ :defaultValue
254
+ end
255
+ end
256
+ props[key] = value
245
257
 
246
258
  elsif key == 'ref'
259
+ next unless value
247
260
  unless value.respond_to?(:call)
248
261
  raise "The ref and dom params must be given a Proc.\n"\
249
262
  "If you want to capture the ref in an instance variable use the `set` method.\n"\
250
263
  "For example `ref: set(:TheRef)` will capture assign the ref to `@TheRef`\n"
251
264
  end
252
- props[key] = %x{
253
- function(dom_node){
254
- if (dom_node !== null && dom_node.__opalInstance !== undefined && dom_node.__opalInstance !== null) {
255
- #{ Hyperstack::Internal::State::Mapper.ignore_mutations { value.call(`dom_node.__opalInstance`) } };
256
- } else if(dom_node !== null && ReactDOM.findDOMNode !== undefined && dom_node.nodeType === undefined) {
257
- #{ Hyperstack::Internal::State::Mapper.ignore_mutations { value.call(`ReactDOM.findDOMNode(dom_node)`) } };
258
- } else if(dom_node !== null){
259
- #{ Hyperstack::Internal::State::Mapper.ignore_mutations { value.call(`dom_node`) } };
260
- }
261
- }
265
+ unless `value.__hyperstack_component_ref_is_already_wrapped`
266
+ fn = value
267
+ value = %x{
268
+ function(dom_node){
269
+ if (dom_node !== null && dom_node.__opalInstance !== undefined && dom_node.__opalInstance !== null) {
270
+ #{ Hyperstack::Internal::State::Mapper.ignore_mutations { fn.call(`dom_node.__opalInstance`) } };
271
+ } else if(dom_node !== null && ReactDOM.findDOMNode !== undefined && dom_node.nodeType === undefined) {
272
+ #{ Hyperstack::Internal::State::Mapper.ignore_mutations { fn.call(`ReactDOM.findDOMNode(dom_node)`) } };
273
+ } else if(dom_node !== null){
274
+ #{ Hyperstack::Internal::State::Mapper.ignore_mutations { fn.call(`dom_node`) } };
262
275
  }
263
-
264
- elsif key == 'dom'
276
+ }
277
+ }
278
+ `value.__hyperstack_component_ref_is_already_wrapped = true`
279
+ end
280
+ props[key] = value
281
+ elsif key == 'jq_ref'
265
282
  unless value.respond_to?(:call)
266
283
  raise "The ref and dom params must be given a Proc.\n"\
267
284
  "If you want to capture the dom node in an instance variable use the `set` method.\n"\
@@ -2,30 +2,58 @@ module Hyperstack
2
2
  module Internal
3
3
  module Component
4
4
  class RenderingContext
5
+ class NotQuiet < Exception; end
5
6
  class << self
6
7
  attr_accessor :waiting_on_resources
7
8
 
9
+ def raise_if_not_quiet?
10
+ @raise_if_not_quiet
11
+ end
12
+
13
+ def raise_if_not_quiet=(x)
14
+ @raise_if_not_quiet = x
15
+ end
16
+
17
+ def quiet_test(component)
18
+ return unless component.waiting_on_resources && raise_if_not_quiet? #&& component.class != RescueMetaWrapper <- WHY can't create a spec that this fails without this, but several fail with it.
19
+ raise NotQuiet.new("#{component} is waiting on resources")
20
+ end
21
+
22
+ def render_string(string)
23
+ @buffer ||= []
24
+ @buffer << string
25
+ end
26
+
8
27
  def render(name, *args, &block)
9
28
  was_outer_most = !@not_outer_most
10
29
  @not_outer_most = true
11
30
  remove_nodes_from_args(args)
12
- @buffer ||= [] unless @buffer
31
+ @buffer ||= [] #unless @buffer
13
32
  if block
14
33
  element = build do
15
- saved_waiting_on_resources = waiting_on_resources
34
+ saved_waiting_on_resources = nil #waiting_on_resources what was the purpose of this its used below to or in with the current elements waiting_for_resources
16
35
  self.waiting_on_resources = nil
17
- run_child_block(name.nil?, &block)
36
+ run_child_block(&block)
18
37
  if name
19
38
  buffer = @buffer.dup
20
39
  ReactWrapper.create_element(name, *args) { buffer }.tap do |element|
21
40
  element.waiting_on_resources = saved_waiting_on_resources || !!buffer.detect { |e| e.waiting_on_resources if e.respond_to?(:waiting_on_resources) }
22
41
  element.waiting_on_resources ||= waiting_on_resources if buffer.last.is_a?(String)
23
42
  end
24
- elsif @buffer.last.is_a? Hyperstack::Component::Element
25
- @buffer.last.tap { |element| element.waiting_on_resources ||= saved_waiting_on_resources }
26
43
  else
27
- buffer_s = @buffer.last.to_s
28
- RenderingContext.render(:span) { buffer_s }.tap { |element| element.waiting_on_resources = saved_waiting_on_resources }
44
+ buffer = @buffer.collect do |item|
45
+ if item.is_a? Hyperstack::Component::Element
46
+ item.waiting_on_resources ||= saved_waiting_on_resources
47
+ item
48
+ else
49
+ RenderingContext.render(:span) { item.to_s }.tap { |element| element.waiting_on_resources = saved_waiting_on_resources }
50
+ end
51
+ end
52
+ if buffer.length > 1
53
+ buffer
54
+ else
55
+ buffer.first
56
+ end
29
57
  end
30
58
  end
31
59
  elsif name.is_a? Hyperstack::Component::Element
@@ -51,6 +79,7 @@ module Hyperstack
51
79
 
52
80
  def delete(element)
53
81
  @buffer.delete(element)
82
+ @last_deleted = element
54
83
  element
55
84
  end
56
85
  alias as_node delete
@@ -72,55 +101,74 @@ module Hyperstack
72
101
  end if args[0] && args[0].is_a?(Hash)
73
102
  end
74
103
 
75
- # run_child_block gathers the element(s) generated by a child block.
104
+ # run_child_block yields to the child rendering block which will put any
105
+ # elements to be rendered into the current rendering buffer.
106
+ #
76
107
  # for example when rendering this div: div { "hello".span; "goodby".span }
77
108
  # two child Elements will be generated.
78
109
  #
79
- # the final value of the block should either be
80
- # 1 an object that responds to :acts_as_string?
81
- # 2 a string,
82
- # 3 an element that is NOT yet pushed on the rendering buffer
83
- # 4 or the last element pushed on the buffer
110
+ # However the block itself will return a value, which in some cases should
111
+ # also added to the buffer:
84
112
  #
85
- # in case 1 we render a span
86
- # in case 2 we automatically push the string onto the buffer
87
- # in case 3 we also push the Element onto the buffer IF the buffer is empty
88
- # case 4 requires no special processing
113
+ # If the final value of the block is a
89
114
  #
90
- # Once we have taken care of these special cases we do a check IF we are in an
91
- # outer rendering scope. In this case react only allows us to generate 1 Element
92
- # so we insure that is the case, and also check to make sure that element in the buffer
93
- # is the element returned
115
+ # a hyper model dummy value that is being loaded, then wrap it in a span and add it to the buffer
116
+ # a string (or if the buffer is empty any value), then add it to the buffer
117
+ # an Element, then add it on the buffer unless it has been just deleted
118
+ # #
119
+ # Note that the reason we don't always allow Strings to be automatically pushed is
120
+ # to avoid confusing results in situations like this:
121
+ # DIV { collection.each { |item| SPAN { item } } }
122
+ # If we accepted any object to be rendered this would generate:
123
+ # DIV { SPAN { collection[0] } SPAN { collection[n] } collection.to_s }
124
+ # which is probably not the desired output. If it was you would just append to_s
125
+ # to the end of the expression, to force it to be added to the output buffer.
126
+ #
127
+ # However if the buffer is empty then it makes sense to automatically apply the `.to_s`
128
+ # to the value, and push it on the buffer, unless it is a falsy value or an array
94
129
 
95
- def run_child_block(is_outer_scope)
130
+ def run_child_block
96
131
  result = yield
97
- if result.respond_to?(:acts_as_string?) && result.acts_as_string?
98
- # hyper-mesh DummyValues respond to acts_as_string, and must
132
+ check_for_component_return(result)
133
+ if dummy_value?(result)
134
+ # hyper-mesh DummyValues must
99
135
  # be converted to spans INSIDE the parent, otherwise the waiting_on_resources
100
136
  # flag will get set in the wrong context
101
137
  RenderingContext.render(:span) { result.to_s }
102
- elsif result.is_a?(String) || (result.is_a?(Hyperstack::Component::Element) && @buffer.empty?)
103
- @buffer << result
138
+ elsif result.is_a?(Hyperstack::Component::Element)
139
+ @buffer << result if @buffer.empty? unless @last_deleted == result
140
+ elsif pushable_string?(result)
141
+ @buffer << result.to_s
104
142
  end
105
- raise_render_error(result) if is_outer_scope && @buffer != [result]
143
+ @last_deleted = nil
144
+ end
145
+
146
+ def check_for_component_return(result)
147
+ # check for a common error of saying (for example) DIV (without parens)
148
+ # which returns the DIV component class instead of a rendered DIV
149
+ return unless result.try :hyper_component?
150
+
151
+ Hyperstack::Component::IsomorphicHelpers.log(
152
+ "a component's render method returned the component class #{result}, did you mean to say #{result}()",
153
+ :warning
154
+ )
155
+ end
156
+
157
+ def dummy_value?(result)
158
+ result.respond_to?(:loading?) && result.loading?
106
159
  end
107
160
 
108
- # heurestically raise a meaningful error based on the situation
109
-
110
- def raise_render_error(result)
111
- improper_render 'A different element was returned than was generated within the DSL.',
112
- 'Possibly improper use of Element#delete.' if @buffer.count == 1
113
- improper_render "Instead #{@buffer.count} elements were generated.",
114
- 'Do you want to wrap your elements in a div?' if @buffer.count > 1
115
- improper_render "Instead the component #{result} was returned.",
116
- "Did you mean #{result}()?" if result.try :hyper_component?
117
- improper_render "Instead the #{result.class} #{result} was returned.",
118
- 'You may need to convert this to a string.'
161
+ def pushable_string?(result)
162
+ # if the buffer is not empty we will only push on strings, and ignore anything else
163
+ return result.is_a?(String) unless @buffer.empty?
164
+
165
+ # if the buffer IS empty then we can push on anything except we avoid nil, false and arrays
166
+ # as these are almost never what you want to render, and if you do there are mechanisms
167
+ # to render them explicitly
168
+ result && result.respond_to?(:to_n) && !result.is_a?(Array)
119
169
  end
120
170
 
121
171
  def improper_render(message, solution)
122
- raise "a component's render method must generate and return exactly 1 element or a string.\n"\
123
- " #{message} #{solution}"
124
172
  end
125
173
  end
126
174
  end
@@ -129,7 +177,7 @@ module Hyperstack
129
177
  end
130
178
 
131
179
  class Object
132
- [:span, :td, :th, :while_loading].each do |tag|
180
+ %i[span td th].each do |tag|
133
181
  define_method(tag) do |*args, &block|
134
182
  args.unshift(tag)
135
183
  # legacy hyperloop allowed tags to be lower case as well so if self is a component
@@ -141,24 +189,26 @@ class Object
141
189
  # in the component.
142
190
  # If we fully deprecate lowercase tags, then this next line can go...
143
191
  return send(*args, &block) if respond_to?(:hyper_component?) && hyper_component?
192
+
144
193
  Hyperstack::Internal::Component::RenderingContext.render(*args) { to_s }
145
194
  end
146
195
  end
147
196
 
148
-
149
197
  def para(*args, &block)
150
198
  args.unshift(:p)
151
199
  # see above comment
152
200
  return send(*args, &block) if respond_to?(:hyper_component?) && hyper_component?
201
+
153
202
  Hyperstack::Internal::Component::RenderingContext.render(*args) { to_s }
154
203
  end
155
204
 
156
205
  def br
157
206
  # see above comment
158
207
  return send(:br) if respond_to?(:hyper_component?) && hyper_component?
159
- Hyperstack::Internal::Component::RenderingContext.render(:span) do
160
- Hyperstack::Internal::Component::RenderingContext.render(to_s)
161
- Hyperstack::Internal::Component::RenderingContext.render(:br)
208
+
209
+ Hyperstack::Internal::Component::RenderingContext.render(Hyperstack::Internal::Component::Tags::FRAGMENT) do
210
+ Hyperstack::Internal::Component::RenderingContext.render(Hyperstack::Internal::Component::Tags::FRAGMENT) { to_s }
211
+ Hyperstack::Internal::Component::RenderingContext.render(Hyperstack::Internal::Component::Tags::BR)
162
212
  end
163
213
  end
164
214