isomorfeus-react 16.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +1 -0
  3. data/README.md +620 -0
  4. data/isomorfeus-react.gemspec +23 -0
  5. data/lib/isomorfeus-react.rb +131 -0
  6. data/lib/isomorfeus/config.rb +84 -0
  7. data/lib/isomorfeus/top_level.rb +48 -0
  8. data/lib/isomorfeus/view_helpers.rb +38 -0
  9. data/lib/lucid_app/api.rb +22 -0
  10. data/lib/lucid_app/base.rb +7 -0
  11. data/lib/lucid_app/context.rb +7 -0
  12. data/lib/lucid_app/mixin.rb +17 -0
  13. data/lib/lucid_app/native_component_constructor.rb +70 -0
  14. data/lib/lucid_component/api.rb +97 -0
  15. data/lib/lucid_component/base.rb +7 -0
  16. data/lib/lucid_component/event_handler.rb +17 -0
  17. data/lib/lucid_component/initializer.rb +12 -0
  18. data/lib/lucid_component/mixin.rb +17 -0
  19. data/lib/lucid_component/native_component_constructor.rb +131 -0
  20. data/lib/react.rb +147 -0
  21. data/lib/react/active_support_support.rb +13 -0
  22. data/lib/react/component/api.rb +226 -0
  23. data/lib/react/component/base.rb +9 -0
  24. data/lib/react/component/elements.rb +78 -0
  25. data/lib/react/component/event_handler.rb +19 -0
  26. data/lib/react/component/features.rb +47 -0
  27. data/lib/react/component/history.rb +36 -0
  28. data/lib/react/component/initializer.rb +11 -0
  29. data/lib/react/component/location.rb +15 -0
  30. data/lib/react/component/match.rb +31 -0
  31. data/lib/react/component/mixin.rb +19 -0
  32. data/lib/react/component/native_component_constructor.rb +76 -0
  33. data/lib/react/component/native_component_validate_prop.rb +37 -0
  34. data/lib/react/component/props.rb +49 -0
  35. data/lib/react/component/resolution.rb +71 -0
  36. data/lib/react/component/should_component_update.rb +14 -0
  37. data/lib/react/component/state.rb +52 -0
  38. data/lib/react/component/unsafe_api.rb +33 -0
  39. data/lib/react/context_wrapper.rb +47 -0
  40. data/lib/react/function_component/creator.rb +47 -0
  41. data/lib/react/function_component/resolution.rb +61 -0
  42. data/lib/react/function_component/runner.rb +19 -0
  43. data/lib/react/native_constant_wrapper.rb +34 -0
  44. data/lib/react/pure_component/base.rb +9 -0
  45. data/lib/react/pure_component/mixin.rb +17 -0
  46. data/lib/react/redux_component/api.rb +132 -0
  47. data/lib/react/redux_component/app_store_defaults.rb +38 -0
  48. data/lib/react/redux_component/app_store_proxy.rb +46 -0
  49. data/lib/react/redux_component/base.rb +9 -0
  50. data/lib/react/redux_component/class_store_proxy.rb +50 -0
  51. data/lib/react/redux_component/component_class_store_defaults.rb +40 -0
  52. data/lib/react/redux_component/component_instance_store_defaults.rb +41 -0
  53. data/lib/react/redux_component/initializer.rb +14 -0
  54. data/lib/react/redux_component/instance_store_proxy.rb +50 -0
  55. data/lib/react/redux_component/mixin.rb +18 -0
  56. data/lib/react/redux_component/native_component_constructor.rb +119 -0
  57. data/lib/react/redux_component/reducers.rb +53 -0
  58. data/lib/react/ref.rb +19 -0
  59. data/lib/react/synthetic_event.rb +53 -0
  60. data/lib/react/version.rb +3 -0
  61. data/lib/react_dom.rb +31 -0
  62. data/lib/react_dom_server.rb +17 -0
  63. metadata +167 -0
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require_relative 'lib/react/version.rb'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'isomorfeus-react'
6
+ s.version = React::VERSION
7
+
8
+ s.authors = ['Jan Biedermann']
9
+ s.email = ['jan@kursator.com']
10
+ s.homepage = 'http://isomorfeus.com'
11
+ s.summary = 'React for Opal Ruby.'
12
+ s.license = 'MIT'
13
+ s.description = 'Write React Components in Ruby.'
14
+
15
+ s.files = `git ls-files`.split("\n").reject { |f| f.match(%r{^(gemfiles|s)/}) }
16
+ #s.test_files = `git ls-files -- {test,s,features}/*`.split("\n")
17
+ s.require_paths = ['lib']
18
+
19
+ s.add_dependency 'opal', '>= 0.11.0', '< 0.12.0'
20
+ s.add_dependency 'opal-activesupport', '~> 0.3.1'
21
+ s.add_dependency 'opal-browser', '~> 0.2.0'
22
+ s.add_dependency 'isomorfeus-redux', '~> 4.0.0'
23
+ end
@@ -0,0 +1,131 @@
1
+ if RUBY_ENGINE == 'opal'
2
+ require 'opal'
3
+ require 'native'
4
+ require 'active_support/core_ext/string'
5
+ require 'react/active_support_support'
6
+ require 'browser/support'
7
+ require 'browser/event'
8
+ require 'browser/event_source'
9
+ require 'browser/screen'
10
+ require 'browser/socket'
11
+ require 'browser/window'
12
+ require 'browser/dom/node'
13
+ require 'browser/dom/element'
14
+ require 'isomorfeus-redux'
15
+ require 'react/version'
16
+ require 'react'
17
+ # require 'react/element' # usually not needed
18
+ require 'react/synthetic_event'
19
+ require 'react_dom'
20
+ # React::Component
21
+ require 'react/component/api'
22
+ require 'react/component/unsafe_api'
23
+ require 'react/component/initializer'
24
+ require 'react/component/features'
25
+ require 'react/native_constant_wrapper'
26
+ require 'react/context_wrapper'
27
+ require 'react/component/native_component_constructor'
28
+ require 'react/component/native_component_validate_prop'
29
+ require 'react/component/props'
30
+ require 'react/component/state'
31
+ require 'react/component/match'
32
+ require 'react/component/location'
33
+ require 'react/component/history'
34
+ require 'react/component/elements'
35
+ require 'react/component/resolution'
36
+ require 'react/component/should_component_update'
37
+ require 'react/component/event_handler'
38
+ require 'react/component/mixin'
39
+ require 'react/component/base'
40
+ # React::PureComponent
41
+ require 'react/pure_component/mixin'
42
+ require 'react/pure_component/base'
43
+ # Function Component
44
+ require 'react/function_component/resolution'
45
+ require 'react/function_component/runner'
46
+ require 'react/function_component/creator'
47
+ # Redux::Component
48
+ require 'react/redux_component/component_class_store_defaults'
49
+ require 'react/redux_component/component_instance_store_defaults'
50
+ require 'react/redux_component/app_store_defaults'
51
+ require 'react/redux_component/api'
52
+ require 'react/redux_component/initializer'
53
+ require 'react/redux_component/app_store_proxy'
54
+ require 'react/redux_component/class_store_proxy'
55
+ require 'react/redux_component/instance_store_proxy'
56
+ require 'react/redux_component/native_component_constructor'
57
+ require 'react/redux_component/mixin'
58
+ require 'react/redux_component/base'
59
+ require 'react/redux_component/reducers'
60
+ require 'isomorfeus/config'
61
+
62
+ # init component reducers
63
+ React::ReduxComponent::Reducers.add_component_reducers_to_store
64
+
65
+ # init LucidApplicationContext (Store Provider and Consumer)
66
+ require 'lucid_app/context'
67
+
68
+ LucidApp::Context.create_application_context
69
+
70
+ # LucidComponent
71
+ require 'lucid_component/api'
72
+ require 'lucid_component/initializer'
73
+ require 'lucid_component/native_component_constructor'
74
+ require 'lucid_component/event_handler'
75
+ require 'lucid_component/mixin'
76
+ require 'lucid_component/base'
77
+
78
+ # LucidApp
79
+ require 'lucid_app/api'
80
+ require 'lucid_app/native_component_constructor'
81
+ require 'lucid_app/mixin'
82
+ require 'lucid_app/base'
83
+
84
+ # allow mounting of components
85
+ require 'isomorfeus/top_level'
86
+
87
+ # initalize Store, options, etc.
88
+ Isomorfeus.init
89
+ else
90
+ require 'opal'
91
+ require 'opal-activesupport'
92
+ require 'opal-browser'
93
+ require 'isomorfeus-redux'
94
+ require 'react/version'
95
+ require 'isomorfeus/config'
96
+ require 'isomorfeus/view_helpers'
97
+
98
+ Opal.append_path(__dir__.untaint)
99
+
100
+ if defined?(Rails)
101
+ module Isomorfeus
102
+ module Model
103
+ class Railtie < Rails::Railtie
104
+ def delete_first(a, e)
105
+ a.delete_at(a.index(e) || a.length)
106
+ end
107
+
108
+ config.before_configuration do |_|
109
+ Rails.configuration.tap do |config|
110
+ # rails will add everything immediately below app to eager and auto load, so we need to remove it
111
+ delete_first config.eager_load_paths, "#{config.root}/app/isomorfeus"
112
+
113
+ unless Rails.env.production?
114
+ # rails will add everything immediately below app to eager and auto load, so we need to remove it
115
+ delete_first config.autoload_paths, "#{config.root}/app/isomorfeus"
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ if Dir.exist?(File.join('app', 'isomorfeus'))
125
+ # Opal.append_path(File.expand_path(File.join('app', 'isomorfeus', 'components')))
126
+ Opal.append_path(File.expand_path(File.join('app', 'isomorfeus'))) unless Opal.paths.include?(File.expand_path(File.join('app', 'isomorfeus')))
127
+ elsif Dir.exist?('isomorfeus')
128
+ # Opal.append_path(File.expand_path(File.join('isomorfeus', 'components')))
129
+ Opal.append_path(File.expand_path('isomorfeus')) unless Opal.paths.include?(File.expand_path('isomorfeus'))
130
+ end
131
+ end
@@ -0,0 +1,84 @@
1
+ if RUBY_ENGINE == 'opal'
2
+ module Isomorfeus
3
+ class << self
4
+ attr_reader :options
5
+ attr_reader :initialized
6
+ attr_reader :store
7
+
8
+ def init
9
+ return if initialized
10
+ @initialized = true
11
+ # at least one reducer must have been added at this stage
12
+ # this happened in isomorfeus-react.rb, where the component reducers were added
13
+ @store = Redux::Store.init!
14
+ `Opal.Isomorfeus.store = #@store`
15
+ init_options
16
+ execute_init_classes
17
+ end
18
+
19
+ def init_options
20
+ return @options if @options
21
+ @options = Hash.new(`Opal.IsomorfeusOptions`)
22
+ @options.keys.each do |option|
23
+ define_singleton_method(option) do
24
+ @options[option]
25
+ end
26
+ end
27
+ end
28
+
29
+ def execute_init_classes
30
+ if options.has_key?(:client_init_class_names)
31
+ client_init_class_names.each do |constant|
32
+ constant.constantize.send(:init)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ else
39
+ module Isomorfeus
40
+ class << self
41
+ attr_accessor :client_init_class_names
42
+ attr_accessor :prerendering
43
+ attr_accessor :version
44
+
45
+ def add_client_option(option)
46
+ @options_for_client ||= Set.new
47
+ @options_for_client << option
48
+ end
49
+
50
+ def add_client_options(options)
51
+ options.each do |option|
52
+ add_client_option(option)
53
+ end
54
+ end
55
+
56
+ def add_client_init_class_name(class_name)
57
+ client_init_class_names << class_name
58
+ end
59
+
60
+ def configuration(&block)
61
+ block.call(self)
62
+ end
63
+
64
+ def options_for_client
65
+ @options_for_client
66
+ end
67
+
68
+ def options_hash_for_client
69
+ opts = {}
70
+ Isomorfeus.options_for_client.each do |option|
71
+ opts[option] = Isomorfeus.send(option)
72
+ end
73
+ opts
74
+ end
75
+ end
76
+
77
+ self.add_client_option(:client_init_class_names)
78
+ self.add_client_option(:version)
79
+
80
+ self.client_init_class_names = []
81
+ self.prerendering = :off
82
+ # self.version = ::Isomorfeus::Component::VERSION # thats equal to the isomorfeus version
83
+ end
84
+ end
@@ -0,0 +1,48 @@
1
+ module Isomorfeus
2
+ class TopLevel
3
+ def self.search_path
4
+ @search_path ||= [Object]
5
+ end
6
+
7
+ def self.on_ready_mount(component, params = nil, element_query = nil)
8
+ # init in case it hasn't been run yet
9
+ Isomorfeus.init
10
+ # this looks a bit odd but works across _all_ browsers, and no event handler mess
11
+ # TODO: server rendering
12
+ %x{
13
+ function do_the_mount() { #{mount(component, params, element_query)} };
14
+ function ready_fun() {
15
+ /in/.test(document.readyState) ? setTimeout(ready_fun,5) : do_the_mount();
16
+ };
17
+ ready_fun();
18
+ }
19
+ end
20
+
21
+ def self.mount(component, params = nil, element_or_query = nil)
22
+ # TODO: server rendering
23
+ if element_or_query.nil?
24
+ if params.nil?
25
+ element = `document.body.querySelector('div')`
26
+ elsif params.class == String
27
+ element = `document.body.querySelector(params)`
28
+ params = nil
29
+ elsif params.is_a?(Browser::DOM::Node)
30
+ element = params.to_n
31
+ params = nil
32
+ end
33
+ else
34
+ if element_or_query.class == String
35
+ element = `document.body.querySelector(params)`
36
+ elsif element_or_query.is_a?(Browser::DOM::Node)
37
+ element = params.to_n
38
+ end
39
+ end
40
+
41
+ ReactDOM.render(React.create_element(component, params), element)
42
+ end
43
+
44
+ def self.ujs_mount
45
+ # TODO: implement mount using RailsUJS, for turbolinks and things
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,38 @@
1
+ module Isomorfeus
2
+ module ViewHelpers
3
+ def isomorfeus_script_tag(options = {})
4
+ # client side used options:
5
+ # current_user_id
6
+ # session_id
7
+ # form_authenticity_token
8
+ options_hash = Isomorfeus.options_hash_for_client
9
+ options_hash.merge!(options)
10
+ tag = <<~SCRIPT
11
+ <script type="text/javascript">
12
+ Opal.IsomorfeusOptions = #{options_hash.to_json};
13
+ Opal.Isomorfeus.$init();
14
+ </script>
15
+ SCRIPT
16
+ tag.respond_to?(:html_safe) ? tag.html_safe : tag
17
+ end
18
+
19
+ def react_component(component_name, params)
20
+ component_name_id = component_id_name(component_name)
21
+ tag = <<~SCRIPT
22
+ <div id="#{component_name_id}"></div>
23
+ <script type="text/javascript">
24
+ var component = Opal.Object.$const_get("#{component_name}");
25
+ var json_params = #{Oj.dump(params, mode: :compat)};
26
+ Opal.Isomorfeus.$const_get('TopLevel').$mount(component, Opal.Hash.$new(json_params), "##{component_name_id}" );
27
+ </script>
28
+ SCRIPT
29
+ tag.respond_to?(:html_safe) ? tag.html_safe : tag
30
+ end
31
+
32
+ private
33
+
34
+ def component_id_name(component_name)
35
+ "#{component_name.underscore}_#{Random.rand.to_s[2..-1]}"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ module LucidApp
2
+ module API
3
+ def self.included(base)
4
+ base.instance_exec do
5
+ def render(&block)
6
+ %x{
7
+ self.react_component.prototype.render = function() {
8
+ Opal.React.render_buffer.push([]);
9
+ Opal.React.active_components.push(this);
10
+ Opal.React.active_redux_components.push(this.__ruby_instance);
11
+ #{`this.__ruby_instance`.instance_exec(&block)};
12
+ Opal.React.active_redux_components.pop();
13
+ Opal.React.active_components.pop();
14
+ var children = Opal.React.render_buffer.pop();
15
+ return React.createElement(LucidApplicationContext.Provider, { value: this.state.isomorfeus_store_state }, children);
16
+ }
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module LucidApp
2
+ class Base
3
+ def self.inherited(base)
4
+ base.include(::LucidApp::Mixin)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module LucidApp
2
+ module Context
3
+ def self.create_application_context
4
+ React.create_context('LucidApplicationContext', Isomorfeus.store)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ module LucidApp
2
+ module Mixin
3
+ def self.included(base)
4
+ base.include(::Native::Wrapper)
5
+ base.extend(::LucidApp::NativeComponentConstructor)
6
+ base.extend(::React::Component::NativeComponentValidateProp)
7
+ base.extend(::React::Component::ShouldComponentUpdate)
8
+ base.extend(::React::Component::EventHandler)
9
+ base.include(::React::Component::Elements)
10
+ base.include(::React::Component::API)
11
+ base.include(::React::ReduxComponent::API)
12
+ base.include(::LucidApp::API)
13
+ base.include(::React::Component::Features)
14
+ base.include(::React::Component::Resolution)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,70 @@
1
+ module LucidApp
2
+ module NativeComponentConstructor
3
+ # for should_component_update we apply ruby semantics for comparing props
4
+ # to do so, we convert the props to ruby hashes and then compare
5
+ # this makes sure, that for example rubys Nil object gets handled properly
6
+ def self.extended(base)
7
+ component_name = base.to_s
8
+ # language=JS
9
+ %x{
10
+ base.react_component = class extends React.Component {
11
+ constructor(props) {
12
+ super(props);
13
+ if (base.$default_state_defined()) {
14
+ this.state = base.$state().$to_n();
15
+ } else {
16
+ this.state = {};
17
+ };
18
+ this.state.isomorfeus_store_state = Opal.Isomorfeus.store.native.getState();
19
+ var current_store_state = this.state.isomorfeus_store_state;
20
+ if (typeof current_store_state.component_class_state[#{component_name}] !== "undefined") {
21
+ this.state.component_class_state = {};
22
+ this.state.component_class_state[#{component_name}] = current_store_state.component_class_state[#{component_name}];
23
+ } else {
24
+ this.state.component_class_state = {};
25
+ this.state.component_class_state[#{component_name}] = {};
26
+ };
27
+ this.__ruby_instance = base.$new(this);
28
+ this.__object_id = this.__ruby_instance.$object_id().$to_s();
29
+ if (!this.state.component_state) {
30
+ this.state.component_state = {};
31
+ this.state.component_state[this.__object_id] = {};
32
+ };
33
+ var event_handlers = #{base.event_handlers};
34
+ for (var i = 0; i < event_handlers.length; i++) {
35
+ this[event_handlers[i]] = this[event_handlers[i]].bind(this);
36
+ }
37
+ var defined_refs = #{base.defined_refs};
38
+ for (var ref in defined_refs) {
39
+ if (defined_refs[ref] != null) {
40
+ this[ref] = function(element) {
41
+ #{`this.__ruby_instance`.instance_exec(React::Ref.new(`element`), `defined_refs[ref]`)}
42
+ }
43
+ this[ref] = this[ref].bind(this);
44
+ } else {
45
+ this[ref] = React.createRef();
46
+ }
47
+ }
48
+ this.listener = this.listener.bind(this);
49
+ this.unsubscriber = Opal.Isomorfeus.store.native.subscribe(this.listener);
50
+ }
51
+ static get displayName() {
52
+ return #{component_name};
53
+ }
54
+ listener() {
55
+ var next_state = Opal.Isomorfeus.store.native.getState();
56
+ var current_ruby_state = Opal.Hash.$new(this.state.isomorfeus_store_state);
57
+ var next_ruby_state = Opal.Hash.$new(next_state);
58
+ if (#{`next_ruby_state` != `current_ruby_state`}) {
59
+ this.setState({isomorfeus_store_state: next_state});
60
+ }
61
+ }
62
+ componentWillUnmount() {
63
+ if (typeof this.unsubscriber === "function") { this.unsubscriber(); };
64
+ }
65
+ }
66
+ }
67
+ end
68
+ end
69
+ end
70
+