amber_component 0.0.2 → 0.0.4

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -1
  5. data/.solargraph.yml +1 -2
  6. data/CONTRIBUTING.md +87 -0
  7. data/Gemfile +4 -1
  8. data/Gemfile.lock +24 -99
  9. data/README.md +25 -42
  10. data/Rakefile +17 -1
  11. data/amber_component.gemspec +4 -2
  12. data/banner.png +0 -0
  13. data/docs/.bundle/config +2 -0
  14. data/docs/.gitignore +5 -0
  15. data/docs/404.html +25 -0
  16. data/docs/Gemfile +37 -0
  17. data/docs/Gemfile.lock +89 -0
  18. data/docs/README.md +19 -0
  19. data/docs/_config.yml +148 -0
  20. data/docs/_data/amber_component.yml +3 -0
  21. data/docs/_sass/_variables.scss +2 -0
  22. data/docs/_sass/color_schemes/amber_component.scss +11 -0
  23. data/docs/_sass/custom/custom.scss +60 -0
  24. data/docs/api/index.md +8 -0
  25. data/docs/assets/images/logo_wide.png +0 -0
  26. data/docs/changelog/index.md +8 -0
  27. data/docs/favicon.ico +0 -0
  28. data/docs/getting_started/index.md +8 -0
  29. data/docs/getting_started/installation.md +7 -0
  30. data/docs/getting_started/ruby_support.md +7 -0
  31. data/docs/getting_started/wireframes.md +7 -0
  32. data/docs/index.md +17 -0
  33. data/docs/introduction/basic_usage.md +7 -0
  34. data/docs/introduction/index.md +8 -0
  35. data/docs/introduction/why_amber_component.md +7 -0
  36. data/docs/resources/index.md +8 -0
  37. data/docs/styles/index.md +8 -0
  38. data/docs/styles/usage.md +7 -0
  39. data/docs/views/index.md +8 -0
  40. data/docs/views/usage.md +7 -0
  41. data/icon.png +0 -0
  42. data/lib/amber_component/assets.rb +59 -0
  43. data/lib/amber_component/base.rb +85 -434
  44. data/lib/amber_component/helpers/class_helper.rb +22 -0
  45. data/lib/amber_component/helpers/component_helper.rb +19 -0
  46. data/lib/amber_component/helpers/css_helper.rb +25 -0
  47. data/lib/amber_component/helpers.rb +11 -0
  48. data/lib/amber_component/prop_definition.rb +54 -0
  49. data/lib/amber_component/props.rb +111 -0
  50. data/lib/amber_component/railtie.rb +21 -0
  51. data/lib/amber_component/rendering.rb +53 -0
  52. data/lib/amber_component/template_handler/erb.rb +17 -0
  53. data/lib/amber_component/template_handler.rb +26 -0
  54. data/lib/amber_component/typed_content.rb +3 -3
  55. data/lib/amber_component/version.rb +1 -1
  56. data/lib/amber_component/views.rb +198 -0
  57. data/lib/amber_component.rb +14 -11
  58. data/lib/generators/amber_component/install_generator.rb +23 -0
  59. data/lib/generators/amber_component/templates/application_component.rb +13 -0
  60. data/lib/generators/amber_component_generator.rb +24 -0
  61. data/lib/generators/templates/component.rb.erb +9 -0
  62. data/lib/generators/templates/component_test.rb.erb +9 -0
  63. data/lib/generators/templates/style.css.erb +3 -0
  64. data/lib/generators/templates/view.html.erb +3 -0
  65. metadata +87 -19
  66. data/.kanbn/index.md +0 -23
  67. data/.kanbn/tasks/add-instance-variables-to-view-when-block-given-markdown.md +0 -9
  68. data/.kanbn/tasks/bind-clas-to-action-view-method-call-example-component-data-data-without-calling-any-method.md +0 -9
  69. data/.kanbn/tasks/bind-scoped-css-to-head-of-doc.md +0 -10
  70. data/.kanbn/tasks/check-if-we-need-full-rails-gem-pack.md +0 -9
  71. data/.kanbn/tasks/simple-proto.md +0 -10
  72. data/.kanbn/tasks/verify-if-template-is-haml-or-erb.md +0 -9
  73. data/PLANS.md +0 -17
  74. data/lib/amber_component/helper.rb +0 -8
  75. data/lib/amber_component/style_injector.rb +0 -52
  76. data/sig/amber_components.rbs +0 -4
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'prop_definition'
4
+
5
+ module ::AmberComponent
6
+ # Provides a DSL for defining component
7
+ # properties.
8
+ module Props
9
+ # Class methods for component properties.
10
+ module ClassMethods
11
+ # @return [Hash{Symbol => AmberComponent::Prop}]
12
+ attr_reader :prop_definitions
13
+
14
+ # @param names [Array<Symbol>]
15
+ # @param type [Class, nil]
16
+ # @param required [Boolean]
17
+ # @param default [Object, Proc, nil]
18
+ # @param allow_nil [Boolean]
19
+ def prop(*names, type: nil, required: false, default: nil, allow_nil: false)
20
+ @prop_definitions ||= {}
21
+ include(@prop_methods_module = ::Module.new) if @prop_methods_module.nil?
22
+
23
+ names.each do |name|
24
+ @prop_definitions[name] = prop_def = PropDefinition.new(
25
+ name: name,
26
+ type: type,
27
+ required: required,
28
+ default: default,
29
+ allow_nil: allow_nil
30
+ )
31
+ raise IncorrectPropTypeError, <<~MSG unless type.nil? || type.is_a?(::Class)
32
+ `type` should be a class but received `#{type.inspect}` (`#{type.class}`)
33
+ MSG
34
+
35
+ @prop_methods_module.attr_reader name
36
+ next @prop_methods_module.attr_writer(name) unless prop_def.type?
37
+
38
+ @prop_methods_module.class_eval( # rubocop:disable Style/DocumentDynamicEvalDefinition
39
+ # def phone=(val)
40
+ # raise IncorrectPropTypeError, <<~MSG unless val.nil? || val.is_a?(String)
41
+ # #{self.class} received `#{val.class}` instead of `String` for `phone` prop
42
+ # MSG
43
+ #
44
+ # @phone = val
45
+ # end
46
+ <<~RUB, __FILE__, __LINE__ + 1
47
+ def #{name}=(val)
48
+ raise IncorrectPropTypeError, <<~MSG unless #{allow_nil ? 'val.nil? ||' : nil} val.is_a?(#{prop_def.type})
49
+ \#{self.class} received `\#{val.class}` instead of `#{prop_def.type}` for `#{name}` prop
50
+ MSG
51
+
52
+ @#{name} = val
53
+ end
54
+ RUB
55
+ ) # rubocop:disable Layout/HeredocArgumentClosingParenthesis
56
+ end
57
+ end
58
+
59
+ # @return [Array<Symbol>, nil]
60
+ def prop_names
61
+ @prop_definitions.keys
62
+ end
63
+
64
+ # @return [Array<Symbol>, nil]
65
+ def required_prop_names
66
+ @prop_definitions&.filter_map do |name, prop_def|
67
+ next unless prop_def.required
68
+
69
+ name
70
+ end
71
+ end
72
+ end
73
+
74
+ # Instance methods for component properties.
75
+ module InstanceMethods
76
+ private
77
+
78
+ # @param props [Hash{Symbol => Object}]
79
+ def initialize(**kwargs)
80
+ bind_props(kwargs)
81
+ end
82
+
83
+ # @param props [Hash{Symbol => Object}]
84
+ # @return [Boolean] `false` when there are no props defined on the class
85
+ # and `true` otherwise
86
+ # @raise [AmberComponent::MissingPropsError] when required props are missing
87
+ # @raise [AmberComponent::IncorrectPropTypeError]
88
+ def bind_props(props)
89
+ return false if self.class.prop_definitions.nil?
90
+
91
+ self.class.prop_definitions.each do |name, prop_def|
92
+ setter_name = :"#{name}="
93
+ public_send(setter_name, prop_def.default!) if prop_def.default?
94
+
95
+ prop_present = props.include? name
96
+
97
+ raise MissingPropsError, <<~MSG if prop_def.required? && !prop_present
98
+ `#{self.class}` has a missing required prop: `#{name.inspect}`
99
+ MSG
100
+
101
+ next unless prop_present
102
+
103
+ value = props[name]
104
+ public_send(setter_name, value)
105
+ end
106
+
107
+ true
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ::AmberComponent
4
+ # Class which hooks into Rails
5
+ # and configures the application.
6
+ class Railtie < ::Rails::Railtie
7
+ initializer 'amber_component.initialization' do |app|
8
+ app.config.assets.paths << (app.root / 'app' / 'components')
9
+
10
+ next if ::Rails.env.production?
11
+
12
+ components_root = app.root / 'app' / 'components'
13
+ component_paths = ::Dir[components_root / '**' / '*.rb']
14
+ app.config.eager_load_paths += component_paths
15
+
16
+ ::ActiveSupport::Reloader.to_prepare do
17
+ component_paths.each { |file| require_dependency(components_root / file) }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ::AmberComponent
4
+ # Provides universal methods for rendering components.
5
+ module Rendering
6
+ # Class methods for rendering.
7
+ module ClassMethods
8
+ # @param kwargs [Hash{Symbol => Object}]
9
+ # @return [String]
10
+ def render(**kwargs, &block)
11
+ comp = new(**kwargs)
12
+
13
+ comp.render(&block)
14
+ end
15
+
16
+ alias call render
17
+ end
18
+
19
+ # Instance methods for rendering.
20
+ module InstanceMethods
21
+ # @return [String]
22
+ def render(&block)
23
+ run_callbacks :render do
24
+ element = render_view(&block)
25
+ # styles = inject_styles
26
+ # element += styles unless styles.nil?
27
+ element.html_safe
28
+ end
29
+ end
30
+
31
+ # Method used internally by Rails to render an object
32
+ # passed to the `render` method.
33
+ #
34
+ # render MyComponent.new(some: :attribute)
35
+ #
36
+ # @param _context [ActionView::Base]
37
+ # @return [String]
38
+ def render_in(_context)
39
+ render
40
+ end
41
+
42
+ protected
43
+
44
+ # @param content [String]
45
+ # @param type [Symbol]
46
+ # @param block [Proc, nil]
47
+ # @return [String]
48
+ def render_string(content, type, block = nil)
49
+ TemplateHandler.render_from_string(self, content, type, block)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_view'
4
+ require 'ostruct'
5
+
6
+ module ::AmberComponent
7
+ module TemplateHandler
8
+ # Handles rendering ERB with Rails-like syntax
9
+ class ERB < ::ActionView::Template::Handlers::ERB::Erubi
10
+ def initialize(input, properties = {})
11
+ properties[:bufvar] ||= "@output_buffer"
12
+ properties[:preamble] = "#{properties[:bufvar]}=#{::ActionView::OutputBuffer}.new;"
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ::AmberComponent
4
+ # Provides code which handles rendering different
5
+ # template languages.
6
+ module TemplateHandler
7
+ class << self
8
+ # @param context [AmberComponent::Base]
9
+ # @param content [String]
10
+ # @param type [Symbol, String]
11
+ # @param block [Proc, nil]
12
+ # @return [String]
13
+ def render_from_string(context, content, type, block = nil)
14
+ options = if type.to_sym == :erb
15
+ { engine_class: ERB }
16
+ else
17
+ {}
18
+ end
19
+
20
+ ::Tilt[type].new(options) { content }.render(context, &block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ require_relative 'template_handler/erb'
@@ -9,9 +9,9 @@ module ::AmberComponent
9
9
  def wrap(val)
10
10
  return val if val.is_a?(self)
11
11
 
12
- unless val.respond_to?(:[])
13
- raise InvalidType, "`TypedContent` should be a `Hash` or `#{self}` but was `#{val.class}` (#{val.inspect})"
14
- end
12
+ raise InvalidTypeError, <<~MSG unless val.respond_to?(:[])
13
+ `TypedContent` should be a `Hash` or `#{self}` but was `#{val.class}` (#{val.inspect})
14
+ MSG
15
15
 
16
16
  new(type: val[:type], content: val[:content])
17
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ::AmberComponent
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.4'
5
5
  end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ::AmberComponent
4
+ # Provides methods concerning view registering and rendering.
5
+ module Views
6
+ # View types with built-in embedded Ruby
7
+ #
8
+ # @return [Set<Symbol>]
9
+ VIEW_TYPES_WITH_RUBY = ::Set[:erb, :haml, :slim].freeze
10
+ # @return [Set<Symbol>]
11
+ ALLOWED_VIEW_TYPES = ::Set[:erb, :haml, :slim, :html, :md, :markdown].freeze
12
+ # @return [Regexp]
13
+ VIEW_FILE_REGEXP = /^view\./.freeze
14
+
15
+ # Class methods for views.
16
+ module ClassMethods
17
+ # Register an inline view by returning a String from the passed block.
18
+ #
19
+ # Usage:
20
+ #
21
+ # view do
22
+ # <<~ERB
23
+ # <h1>
24
+ # Hello <%= @name %>
25
+ # </h1>
26
+ # ERB
27
+ # end
28
+ #
29
+ # or:
30
+ #
31
+ # view :haml do
32
+ # <<~HAML
33
+ # %h1
34
+ # Hello
35
+ # = @name
36
+ # HAML
37
+ # end
38
+ #
39
+ # @param type [Symbol]
40
+ # @return [void]
41
+ def view(type = :erb, &block)
42
+ @method_view = TypedContent.new(type: type, content: block)
43
+ end
44
+
45
+ # ERB/Haml/Slim view registered through the `view` method.
46
+ #
47
+ # @return [TypedContent]
48
+ attr_reader :method_view
49
+
50
+ # @return [String]
51
+ def view_path
52
+ asset_path view_file_name
53
+ end
54
+
55
+ # @return [String, nil]
56
+ def view_file_name
57
+ files = asset_file_names(VIEW_FILE_REGEXP)
58
+ raise MultipleViewsError, "More than one view file for `#{name}` found!" if files.length > 1
59
+
60
+ files.first
61
+ end
62
+
63
+ # @return [Symbol]
64
+ def view_type
65
+ (view_file_name.split('.')[1..].grep_v(/erb/).last || 'erb')&.to_sym
66
+ end
67
+ end
68
+
69
+ # Instance methods for views.
70
+ module InstanceMethods
71
+ protected
72
+
73
+ # @return [String]
74
+ def render_view(&block)
75
+ view_from_file = render_view_from_file(&block)
76
+ view_from_method = render_class_method_view(&block)
77
+ view_from_inline = render_view_from_inline(&block)
78
+
79
+ view_content = view_from_file unless view_from_file.empty?
80
+ view_content = view_from_method unless view_from_method.empty?
81
+ view_content = view_from_inline unless view_from_inline.empty?
82
+
83
+ if view_content.nil? || view_content.empty?
84
+ raise ViewFileNotFoundError, "View for `#{self.class}` could not be found!"
85
+ end
86
+
87
+ view_content
88
+ end
89
+
90
+ # Helper method to render view from string or with other provided type.
91
+ #
92
+ # Usage:
93
+ #
94
+ # render_view_from_content('<h1>Hello World</h1>')
95
+ #
96
+ # or:
97
+ #
98
+ # render_view_from_content content: '**Hello World**', type: 'md'
99
+ #
100
+ # @param content [TypedContent, Hash{Symbol => String, Symbol, Proc}, String]
101
+ # @return [String, nil]
102
+ def render_view_from_content(content, &block)
103
+ return '' unless content
104
+ return content if content.is_a?(::String)
105
+
106
+ content = TypedContent.wrap(content)
107
+ type = content.type
108
+ content = content.to_s
109
+
110
+ if content.empty?
111
+ raise EmptyViewError, <<~ERR.squish
112
+ Custom view for `#{self.class}` from view method cannot be empty!
113
+ ERR
114
+ end
115
+
116
+ unless ALLOWED_VIEW_TYPES.include? type
117
+ raise UnknownViewTypeError, <<~ERR.squish
118
+ Unknown view type for `#{self.class}` from view method!
119
+ Check return value of param type in `view :[type] do`
120
+ ERR
121
+ end
122
+
123
+ unless VIEW_TYPES_WITH_RUBY.include? type
124
+ # first render the content with ERB if the
125
+ # type does not support embedding Ruby by default
126
+ content = render_string(content, :erb, block)
127
+ end
128
+
129
+ render_string(content, type, block)
130
+ end
131
+
132
+ # @return [String]
133
+ def render_view_from_file(&block)
134
+ view_path = self.class.view_path
135
+ return '' if view_path.nil? || !::File.file?(view_path)
136
+
137
+ content = ::File.read(view_path)
138
+ type = self.class.view_type
139
+
140
+ unless VIEW_TYPES_WITH_RUBY.include? type
141
+ content = render_string(content, :erb, block)
142
+ end
143
+
144
+ render_string(content, type, block)
145
+ end
146
+
147
+ # Method returning view from method in class file.
148
+ # Usage:
149
+ #
150
+ # view do
151
+ # <<~HTML
152
+ # <h1>
153
+ # Hello <%= @name %>
154
+ # </h1>
155
+ # HTML
156
+ # end
157
+ #
158
+ # or:
159
+ #
160
+ # view :haml do
161
+ # <<~HAML
162
+ # %h1
163
+ # Hello
164
+ # = @name
165
+ # HAML
166
+ # end
167
+ #
168
+ # @return [String]
169
+ def render_class_method_view(&block)
170
+ render_view_from_content(self.class.method_view, &block)
171
+ end
172
+
173
+ # Method returning view from params in view.
174
+ # Usage:
175
+ #
176
+ # <%= ExampleComponent data: data, view: "<h1>Hello #{@name}</h1>" %>
177
+ #
178
+ # or:
179
+ #
180
+ # <%= ExampleComponent data: data, view: { content: "<h1>Hello #{@name}</h1>", type: 'erb' } %>
181
+ #
182
+ # @return [String]
183
+ def render_view_from_inline(&block)
184
+ data = \
185
+ if @view.is_a? ::String
186
+ TypedContent.new(
187
+ type: :erb,
188
+ content: @view
189
+ )
190
+ else
191
+ @view
192
+ end
193
+
194
+ render_view_from_content(data, &block)
195
+ end
196
+ end
197
+ end
198
+ end
@@ -1,24 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rails'
4
3
  require 'active_support'
5
4
  require 'active_support/core_ext'
6
5
 
7
6
  module ::AmberComponent
8
7
  class Error < ::StandardError; end
9
- class ViewFileNotFound < Error; end
10
- class InvalidType < Error; end
8
+ class MissingPropsError < Error; end
9
+ class IncorrectPropTypeError < Error; end
10
+ class ViewFileNotFoundError < Error; end
11
+ class InvalidTypeError < Error; end
11
12
 
12
- class EmptyView < Error; end
13
- class UnknownViewType < Error; end
14
- class MultipleViews < Error; end
15
-
16
- class EmptyStyle < Error; end
17
- class UnknownStyleType < Error; end
18
- class MultipleStyles < Error; end
13
+ class EmptyViewError < Error; end
14
+ class UnknownViewTypeError < Error; end
15
+ class MultipleViewsError < Error; end
19
16
  end
20
17
 
21
18
  require_relative 'amber_component/version'
22
- require_relative 'amber_component/helper'
19
+ require_relative 'amber_component/helpers'
23
20
  require_relative 'amber_component/typed_content'
21
+ require_relative 'amber_component/template_handler'
22
+ require_relative 'amber_component/views'
23
+ require_relative 'amber_component/assets'
24
+ require_relative 'amber_component/rendering'
25
+ require_relative 'amber_component/props'
24
26
  require_relative 'amber_component/base'
27
+ require_relative 'amber_component/railtie' if defined?(::Rails::Railtie)
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module ::AmberComponent
6
+ module Generators
7
+ # A Rails generator which installs the `amber_component`
8
+ # library in a Rails project.
9
+ class InstallGenerator < ::Rails::Generators::Base
10
+ desc 'Install the AmberComponent gem'
11
+ source_root ::File.expand_path('templates', __dir__)
12
+
13
+ # copy rake tasks
14
+ def copy_tasks
15
+ copy_file 'application_component.rb', 'app/components/application_component.rb'
16
+
17
+ inject_into_file 'app/assets/stylesheets/application.css', after: "*= require_tree .\n" do
18
+ " *= require_tree ./../../components\n"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Abstract class which should serve as a superclass
4
+ # for all your custom components in this app.
5
+ #
6
+ # @abstract Subclass to create a new component.
7
+ class ::ApplicationComponent < ::AmberComponent::Base
8
+ # Include your global application helper.
9
+ include ::ApplicationHelper
10
+ # Include the helper methods for your application's
11
+ # routes.
12
+ include ::Rails.application.routes.url_helpers
13
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ # A Rails generator which creates a new Amber component.
6
+ class AmberComponentGenerator < ::Rails::Generators::NamedBase
7
+ desc 'Generate a new component'
8
+ source_root ::File.expand_path('templates', __dir__)
9
+
10
+ # copy rake tasks
11
+ def copy_tasks
12
+ template 'component.rb.erb', "app/components/#{file_path}.rb"
13
+ template 'component_test.rb.erb', "test/components/#{file_path}_test.rb"
14
+ template 'view.html.erb', "app/components/#{file_path}/view.html.erb"
15
+ template 'style.css.erb', "app/components/#{file_path}/style.css"
16
+ end
17
+
18
+ def file_name
19
+ name = super
20
+ return name if name.end_with? '_component'
21
+
22
+ "#{name}_component"
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %> < ::ApplicationComponent
4
+ # Your code goes here
5
+
6
+ after_initialize do
7
+ @time = ::Time.now
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class <%= class_name %>Test < ::ActiveSupport::TestCase
6
+ # test 'the truth' do
7
+ # assert true
8
+ # end
9
+ end
@@ -0,0 +1,3 @@
1
+ .<%= singular_table_name %> {
2
+ color: blue;
3
+ }
@@ -0,0 +1,3 @@
1
+ <h2 class='<%= singular_table_name %>'>
2
+ Hello from <b><%= class_name %></b>, initialized at: <%%= @time %>
3
+ </h2>