OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * Part of the template compilation that concerns with extracting information | 6 * Part of the template compilation that concerns with extracting information |
7 * from the HTML parse tree. | 7 * from the HTML parse tree. |
8 */ | 8 */ |
9 library analyzer; | 9 library analyzer; |
10 | 10 |
(...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
95 void visitElementInfo(ElementInfo info) { | 95 void visitElementInfo(ElementInfo info) { |
96 var node = info.node; | 96 var node = info.node; |
97 | 97 |
98 if (node.id != '') info.identifier = '__${toCamelCase(node.id)}'; | 98 if (node.id != '') info.identifier = '__${toCamelCase(node.id)}'; |
99 if (node.tagName == 'body' || (_currentInfo is ComponentInfo | 99 if (node.tagName == 'body' || (_currentInfo is ComponentInfo |
100 && (_currentInfo as ComponentInfo).template == node)) { | 100 && (_currentInfo as ComponentInfo).template == node)) { |
101 info.isRoot = true; | 101 info.isRoot = true; |
102 info.identifier = '_root'; | 102 info.identifier = '_root'; |
103 } | 103 } |
104 | 104 |
105 node.attributes.forEach((k, v) => visitAttribute(info, k, v)); | |
106 | |
107 _bindCustomElement(node, info); | 105 _bindCustomElement(node, info); |
108 | 106 |
109 var lastInfo = _currentInfo; | 107 var lastInfo = _currentInfo; |
110 if (node.tagName == 'element') { | 108 if (node.tagName == 'element') { |
111 // If element is invalid _ElementLoader already reported an error, but | 109 // If element is invalid _ElementLoader already reported an error, but |
112 // we skip the body of the element here. | 110 // we skip the body of the element here. |
113 var name = node.attributes['name']; | 111 var name = node.attributes['name']; |
114 if (name == null) return; | 112 if (name == null) return; |
115 var component = _fileInfo.components[name]; | 113 var component = _fileInfo.components[name]; |
116 if (component == null) return; | 114 if (component == null) return; |
117 | 115 |
118 // Associate ElementInfo of the <element> tag with its component. | 116 // Associate ElementInfo of the <element> tag with its component. |
119 component.elemInfo = info; | 117 component.elemInfo = info; |
120 | 118 |
121 _bindExtends(component); | 119 _bindExtends(component); |
122 | 120 |
123 _currentInfo = component; | 121 _currentInfo = component; |
124 } | 122 } |
125 | 123 |
| 124 node.attributes.forEach((k, v) => visitAttribute(info, k, v)); |
| 125 |
126 var savedParent = _parent; | 126 var savedParent = _parent; |
127 _parent = info; | 127 _parent = info; |
128 | 128 |
129 // Invoke super to visit children. | 129 // Invoke super to visit children. |
130 super.visitElement(node); | 130 super.visitElement(node); |
131 _currentInfo = lastInfo; | 131 _currentInfo = lastInfo; |
132 | 132 |
133 _parent = savedParent; | 133 _parent = savedParent; |
134 | 134 |
135 if (_needsIdentifier(info)) { | 135 if (_needsIdentifier(info)) { |
(...skipping 195 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
331 attrInfo = _readAttribute(info, name, value); | 331 attrInfo = _readAttribute(info, name, value); |
332 } | 332 } |
333 | 333 |
334 if (attrInfo != null) { | 334 if (attrInfo != null) { |
335 info.attributes[name] = attrInfo; | 335 info.attributes[name] = attrInfo; |
336 info.hasDataBinding = true; | 336 info.hasDataBinding = true; |
337 } | 337 } |
338 } | 338 } |
339 | 339 |
340 bool _readDataValue(ElementInfo info, String value) { | 340 bool _readDataValue(ElementInfo info, String value) { |
| 341 messages.warning('data-value is deprecated. ' |
| 342 'Given data-value="fieldName:expr", replace it with ' |
| 343 'field-name="{{expr}}". Unlike data-value, "expr" will be watched and ' |
| 344 'fieldName will automatically update. You may also use ' |
| 345 'bind-field-name="dartAssignableValue" to get two-way data binding.', |
| 346 info.node.sourceSpan, file: _fileInfo.path); |
| 347 |
341 var colonIdx = value.indexOf(':'); | 348 var colonIdx = value.indexOf(':'); |
342 if (colonIdx <= 0) { | 349 if (colonIdx <= 0) { |
343 messages.error('data-value attribute should be of the form ' | 350 messages.error('data-value attribute should be of the form ' |
344 'data-value="name:value" or data-value=' | 351 'data-value="name:value" or data-value=' |
345 '"name1:value1,name2:value2,..." for multiple assigments.', | 352 '"name1:value1,name2:value2,..." for multiple assigments.', |
346 info.node.sourceSpan, file: _fileInfo.path); | 353 info.node.sourceSpan, file: _fileInfo.path); |
347 return false; | 354 return false; |
348 } | 355 } |
349 var name = value.substring(0, colonIdx); | 356 var name = value.substring(0, colonIdx); |
350 value = value.substring(colonIdx + 1); | 357 value = value.substring(colonIdx + 1); |
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
422 var name = value.substring(0, colonIdx); | 429 var name = value.substring(0, colonIdx); |
423 value = value.substring(colonIdx + 1); | 430 value = value.substring(colonIdx + 1); |
424 | 431 |
425 return _readTwoWayBinding(info, name, value); | 432 return _readTwoWayBinding(info, name, value); |
426 } | 433 } |
427 | 434 |
428 // http://dev.w3.org/html5/spec/the-input-element.html#the-input-element | 435 // http://dev.w3.org/html5/spec/the-input-element.html#the-input-element |
429 /** Support for two-way bindings. */ | 436 /** Support for two-way bindings. */ |
430 bool _readTwoWayBinding(ElementInfo info, String name, String bindingExpr) { | 437 bool _readTwoWayBinding(ElementInfo info, String name, String bindingExpr) { |
431 var elem = info.node; | 438 var elem = info.node; |
432 var isInput = elem.tagName == 'input'; | 439 |
433 var isTextArea = elem.tagName == 'textarea'; | 440 // Find the HTML tag name. |
434 var isSelect = elem.tagName == 'select'; | 441 var isInput = info.baseTagName == 'input'; |
| 442 var isTextArea = info.baseTagName == 'textarea'; |
| 443 var isSelect = info.baseTagName == 'select'; |
435 var inputType = elem.attributes['type']; | 444 var inputType = elem.attributes['type']; |
436 | 445 |
437 String eventName; | 446 String eventName; |
438 | 447 |
439 // Special two-way binding logic for input elements. | 448 // Special two-way binding logic for input elements. |
440 if (isInput && name == 'checked') { | 449 if (isInput && name == 'checked') { |
441 if (inputType == 'radio') { | 450 if (inputType == 'radio') { |
442 if (!_isValidRadioButton(info)) return false; | 451 if (!_isValidRadioButton(info)) return false; |
443 } else if (inputType != 'checkbox') { | 452 } else if (inputType != 'checkbox') { |
444 messages.error('checked is only supported in HTML with type="radio" ' | 453 messages.error('checked is only supported in HTML with type="radio" ' |
445 'or type="checked".', info.node.sourceSpan, file: _fileInfo.path); | 454 'or type="checked".', info.node.sourceSpan, file: _fileInfo.path); |
446 return false; | 455 return false; |
447 } | 456 } |
448 | 457 |
449 // Both 'click' and 'change' seem reliable on all the modern browsers. | 458 // Both 'click' and 'change' seem reliable on all the modern browsers. |
450 eventName = 'change'; | 459 eventName = 'change'; |
451 } else if (isSelect && (name == 'selectedIndex' || name == 'value')) { | 460 } else if (isSelect && (name == 'selectedIndex' || name == 'value')) { |
452 eventName = 'change'; | 461 eventName = 'change'; |
453 } else if (isInput && name == 'value' && inputType == 'radio') { | 462 } else if (isInput && name == 'value' && inputType == 'radio') { |
454 return _addRadioValueBinding(info, bindingExpr); | 463 return _addRadioValueBinding(info, bindingExpr); |
455 } else if (isTextArea && name == 'value' || isInput && | 464 } else if (isTextArea && name == 'value' || isInput && |
456 (name == 'value' || name == 'valueAsDate' || name == 'valueAsNumber')) { | 465 (name == 'value' || name == 'valueAsDate' || name == 'valueAsNumber')) { |
457 // Input event is fired more frequently than "change" on some browsers. | 466 // Input event is fired more frequently than "change" on some browsers. |
458 // We want to update the value for each keystroke. | 467 // We want to update the value for each keystroke. |
459 eventName = 'input'; | 468 eventName = 'input'; |
| 469 } else if (info.component != null) { |
| 470 // Assume we are binding a field on the component. |
| 471 // TODO(jmesserly): validate this assumption about the user's code by |
| 472 // using compile time mirrors. |
| 473 |
| 474 _checkDuplicateAttribute(info, name); |
| 475 info.attributes[name] = new AttributeInfo([bindingExpr], |
| 476 customTwoWayBinding: true); |
| 477 info.hasDataBinding = true; |
| 478 return true; |
| 479 |
460 } else { | 480 } else { |
461 messages.error('Unknown two-way binding attribute $name. Ignored.', | 481 messages.error('Unknown two-way binding attribute $name. Ignored.', |
462 info.node.sourceSpan, file: _fileInfo.path); | 482 info.node.sourceSpan, file: _fileInfo.path); |
463 return false; | 483 return false; |
464 } | 484 } |
465 | 485 |
466 if (elem.attributes[name] != null) { | 486 _checkDuplicateAttribute(info, name); |
467 messages.warning('Duplicate attribute $name. You should provide either ' | |
468 'the two-way binding or the attribute itself. The attribute will be ' | |
469 'ignored.', info.node.sourceSpan, file: _fileInfo.path); | |
470 info.removeAttributes.add(name); | |
471 } | |
472 | 487 |
473 info.attributes[name] = new AttributeInfo([bindingExpr]); | 488 info.attributes[name] = new AttributeInfo([bindingExpr]); |
474 _addEvent(info, eventName, (e) => '$bindingExpr = $e.$name'); | 489 _addEvent(info, eventName, (e) => '$bindingExpr = $e.$name'); |
475 info.hasDataBinding = true; | 490 info.hasDataBinding = true; |
476 return true; | 491 return true; |
477 } | 492 } |
478 | 493 |
| 494 void _checkDuplicateAttribute(ElementInfo info, String name) { |
| 495 if (info.node.attributes[name] != null) { |
| 496 messages.warning('Duplicate attribute $name. You should provide either ' |
| 497 'the two-way binding or the attribute itself. The attribute will be ' |
| 498 'ignored.', info.node.sourceSpan, file: _fileInfo.path); |
| 499 info.removeAttributes.add(name); |
| 500 } |
| 501 } |
| 502 |
479 bool _isValidRadioButton(ElementInfo info) { | 503 bool _isValidRadioButton(ElementInfo info) { |
480 if (info.attributes['checked'] == null) return true; | 504 if (info.attributes['checked'] == null) return true; |
481 | 505 |
482 messages.error('Radio buttons cannot have both "checked" and "value" ' | 506 messages.error('Radio buttons cannot have both "checked" and "value" ' |
483 'two-way bindings. Either use checked:\n' | 507 'two-way bindings. Either use checked:\n' |
484 ' <input type="radio" bind-checked="myBooleanVar">\n' | 508 ' <input type="radio" bind-checked="myBooleanVar">\n' |
485 'or value:\n' | 509 'or value:\n' |
486 ' <input type="radio" bind-value="myStringVar" value="theValue">', | 510 ' <input type="radio" bind-value="myStringVar" value="theValue">', |
487 info.node.sourceSpan, file: _fileInfo.path); | 511 info.node.sourceSpan, file: _fileInfo.path); |
488 return false; | 512 return false; |
(...skipping 404 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
893 end = text.indexOf('}}', start); | 917 end = text.indexOf('}}', start); |
894 if (end < 0) { | 918 if (end < 0) { |
895 start = length; | 919 start = length; |
896 return false; | 920 return false; |
897 } | 921 } |
898 // For consistency, start and end both include the curly braces. | 922 // For consistency, start and end both include the curly braces. |
899 end += 2; | 923 end += 2; |
900 return true; | 924 return true; |
901 } | 925 } |
902 } | 926 } |
OLD | NEW |