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|