lib/amber_component/base.rb in amber_component-0.0.2 vs lib/amber_component/base.rb in amber_component-0.0.3
- old
+ new
@@ -1,230 +1,106 @@
# frozen_string_literal: true
require 'set'
require 'erb'
require 'tilt'
-require 'active_model/callbacks'
require 'memery'
+require 'active_model/callbacks'
+require 'action_view'
-require_relative './style_injector'
-
-# Abstract class which serves as a base
-# for all Amber Components.
-#
-# There are a few life cycle callbacks that can be defined.
-# The same way as in `ActiveRecord` models and `ActionController` controllers.
-#
-# - before_render
-# - around_render
-# - after_render
-# - before_initialize
-# - around_initialize
-# - after_initialize
-#
-# class ButtonComponent < ::AmberComponent::Base
-# # You can provide a Symbol of the method that should be called
-# before_render :before_render_method
-# # Or provide a block that will be executed
-# after_initialize do
-# # Your code here
-# end
-#
-# def before_render_method
-# # Your code here
-# end
-# end
-#
-#
-# @abstract Create a subclass to define a new component.
module ::AmberComponent
- class Base # :nodoc:
+ # Abstract class which serves as a base
+ # for all Amber Components.
+ #
+ # There are a few life cycle callbacks that can be defined.
+ # The same way as in `ActiveRecord` models and `ActionController` controllers.
+ #
+ # - before_render
+ # - around_render
+ # - after_render
+ # - before_initialize
+ # - around_initialize
+ # - after_initialize
+ #
+ # class ButtonComponent < ::AmberComponent::Base
+ # # You can provide a Symbol of the method that should be called
+ # before_render :before_render_method
+ # # Or provide a block that will be executed
+ # after_initialize do
+ # # Your code here
+ # end
+ #
+ # def before_render_method
+ # # Your code here
+ # end
+ # end
+ #
+ #
+ # @abstract Create a subclass to define a new component.
+ class Base < ::ActionView::Base
+ # for defining callback such as `after_initialize`
extend ::ActiveModel::Callbacks
+ extend Helpers::ClassHelper
- include Helper
+ include Helpers::CssHelper
+ include Views::InstanceMethods
+ extend Views::ClassMethods
+ include Assets::InstanceMethods
+ extend Assets::ClassMethods
+ include Rendering::InstanceMethods
+ extend Rendering::ClassMethods
- # @return [Regexp]
- VIEW_FILE_REGEXP = /^view\./.freeze
- # @return [Regexp]
- STYLE_FILE_REGEXP = /^style\./.freeze
-
- # View types with built-in embedded Ruby
- #
- # @return [Set<Symbol>]
- VIEW_TYPES_WITH_RUBY = ::Set[:erb, :haml, :slim].freeze
- # @return [Set<Symbol>]
- ALLOWED_VIEW_TYPES = ::Set[:erb, :haml, :slim, :html, :md, :markdown].freeze
- # @return [Set<Symbol>]
- ALLOWED_STYLE_TYPES = ::Set[:sass, :scss, :less].freeze
-
class << self
include ::Memery
- # @param kwargs [Hash{Symbol => Object}]
- # @return [String]
- def run(**kwargs, &block)
- comp = new(**kwargs)
+ memoize :asset_dir_path
- comp.render(&block)
- end
-
- alias call run
-
-
- # @return [String]
- memoize def asset_dir_path
- class_const_name = name.split('::').last
- component_file_path, = module_parent.const_source_location(class_const_name)
-
- component_file_path.delete_suffix('.rb')
- end
-
- # @return [String]
- def view_path
- asset_path view_file_name
- end
-
- # @return [String, nil]
- def view_file_name
- files = asset_file_names(VIEW_FILE_REGEXP)
- raise MultipleViews, "More than one view file for `#{name}` found!" if files.length > 1
-
- files.first
- end
-
- # @return [Symbol]
- def view_type
- (view_file_name.split('.')[1..].reject { _1.match?(/erb/) }.last || 'erb')&.to_sym
- end
-
- # @return [String]
- def style_path
- asset_path style_file_name
- end
-
- # @return [String, nil]
- def style_file_name
- files = asset_file_names(STYLE_FILE_REGEXP)
- raise MultipleStyles, "More than one style file for `#{name}` found!" if files.length > 1
-
- files.first
- end
-
- # @return [Symbol]
- def style_type
- (style_file_name.split('.')[1..].reject { _1.match?(/erb/) }.last || 'erb')&.to_sym
- end
-
# Memoize these methods in production
- if defined?(::Rails) && ::Rails.env.production?
+ if ::ENV['RAILS_ENV'] == 'production'
memoize :view_path
memoize :view_file_name
memoize :view_type
-
- memoize :style_path
- memoize :style_file_name
- memoize :style_type
end
- # Register an inline view by returning a String from the passed block.
- #
- # Usage:
- #
- # view do
- # <<~ERB
- # <h1>
- # Hello <%= @name %>
- # </h1>
- # ERB
- # end
- #
- # or:
- #
- # view :haml do
- # <<~HAML
- # %h1
- # Hello
- # = @name
- # HAML
- # end
- #
- # @param type [Symbol]
- # @return [void]
- def view(type = :erb, &block)
- @method_view = TypedContent.new(type: type, content: block)
- end
-
- # ERB/Haml/Slim view registered through the `view` method.
- #
- # @return [TypedContent]
- attr_reader :method_view
-
- # Register an inline style by returning a String from the passed block.
- #
- # Usage:
- #
- # style do
- # '.my-class { color: red; }'
- # end
- #
- # or:
- #
- # style :sass do
- # <<~SASS
- # .my-class
- # color: red
- # SASS
- # end
- #
- # @param type [Symbol]
- # @return [void]
- def style(type = :css, &block)
- @method_style = TypedContent.new(type: type, content: block)
- end
-
- # CSS/SCSS/Sass styles registered through the `style` method.
- #
- # @return [TypedContent]
- attr_reader :method_style
-
private
# @param subclass [Class]
# @return [void]
def inherited(subclass)
+ super
# @type [Module]
- parent_module = subclass.module_parent
method_body = proc do |**kwargs, &block|
- subclass.run(**kwargs, &block)
+ subclass.render(**kwargs, &block)
end
+ parent_module = subclass.module_parent
- Helper.define_method(subclass.name, &method_body) && return if parent_module.equal? ::Object
+ if parent_module.equal?(::Object)
+ method_name = subclass.name
+ define_helper_method(subclass, Helpers::ComponentHelper, method_name, method_body)
+ define_helper_method(subclass, Helpers::ComponentHelper, method_name.underscore, method_body)
+ return
+ end
- parent_module.define_singleton_method(subclass.name.split('::').last, &method_body)
+ method_name = subclass.const_name
+ define_helper_method(subclass, parent_module.singleton_class, method_name, method_body)
+ define_helper_method(subclass, parent_module.singleton_class, method_name.underscore, method_body)
end
- # @param file_name [String, nil]
- # @return [String, nil]
- def asset_path(file_name)
- return unless file_name
+ # @param component [Class]
+ # @param mod [Module, Class]
+ # @param method_name [String, Symbol]
+ # @param body [Proc]
+ def define_helper_method(component, mod, method_name, body)
+ mod.define_method(method_name, &body)
- ::File.join(asset_dir_path, file_name)
- end
+ return if ::ENV['RAILS_ENV'] == 'production'
- # Returns the name of the file inside the asset directory
- # of this component that matches the provided `Regexp`
- #
- # @param type_regexp [Regexp]
- # @return [Array<String>]
- def asset_file_names(type_regexp)
- return [] unless ::File.directory?(asset_dir_path)
-
- ::Dir.entries(asset_dir_path).select do |file|
- next unless ::File.file?(::File.join(asset_dir_path, file))
-
- file.match? type_regexp
- end
+ ::Warning.warn <<~WARN if mod.instance_methods.include?(method_name)
+ #{caller(0, 1).first}: warning:
+ `#{component}` shadows the name of an already existing `#{mod}` method `#{method_name}`.
+ Consider renaming this component, because the original method will be overwritten.
+ WARN
end
end
define_model_callbacks :initialize, :render
@@ -233,260 +109,16 @@
run_callbacks :initialize do
bind_variables(kwargs)
end
end
- # @return [String]
- def render(&block)
- run_callbacks :render do
- element = inject_views(&block)
- styles = inject_styles
- element += styles unless styles.nil?
- element.html_safe
- end
- end
-
- def render_in(context)
- byebug
- end
-
private
# @param kwargs [Hash{Symbol => Object}]
# @return [void]
def bind_variables(kwargs)
kwargs.each do |key, value|
instance_variable_set("@#{key}", value)
end
- end
-
- # Helper method to render view from string or with other provided type.
- #
- # Usage:
- #
- # render_custom_view('<h1>Hello World</h1>')
- #
- # or:
- #
- # render_custom_view content: '**Hello World**', type: 'md'
- #
- # @param style [TypedContent, Hash{Symbol => String, Symbol, Proc}, String]
- # @return [String, nil]
- def render_custom_view(view, &block)
- return '' unless view
- return view if view.is_a?(::String)
-
- view = TypedContent.wrap(view)
- type = view.type
- content = view.to_s
-
- if content.empty?
- raise EmptyView, <<~ERR.squish
- Custom view for `#{self.class}` from view method cannot be empty!
- ERR
- end
-
- unless ALLOWED_VIEW_TYPES.include? type
- raise UnknownViewType, <<~ERR.squish
- Unknown view type for `#{self.class}` from view method!
- Check return value of param type in `view :[type] do`
- ERR
- end
-
- unless VIEW_TYPES_WITH_RUBY.include? type
- # first render the content with ERB if the
- # type does not support embedding Ruby by default
- content = render_string(content, :erb, block)
- end
-
- render_string(content, type, block)
- end
-
- # @return [String]
- def render_view_from_file(&block)
- view_path = self.class.view_path
- return '' if view_path.nil? || !::File.file?(view_path)
-
- content = ::File.read(view_path)
- type = self.class.view_type
-
- unless VIEW_TYPES_WITH_RUBY.include? type
- content = render_string(content, :erb, block)
- end
-
- render_string(content, type, block)
- end
-
- # Method returning view from method in class file.
- # Usage:
- #
- # view do
- # <<~HTML
- # <h1>
- # Hello <%= @name %>
- # </h1>
- # HTML
- # end
- #
- # or:
- #
- # view :haml do
- # <<~HAML
- # %h1
- # Hello
- # = @name
- # HAML
- # end
- #
- # @return [String]
- def render_class_method_view(&block)
- render_custom_view(self.class.method_view, &block)
- end
-
- # Method returning view from params in view.
- # Usage:
- #
- # <%= ExampleComponent data: data, view: "<h1>Hello #{@name}</h1>" %>
- #
- # or:
- #
- # <%= ExampleComponent data: data, view: { content: "<h1>Hello #{@name}</h1>", type: 'erb' } %>
- #
- # @return [String]
- def render_view_from_inline(&block)
- data = \
- if @view.is_a? String
- TypedContent.new(
- type: :erb,
- content: @view
- )
- else
- @view
- end
-
- render_custom_view(data, &block)
- end
-
- # @return [String]
- def inject_views(&block)
- view_from_file = render_view_from_file(&block)
- view_from_method = render_class_method_view(&block)
- view_from_inline = render_view_from_inline(&block)
-
- view_content = view_from_file unless view_from_file.empty?
- view_content = view_from_method unless view_from_method.empty?
- view_content = view_from_inline unless view_from_inline.empty?
-
- if view_content.nil? || view_content.empty?
- raise ViewFileNotFound, "View for `#{self.class}` could not be found!"
- end
-
- view_content
- end
-
- # Helper method to render style from css string or with other provided type.
- #
- # Usage:
- #
- # render_custom_style('.my-class { color: red; }')
- #
- # or:
- #
- # render_custom_style style: '.my-class { color: red; }', type: 'sass'
- #
- # @param style [TypedContent, Hash{Symbol => Symbol, String, Proc}, String]
- # @return [String, nil]
- def render_custom_style(style)
- return '' unless style
- return style if style.is_a?(::String)
-
- style = TypedContent.wrap(style)
- type = style.type
- content = style.to_s
-
- if content.empty?
- raise EmptyStyle, <<~ERR.squish
- Custom style for `#{self.class}` from style method cannot be empty!
- ERR
- end
-
- unless ALLOWED_STYLE_TYPES.include? type
- raise UnknownStyleType, <<~ERR.squish
- Unknown style type for `#{self.class}` from style method!
- Check return value of param type in `style :[type] do`
- ERR
- end
-
- # first render the content with ERB
- content = render_string(content, :erb)
-
- render_string(content, type)
- end
-
- # Method returning style from file (style.(css|sass|scss|less)) if exists.
- #
- # @return [String]
- def render_style_from_file
- style_path = self.class.style_path
- return '' unless style_path
-
- content = ::File.read(style_path)
- type = self.class.style_type
-
- return content if type == :css
-
- content = render_string(content, :erb)
- render_string(content, type)
- end
-
- # Method returning style from method in class file.
- # Usage:
- #
- # style do
- # '.my-class { color: red; }'
- # end
- #
- # or:
- #
- # style :sass do
- # <<~SASS
- # .my-class
- # color: red
- # SASS
- # end
- #
- # @return [String]
- def render_class_method_style
- render_custom_style(self.class.method_style)
- end
-
- # Method returning style from params in view.
- # Usage:
- #
- # <%= ExampleComponent data: data, style: '.my-class { color: red; }' %>
- #
- # or:
- #
- # <%= ExampleComponent data: data, style: {style: '.my-class { color: red; }', type: 'sass'} %>
- #
- # @return [String]
- def render_style_from_inline
- render_custom_style(@style)
- end
-
- # @param content [String]
- # @param type [Symbol]
- # @param block [Proc, nil]
- # @return [String]
- def render_string(content, type, block = nil)
- ::Tilt[type].new { content }.render(self, &block)
- end
-
- # @return [String]
- def inject_styles
- style_content = render_style_from_file + render_class_method_style + render_style_from_inline
- return if style_content.empty?
-
- ::AmberComponent::StyleInjector.inject(style_content)
end
end
end