amber_component 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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>