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