Skip to content

Commit b83d0b7

Browse files
authored
Merge pull request #455 from matestack/20200909_make-properties-inheritable
[FEATURE] make properties inheritable
2 parents 5c5d0e7 + 12ccce7 commit b83d0b7

File tree

5 files changed

+99
-22
lines changed

5 files changed

+99
-22
lines changed

Gemfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ group :development, :test do
2525
gem 'simplecov', require: false, group: :test
2626
gem 'byebug'
2727
gem 'webmock'
28-
gem 'pry-rails'
29-
gem 'pry-byebug'
3028
gem 'turbolinks'
3129
end
3230

3331
group :test do
32+
gem 'pry-rails'
33+
gem 'pry-byebug'
3434
gem "generator_spec"
3535
gem "rspec-retry" # repeating flaky tests
3636
gem "rspec-wait", "~> 0.0.9"

app/lib/matestack/ui/core/html_attributes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def self.included(base)
1919
module ClassMethods
2020

2121
def inherited(subclass)
22+
super
2223
subclass.html_attributes *self.allowed_html_attributes
2324
end
2425

app/lib/matestack/ui/core/properties.rb

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,51 @@ def initialize(model=nil, options={})
2020
options = model.dup if options.empty? && model.is_a?(Hash)
2121
required_hooks(options)
2222
optional_hooks(options)
23+
set_values(options)
2324
super
2425
end
2526
end
2627

2728
module ClassMethods
29+
30+
def inherited(subclass)
31+
super
32+
subclass.property_keys *self.all_property_keys
33+
end
34+
35+
# contains all property keys, or property hashes regardless of wheter they are optional or required
36+
# used in order to initialize values of properties correctly for subclasses and make properties inheritable
37+
def all_property_keys
38+
@all_properties.flatten!&.uniq! if defined? @all_properties
39+
@all_properties ||= []
40+
end
41+
42+
def created_properties
43+
@created_properties ||= []
44+
end
45+
46+
# add property keys to array containing all property keys
47+
def property_keys(*attributes)
48+
attributes.each do |attribute|
49+
if attribute.is_a?(Hash)
50+
attribute.each do |temp|
51+
all_property_keys.push({ "#{temp.first}": temp.last})
52+
end
53+
else
54+
all_property_keys.push(attribute)
55+
end
56+
end
57+
end
58+
2859
# define optinoal properties for custom components with `optional :foo, :bar`
2960
def optional(*properties)
61+
property_keys *properties
3062
add_properties_to_list(optional_properties, properties)
3163
end
3264

3365
# define required properties for custom components with `requires :title, :foo, :bar`
3466
def requires(*properties)
67+
property_keys *properties
3568
add_properties_to_list(requires_properties, properties)
3669
end
3770

@@ -47,7 +80,7 @@ def add_properties_to_list(list, properties)
4780

4881
# array of properties created from the component
4982
def optional_properties
50-
@component_properties ||= []
83+
@optional_properties ||= []
5184
end
5285

5386
# array of required properties from the component
@@ -58,30 +91,50 @@ def requires_properties
5891

5992
def optional_hooks(options)
6093
self.class.optional_properties.compact.each do |prop|
61-
if prop.is_a? Array
62-
hash = prop.flatten
63-
options[hash.last[:as]] = options[hash.first]
64-
prop = hash.last[:as]
65-
end
66-
raise PropertyOverwritingExistingMethodException, "Optional property #{prop} would overwrite already defined instance method for #{self.class}" if self.respond_to? prop
67-
send(:define_singleton_method, prop) do
68-
options[prop]
69-
end
94+
prop = extract_property_key(options, prop)
95+
if self.respond_to?(prop) && !self.class.created_properties.include?(prop)
96+
raise PropertyOverwritingExistingMethodException, "Optional property \"#{prop}\" would overwrite already defined instance method for #{self.class}" if self.respond_to? prop
97+
end
98+
define_getter_and_setter_for_property(prop)
7099
end
71100
end
72101

