lib/hot_module.rb in hot_module-1.0.0.alpha10 vs lib/hot_module.rb in hot_module-1.0.0.alpha11
- old
+ new
@@ -1,10 +1,11 @@
# frozen_string_literal: true
require_relative "hot_module/version"
require "nokolexbor"
+require "concurrent"
# Include this module into your own component class
module HoTModuLe
class Error < StandardError; end
@@ -21,14 +22,34 @@
AttributeBinding = Struct.new(:matcher, :method_name, :method, :only_for_tag, keyword_init: true) # rubocop:disable Lint/StructNewOverride
require_relative "hot_module/fragment"
require_relative "hot_module/query_selection"
+ def self.registered_elements
+ @registered_elements ||= Concurrent::Set.new
+
+ @registered_elements.each do |component|
+ begin
+ next if Kernel.const_get(component.to_s) == component # thin out unloaded consts
+ rescue NameError; end # rubocop:disable Lint/SuppressedException
+
+ @registered_elements.delete component
+ end
+
+ @registered_elements
+ end
+
# @param klass [Class]
# @return [void]
def self.included(klass)
klass.extend ClassMethods
+
+ klass.attribute_binding "hmod:children", :_hmod_children_binding, only: :template
+ klass.attribute_binding "hmod:replace", :_hmod_replace_binding
+ klass.attribute_binding "hmod:text", :_hmod_expr_binding
+ klass.attribute_binding "hmod:html", :_hmod_expr_binding
+
# Don't stomp on a superclass's `content` method
return if klass.instance_methods.include?(:content)
klass.include ContentMethod
end
@@ -39,11 +60,12 @@
Array(method_symbols).each do |method_symbol|
alias_method(method_symbol.to_s.gsub(/(?!^)_[a-z0-9]/) { |match| match[1].upcase }, method_symbol)
end
end
- def html_file_extensions = %w[module.html tmpl.html html].freeze
+ def html_file_extensions = %w[module.html mod.html html].freeze
+
def processed_css_extension = "css-local"
# @param tag_name [String]
# @param html_module [String] if not provided, a class method called `source_location` must be
# available with the absolute path of the Ruby file
@@ -52,47 +74,46 @@
def custom_element(tag_name, html_module = nil, shadow_root: true) # rubocop:disable Metrics/AbcSize
if html_module.nil? && !respond_to?(:source_location)
raise HoTModuLe::Error, "You must either supply a file path argument or respond to `source_location'"
end
- @tag_name = tag_name
+ self.tag_name tag_name
if html_module
- @html_module = html_module
+ self.html_module html_module
else
basepath = File.join(File.dirname(source_location), File.basename(source_location, ".*"))
- @html_module = html_file_extensions.lazy.filter_map do |ext|
+ self.html_module(html_file_extensions.lazy.filter_map do |ext|
path = "#{basepath}.#{ext}"
File.exist?(path) ? path : nil
- end.first
+ end.first)
raise HoTModuLe::Error, "Cannot find sidecar HTML module for `#{self}'" unless @html_module
end
- @shadow_root = shadow_root
+ self.shadow_root shadow_root
end
# @param value [String]
# @return [String]
def tag_name(value = nil)
- @tag_name ||= value
+ @tag_name ||= begin
+ HoTModuLe.registered_elements << self
+ value
+ end
end
# @param value [String]
# @return [String]
- def html_module(value = nil)
- @html_module ||= value
- end
+ def html_module(value = nil) = @html_module ||= value
# @param value [Boolean]
# @return [Boolean]
- def shadow_root(value = nil)
- @shadow_root ||= value
- end
+ def shadow_root(value = nil) = @shadow_root ||= value
- # @return [Nokogiri::XML::Element]
+ # @return [Nokolexbor::Element]
def doc
@doc ||= begin
@doc_html = "<#{tag_name}>#{File.read(html_module).strip}</#{tag_name}>"
Nokolexbor::DocumentFragment.parse(@doc_html).first_element_child
end
@@ -101,13 +122,11 @@
def line_number_of_node(node)
loc = node.source_location
instance_variable_get(:@doc_html)[0..loc].count("\n") + 1
end
- def attribute_bindings
- @attribute_bindings ||= []
- end
+ def attribute_bindings = @attribute_bindings ||= []
def attribute_binding(matcher, method_name, only: nil)
attribute_bindings << AttributeBinding.new(
matcher: Regexp.new(matcher),
method_name: method_name,
@@ -115,26 +134,25 @@
)
end
end
module ContentMethod
- # @return [String, Nokogiri::XML::Element]
- def content; end
+ # @return [String, Nokolexbor::Element]
+ def content = @_content
end
# Override in component
#
# @return [Hash]
- def attributes
- {}
- end
+ def attributes = {}
# @param attributes [Hash]
- # @param content [String, Nokogiri::XML::Element]
+ # @param content [String, Nokolexbor::Element]
# @param return_node [Boolean]
def render_element(attributes: self.attributes, content: self.content, return_node: false) # rubocop:disable Metrics
doc = self.class.doc.clone
+ @_content = content
tmpl_el = doc.css("> template").find { _1.attributes.empty? }
unless tmpl_el
tmpl_el = doc.document.create_element("template")
@@ -144,10 +162,37 @@
end
# Process all the template bits
process_fragment(tmpl_el)
+ HoTModuLe.registered_elements.each do |component|
+ tmpl_el.children[0].css(component.tag_name).reverse.each do |node|
+ if node["hmod:ignore"]
+ node.attribute("hmod:ignore").remove
+ next
+ end
+
+ attrs = node.attributes.transform_values(&:value)
+ attrs.reject! { |k| k.start_with?("hmod:") }
+ new_attrs = {}
+ attrs.each do |k, v|
+ next unless k.start_with?("arg:")
+
+ new_key = k.delete_prefix("arg:")
+ attrs.delete(k)
+ new_attrs[new_key] = instance_eval(v, self.class.html_module, self.class.line_number_of_node(node))
+ end
+ attrs.merge!(new_attrs)
+ attrs.transform_keys!(&:to_sym)
+
+ new_node = node.replace(
+ component.new(**attrs).render_element(content: node.children)
+ )
+ new_node.first.attribute("hmod:ignore")&.remove
+ end
+ end
+
# Set attributes on the custom element
attributes.each { |k, v| doc[k.to_s.tr("_", "-")] = value_to_attribute(v) if v }
# Look for external and internal styles
output_styles = ""
@@ -180,35 +225,32 @@
# We'll transfer everything over to a single style element
style_tag = tmpl_el.document.create_element("style")
style_tag.content = output_styles
end
+ child_content = @_replaced_children || content
if self.class.shadow_root
# Guess what? We can reuse the same template tag! =)
tmpl_el["shadowrootmode"] = "open"
tmpl_el.children[0] << style_tag if style_tag
- doc << content if content
+ doc << child_content if child_content
else
tmpl_el.children[0] << style_tag if style_tag
- tmpl_el.children[0].at_css("slot:not([name])")&.swap(content) if content
+ tmpl_el.children[0].at_css("slot:not([name])")&.swap(child_content) if child_content
tmpl_el.children[0].children.each do |node|
doc << node
end
tmpl_el.remove
end
# And that is that.
return_node ? doc : doc.to_html
end
- def call(...)
- render_element(...)
- end
+ def call(...) = render_element(...)
- def inspect
- "#<#{self.class.name} #{attributes}>"
- end
+ def inspect = "#<#{self.class.name} #{attributes}>"
def value_to_attribute(val)
case val
when String, Numeric
val
@@ -217,17 +259,19 @@
else
val.to_json
end
end
+ def node_or_string(val)
+ val.is_a?(Nokolexbor::Node) ? val : val.to_s
+ end
+
# Override in component if need be, otherwise we'll use the node walker/binding pipeline
#
- # @param fragment [Nokogiri::XML::Element]
+ # @param fragment [Nokolexbor::Element]
# @return [void]
- def process_fragment(fragment)
- Fragment.new(fragment, self).process
- end
+ def process_fragment(fragment) = Fragment.new(fragment, self).process
def process_list(attribute:, node:, item_node:, for_in:) # rubocop:disable Metrics
_context_nodes.push(node)
lh = for_in[0].strip.delete_prefix("(").delete_suffix(")").split(",").map!(&:strip)
@@ -273,17 +317,13 @@
else
Array[obj]
end.join(" ")
end
- def _context_nodes
- @_context_nodes ||= []
- end
+ def _context_nodes = @_context_nodes ||= []
- def _context_locals
- @_context_locals ||= {}
- end
+ def _context_locals = @_context_locals ||= {}
def _check_stack(node)
node_and_ancestors = [node, *node.ancestors.to_a]
stack_misses = 0
@@ -300,9 +340,34 @@
def _in_context_nodes
previous_context = _context_locals
yield previous_context
@_context_locals = previous_context
+ end
+
+ def _hmod_children_binding(attribute:, node:) # rubocop:disable Lint/UnusedMethodArgument
+ @_replaced_children = node.children[0]
+ node.remove
+ end
+
+ def _hmod_replace_binding(attribute:, node:)
+ if node.name == "template"
+ node.children[0].inner_html = node_or_string(evaluate_attribute_expression(attribute))
+ node.replace(node.children[0].children)
+ else
+ node.inner_html = node_or_string(evaluate_attribute_expression(attribute))
+ node.replace(node.children)
+ end
+ end
+
+ def _hmod_expr_binding(attribute:, node:)
+ if attribute.name.end_with?(":text")
+ node.content = node_or_string(evaluate_attribute_expression(attribute))
+ attribute.parent.delete(attribute.name)
+ elsif attribute.name.end_with?(":html")
+ node.inner_html = node_or_string(evaluate_attribute_expression(attribute))
+ attribute.parent.delete(attribute.name)
+ end
end
end
if defined?(Bridgetown)
Bridgetown.initializer :hot_module do |config|