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 /** Common utility functions used by code generated by the dwc compiler. */ | 5 /** Common utility functions used by code generated by the dwc compiler. */ |
6 library templating; | 6 library templating; |
7 | 7 |
8 import 'dart:async'; | 8 import 'dart:async'; |
| 9 import 'dart:collection'; |
9 import 'dart:html'; | 10 import 'dart:html'; |
10 import 'dart:uri'; | 11 import 'dart:uri'; |
11 import 'package:web_ui/safe_html.dart'; | 12 import 'package:web_ui/safe_html.dart'; |
| 13 import 'package:web_ui/observe.dart'; |
12 import 'package:web_ui/watcher.dart'; | 14 import 'package:web_ui/watcher.dart'; |
13 | 15 |
14 /** | 16 /** |
15 * Take the value of a bound expression and creates an HTML node with its value. | 17 * Take the value of a bound expression and creates an HTML node with its value. |
16 * Normally bindings are associated with text nodes, unless [binding] has the | 18 * Normally bindings are associated with text nodes, unless [binding] has the |
17 * [SafeHtml] type, in which case an html element is created for it. | 19 * [SafeHtml] type, in which case an html element is created for it. |
18 */ | 20 */ |
19 Node nodeForBinding(binding) => binding is SafeHtml | 21 Node nodeForBinding(binding) => binding is SafeHtml |
20 ? new Element.html(binding.toString()) : new Text(binding.toString()); | 22 ? new Element.html(binding.toString()) : new Text(binding.toString()); |
21 | 23 |
(...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
129 * * binding classes with a string: | 131 * * binding classes with a string: |
130 * | 132 * |
131 * bindCssClasses(e, () => "${class1 != null ? class1 : ''} " | 133 * bindCssClasses(e, () => "${class1 != null ? class1 : ''} " |
132 * "${class2 != null ? class2 : ''}"); | 134 * "${class2 != null ? class2 : ''}"); |
133 * | 135 * |
134 * * binding classes separately: | 136 * * binding classes separately: |
135 * | 137 * |
136 * bindCssClasses(e, () => class1); | 138 * bindCssClasses(e, () => class1); |
137 * bindCssClasses(e, () => class2); | 139 * bindCssClasses(e, () => class2); |
138 */ | 140 */ |
139 WatcherDisposer bindCssClasses(Element elem, dynamic exp()) { | 141 ChangeUnobserver bindCssClasses(Element elem, dynamic exp()) { |
140 return watchAndInvoke(exp, (e) { | 142 return watchAndInvoke(_observeList(exp), (e) { |
141 updateCssClass(elem, false, e.oldValue); | 143 updateCssClass(elem, false, e.oldValue); |
142 updateCssClass(elem, true, e.newValue); | 144 updateCssClass(elem, true, e.newValue); |
143 }, 'css-class-bind'); | 145 }, 'css-class-bind'); |
144 } | 146 } |
145 | 147 |
146 /** Bind the result of [exp] to the style attribute in [elem]. */ | 148 /** Bind the result of [exp] to the style attribute in [elem]. */ |
147 WatcherDisposer bindStyle(Element elem, Map<String, String> exp()) { | 149 ChangeUnobserver bindStyle(Element elem, Map<String, String> exp()) { |
148 return watchAndInvoke(exp, (e) { | 150 return watchAndInvoke(_observeMap(exp), (e) { |
149 if (e.oldValue is Map<String, String>) { | 151 if (e.oldValue is Map<String, String>) { |
150 var props = e.newValue; | 152 var props = e.newValue; |
151 if (props is! Map<String, String>) props = const {}; | 153 if (props is! Map<String, String>) props = const {}; |
152 for (var property in e.oldValue.keys) { | 154 for (var property in e.oldValue.keys) { |
153 if (!props.containsKey(property)) { | 155 if (!props.containsKey(property)) { |
154 // Value will not be overwritten with new setting. Remove. | 156 // Value will not be overwritten with new setting. Remove. |
155 elem.style.removeProperty(property); | 157 elem.style.removeProperty(property); |
156 } | 158 } |
157 } | 159 } |
158 } | 160 } |
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
226 | 228 |
227 void remove() { | 229 void remove() { |
228 _subscription.cancel(); | 230 _subscription.cancel(); |
229 _subscription = null; | 231 _subscription = null; |
230 } | 232 } |
231 } | 233 } |
232 | 234 |
233 /** Represents a generic data binding and a corresponding action. */ | 235 /** Represents a generic data binding and a corresponding action. */ |
234 class Binding extends TemplateItem { | 236 class Binding extends TemplateItem { |
235 final exp; | 237 final exp; |
236 final ValueWatcher action; | 238 final ChangeObserver action; |
237 WatcherDisposer stopper; | 239 ChangeUnobserver stopper; |
238 | 240 |
239 Binding(this.exp, this.action); | 241 Binding(this.exp, this.action); |
240 | 242 |
241 void insert() { | 243 void insert() { |
242 if (stopper != null) throw new StateError('binding already attached'); | 244 if (stopper != null) throw new StateError('binding already attached'); |
243 stopper = watchAndInvoke(exp, action, 'generic-binding'); | 245 stopper = watchAndInvoke(exp, action, 'generic-binding'); |
244 } | 246 } |
245 | 247 |
246 void remove() { | 248 void remove() { |
247 stopper(); | 249 stopper(); |
248 stopper = null; | 250 stopper = null; |
249 } | 251 } |
250 } | 252 } |
251 | 253 |
252 /** Represents a binding to a style attribute. */ | 254 /** Represents a binding to a style attribute. */ |
253 class StyleAttrBinding extends TemplateItem { | 255 class StyleAttrBinding extends TemplateItem { |
254 final exp; | 256 final exp; |
255 final Element elem; | 257 final Element elem; |
256 WatcherDisposer stopper; | 258 ChangeUnobserver stopper; |
257 | 259 |
258 StyleAttrBinding(this.elem, this.exp); | 260 StyleAttrBinding(this.elem, this.exp); |
259 | 261 |
260 void insert() { | 262 void insert() { |
261 if (stopper != null) throw new StateError('style binding already attached'); | 263 if (stopper != null) throw new StateError('style binding already attached'); |
262 stopper = bindStyle(elem, exp); | 264 stopper = bindStyle(elem, exp); |
263 } | 265 } |
264 | 266 |
265 void remove() { | 267 void remove() { |
266 stopper(); | 268 stopper(); |
267 stopper = null; | 269 stopper = null; |
268 } | 270 } |
269 } | 271 } |
270 | 272 |
271 /** Represents a binding to a class attribute. */ | 273 /** Represents a binding to a class attribute. */ |
272 class ClassAttrBinding extends TemplateItem { | 274 class ClassAttrBinding extends TemplateItem { |
273 final Element elem; | 275 final Element elem; |
274 final exp; | 276 final exp; |
275 WatcherDisposer stopper; | 277 ChangeUnobserver stopper; |
276 | 278 |
277 ClassAttrBinding(this.elem, this.exp); | 279 ClassAttrBinding(this.elem, this.exp); |
278 | 280 |
279 void insert() { | 281 void insert() { |
280 if (stopper != null) throw new StateError('class binding already attached'); | 282 if (stopper != null) throw new StateError('class binding already attached'); |
281 stopper = bindCssClasses(elem, exp); | 283 stopper = bindCssClasses(elem, exp); |
282 } | 284 } |
283 | 285 |
284 void remove() { | 286 void remove() { |
285 stopper(); | 287 stopper(); |
(...skipping 14 matching lines...) Expand all Loading... |
300 * or from a DOM property (which is internally also a Dart expression). | 302 * or from a DOM property (which is internally also a Dart expression). |
301 */ | 303 */ |
302 final Getter getter; | 304 final Getter getter; |
303 | 305 |
304 /** | 306 /** |
305 * Whether this is a binding that assigns a DOM attribute accepting URL | 307 * Whether this is a binding that assigns a DOM attribute accepting URL |
306 * values. If so, the value assigned to the attribute needs to be sanitized. | 308 * values. If so, the value assigned to the attribute needs to be sanitized. |
307 */ | 309 */ |
308 final bool isUrl; | 310 final bool isUrl; |
309 | 311 |
310 WatcherDisposer stopper; | 312 ChangeUnobserver stopper; |
311 | 313 |
312 DomPropertyBinding(this.getter, this.setter, this.isUrl); | 314 DomPropertyBinding(this.getter, this.setter, this.isUrl); |
313 | 315 |
314 void insert() { | 316 void insert() { |
315 if (stopper != null) throw new StateError('data binding already attached.'); | 317 if (stopper != null) throw new StateError('data binding already attached.'); |
316 stopper = watchAndInvoke(getter, (e) { | 318 stopper = watchAndInvoke(getter, (e) { |
317 setter(isUrl ? sanitizeUri(e.newValue) : e.newValue); | 319 setter(isUrl ? sanitizeUri(e.newValue) : e.newValue); |
318 }, 'dom-property-binding'); | 320 }, 'dom-property-binding'); |
319 } | 321 } |
320 | 322 |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
364 final List<Node> nodes = []; | 366 final List<Node> nodes = []; |
365 | 367 |
366 Template(this.node); | 368 Template(this.node); |
367 | 369 |
368 /** Associate the event listener while this template is visible. */ | 370 /** Associate the event listener while this template is visible. */ |
369 void listen(Stream<Event> stream, EventListener listener) { | 371 void listen(Stream<Event> stream, EventListener listener) { |
370 children.add(new Listener(stream, (e) { listener(e); dispatch(); })); | 372 children.add(new Listener(stream, (e) { listener(e); dispatch(); })); |
371 } | 373 } |
372 | 374 |
373 /** Run [action] when [exp] changes (while this template is visible). */ | 375 /** Run [action] when [exp] changes (while this template is visible). */ |
374 void bind(exp, ValueWatcher action) { | 376 void bind(exp, ChangeObserver action) { |
375 children.add(new Binding(exp, action)); | 377 children.add(new Binding(exp, action)); |
376 } | 378 } |
377 | 379 |
378 /** Create and bind a [Node] to [exp] while this template is visible. */ | 380 /** Create and bind a [Node] to [exp] while this template is visible. */ |
379 Node contentBind(Function exp) { | 381 Node contentBind(Function exp) { |
380 var bindNode = new Text(''); | 382 var bindNode = new Text(''); |
381 children.add(new Binding(() => '${exp()}', (e) { | 383 children.add(new Binding(() => '${exp()}', (e) { |
382 bindNode = updateBinding(exp(), bindNode, e.newValue); | 384 bindNode = updateBinding(exp(), bindNode, e.newValue); |
383 })); | 385 })); |
384 return bindNode; | 386 return bindNode; |
(...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
469 // </tr> | 471 // </tr> |
470 // | 472 // |
471 // We can't necessarily rely on child position because of possible mutation, | 473 // We can't necessarily rely on child position because of possible mutation, |
472 // unless we're willing to say that "if" requires a fixed number of children. | 474 // unless we're willing to say that "if" requires a fixed number of children. |
473 // If that's the case, we need a way to check for this error case and alert the | 475 // If that's the case, we need a way to check for this error case and alert the |
474 // developer. | 476 // developer. |
475 abstract class PlaceholderTemplate extends Template { | 477 abstract class PlaceholderTemplate extends Template { |
476 /** Expression watch by this template (condition or loop expression). */ | 478 /** Expression watch by this template (condition or loop expression). */ |
477 final exp; | 479 final exp; |
478 | 480 |
479 WatcherDisposer stopper; | 481 ChangeUnobserver stopper; |
480 | 482 |
481 PlaceholderTemplate(Node reference, this.exp) | 483 PlaceholderTemplate(Node reference, this.exp) |
482 : super(reference); | 484 : super(reference); |
483 | 485 |
484 // Delay creating the template body until this is inserted. | 486 // Delay creating the template body until this is inserted. |
485 void create() {} | 487 void create() {} |
486 | 488 |
487 void insert() { | 489 void insert() { |
488 super.create(); | 490 super.create(); |
489 if (nodes.length > 0) { | 491 if (nodes.length > 0) { |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
537 }, 'conditional-binding'); | 539 }, 'conditional-binding'); |
538 } | 540 } |
539 | 541 |
540 void remove() { | 542 void remove() { |
541 super.remove(); | 543 super.remove(); |
542 stopper(); | 544 stopper(); |
543 stopper = null; | 545 stopper = null; |
544 } | 546 } |
545 } | 547 } |
546 | 548 |
| 549 _observeList(exp) { |
| 550 if (!useObservers) return exp; |
| 551 |
| 552 // TODO(jmesserly): implement detailed change records in Observable* types, |
| 553 // so we can observe the list itself and react to all changes. |
| 554 // For now we call .toList which causes us to depend on all items. This |
| 555 // also works for Iterables. |
| 556 return () { |
| 557 var x = exp(); |
| 558 return x is Iterable ? x.toList() : x; |
| 559 }; |
| 560 } |
| 561 |
| 562 _observeMap(exp) { |
| 563 if (!useObservers) return exp; |
| 564 |
| 565 // TODO(jmesserly): this has similar issues as _observeList. Ideally |
| 566 // observers can observe all changes to the resulting map, so we don't need a |
| 567 // copy here. |
| 568 return () { |
| 569 var x = exp(); |
| 570 return x is Map ? new LinkedHashMap.from(x) : x; |
| 571 }; |
| 572 } |
| 573 |
547 /** Function to set up the contents of a loop template. */ | 574 /** Function to set up the contents of a loop template. */ |
548 typedef void LoopIterationSetup(loopVariable, Template template); | 575 typedef void LoopIterationSetup(loopVariable, Template template); |
549 | 576 |
550 /** A template loop of the form `<template iterate="x in list ">`. */ | 577 /** A template loop of the form `<template iterate="x in list ">`. */ |
551 class LoopTemplate extends PlaceholderTemplate { | 578 class LoopTemplate extends PlaceholderTemplate { |
552 final LoopIterationSetup iterSetup; | 579 final LoopIterationSetup iterSetup; |
553 | 580 |
554 LoopTemplate(Node reference, exp, this.iterSetup) : super(reference, exp); | 581 LoopTemplate(Node reference, exp, this.iterSetup) : super(reference, exp); |
555 | 582 |
556 void insert() { | 583 void insert() { |
557 stopper = watchAndInvoke(exp, (e) { | 584 stopper = watchAndInvoke(_observeList(exp), (e) { |
558 super.remove(); | 585 super.remove(); |
559 for (var x in e.newValue) { | 586 for (var x in e.newValue) { |
560 iterSetup(x, this); | 587 iterSetup(x, this); |
561 } | 588 } |
562 super.insert(); | 589 super.insert(); |
563 }, 'loop-binding'); | 590 }, 'loop-binding'); |
564 } | 591 } |
565 | 592 |
566 void remove() { | 593 void remove() { |
567 super.remove(); | 594 super.remove(); |
568 stopper(); | 595 stopper(); |
569 stopper = null; | 596 stopper = null; |
570 } | 597 } |
571 } | 598 } |
572 | 599 |
573 /** | 600 /** |
574 * A template loop of the form `<td template iterate="x in list ">`. Unlike | 601 * A template loop of the form `<td template iterate="x in list ">`. Unlike |
575 * [LoopTemplate], here we insert children directly then node annotated with the | 602 * [LoopTemplate], here we insert children directly then node annotated with the |
576 * template attribute. | 603 * template attribute. |
577 */ | 604 */ |
578 class LoopTemplateInAttribute extends Template { | 605 class LoopTemplateInAttribute extends Template { |
579 final LoopIterationSetup iterSetup; | 606 final LoopIterationSetup iterSetup; |
580 final exp; | 607 final exp; |
581 WatcherDisposer stopper; | 608 ChangeUnobserver stopper; |
582 | 609 |
583 LoopTemplateInAttribute(Node node, this.exp, this.iterSetup) : super(node); | 610 LoopTemplateInAttribute(Node node, this.exp, this.iterSetup) : super(node); |
584 | 611 |
585 // Delay creating the template body until this is inserted. | 612 // Delay creating the template body until this is inserted. |
586 void create() {} | 613 void create() {} |
587 | 614 |
588 void insert() { | 615 void insert() { |
589 stopper = watchAndInvoke(exp, (e) { | 616 stopper = watchAndInvoke(_observeList(exp), (e) { |
590 _removeInternal(); | 617 _removeInternal(); |
591 for (var x in e.newValue) { | 618 for (var x in e.newValue) { |
592 iterSetup(x, this); | 619 iterSetup(x, this); |
593 } | 620 } |
594 super.create(); | 621 super.create(); |
595 node.nodes.addAll(nodes); | 622 node.nodes.addAll(nodes); |
596 super.insert(); | 623 super.insert(); |
597 }, 'loop-attribute-binding'); | 624 }, 'loop-attribute-binding'); |
598 } | 625 } |
599 | 626 |
600 void _removeInternal() { | 627 void _removeInternal() { |
601 super.remove(); | 628 super.remove(); |
602 node.nodes.clear(); | 629 node.nodes.clear(); |
603 nodes.clear(); | 630 nodes.clear(); |
604 } | 631 } |
605 | 632 |
606 void remove() { | 633 void remove() { |
607 _removeInternal(); | 634 _removeInternal(); |
608 stopper(); | 635 stopper(); |
609 stopper = null; | 636 stopper = null; |
610 } | 637 } |
611 } | 638 } |
OLD | NEW |