73102
def required_hooks(options)
74103
self.class.requires_properties.compact.each do |prop|
75-
if prop.is_a? Array
76-
hash = prop.flatten
77-
options[hash.last[:as]] = options[hash.first]
78-
prop = hash.last[:as]
79-
end
104+
prop = extract_property_key(options, prop)
80105
raise PropertyMissingException, "Required property #{prop} is missing for #{self.class}" if options[prop].nil?
81-
raise PropertyOverwritingExistingMethodException, "Required property #{prop} would overwrite already defined instance method for #{self.class}" if self.respond_to? prop
82-
send(:define_singleton_method, prop) do
83-
options[prop]
106+
if self.respond_to?(prop) && !self.class.created_properties.include?(prop)
107+
raise PropertyOverwritingExistingMethodException, "Required property \"#{prop}\" would overwrite already defined instance method for #{self.class}" if self.respond_to? prop
84108
end
109+
define_getter_and_setter_for_property(prop)
110+
end
111+
end
112+
113+
def set_values(options)
114+
self.class.all_property_keys.compact.each do |prop|
115+
value_key = prop
116+
prop = extract_property_key(options, prop)
117+
send(:"#{prop}=", options[prop])
118+
end
119+
end
120+
121+
# returns property key and sets alias if hash with as option is given
122+
def extract_property_key(options, prop)
123+
if prop.is_a?(Array) || prop.is_a?(Hash)
124+
hash = prop.flatten
125+
options[hash.last[:as]] = options[hash.first]
126+
prop = hash.last[:as]
127+
end
128+
prop
129+
end
130+
131+
def define_getter_and_setter_for_property(prop)
132+
self.class.created_properties.push(prop)
133+
self.class.send(:define_method, prop) do
134+
self.instance_variable_get(:"@#{prop}")
135+
end
136+
self.class.send(:define_method, :"#{prop}=") do |value|
137+
self.instance_variable_set(:"@#{prop}", value)
85138
end
86139
end
87140

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
class Components::Fancy::Card < Matestack::Ui::StaticComponent
22

3+
optional :foobar
34

45
end

spec/test/components/base/properties_spec.rb

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def response
9494

9595
visit '/example'
9696
expect(page).to have_content(Matestack::Ui::Core::Properties::PropertyOverwritingExistingMethodException.to_s)
97-
expect(page).to have_content('Required property response would overwrite already defined instance method for TempPropertyComponent')
97+
expect(page).to have_content('Required property "response" would overwrite already defined instance method for TempPropertyComponent')
9898
end
9999

100100
it 'should raise exception if optional property overwrites existing method' do
@@ -113,7 +113,7 @@ def response
113113

114114
visit '/example'
115115
expect(page).to have_content(Matestack::Ui::Core::Properties::PropertyOverwritingExistingMethodException.to_s)
116-
expect(page).to have_content('Optional property response would overwrite already defined instance method for TempOptionalPropertyComponent')
116+
expect(page).to have_content('Optional property "response" would overwrite already defined instance method for TempOptionalPropertyComponent')
117117
end
118118

119119
it 'should create instance method with given alias name for required properties' do
@@ -170,6 +170,7 @@ def prepare
170170
@bar = desc
171171
end
172172
def response
173+
plain @foo
173174
paragraph text: @foo
174175
paragraph text: @bar
175176
end
@@ -232,4 +233,25 @@ def other_slot
232233
expect(stripped(static_output)).to include(stripped(expected_static_output))
233234
end
234235

236+
it 'should be inheritable' do
237+
class Component < Matestack::Ui::Component
238+
optional :foobar, response: { as: :test }
239+
def response
240+
end
241+
end
242+
class AnotherComponent < Component
243+
optional :custom
244+
end
245+
component = Component.new(foobar: 'Foobar', response: 'Response')
246+
another_component = AnotherComponent.new(custom: 'hi', foobar: 'foobar', response: 'response')
247+
expect(component.respond_to? :foobar).to be(true)
248+
expect(component.respond_to? :test).to be(true)
249+
expect(another_component.respond_to? :custom).to be(true)
250+
expect(another_component.custom).to eq('hi')
251+
expect(another_component.respond_to? :foobar).to be(true)
252+
expect(another_component.foobar).to eq('foobar')
253+
expect(another_component.respond_to? :test).to be(true)
254+
expect(another_component.test).to eq('response')
255+
end
256+
235257
end

0 commit comments

Comments
 (0)