Class: RSX::Component

Inherits:
Object
  • Object
show all
Defined in:
lib/rsx/component.rb

Overview

Base class for every component defined in a .rsx file.

component Greeting do |name:|
return <p>Hello {name}</p>
end

compiles to a subclass whose rsx_render is the block body. Components are plain Ruby objects: one instance per render, no inheritance requirements on the caller, and no framework needed to use them.

Constant Summary collapse

EMPTY_PROPS =
{}.freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(props = nil, parent = nil) ⇒ Component

Returns a new instance of Component.



128
129
130
131
# File 'lib/rsx/component.rb', line 128

def initialize(props = nil, parent = nil)
  @props = props || EMPTY_PROPS
  @rsx_parent = parent
end

Class Attribute Details

.rsx_cache_optionsObject



20
21
22
# File 'lib/rsx/component.rb', line 20

def rsx_cache_options
  @rsx_cache_options
end

.rsx_source_digestObject

Returns the value of attribute rsx_source_digest.



17
18
19
# File 'lib/rsx/component.rb', line 17

def rsx_source_digest
  @rsx_source_digest
end

.rsx_source_pathObject

Returns the value of attribute rsx_source_path.



17
18
19
# File 'lib/rsx/component.rb', line 17

def rsx_source_path
  @rsx_source_path
end

Instance Attribute Details

#propsObject (readonly)

Returns the value of attribute props.



126
127
128
# File 'lib/rsx/component.rb', line 126

def props
  @props
end

Class Method Details

.cache(options = true) ⇒ Object

Enables whole-component caching.

cache expires_in: 300
cache key: ->(props) { [props[:user], props[:locale]] }


38
39
40
# File 'lib/rsx/component.rb', line 38

def cache(options = true)
  @rsx_cache_options = RSX.normalize_cache_options(options)
end

.call(**props) ⇒ Object

Public API: Button.call(label: "Save") => SafeString



57
58
59
# File 'lib/rsx/component.rb', line 57

def call(**props)
  rsx_call(props)
end

.inspectObject



121
122
123
# File 'lib/rsx/component.rb', line 121

def inspect
  name || super
end

.render(**props) ⇒ Object



61
62
63
# File 'lib/rsx/component.rb', line 61

def render(**props)
  rsx_call(props)
end

.rsx_accepted_propsObject

The keyword props this component declares.



94
95
96
97
98
# File 'lib/rsx/component.rb', line 94

def rsx_accepted_props
  @rsx_accepted_props ||= rsx_render_parameters.filter_map do |kind, name|
    name if %i[key keyreq].include?(kind)
  end
end

.rsx_accepts_extra_props?Boolean

Returns:

  • (Boolean)


100
101
102
103
104
# File 'lib/rsx/component.rb', line 100

def rsx_accepts_extra_props?
  return @rsx_accepts_extra_props if defined?(@rsx_accepts_extra_props)

  @rsx_accepts_extra_props = rsx_render_parameters.any? { |kind, _| kind == :keyrest }
end

.rsx_cache_key(props, options) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/rsx/component.rb', line 65

def rsx_cache_key(props, options)
  custom = options[:key]
  payload =
    if custom.nil?
      (props || EMPTY_PROPS).reject { |key, _| key == :children }
    elsif custom.respond_to?(:arity) && custom.arity.zero?
      custom.call
    else
      custom.call(props || EMPTY_PROPS)
    end

  "rsx/#{name || "component"}/#{rsx_source_digest || "0"}/#{RSX.stable_key(payload)}"
end

.rsx_call(props = nil, parent = nil) ⇒ Object

Renders the component. This is the entry point used by compiled markup.



43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/rsx/component.rb', line 43

def rsx_call(props = nil, parent = nil)
  return (@rsx_static_output ||= new(props, parent).rsx_perform) if rsx_static?

  options = rsx_cache_options
  return new(props, parent).rsx_perform unless options

  key = rsx_cache_key(props, options)
  cached = RSX.cache.fetch(key, expires_in: options[:expires_in]) do
    new(props, parent).rsx_perform.to_s
  end
  cached.is_a?(SafeString) ? cached : SafeString.new(cached)
end

.rsx_filter_props(props) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/rsx/component.rb', line 106

def rsx_filter_props(props)
  return EMPTY_PROPS if props.nil? || props.empty?
  return props if rsx_accepts_extra_props?

  accepted = rsx_accepted_props
  unknown = props.keys - accepted - [:children]
  unless unknown.empty?
    raise PropsError, "#{name} does not accept #{unknown.map(&:inspect).join(", ")}. " \
                      "Declared props: #{accepted.map(&:inspect).join(", ")}. " \
                      "Add `**rest` to the component parameters to accept anything else."
  end

  accepted.include?(:children) ? props : props.except(:children)
