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

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