.
80 | */
81 | if(
82 | skip_children == null ||
83 | (skip_children.length > 1 && n.attributes[skip_children[0]] != skip_children[1]) ||
84 | (skip_children.length == 1 && n.attributes[skip_children[0]] == null )
85 | )
86 | DOMVirtualContainer.findNodesByName(regexp_as_str, n).forEach((n) => result.add(n));
87 | }
88 |
89 | });
90 |
91 | return result;
92 | }
93 |
94 |
95 | /*********************************************************************************************/
96 | /* Instance variables */
97 | /*********************************************************************************************/
98 |
99 | Node _opening;
100 | Node _closing;
101 | String name ;
102 |
103 | /*********************************************************************************************/
104 | /* Constructors */
105 | /*********************************************************************************************/
106 |
107 | DOMVirtualContainer(Node node) {
108 |
109 | var next_node = node.nextNode;
110 | _opening = node;
111 | while(next_node != null) {
112 | if(next_node.nodeValue == "END-OF-${node.nodeValue}") {
113 | _closing = next_node;
114 | break;
115 | }
116 | next_node = next_node.nextNode;
117 | }
118 |
119 | if(_closing == null) {
120 | _opening = new DocumentFragment.html("").childNodes[0];
121 | _closing = new DocumentFragment.html("").childNodes[0];
122 | node.parent.insertBefore(_opening, node);
123 | node.parent.insertBefore(_closing, node);
124 | node.remove();
125 | }
126 |
127 | this.name = _opening.nodeValue;
128 |
129 | }
130 |
131 | factory DOMVirtualContainer.find(String regexp, Element container, { skip_children: null }) {
132 | var comment_nodes = DOMVirtualContainer.findNodesByName(regexp, container, skip_children: skip_children);
133 | if(comment_nodes.isEmpty || comment_nodes.first == null) return;
134 | else return new DOMVirtualContainer(comment_nodes.first);
135 | }
136 |
137 | factory DOMVirtualContainer.findAll(String regexp, Element container, { skip_children: null }) {
138 | var comment_nodes = DOMVirtualContainer.findNodesByName(regexp, container, skip_children: skip_children);
139 | if(comment_nodes.isEmpty) return [];
140 |
141 | var invisible_containers = [];
142 | comment_nodes.forEach((n) {
143 | invisible_containers.add(new DOMVirtualContainer(n));
144 | });
145 | return invisible_containers;
146 | }
147 |
148 | /*********************************************************************************************/
149 | /* Getters and setters */
150 | /*********************************************************************************************/
151 | get children {
152 | var _children = [];
153 | var next_node = _opening.nextNode;
154 | while (next_node != _closing) {
155 | _children.add(next_node);
156 | next_node = next_node.nextNode;
157 | }
158 | return _children;
159 | }
160 |
161 | get text => _opening.nextNode.text;
162 | set text(v) {
163 | children.forEach((c) => c.remove());
164 | append(new Text(v));
165 | }
166 |
167 | /*********************************************************************************************/
168 | /* Public Methods */
169 | /*********************************************************************************************/
170 |
171 | prepend(Node el) {
172 | _opening.parent.insertBefore(el, _opening.nextNode);
173 | }
174 |
175 | append(Node el) {
176 | _closing.parent.insertBefore(el, _closing);
177 | }
178 |
179 | }
180 |
--------------------------------------------------------------------------------
/test/component_temple_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:html';
2 | import 'dart:mirrors';
3 | import 'package:unittest/unittest.dart';
4 | import 'package:unittest/html_config.dart';
5 | import '../lib/component_temple.dart';
6 |
7 | import 'package:attributable/attributable.dart';
8 | import 'package:observable_roles/observable_roles.dart';
9 | import 'package:validatable/validatable.dart';
10 |
11 | class ButtonComponent extends Component {
12 |
13 | ButtonComponent(template, element) : super(template, element);
14 | ButtonComponent.build(Map properties) : super.build(properties);
15 |
16 | }
17 |
18 | class ValidatableComponent extends Component with Validatable {
19 |
20 | ValidatableComponent(template, element) : super(template, element);
21 | ValidatableComponent.build(Map properties) : super.build(properties);
22 |
23 | final List attribute_names = ['attr1', 'attr2'];
24 |
25 | final Map validations = {
26 | 'attr1' : { 'isLessThan' : 10 },
27 | 'attr2' : { 'isLongerThan' : 5 }
28 | };
29 |
30 | }
31 |
32 | class CaptionAsContentButtonComponent extends Component {
33 |
34 | CaptionAsContentButtonComponent(template, element) : super(template, element);
35 |
36 | final Map attribute_callbacks = {
37 | 'default' : (attr_name, self) => self.updateProperty(attr_name),
38 | 'caption' : (attr_name, self) => self.content.text = self.getAttributeAsString(attr_name)
39 | };
40 |
41 | }
42 |
43 | class MockComponent extends Component {
44 |
45 | final Map internal_event_handlers = {
46 | '.clickableStuff' : { 'click' : 'increment_counter' }
47 | };
48 |
49 | final Map event_handlers = {
50 | 'click' : null,
51 | 'increment_counter' : null
52 | };
53 |
54 | List mock_calls = [];
55 | MockComponent(template, element) : super(template, element);
56 |
57 | @override captureEvent(event, [Publisher p]) {
58 | mock_calls.add("called captureEvent() with $event");
59 | super.captureEvent(event, p);
60 | }
61 |
62 | }
63 |
64 | class DummyModel extends Object with Attributable, Publisher {
65 |
66 | final List attribute_names = ['caption'];
67 |
68 | get caption => attributes['caption'] ;
69 | set caption(v) => attributes['caption'] = v;
70 |
71 | }
72 |
73 |
74 | main() {
75 |
76 | useHtmlConfiguration();
77 |
78 | TemplateStack.collect(document);
79 | TemplateStack.buildComponents(document.querySelector('body'));
80 |
81 | group('TemplateStack', () {
82 |
83 | test('creates instances of Template for all found templates', () {
84 | expect(TemplateStack.templates.length, equals(8));
85 | TemplateStack.templates.forEach((k,v) => expect((v is Template), isTrue));
86 | });
87 |
88 | test('assigns child components to parents', () {
89 | var nested_button = TemplateStack.templates['button'].components['Button_4'];
90 | var button_container = TemplateStack.templates['buttonContainer'].components['ButtonContainer_1'];
91 | expect(nested_button.parent, equals(button_container));
92 | expect(button_container.children().length, equals(2) );
93 | expect(button_container.children()[0], equals(nested_button) );
94 | });
95 |
96 | });
97 |
98 | group('Template', () {
99 |
100 | test('creates components out of itself from appropriate subclassess of Component', () {
101 |
102 | /* Note that here we're checking for 4 buttons and 1 greenButton,
103 | but 5 div.button elements. Look inside runner.html! I'm checking that
104 | a template by the name greenButton still uses ButtonComponent class because
105 | its attribute `component-class-name` is set to 'button'.
106 | */
107 | expect(TemplateStack.templates['button'].components.length, equals(5));
108 | expect(TemplateStack.templates['greenButton'].components.length, equals(1));
109 | expect(document.querySelectorAll('div.button').length, equals(6));
110 |
111 | TemplateStack.templates['button'].components.forEach((k,v) {
112 | ClassMirror component_class = reflect(v).type;
113 | expect(component_class.simpleName, equals(#ButtonComponent));
114 | });
115 |
116 | });
117 |
118 | test('bases components numeration on hashed (time + random) nuber rather than integers if configured', () {
119 | expect(
120 | new RegExp('AltNumeration_..+').hasMatch(TemplateStack.templates['altNumeration'].components.values.first.name),
121 | isTrue
122 | );
123 | });
124 |
125 | });
126 |
127 | group('Component', () {
128 |
129 | test('sets component id as a data- attribute to the component\'s html element', () {
130 | expect(document.querySelectorAll('.captionButton')[0].dataset['componentName'], equals('CaptionButton_1'));
131 | });
132 |
133 | test('replaces content virtual container inside the template html for the contents of its custom element', () {
134 | expect(document.querySelectorAll('div.button')[0].text, equals('This is button 1 caption'));
135 | });
136 |
137 | test('puts property values into appropriate virtual containers', () {
138 | var property_container = new DOMVirtualContainer.find('Component_property:caption', document.querySelector('.captionButton'));
139 | expect(property_container.text, equals('hello'));
140 | });
141 |
142 | test('takes values for component properties from custom element data-property- attributes', () {
143 | expect(TemplateStack.templates['captionButton'].components['CaptionButton_1'].caption, equals('hello'));
144 | });
145 |
146 | test('builds nested components using appropriate templates', () {
147 | expect(document.querySelectorAll('.buttonContainer .button').length, equals(2));
148 | });
149 |
150 | test('changes corresponding element contents in DOM when a field changes', () {
151 | var property_container = new DOMVirtualContainer.find('Component_property:caption', document.querySelector('.captionButton'));
152 | TemplateStack.templates['captionButton'].components['CaptionButton_1'].caption = 'new caption';
153 | expect(property_container.text, equals('new caption'));
154 | });
155 |
156 | test('notifies parent of the event', () {
157 |
158 | // 1. Trigger a click on a dom element
159 | document.querySelectorAll('.mock .mock')[0].click();
160 |
161 | // 2. Make sure the component caught the event
162 | expect(TemplateStack.templates['mock'].components['Mock_2'].mock_calls[0], equals('called captureEvent() with click'));
163 |
164 | // 3. Make sure the parent of the component was also notified of the event
165 | expect(TemplateStack.templates['mock'].components['Mock_1'].mock_calls[0], equals('called captureEvent() with any.click'));
166 |
167 | });
168 |
169 | test('handles events of DOM elements which the component is composed of', () {
170 | document.querySelectorAll('.mock .mock .clickableStuff')[0].click();
171 | expect(TemplateStack.templates['mock'].components['Mock_2'].mock_calls.contains('called captureEvent() with increment_counter'), equals(true));
172 | expect(TemplateStack.templates['mock'].components['Mock_1'].mock_calls.contains('called captureEvent() with any.increment_counter'), equals(true));
173 | });
174 |
175 | test('gets assigned a role', () {
176 | expect(TemplateStack.templates['button'].components['Button_1'].role, equals('stupid'));
177 | });
178 |
179 | test('puts newly added childrens\' DOM elements into containers based on their role', () {
180 | var button_container = TemplateStack.templates['buttonContainer'].components['ButtonContainer_1'];
181 | var button_without_role = new ButtonComponent.build({});
182 | var button_with_role = new ButtonComponent.build({ 'role': 'submit'});
183 | button_container.addChild(button_without_role);
184 | button_container.addChild(button_with_role);
185 | var any_children = new DOMVirtualContainer.find('Component_children:any', button_container.element);
186 | var submit_children = new DOMVirtualContainer.find('Component_children:submit', button_container.element);
187 | expect(any_children.children[0], equals(button_without_role.element));
188 | expect(submit_children.children[0], equals(button_with_role.element));
189 | });
190 |
191 | test('executes a function when adding a new child', () {
192 | var button_container = TemplateStack.templates['buttonContainer'].components['ButtonContainer_1'];
193 | var new_button = new ButtonComponent.build({});
194 | var function_called = false;
195 | button_container.addChild(new_button, (c) {
196 | function_called = true;
197 | });
198 | expect(function_called, isTrue);
199 | });
200 |
201 | test('assigns a custom name for the component, does not allow to change it later', () {
202 | var super_button = TemplateStack.templates['button'].components['SuperButton_1'];
203 | expect(super_button, isNotNull);
204 | expect(() => super_button.name = "NewSuperButtonName_1", throwsException);
205 | });
206 |
207 | test('removes itself from DOM and from the Template\'s components Map', () {
208 | var button_container = TemplateStack.templates['buttonContainer'].components['ButtonContainer_1'];
209 | var button_components = TemplateStack.templates['button'].components;
210 | var new_button = new ButtonComponent.build({ 'name' : 'ButtonToBeRemovedLater_1' });
211 | button_container.addChild(new_button);
212 | expect(button_components['ButtonToBeRemovedLater_1'], isNotNull);
213 | expect(document.querySelector('.button[data-component-name=ButtonToBeRemovedLater_1]'), isNotNull);
214 | new_button.remove();
215 | expect(button_components['ButtonToBeRemovedLater_1'], isNull);
216 | expect(document.querySelector('.button[data-component-name=ButtonToBeRemovedLater_1]'), isNull);
217 | });
218 |
219 | test('updates content when binded attribute is updated', () {
220 | // This actually checks the concept, not the core functionality of the Component class.
221 | // The behavior responsible for this test passing is defined above in this file
222 | // in a CaptionAsContentButtonComponent class.
223 | var component = TemplateStack.templates['captionAsContentButton'].components['CaptionAsContentButton_1'];
224 | var content = new DOMVirtualContainer.find('Component_content', component.element);
225 | component.caption = "new content";
226 | expect(content.text, equals('new content'));
227 | });
228 |
229 |
230 | group('adding and removing children with roles', () {
231 |
232 | var button_container = TemplateStack.templates['buttonContainer'].components['ButtonContainer_1'];
233 | button_container.addChild(new ButtonComponent.build({ 'role': 'temporary_button'}));
234 |
235 | test('finds children with specific roles', () {
236 | expect(button_container.children('temporary_button').length, equals(1));
237 | expect((button_container.children().length > 1), isTrue);
238 | expect((button_container.children('temporary_button')[0] is Component), isTrue);
239 | });
240 |
241 | test("when removed, removes itself from the parent's, children list", () {
242 | var button = new ButtonComponent.build({ 'role': 'temporary_button' });
243 | button_container.addChild(button);
244 | expect(button_container.children('temporary_button').contains(button), isTrue);
245 | expect(button_container.children().contains(button), isTrue);
246 | button.remove();
247 | expect(button_container.children('temporary_button').contains(button), isFalse);
248 | expect(button_container.children().contains(button), isFalse);
249 | });
250 |
251 | });
252 |
253 | test('updates all attributes when subscribing component to the model and then when updating the model', () {
254 | var button = new ButtonComponent.build({ 'template_name': 'captionButton' });
255 | expect(button.caption, isNull);
256 | var model = new DummyModel();
257 | model.caption = 'caption change 1';
258 | button.model = model;
259 | expect(button.caption, equals('caption change 1'));
260 | expect(new DOMVirtualContainer.find('Component_property:caption', button.element).text, equals('caption change 1'));
261 | model.caption = 'caption change 2';
262 | button.captureEvent('model.update', model);
263 | expect(button.caption, equals('caption change 2'));
264 | expect(new DOMVirtualContainer.find('Component_property:caption', button.element).text, equals('caption change 2'));
265 | });
266 |
267 | test('updates attributes in bulk, adds validation errors (Validatable is mixed in)', () {
268 | var button = new ButtonComponent.build({});
269 | button.updateAttributes({ 'caption' : 'new caption'});
270 | expect(button.caption, equals('new caption'));
271 | var validatable_component = new ValidatableComponent.build({ 'template_name': 'button'});
272 | expect(validatable_component.updateAttributes({ 'attr1' : 11, 'attr2': ''}), isFalse);
273 | expect(validatable_component.validation_errors.keys.contains('attr1'), isTrue);
274 | expect(validatable_component.validation_errors.keys.contains('attr2'), isTrue);
275 | });
276 |
277 | });
278 |
279 | group('string case operations', () {
280 | test('converts dasherized names int camelcase', () {
281 | expect(dashedToCamelcase('hello-world'), equals('HelloWorld'));
282 | });
283 | });
284 |
285 | test("finds all subclasses", () {
286 | expect(findSubclasses('Component').length, equals(3));
287 | expect(findSubclasses('Component')[1], equals(reflectClass(ButtonComponent)));
288 | });
289 |
290 | }
291 |
--------------------------------------------------------------------------------
/lib/component.dart:
--------------------------------------------------------------------------------
1 | part of component_temple;
2 |
3 | class Component extends Object with observable.Subscriber, observable.Publisher, Attributable {
4 |
5 | static NodeValidatorBuilder childNodeValidator(tag_name) {
6 | return new NodeValidatorBuilder()
7 | ..allowHtml5()
8 | ..allowElement(tag_name);
9 | }
10 |
11 | /*********************************************************************************************/
12 | /* Instance variables declarations */
13 | /*********************************************************************************************/
14 |
15 | Element element ; // DOM element which corresponds to the current object
16 | Element content ; // DOM element which holds the {{content}} of the element
17 | Template template ; // our Template object we're going to use to generate an element
18 | Component _parent ; // another Component which is a parent of the current one (could be null)
19 | String _name ; // a combination of unique id and a name of the component, for instance Button_1
20 | // could also be set by user and be whatever.
21 |
22 | /* Roles determine what a parent does with events from components.
23 | A role may be defined in html by adding `data-component-role` attribute.
24 | */
25 | String role = 'any';
26 |
27 | Map virtual_containers = {};
28 |
29 | var _model; // We don't really want to specify class name here and require TempleModel library;
30 | // It's better to have more freedom.
31 |
32 | /* Each value of this map holds a child with a specific role, while keys are role names.
33 | It is not supposed to be accessed directly (but it has to be public, cos for some reason
34 | subclasses in Dart don't recognize superclass instance vars that start with _.
35 | Anyway, a proper way to access children is by using a children() getter - note how () at the end
36 | are obligatory.
37 | */
38 | Map children_by_roles = {};
39 |
40 | /* This is how you list attributes for your component. They are not real properties. Instead,
41 | getters and setters are dynamic (caught by noSuchMethod). This is helpful because
42 | we want to be able to execute callbacks on attribute change.
43 | */
44 | final List attribute_names = ['caption'];
45 | Map attributes = {};
46 |
47 | /* When an attribute is updated, these are the callbacks that get invoked.
48 | There's only one default one and it updates the corresponding DOM element with the
49 | new value. You can either redefine it or add your own callbacks for each
50 | particular attribute.
51 |
52 | `self_mirror` argument is a mirror of the current object. We can't simply pass
53 | `this`, because this raises an exception.
54 | */
55 | final Map attribute_callbacks = {
56 | 'default' : (attr_name, self) => self.updateProperty(attr_name)
57 | };
58 |
59 | /* This is where events are defined.
60 | If you don't want this Element do anything on the event, but still want its parent
61 | to be notified, use null as a value for the key (a key represents an event name).
62 | */
63 | final Map event_handlers = {
64 |
65 | 'click' : null, // Still notify the parent of the click event.
66 |
67 | 'model.update' : (self, model) {
68 | self.attribute_names.forEach((attr_name) {
69 | if(model.attribute_names.contains(attr_name))
70 | self.attributes[attr_name] = model.attributes[attr_name];
71 | });
72 | self.updateElement();
73 | }
74 |
75 | };
76 |
77 | /* Components may consist of several DOM elemnts, and each may be able to trigger an event.
78 | This event is caught here and then we can decide what custom event in our component is
79 | then called.
80 |
81 | They key of the map is a selector used to find a an element withing the component with
82 | querySelectorAll(). The value is another Map: they key is name of the DOM event, the value
83 | is the name of the Component event that's going to be called.
84 | */
85 | final Map internal_event_handlers = {};
86 |
87 | /* There's no reason to save those Streams yet, but we may later need to be able
88 | to cancel event listeners, so let's do it.
89 | */
90 | Map event_listeners = {};
91 |
92 | /* Put things like making a Button inactive or animation in here.
93 | Basic Component is dumb, cannot behave itself.
94 | */
95 | final Map behaviors = {};
96 |
97 |
98 | /*********************************************************************************************/
99 | /* Constructors */
100 | /*********************************************************************************************/
101 |
102 | /* Used by Template objects only */
103 | Component(this.template, this.element) {
104 | _shared_constructor();
105 | }
106 |
107 | /* Used to create components dynamically from code */
108 | Component.build(Map properties) {
109 |
110 | if(properties['template_name'] == null) {
111 | properties['template_name'] = downcaseFirstLetter(MirrorSystem.getName(reflect(this).type.simpleName).replaceFirst('Component', ''));
112 | }
113 |
114 | if(properties['role'] == null)
115 | properties['role'] = 'any';
116 |
117 | this.role = properties['role'];
118 | this.template = TemplateStack.templates[properties['template_name']];
119 | this.element = new Element.html("<${this.template.name}>${this.template.name}>", validator: Component.childNodeValidator(properties['template_name']));
120 | this._name = properties['name'];
121 | _shared_constructor();
122 | this.template.addComponent(this);
123 | }
124 |
125 | _shared_constructor() {
126 |
127 | _setDefaultProperties();
128 |
129 | if(this.name == null) {
130 | if(this.element.dataset['componentName'] != null) {
131 | this._name = this.element.dataset['componentName'];
132 | } else {
133 | this._name = "${this.template.name}_${this.template.next_component_number()}";
134 | }
135 | }
136 |
137 | if(this.element.dataset['componentRole'] != null) {
138 | this.role = this.element.dataset['componentRole'];
139 | }
140 |
141 | var template_element = this.template.buildDomElement();
142 | this.content = new DOMVirtualContainer.find('Component_content', template_element, skip_children: ['data-component-name']);
143 | if(this.content != null) {
144 | this.element.childNodes.forEach((n) {
145 | this.content.append(n.clone(true));
146 | });
147 | }
148 |
149 | prvt_replaceDomElementWithTemplate(template_element);
150 | updateElement();
151 | _setDOMEventListeners();
152 |
153 | }
154 |
155 | /*********************************************************************************************/
156 | /* Custom getters and setters */
157 | /*********************************************************************************************/
158 |
159 | get parent => _parent;
160 | set parent(Component p) {
161 | setParent(p);
162 | if(parent.children_by_roles[this.role] == null) { parent.children_by_roles[this.role] = []; }
163 | parent.children_by_roles[this.role].add(this);
164 | }
165 |
166 | /* This is for cases where we want to set a parent, but then manually add
167 | a child to the children's list.
168 | */
169 | setParent(Component p) {
170 | _parent = p;
171 | // Whenever we add a parent, it becomes a subscriber to all the events happening in this view.
172 | addObservingSubscriber(p);
173 | }
174 |
175 | get name => _name;
176 | set name(String new_name) {
177 | // Probably later this should be allowed
178 | // but for now it's too much work: change DOM element data attr,
179 | // change Template's component Map name etc.
180 | throw new Exception("You cannot change component's name after it was already built.");
181 | }
182 |
183 | /* Subscribes this Component to the model's events and immediately
184 | invokes 'model.update' callback to sync with model attributes
185 | */
186 | get model => _model;
187 | set model(m) {
188 |
189 | if(!(m is observable.Publisher) || !(m is Attributable))
190 | throw new Exception("Model must implement observable.Publisher and Attributable interfaces!");
191 |
192 | m.addObservingSubscriber(reflect(this).reflectee);
193 | captureEvent('model.update', m) ;
194 | _model = m;
195 |
196 | }
197 |
198 | /* Returns children of specific roles or all children.
199 | Always use () when calling this getter, because it's not actually a getter,
200 | but philosophically it is.
201 | */
202 | children([role]) {
203 | if(role == null || role == 'any') {
204 | var result = [];
205 | children_by_roles.forEach((k,v) {
206 | result.addAll(v);
207 | });
208 | return result;
209 | } else {
210 | return children_by_roles[role];
211 | }
212 | }
213 |
214 | /*********************************************************************************************/
215 | /* Public Methods */
216 | /*********************************************************************************************/
217 |
218 | addChild(Component child, [Function f]) {
219 | child.parent = this;
220 | if(f != null) {
221 | f(child);
222 | } else {
223 | addChildToDOM(child);
224 | }
225 | }
226 |
227 | addChildToDOM(Component child) {
228 | var children_container = _getFirstVirtualContainer("children:${child.role}");
229 | if(children_container != null) { children_container.append(child.element); }
230 | }
231 |
232 | remove() {
233 | this.children().forEach((c) => c.remove()); // remove children before removing itself!
234 | if(this.parent != null)
235 | this.parent.prvt_removeChild(reflect(this).reflectee);
236 | this.template.components.remove(this.name);
237 | this.element.remove();
238 | this.element = null;
239 | if(model != null) {
240 | model.removeObservingSubscriber(this);
241 | model = null;
242 | }
243 | }
244 |
245 | /* Updates the property container of a DOM element */
246 | updateProperty(property_name, [value=null]) {
247 |
248 | if(value == null)
249 | value = getAttributeAsString(property_name);
250 |
251 | var property_containers = _getVirtualContainers('property:${property_name}');
252 | property_containers.forEach((c) => c.text = value);
253 |
254 | }
255 |
256 | /* Calls for attributes callbacks for each attribute and, supposedly,
257 | updates property containers in a DOM element. Of course, that is the default
258 | behavior when an attribute is updated, but it can be changed if another
259 | callback is assigned.
260 | */
261 | updateElement() {
262 | attribute_names.forEach((attr_name) {
263 | invokeAttributeCallback(attr_name);
264 | });
265 | }
266 |
267 | getAttributeAsString(attr_name) {
268 | var value = attributes[attr_name];
269 | if(value == null) { value = ''; }
270 | else if(!(value is String)) { value = value.toString(); }
271 | return value;
272 | }
273 |
274 | updateAttributes(attrs) {
275 | return super.updateAttributes(attrs, () {
276 | try {
277 | if(this is Validatable) {
278 | validate();
279 | if(valid)
280 | return true;
281 | else
282 | return false;
283 | }
284 | } on TypeError {
285 | return true;
286 | // Do nothing, Validatable is not loaded, who gives a shit!
287 | }
288 | });
289 | }
290 |
291 | behave(b) {
292 | if(this.behaviors[b] != null) {
293 | this.behaviors[b](this.element);
294 | } else {
295 | throw new Exception("No behavior `${b}` defined for ${this.template.componentClassName}");
296 | }
297 | }
298 |
299 | /*********************************************************************************************/
300 | /* Private Methods */
301 | /*********************************************************************************************/
302 |
303 | /* Extracts property values from component's custom html element attributes and assigns them to
304 | to properties of the object. Can only be called from constructor, since the element
305 | gets replaced.
306 | */
307 | _setDefaultProperties() {
308 |
309 | this.element.dataset.forEach((k,v) {
310 | var property_name = downcaseFirstLetter(k.replaceAll('property', ''));
311 | if(attribute_names.contains(property_name)) {
312 | attributes[property_name] = v;
313 | }
314 | });
315 |
316 | }
317 |
318 | _propertyElements([property_name=null]) {
319 | if(property_name == null) {
320 | return _getVirtualContainers("property.*");
321 | } else {
322 | return _getVirtualContainers("property:${property_name}");
323 | }
324 | }
325 |
326 | /* Do not use this method anywhere. It should only be used by remove() method of the child.
327 | That's because a child is only removed from the children_by_roles list, but not from the DOM.
328 | */
329 | prvt_removeChild(Component child) {
330 | if(children_by_roles[child.role] != null)
331 | children_by_roles[child.role].remove(child);
332 | }
333 |
334 | /* This only called from constructor and is for cases when we want to redefine
335 | it in subclasses. It may sometimes be required. Like for example when it is impossible
336 | to create a template element whose root element is
. Instead, such an element must be wrapped
337 | in , then in a subclass which handles the template this would extracted from
338 | the and insrted into the DOM - this would happen in this redefined method.
339 | */
340 | prvt_replaceDomElementWithTemplate(template_element) {
341 | template_element.dataset['componentName'] = this.name;
342 | this.element.replaceWith(template_element); // Replacing element in the DOM
343 | this.element = template_element ; // Replacing element in component instance
344 | }
345 |
346 | _getVirtualContainers(container_name) {
347 | if(virtual_containers[container_name] == null)
348 | virtual_containers[container_name] = new DOMVirtualContainer.findAll('Component_${container_name}', this.element, skip_children: ['data-component-name']);
349 | return virtual_containers[container_name];
350 | }
351 |
352 | _getFirstVirtualContainer(container_name) {
353 | return new DOMVirtualContainer.find('Component_${container_name}', this.element, skip_children: ['data-component-name']);
354 | }
355 |
356 | _setDOMEventListeners() {
357 |
358 | event_handlers.forEach((k,v) {
359 | // Only set those listeners whose event names don't have a dot in them,
360 | // which indicates those are reserved for children.
361 | if(!k.contains('.')) {
362 | event_listeners[k] = element.on[k].listen((event) {
363 | event.stopPropagation();
364 | captureEvent(k);
365 | });
366 | }
367 |
368 | internal_event_handlers.forEach((el_query, el_events) {
369 | element.querySelectorAll("${el_query}").forEach((el) {
370 | el_events.forEach((e,h) {
371 | el.on[e].listen((event) {
372 | event.stopPropagation();
373 | captureEvent(h);
374 | });
375 | });
376 | });
377 | });
378 |
379 | });
380 | }
381 |
382 |
383 | /*********************************************************************************************/
384 | /* Everything else */
385 | /*********************************************************************************************/
386 |
387 | noSuchMethod(Invocation i) {
388 | var result = prvt_noSuchGetterOrSetter(i);
389 | if(result != false)
390 | return result;
391 | else
392 | super.noSuchMethod(i);
393 | }
394 |
395 | }
396 |
--------------------------------------------------------------------------------