end

.rsx_render_parametersObject



87
88
89
90
91
# File 'lib/rsx/component.rb', line 87

def rsx_render_parameters
  instance_method(:rsx_render).parameters
rescue NameError
  []
end

.rsx_render_styleObject



79
80
81
82
83
84
85
# File 'lib/rsx/component.rb', line 79

def rsx_render_style
  return @rsx_render_style if defined?(@rsx_render_style)

  parameters = rsx_render_parameters
  keyword = parameters.any? { |kind, _| %i[key keyreq keyrest].include?(kind) }
  @rsx_render_style = keyword ? :keyword : :positional
end

.rsx_static!Object

Marks a component whose output never varies. The compiler sets this automatically when a component body is nothing but static markup.



26
27
28
# File 'lib/rsx/component.rb', line 26

def rsx_static!
  @rsx_static = true
end

.rsx_static?Boolean

Returns:

  • (Boolean)


30
31
32
# File 'lib/rsx/component.rb', line 30

def rsx_static?
  @rsx_static ? true : false
end

Instance Method Details

#cache(key, expires_in: nil) ⇒ Object

Caches a fragment of markup.

{cache(["sidebar", user.id], expires_in: 300) do
<nav>...</nav>
end}


193
194
195
196
197
# File 'lib/rsx/component.rb', line 193

def cache(key, expires_in: nil)
  full_key = "rsx/#{self.class.name}/#{self.class.rsx_source_digest || "0"}/#{RSX.stable_key(key)}"
  cached = RSX.cache.fetch(full_key, expires_in: expires_in) { RSX.child(yield).to_s }
  cached.is_a?(SafeString) ? cached : SafeString.new(cached)
end

#childrenObject

The markup nested inside this component's tag, rendered on demand.



149
150
151
# File 'lib/rsx/component.rb', line 149

def children
  @props[:children]
end

#children?Boolean

Returns:

  • (Boolean)


153
154
155
156
# File 'lib/rsx/component.rb', line 153

def children?
  child = children
  !child.nil? && !(child.respond_to?(:empty?) && child.empty?)
end

#helpersObject Also known as: view_context

The nearest non-component render context, i.e. the Rails view. Gives access to url helpers, form builders, t, asset helpers and anything else the application exposes to templates.

Raises:



167
168
169
170
171
172
173
174
# File 'lib/rsx/component.rb', line 167

def helpers
  node = @rsx_parent
  node = node.rsx_parent while node.is_a?(Component)
  return node unless node.nil?

  raise Error, "#{self.class.name} has no view context. Render it from a Rails view, " \
               "or pass one with RSX.render(#{self.class.name}, context: view)."
end

#helpers?Boolean

Returns:

  • (Boolean)


177
178
179
180
181
# File 'lib/rsx/component.rb', line 177

def helpers?
  node = @rsx_parent
  node = node.rsx_parent while node.is_a?(Component)
  !node.nil?
end

#inspectObject



208
209
210
# File 'lib/rsx/component.rb', line 208

def inspect
  "#<#{self.class.name} #{@props.inspect}>"
end

#raw(value) ⇒ Object

Escape hatch for trusted HTML built elsewhere.



200
201
202
# File 'lib/rsx/component.rb', line 200

def raw(value)
  RSX.raw(value)
end

#rsx_parentObject

The object that rendered this component: a view context in Rails, the parent component when nested, or nil when rendered directly.



160
161
162
# File 'lib/rsx/component.rb', line 160

def rsx_parent
  @rsx_parent
end

#rsx_performObject



133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/rsx/component.rb', line 133

def rsx_perform
  result =
    if self.class.rsx_render_style == :keyword
      rsx_render(**self.class.rsx_filter_props(@props))
    else
      rsx_render(@props)
    end
  RSX.child(result)
rescue ArgumentError => e
  raise unless e.message.start_with?("missing keyword", "unknown keyword", "wrong number of arguments")

  raise PropsError, "#{self.class.name}: #{e.message}. " \
                    "Declared props: #{self.class.rsx_accepted_props.map(&:inspect).join(", ")}"
end

#to_sObject



204
205
206
# File 'lib/rsx/component.rb', line 204

def to_s
  rsx_perform
end

#use_context(context) ⇒ Object

Reads a value provided by an enclosing <Ctx.Provider>.



184
185
186
# File 'lib/rsx/component.rb', line 184

def use_context(context)
  context.value
end