diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c55b5165..4562060f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,91 @@ + +# v0.9.8 cozy-porcupine (2014-02-19) + + +## Bug Fixes + +- **DateFilter:** fix a wrong type + ([cec3edad](https://github.com/angular/angular.dart/commit/cec3edad1944a8411882b0a87ea6193c25513392), + [#579](https://github.com/angular/angular.dart/issues/579)) +- **compiler:** support filters in attribute expressions + ([8f020f99](https://github.com/angular/angular.dart/commit/8f020f998e8a4b7d5b595e5c44086fa2628fe8b3), + [#571](https://github.com/angular/angular.dart/issues/571), [#580](https://github.com/angular/angular.dart/issues/580)) +- **di:** Upgrade dependency of package di preventing problems with dart sdk 1.1 resolves #408 + ([1f85a8ce](https://github.com/angular/angular.dart/commit/1f85a8cee164d85d6eed43e7604a0190d1542d84), + [#408](https://github.com/angular/angular.dart/issues/408), [#583](https://github.com/angular/angular.dart/issues/583)) +- **doc-gen:** dartbug.com/16752 + ([9a1ef31d](https://github.com/angular/angular.dart/commit/9a1ef31d66f151f22b79893e11251a6780605257)) +- **generator:** remove invalid sort on elements + ([e2a00abe](https://github.com/angular/angular.dart/commit/e2a00abe371bb2d9d3c1d3c19849e075a32e92e4), + [#554](https://github.com/angular/angular.dart/issues/554)) +- **ng-attr:** remove camel-cased dom attributes + ([b5e45117](https://github.com/angular/angular.dart/commit/b5e45117c17fdd07d5db659815eb49c2dca17b84), + [#567](https://github.com/angular/angular.dart/issues/567)) +- **ng-pluralize:** use ${..} to interpolate + ([a630487d](https://github.com/angular/angular.dart/commit/a630487d302e396a920e02c8db5d256a81d3dd1a), + [#572](https://github.com/angular/angular.dart/issues/572)) +- **ng-value:** Add ng-value support for checked/radio/option + ([8fc2c0f4](https://github.com/angular/angular.dart/commit/8fc2c0f49aabc53ee6240ad8063ecf6c9c8b8a1f)) +- **ngModel:** + - ensure checkboxes and radio buttons are flagged as dirty when changed + ([5766a6a1](https://github.com/angular/angular.dart/commit/5766a6a173dc1d65b9293fd5bd0bcbc21b0791ec), + [#569](https://github.com/angular/angular.dart/issues/569), [#585](https://github.com/angular/angular.dart/issues/585)) + - process input type=number according to convention, using valueAsNumber + ([cf0160b8](https://github.com/angular/angular.dart/commit/cf0160b8c316a39ac9d0fcce843c6f764429a1d4), + [#574](https://github.com/angular/angular.dart/issues/574), [#577](https://github.com/angular/angular.dart/issues/577)) + - ensure validation occurs when the model value changes upon digest + ([f34e0b31](https://github.com/angular/angular.dart/commit/f34e0b31a6f2f42457a6d1a1b5b5aaa7e2ef86fe)) +- **ngShow:** Add/remove ng-hide class instead of ng-show class + ([0b88d2e8](https://github.com/angular/angular.dart/commit/0b88d2e8102db8b89f38b00c277b9023b260285e), + [#521](https://github.com/angular/angular.dart/issues/521)) +- **package.json:** add repo, licenses and switch to devDependencies + ([d099db59](https://github.com/angular/angular.dart/commit/d099db5944e2287fbf97a13b1aa73f8082652e09), + [#544](https://github.com/angular/angular.dart/issues/544), [#545](https://github.com/angular/angular.dart/issues/545)) +- **scope:** Use Iterable instead of List + ([951fa178](https://github.com/angular/angular.dart/commit/951fa1783afa65f410a2b82249850eed458ed294), + [#565](https://github.com/angular/angular.dart/issues/565)) + + +## Features + +- **forms:** + - generate ng-submit-valid / ng-submit-invalid CSS classes upon form submission + ([4bf9447c](https://github.com/angular/angular.dart/commit/4bf9447cc64650d6c73b66c844fb5396b4a2ae27)) + - provide support for reseting forms, fieldsets and models + ([c75202d5](https://github.com/angular/angular.dart/commit/c75202d5d7ecabd01366f2198e0c0c3b5c087e59)) +- **ngModel:** Treat the values of number and range inputs as numbers + ([e703bd1b](https://github.com/angular/angular.dart/commit/e703bd1bc75f4d6420afad0bbb975b3e23672ff8), + [#527](https://github.com/angular/angular.dart/issues/527)) + + +## Breaking Changes +- **ng-attr** + - Due to ([b5e45117](https://github.com/angular/angular.dart/commit/b5e45117c17fdd07d5db659815eb49c2dca17b84), + mappings in annotations must use snake-case-names instead of + camelCaseNames.  To migrate your code, follow the example below: + + Before: + + @NgComponent( + // … + map: const { + 'domAttributeName': '=>fieldSetter' + } + ) + class MyComponent { … + + After: + + @NgComponent( + // … + map: const { + 'dom-attribute-name': '=>fieldSetter' + } + ) + class MyComponent { … + + + # v0.9.7 pachyderm-moisturization (2014-02-10) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8a90dde5..3d12f293a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,11 +90,7 @@ Before you submit your pull request consider the following guidelines: git commit -a ``` -* Build your changes locally to ensure all the tests pass - - ```shell - ./run-test.sh - ``` +* Build your changes locally to ensure all the tests pass: see the [developer documentation][dev-doc]. * Push your branch to Github: @@ -226,3 +222,4 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise [commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# [communityMilestone]: https://github.com/angular/angular.dart/issues?milestone=13&state=open [coc]: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md +[dev-doc]: https://github.com/angular/angular.dart/blob/master/DEVELOPER.md diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 000000000..ec2f93254 --- /dev/null +++ b/DEVELOPER.md @@ -0,0 +1,167 @@ +# Building and Testing AngularDart + +This document describes how to set up your development environment to build and test AngularDart, and +explains the basic mechanics of using `git`, `node`, and `npm`. + +See the [contributing guidelines](https://github.com/angular/angular.dart/blob/master/CONTRIBUTING.md) for how to contribute your own code to + +1. [Prerequisite Software](#prerequisite-software) +2. [Getting the Sources](#getting-the-sources) +3. [Environment Variable Setup](#environment-variable-setup) +4. [Installing NPM Modules and Dart Packages](#installing-npm-modules-and-dart-packages) +5. [Running Tests Locally](#running-tests-locally) +6. [Continuous Integration using Travis](#continuous-integration-using-travis) + +## Prerequisite Software + +Before you can build and test AngularDart, you must install and configure the +following products on your development machine: + +* [Dart](https://www.dartlang.org/): as can be expected, AngularDart requires + an installation of the Dart-SDK and Dartium (a version of + [Chromium](http://www.chromium.org) with native support for Dart through the + Dart VM). One of the **simplest** ways to get both is to install the **Dart + Editor bundle**, which includes the editor, sdk and Dartium. See the [Dart + tools download page for + instructions](https://www.dartlang.org/tools/download.html). + +* [Git](http://git-scm.com/) and/or the **Github app** (for + [Mac](http://mac.github.com/) or [Windows](http://windows.github.com/)): the + [Github Guide to Installing + Git](https://help.github.com/articles/set-up-git) is a good source of + information. + +* [Node.js](http://nodejs.org): We use Node to run a development web server, + run tests, and generate distributable files. We also use Node's Package + Manager (`npm`). Depending on your system, you can install Node either from + source or as a pre-packaged bundle. + +## Getting the Sources + +Forking and Cloning the AngularDart repository: + +1. Login to your Github account or create one by following the instructions given [here](https://github.com/signup/free). +Afterwards. +2. [Fork](http://help.github.com/forking) the [main AngularDart repository](https://github.com/angular/angular.dart). +3. Clone your fork of the AngularDart repository and define an `upstream` remote pointing back to the AngularDart repository that you forked in the first place: + +```shell +# Clone your Github repository: +git clone git@github.com:/angular.dart.git + +# Go to the AngularDart directory: +cd angular.dart + +# Add the main AngularDart repository as an upstream remote to your repository: +git remote add upstream https://github.com/angular/angular.dart.git +``` + +## Environment Variable Setup + + +Define the environment variables listed below. These are mainly needed for the +test scripts. The notation shown here is for +[`bash`](http://www.gnu.org/software/bash/); adapt as appropriate for your +favorite shell. (Examples given below of possible values for initializing the +environment variables assume Mac OS X and that you have installed the Dart +Editor in the directory named by `$DART_EDITOR_DIR`. This is only for +illustrative purposes.) + +```shell +# CHROME_BIN: path to a Chrome browser executable; e.g., +export CHROME_BIN="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + +# CHROME_CANARY_BIN: path to a Dartium browser executable; e.g., +export CHROME_CANARY_BIN="$DART_EDITOR_DIR/chromium/Chromium.app/Contents/MacOS/Chromium" +``` +**Note**: the `$CHROME_CANARY_BIN` environment variable is used by karma to run your tests +in dartium instead of chromium. If you don't do this, the dart2js compile will make the tests +run extremely slow since it has to wait for a full js compile each time. + +You should also add the Dart SDK `bin` directory to your path and/or define `DART_SDK`; e.g. + +```shell +# DART_SDK: path to a Dart SDK directory; e.g., +export DART_SDK="$DART_EDITOR_DIR/dart-sdk" + +# Update PATH to include the Dart SDK bin directory +PATH+=":$DART_SDK/bin" +``` +## Installing NPM Modules and Dart Packages + +Next, install the modules and packages needed to run AngularDart tests: + +```shell +# Install node.js dependencies: +npm install + +# Install karma onto your command line (optional) +npm install karma -g + +# Install Dart packages +pub install +``` + +## Running Tests Locally + +NOTE: scripts are being written to embody the following steps. + +To run base tests: + +```shell +# Source a script to define yet more environment variables +. ./scripts/env.sh + +# Run io tests: +dart --checked test/io/all.dart + +# Run expression extractor tests: +scripts/test-expression-extractor.sh + +Run the Dart Analyzer: +./scripts/analyze.sh +``` + +To run Karma tests over Dartium, execute the following shell commands (which +will launch the Karma server): + +```shell +. ./scripts/env.sh +node "node_modules/karma/bin/karma" start karma.conf \ + --reporters=junit,dots --port=8765 --runner-port=8766 \ + --browsers=Dartium +``` + +In another shell window or tab, or from your favorite IDE, launch the Karma +tests proper by executing: + +```shell +. ./scripts/env.sh +karma_run.sh +``` + +**Note:**: If the dart analyzer fails with warnings, the tests will not run. +You can manually run the tests if this happens: + +```shell +karma run --port=8765 +``` + +## Debugging + +In the dart editor you can configure a dartium launch target for the karma test runner debug page. +The menu option is under Run > Manage Launches > Create new Dartium Launch. + +``` +http://localhost:8765/debug.html +``` + +If you want to only run a single test you can alter the test you wish to run by changing `it` to `iit` +or `describe` to `ddescribe`. This will only run that individual test and make it much easier to debug. + + +## Continuous Integration using Travis + +See the instructions given [here](https://github.com/angular/angular.dart/blob/master/travis.md). + +----- diff --git a/demo/animate_demo/animate_demo.dart b/demo/animate_demo/animate_demo.dart new file mode 100644 index 000000000..7d68b78da --- /dev/null +++ b/demo/animate_demo/animate_demo.dart @@ -0,0 +1,85 @@ +import 'dart:html' as dom; + +import 'package:angular/angular.dart'; +import 'package:angular/animate/module.dart'; + +// This annotation allows Dart to shake away any classes +// not used from Dart code nor listed in another @MirrorsUsed. +// +// If you create classes that are referenced from the Angular +// expressions, you must include a library target in @MirrorsUsed. +@MirrorsUsed(override: '*') +import 'dart:mirrors'; + +@NgController( + selector: '[animation-demo]', + publishAs: 'adc' +) +class AnimationDemoController { + final dom.Element rootElement; + final NgAnimate animate; + bool areThingsVisible = false; + bool boxToggle = false; + bool ifToggle = false; + int thingNumber = 1; + String currentThing; + + dom.Element _boxElement; + dom.Element _hostElement; + List _animatedBoxes = []; + List listOfThings = []; + + AnimationDemoController(this.animate, this.rootElement) { + _boxElement = rootElement.querySelector(".animated-box"); + _hostElement = rootElement.querySelector(".animated-host"); + } + + animateABox() { + if(_boxElement != null) { + if(boxToggle) { + animate.removeClass([_boxElement], "magic"); + } else { + animate.addClass([_boxElement], "magic"); + } + boxToggle = !boxToggle; + } + } + + toggleABunchOfThings() { + if(_hostElement != null) { + if(!areThingsVisible && _animatedBoxes.length == 0) { + + for(int i = 0; i < 1000; i++) { + var element = new dom.Element.div(); + element.classes.add("magic-box"); + _animatedBoxes.add(element); + } + animate.insert(_animatedBoxes, _hostElement); + } else if (!areThingsVisible) { + // I'm not sure what to do about this + animate.insert(_animatedBoxes, _hostElement); + } else if (_animatedBoxes.length > 0) { + animate.remove(_animatedBoxes); + } + + areThingsVisible = !areThingsVisible; + } + } + + addThing() { + listOfThings.add("Thing-$thingNumber"); + thingNumber++; + } + + removeThing() { + if(listOfThings.length > 0) { + listOfThings.removeLast(); + } + } +} + +main() { + ngBootstrap(module: new Module() + ..install(new NgAnimateModule()) + ..type(AnimationDemoController)); +} diff --git a/demo/animate_demo/index.html b/demo/animate_demo/index.html new file mode 100644 index 000000000..0c7d6c3e2 --- /dev/null +++ b/demo/animate_demo/index.html @@ -0,0 +1,53 @@ + + + + Hello, World! + + + + +

ng-repeat Demo

+ + +
    +
  • +
      +
    • + {{thing2}} +
    • +
    +
  • +
+ + +

ng-switch Demo

+ + + + +
+
I'm THING A
+
I'm THING B
+
I'm THING C
+
Uhhh I'm NoThing
+
+ + +

ng-if Demo

+ +
+ I am a div with content. Just your standared ng-if. +
+
+ +

Animate Demos

+ +
+ + +
+ + + + + diff --git a/demo/animate_demo/pubspec.lock b/demo/animate_demo/pubspec.lock new file mode 100644 index 000000000..72ea5ab87 --- /dev/null +++ b/demo/animate_demo/pubspec.lock @@ -0,0 +1,81 @@ +# Generated by pub +# See http://pub.dartlang.org/doc/glossary.html#lockfile +packages: + analyzer: + description: analyzer + source: hosted + version: "0.10.5" + angular: + description: + path: "../.." + relative: true + source: path + version: "0.9.7" + args: + description: args + source: hosted + version: "0.9.0" + browser: + description: browser + source: hosted + version: "0.9.1" + collection: + description: collection + source: hosted + version: "0.9.1" + di: + description: di + source: hosted + version: "0.0.32" + html5lib: + description: html5lib + source: hosted + version: "0.9.1" + intl: + description: intl + source: hosted + version: "0.9.1" + logging: + description: logging + source: hosted + version: "0.9.1+1" + path: + description: path + source: hosted + version: "1.0.0" + perf_api: + description: perf_api + source: hosted + version: "0.0.8" + quiver: + description: quiver + source: hosted + version: "0.17.0" + route_hierarchical: + description: route_hierarchical + source: hosted + version: "0.4.14" + shadow_dom: + description: shadow_dom + source: hosted + version: "0.9.2" + source_maps: + description: source_maps + source: hosted + version: "0.9.0" + stack_trace: + description: stack_trace + source: hosted + version: "0.9.1" + unittest: + description: unittest + source: hosted + version: "0.10.0" + unmodifiable_collection: + description: unmodifiable_collection + source: hosted + version: "0.9.2+1" + utf: + description: utf + source: hosted + version: "0.9.0" diff --git a/demo/animate_demo/pubspec.yaml b/demo/animate_demo/pubspec.yaml new file mode 100644 index 000000000..7256f7122 --- /dev/null +++ b/demo/animate_demo/pubspec.yaml @@ -0,0 +1,7 @@ +name: angular_animate_demo +version: 0.0.1 +dependencies: + angular: + path: ../.. + browser: any + unittest: any diff --git a/demo/animate_demo/style.css b/demo/animate_demo/style.css new file mode 100644 index 000000000..0c80f3225 --- /dev/null +++ b/demo/animate_demo/style.css @@ -0,0 +1,180 @@ +[ng-cloak] { + display: none !important; +} + +body { + font-family: "Open Sans" sans-serif; + font-size: 12px; + color: #303030; +} + +.thingy .thingy { + display: inline-block; + min-width: 50px; +} + +.thingy.ng-insert { + transform: scale(1.1, 1.1); + -webkit-transform: scale(1.1,1.1); + + opacity: 0; + transition: all 1000ms; +} + +.thingy.ng-insert.ng-insert-active { + transform: scale(1.0, 1.0); + -webkit-transform: scale(1.0,1.0); + + opacity: 1; +} + +.thingy .thingy.ng-insert, +.thingy .thingy.ng-remove { + color: #000; +} + + +.thingy .thingy.ng-insert.ng-insert-active, +.thingy .thingy.ng-remove.ng-remove-active { + color: #F00; +} + + +.thingy.ng-remove { + transform: scale(1.0, 1.0); + -webkit-transform: scale(1.0,1.0); + + opacity: 1; + transition: all 1000ms; +} + +.thingy.ng-remove.ng-remove-active { + transform: scale(1.1, 1.1); + -webkit-transform: scale(1.1,1.1); + + opacity: 0; +} + +.some-div { + height: 100px; + opacity: 1; +} + +.some-div.ng-insert { + opacity: 0; + height: 0; + transition: all 1000ms ease; +} + +.some-div.ng-insert.ng-insert-active { + opacity: 1; + height: 100px; +} + +.some-div.ng-remove { + opacity: 1; + height: 100px; + transition: all 1000ms ease; +} + +.some-div.ng-remove.ng-remove-active { + opacity: 0; + height: 0px; +} + +.animated-box { + margin-top: 20px; + margin-bottom: 20px; + width: 100px; + height: 100px; + background-color: #1080F0; +} + +.animated-box.magic-add { + transition: margin-left 1000ms ease; +} + +.animated-box.magic-remove { + transition: margin-left 3000ms ease; +} + +.animated-box.magic-add.magic-add-active { + margin-left: 200px; +} + +.animated-box.magic-remove.magic-remove-active { + margin-left: 0px; +} + +.animated-box.magic { + margin-left: 200px; +} + +.animated-host { + margin-top: 10px; + height: 1000px; + width: 625px; + line-height: 25px; +} + + +.magic-box { + display: block; + margin-right: 5px; + margin-top: 5px; + + width: 20px; + height: 20px; + background-color: #1080F0; + line-height: 20px; + float: left; + opacity: 1; +/* + opacity: 1; + transform: scale(1.0,1.0); + -webkit-transform: scale(1.0,1.0); */ +} + +.magic-box.ng-insert { + opacity: 0; + transition: all 1000ms ease; +} + +.magic-box.ng-insert.ng-insert-active { + opacity: 1; +} + +.magic-box.ng-remove { + opacity: 1; + transition: all 1000ms ease; +} + +.magic-box.ng-remove.ng-remove-active { + opacity: 0; +} +/* + +.magic-box.ng-insert { + transform: scale(0.0, 0.0); + -webkit-transform: scale(0.0,0.0); + + opacity: 0; + transition: all 10000ms ease; +} + +.magic-box.ng-insert.ng-insert-active { + transform: scale(1.0, 1.0); + -webkit-transform: scale(1.0, 1.0); + opacity: 0.5; +} + +.magic-box.ng-remove { + transform: scale(1.0, 1.0); + -webkit-transform: scale(1.0, 1.0); + transition: all 500ms ease; +} +.magic-box.ng-remove.ng-remove-active { + transform: scale(1.1, 1.1); + -webkit-transform: scale(1.1, 1.1); + opacity: 0; +} */ \ No newline at end of file diff --git a/demo/bouncing_balls/bouncy_balls.dart b/demo/bouncing_balls/bouncy_balls.dart index a1734d203..621fc5c80 100644 --- a/demo/bouncing_balls/bouncy_balls.dart +++ b/demo/bouncing_balls/bouncy_balls.dart @@ -17,7 +17,7 @@ class BallModel { static _color() { var color = '#'; - for(var i=0; i < 6; i++) { + for(var i = 0; i < 6; i++) { color += (16 * random.nextDouble()).floor().toRadixString(16); } return color; @@ -27,58 +27,59 @@ class BallModel { @NgController( selector: '[bounce-controller]', - publishAs: 'bounce' -) + publishAs: 'bounce') class BounceController { var lastTime = window.performance.now(); var run = true; var fps = 0; var digestTime = 0; + var currentDigestTime = 0; var balls = []; - var zone; - var scope; + final NgZone zone; + final Scope scope; var ballClassName = 'ball'; - BounceController(NgZone this.zone, Scope this.scope) { + BounceController(this.zone, this.scope) { changeCount(100); tick(); } - toggleCSS() { + void toggleCSS() { ballClassName = ballClassName == '' ? 'ball' : ''; } - playPause() { + void playPause() { run = !run; if (run) requestAnimationFrame(tick); } - requestAnimationFrame(fn) { + void requestAnimationFrame(fn) { window.requestAnimationFrame((_) => zone.run(fn)); } - changeCount(count) { + void changeCount(count) { while(count > 0) { balls.add(new BallModel()); count--; } - while(count < 0) { + while(count < 0 && balls.isNotEmpty) { balls.removeAt(0); count++; } tick(); } - timeDigest() { + void timeDigest() { var start = window.performance.now(); - scope.$evalAsync(() { - digestTime = (window.performance.now() - start).round(); - }, outsideDigest: true); + digestTime = currentDigestTime; + scope.rootScope.domRead(() { + currentDigestTime = (window.performance.now() - start).round(); + }); } - tick() { - var now = window.performance.now(), - delay = now - lastTime; + void tick() { + var now = window.performance.now(); + var delay = now - lastTime; fps = (1000/delay).round(); for(var i=0, ii=balls.length; iposition' - } -) + "ballPosition": '=>position'}) class BallPositionDirective { - Element element; - Scope scope; - BallPositionDirective(Element this.element, Scope this.scope); + final Element element; + final Scope scope; + BallPositionDirective(this.element, this.scope); set position(BallModel model) { element.style.backgroundColor = model.color; - scope.$watch(() { - element.style.left = '${model.x + 10}px'; - element.style.top = '${model.y + 10}px'; - }); + scope + ..watch('x', (x, _) => element.style.left = '${x + 10}px', context: model, readOnly: true) + ..watch('y', (y, _) => element.style.top = '${y + 10}px', context: model, readOnly: true); } } diff --git a/demo/bouncing_balls/index.html b/demo/bouncing_balls/index.html index 267371c28..568d6336d 100644 --- a/demo/bouncing_balls/index.html +++ b/demo/bouncing_balls/index.html @@ -45,7 +45,9 @@
-
+
+
+
{{bounce.fps}} fps. ({{bounce.balls.length}} balls) [{{(1000/bounce.fps).round()}} ms]
diff --git a/demo/bouncing_balls/pubspec.lock b/demo/bouncing_balls/pubspec.lock index 989a95424..8fbecc02c 100644 --- a/demo/bouncing_balls/pubspec.lock +++ b/demo/bouncing_balls/pubspec.lock @@ -5,16 +5,12 @@ packages: description: analyzer source: hosted version: "0.10.5" - analyzer_experimental: - description: analyzer_experimental - source: hosted - version: "0.8.6" angular: description: - path: "/usr/local/google/gits/angular-dart/demo/bouncing_balls/../.." + path: "../.." relative: true source: path - version: "0.9.3" + version: "0.9.7" args: description: args source: hosted @@ -22,31 +18,27 @@ packages: browser: description: browser source: hosted - version: "0.8.7" + version: "0.9.1" collection: description: collection source: hosted - version: "0.9.0" + version: "0.9.1" di: description: di source: hosted - version: "0.0.24" + version: "0.0.32" html5lib: description: html5lib source: hosted - version: "0.8.7" + version: "0.9.1" intl: description: intl source: hosted - version: "0.8.7" + version: "0.9.1" logging: description: logging source: hosted version: "0.9.1+1" - meta: - description: meta - source: hosted - version: "0.8.7" path: description: path source: hosted @@ -58,24 +50,28 @@ packages: route_hierarchical: description: route_hierarchical source: hosted - version: "0.4.7" + version: "0.4.14" + shadow_dom: + description: shadow_dom + source: hosted + version: "0.9.1" source_maps: description: source_maps source: hosted - version: "0.8.7" + version: "0.9.0" stack_trace: description: stack_trace source: hosted - version: "0.8.7" + version: "0.9.1" unittest: description: unittest source: hosted - version: "0.8.7" + version: "0.10.0" unmodifiable_collection: description: unmodifiable_collection source: hosted - version: "0.9.2" + version: "0.9.2+1" utf: description: utf source: hosted - version: "0.8.7" + version: "0.9.0" diff --git a/demo/helloworld/helloworld.dart b/demo/helloworld/helloworld.dart index 50e5d2c57..338e33bb6 100644 --- a/demo/helloworld/helloworld.dart +++ b/demo/helloworld/helloworld.dart @@ -10,8 +10,7 @@ import 'dart:mirrors'; @NgController( selector: '[hello-world-controller]', - publishAs: 'ctrl' -) + publishAs: 'ctrl') class HelloWorldController { String name = "world"; } diff --git a/demo/helloworld/pubspec.lock b/demo/helloworld/pubspec.lock new file mode 100644 index 000000000..8fbecc02c --- /dev/null +++ b/demo/helloworld/pubspec.lock @@ -0,0 +1,77 @@ +# Generated by pub +# See http://pub.dartlang.org/doc/glossary.html#lockfile +packages: + analyzer: + description: analyzer + source: hosted + version: "0.10.5" + angular: + description: + path: "../.." + relative: true + source: path + version: "0.9.7" + args: + description: args + source: hosted + version: "0.9.0" + browser: + description: browser + source: hosted + version: "0.9.1" + collection: + description: collection + source: hosted + version: "0.9.1" + di: + description: di + source: hosted + version: "0.0.32" + html5lib: + description: html5lib + source: hosted + version: "0.9.1" + intl: + description: intl + source: hosted + version: "0.9.1" + logging: + description: logging + source: hosted + version: "0.9.1+1" + path: + description: path + source: hosted + version: "1.0.0" + perf_api: + description: perf_api + source: hosted + version: "0.0.8" + route_hierarchical: + description: route_hierarchical + source: hosted + version: "0.4.14" + shadow_dom: + description: shadow_dom + source: hosted + version: "0.9.1" + source_maps: + description: source_maps + source: hosted + version: "0.9.0" + stack_trace: + description: stack_trace + source: hosted + version: "0.9.1" + unittest: + description: unittest + source: hosted + version: "0.10.0" + unmodifiable_collection: + description: unmodifiable_collection + source: hosted + version: "0.9.2+1" + utf: + description: utf + source: hosted + version: "0.9.0" diff --git a/demo/todo/pubspec.lock b/demo/todo/pubspec.lock index dca084f5b..407ce085f 100644 --- a/demo/todo/pubspec.lock +++ b/demo/todo/pubspec.lock @@ -10,7 +10,7 @@ packages: path: "../.." relative: true source: path - version: "0.9.4" + version: "0.9.7" args: description: args source: hosted @@ -47,10 +47,14 @@ packages: description: perf_api source: hosted version: "0.0.8" + quiver: + description: quiver + source: hosted + version: "0.17.0" route_hierarchical: description: route_hierarchical source: hosted - version: "0.4.10" + version: "0.4.14" shadow_dom: description: shadow_dom source: hosted @@ -66,11 +70,11 @@ packages: unittest: description: unittest source: hosted - version: "0.9.3" + version: "0.10.0" unmodifiable_collection: description: unmodifiable_collection source: hosted - version: "0.9.2" + version: "0.9.2+1" utf: description: utf source: hosted diff --git a/demo/todo/web/index.html b/demo/todo/web/index.html index a3010d815..7cea710c6 100644 --- a/demo/todo/web/index.html +++ b/demo/todo/web/index.html @@ -8,33 +8,32 @@ -
Wait, Dart is loading this awesome app...
+
Wait, Dart is loading this awesome app...
-
-

Things To Do ;-)

+
+

Things To Do ;-)

-
- - -
- -

Remaining {{todo.remaining()}} of {{todo.items.length}} items.

+
+ + +
+

Remaining {{todo.remaining()}} of {{todo.items.length}} items.

-
    -
  • - -
  • -
-
- - - -
+
    +
  • + +
  • +
-
+
+ + + +
+
diff --git a/demo/todo/web/main.dart b/demo/todo/web/main.dart index 5c67d8084..3f34f0a85 100644 --- a/demo/todo/web/main.dart +++ b/demo/todo/web/main.dart @@ -5,18 +5,17 @@ import 'todo.dart'; import 'dart:html'; -// Everything in the 'todo' library should be preserved by MirrorsUsed +// Everything in the 'todo' library should be preserved by MirrorsUsed. @MirrorsUsed( - targets: const['todo'], + targets: const ['todo'], override: '*') import 'dart:mirrors'; main() { - print(window.location.search); var module = new Module() - ..type(TodoController) - ..type(PlaybackHttpBackendConfig); + ..type(TodoController) + ..type(PlaybackHttpBackendConfig); // If these is a query in the URL, use the server-backed // TodoController. Otherwise, use the stored-data controller. @@ -39,5 +38,5 @@ main() { module.type(HttpBackend, implementedBy: PlaybackHttpBackend); } - ngBootstrap(module:module); + ngBootstrap(module: module); } diff --git a/demo/todo/web/todo.dart b/demo/todo/web/todo.dart index d287be25c..289500a82 100644 --- a/demo/todo/web/todo.dart +++ b/demo/todo/web/todo.dart @@ -7,13 +7,13 @@ class Item { String text; bool done; - Item([String this.text = '', bool this.done = false]); + Item([this.text = '', this.done = false]); bool get isEmpty => text.isEmpty; - clone() => new Item(text, done); + Item clone() => new Item(text, done); - clear() { + void clear() { text = ''; done = false; } @@ -50,11 +50,10 @@ class HttpServerController implements ServerController { @NgController( - selector: '[todo-controller]', - publishAs: 'todo' -) + selector: '[todo-controller]', + publishAs: 'todo') class TodoController { - List items; + var items = []; Item newItem; TodoController(ServerController serverController) { @@ -69,33 +68,24 @@ class TodoController { } // workaround for https://github.com/angular/angular.dart/issues/37 - dynamic operator [](String key) { - if (key == 'newItem') { - return newItem; - } - return null; - } + dynamic operator [](String key) => key == 'newItem' ? newItem : null; - add() { + void add() { if (newItem.isEmpty) return; items.add(newItem.clone()); newItem.clear(); } - markAllDone() { + void markAllDone() { items.forEach((item) => item.done = true); } - archiveDone() { + void archiveDone() { items.removeWhere((item) => item.done); } - String classFor(Item item) { - return item.done ? 'done' : ''; - } + String classFor(Item item) => item.done ? 'done' : ''; - int remaining() { - return items.where((item) => !item.done).length; - } + int remaining() => items.fold(0, (count, item) => count += item.done ? 0 : 1); } diff --git a/karma-perf.conf.js b/karma-perf.conf.js index 9cf22f9b2..a8d85d389 100644 --- a/karma-perf.conf.js +++ b/karma-perf.conf.js @@ -9,6 +9,7 @@ module.exports = function(config) { // optionally 'watched' only. files: [ 'perf/dom/*.dart', + 'perf/*_perf.dart', 'test/config/filter_tests.dart', {pattern: '**/*.dart', watched: true, included: false, served: true}, 'packages/browser/dart.js', diff --git a/lib/angular.dart b/lib/angular.dart index a663a77d5..3d7c69312 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -29,6 +29,7 @@ import 'package:di/dynamic_injector.dart'; */ @MirrorsUsed(targets: const [ 'angular', + 'angular.animate', 'angular.core', 'angular.core.dom', 'angular.filter', @@ -51,6 +52,7 @@ metaTargets: const [ ]) import 'dart:mirrors' show MirrorsUsed; +import 'package:angular/animate/module.dart'; import 'package:angular/core/module.dart'; import 'package:angular/core_dom/module.dart'; import 'package:angular/directive/module.dart'; diff --git a/lib/animate/animate.dart b/lib/animate/animate.dart new file mode 100644 index 000000000..87107af5b --- /dev/null +++ b/lib/animate/animate.dart @@ -0,0 +1,82 @@ +part of angular.animate; + +/** + * The [NgAnimate] service provides dom lifecycle mangement, detection and + * analysis of css animations, and hooks for custom animations. When any of + * these animations are run, [AnimationHandle]s are provided so that animations + * can be controled and so custom dom manipulations can occur when animations + * complete. + * + * TODO: Implement a staggered animation implementation similar to the + * AngularJS version. + */ +abstract class NgAnimate { + /** + * Add the [cssClass] to the classes on each element in [nodes] after + * running any defined animations. This is equivalent to running addClass on + * each element in [nodes] and returning Future.wait(handles); for the + * onCompleted property on [AnimationHandle]. + * + * Any existing animations running on any element in [nodes] will be + * canceled. + */ + AnimationHandle addClass(Iterable nodes, String cssClass); + + /** + * Remove the [cssClass] from the classes on each element in [nodes] after + * running any defined animations. This is equivalent to running removeClass + * on each element in [nodes] and returning Future.wait(handles); for the + * onCompleted property on [AnimationHandle]. + * + * Any existing animations running on any element in [nodes] will be + * canceled. + */ + AnimationHandle removeClass(Iterable nodes, String cssClass); + + /** + * Perform an 'add' animation for each element in [nodes]. The elements + * must exist in the dom. This is equivalent to running add on each element + * in [nodes] and returning Future.wait(handles); for the onCompleted + * property on [AnimationHandle]. + * + * Any existing animations running on any element in [nodes] will be + * canceled. + */ + AnimationHandle insert(Iterable nodes, dom.Node parent, { dom.Node insertBefore }); + + /** + * Perform a 'remove' animation for each element in [nodes]. The elements + * must exist in the dom and should not be detached until the [onCompleted] + * future on the [AnimationHandle] is executed AND the [AnimationResult] is + * [AnimationResult.COMPLETED] or [AnimationResult.COMPLETED_IGNORED]. + * + * This is equivalent to running remove on each element in [nodes] and + * returning Future.wait(handles); for the onCompleted property on + * [AnimationHandle]. + * + * Any existing animations running on any element in [nodes] will be + * canceled. + */ + AnimationHandle remove(Iterable nodes); + + /** + * Perform a 'move' animation for each element in [nodes]. The elements + * must exist in the dom. This is equivalent to running move on each element + * in [nodes] and returning Future.wait(handles); for the onCompleted + * property on [AnimationHandle]. + * + * Any existing animations running on any element in [nodes] will be + * canceled. + */ + AnimationHandle move(Iterable nodes, dom.Node parent, { dom.Node insertBefore }); + + /** + * Play a set of animations. This is equivalent to running play on each + * animation in [elements] and returning Future.wait(handles); for the + * onCompleted property on [AnimationHandle]. + * + * Any existing animations running on any element in [elements] will be + * canceled. + */ + AnimationHandle play(Iterable animations); +} diff --git a/lib/animate/animation.dart b/lib/animate/animation.dart new file mode 100644 index 000000000..c06c403c6 --- /dev/null +++ b/lib/animate/animation.dart @@ -0,0 +1,116 @@ +part of angular.animate; + +/** + * Final result of an animation after it is no longer attached to the element. + */ +class AnimationResult { + /// Animation was run (if it exists) and completed successfully. + static const COMPLETED = const AnimationResult._('COMPLETED'); + + /// Animation was skipped, but should be continued. + static const COMPLETED_IGNORED = const AnimationResult._('COMPLETED_IGNORED'); + + /// A [CANCELED] animation should not procced with it's final effects. + static const CANCELED = const AnimationResult._('CANCELED'); + + /// Convienence method if you don't care exactly how an animation completed + /// only that it did. + bool get isCompleted => this == COMPLETED || this == COMPLETED_IGNORED; + + final String value; + const AnimationResult._(this.value); +} + +/** + * An [Animation] is a per element state machine that can be implemented and + * used by the animation system. All methods are optional, but not implementing + * some methods may result in unintended behavior and you should understand what + * each method does. + * + * The standard lifecycle of dom events is as follows: + * + * 1. attach() - Any dom modifications required to 'attach' to the target + * element should be executed. Any previously running animations should + * have already been completed, canceled, or detached. + * + * 2. start() - Read any computed information that may be needed from the + * element to setup the animation. Do not change the DOM for performance + * reasons. + * + * 3. update() - Every animation frame do DOM mutates and / or decide to + * continue or not. Return true if you are still animating. + * + * 4. read() - Every animation frame you may read computed state here. + * + * 5. detatch() - After update returns false, detach will be executed and you + * should physically detach from the dom and execute onCompleted futures + * so that external code that depends on your animation can do dom + * mutates as well. + * + * Additionally, interruptAndCancel() and interruptAndComplete are used to + * forcibly interupt an animation, and the implementation should immediatly + * detach from [element]. + */ +abstract class Animation { + /// The element this animation is tied too. + final dom.Element element; + Future get onCompleted; + + Animation(this.element) { + assert(element != null); + } + + /** + * Perform dom mutations to attach an initialize the animation on [element]. + * The animation should not modify the [element] until this method is called. + */ + attach() { } + + /** + * This performs DOM reads to compute information about the animation, and + * will occur after attach. [time] is a date time representation of the + * current time, and [offsetMs] is the time since the last animation frame. + */ + start(DateTime time, num offsetMs) { } + + /** + * Occurs every animation frame. Return false to stop receiving animation + * frame updates. Detach will be called after [update] returns false. + * + * [time] is a [DateTime] representation of the current time + * [offsetMs] is the time since the last animation frame. + */ + bool update(DateTime time, num offsetMs) { return false; } + + /** + * Occurs every animation frame after [update] is called and should be used + * to read out DOM state information if needed. + * + * [time] is a [DateTime] representation of the current time + * [offsetMs] is the time since the last animation frame. + */ + read(DateTime time, num offsetMs) { } + + /** + * When [update] returns false, this will be called on the same animation + * frame. Any temporary classes or element modifications should be removed + * from the element and the onCompleted future should be executed. + */ + detach(DateTime time, num offsetMs) { } + + /** + * This occurs when another animation interupts this animation or the cancel() + * method is called on the AnimationHandel. The animation should remove any + * temporary classes or element modifications and the onCompleted future + * should be executed with a result of [CANCELED]. + */ + interruptAndCancel() { } + + /** + * This occurs when the complete() method is called on the AnimationHandel. + * The animation should remove any temporary classes or element modifications, + * finish any final permanent modifications and the onCompleted future + * should be executed with a result of [COMPLETED]. + */ + interruptAndComplete() { } +} \ No newline at end of file diff --git a/lib/animate/animation_handle.dart b/lib/animate/animation_handle.dart new file mode 100644 index 000000000..8d3c4c11a --- /dev/null +++ b/lib/animate/animation_handle.dart @@ -0,0 +1,138 @@ +part of angular.animate; + +/** + * Animation handle for controlling and listening to animation completion. + */ +abstract class AnimationHandle { + /** + * Executed once when the animation is completed with the type of completion + * result. + */ + Future get onCompleted; + + /** + * Stop and complete the animation immediatly. This has no effect if the + * animation has already completed. + * + * The onCompleted future will be executed if the animation has not been + * completed. + */ + void complete(); + + /** + * Stop and cancel the animation immediatly. This has no effect if the + * animation has already completed. + * + * The onCompleted future will be executed if the animation has not been + * completed. + */ + void cancel(); +} + +/** + * This is a proxy class for dealing with a set of elements where the 'same' + * or similar animations are being run on them and it's more convenient to have + * a merged animation handle to control and listen to the entire set of + * elements. + */ +class _MultiAnimationHandle extends AnimationHandle { + final List _animationHandles; + Future _onCompleted; + + /** + * On completed executes once EVERY other future is completed via + * Future.wait(). The animation result will be the 'lowest' common result + * that is returned across all results. + * + * if every animation returns [AnimationResult.COMPLETED], + * [AnimationResult.COMPLETED] will be returned. + * if any animation was [AnimationResult.COMPLETED_IGNORED] instead, even if + * some animations were completed, [AnimationResult.COMPLETED_IGNORED] will + * be returned. + * if any animation was [AnimationResult.CANCELED], the result will be + * [AnimationResult.CANCELED]. + */ + Future get onCompleted => _onCompleted; + + /// Create a new [AnimationHandle] with a set of existing [AnimationHandle]s. + _MultiAnimationHandle(Iterable animationHandles) + : _animationHandles = animationHandles.toList(growable: false) { + _onCompleted = Future.wait(_animationHandles.map((x) => x.onCompleted)) + .then((results) { + // This ensures that the 'lowest' common result is returned. + // if every animation COMPLETED, COMPLETED will be returned. + // if any animation was COMPLETED_IGNORED instead, even if + // animations were completed, COMPLETED_IGNORED will be returned. + // if any animation was canceled, the result will be CANCELED + var rtrn = AnimationResult.COMPLETED; + for(var result in results) { + if(result == AnimationResult.CANCELED) + return AnimationResult.CANCELED; + if(result == AnimationResult.COMPLETED_IGNORED) + rtrn = result; + } + return rtrn; + }); + } + + /// For each of the tracked [AnimationHandle]s, call complete(). + complete() { + for(var handle in _animationHandles) { + handle.complete(); + } + } + + /// For each of the tracked [AnimationHandle]s, call cancel(). + cancel() { + for(var handle in _animationHandles) { + handle.cancel(); + } + } +} + +/** + * Completed animation handle that is used when an animation is ignored and the + * final effect of the animation is immediatly completed. + */ +class _CompletedAnimationHandle extends AnimationHandle { + Future _future; + get onCompleted { + if(_future == null) { + var completer = new Completer(); + completer.complete(AnimationResult.COMPLETED_IGNORED); + _future = completer.future; + } + return _future; + } + + _CompletedAnimationHandle({Future future}) + : _future = future; + + complete() { } + cancel() { } +} + + +/** + * Animation handle that works with the [AnimationRunner] so that calling code + * can manage and listen to the lifecycle of an animation. + */ +class _AnimationRunnerHandle extends AnimationHandle { + final AnimationRunner _runner; + final Animation _animation; + + get onCompleted => _animation.onCompleted; + + _AnimationRunnerHandle(this._runner, this._animation) { + assert(_runner != null); + assert(_animation != null); + } + + complete() { + _runner.interruptAndComplete(_animation); + } + + cancel() { + _runner.interruptAndComplete(_animation); + } +} \ No newline at end of file diff --git a/lib/animate/animation_runner.dart b/lib/animate/animation_runner.dart new file mode 100644 index 000000000..67e71e938 --- /dev/null +++ b/lib/animate/animation_runner.dart @@ -0,0 +1,204 @@ +part of angular.animate; + +/** + * Window.animationFrame update loop and state machine for animations. + * + * TODO(codelogic): Find a way to detect and rate-limit the number of concurrent + * animations that are run at the same time. + * + * TODO(codelogic): Shadow dom may prevents parent walks from + * detecting parent animations. + */ +class AnimationRunner { + final Clock _clock; + final dom.Window _wnd; + + bool _animationFrameQueued = false; + + // Active animations are stored so that the classes can later be removed + // if an additional animation executes on the same element. + final BiMap _activeAnimations = new BiMap(); + + final List _attached = []; + final List _updating = []; + final List _completed = []; + + final Profiler _profiler; + final NgZone _zone; + + /** + * The animation runner which requires the dom [Window] for + * requestAnimationFrame and a [Clock] instance for providing absolute time + * for animation. The [profiler] is optional and will report timing + * information for the animation loop if provided. + */ + AnimationRunner(this._wnd, this._clock, this._zone, [Profiler profiler]) + : _profiler = _getProfiler(profiler); + + // For some reason the turnary operator doesn't want to work with profiler. + static Profiler _getProfiler(Profiler value) { + if (value == null) return new Profiler(); + return value; + } + + /** + * Start and play an animation through the state transitions defined in + * [Animation]. + */ + AnimationHandle play(Animation animation) { + _clearElement(animation.element); + _activeAnimations[animation.element] = animation; + + animation.attach(); + _attached.add(animation); + + _queueAnimationFrame(); + + var animationHandle = new _AnimationRunnerHandle(this, animation); + return animationHandle; + } + + _queueAnimationFrame() { + if(!_animationFrameQueued) { + _animationFrameQueued = true; + + _wnd.animationFrame.then((offsetMs) + => _animationFrame(offsetMs)); + } + } + + /* On the browsers animation frame event, update animations and progress + * through the animation state model: + * + * 1. attach() - pre-animation frame. + * 2. start(...) - frame 1 + * 3. update(...) - frame 1+n + * 4. read(...) - frame 1+n + * 5. _repeat until update(...) returns false on frame m_ + * 6. update(...) - frame m + * 7. detach(...) - frame m + * + * At any point any animation may be updated by calling interrupt and cancel + * with a reference to the [Animation] to cancel. The [AnimationRunner] will + * then forget about the [Animation] and will not call any further methods on + * the [Animation]. + */ + _animationFrame(num offsetMs) { + _profiler.startTimer("AnimationRunner.AnimationFrame"); + _animationFrameQueued = false; + + // It's easier and more consistent to reason about time if we freeze it for + // the duration of this function. + var now = _clock.now(); // + + _profiler.startTimer("AnimationRunner.AnimationFrame.DomMutates"); + // Dom mutates + _update(now, offsetMs); + _detachCompleted(now, offsetMs); + + _profiler.stopTimer("AnimationRunner.AnimationFrame.DomMutates"); + _profiler.startTimer("AnimationRunner.AnimationFrame.DomReads"); + + // Dom reads + _reads(now, offsetMs); + _startAttached(now, offsetMs); + + _profiler.stopTimer("AnimationRunner.AnimationFrame.DomReads"); + + // We don't need to continue queuing animation frames + // if there are no more animations to process. + if(_updating.length > 0) { + _queueAnimationFrame(); + } + + _profiler.stopTimer("AnimationRunner.AnimationFrame"); + } + + _update(DateTime now, num offset) { + for(int i=0; i<_updating.length; i++) { + var animation = _updating[i]; + if(!animation.update(now, offset)) { + _completed.add(animation); + _updating.removeAt(i); + i--; + } + } + } + + _reads(DateTime now, num offsetMs) { + for(var animation in _updating) { + animation.read(now, offsetMs); + } + } + + _detachCompleted(DateTime now, num offsetMs) { + for(var animation in _completed) { + _activeAnimations.remove(animation.element); + animation.detach(now, offsetMs); + } + _completed.clear(); + } + + _startAttached(DateTime now, num offsetMs) { + for(var animation in _attached) { + animation.start(now, offsetMs); + _updating.add(animation); + } + _attached.clear(); + } + + _clearElement(element) { + if(_activeAnimations.containsKey(element)) { + var animation = _activeAnimations[element]; + _forget(animation); + animation.interruptAndCancel(); + } + } + + _forget(Animation animation) { + assert(animation != null); + + _attached.remove(animation); + _completed.remove(animation); + _updating.remove(animation); + _activeAnimations.remove(animation.element); + } + + /** + * This will return true if this [element] or any of the parent elements have + * active animations applied to them and false if there is not. + */ + bool hasRunningParentAnimation(dom.Element element) { + while(element != null) { + if(_activeAnimations.containsKey(element)) + return true; + element = element.parent; + } + + return false; + } + + /** + * If the animation runner is currently tracking this animation it will remove + * the animation from the list of active animations and any currently updating + * animations, and call interruptAndCancel() on the [Animation] instance. + */ + interruptAndCancel(Animation animation) { + if(_activeAnimations.containsValue(animation)) { + _forget(animation); + animation.interruptAndCancel(); + } + } + + /** + * If the animation runner is currently tracking this animation it will remove + * the animation from the list of active animations and any currently updating + * animations, and call interruptAndComplete() on the [Animation] instance. + */ + interruptAndComplete(Animation animation) { + if(_activeAnimations.containsValue(animation)) { + _forget(animation); + animation.interruptAndComplete(); + } + } +} diff --git a/lib/animate/css_animate.dart b/lib/animate/css_animate.dart new file mode 100644 index 000000000..fda8eb631 --- /dev/null +++ b/lib/animate/css_animate.dart @@ -0,0 +1,154 @@ +part of angular.animate; + +/** + * This defines the standard set of CSS animation classes, transitions, and + * nomeanclature that will eventually be the foundation of the AngularDart + * animation framework. This implementation uses the [AnimationRunner] class to + * queue and run CSS based transition and keyframe animations, and provides a + * [play(animation)] hook for running arbetrary animations. + * + * TODO(codelogic): There needs to be a way to turn animations on / off for + * sections of DOM so that they don't ever get animation classes added + * in these cases. + */ +class CssAnimate extends NgAnimate { + static const String ngAnimateCssClass = "ng-animate"; + static const String ngMoveCssClass = "ng-move"; + static const String ngInsertCssClass = "ng-insert"; + static const String ngRemoveCssClass = "ng-remove"; + + static const String ngAddPostfix = "add"; + static const String ngRemovePostfix = "remove"; + static const String ngActivePostfix = "active"; + + AnimationRunner _animationRunner; + NoAnimate _noAnimate; + final Profiler profiler; + + CssAnimate(AnimationRunner this._animationRunner, this._noAnimate, + [ this.profiler ]); + + AnimationHandle addClass(Iterable nodes, String cssClass) { + var elements = _partition(_elements(nodes)); + + var animateHandles = elements.animate.map((el) { + return _cssAnimation(el, "$cssClass-$ngAddPostfix", + cssClassToAdd: cssClass); + }); + + if(elements.noAnimate.length > 0) + return _pickAnimationHandle(animateHandles, + _noAnimate.addClass(elements.noAnimate, cssClass)); + return _pickAnimationHandle(animateHandles); + } + + AnimationHandle removeClass(Iterable nodes, String cssClass) { + var elements = _partition(_elements(nodes)); + + var animateHandles = elements.animate.map((el) { + return _cssAnimation(el, "$cssClass-$ngRemovePostfix", + cssClassToRemove: cssClass); + }); + + if(elements.noAnimate.length > 0) + return _pickAnimationHandle(animateHandles, + _noAnimate.removeClass(elements.noAnimate, cssClass)); + return _pickAnimationHandle(animateHandles); + } + + AnimationHandle insert(Iterable nodes, dom.Node parent, { dom.Node insertBefore }) { + _domInsert(nodes, parent, insertBefore: insertBefore); + + var animateHandles = _elements(nodes).where((el) { + return !_animationRunner.hasRunningParentAnimation(el.parent); + }).map((el) { + return _cssAnimation(el, ngInsertCssClass); + }); + + return _pickAnimationHandle(animateHandles); + } + + AnimationHandle remove(Iterable nodes) { + var elements = _partition(_allNodesBetween(nodes)); + + var animateHandles = elements.animate.map((el) { + return _cssAnimation(el, ngRemoveCssClass)..onCompleted.then((result) { + if(result.isCompleted) { + el.remove(); + } + }); + }); + elements.noAnimate.forEach((el) => el.remove()); + return _pickAnimationHandle(animateHandles); + } + + AnimationHandle move(Iterable nodes, dom.Node parent, { dom.Node insertBefore }) { + _domMove(nodes, parent, insertBefore: insertBefore); + + var animateHandles = _elements(nodes).where((el) { + return !_animationRunner.hasRunningParentAnimation(el.parent); + }).map((el) { + return _cssAnimation(el, ngMoveCssClass); + }); + + return _pickAnimationHandle(animateHandles); + } + + AnimationHandle play(Iterable animations) { + // TODO(codelogic): Should we skip the running parent animation check for custom animations? + return _pickAnimationHandle(animations.map((a) => _animationRunner.play(a))); + } + + AnimationHandle _cssAnimation(dom.Element element, + String cssEventClass, + { String cssClassToAdd, + String cssClassToRemove}) { + + var animation = new CssAnimation( + element, + cssEventClass, + "$cssEventClass-$ngActivePostfix", + cssClassToAdd: cssClassToAdd, + cssClassToRemove: cssClassToRemove, + profiler: profiler); + + return _animationRunner.play(animation); + } + + static AnimationHandle _pickAnimationHandle(Iterable animated, [AnimationHandle noAnimate]) { + List handles; + + if(animated != null) + handles = animated.toList(); + else if (noAnimate == null) + return new _CompletedAnimationHandle(); + else + return noAnimate; + + if(noAnimate != null) + handles.add(noAnimate); + + if(handles.length == 1) + return handles.first; + + return new _MultiAnimationHandle(handles); + } + + _RunnableAnimations _partition(Iterable nodes) { + var runnable = new _RunnableAnimations(); + nodes.forEach((el) { + if(el.nodeType != dom.Node.ELEMENT_NODE + || _animationRunner.hasRunningParentAnimation(el.parentNode)) { + runnable.noAnimate.add(el); + } else { + runnable.animate.add(el); + } + }); + return runnable; + } +} + +class _RunnableAnimations { + final animate = []; + final noAnimate = []; +} diff --git a/lib/animate/css_animation.dart b/lib/animate/css_animation.dart new file mode 100644 index 000000000..1bed05f4d --- /dev/null +++ b/lib/animate/css_animation.dart @@ -0,0 +1,142 @@ +part of angular.animate; + +/** + * [Animation] implementation for handling the standard angular 'event' and + * 'event-active' class pattern with css. This will compute transition and + * animation duration from the css classes and use it to complete futures when + * the css animations complete. + */ +class CssAnimation extends Animation { + final String cssClassToAdd; + final String cssClassToRemove; + + final String cssEventClass; + final String cssEventActiveClass; + + final Completer _completer = new Completer(); + + AnimationResult _result; + bool _isActive = false; + + Future get onCompleted => _completer.future; + + DateTime startTime; + Duration duration; + + final Profiler profiler; + + CssAnimation(dom.Element targetElement, + this.cssEventClass, + this.cssEventActiveClass, + { this.profiler, + this.cssClassToAdd, + this.cssClassToRemove }) + : super(targetElement); + + attach() { + // this happens right after creation time but before the first window + // animation frame is called. + element.classes.add(cssEventClass); + } + + start(DateTime time, num offsetMs) { + // This occurs on the first animation frame. + // TODO(codelogic): It might be good to find some way of defering this to + // the next digest loop instead of the first animation frame. + this.startTime = time; + duration = _computeDuration(); + } + + bool update(DateTime time, num offsetMs) { + // This will always run after the first animationFrame is queued so that + // inserted elements have the base event class applied before adding the + // active class to the element. If this is not done, inserted dom nodes + // will not run their enter animation. + if(!_isActive && duration != Duration.ZERO) { + element.classes.add(cssEventActiveClass); + _isActive = true; + } else if (time.isAfter(startTime.add(duration)) + || duration == null || duration == Duration.ZERO) { + // TODO(codelogic): If the initial frame takes a significant amount of + // time, the computed duration + startTime might not actually represent + // the end of the animation + + // Done with the animation + return false; + } + + // Continue updating + return true; + } + + detach(DateTime time, num offsetMs) { + if (!_completer.isCompleted) { + _onComplete(AnimationResult.COMPLETED); + } + } + + interruptAndCancel() { + if (!_completer.isCompleted) { + _removeEventAnimationClasses(); + _result = AnimationResult.CANCELED; + _completer.complete(_result); + } + } + + interruptAndComplete() { + if (!_completer.isCompleted) { + _onComplete(AnimationResult.COMPLETED_IGNORED); + } + } + + // Since there are two different ways to 'complete' an animation: + void _onComplete(AnimationResult result) { + _removeEventAnimationClasses(); + _result = result; + if(cssClassToAdd != null) { + element.classes.add(cssClassToAdd); + } else if(cssClassToRemove != null) { + element.classes.remove(cssClassToRemove); + } + _completer.complete(_result); + } + + /// Cleanup css event classes. + _removeEventAnimationClasses() { + element.classes.remove(cssEventClass); + element.classes.remove(cssEventActiveClass); + } + + Duration _computeDuration() { + // TODO(codelogic) this needs to take into account animation, repetition + // count and see if delay affects the computed duration. + + // TODO(codelogic): It might be possible to cache durations and avoid the + // getComputedStyle() hit for elements and transitions we've already seen. + var style = element.getComputedStyle(); + var cssDurationString = style.transitionDuration; + var keyframeDurationString = style.animationDuration; + var cssDuration = _parseCssDuration(cssDurationString); + var keyframeDuration = _parseCssDuration(keyframeDurationString); + + return cssDuration > keyframeDuration ? cssDuration : keyframeDuration; + } + + Duration _parseCssDuration(String duration) { + // Assume milliseconds + if (duration.endsWith("ms")) { + var ms = double.parse(duration.substring(0, duration.length - 2)); + int microseconds = Duration.MICROSECONDS_PER_MILLISECOND * ms; + return new Duration(microseconds: microseconds.round()); + } + + // Assume seconds + if (duration.endsWith("s")) { + var seconds = double.parse(duration.substring(0, duration.length - 1)); + var microseconds = Duration.MICROSECONDS_PER_SECOND * seconds; + return new Duration(microseconds: microseconds.round()); + } + + return Duration.ZERO; + } +} \ No newline at end of file diff --git a/lib/animate/dom_tools.dart b/lib/animate/dom_tools.dart new file mode 100644 index 000000000..251fde8d6 --- /dev/null +++ b/lib/animate/dom_tools.dart @@ -0,0 +1,45 @@ +part of angular.animate; + +void _domRemove(List nodes) { + // Not every element is sequential if the list of nodes only + // includes the elements. Removing a block also includes + // removing non-element nodes inbetween. + for(var j = 0, jj = nodes.length; j < jj; j++) { + dom.Node current = nodes[j]; + dom.Node next = j+1 < jj ? nodes[j+1] : null; + + while(next != null && current.nextNode != next) { + current.nextNode.remove(); + } + nodes[j].remove(); + } +} + +List _allNodesBetween(List nodes) { + var result = []; + // Not every element is sequential if the list of nodes only + // includes the elements. Removing a block also includes + // removing non-element nodes inbetween. + for(var j = 0, jj = nodes.length; j < jj; j++) { + dom.Node current = nodes[j]; + dom.Node next = j+1 < jj ? nodes[j+1] : null; + + while(next != null && current.nextNode != next) { + result.add(current.nextNode); + current = current.nextNode; + } + result.add(nodes[j]); + } + return result; +} + +void _domInsert(Iterable nodes, dom.Node parent, { dom.Node insertBefore }) { + parent.insertAllBefore(nodes, insertBefore); +} + +void _domMove(Iterable nodes, dom.Node parent, { dom.Node insertBefore }) { + nodes.forEach((n) { + if(n.parentNode == null) n.remove(); + parent.insertBefore(n, insertBefore); + }); +} diff --git a/lib/animate/module.dart b/lib/animate/module.dart new file mode 100644 index 000000000..3953380fc --- /dev/null +++ b/lib/animate/module.dart @@ -0,0 +1,65 @@ +library angular.animate; + +import 'dart:async'; +import 'dart:html' as dom; + +import 'package:angular/core/module.dart'; +import 'package:logging/logging.dart'; +import 'package:perf_api/perf_api.dart'; +import 'package:quiver/collection.dart'; +import 'package:quiver/time.dart'; +import 'package:di/di.dart'; + +part 'animate.dart'; +part 'animation.dart'; +part 'animation_handle.dart'; +part 'animation_runner.dart'; +part 'css_animation.dart'; +part 'css_animate.dart'; +part 'dom_tools.dart'; +part 'no_animate.dart'; + +final Logger _logger = new Logger('ng.animate'); + +/** + * Installing the NgAnimateModule will enable the [CssAnimate] animation + * implementation in your application. This will change the behavior of block + * construction and allow you to add and define css keyframe animations and + * transitions in the styles of your elements. + * + * Example html: + * + *
...
+ * + * Example css defining an opacity transition over .5 seconds using the + * `.ng-insert` and `.ng-remove` css classes: + * + * magic.ng-insert { + * transition: all 500ms; + * opacity: 0; + * } + * magic.ng-insert-active { + * opacity: 1; + * } + * + * magic.ng-remove { + * transition: all 500ms; + * opacity: 1; + * } + * magic.ng-insert-active { + * opacity: 0; + * } + */ +class NgAnimateModule extends Module { + NgAnimateModule() { + value(Clock, new Clock()); + value(dom.Window, dom.window); + type(AnimationRunner); + type(NoAnimate); + type(NgAnimate, implementedBy: CssAnimate); + } + + NgAnimateModule.noOp() { + type(NgAnimate, implementedBy: NoAnimate); + } +} \ No newline at end of file diff --git a/lib/animate/no_animate.dart b/lib/animate/no_animate.dart new file mode 100644 index 000000000..30a13ec3f --- /dev/null +++ b/lib/animate/no_animate.dart @@ -0,0 +1,45 @@ +part of angular.animate; + +/** + * Instantly complete animations and return a AnimationHandle that will + * complete on the next digest loop. + */ +class NoAnimate extends NgAnimate { + AnimationHandle addClass(Iterable nodes, String cssClass) { + _elements(nodes).forEach((el) => el.classes.add(cssClass)); + return new _CompletedAnimationHandle(); + } + + AnimationHandle removeClass(Iterable nodes, String cssClass) { + _elements(nodes).forEach((el) => el.classes.remove(cssClass)); + return new _CompletedAnimationHandle(); + } + + AnimationHandle insert(Iterable nodes, dom.Node parent, { dom.Node insertBefore } ) { + _domInsert(nodes, parent, insertBefore: insertBefore); + return new _CompletedAnimationHandle(); + } + + AnimationHandle remove(Iterable nodes) { + _domRemove(nodes.toList(growable: false)); + return new _CompletedAnimationHandle(); + } + + AnimationHandle move(Iterable nodes, dom.Node parent, { dom.Node insertBefore }) { + _domMove(nodes, parent, insertBefore: insertBefore); + return new _CompletedAnimationHandle(); + } + + AnimationHandle play(Iterable animations) { + var handle = new _MultiAnimationHandle( + animations.map((a) => new _CompletedAnimationHandle(future: a.onCompleted))); + + animations.forEach((a) => a.interruptAndComplete()); + + return handle; + } +} + +Iterable _elements(Iterable nodes) { + return nodes.where((el) => el.nodeType == dom.Node.ELEMENT_NODE); +} \ No newline at end of file diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 2757135d0..1f28cc36b 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -17,6 +17,7 @@ class AngularModule extends Module { install(new NgCoreModule()); install(new NgCoreDomModule()); install(new NgDirectiveModule()); + install(new NgAnimateModule.noOp()); install(new NgFilterModule()); install(new NgPerfModule()); install(new NgRoutingModule()); @@ -61,11 +62,11 @@ Injector _defaultInjectorFactory(List modules) => * Injector injector = ngBootstrap(module: myAppModule); */ Injector ngBootstrap({ - Module module: null, - List modules: null, - dom.Element element: null, - String selector: '[ng-app]', - Injector injectorFactory(List modules): _defaultInjectorFactory}) { + Module module: null, + List modules: null, + dom.Element element: null, + String selector: '[ng-app]', + Injector injectorFactory(List modules): _defaultInjectorFactory}) { _publishToJavaScript(); var ngModules = [new AngularModule()]; @@ -74,7 +75,9 @@ Injector ngBootstrap({ if (element == null) { element = dom.querySelector(selector); var document = dom.window.document; - if (element == null) element = document.childNodes.firstWhere((e) => e is dom.Element); + if (element == null) { + element = document.childNodes.firstWhere((e) => e is dom.Element); + } } // The injector must be created inside the zone, so we create the @@ -87,7 +90,8 @@ Injector ngBootstrap({ return zone.run(() { var rootElements = [element]; Injector injector = injectorFactory(ngModules); - injector.get(Compiler)(rootElements, injector.get(DirectiveMap))(injector, rootElements); + injector.get(Compiler)(rootElements, injector.get(DirectiveMap)) + (injector, rootElements); return injector; }); } diff --git a/lib/change_detection/ast.dart b/lib/change_detection/ast.dart index d6ba9c5c3..66c55fba3 100644 --- a/lib/change_detection/ast.dart +++ b/lib/change_detection/ast.dart @@ -9,9 +9,15 @@ part of angular.watch_group; abstract class AST { static final String _CONTEXT = '#'; final String expression; - AST(this.expression) { assert(expression!=null); } + AST(expression) + : expression = expression.startsWith('#.') + ? expression.substring(2) + : expression + { + assert(expression!=null); + } WatchRecord<_Handler> setupWatch(WatchGroup watchGroup); - toString() => expression; + String toString() => expression; } /** @@ -21,8 +27,8 @@ abstract class AST { */ class ContextReferenceAST extends AST { ContextReferenceAST(): super(AST._CONTEXT); - WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) - => new _ConstantWatchRecord(watchGroup, expression, watchGroup.context); + WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => + new _ConstantWatchRecord(watchGroup, expression, watchGroup.context); } /** @@ -33,12 +39,14 @@ class ContextReferenceAST extends AST { class ConstantAST extends AST { final constant; - ConstantAST(dynamic constant): - super('$constant'), - constant = constant; + ConstantAST(constant, [String expression]) + : constant = constant, + super(expression == null + ? constant is String ? '"$constant"' : '$constant' + : expression); - WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) - => new _ConstantWatchRecord(watchGroup, expression, constant); + WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => + new _ConstantWatchRecord(watchGroup, expression, constant); } /** @@ -51,13 +59,12 @@ class FieldReadAST extends AST { final String name; FieldReadAST(lhs, name) - : super(lhs.expression == AST._CONTEXT ? name : '$lhs.$name'), - lhs = lhs, - name = name; + : lhs = lhs, + name = name, + super('$lhs.$name'); WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => - watchGroup.addFieldWatch(lhs, name, expression); - + watchGroup.addFieldWatch(lhs, name, expression); } /** @@ -77,7 +84,7 @@ class PureFunctionAST extends AST { name = name; WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => - watchGroup.addFunctionWatch(fn, argsAST, expression); + watchGroup.addFunctionWatch(fn, argsAST, expression); } /** @@ -97,22 +104,21 @@ class MethodAST extends AST { argsAST = argsAST; WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => - watchGroup.addMethodWatch(lhsAST, name, argsAST, expression); + watchGroup.addMethodWatch(lhsAST, name, argsAST, expression); } class CollectionAST extends AST { final AST valueAST; - CollectionAST(valueAST): - super('#collection($valueAST)'), - valueAST = valueAST; + CollectionAST(valueAST) + : valueAST = valueAST, + super('#collection($valueAST)'); - WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) { - return watchGroup.addCollectionWatch(valueAST); - } + WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => + watchGroup.addCollectionWatch(valueAST); } -_argList(List items) => items.join(', '); +String _argList(List items) => items.join(', '); /** * The name is a bit oxymoron, but it is essentially the NullObject pattern. @@ -124,9 +130,9 @@ class _ConstantWatchRecord extends WatchRecord<_Handler> { final currentValue; final _Handler handler; - _ConstantWatchRecord(WatchGroup watchGroup, String expression, dynamic currentValue): - currentValue = currentValue, - handler = new _ConstantHandler(watchGroup, expression, currentValue); + _ConstantWatchRecord(WatchGroup watchGroup, String expression, currentValue) + : currentValue = currentValue, + handler = new _ConstantHandler(watchGroup, expression, currentValue); ChangeRecord<_Handler> check() => null; void remove() => null; diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index e7237b5b3..2eb49fe2e 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -1,8 +1,10 @@ library change_detection; +typedef EvalExceptionHandler(error, stack); + /** * An interface for [ChangeDetectorGroup] groups related watches together. It - * guarentees that within the group all watches will be reported in the order in + * guarantees that within the group all watches will be reported in the order in * which they were registered. It also provides an efficient way of removing the * watch group. */ @@ -11,8 +13,8 @@ abstract class ChangeDetectorGroup { * Watch a specific [field] on an [object]. * * If the [field] is: - * - _name_ - Name of the field to watch. (If the [object] is a Map then - * treat it as a key.) + * - _name_ - Name of the property to watch. (If the [object] is a Map then + * treat the name as a key.) * - _[]_ - Watch all items in an array. * - _{}_ - Watch all items in a Map. * - _._ - Watch the actual object identity. @@ -25,7 +27,6 @@ abstract class ChangeDetectorGroup { */ WatchRecord watch(Object object, String field, H handler); - /** Use to remove all watches in the group in an efficient manner. */ void remove(); @@ -50,10 +51,10 @@ abstract class ChangeDetectorGroup { abstract class ChangeDetector extends ChangeDetectorGroup { /** * This method does the work of collecting the changes and returns them as a - * linked list of [ChangeRecord]s. The [ChangeRecord]s are to be returned in - * the same order as they were registered. + * linked list of [ChangeRecord]s. The [ChangeRecord]s are returned in the + * same order as they were registered. */ - ChangeRecord collectChanges(); + ChangeRecord collectChanges([EvalExceptionHandler exceptionHandler]); } abstract class Record { @@ -92,7 +93,7 @@ abstract class WatchRecord extends Record { /** * Check to see if the field on the object has changed. Returns [null] if no - * change, or a [ChangeRecord] if the change has been detected. + * change, or a [ChangeRecord] if a change has been detected. */ ChangeRecord check(); @@ -110,69 +111,130 @@ abstract class ChangeRecord extends Record { } /** - * If [ChangeDetector] is watching a collection (an [Iterable]) then the - * [currentValue] of [Record] will contain this object. The object contains a - * summary of changes to the collection since the last execution. The changes - * are reported as a list of [CollectionChangeItem]s which contain the current - * and previous position in the list as well as the item. + * If the [ChangeDetector] is watching a [Map] then the [currentValue] of + * [Record] will contain an instance of this object. A [MapChangeRecord] + * contains the changes to the map since the last execution. The changes are + * reported as a list of [MapKeyValue]s which contain the key as well as its + * current and previous value. + */ +abstract class MapChangeRecord { + /// The underlying iterable object + Map get map; + + /// A list of [CollectionKeyValue]s which are in the iteration order. */ + KeyValue get mapHead; + /// A list of changed items. + ChangedKeyValue get changesHead; + /// A list of new added items. + AddedKeyValue get additionsHead; + /// A list of removed items + RemovedKeyValue get removalsHead; + + void forEachChange(void f(ChangedKeyValue change)); + void forEachAddition(void f(AddedKeyValue addition)); + void forEachRemoval(void f(RemovedKeyValue removal)); +} + +/** + * Each item in map is wrapped in [MapKeyValue], which can track + * the [item]s [currentValue] and [previousValue] location. + */ +abstract class MapKeyValue { + /// The item. + K get key; + + /// Previous item location in the list or [null] if addition. + V get previousValue; + + /// Current item location in the list or [null] if removal. + V get currentValue; +} + +abstract class KeyValue extends MapKeyValue { + KeyValue get nextKeyValue; +} + +abstract class AddedKeyValue extends MapKeyValue { + AddedKeyValue get nextAddedKeyValue; +} + +abstract class RemovedKeyValue extends MapKeyValue { + RemovedKeyValue get nextRemovedKeyValue; +} + +abstract class ChangedKeyValue extends MapKeyValue { + ChangedKeyValue get nextChangedKeyValue; +} + + +/** + * If the [ChangeDetector] is watching an [Iterable] then the [currentValue] of + * [Record] will contain this object. The [CollectionChangeRecord] contains the + * changes to the collection since the last execution. The changes are reported + * as a list of [CollectionChangeItem]s which contain the item as well as its + * current and previous position in the list. */ -abstract class CollectionChangeRecord { +abstract class CollectionChangeRecord { /** The underlying iterable object */ Iterable get iterable; /** A list of [CollectionItem]s which are in the iteration order. */ - CollectionItem get collectionHead; + CollectionItem get collectionHead; /** A list of new [AddedItem]s. */ - AddedItem get additionsHead; + AddedItem get additionsHead; /** A list of [MovedItem]s. */ - MovedItem get movesHead; + MovedItem get movesHead; /** A list of [RemovedItem]s. */ - RemovedItem get removalsHead; + RemovedItem get removalsHead; + + void forEachAddition(void f(AddedItem addition)); + void forEachMove(void f(MovedItem move)); + void forEachRemoval(void f(RemovedItem removal)); } /** - * Each item in collection is wrapped in [CollectionChangeItem], which can track - * the [item]s [currentKey] and [previousKey] location. + * Each changed item in the collection is wrapped in a [CollectionChangeItem], + * which tracks the [item]s [currentKey] and [previousKey] location. */ -abstract class CollectionChangeItem { +abstract class CollectionChangeItem { /** Previous item location in the list or [null] if addition. */ - K get previousKey; + int get previousIndex; /** Current item location in the list or [null] if removal. */ - K get currentKey; + int get currentIndex; /** The item. */ V get item; } /** - * Used to create a linked list of collection items. - * These items are always in the iteration order of the collection. + * Used to create a linked list of collection items. These items are always in + * the iteration order of the collection. */ -abstract class CollectionItem extends CollectionChangeItem { - CollectionItem get nextCollectionItem; +abstract class CollectionItem extends CollectionChangeItem { + CollectionItem get nextCollectionItem; } /** - * A linked list of new items added to the collection. - * These items are always in the iteration order of the collection. + * A linked list of new items added to the collection. These items are always in + * the iteration order of the collection. */ -abstract class AddedItem extends CollectionChangeItem { - AddedItem get nextAddedItem; +abstract class AddedItem extends CollectionChangeItem { + AddedItem get nextAddedItem; } /** - * A linked list of moved items in to the collection. - * These items are always in the iteration order of the collection. + * A linked list of items moved in the collection. These items are always in + * the iteration order of the collection. */ -abstract class MovedItem extends CollectionChangeItem { - MovedItem get nextMovedItem; +abstract class MovedItem extends CollectionChangeItem { + MovedItem get nextMovedItem; } /** - * A linked list of removed items in to the collection. - * These items are always in the iteration order of the collection. + * A linked list of items removed from the collection. These items are always + * in the iteration order of the collection. */ -abstract class RemovedItem extends CollectionChangeItem { - RemovedItem get nextRemovedItem; +abstract class RemovedItem extends CollectionChangeItem { + RemovedItem get nextRemovedItem; } diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 1d32b7fae..997be11aa 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -7,7 +7,7 @@ import 'package:angular/change_detection/change_detection.dart'; typedef FieldGetter(object); class GetterCache { - Map _map; + final Map _map; GetterCache(this._map); @@ -146,7 +146,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { } } - _recordAdd(DirtyCheckingRecord record) { + DirtyCheckingRecord _recordAdd(DirtyCheckingRecord record) { DirtyCheckingRecord previous = _tail; DirtyCheckingRecord next = previous == null ? null : previous._nextWatch; @@ -163,7 +163,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { return record; } - _recordRemove(DirtyCheckingRecord record) { + void _recordRemove(DirtyCheckingRecord record) { DirtyCheckingRecord previous = record._prevWatch; DirtyCheckingRecord next = record._nextWatch; @@ -182,7 +182,7 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { } } - toString() { + String toString() { var lines = []; if (_parent == null) { var allRecords = []; @@ -216,17 +216,25 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup implements ChangeDetector { DirtyCheckingChangeDetector(GetterCache getterCache): super(null, getterCache); - DirtyCheckingRecord collectChanges() { + DirtyCheckingRecord collectChanges([EvalExceptionHandler exceptionHandler]) { DirtyCheckingRecord changeHead = null; DirtyCheckingRecord changeTail = null; DirtyCheckingRecord current = _head; // current index while (current != null) { - if (current.check() != null) { - if (changeHead == null) { - changeHead = changeTail = current; + try { + if (current.check() != null) { + if (changeHead == null) { + changeHead = changeTail = current; + } else { + changeTail = changeTail.nextChange = current; + } + } + } catch (e, s) { + if (exceptionHandler == null) { + rethrow; } else { - changeTail = changeTail.nextChange = current; + exceptionHandler(e, s); } } current = current._nextWatch; @@ -235,7 +243,7 @@ class DirtyCheckingChangeDetector extends DirtyCheckingChangeDetectorGroup return changeHead; } - remove() { + void remove() { throw new StateError('Root ChangeDetector can not be removed'); } } @@ -299,33 +307,42 @@ class DirtyCheckingRecord implements ChangeRecord, WatchRecord { * reflection. If [Map] then it sets up map accessor. */ set object(obj) { - this._object = obj; + _object = obj; if (obj == null) { _mode = _MODE_IDENTITY_; - } else if (field == null) { + return; + } + + if (field == null) { _instanceMirror = null; if (obj is Map) { - _mode = _MODE_MAP_; - assert('implement' == false); - currentValue = null; //new _MapChangeRecord(); + if (_mode != _MODE_MAP_) { + // Last one was collection as well, don't reset state. + _mode = _MODE_MAP_; + currentValue = new _MapChangeRecord(); + } } else if (obj is Iterable) { - if (_mode == _MODE_ITERABLE_) return; // Last one was collection as well, don't reset state. - _mode = _MODE_ITERABLE_; - currentValue = new _CollectionChangeRecord(); + if (_mode != _MODE_ITERABLE_) { + // Last one was collection as well, don't reset state. + _mode = _MODE_ITERABLE_; + currentValue = new _CollectionChangeRecord(); + } } else { _mode = _MODE_IDENTITY_; } + + return; + } + + if (obj is Map) { + _mode = _MODE_MAP_FIELD_; + _instanceMirror = null; + } else if (_getter != null) { + _mode = _MODE_GETTER_; + _instanceMirror = null; } else { - if (obj is Map) { - _mode = _MODE_MAP_FIELD_; - _instanceMirror = null; - } else if (_getter != null) { - _mode = _MODE_GETTER_; - _instanceMirror = null; - } else { - _mode = _MODE_REFLECT_; - _instanceMirror = reflect(obj); - } + _mode = _MODE_REFLECT_; + _instanceMirror = reflect(obj); } } @@ -348,9 +365,9 @@ class DirtyCheckingRecord implements ChangeRecord, WatchRecord { current = object; break; case _MODE_MAP_: - return mapCheck(object) ? this : null; + return (currentValue as _MapChangeRecord)._check(object) ? this : null; case _MODE_ITERABLE_: - return iterableCheck(object) ? this : null; + return (currentValue as _CollectionChangeRecord)._check(object) ? this : null; default: assert(false); } @@ -363,7 +380,7 @@ class DirtyCheckingRecord implements ChangeRecord, WatchRecord { // is the same. We save the value so that next time identity will pass currentValue = current; } else if (last is num && last.isNaN && current is num && current.isNaN) { - // we need this for JavaScript since in JS NaN !== NaN. + // we need this for the compiled JavaScript since in JS NaN !== NaN. } else { previousValue = last; currentValue = current; @@ -373,43 +390,352 @@ class DirtyCheckingRecord implements ChangeRecord, WatchRecord { return null; } - mapCheck(Map map) { - assert('TODO: implement!' == true); - /* - _MapChangeRecord mapChangeRecord = currentValue as _MapChangeRecord; - ItemRecord record = mapChangeRecord._collectionHead; - mapChangeRecord.truncate(record); + + void remove() { + _group._recordRemove(this); + } + + String toString() => '${_MODE_NAMES[_mode]}[$field]'; +} + +final Object _INITIAL_ = new Object(); + +class _MapChangeRecord implements MapChangeRecord { + final Map _records = new Map(); + Map _map; + KeyValueRecord _mapHead; + KeyValueRecord _changesHead, _changesTail; + KeyValueRecord _additionsHead, _additionsTail; + KeyValueRecord _removalsHead, _removalsTail; + + Map get map => _map; + KeyValue get mapHead => _mapHead; + ChangedKeyValue get changesHead => _changesHead; + AddedKeyValue get additionsHead => _additionsHead; + RemovedKeyValue get removalsHead => _removalsHead; + + get isDirty => _additionsHead != null || + _changesHead != null || + _removalsHead != null; + + void forEachChange(void f(ChangedKeyValue change)) { + KeyValueRecord record = _changesHead; + while(record != null) { + f(record); + record = record._nextChangedKeyValue; + } + } + + void forEachAddition(void f(AddedKeyValue addition)){ + KeyValueRecord record = _additionsHead; + while(record != null) { + f(record); + record = record._nextAddedKeyValue; + } + } + + void forEachRemoval(void f(RemovedKeyValue removal)){ + KeyValueRecord record = _removalsHead; + while(record != null) { + f(record); + record = record._nextRemovedKeyValue; + } + } + + + bool _check(Map map) { + _reset(); + _map = map; + Map records = _records; + KeyValueRecord oldSeqRecord = _mapHead; + KeyValueRecord lastOldSeqRecord; + KeyValueRecord lastNewSeqRecord; + var seqChanged = false; map.forEach((key, value) { - if (record == null || !identical(value, record.item)) { } + var newSeqRecord; + if (oldSeqRecord != null && key == oldSeqRecord.key) { + newSeqRecord = oldSeqRecord; + if (!identical(value, oldSeqRecord._currentValue)) { + var prev = oldSeqRecord._previousValue = oldSeqRecord._currentValue; + oldSeqRecord._currentValue = value; + if (!((value is String && prev is String && value == prev) || + (value is num && value.isNaN && prev is num && prev.isNaN))) { + // Check string by value rather than reference + _addToChanges(oldSeqRecord); + } + } + } else { + seqChanged = true; + if (oldSeqRecord != null) { + oldSeqRecord._nextKeyValue = null; + _removeFromSeq(lastOldSeqRecord, oldSeqRecord); + _addToRemovals(oldSeqRecord); + } + if (records.containsKey(key)) { + newSeqRecord = records[key]; + } else { + newSeqRecord = records[key] = new KeyValueRecord(key); + newSeqRecord._currentValue = value; + _addToAdditions(newSeqRecord); + } + } + + if (seqChanged) { + if (_isInRemovals(newSeqRecord)) { + _removeFromRemovals(newSeqRecord); + } + if (lastNewSeqRecord == null) { + _mapHead = newSeqRecord; + } else { + lastNewSeqRecord._nextKeyValue = newSeqRecord; + } + } + lastOldSeqRecord = oldSeqRecord; + lastNewSeqRecord = newSeqRecord; + oldSeqRecord = oldSeqRecord == null ? null : oldSeqRecord._nextKeyValue; }); - return mapChangeRecord.isDirty; - */ + _truncate(lastOldSeqRecord, oldSeqRecord); + return isDirty; } + void _reset() { + var record = _changesHead; + while (record != null) { + record._previousValue = record._currentValue; + record = record._nextChangedKeyValue; + } - /** - * Check the [Iterable] [collection] for changes. - */ - iterableCheck(Iterable collection) { - _CollectionChangeRecord collectionChangeRecord = - currentValue as _CollectionChangeRecord; - collectionChangeRecord._reset(); - ItemRecord record = collectionChangeRecord._collectionHead; + record = _additionsHead; + while (record != null) { + record._previousValue = record._currentValue; + record = record._nextAddedKeyValue; + } + + assert((() { + var record = _changesHead; + while (record != null) { + var nextRecord = record._nextChangedKeyValue; + record._nextChangedKeyValue = null; + record = nextRecord; + } + + record = _additionsHead; + while (record != null) { + var nextRecord = record._nextAddedKeyValue; + record._nextAddedKeyValue = null; + record = nextRecord; + } + + record = _removalsHead; + while (record != null) { + var nextRecord = record._nextRemovedKeyValue; + record._nextRemovedKeyValue = null; + record = nextRecord; + } + + return true; + })()); + _changesHead = _changesTail = null; + _additionsHead = _additionsTail = null; + _removalsHead = _removalsTail = null; + } + + void _truncate(KeyValueRecord lastRecord, KeyValueRecord record) { + while(record != null) { + if (lastRecord == null) { + _mapHead = null; + } else { + lastRecord._nextKeyValue = null; + } + var nextRecord = record._nextKeyValue; + assert((() { + record._nextKeyValue = null; + return true; + })()); + _addToRemovals(record); + lastRecord = record; + record = nextRecord; + } + + record = _removalsHead; + while (record != null) { + record._previousValue = record._currentValue; + record._currentValue = null; + _records.remove(record.key); + record = record._nextRemovedKeyValue; + } + } + + bool _isInRemovals(KeyValueRecord record) => + record == _removalsHead || + record._nextRemovedKeyValue != null || + record._prevRemovedKeyValue != null; + + void _addToRemovals(KeyValueRecord record) { + assert(record._nextKeyValue == null); + assert(record._nextAddedKeyValue == null); + assert(record._nextChangedKeyValue == null); + assert(record._nextRemovedKeyValue == null); + assert(record._prevRemovedKeyValue == null); + if (_removalsHead == null) { + _removalsHead = _removalsTail = record; + } else { + _removalsTail._nextRemovedKeyValue = record; + record._prevRemovedKeyValue = _removalsTail; + _removalsTail = record; + } + } + + void _removeFromSeq(KeyValueRecord prev, KeyValueRecord record) { + KeyValueRecord next = record._nextKeyValue; + if (prev == null) { + _mapHead = next; + } else { + prev._nextKeyValue = next; + } + assert((() { + record._nextKeyValue = null; + return true; + })()); + } + + void _removeFromRemovals(KeyValueRecord record) { + assert(record._nextKeyValue == null); + assert(record._nextAddedKeyValue == null); + assert(record._nextChangedKeyValue == null); + + var prev = record._prevRemovedKeyValue; + var next = record._nextRemovedKeyValue; + if (prev == null) { + _removalsHead = next; + } else { + prev._nextRemovedKeyValue = next; + } + if (next == null) { + _removalsTail = prev; + } else { + next._prevRemovedKeyValue = prev; + } + record._prevRemovedKeyValue = record._nextRemovedKeyValue = null; + } + + void _addToAdditions(KeyValueRecord record) { + assert(record._nextKeyValue == null); + assert(record._nextAddedKeyValue == null); + assert(record._nextChangedKeyValue == null); + assert(record._nextRemovedKeyValue == null); + assert(record._prevRemovedKeyValue == null); + if (_additionsHead == null) { + _additionsHead = _additionsTail = record; + } else { + _additionsTail._nextAddedKeyValue = record; + _additionsTail = record; + } + } + + void _addToChanges(KeyValueRecord record) { + assert(record._nextAddedKeyValue == null); + assert(record._nextChangedKeyValue == null); + assert(record._nextRemovedKeyValue == null); + assert(record._prevRemovedKeyValue == null); + if (_changesHead == null) { + _changesHead = _changesTail = record; + } else { + _changesTail._nextChangedKeyValue = record; + _changesTail = record; + } + } +} + +class KeyValueRecord implements KeyValue, AddedKeyValue, + RemovedKeyValue, ChangedKeyValue { + final K key; + V _previousValue, _currentValue; + + KeyValueRecord _nextKeyValue; + KeyValueRecord _nextAddedKeyValue; + KeyValueRecord _nextRemovedKeyValue, _prevRemovedKeyValue; + KeyValueRecord _nextChangedKeyValue; + + KeyValueRecord(this.key); + + V get previousValue => _previousValue; + V get currentValue => _currentValue; + KeyValue get nextKeyValue => _nextKeyValue; + AddedKeyValue get nextAddedKeyValue => _nextAddedKeyValue; + RemovedKeyValue get nextRemovedKeyValue => _nextRemovedKeyValue; + ChangedKeyValue get nextChangedKeyValue => _nextChangedKeyValue; + + String toString() => _previousValue == _currentValue + ? key + : '$key[$_previousValue -> $_currentValue]'; +} + + +class _CollectionChangeRecord implements CollectionChangeRecord { + Iterable _iterable; + /** Used to keep track of items during moves. */ + DuplicateMap _items = new DuplicateMap(); + + /** Used to keep track of removed items. */ + DuplicateMap _removedItems = new DuplicateMap(); + + ItemRecord _collectionHead, _collectionTail; + ItemRecord _additionsHead, _additionsTail; + ItemRecord _movesHead, _movesTail; + ItemRecord _removalsHead, _removalsTail; + + CollectionChangeItem get collectionHead => _collectionHead; + CollectionChangeItem get additionsHead => _additionsHead; + CollectionChangeItem get movesHead => _movesHead; + CollectionChangeItem get removalsHead => _removalsHead; + + void forEachAddition(void f(AddedItem addition)){ + ItemRecord record = _additionsHead; + while(record != null) { + f(record); + record = record._nextAddedRec; + } + } + + void forEachMove(void f(MovedItem change)) { + ItemRecord record = _movesHead; + while(record != null) { + f(record); + record = record._nextMovedRec; + } + } + + void forEachRemoval(void f(RemovedItem removal)){ + ItemRecord record = _removalsHead; + while(record != null) { + f(record); + record = record._nextRemovedRec; + } + } + + Iterable get iterable => _iterable; + + bool _check(Iterable collection) { + _reset(); + ItemRecord record = _collectionHead; bool maybeDirty = false; if ((collection is UnmodifiableListView) && - identical(collectionChangeRecord._iterable, collection)) { + identical(_iterable, collection)) { // Short circuit and assume that the list has not been modified. return false; - } else if (collection is List) { + } + + if (collection is List) { List list = collection; - for(int index = 0, length = list.length; index < length; index++) { + for(int index = 0; index < list.length; index++) { var item = list[index]; if (record == null || !identical(item, record.item)) { - record = collectionChangeRecord.mismatch(record, item, index); + record = mismatch(record, item, index); maybeDirty = true; } else if (maybeDirty) { // TODO(misko): can we limit this to duplicates only? - record = collectionChangeRecord.verifyReinsertion(record, item, index); + record = verifyReinsertion(record, item, index); } record = record._nextRec; } @@ -417,89 +743,56 @@ class DirtyCheckingRecord implements ChangeRecord, WatchRecord { int index = 0; for(var item in collection) { if (record == null || !identical(item, record.item)) { - record = collectionChangeRecord.mismatch(record, item, index); + record = mismatch(record, item, index); maybeDirty = true; } else if (maybeDirty) { // TODO(misko): can we limit this to duplicates only? - record = collectionChangeRecord.verifyReinsertion(record, item, index); + record = verifyReinsertion(record, item, index); } record = record._nextRec; index++; } } - collectionChangeRecord.truncate(record); - collectionChangeRecord._iterable = collection; - return collectionChangeRecord.isDirty; - } - remove() { - _group._recordRemove(this); + _truncate(record); + _iterable = collection; + return isDirty; } - toString() => '${_MODE_NAMES[_mode]}[$field]'; -} - -final Object _INITIAL_ = new Object(); - -//class _MapChangeRecord implements CollectionChangeRecord { -//} - -class _CollectionChangeRecord implements CollectionChangeRecord { - Iterable _iterable; - /** Used to keep track of items during moves. */ - DuplicateMap _items = new DuplicateMap(); - - /** Used to keep track of removed items. */ - DuplicateMap _removedItems = new DuplicateMap(); - - ItemRecord _collectionHead, _collectionTail; - ItemRecord _additionsHead, _additionsTail; - ItemRecord _movesHead, _movesTail; - ItemRecord _removalsHead, _removalsTail; - - CollectionChangeItem get collectionHead => _collectionHead; - CollectionChangeItem get additionsHead => _additionsHead; - CollectionChangeItem get movesHead => _movesHead; - CollectionChangeItem get removalsHead => _removalsHead; - - Iterable get iterable => _iterable; - /** * Reset the state of the change objects to show no changes. This means set * previousKey to currentKey, and clear all of the queues (additions, moves, * removals). */ - _reset() { + void _reset() { ItemRecord record; record = _additionsHead; while(record != null) { - record.previousKey = record.currentKey; + record.previousIndex = record.currentIndex; record = record._nextAddedRec; } _additionsHead = _additionsTail = null; record = _movesHead; while(record != null) { - record.previousKey = record.currentKey; - record = record._nextMovedRec; + record.previousIndex = record.currentIndex; + var nextRecord = record._nextMovedRec; + assert((record._nextMovedRec = null) == null); + record = nextRecord; } _movesHead = _movesTail = null; - - record = _removalsHead; - while(record != null) { - record.previousKey = record.currentKey; - record = record._nextRemovedRec; - } _removalsHead = _removalsTail = null; + assert(isDirty == false); } /** * A [_CollectionChangeRecord] is considered dirty if it has additions, moves * or removals. */ - get isDirty => _additionsHead != null || _movesHead != null || - _removalsHead != null; + get isDirty => _additionsHead != null || + _movesHead != null || + _removalsHead != null; /** * This is the core function which handles differences between collections. @@ -509,19 +802,26 @@ class _CollectionChangeRecord implements CollectionChangeRecord { * - [item] is the current item in the collection * - [index] is the position of the item in the collection */ - mismatch(ItemRecord record, dynamic item, int index) { + ItemRecord mismatch(ItemRecord record, item, int index) { // Guard against bogus String changes - if (record != null && item is String && record.item is String && - record == item) { - // this is false change in strings we need to recover, and pretend it is - // the same. We save the value so that next time identity will pass - return record..item = item; + if (record != null) { + if (item is String && record.item is String && record.item == item) { + // this is false change in strings we need to recover, and pretend it is + // the same. We save the value so that next time identity can pass + return record..item = item; + } + + if (item is num && item.isNaN && record.item is num && record.item.isNaN){ + // we need this for JavaScript since in JS NaN !== NaN. + return record; + } } - // find the previous record os that we know where to insert after. + // find the previous record so that we know where to insert after. ItemRecord prev = record == null ? _collectionTail : record._prevRec; - // Remove the record from the collection since we know it does not match the item. + // Remove the record from the collection since we know it does not match the + // item. if (record != null) _collection_remove(record); // Attempt to see if we have seen the item before. record = _items.get(item, index); @@ -532,7 +832,8 @@ class _CollectionChangeRecord implements CollectionChangeRecord { // Never seen it, check evicted list. record = _removedItems.get(item); if (record != null) { - // It is an item which we have earlier evict it, reinsert it back into the list. + // It is an item which we have earlier evict it, reinsert it back into + // the list. _collection_reinsertAfter(record, prev, index); } else { // It is a new item add it. @@ -568,12 +869,13 @@ class _CollectionChangeRecord implements CollectionChangeRecord { * position. This is incorrect, since a better way to think of it is as insert * of 'b' rather then switch 'a' with 'b' and then add 'a' at the end. */ - verifyReinsertion(ItemRecord record, dynamic item, int index) { + ItemRecord verifyReinsertion(ItemRecord record, dynamic item, + int index) { ItemRecord reinsertRecord = _removedItems.get(item); if (reinsertRecord != null) { record = _collection_reinsertAfter(reinsertRecord, record._prevRec, index); - } else if (record.currentKey != index) { - record.currentKey = index; + } else if (record.currentIndex != index) { + record.currentIndex = index; _moves_add(record); } return record; @@ -584,7 +886,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { * * - [record] The first excess [ItemRecord]. */ - void truncate(ItemRecord record) { + void _truncate(ItemRecord record) { // Anything after that needs to be removed; while(record != null) { ItemRecord nextRecord = record._nextRec; @@ -594,7 +896,8 @@ class _CollectionChangeRecord implements CollectionChangeRecord { _removedItems.clear(); } - ItemRecord _collection_reinsertAfter(ItemRecord record, ItemRecord insertPrev, int index) { + ItemRecord _collection_reinsertAfter(ItemRecord record, ItemRecord insertPrev, + int index) { _removedItems.remove(record); var prev = record._prevRemovedRec; var next = record._nextRemovedRec; @@ -618,14 +921,16 @@ class _CollectionChangeRecord implements CollectionChangeRecord { return record; } - ItemRecord _collection_moveAfter(ItemRecord record, ItemRecord prev, int index) { + ItemRecord _collection_moveAfter(ItemRecord record, ItemRecord prev, + int index) { _collection_unlink(record); _collection_insertAfter(record, prev, index); _moves_add(record); return record; } - ItemRecord _collection_addAfter(ItemRecord record, ItemRecord prev, int index) { + ItemRecord _collection_addAfter(ItemRecord record, ItemRecord prev, + int index) { _collection_insertAfter(record, prev, index); if (_additionsTail == null) { @@ -639,7 +944,8 @@ class _CollectionChangeRecord implements CollectionChangeRecord { return record; } - ItemRecord _collection_insertAfter(ItemRecord record, ItemRecord prev, int index) { + ItemRecord _collection_insertAfter(ItemRecord record, ItemRecord prev, + int index) { assert(record != prev); assert(record._nextRec == null); assert(record._prevRec == null); @@ -661,7 +967,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } _items.put(record); - record.currentKey = index; + record.currentIndex = index; return record; } @@ -692,12 +998,12 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } ItemRecord _moves_add(ItemRecord record) { + assert(record._nextMovedRec == null); if (_movesTail == null) { assert(_movesHead == null); _movesTail = _movesHead = record; } else { assert(_movesTail._nextMovedRec == null); - assert(record._nextMovedRec == null); _movesTail = _movesTail._nextMovedRec = record; } @@ -705,7 +1011,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } ItemRecord _removals_add(ItemRecord record) { - record.currentKey = null; + record.currentIndex = null; _removedItems.put(record); if (_removalsTail == null) { @@ -720,7 +1026,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { return record; } - toString() { + String toString() { ItemRecord record; var list = []; @@ -760,32 +1066,33 @@ removals: ${removals.join(", ")}' } } -class ItemRecord implements CollectionItem, AddedItem, MovedItem, RemovedItem { - K previousKey = null; - K currentKey = null; +class ItemRecord implements CollectionItem, AddedItem, MovedItem, + RemovedItem { + int previousIndex = null; + int currentIndex = null; V item = _INITIAL_; - ItemRecord _prevRec, _nextRec; - ItemRecord _prevDupRec, _nextDupRec; - ItemRecord _prevRemovedRec, _nextRemovedRec; - ItemRecord _nextAddedRec, _nextMovedRec; + ItemRecord _prevRec, _nextRec; + ItemRecord _prevDupRec, _nextDupRec; + ItemRecord _prevRemovedRec, _nextRemovedRec; + ItemRecord _nextAddedRec, _nextMovedRec; - CollectionItem get nextCollectionItem => _nextRec; - RemovedItem get nextRemovedItem => _nextRemovedRec; - AddedItem get nextAddedItem => _nextAddedRec; - MovedItem get nextMovedItem => _nextMovedRec; + CollectionItem get nextCollectionItem => _nextRec; + RemovedItem get nextRemovedItem => _nextRemovedRec; + AddedItem get nextAddedItem => _nextAddedRec; + MovedItem get nextMovedItem => _nextMovedRec; ItemRecord(this.item); - toString() => previousKey == currentKey ? - '$item' : - '$item[$previousKey -> $currentKey]'; + String toString() => previousIndex == currentIndex + ? '$item' + : '$item[$previousIndex -> $currentIndex]'; } class _DuplicateItemRecordList { ItemRecord head, tail; - add(ItemRecord record, ItemRecord beforeRecord) { + void add(ItemRecord record, ItemRecord beforeRecord) { assert(record._prevDupRec == null); assert(record._nextDupRec == null); assert(beforeRecord == null ? true : beforeRecord.item == record.item); @@ -803,17 +1110,21 @@ class _DuplicateItemRecordList { var next = beforeRecord; record._prevDupRec = prev; record._nextDupRec = next; - if (prev == null) head = record; else prev._nextDupRec = record; + if (prev == null) { + head = record; + } else { + prev._nextDupRec = record; + } next._prevDupRec = record; } } } - ItemRecord get(dynamic key, int hideIndex) { + ItemRecord get(key, int hideIndex) { ItemRecord record = head; while(record != null) { - if (hideIndex == null ? true : hideIndex < record.currentKey && - identical(record.item, key)) { + if (hideIndex == null || + hideIndex < record.currentIndex && identical(record.item, key)) { return record; } record = record._nextDupRec; @@ -853,17 +1164,17 @@ class _DuplicateItemRecordList { } /** - * This is a custom map which supports duplicate [ItemRecord] values for each key. + * This is a custom map which supports duplicate [ItemRecord] values for each + * key. */ class DuplicateMap { - final Map map = - new Map(); + final map = {}; void put(ItemRecord record, [ItemRecord beforeRecord = null]) { assert(record._nextDupRec == null); assert(record._prevDupRec == null); - map.putIfAbsent(record.item, () => - new _DuplicateItemRecordList()).add(record, beforeRecord); + map.putIfAbsent(record.item, () => new _DuplicateItemRecordList()) + .add(record, beforeRecord); } /** @@ -875,20 +1186,19 @@ class DuplicateMap { * then asking if we have any more `a`s needs to return the last `a` not the * first or second. */ - ItemRecord get(dynamic key, [int hideIndex]) { + ItemRecord get(key, [int hideIndex]) { _DuplicateItemRecordList recordList = map[key]; - ItemRecord item = recordList == null ? null : recordList.get(key, hideIndex); - return item; + return recordList == null ? null : recordList.get(key, hideIndex); } ItemRecord remove(ItemRecord record) { _DuplicateItemRecordList recordList = map[record.item]; assert(recordList != null); - if (recordList.remove(record)) { - map.remove(record.item); - } + if (recordList.remove(record)) map.remove(record.item); return record; } - clear() => map.clear(); + void clear() { + map.clear(); + } } diff --git a/lib/change_detection/linked_list.dart b/lib/change_detection/linked_list.dart index 6c7f883ef..1c31c5657 100644 --- a/lib/change_detection/linked_list.dart +++ b/lib/change_detection/linked_list.dart @@ -21,9 +21,9 @@ class _LinkedList { return item; } - static _isEmpty(_Handler list) => list._head == null; + static bool _isEmpty(_Handler list) => list._head == null; - static _remove(_Handler list, _Handler item) { + static void _remove(_Handler list, _Handler item) { var previous = item._previous; var next = item._next; @@ -48,9 +48,9 @@ class _ArgHandlerList { return item; } - static _isEmpty(_InvokeHandler list) => list._argHandlerHead == null; + static bool _isEmpty(_InvokeHandler list) => list._argHandlerHead == null; - static _remove(_InvokeHandler list, _ArgHandler item) { + static void _remove(_InvokeHandler list, _ArgHandler item) { var previous = item._previousArgHandler; var next = item._nextArgHandler; @@ -75,9 +75,9 @@ class _WatchList { return item; } - static _isEmpty(_Handler list) => list._watchHead == null; + static bool _isEmpty(_Handler list) => list._watchHead == null; - static _remove(_Handler list, Watch item) { + static void _remove(_Handler list, Watch item) { var previous = item._previousWatch; var next = item._nextWatch; @@ -106,13 +106,12 @@ abstract class _EvalWatchList { if (prev != null) prev._nextEvalWatch = item; if (next != null) next._previousEvalWatch = item; - list._evalWatchTail = item; - return item; + return list._evalWatchTail = item; } - static _isEmpty(_EvalWatchList list) => list._evalWatchHead == null; + static bool _isEmpty(_EvalWatchList list) => list._evalWatchHead == null; - static _remove(_EvalWatchList list, _EvalWatchRecord item) { + static void _remove(_EvalWatchList list, _EvalWatchRecord item) { assert(item.watchGrp == list); var prev = item._previousEvalWatch; var next = item._nextEvalWatch; @@ -149,9 +148,9 @@ class _WatchGroupList { return item; } - static _isEmpty(_WatchGroupList list) => list._watchGroupHead == null; + static bool _isEmpty(_WatchGroupList list) => list._watchGroupHead == null; - static _remove(_WatchGroupList list, WatchGroup item) { + static void _remove(_WatchGroupList list, WatchGroup item) { var previous = item._previousWatchGroup; var next = item._nextWatchGroup; diff --git a/lib/change_detection/prototype_map.dart b/lib/change_detection/prototype_map.dart index 5c0b7f298..130444184 100644 --- a/lib/change_detection/prototype_map.dart +++ b/lib/change_detection/prototype_map.dart @@ -3,22 +3,35 @@ part of angular.watch_group; class PrototypeMap implements Map { final Map prototype; final Map self = new Map(); + PrototypeMap(this.prototype); - operator []=(name, value) => self[name] = value; - operator [](name) => self.containsKey(name) ? self[name] : prototype[name]; + void operator []=(name, value) { + self[name] = value; + } + V operator [](name) => self.containsKey(name) ? self[name] : prototype[name]; - get isEmpty => self.isEmpty && prototype.isEmpty; - get isNotEmpty => self.isNotEmpty || prototype.isNotEmpty; - get keys => self.keys; - get values => self.values; - get length => self.length; + bool get isEmpty => self.isEmpty && prototype.isEmpty; + bool get isNotEmpty => self.isNotEmpty || prototype.isNotEmpty; + // todo(vbe) include prototype keys ? + Iterable get keys => self.keys; + // todo(vbe) include prototype values ? + Iterable get values => self.values; + int get length => self.length; - forEach(fn) => self.forEach(fn); - remove(key) => self.remove(key); + void forEach(fn) { + // todo(vbe) include prototype ? + self.forEach(fn); + } + V remove(key) => self.remove(key); clear() => self.clear; - containsKey(key) => self.containsKey(key); - containsValue(key) => self.containsValue(key); - addAll(map) => self.addAll(map); - putIfAbsent(key, fn) => self.putIfAbsent(key, fn); + // todo(vbe) include prototype ? + bool containsKey(key) => self.containsKey(key); + // todo(vbe) include prototype ? + bool containsValue(key) => self.containsValue(key); + void addAll(map) { + self.addAll(map); + } + // todo(vbe) include prototype ? + V putIfAbsent(key, fn) => self.putIfAbsent(key, fn); } diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index 923cb1b2a..13ca937d9 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -7,7 +7,8 @@ part 'linked_list.dart'; part 'ast.dart'; part 'prototype_map.dart'; -typedef ReactionFn(value, previousValue, object); +typedef ReactionFn(value, previousValue); +typedef ChangeLog(String expression, current, previous); /** * Extend this class if you wish to pretend to be a function, but you don't know @@ -48,6 +49,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { /// STATS: Number of field watchers which are in use. int _fieldCost = 0; int _collectionCost = 0; + int _evalCost = 0; /// STATS: Number of field watchers which are in use including child [WatchGroup]s. int get fieldCost => _fieldCost; @@ -75,7 +77,6 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { /// STATS: Number of invocation watchers (closures/methods) which are in use. int get evalCost => _evalCost; - int _evalCost = 0; /// STATS: Number of invocation watchers which are in use including child [WatchGroup]s. int get totalEvalCost { @@ -95,8 +96,8 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { _nextWatchGroup; final WatchGroup _parentWatchGroup; - WatchGroup._child(_parentWatchGroup, this._changeDetector, - this.context, this._cache, this._rootGroup) + WatchGroup._child(_parentWatchGroup, this._changeDetector, this.context, + this._cache, this._rootGroup) : _parentWatchGroup = _parentWatchGroup, id = '${_parentWatchGroup.id}.${_parentWatchGroup._nextChildId++}' { @@ -153,10 +154,11 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { var watchRecord = _changeDetector.watch(null, null, collectionHandler); _collectionCost++; collectionHandler.watchRecord = watchRecord; - WatchRecord<_Handler> astWR = _cache.putIfAbsent(ast.expression, () => ast.setupWatch(this)); + WatchRecord<_Handler> astWR = _cache.putIfAbsent(ast.expression, + () => ast.setupWatch(this)); - // We set a field forwarding handler on LHS. This will allow the change objects to propagate - // to the current WatchRecord. + // We set a field forwarding handler on LHS. This will allow the change + // objects to propagate to the current WatchRecord. astWR.handler.addForwardHandler(collectionHandler); // propagate the value from the LHS to here @@ -222,8 +224,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { } WatchGroup get _childWatchGroupTail { - WatchGroup tail = this; - WatchGroup nextTail; + var tail = this, nextTail; while ((nextTail = tail._watchGroupTail) != null) { tail = nextTail; } @@ -244,7 +245,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { this, _changeDetector.newGroup(), context == null ? this.context : context, - context == null ? this._cache: new Map>(), + context == null ? this._cache: >{}, _rootGroup == null ? this : _rootGroup); _WatchGroupList._add(this, childGroup); var marker = childGroup._marker; @@ -320,7 +321,7 @@ class RootWatchGroup extends WatchGroup { RootWatchGroup(ChangeDetector changeDetector, Object context): super._root(changeDetector, context); - get _rootGroup => this; + RootWatchGroup get _rootGroup => this; /** * Detect changes and process the [ReactionFn]s. @@ -330,14 +331,19 @@ class RootWatchGroup extends WatchGroup { * 2) process function/closure/method changes * 3) call an [ReactionFn]s * - * Each step is called in sequence. ([ReactionFn]s are not called until all previous steps are - * completed). + * Each step is called in sequence. ([ReactionFn]s are not called until all + * previous steps are completed). */ - int detectChanges() { + int detectChanges({EvalExceptionHandler exceptionHandler, + ChangeLog changeLog}) { // Process the ChangeRecords from the change detector ChangeRecord<_Handler> changeRecord = - (_changeDetector as ChangeDetector<_Handler>).collectChanges(); + (_changeDetector as ChangeDetector<_Handler>) + .collectChanges(exceptionHandler); while (changeRecord != null) { + if (changeLog != null) changeLog(changeRecord.handler.expression, + changeRecord.currentValue, + changeRecord.previousValue); changeRecord.handler.onChange(changeRecord); changeRecord = changeRecord.nextChange; } @@ -346,7 +352,16 @@ class RootWatchGroup extends WatchGroup { // Process our own function evaluations _EvalWatchRecord evalRecord = _evalWatchHead; while (evalRecord != null) { - evalRecord.check(); + try { + var change = evalRecord.check(); + if (change != null && changeLog != null) { + changeLog(evalRecord.handler.expression, + evalRecord.currentValue, + evalRecord.previousValue); + } + } catch (e, s) { + if (exceptionHandler == null) rethrow; else exceptionHandler(e, s); + } evalRecord = evalRecord._nextEvalWatch; } @@ -356,7 +371,11 @@ class RootWatchGroup extends WatchGroup { Watch dirtyWatch = _dirtyWatchHead; while(dirtyWatch != null) { count++; - dirtyWatch.invoke(); + try { + dirtyWatch.invoke(); + } catch (e, s) { + if (exceptionHandler == null) rethrow; else exceptionHandler(e, s); + } dirtyWatch = dirtyWatch._nextDirtyWatch; } _dirtyWatchHead = _dirtyWatchTail = null; @@ -398,12 +417,13 @@ class Watch { get expression => _record.handler.expression; - invoke() { + void invoke() { + if (_deleted || !_dirty) return; _dirty = false; - reactionFn(_record.currentValue, _record.previousValue, _record.object); + reactionFn(_record.currentValue, _record.previousValue); } - remove() { + void remove() { if (_deleted) throw new StateError('Already deleted!'); _deleted = true; var handler = _record.handler; @@ -475,11 +495,11 @@ abstract class _Handler implements _LinkedList, _LinkedListItem, _WatchList { } } - _releaseWatch() { + void _releaseWatch() { watchRecord.remove(); watchGrp._fieldCost--; } - acceptValue(dynamic object) => null; + acceptValue(object) => null; void onChange(ChangeRecord<_Handler> record) { assert(_next != this); // verify we are not detached @@ -515,7 +535,7 @@ class _FieldHandler extends _Handler { * This function forwards the watched object to the next [_Handler] * synchronously. */ - acceptValue(dynamic object) { + void acceptValue(object) { watchRecord.object = object; var changeRecord = watchRecord.check(); if (changeRecord != null) onChange(changeRecord); @@ -523,16 +543,18 @@ class _FieldHandler extends _Handler { } class _CollectionHandler extends _Handler { - _CollectionHandler(watchGrp, expression): super(watchGrp, expression); + _CollectionHandler(WatchGroup watchGrp, String expression) + : super(watchGrp, expression); /** * This function forwards the watched object to the next [_Handler] synchronously. */ - acceptValue(dynamic object) { + void acceptValue(object) { watchRecord.object = object; var changeRecord = watchRecord.check(); if (changeRecord != null) onChange(changeRecord); } - _releaseWatch() { + + void _releaseWatch() { watchRecord.remove(); watchGrp._collectionCost--; } @@ -548,9 +570,10 @@ class _ArgHandler extends _Handler { _releaseWatch() => null; _ArgHandler(WatchGroup watchGrp, this.watchRecord, int index) - : super(watchGrp, 'arg[$index]'), index = index; + : index = index, + super(watchGrp, 'arg[$index]'); - acceptValue(dynamic object) { + void acceptValue(object) { watchRecord.dirtyArgs = true; watchRecord.args[index] = object; } @@ -559,13 +582,22 @@ class _ArgHandler extends _Handler { class _InvokeHandler extends _Handler implements _ArgHandlerList { _ArgHandler _argHandlerHead, _argHandlerTail; - _InvokeHandler(watchGrp, expression): super(watchGrp, expression); + _InvokeHandler(WatchGroup watchGrp, String expression) + : super(watchGrp, expression); - acceptValue(dynamic object) => watchRecord.object = object; + void acceptValue(object) { + watchRecord.object = object; + } - _releaseWatch() => (watchRecord as _EvalWatchRecord).remove(); + void onChange(ChangeRecord<_Handler> record) { + super.onChange(record); + } + + void _releaseWatch() { + (watchRecord as _EvalWatchRecord).remove(); + } - release() { + void release() { super.release(); _ArgHandler current = _argHandlerHead; while(current != null) { @@ -601,11 +633,14 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, ChangeRecord<_Handler> _EvalWatchRecord(this.watchGrp, this.handler, this.fn, name, int arity) : args = new List(arity), name = name, - symbol = name == null ? null : new Symbol(name) - { - if (fn is FunctionApply) mode = _MODE_FUNCTION_APPLY_; - else if (fn is Function) mode = _MODE_FUNCTION_; - else mode = _MODE_NULL_; + symbol = name == null ? null : new Symbol(name) { + if (fn is FunctionApply) { + mode = _MODE_FUNCTION_APPLY_; + } else if (fn is Function) { + mode = _MODE_FUNCTION_; + } else { + mode = _MODE_NULL_; + } } _EvalWatchRecord.marker() @@ -646,11 +681,9 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, ChangeRecord<_Handler> mode = _MODE_MAP_CLOSURE_; } else { _instanceMirror = reflect(value); - if(_hasMethod(_instanceMirror.type, symbol)) { - mode = _MODE_METHOD_; - } else { - mode = _MODE_FIELD_CLOSURE_; - } + mode = _hasMethod(_instanceMirror.type, symbol) + ? _MODE_METHOD_ + : _MODE_FIELD_CLOSURE_; } } } @@ -703,14 +736,14 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, ChangeRecord<_Handler> get nextChange => null; - remove() { + void remove() { assert(mode != _MODE_DELETED_); assert((mode = _MODE_DELETED_) == _MODE_DELETED_); // Mark as deleted. watchGrp._evalCost--; _EvalWatchList._remove(watchGrp, this); } - toString() { + String toString() { if (mode == _MODE_MARKER_) return 'MARKER[$currentValue]'; return '${watchGrp.id}:${handler.expression}'; } diff --git a/lib/core/directive.dart b/lib/core/directive.dart index a5f32c04c..63fd69a0f 100644 --- a/lib/core/directive.dart +++ b/lib/core/directive.dart @@ -132,7 +132,7 @@ abstract class NgAnnotation { /** * Use the list to specify a expressions which are evaluated dynamically - * (ex. via [Scope.$eval]) and are otherwise not statically discoverable. + * (ex. via [Scope.eval]) and are otherwise not statically discoverable. */ final List exportExpressions; @@ -167,7 +167,7 @@ abstract class NgAnnotation { * Components can implement [NgAttachAware], [NgDetachAware], * [NgShadowRootAware] and declare these optional methods: * - * * `attach()` - Called on first [Scope.$digest()]. + * * `attach()` - Called on first [Scope.apply()]. * * `detach()` - Called on when owning scope is destroyed. * * `onShadowRoot(ShadowRoot shadowRoot)` - Called when [ShadowRoot] is loaded. */ @@ -261,7 +261,7 @@ RegExp _ATTR_NAME = new RegExp(r'\[([^\]]+)\]$'); * Directives can implement [NgAttachAware], [NgDetachAware] and * declare these optional methods: * - * * `attach()` - Called on first [Scope.$digest()]. + * * `attach()` - Called on first [Scope.apply()]. * * `detach()` - Called on when owning scope is destroyed. */ class NgDirective extends NgAnnotation { @@ -304,7 +304,7 @@ class NgDirective extends NgAnnotation { * Controllers can implement [NgAttachAware], [NgDetachAware] and * declare these optional methods: * - * * `attach()` - Called on first [Scope.$digest()]. + * * `attach()` - Called on first [Scope.apply()]. * * `detach()` - Called on when owning scope is destroyed. */ class NgController extends NgDirective { diff --git a/lib/core/interpolate.dart b/lib/core/interpolate.dart index 814d096a7..9a976019d 100644 --- a/lib/core/interpolate.dart +++ b/lib/core/interpolate.dart @@ -1,19 +1,15 @@ part of angular.core; -String _startSymbol = '{{'; -String _endSymbol = '}}'; -int _startSymbolLength = _startSymbol.length; -int _endSymbolLength = _endSymbol.length; - class Interpolation { final String template; final List seperators; - final List watchExpressions; + final List expressions; Function setter = (_) => _; - Interpolation(this.template, this.seperators, this.watchExpressions); + Interpolation(this.template, this.seperators, this.expressions); - String call(List parts, [_, __]) { + String call(List parts, [_]) { + if (parts == null) return seperators.join(''); var str = []; for(var i = 0, ii = parts.length; i < ii; i++) { str.add(seperators[i]); @@ -48,8 +44,13 @@ class Interpolate { * have embedded expression in order to return an interpolation function. * Strings with no embedded expression will return null for the * interpolation function. + * - `startSymbol`: The symbol to start interpolation. '{{' by default. + * - `endSymbol`: The symbol to end interpolation. '}}' by default. */ - Interpolation call(String template, [bool mustHaveExpression = false]) { + Interpolation call(String template, [bool mustHaveExpression = false, + String startSymbol = '{{', String endSymbol = '}}']) { + int startSymbolLength = startSymbol.length; + int endSymbolLength = endSymbol.length; int startIndex; int endIndex; int index = 0; @@ -58,16 +59,15 @@ class Interpolate { bool shouldAddSeparator = true; String exp; List separators = []; - List watchExpressions = []; + List expressions = []; while(index < length) { - if ( ((startIndex = template.indexOf(_startSymbol, index)) != -1) && - ((endIndex = template.indexOf(_endSymbol, startIndex + _startSymbolLength)) != -1) ) { + if ( ((startIndex = template.indexOf(startSymbol, index)) != -1) && + ((endIndex = template.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { separators.add(template.substring(index, startIndex)); - exp = template.substring(startIndex + _startSymbolLength, endIndex); - Expression expression = _parse(exp); - watchExpressions.add(expression.eval); - index = endIndex + _endSymbolLength; + exp = template.substring(startIndex + startSymbolLength, endIndex); + expressions.add(exp); + index = endIndex + endSymbolLength; hasInterpolation = true; } else { // we did not find anything, so we have to add the remainder to the chunks array @@ -80,7 +80,7 @@ class Interpolate { separators.add(''); } return (!mustHaveExpression || hasInterpolation) - ? new Interpolation(template, separators, watchExpressions) + ? new Interpolation(template, separators, expressions) : null; } } diff --git a/lib/core/module.dart b/lib/core/module.dart index 58a479f07..70ee0c5ae 100644 --- a/lib/core/module.dart +++ b/lib/core/module.dart @@ -2,11 +2,9 @@ library angular.core; import 'dart:async' as async; import 'dart:collection'; -import 'dart:convert' show JSON; import 'dart:mirrors'; import 'package:di/di.dart'; -import 'package:perf_api/perf_api.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/core/parser/lexer.dart'; @@ -15,6 +13,13 @@ import 'package:angular/utils.dart'; import 'package:angular/core/service.dart'; export 'package:angular/core/service.dart'; +import 'package:angular/change_detection/watch_group.dart'; +export 'package:angular/change_detection/watch_group.dart'; +import 'package:angular/change_detection/change_detection.dart'; +import 'package:angular/change_detection/dirty_checking_change_detector.dart'; +import 'package:angular/core/parser/utils.dart'; +import 'package:angular/core/parser/syntax.dart'; + part "cache.dart"; part "directive.dart"; part "exception_handler.dart"; @@ -34,7 +39,17 @@ class NgCoreModule extends Module { type(ExceptionHandler); type(FilterMap); type(Interpolate); - type(Scope); + type(RootScope); + value(GetterCache, new GetterCache({})); + value(Object, {}); // RootScope context + factory(Scope, (injector) { +// try { throw null; } +// catch (e, s) { +// print('DEPRECATED reference to Scope:\n$s'); +// } + return injector.get(RootScope); + }); + type(AstParser); type(NgZone); type(Parser, implementedBy: DynamicParser); diff --git a/lib/core/parser/eval.dart b/lib/core/parser/eval.dart index 1a99b5ac7..5ef3f0e9b 100644 --- a/lib/core/parser/eval.dart +++ b/lib/core/parser/eval.dart @@ -3,7 +3,6 @@ library angular.core.parser.eval; import 'package:angular/core/parser/syntax.dart' as syntax; import 'package:angular/core/parser/utils.dart'; import 'package:angular/core/module.dart'; -import 'package:angular/core/parser/syntax.dart'; export 'package:angular/core/parser/eval_access.dart'; export 'package:angular/core/parser/eval_calls.dart'; diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 6d1d004c6..3a5c46bcf 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -1,1039 +1,892 @@ part of angular.core; +NOT_IMPLEMENTED() { + throw new StateError('Not Implemented'); +} + +typedef EvalFunction0(); +typedef EvalFunction1(context); /** - * Injected into the listener function within [Scope.$on] to provide event-specific - * details to the scope listener. + * Injected into the listener function within [Scope.on] to provide + * event-specific details to the scope listener. */ class ScopeEvent { + static final String DESTROY = 'ng-destroy'; + + /** + * Data attached to the event. This would be the optional parameter + * from [Scope.emit] and [Scope.broadcast]. + */ + final data; /** * The name of the intercepted scope event. */ - String name; + final String name; /** - * The origin scope that triggered the event (via $broadcast or $emit). + * The origin scope that triggered the event (via broadcast or emit). */ - Scope targetScope; + final Scope targetScope; /** - * The destination scope that intercepted the event. + * The destination scope that intercepted the event. As + * the event traverses the scope hierarchy the the event instance + * stays the same, but the [currentScope] reflects the scope + * of the current listener which is firing. */ - Scope currentScope; + Scope get currentScope => _currentScope; + Scope _currentScope; /** * true or false depending on if stopPropagation() was executed. */ - bool propagationStopped = false; + bool get propagationStopped => _propagationStopped; + bool _propagationStopped = false; /** * true or false depending on if preventDefault() was executed. */ - bool defaultPrevented = false; + bool get defaultPrevented => _defaultPrevented; + bool _defaultPrevented = false; /** ** [name] - The name of the scope event. ** [targetScope] - The destination scope that is listening on the event. */ - ScopeEvent(this.name, this.targetScope); + ScopeEvent(this.name, this.targetScope, this.data); /** - * Prevents the intercepted event from propagating further to successive scopes. + * Prevents the intercepted event from propagating further to successive + * scopes. */ - stopPropagation () => propagationStopped = true; + void stopPropagation () { + _propagationStopped = true; + } /** * Sets the defaultPrevented flag to true. */ - preventDefault() => defaultPrevented = true; + void preventDefault() { + _defaultPrevented = true; + } } /** - * Allows the configuration of [Scope.$digest] iteration maximum time-to-live + * Allows the configuration of [Scope.digest] iteration maximum time-to-live * value. Digest keeps checking the state of the watcher getters until it * can execute one full iteration with no watchers triggering. TTL is used * to prevent an infinite loop where watch A triggers watch B which in turn - * triggers watch A. If the system does not stabilize in TTL iteration then - * an digest is stop an an exception is thrown. + * triggers watch A. If the system does not stabilize in TTL iterations then + * the digest is stopped and an exception is thrown. */ @NgInjectableService() class ScopeDigestTTL { - final num ttl; + final int ttl; ScopeDigestTTL(): ttl = 5; - ScopeDigestTTL.value(num this.ttl); + ScopeDigestTTL.value(this.ttl); +} + +//TODO(misko): I don't think this should be in scope. +class ScopeLocals implements Map { + static wrapper(scope, Map locals) => + new ScopeLocals(scope, locals); + + Map _scope; + Map _locals; + + ScopeLocals(this._scope, this._locals); + + void operator []=(String name, value) { + _scope[name] = value; + } + dynamic operator [](String name) => + (_locals.containsKey(name) ? _locals : _scope)[name]; + + bool get isEmpty => _scope.isEmpty && _locals.isEmpty; + bool get isNotEmpty => _scope.isNotEmpty || _locals.isNotEmpty; + List get keys => _scope.keys; + List get values => _scope.values; + int get length => _scope.length; + + void forEach(fn) { + _scope.forEach(fn); + } + dynamic remove(key) => _scope.remove(key); + void clear() { + _scope.clear; + } + bool containsKey(key) => _scope.containsKey(key); + bool containsValue(key) => _scope.containsValue(key); + void addAll(map) { + _scope.addAll(map); + } + dynamic putIfAbsent(key, fn) => _scope.putIfAbsent(key, fn); } /** - * Scope has two responsibilities. 1) to keep track af watches and 2) - * to keep references to the model so that they are available for - * data-binding. + * [Scope] is represents a collection of [watch]es [observe]ers, and [context] + * for the watchers, observers and [eval]uations. Scopes structure loosely + * mimics the DOM structure. Scopes and [Block]s are bound to each other. + * As scopes are created and destroyed by [BlockFactory] they are responsible + * for change detection, change processing and memory management. */ -@proxy -@NgInjectableService() -class Scope implements Map { - final ExceptionHandler _exceptionHandler; - final Parser _parser; - final NgZone _zone; - final num _ttl; - final Map _properties = {}; - final _WatchList _watchers = new _WatchList(); - final Map> _listeners = {}; - final bool _isolate; - final bool _lazy; - final Profiler _perf; - final FilterMap _filters; +class Scope { /** - * The direct parent scope that created this scope (this can also be the $rootScope) + * The default execution context for [watch]es [observe]ers, and [eval]uation. */ - final Scope $parent; + final context; /** - * The auto-incremented ID of the scope + * The [RootScope] of the application. */ - String $id; + final RootScope rootScope; + + Scope _parentScope; /** - * The topmost scope of the application (same as $rootScope). + * The parent [Scope]. */ - Scope $root; - num _nextId = 0; - String _phase; - List _innerAsyncQueue; - List _outerAsyncQueue; - Scope _nextSibling, _prevSibling, _childHead, _childTail; - bool _skipAutoDigest = false; - bool _disabled = false; - - _set$Properties() { - _properties[r'this'] = this; - _properties[r'$id'] = this.$id; - _properties[r'$parent'] = this.$parent; - _properties[r'$root'] = this.$root; - } + Scope get parentScope => _parentScope; + bool get isAttached => _parentScope == null ? false : _parentScope.isAttached; - Scope(this._exceptionHandler, this._parser, ScopeDigestTTL ttl, - this._zone, this._perf, this._filters): - $parent = null, _isolate = false, _lazy = false, _ttl = ttl.ttl { - $root = this; - $id = '_${$root._nextId++}'; - _innerAsyncQueue = []; - _outerAsyncQueue = []; - - // Set up the zone to auto digest this scope. - _zone.onTurnDone = _autoDigestOnTurnDone; - _zone.onError = (e, s, ls) => _exceptionHandler(e, s); - _set$Properties(); - } + // TODO(misko): WatchGroup should be private. + // Instead we should expose performance stats about the watches + // such as # of watches, checks/1ms, field checks, function checks, etc + final WatchGroup _readWriteGroup; + final WatchGroup _readOnlyGroup; + final int _depth; + final int _index; + + Scope _childHead, _childTail, _next, _prev; + _Streams _streams; + int _nextChildIndex = 0; + + Scope(Object this.context, this.rootScope, this._parentScope, this._depth, + this._index, this._readWriteGroup, this._readOnlyGroup); - Scope._child(Scope parent, bool this._isolate, bool this._lazy, this._perf, filters) - : $parent = parent, _ttl = parent._ttl, _parser = parent._parser, - _exceptionHandler = parent._exceptionHandler, _zone = parent._zone, - _filters = filters == null ? parent._filters : filters { - $root = $parent.$root; - $id = '_${$root._nextId++}'; - _innerAsyncQueue = $parent._innerAsyncQueue; - _outerAsyncQueue = $parent._outerAsyncQueue; - - _prevSibling = $parent._childTail; - if ($parent._childHead != null) { - $parent._childTail._nextSibling = this; - $parent._childTail = this; + /** + * A [watch] sets up a watch in the [digest] phase of the [apply] cycle. + * + * Use [watch] if the reaction function can cause updates to model. In your + * controller code you will most likely use [watch]. + */ + Watch watch(expression, ReactionFn reactionFn, + {context, FilterMap filters, bool readOnly: false}) { + assert(isAttached); + assert(expression != null); + AST ast; + Watch watch; + ReactionFn fn = reactionFn; + if (expression is AST) { + ast = expression; + } else if (expression is String) { + if (expression.startsWith('::')) { + expression = expression.substring(2); + fn = (value, last) { + if (value != null) { + watch.remove(); + return reactionFn(value, last); + } + }; + } else if (expression.startsWith(':')) { + expression = expression.substring(1); + fn = (value, last) { + if (value != null) { + return reactionFn(value, last); + } + }; + } + ast = rootScope._astParser(expression, context: context, filters: filters); } else { - $parent._childHead = $parent._childTail = this; + throw 'expressions must be String or AST got $expression.'; } - _set$Properties(); + return watch = (readOnly ? _readOnlyGroup : _readWriteGroup).watch(ast, fn); } - _autoDigestOnTurnDone() { - if ($root._skipAutoDigest) { - $root._skipAutoDigest = false; + dynamic eval(expression, [Map locals]) { + assert(isAttached); + assert(expression == null || + expression is String || + expression is Function); + if (expression is String && expression.isNotEmpty) { + var obj = locals == null ? context : new ScopeLocals(context, locals); + return rootScope._parser(expression).eval(obj); } else { - $digest(); + assert(locals == null); + if (expression is EvalFunction1) return expression(context); + if (expression is EvalFunction0) return expression(); } } - _identical(a, b) => - identical(a, b) || - (a is String && b is String && a == b) || - (a is num && b is num && a.isNaN && b.isNaN); + dynamic applyInZone([expression, Map locals]) => + rootScope._zone.run(() => apply(expression, locals)); - containsKey(String name) { - for (var scope = this; scope != null; scope = scope.$parent) { - if (scope._properties.containsKey(name)) { - return true; - } else if(scope._isolate) { - break; - } + dynamic apply([expression, Map locals]) { + rootScope._transitionState(null, RootScope.STATE_APPLY); + try { + return eval(expression, locals); + } catch (e, s) { + rootScope._exceptionHandler(e, s); + } finally { + rootScope + .._transitionState(RootScope.STATE_APPLY, null) + ..digest() + ..flush(); } - return false; } - remove(String name) => this._properties.remove(name); - operator []=(String name, value) => _properties[name] = value; - operator [](String name) { - for (var scope = this; scope != null; scope = scope.$parent) { - if (scope._properties.containsKey(name)) { - return scope._properties[name]; - } else if(scope._isolate) { - break; - } - } - return null; + ScopeEvent emit(String name, [data]) { + assert(isAttached); + return _Streams.emit(this, name, data); + } + ScopeEvent broadcast(String name, [data]) { + assert(isAttached); + return _Streams.broadcast(this, name, data); + } + ScopeStream on(String name) { + assert(isAttached); + return _Streams.on(this, rootScope._exceptionHandler, name); + } + + Scope createChild(Object childContext) { + assert(isAttached); + var child = new Scope(childContext, rootScope, this, + _depth + 1, _nextChildIndex++, + _readWriteGroup.newGroup(childContext), + _readOnlyGroup.newGroup(childContext)); + var next = null; + var prev = _childTail; + child._next = next; + child._prev = prev; + if (prev == null) _childHead = child; else prev._next = child; + if (next == null) _childTail = child; else next._prev = child; + return child; } - noSuchMethod(Invocation invocation) { - var name = MirrorSystem.getName(invocation.memberName); - if (invocation.isGetter) { - return this[name]; - } else if (invocation.isSetter) { - var value = invocation.positionalArguments[0]; - name = name.substring(0, name.length - 1); - this[name] = value; - return value; + void destroy() { + assert(isAttached); + broadcast(ScopeEvent.DESTROY); + _Streams.destroy(this); + + var prev = _prev; + var next = _next; + if (prev == null) { + _parentScope._childHead = next; } else { - if (this[name] is Function) { - return this[name](); - } else { - super.noSuchMethod(invocation); - } + prev._next = next; + } + if (next == null) { + _parentScope._childTail = prev; + } else { + next._prev = prev; } - } + this._next = this._prev = null; - /** - * Create a new child [Scope]. - * - * * [isolate] - If set to true the child scope does not inherit properties from the parent scope. - * This in essence creates an independent (isolated) view for the users of the scope. - * * [lazy] - If set to true the scope digest will only run if the scope is marked as [$dirty]. - * This is usefull if we expect that the bindings in the scope are constant and there is no need - * to check them on each digest. The digest can be forced by marking it [$dirty]. - */ - $new({bool isolate: false, bool lazy: false, FilterMap filters}) => - new Scope._child(this, isolate, lazy, _perf, filters); + _readWriteGroup.remove(); + _readOnlyGroup.remove(); + _parentScope = null; + } +} - /** - * *EXPERIMENTAL:* This feature is experimental. We reserve the right to change or delete it. - * - * A dissabled scope will not be part of the [$digest] cycle until it is re-enabled. - */ - set $disabled(value) => this._disabled = value; - get $disabled => this._disabled; - /** - * Registers a listener callback to be executed whenever the [watchExpression] changes. - * - * The watchExpression is called on every call to [$digest] and should return the value that - * will be watched. (Since [$digest] reruns when it detects changes the watchExpression can - * execute multiple times per [$digest] and should be idempotent.) - * - * The listener is called only when the value from the current [watchExpression] and the - * previous call to [watchExpression] are not identical (with the exception of the initial run, - * see below). - * - * The watch listener may change the model, which may trigger other listeners to fire. This is - * achieved by rerunning the watchers until no changes are detected. The rerun iteration limit - * is 10 to prevent an infinite loop deadlock. - * If you want to be notified whenever [$digest] is called, you can register a [watchExpression] - * function with no listener. (Since [watchExpression] can execute multiple times per [$digest] - * cycle when a change is detected, be prepared for multiple calls to your listener.) - * - * After a watcher is registered with the scope, the listener fn is called asynchronously - * (via [$evalAsync]) to initialize the watcher. In rare cases, this is undesirable because the - * listener is called when the result of [watchExpression] didn't change. To detect this - * scenario within the listener fn, you can compare the newVal and oldVal. If these two values - * are identical then the listener was called due to initialization. - * - * * [watchExpression] - can be any one of these: a [Function] - `(Scope scope) => ...;` or a - * [String] - `expression` which is compiled with [Parser] service into a function - * * [listener] - A [Function] `(currentValue, previousValue, Scope scope) => ...;` - * * [watchStr] - Used as a debbuging hint to easier identify which expression is associated with - * this watcher. - */ - $watch(watchExpression, [Function listener, String watchStr]) { - if (watchStr == null) { - watchStr = watchExpression.toString(); +class RootScope extends Scope { + static final STATE_APPLY = 'apply'; + static final STATE_DIGEST = 'digest'; + static final STATE_FLUSH = 'digest'; - // Keep prod fast - assert((() { - watchStr = _source(watchExpression); - return true; - })()); - } - var watcher = new _Watch(_compileToFn(listener), _initWatchVal, - _compileToFn(watchExpression), watchStr); - _watchers.addLast(watcher); - return () => _watchers.remove(watcher); - } + final ExceptionHandler _exceptionHandler; + final AstParser _astParser; + final Parser _parser; + final ScopeDigestTTL _ttl; + final ExpressionVisitor visitor = new ExpressionVisitor(); // TODO(misko): delete me + final NgZone _zone; - /** - * A variant of [$watch] where it watches a collection of [watchExpressios]. If any - * one expression in the collection changes the [listener] is executed. - * - * * [watcherExpressions] - `List` - * * [Listener] - `(List newValues, List previousValues, Scope scope)` - */ - $watchSet(List watchExpressions, [Function listener, String watchStr]) { - if (watchExpressions.length == 0) return () => null; - - var lastValues = new List(watchExpressions.length); - var currentValues = new List(watchExpressions.length); - - if (watchExpressions.length == 1) { - // Special case size of one. - return $watch(watchExpressions[0], (value, oldValue, scope) { - currentValues[0] = value; - lastValues[0] = oldValue; - listener(currentValues, lastValues, scope); - }); - } - var deregesterFns = []; - var changeCount = 0; - for(var i = 0, ii = watchExpressions.length; i < ii; i++) { - deregesterFns.add($watch(watchExpressions[i], (value, oldValue, __) { - currentValues[i] = value; - lastValues[i] = oldValue; - changeCount++; - })); - } - deregesterFns.add($watch((s) => changeCount, (c, o, scope) { - listener(currentValues, lastValues, scope); - })); - return () { - for(var i = 0, ii = deregesterFns.length; i < ii; i++) { - deregesterFns[i](); - } + _FunctionChain _runAsyncHead, _runAsyncTail; + _FunctionChain _domWriteHead, _domWriteTail; + _FunctionChain _domReadHead, _domReadTail; + + String _state; + + RootScope(Object context, this._astParser, this._parser, + GetterCache cacheGetter, FilterMap filterMap, + this._exceptionHandler, this._ttl, this._zone) + : super(context, null, null, 0, 0, + new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context), + new RootWatchGroup(new DirtyCheckingChangeDetector(cacheGetter), context)) + { + _zone.onTurnDone = () { + digest(); + flush(); }; } - /** - * Shallow watches the properties of an object and fires whenever any of the properties change - * (for arrays, this implies watching the array items; for object maps, this implies watching - * the properties). If a change is detected, the listener callback is fired. - * - * The obj collection is observed via standard [$watch] operation and is examined on every call - * to [$digest] to see if any items have been added, removed, or moved. - * - * The listener is called whenever anything within the obj has changed. Examples include - * adding, removing, and moving items belonging to an object or array. - */ - $watchCollection(obj, listener, [String expression, bool shallow=false]) { - var oldValue; - var newValue; - int changeDetected = 0; - Function objGetter = relaxFnArgs2(_compileToFn(obj)); - List internalArray = []; - Map internalMap = {}; - int oldLength = 0; - int newLength; - var key; - List keysToRemove = []; - Function detectNewKeys = (key, value) { - newLength++; - if (oldValue.containsKey(key)) { - if (!_identical(oldValue[key], value)) { - changeDetected++; - oldValue[key] = value; - } - } else { - oldLength++; - oldValue[key] = value; - changeDetected++; - } - }; - Function findMissingKeys = (key, _) { - if (!newValue.containsKey(key)) { - oldLength--; - keysToRemove.add(key); - } - }; - - Function removeMissingKeys = (k) => oldValue.remove(k); - - var $watchCollectionWatch; + RootScope get rootScope => this; + bool get isAttached => true; - if (shallow) { - $watchCollectionWatch = (_) { - newValue = objGetter(this, _filters); - newLength = newValue == null ? 0 : newValue.length; - if (newLength != oldLength) { - oldLength = newLength; - changeDetected++; - } - if (!identical(oldValue, newValue)) { - oldValue = newValue; - changeDetected++; + void digest() { + _transitionState(null, STATE_DIGEST); + try { + var rootWatchGroup = (_readWriteGroup as RootWatchGroup); + + int digestTTL = _ttl.ttl; + const int LOG_COUNT = 3; + List log; + List digestLog; + var count; + ChangeLog changeLog; + do { + while(_runAsyncHead != null) { + try { + _runAsyncHead.fn(); + } catch (e, s) { + _exceptionHandler(e, s); + } + _runAsyncHead = _runAsyncHead._next; } - return changeDetected; - }; - } else { - $watchCollectionWatch = (_) { - newValue = objGetter(this, _filters); - if (newValue is! Map && newValue is! List) { - if (!_identical(oldValue, newValue)) { - oldValue = newValue; - changeDetected++; + digestTTL--; + count = rootWatchGroup.detectChanges( + exceptionHandler: _exceptionHandler, changeLog: changeLog); + + if (digestTTL <= LOG_COUNT) { + if (changeLog == null) { + log = []; + digestLog = []; + changeLog = (e, c, p) => digestLog.add('$e: $c <= $p'); + } else { + log.add(digestLog.join(', ')); + digestLog.clear(); } - } else if (newValue is Iterable) { - if (!_identical(oldValue, internalArray)) { - // we are transitioning from something which was not an array into array. - oldValue = internalArray; - oldLength = oldValue.length = 0; - changeDetected++; - } - - newLength = newValue.length; + } + if (digestTTL == 0) { + throw 'Model did not stabilize in ${_ttl.ttl} digests. ' + 'Last $LOG_COUNT iterations:\n${log.join('\n')}'; + } + } while (count > 0); + } finally { + _transitionState(STATE_DIGEST, null); + } + } - if (oldLength != newLength) { - // if lengths do not match we need to trigger change notification - changeDetected++; - oldValue.length = oldLength = newLength; - } - // copy the items to oldValue and look for changes. - for (var i = 0; i < newLength; i++) { - if (!_identical(oldValue[i], newValue.elementAt(i))) { - changeDetected++; - oldValue[i] = newValue.elementAt(i); - } - } - } else { // Map - if (!_identical(oldValue, internalMap)) { - // we are transitioning from something which was not an object into object. - oldValue = internalMap = {}; - oldLength = 0; - changeDetected++; + void flush() { + _transitionState(null, STATE_FLUSH); + var observeGroup = this._readOnlyGroup as RootWatchGroup; + bool runObservers = true; + try { + do { + while(_domWriteHead != null) { + try { + _domWriteHead.fn(); + } catch (e, s) { + _exceptionHandler(e, s); } - // copy the items to oldValue and look for changes. - newLength = 0; - newValue.forEach(detectNewKeys); - if (oldLength > newLength) { - // we used to have more keys, need to find them and destroy them. - changeDetected++; - oldValue.forEach(findMissingKeys); - keysToRemove.forEach(removeMissingKeys); - keysToRemove.clear(); + _domWriteHead = _domWriteHead._next; + } + if (runObservers) { + runObservers = false; + observeGroup.detectChanges(exceptionHandler:_exceptionHandler); + } + while(_domReadHead != null) { + try { + _domReadHead.fn(); + } catch (e, s) { + _exceptionHandler(e, s); } + _domReadHead = _domReadHead._next; } - return changeDetected; - }; + } while (_domWriteHead != null || _domReadHead != null); + assert((() { + var watchLog = []; + var observeLog = []; + (_readWriteGroup as RootWatchGroup).detectChanges( + changeLog: (s, c, p) => watchLog.add('$s: $c <= $p')); + (observeGroup as RootWatchGroup).detectChanges( + changeLog: (s, c, p) => watchLog.add('$s: $c <= $p')); + if (watchLog.isNotEmpty || observeLog.isNotEmpty) { + throw 'Observer reaction functions should not change model. \n' + 'These watch changes were detected: ${watchLog.join('; ')}\n' + 'These observe changes were detected: ${observeLog.join('; ')}'; + } + return true; + })()); + } finally { + _transitionState(STATE_FLUSH, null); } - var $watchCollectionAction = (_, __, ___) { - relaxFnApply(listener, [newValue, oldValue, this]); - }; - - return this.$watch($watchCollectionWatch, - $watchCollectionAction, - expression == null ? obj : expression); } - - /** - * Add this function to your code if you want to add a $digest - * and want to assert that the digest will be called on this turn. - * This method will be deleted when we are comfortable with - * auto-digesting scope. - */ - $$verifyDigestWillRun() { - assert(!$root._skipAutoDigest); - _zone.assertInTurn(); - } - - /** - * *EXPERIMENTAL:* This feature is experimental. We reserve the right to change or delete it. - * - * Marks a scope as dirty. If the scope is lazy (see [$new]) then the scope will be included - * in the next [$digest]. - * - * NOTE: This has no effect for non-lazy scopes. - */ - $dirty() { - this._disabled = false; - } - - /** - * Processes all of the watchers of the current scope and its children. - * Because a watcher's listener can change the model, the `$digest()` operation keeps calling - * the watchers no further response data has changed. This means that it is possible to get - * into an infinite loop. This function will throw `'Maximum iteration limit exceeded.'` - * if the number of iterations exceeds 10. - * - * There should really be no need to call $digest() in production code since everything is - * handled behind the scenes with zones and object mutation events. However, in testing - * both $digest and [$apply] are useful to control state and simulate the scope life cycle in - * a step-by-step manner. - * - * Refer to [$watch], [$watchSet] or [$watchCollection] to see how to register watchers that - * are executed during the digest cycle. - */ - $digest() { - try { - _beginPhase('\$digest'); - _digestWhileDirtyLoop(); - } catch (e, s) { - _exceptionHandler(e, s); - } finally { - _clearPhase(); + // QUEUES + void runAsync(fn()) { + var chain = new _FunctionChain(fn); + if (_runAsyncHead == null) { + _runAsyncHead = _runAsyncTail = chain; + } else { + _runAsyncTail = _runAsyncTail._next = chain; } } - - _digestWhileDirtyLoop() { - _digestHandleQueue('ng.innerAsync', _innerAsyncQueue); - - int timerId; - assert((timerId = _perf.startTimer('ng.dirty_check', 0)) != false); - _Watch lastDirtyWatch = _digestComputeLastDirty(); - assert(_perf.stopTimer(timerId) != false); - - if (lastDirtyWatch == null) { - _digestHandleQueue('ng.outerAsync', _outerAsyncQueue); - return; + void domWrite(fn()) { + var chain = new _FunctionChain(fn); + if (_domWriteHead == null) { + _domWriteHead = _domWriteTail = chain; + } else { + _domWriteTail = _domWriteTail._next = chain; } + } - List> watchLog = []; - for (int iteration = 1, ttl = _ttl; iteration < ttl; iteration++) { - _Watch stopWatch = _digestHandleQueue('ng.innerAsync', _innerAsyncQueue) - ? null // Evaluating async work requires re-evaluating all watchers. - : lastDirtyWatch; - lastDirtyWatch = null; - - List expressionLog; - if (ttl - iteration <= 3) { - expressionLog = []; - watchLog.add(expressionLog); - } - - int timerId; - assert((timerId = _perf.startTimer('ng.dirty_check', iteration)) != false); - lastDirtyWatch = _digestComputeLastDirtyUntil(stopWatch, expressionLog); - assert(_perf.stopTimer(timerId) != false); - - if (lastDirtyWatch == null) { - _digestComputePerfCounters(); - _digestHandleQueue('ng.outerAsync', _outerAsyncQueue); - return; - } + void domRead(fn()) { + var chain = new _FunctionChain(fn); + if (_domReadHead == null) { + _domReadHead = _domReadTail = chain; + } else { + _domReadTail = _domReadTail._next = chain; } - - // I've seen things you people wouldn't believe. Attack ships on fire - // off the shoulder of Orion. I've watched C-beams glitter in the dark - // near the Tannhauser Gate. All those moments will be lost in time, - // like tears in rain. Time to die. - throw '$_ttl \$digest() iterations reached. Aborting!\n' - 'Watchers fired in the last ${watchLog.length} iterations: ' - '${_toJson(watchLog)}'; } + void destroy() {} - bool _digestHandleQueue(String timerName, List queue) { - if (queue.isEmpty) { - return false; - } - do { - var timerId; - try { - var workFn = queue.removeAt(0); - assert((timerId = _perf.startTimer(timerName, _source(workFn))) != false); - $root.$eval(workFn); - } catch (e, s) { - _exceptionHandler(e, s); - } finally { - assert(_perf.stopTimer(timerId) != false); - } - } while (queue.isNotEmpty); - return true; + void _transitionState(String from, String to) { + assert(isAttached); + if (_state != from) throw "$_state already in progress can not enter $to."; + _state = to; } +} - - _Watch _digestComputeLastDirty() { - int watcherCount = 0; - int scopeCount = 0; - Scope scope = this; - do { - _WatchList watchers = scope._watchers; - watcherCount += watchers.length; - scopeCount++; - for (_Watch watch = watchers.head; watch != null; watch = watch.next) { - var last = watch.last; - var value = watch.get(scope, scope._filters); - if (!_identical(value, last)) { - return _digestHandleDirty(scope, watch, last, value, null); +/** + * Keeps track of Streams for each Scope. When emitting events + * we would need to walk the whole tree. Its faster if we can prune + * the Scopes we have to visit. + * + * Scope with no [_ScopeStreams] has no events registered on itself or children + * + * We keep track of [Stream]s, and also child scope [Stream]s. To save + * memory we use the same stream object on all of our parents if they don't + * have one. But that means that we have to keep track if the stream belongs + * to the node. + * + * Scope with [_ScopeStreams] but who's [_scope] does not match the scope + * is only inherited + * + * Only [Scope] with [_ScopeStreams] who's [_scope] matches the [Scope] + * instance is the actual scope. + * + * Once the [Stream] is created it can not be removed even if all listeners + * are canceled. That is because we don't know if someone still has reference + * to it. + */ +class _Streams { + final ExceptionHandler _exceptionHandler; + /// Scope we belong to. + final Scope _scope; + /// [Stream]s for [_scope] only + final _streams = new Map(); + /// Child [Scope] event counts. + final Map _typeCounts; + + _Streams(this._scope, this._exceptionHandler, _Streams inheritStreams) + : _typeCounts = inheritStreams == null + ? {} + : new Map.from(inheritStreams._typeCounts); + + static ScopeEvent emit(Scope scope, String name, data) { + var event = new ScopeEvent(name, scope, data); + var scopeCursor = scope; + while(scopeCursor != null) { + if (scopeCursor._streams != null && + scopeCursor._streams._scope == scopeCursor) { + ScopeStream stream = scopeCursor._streams._streams[name]; + if (stream != null) { + event._currentScope = scopeCursor; + stream._fire(event); + if (event.propagationStopped) return event; } } - } while ((scope = _digestComputeNextScope(scope)) != null); - _digestUpdatePerfCounters(watcherCount, scopeCount); - return null; + scopeCursor = scopeCursor._parentScope; + } + return event; } - - _Watch _digestComputeLastDirtyUntil(_Watch stopWatch, List log) { - int watcherCount = 0; - int scopeCount = 0; - Scope scope = this; - do { - _WatchList watchers = scope._watchers; - watcherCount += watchers.length; - scopeCount++; - for (_Watch watch = watchers.head; watch != null; watch = watch.next) { - if (identical(stopWatch, watch)) return null; - var last = watch.last; - var value = watch.get(scope, scope._filters); - if (!_identical(value, last)) { - return _digestHandleDirty(scope, watch, last, value, log); + static ScopeEvent broadcast(Scope scope, String name, data) { + _Streams scopeStreams = scope._streams; + var event = new ScopeEvent(name, scope, data); + if (scopeStreams != null && scopeStreams._typeCounts.containsKey(name)) { + var queue = new Queue()..addFirst(scopeStreams._scope); + while (queue.isNotEmpty) { + scope = queue.removeFirst(); + scopeStreams = scope._streams; + assert(scopeStreams._scope == scope); + if(scopeStreams._streams.containsKey(name)) { + var stream = scopeStreams._streams[name]; + event._currentScope = scope; + stream._fire(event); + } + // Reverse traversal so that when the queue is read it is correct order. + var childScope = scope._childTail; + while(childScope != null) { + scopeStreams = childScope._streams; + if (scopeStreams != null && + scopeStreams._typeCounts.containsKey(name)) { + queue.addFirst(scopeStreams._scope); + } + childScope = childScope._prev; } } - } while ((scope = _digestComputeNextScope(scope)) != null); - return null; + } + return event; } - - _Watch _digestHandleDirty(Scope scope, _Watch watch, last, value, List log) { - _Watch lastDirtyWatch; - while (true) { - if (!_identical(value, last)) { - lastDirtyWatch = watch; - if (log != null) log.add(watch.exp == null ? '[unknown]' : watch.exp); - watch.last = value; - var fireTimer; - assert((fireTimer = _perf.startTimer('ng.fire', watch.exp)) != false); - watch.fn(value, identical(_initWatchVal, last) ? value : last, scope); - assert(_perf.stopTimer(fireTimer) != false); - } - watch = watch.next; - while (watch == null) { - scope = _digestComputeNextScope(scope); - if (scope == null) return lastDirtyWatch; - watch = scope._watchers.head; + static ScopeStream on(Scope scope, + ExceptionHandler _exceptionHandler, + String name) { + var scopeStream = scope._streams; + if (scopeStream == null || scopeStream._scope != scope) { + // We either don't have [_ScopeStreams] or it is inherited. + var newStreams = new _Streams(scope, _exceptionHandler, scopeStream); + var scopeCursor = scope; + while (scopeCursor != null && scopeCursor._streams == scopeStream) { + scopeCursor._streams = newStreams; + scopeCursor = scopeCursor._parentScope; } - last = watch.last; - value = watch.get(scope, scope._filters); + scopeStream = newStreams; } + return scopeStream._get(scope, name); } - - Scope _digestComputeNextScope(Scope scope) { - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $broadcast - Scope target = this; - Scope childHead = scope._childHead; - while (childHead != null && childHead._disabled) { - childHead = childHead._nextSibling; + static void destroy(Scope scope) { + var toBeDeletedStreams = scope._streams; + if (toBeDeletedStreams == null) return; + scope = scope._parentScope; // skip current state as not to delete listeners + while (scope != null && scope._streams == toBeDeletedStreams) { + scope._streams = null; + scope = scope._parentScope; } - if (childHead == null) { - if (scope == target) { - return null; - } else { - Scope next = scope._nextSibling; - if (next == null) { - while (scope != target && (next = scope._nextSibling) == null) { - scope = scope.$parent; - } + if (scope == null) return; + var parentStreams = scope._streams; + assert(parentStreams != toBeDeletedStreams); + toBeDeletedStreams._typeCounts.forEach( + (name, count) => parentStreams._addCount(name, -count)); + } + + async.Stream _get(Scope scope, String name) { + assert(scope._streams == this); + assert(scope._streams._scope == scope); + assert(_exceptionHandler != null); + return _streams.putIfAbsent(name, () => + new ScopeStream(this, _exceptionHandler, name)); + } + + void _addCount(String name, int amount) { + // decrement the counters on all parent scopes + _Streams lastStreams = null; + var scope = _scope; + while (scope != null) { + if (lastStreams != scope._streams) { + // we have a transition, need to decrement it + lastStreams = scope._streams; + int count = lastStreams._typeCounts[name]; + count = count == null ? amount : count + amount; + assert(count >= 0); + if (count == 0) { + lastStreams._typeCounts.remove(name); + } else { + lastStreams._typeCounts[name] = count; } - return next; } - } else { - if (childHead._lazy) childHead._disabled = true; - return childHead; + scope = scope._parentScope; } } +} +class ScopeStream extends async.Stream { + final ExceptionHandler _exceptionHandler; + final _Streams _streams; + final String _name; + final subscriptions = []; - void _digestComputePerfCounters() { - int watcherCount = 0, scopeCount = 0; - Scope scope = this; - do { - scopeCount++; - watcherCount += scope._watchers.length; - } while ((scope = _digestComputeNextScope(scope)) != null); - _digestUpdatePerfCounters(watcherCount, scopeCount); - } - - - void _digestUpdatePerfCounters(int watcherCount, int scopeCount) { - _perf.counters['ng.scope.watchers'] = watcherCount; - _perf.counters['ng.scopes'] = scopeCount; - } - - - /** - * Removes the current scope (and all of its children) from the parent scope. Removal implies - * that calls to $digest() will no longer propagate to the current scope and its children. - * Removal also implies that the current scope is eligible for garbage collection. - * - * The `$destroy()` operation is usually used within directives that perform transclusion on - * multiple child elements (like ngRepeat) which create multiple child scopes. - * - * Just before a scope is destroyed, a `$destroy` event is broadcasted on this scope. This is - * a great way for child scopes (such as shared directives or controllers) to detect to and - * perform any necessary cleanup before the scope is removed from the application. - * - * Note that, in AngularDart, there is also a `$destroy` jQuery DOM event, which can be used to - * clean up DOM bindings before an element is removed from the DOM. - */ - $destroy() { - if ($root == this) return; // we can't remove the root node; - - $broadcast(r'$destroy'); + ScopeStream(this._streams, this._exceptionHandler, this._name); - if ($parent._childHead == this) $parent._childHead = _nextSibling; - if ($parent._childTail == this) $parent._childTail = _prevSibling; - if (_prevSibling != null) _prevSibling._nextSibling = _nextSibling; - if (_nextSibling != null) _nextSibling._prevSibling = _prevSibling; + ScopeStreamSubscription listen(void onData(ScopeEvent event), + { Function onError, + void onDone(), + bool cancelOnError }) { + if (subscriptions.isEmpty) _streams._addCount(_name, 1); + var subscription = new ScopeStreamSubscription(this, onData); + subscriptions.add(subscription); + return subscription; } - - /** - * Evaluates the expression against the current scope and returns the result. Note that, the - * expression data is relative to the data within the scope. Therefore an expression such as - * `a + b` will deference variables `a` and `b` and return a result so long as `a` and `b` - * exist on the scope. - * - * * [expr] - The expression that will be evaluated. This can be both a Function or a String. - * * [locals] - An optional Map of key/value data that will override any matching scope members - * for the purposes of the evaluation. - */ - $eval(expr, [locals]) { - return relaxFnArgs(_compileToFn(expr))(locals == null ? this : new ScopeLocals(this, locals), _filters); + void _fire(ScopeEvent event) { + for(ScopeStreamSubscription subscription in subscriptions) { + try { + subscription._onData(event); + } catch (e, s) { + _exceptionHandler(e, s); + } + } } - - /** - * Evaluates the expression against the current scope at a later point in time. The $evalAsync - * operation may not get run right away (depending if an existing digest cycle is going on) and - * may therefore be issued later on (by a follow-up digest cycle). Note that at least one digest - * cycle will be performed after the expression is evaluated. However, If triggering an additional - * digest cycle is not desired then this can be avoided by placing `{outsideDigest: true}` as - * the 2nd parameter to the function. - * - * * [expr] - The expression that will be evaluated. This can be both a Function or a String. - * * [outsideDigest] - Whether or not to trigger a follow-up digest after evaluation. - */ - $evalAsync(expr, {outsideDigest: false}) { - if (outsideDigest) { - _outerAsyncQueue.add(expr); + void _remove(ScopeStreamSubscription subscription) { + assert(subscription._scopeStream == this); + if (subscriptions.remove(subscription)) { + if (subscriptions.isEmpty) _streams._addCount(_name, -1); } else { - _innerAsyncQueue.add(expr); + throw new StateError('AlreadyCanceled'); } } +} +class ScopeStreamSubscription implements async.StreamSubscription { + final ScopeStream _scopeStream; + final Function _onData; + ScopeStreamSubscription(this._scopeStream, this._onData); + + // TODO(vbe) should return a Future + cancel() => _scopeStream._remove(this); + + void onData(void handleData(ScopeEvent data)) => NOT_IMPLEMENTED(); + void onError(Function handleError) => NOT_IMPLEMENTED(); + void onDone(void handleDone()) => NOT_IMPLEMENTED(); + void pause([async.Future resumeSignal]) => NOT_IMPLEMENTED(); + void resume() => NOT_IMPLEMENTED(); + bool get isPaused => NOT_IMPLEMENTED(); + async.Future asFuture([var futureValue]) => NOT_IMPLEMENTED(); +} - /** - * Skip running a $digest at the end of this turn. - * The primary use case is to skip the digest in the current VM turn because - * you just scheduled or are otherwise certain of an impending VM turn and the - * digest at the end of that turn is sufficient. You should be able to answer - * "No" to the question "Is there any other code that is aware that this VM - * turn occurred and therefore expected a digest?". If your answer is "Yes", - * then you run the risk that the very next VM turn is not for your event and - * now that other code runs in that turn and sees stale values. - * - * You might call this function, for instance, from an event listener where, - * though the event occurred, you need to wait for another event before you can - * perform something meaningful. You might schedule that other event, - * set a flag for the handler of the other event to recognize, etc. and then - * call this method to skip the digest this cycle. Note that you should call - * this function *after* you have successfully confirmed that the expected VM - * turn will occur (perhaps by scheduling it) to ensure that the digest - * actually does take place on that turn. - */ - $skipAutoDigest() { - _zone.assertInTurn(); - $root._skipAutoDigest = true; - } - +class _FunctionChain { + final Function fn; + _FunctionChain _next; - /** - * Triggers a digest operation much like [$digest] does, however, also accepts an - * optional expression to evaluate alongside the digest operation. The result of that - * expression will be returned afterwards. Much like with $digest, $apply should only be - * used within unit tests to simulate the life cycle of a scope. See [$digest] to learn - * more. - * - * * [expr] - optional expression which will be evaluated after the digest is performed. See [$eval] - * to learn more about expressions. - */ - $apply([expr]) { - return _zone.run(() { - var timerId; - try { - assert((timerId = _perf.startTimer('ng.\$apply', _source(expr))) != false); - return $eval(expr); - } catch (e, s) { - _exceptionHandler(e, s); - } finally { - assert(_perf.stopTimer(timerId) != false); - } - }); + _FunctionChain(fn()) + : fn = fn + { + assert(fn != null); } +} +class AstParser { + final Parser _parser; + int _id = 0; + ExpressionVisitor _visitor = new ExpressionVisitor(); - /** - * Registers a scope-based event listener to intercept events triggered by - * [$broadcast] (from any parent scopes) or [$emit] (from child scopes) that - * match the given event name. $on accepts two arguments: - * - * * [name] - Refers to the event name that the scope will listen on. - * * [listener] - Refers to the callback function which is executed when the event - * is intercepted. - * - * - * When the listener function is executed, an instance of [ScopeEvent] will be passed - * as the first parameter to the function. - * - * Any additional parameters available within the listener callback function are those that - * are set by the $broadcast or $emit scope methods (which are set by the origin scope which - * is the scope that first triggered the scope event). - */ - $on(name, listener) { - var namedListeners = _listeners[name]; - if (!_listeners.containsKey(name)) { - _listeners[name] = namedListeners = []; - } - namedListeners.add(listener); + AstParser(this._parser); - return () { - namedListeners.remove(listener); - }; + AST call(String exp, { FilterMap filters, + bool collection:false, + Object context:null }) { + _visitor.filters = filters; + AST contextRef = _visitor.contextRef; + try { + if (context != null) { + _visitor.contextRef = new ConstantAST(context, '#${_id++}'); + } + var ast = _parser(exp); + return collection ? _visitor.visitCollection(ast) : _visitor.visit(ast); + } finally { + _visitor.contextRef = contextRef; + _visitor.filters = null; + } } +} +class ExpressionVisitor implements Visitor { + static final ContextReferenceAST scopeContextRef = new ContextReferenceAST(); + AST contextRef = scopeContextRef; - /** - * Triggers a scope event referenced by the [name] parameters upwards towards the root of the - * scope tree. If intercepted, by a parent scope containing a matching scope event listener - * (which is registered via the [$on] scope method), then the event listener callback function - * will be executed. - * - * * [name] - The scope event name that will be triggered. - * * [args] - An optional list of arguments that will be fed into the listener callback function - * for any event listeners that are registered via [$on]. - */ - $emit(name, [List args]) { - var empty = [], - namedListeners, - scope = this, - event = new ScopeEvent(name, this), - listenerArgs = [event], - i; - - if (args != null) { - listenerArgs.addAll(args); - } + AST ast; + FilterMap filters; - do { - namedListeners = scope._listeners[name]; - if (namedListeners != null) { - event.currentScope = scope; - i = 0; - for (var length = namedListeners.length; i new CollectionAST(visit(exp)); + AST _mapToAst(Expression expression) => visit(expression); - /** - * Triggers a scope event referenced by the [name] parameters dowards towards the leaf nodes of the - * scope tree. If intercepted, by a child scope containing a matching scope event listener - * (which is registered via the [$on] scope method), then the event listener callback function - * will be executed. - * - * * [name] - The scope event name that will be triggered. - * * [listenerArgs] - An optional list of arguments that will be fed into the listener callback function - * for any event listeners that are registered via [$on]. - */ - $broadcast(String name, [List listenerArgs]) { - var target = this, - current = target, - next = target, - event = new ScopeEvent(name, this); - - //down while you can, then up and next sibling or up and next sibling until back at root - if (listenerArgs == null) { - listenerArgs = []; - } - listenerArgs.insert(0, event); - do { - current = next; - event.currentScope = current; - if (current._listeners.containsKey(name)) { - current._listeners[name].forEach((listener) { - try { - relaxFnApply(listener, listenerArgs); - } catch(e, s) { - _exceptionHandler(e, s); - } - }); - } + List _toAst(List expressions) => + expressions.map(_mapToAst).toList(); - // Insanity Warning: scope depth-first traversal - // yes, this code is a bit crazy, but it works and we have tests to prove it! - // this piece should be kept in sync with the traversal in $broadcast - if (current._childHead == null) { - if (current == target) { - next = null; - } else { - next = current._nextSibling; - if (next == null) { - while(current != target && (next = current._nextSibling) == null) { - current = current.$parent; - } - } - } - } else { - next = current._childHead; - } - } while ((current = next) != null); - - return event; + void visitCallScope(CallScope exp) { + ast = new MethodAST(contextRef, exp.name, _toAst(exp.arguments)); + } + void visitCallMember(CallMember exp) { + ast = new MethodAST(visit(exp.object), exp.name, _toAst(exp.arguments)); + } + visitAccessScope(AccessScope exp) { + ast = new FieldReadAST(contextRef, exp.name); + } + visitAccessMember(AccessMember exp) { + ast = new FieldReadAST(visit(exp.object), exp.name); + } + visitBinary(Binary exp) { + ast = new PureFunctionAST(exp.operation, + _operationToFunction(exp.operation), + [visit(exp.left), visit(exp.right)]); + } + void visitPrefix(Prefix exp) { + ast = new PureFunctionAST(exp.operation, + _operationToFunction(exp.operation), + [visit(exp.expression)]); + } + void visitConditional(Conditional exp) { + ast = new PureFunctionAST('?:', _operation_ternary, + [visit(exp.condition), visit(exp.yes), + visit(exp.no)]); + } + void visitAccessKeyed(AccessKeyed exp) { + ast = new PureFunctionAST('[]', _operation_bracket, + [visit(exp.object), visit(exp.key)]); + } + void visitLiteralPrimitive(LiteralPrimitive exp) { + ast = new ConstantAST(exp.value); + } + void visitLiteralString(LiteralString exp) { + ast = new ConstantAST(exp.value); + } + void visitLiteralArray(LiteralArray exp) { + List items = _toAst(exp.elements); + ast = new PureFunctionAST('[${items.join(', ')}]', new ArrayFn(), items); } - _beginPhase(phase) { - if ($root._phase != null) { - // TODO(deboer): Remove the []s when dartbug.com/11999 is fixed. - throw ['${$root._phase} already in progress']; + void visitLiteralObject(LiteralObject exp) { + List keys = exp.keys; + List values = _toAst(exp.values); + assert(keys.length == values.length); + var kv = []; + for(var i = 0; i < keys.length; i++) { + kv.add('${keys[i]}: ${values[i]}'); } - assert(_perf.startTimer('ng.phase.${phase}') != false); + ast = new PureFunctionAST('{${kv.join(', ')}}', new MapFn(keys), values); + } - $root._phase = phase; + void visitFilter(Filter exp) { + Function filterFunction = filters(exp.name); + List args = [visitCollection(exp.expression)]; + args.addAll(_toAst(exp.arguments).map((ast) => new CollectionAST(ast))); + ast = new PureFunctionAST('|${exp.name}', + new _FilterWrapper(filterFunction, args.length), args); } - _clearPhase() { - assert(_perf.stopTimer('ng.phase.${$root._phase}') != false); - $root._phase = null; + // TODO(misko): this is a corner case. Choosing not to implement for now. + void visitCallFunction(CallFunction exp) { + _notSupported("function's returing functions"); + } + void visitAssign(Assign exp) { + _notSupported('assignement'); + } + void visitLiteral(Literal exp) { + _notSupported('literal'); + } + void visitExpression(Expression exp) { + _notSupported('?'); + } + void visitChain(Chain exp) { + _notSupported(';'); } - Function _compileToFn(exp) { - if (exp == null) { - return () => null; - } else if (exp is String) { - Expression expression = _parser(exp); - return expression.eval; - } else if (exp is Function) { - return exp; - } else { - throw 'Expecting String or Function'; - } + void _notSupported(String name) { + throw new StateError("Can not watch expression containing '$name'."); } } -@proxy -class ScopeLocals implements Scope, Map { - static wrapper(dynamic scope, Map locals) => new ScopeLocals(scope, locals); - - dynamic _scope; - Map _locals; - - ScopeLocals(this._scope, this._locals); - - operator []=(String name, value) => _scope[name] = value; - operator [](String name) => (_locals.containsKey(name) ? _locals : _scope)[name]; - - noSuchMethod(Invocation invocation) => mirror.reflect(_scope).delegate(invocation); +Function _operationToFunction(String operation) { + switch(operation) { + case '!' : return _operation_negate; + case '+' : return _operation_add; + case '-' : return _operation_subtract; + case '*' : return _operation_multiply; + case '/' : return _operation_divide; + case '~/' : return _operation_divide_int; + case '%' : return _operation_remainder; + case '==' : return _operation_equals; + case '!=' : return _operation_not_equals; + case '<' : return _operation_less_then; + case '>' : return _operation_greater_then; + case '<=' : return _operation_less_or_equals_then; + case '>=' : return _operation_greater_or_equals_then; + case '^' : return _operation_power; + case '&' : return _operation_bitwise_and; + case '&&' : return _operation_logical_and; + case '||' : return _operation_logical_or; + default: throw new StateError(operation); + } } -class _InitWatchVal { const _InitWatchVal(); } -const _initWatchVal = const _InitWatchVal(); - -class _Watch { - final Function fn; - final Function get; - final String exp; - var last; - - _Watch previous; - _Watch next; - - _Watch(fn, this.last, getFn, this.exp) - : this.fn = relaxFnArgs3(fn) - , this.get = relaxFnArgs2(getFn); +_operation_negate(value) => !toBool(value); +_operation_add(left, right) => autoConvertAdd(left, right); +_operation_subtract(left, right) => left - right; +_operation_multiply(left, right) => left * right; +_operation_divide(left, right) => left / right; +_operation_divide_int(left, right) => left ~/ right; +_operation_remainder(left, right) => left % right; +_operation_equals(left, right) => left == right; +_operation_not_equals(left, right) => left != right; +_operation_less_then(left, right) => left < right; +_operation_greater_then(left, right) => (left == null || right == null) ? false : left > right; +_operation_less_or_equals_then(left, right) => left <= right; +_operation_greater_or_equals_then(left, right) => left >= right; +_operation_power(left, right) => left ^ right; +_operation_bitwise_and(left, right) => left & right; +// TODO(misko): these should short circuit the evaluation. +_operation_logical_and(left, right) => toBool(left) && toBool(right); +_operation_logical_or(left, right) => toBool(left) || toBool(right); + +_operation_ternary(condition, yes, no) => toBool(condition) ? yes : no; +_operation_bracket(obj, key) => obj == null ? null : obj[key]; + +class ArrayFn extends FunctionApply { + // TODO(misko): figure out why do we need to make a copy? + apply(List args) => new List.from(args); } -class _WatchList { - int length = 0; - _Watch head; - _Watch tail; - - void addLast(_Watch watch) { - assert(watch.previous == null); - assert(watch.next == null); - if (tail == null) { - tail = head = watch; - } else { - watch.previous = tail; - tail.next = watch; - tail = watch; - } - length++; - } +class MapFn extends FunctionApply { + final List keys; - void remove(_Watch watch) { - if (watch == head) { - _Watch next = watch.next; - if (next == null) tail = null; - else next.previous = null; - head = next; - } else if (watch == tail) { - _Watch previous = watch.previous; - previous.next = null; - tail = previous; - } else { - _Watch next = watch.next; - _Watch previous = watch.previous; - previous.next = next; - next.previous = previous; - } - length--; - } -} + MapFn(this.keys); -_toJson(obj) { - try { - return JSON.encode(obj); - } catch(e) { - var ret = "NOT-JSONABLE"; - // Keep prod fast. - assert(() { - var mirror = reflect(obj); - if (mirror is ClosureMirror) { - // work-around dartbug.com/14130 - try { - ret = mirror.function.source; - } on NoSuchMethodError catch (e) { - } on UnimplementedError catch (e) { - } - } - return true; - }); - return ret; + apply(List values) { + // TODO(misko): figure out why do we need to make a copy instead of reusing instance? + assert(values.length == keys.length); + return new Map.fromIterables(keys, values); } } -String _source(obj) { - if (obj is Function) { - var m = reflect(obj); - if (m is ClosureMirror) { - // work-around dartbug.com/14130 - try { - return "FN: ${m.function.source}"; - } on NoSuchMethodError catch (e) { - } on UnimplementedError catch (e) { +class _FilterWrapper extends FunctionApply { + final Function filterFn; + final List args; + final List argsWatches; + _FilterWrapper(this.filterFn, length): + args = new List(length), + argsWatches = new List(length); + + apply(List values) { + for(var i=0; i < values.length; i++) { + var value = values[i]; + var lastValue = args[i]; + if (!identical(value, lastValue)) { + if (value is CollectionChangeRecord) { + args[i] = (value as CollectionChangeRecord).iterable; + } else { + args[i] = value; + } } } + var value = Function.apply(filterFn, args); + if (value is Iterable) { + // Since filters are pure we can guarantee that this well never change. + // By wrapping in UnmodifiableListView we can hint to the dirty checker + // and short circuit the iterator. + value = new UnmodifiableListView(value); + } + return value; } - return '$obj'; } diff --git a/lib/core/zone.dart b/lib/core/zone.dart index 346724b3d..600b775aa 100644 --- a/lib/core/zone.dart +++ b/lib/core/zone.dart @@ -29,8 +29,13 @@ class LongStackTrace { * A better zone API which implements onTurnDone. */ class NgZone { - NgZone() { - _zone = async.Zone.current.fork(specification: new async.ZoneSpecification( + final async.Zone _outerZone; + async.Zone _zone; + + NgZone() + : _outerZone = async.Zone.current + { + _zone = _outerZone.fork(specification: new async.ZoneSpecification( run: _onRun, runUnary: _onRunUnary, scheduleMicrotask: _onScheduleMicrotask, @@ -38,7 +43,6 @@ class NgZone { )); } - async.Zone _zone; List _asyncQueue = []; bool _errorThrownFromOnRun = false; @@ -82,7 +86,7 @@ class NgZone { _inFinishTurn = true; try { // Two loops here: the inner one runs all queued microtasks, - // the outer runs onTurnDone (e.g. scope.$digest) and then + // the outer runs onTurnDone (e.g. scope.digest) and then // any microtasks which may have been queued from onTurnDone. do { while (!_asyncQueue.isEmpty) { @@ -142,6 +146,22 @@ class NgZone { */ run(body()) => _zone.run(body); + /** + * Allows one to escape the auto-digest mechanism of Angular. + * + * myFunction(NgZone zone, Element element) { + * element.onClick.listen(() { + * // auto-digest will run after element click. + * }); + * zone.runOutsideAngular(() { + * element.onMouseMove.listen(() { + * // auto-digest will NOT run after mouse move + * }); + * }); + * } + */ + runOutsideAngular(body()) => _outerZone.run(body); + assertInTurn() { assert(_runningInTurn > 0 || _inFinishTurn); } diff --git a/lib/core_dom/block.dart b/lib/core_dom/block.dart index c3241259b..3f2e15dab 100644 --- a/lib/core_dom/block.dart +++ b/lib/core_dom/block.dart @@ -36,8 +36,9 @@ class Block implements ElementWrapper { Function onMove; List _directives = []; + final NgAnimate _animate; - Block(this.elements); + Block(this.elements, this._animate); Block insertAfter(ElementWrapper previousBlock) { // Update Link List. @@ -55,8 +56,9 @@ class Block implements ElementWrapper { dom.Node parentElement = previousElement.parentNode; bool preventDefault = false; - Function insertDomElements = () => - elements.forEach((el) => parentElement.insertBefore(el, insertBeforeElement)); + Function insertDomElements = () { + _animate.insert(elements, parentElement, insertBefore: insertBeforeElement); + }; if (onInsert != null) { onInsert({ @@ -78,22 +80,15 @@ class Block implements ElementWrapper { bool preventDefault = false; Function removeDomElements = () { - for(var j = 0, jj = elements.length; j < jj; j++) { - dom.Node current = elements[j]; - dom.Node next = j+1 < jj ? elements[j+1] : null; - - while(next != null && current.nextNode != next) { - current.nextNode.remove(); - } - elements[j].remove(); - } + _animate.remove(elements); }; if (onRemove != null) { onRemove({ "preventDefault": () { preventDefault = true; - return removeDomElements(); + removeDomElements(); + return this; }, "element": elements[0] }); @@ -116,7 +111,7 @@ class Block implements ElementWrapper { previousElement = previousElements[previousElements.length - 1], insertBeforeElement = previousElement.nextNode, parentElement = previousElement.parentNode; - + elements.forEach((el) => parentElement.insertBefore(el, insertBeforeElement)); // Remove block from list diff --git a/lib/core_dom/block_factory.dart b/lib/core_dom/block_factory.dart index 8856e37ca..83f468747 100644 --- a/lib/core_dom/block_factory.dart +++ b/lib/core_dom/block_factory.dart @@ -43,7 +43,7 @@ class BlockFactory { var timerId; try { assert((timerId = _perf.startTimer('ng.block')) != false); - var block = new Block(elements); + var block = new Block(elements, injector.get(NgAnimate)); _link(block, elements, directivePositions, injector); return block; } finally { @@ -99,6 +99,7 @@ class BlockFactory { assert((timerId = _perf.startTimer('ng.block.link.setUp', _html(node))) != false); Injector nodeInjector; Scope scope = parentInjector.get(Scope); + FilterMap filters = parentInjector.get(FilterMap); Map fctrs; var nodeAttrs = node is dom.Element ? new NodeAttrs(node) : null; ElementProbe probe; @@ -119,7 +120,7 @@ class BlockFactory { NgAnnotation annotation = ref.annotation; var visibility = _elementOnly; if (ref.annotation is NgController) { - scope = scope.$new(); + scope = scope.createChild({}); nodeModule.value(Scope, scope); } if (ref.annotation.visibility == NgDirective.CHILDREN_VISIBILITY) { @@ -131,7 +132,7 @@ class BlockFactory { nodeModule.factory(NgTextMustacheDirective, (Injector injector) { return new NgTextMustacheDirective( node, ref.value, injector.get(Interpolate), injector.get(Scope), - injector.get(TextChangeListener)); + injector.get(AstParser), injector.get(FilterMap)); }); } else if (ref.type == NgAttrMustacheDirective) { if (nodesAttrsDirectives == null) { @@ -140,7 +141,8 @@ class BlockFactory { var scope = injector.get(Scope); var interpolate = injector.get(Interpolate); for(var ref in nodesAttrsDirectives) { - new NgAttrMustacheDirective(nodeAttrs, ref.value, interpolate, scope); + new NgAttrMustacheDirective(nodeAttrs, ref.value, interpolate, + scope, injector.get(AstParser), injector.get(FilterMap)); } }); } @@ -193,23 +195,46 @@ class BlockFactory { assert((linkMapTimer = _perf.startTimer('ng.block.link.map', ref.type)) != false); var shadowScope = (fctrs != null && fctrs.containsKey(ref.type)) ? fctrs[ref.type].shadowScope : null; if (ref.annotation is NgController) { - scope[(ref.annotation as NgController).publishAs] = controller; + scope.context[(ref.annotation as NgController).publishAs] = controller; } else if (ref.annotation is NgComponent) { - shadowScope[(ref.annotation as NgComponent).publishAs] = controller; + shadowScope.context[(ref.annotation as NgComponent).publishAs] = controller; } if (nodeAttrs == null) nodeAttrs = new _AnchorAttrs(ref); + var attachDelayStatus = controller is NgAttachAware ? [false] : null; + checkAttachReady() { + if (attachDelayStatus.reduce((a, b) => a && b)) { + attachDelayStatus = null; + controller.attach(); + } + } for(var map in ref.mappings) { - map(nodeAttrs, scope, controller); + var notify; + if (attachDelayStatus != null) { + var index = attachDelayStatus.length; + attachDelayStatus.add(false); + notify = () { + if (attachDelayStatus != null) { + attachDelayStatus[index] = true; + checkAttachReady(); + } + }; + } else { + notify = () => null; + } + map(nodeAttrs, scope, controller, filters, notify); } - if (controller is NgAttachAware) { - var removeWatcher; - removeWatcher = scope.$watch(() { - removeWatcher(); - controller.attach(); - }); + if (attachDelayStatus != null) { + Watch watch; + watch = scope.watch( + '1', // Cheat a bit. + (_, __) { + watch.remove(); + attachDelayStatus[0] = true; + checkAttachReady(); + }); } if (controller is NgDetachAware) { - scope.$on(r'$destroy', controller.detach); + scope.on(ScopeEvent.DESTROY).listen((_) => controller.detach()); } assert(_perf.stopTimer(linkMapTimer) != false); } finally { @@ -302,7 +327,7 @@ class _ComponentFactory { shadowDom.applyAuthorStyles = component.applyAuthorStyles; shadowDom.resetStyleInheritance = component.resetStyleInheritance; - shadowScope = scope.$new(isolate: true); + shadowScope = scope.createChild({}); // Isolate // TODO(pavelgj): fetching CSS with Http is mainly an attempt to // work around an unfiled Chrome bug when reloading same CSS breaks // styles all over the page. We shouldn't be doing browsers work, diff --git a/lib/core_dom/common.dart b/lib/core_dom/common.dart index 012023c00..47e004d0f 100644 --- a/lib/core_dom/common.dart +++ b/lib/core_dom/common.dart @@ -4,7 +4,8 @@ List cloneElements(elements) { return elements.map((el) => el.clone(true)).toList(); } -typedef ApplyMapping(NodeAttrs attrs, Scope scope, Object dst); +typedef ApplyMapping(NodeAttrs attrs, Scope scope, Object dst, + FilterMap filters, notify()); class DirectiveRef { final dom.Node element; @@ -28,9 +29,10 @@ class DirectiveRef { * services from the provided modules. */ Injector forceNewDirectivesAndFilters(Injector injector, List modules) { - modules.add(new Module() - ..factory(Scope, - (i) => i.parent.get(Scope).$new(filters: i.get(FilterMap)))); + modules.add(new Module()..factory(Scope, (i) { + var scope = i.parent.get(Scope); + return scope.createChild(new PrototypeMap(scope.context)); + })); return injector.createChild(modules, forceNewInstances: [DirectiveMap, FilterMap]); } diff --git a/lib/core_dom/compiler.dart b/lib/core_dom/compiler.dart index 081bbfa92..961fa6dfd 100644 --- a/lib/core_dom/compiler.dart +++ b/lib/core_dom/compiler.dart @@ -139,68 +139,91 @@ class Compiler { var mode = match[1]; var dstPath = match[2]; - Expression dstPathFn = _parser(dstPath.isEmpty ? attrName : dstPath); + String dstExpression = dstPath.isEmpty ? attrName : dstPath; + Expression dstPathFn = _parser(dstExpression); if (!dstPathFn.isAssignable) { throw "Expression '$dstPath' is not assignable in mapping '$mapping' for attribute '$attrName'."; } ApplyMapping mappingFn; switch (mode) { case '@': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst) { - attrs.observe(attrName, (value) => dstPathFn.assign(dst, value)); + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, FilterMap filters, notify()) { + attrs.observe(attrName, (value) { + dstPathFn.assign(controller, value); + notify(); + }); }; break; case '<=>': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst) { - if (attrs[attrName] == null) return; - Expression attrExprFn = _parser(attrs[attrName]); - var shadowValue = null; - scope.$watch( - () => attrExprFn.eval(scope), - (v) => dstPathFn.assign(dst, shadowValue = v), - attrs[attrName]); - if (attrExprFn.isAssignable) { - scope.$watch( - () => dstPathFn.eval(dst), - (v) { - if (shadowValue != v) { - shadowValue = v; - attrExprFn.assign(scope, v); + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, FilterMap filters, notify()) { + if (attrs[attrName] == null) return notify(); + String expression = attrs[attrName]; + Expression expressionFn = _parser(expression); + var blockOutbound = false; + var blockInbound = false; + scope.watch( + expression, + (inboundValue, _) { + if (!blockInbound) { + blockOutbound = true; + scope.rootScope.runAsync(() => blockOutbound = false); + var value = dstPathFn.assign(controller, inboundValue); + notify(); + return value; + } + }, + filters: filters + ); + if (expressionFn.isAssignable) { + scope.watch( + dstExpression, + (outboundValue, _) { + if(!blockOutbound) { + blockInbound = true; + scope.rootScope.runAsync(() => blockInbound = false); + expressionFn.assign(scope.context, outboundValue); + notify(); } }, - dstPath); + context: controller, + filters: filters + ); } }; break; case '=>': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst) { - if (attrs[attrName] == null) return; + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, FilterMap filters, notify()) { + if (attrs[attrName] == null) return notify(); Expression attrExprFn = _parser(attrs[attrName]); var shadowValue = null; - scope.$watch( - () => attrExprFn.eval(scope), - (v) => dstPathFn.assign(dst, shadowValue = v), - attrs[attrName]); + scope.watch(attrs[attrName], + (v, _) { + dstPathFn.assign(controller, shadowValue = v); + notify(); + }, + filters: filters); }; break; case '=>!': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst) { - if (attrs[attrName] == null) return; + mappingFn = (NodeAttrs attrs, Scope scope, Object controller, FilterMap filters, notify()) { + if (attrs[attrName] == null) return notify(); Expression attrExprFn = _parser(attrs[attrName]); - var stopWatching; - stopWatching = scope.$watch( - () => attrExprFn.eval(scope), - (value) { - if (dstPathFn.assign(dst, value) != null) { - stopWatching(); + var watch; + watch = scope.watch( + attrs[attrName], + (value, _) { + if (dstPathFn.assign(controller, value) != null) { + watch.remove(); } }, - attrs[attrName]); + filters: filters); + notify(); }; break; case '&': - mappingFn = (NodeAttrs attrs, Scope scope, Object dst) { - dstPathFn.assign(dst, _parser(attrs[attrName]).bind(scope, ScopeLocals.wrapper)); + mappingFn = (NodeAttrs attrs, Scope scope, Object dst, FilterMap filters, notify()) { + dstPathFn.assign(dst, _parser(attrs[attrName]).bind(scope.context, ScopeLocals.wrapper)); + notify(); }; break; } diff --git a/lib/core_dom/directive.dart b/lib/core_dom/directive.dart index 647f954ea..1a8bfada6 100644 --- a/lib/core_dom/directive.dart +++ b/lib/core_dom/directive.dart @@ -5,13 +5,6 @@ part of angular.core.dom; */ typedef AttributeChanged(String newValue); -/** - * Callback function used to notify of text changes. - */ -abstract class TextChangeListener{ - call(String text); -} - /** * NodeAttrs is a facade for element attributes. The facade is responsible * for normalizing attribute names as well as allowing access to the @@ -25,14 +18,13 @@ class NodeAttrs { NodeAttrs(this.element); operator [](String attributeName) => - element.attributes[snakecase(attributeName, '-')]; + element.attributes[attributeName]; operator []=(String attributeName, String value) { - var snakeName = snakecase(attributeName, '-'); if (value == null) { - element.attributes.remove(snakeName); + element.attributes.remove(attributeName); } else { - element.attributes[snakeName] = value; + element.attributes[attributeName] = value; } if (_observers != null && _observers.containsKey(attributeName)) { _observers[attributeName].forEach((fn) => fn(value)); @@ -56,14 +48,14 @@ class NodeAttrs { } void forEach(void f(String k, String v)) { - element.attributes.forEach((k, v) => f(camelcase(k), v)); + element.attributes.forEach(f); } bool containsKey(String attributeName) => - element.attributes.containsKey(snakecase(attributeName, '-')); + element.attributes.containsKey(attributeName); Iterable get keys => - element.attributes.keys.map((name) => camelcase(name)); + element.attributes.keys; } /** diff --git a/lib/core_dom/http.dart b/lib/core_dom/http.dart index 6adeb2ff4..15f44dbc8 100644 --- a/lib/core_dom/http.dart +++ b/lib/core_dom/http.dart @@ -22,21 +22,11 @@ class HttpBackend { async.Future request(String url, {String method, bool withCredentials, String responseType, String mimeType, Map requestHeaders, sendData, - void onProgress(dom.ProgressEvent e)}) { - // Complete inside a then to work-around dartbug.com/13051 - var c = new async.Completer(); - - dom.HttpRequest.request(url, - method: method, - withCredentials: withCredentials, - responseType: responseType, - mimeType: mimeType, - requestHeaders: requestHeaders, - sendData: sendData, - onProgress: onProgress).then((x) => c.complete(x), - onError: (e, stackTrace) => c.completeError(e, stackTrace)); - return c.future; - } + void onProgress(dom.ProgressEvent e)}) => + dom.HttpRequest.request(url, method: method, + withCredentials: withCredentials, responseType: responseType, + mimeType: mimeType, requestHeaders: requestHeaders, + sendData: sendData, onProgress: onProgress); } @NgInjectableService() @@ -62,9 +52,8 @@ class HttpInterceptor { /** * All parameters are optional. */ - HttpInterceptor({ - this.request, this.response, - this.requestError, this.responseError}); + HttpInterceptor({this.request, this.response, this.requestError, + this.responseError}); } @@ -79,7 +68,8 @@ class HttpInterceptor { */ class DefaultTransformDataHttpInterceptor implements HttpInterceptor { Function request = (HttpResponseConfig config) { - if (config.data != null && config.data is! String && config.data is! dom.File) { + if (config.data != null && config.data is! String && + config.data is! dom.File) { config.data = JSON.encode(config.data); } return config; @@ -90,8 +80,7 @@ class DefaultTransformDataHttpInterceptor implements HttpInterceptor { static var _PROTECTION_PREFIX = new RegExp('^\\)\\]\\}\',?\\n'); Function response = (HttpResponse r) { if (r.data is String) { - var d = r.data; - d = d.replaceFirst(_PROTECTION_PREFIX, ''); + var d = r.data.replaceFirst(_PROTECTION_PREFIX, ''); if (d.contains(_JSON_START) && d.contains(_JSON_END)) { d = JSON.decode(d); } @@ -108,7 +97,8 @@ class DefaultTransformDataHttpInterceptor implements HttpInterceptor { */ @NgInjectableService() class HttpInterceptors { - List _interceptors = [new DefaultTransformDataHttpInterceptor()]; + List _interceptors = + [new DefaultTransformDataHttpInterceptor()]; add(HttpInterceptor x) => _interceptors.add(x); addAll(List x) => _interceptors.addAll(x); @@ -119,12 +109,13 @@ class HttpInterceptors { constructChain(List chain) { _interceptors.reversed.forEach((HttpInterceptor i) { // AngularJS has an optimization of not including null interceptors. - chain.insert(0, [ - i.request == null ? (x) => x : i.request, - i.requestError]); - chain.add([ - i.response == null ? (x) => x : i.response, - i.responseError]); + chain + ..insert(0, [ + i.request == null ? (x) => x : i.request, + i.requestError]) + ..add([ + i.response == null ? (x) => x : i.response, + i.responseError]); }); } @@ -136,7 +127,8 @@ class HttpInterceptors { } /** - * Creates a [HttpInterceptors] from a [List]. Does not include the default interceptors. + * Creates a [HttpInterceptors] from a [List]. Does not include the default + * interceptors. */ HttpInterceptors.of([List interceptors]) { _interceptors = interceptors; @@ -228,12 +220,10 @@ class HttpResponse { /** * The response's headers. Without parameters, this method will return the - * [Map] of headers. With [key] parameter, this method will return the specific - * header. + * [Map] of headers. With [key] parameter, this method will return the + * specific header. */ - headers([String key]) { - return key == null ? _headers : _headers[key]; - } + headers([String key]) => key == null ? _headers : _headers[key]; /** * Useful for debugging. @@ -246,20 +236,12 @@ class HttpResponse { */ @NgInjectableService() class HttpDefaultHeaders { - static String _defaultContentType = 'application/json;charset=utf-8'; - Map _headers = { - 'COMMON': { - 'Accept': 'application/json, text/plain, */*' - }, - 'POST' : { - 'Content-Type': _defaultContentType - }, - 'PUT' : { - 'Content-Type': _defaultContentType - }, - 'PATCH' : { - 'Content-Type': _defaultContentType - } + static var _defaultContentType = 'application/json;charset=utf-8'; + var _headers = { + 'COMMON': {'Accept': 'application/json, text/plain, */*'}, + 'POST' : {'Content-Type': _defaultContentType}, + 'PUT' : {'Content-Type': _defaultContentType }, + 'PATCH' : {'Content-Type': _defaultContentType} }; _applyHeaders(method, ucHeaders, headers) { @@ -288,9 +270,7 @@ class HttpDefaultHeaders { * Passing 'common' as [method] will return a Map that contains headers * common to all operations. */ - operator[](method) { - return _headers[method.toUpperCase()]; - } + operator[](method) => _headers[method.toUpperCase()]; } /** @@ -331,8 +311,8 @@ class HttpDefaults { } /** - * The [Http] service facilitates communication with the remote HTTP servers. It - * uses dart:html's [HttpRequest] and provides a number of features on top + * The [Http] service facilitates communication with the remote HTTP servers. + * It uses dart:html's [HttpRequest] and provides a number of features on top * of the core Dart library. * * For unit testing, applications should use the [MockHttpBackend] service. @@ -343,12 +323,12 @@ class HttpDefaults { * * http(method: 'GET', url: '/someUrl') * .then((HttpResponse response) { .. }, - * onError: (HttpRequest request) { .. }); + * onError: (HttpRequest request) { .. }); * * A response status code between 200 and 299 is considered a success status and - * will result in the 'then' being called. Note that if the response is a redirect, - * Dart's [HttpRequest] will transparently follow it, meaning that the error callback will not be - * called for such responses. + * will result in the 'then' being called. Note that if the response is a + * redirect, Dart's [HttpRequest] will transparently follow it, meaning that the + * error callback will not be called for such responses. * * # Shortcut methods * @@ -389,7 +369,7 @@ class HttpDefaults { */ @NgInjectableService() class Http { - Map> _pendingRequests = >{}; + var _pendingRequests = >{}; BrowserCookies _cookies; LocationWrapper _location; UrlRewriter _rewriter; @@ -404,31 +384,27 @@ class Http { /** * Constructor, useful for DI. */ - Http(this._cookies, this._location, this._rewriter, this._backend, this.defaults, this._interceptors); + Http(this._cookies, this._location, this._rewriter, this._backend, + this.defaults, this._interceptors); /** * DEPRECATED */ - async.Future getString(String url, - {bool withCredentials, void onProgress(dom.ProgressEvent e), Cache cache}) { - return request(url, - withCredentials: withCredentials, - onProgress: onProgress, - cache: cache).then((HttpResponse xhr) => xhr.responseText); - } + async.Future getString(String url, {bool withCredentials, + void onProgress(dom.ProgressEvent e), Cache cache}) => + request(url, + withCredentials: withCredentials, + onProgress: onProgress, + cache: cache).then((HttpResponse xhr) => xhr.responseText); /** - * Parse a request URL and determine whether this is a same-origin request as the application document. - * - * @param {string|Uri} requestUrl The url of the request as a string that will be resolved - * or a parsed URL object. - * @returns {boolean} Whether the request is for the same origin as the application document. + * Parse a [requestUrl] and determine whether this is a same-origin request as + * the application document. */ - _urlIsSameOrigin(String requestUrl) { + bool _urlIsSameOrigin(String requestUrl) { Uri originUrl = Uri.parse(_location.location.toString()); Uri parsed = originUrl.resolve(requestUrl); - return (parsed.scheme == originUrl.scheme && - parsed.host == originUrl.host); + return (parsed.scheme == originUrl.scheme && parsed.host == originUrl.host); } /** @@ -468,39 +444,39 @@ class Http { method = method.toUpperCase(); - if (headers == null) { headers = {}; } + if (headers == null) headers = {}; defaults.headers.setHeaders(headers, method); var xsrfValue = _urlIsSameOrigin(url) ? - _cookies[xsrfCookieName != null ? xsrfCookieName : defaults.xsrfCookieName] : null; + _cookies[xsrfCookieName != null ? xsrfCookieName : defaults.xsrfCookieName] : + null; if (xsrfValue != null) { - headers[xsrfHeaderName != null ? xsrfHeaderName : defaults.xsrfHeaderName] = xsrfValue; + headers[xsrfHeaderName != null ? xsrfHeaderName : defaults.xsrfHeaderName] + = xsrfValue; } // Check for functions in headers - headers.forEach((k,v) { - if (v is Function) { - headers[k] = v(); - } + headers.forEach((k, v) { + if (v is Function) headers[k] = v(); }); var serverRequest = (HttpResponseConfig config) { - assert(config.data == null || config.data is String || config.data is dom.File); + assert(config.data == null || config.data is String || + config.data is dom.File); // Strip content-type if data is undefined if (config.data == null) { new List.from(headers.keys) - .where((h) => h.toUpperCase() == 'CONTENT-TYPE') - .forEach((h) => headers.remove(h)); + .where((h) => h.toUpperCase() == 'CONTENT-TYPE') + .forEach((h) => headers.remove(h)); } - return request( - null, - config: config, - method: method, - sendData: config.data, - requestHeaders: config.headers, - cache: cache); + return request(null, + config: config, + method: method, + sendData: config.data, + requestHeaders: config.headers, + cache: cache); }; var chain = [[serverRequest, null]]; @@ -541,9 +517,9 @@ class Http { interceptors, cache, timeout - }) => call(method: 'GET', url: url, data: data, params: params, headers: headers, - xsrfHeaderName: xsrfHeaderName, xsrfCookieName: xsrfCookieName, - interceptors: interceptors, + }) => call(method: 'GET', url: url, data: data, params: params, + headers: headers, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, timeout: timeout); /** @@ -559,9 +535,9 @@ class Http { interceptors, cache, timeout - }) => call(method: 'DELETE', url: url, data: data, params: params, headers: headers, - xsrfHeaderName: xsrfHeaderName, xsrfCookieName: xsrfCookieName, - interceptors: interceptors, + }) => call(method: 'DELETE', url: url, data: data, params: params, + headers: headers, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, timeout: timeout); /** @@ -577,9 +553,9 @@ class Http { interceptors, cache, timeout - }) => call(method: 'HEAD', url: url, data: data, params: params, headers: headers, - xsrfHeaderName: xsrfHeaderName, xsrfCookieName: xsrfCookieName, - interceptors: interceptors, + }) => call(method: 'HEAD', url: url, data: data, params: params, + headers: headers, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, timeout: timeout); /** @@ -594,9 +570,9 @@ class Http { interceptors, cache, timeout - }) => call(method: 'PUT', url: url, data: data, params: params, headers: headers, - xsrfHeaderName: xsrfHeaderName, xsrfCookieName: xsrfCookieName, - interceptors: interceptors, + }) => call(method: 'PUT', url: url, data: data, params: params, + headers: headers, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, timeout: timeout); /** @@ -611,9 +587,9 @@ class Http { interceptors, cache, timeout - }) => call(method: 'POST', url: url, data: data, params: params, headers: headers, - xsrfHeaderName: xsrfHeaderName, xsrfCookieName: xsrfCookieName, - interceptors: interceptors, + }) => call(method: 'POST', url: url, data: data, params: params, + headers: headers, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, timeout: timeout); /** @@ -629,9 +605,9 @@ class Http { interceptors, cache, timeout - }) => call(method: 'JSONP', url: url, data: data, params: params, headers: headers, - xsrfHeaderName: xsrfHeaderName, xsrfCookieName: xsrfCookieName, - interceptors: interceptors, + }) => call(method: 'JSONP', url: url, data: data, params: params, + headers: headers, xsrfHeaderName: xsrfHeaderName, + xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, timeout: timeout); /** @@ -648,14 +624,10 @@ class Http { var i = line.indexOf(':'); if (i == -1) return; var key = line.substring(0, i).trim().toLowerCase(); - var val = line.substring(i + 1).trim(); - - if (key != '') { - if (parsed.containsKey(key)) { - parsed[key] += ', ' + val; - } else { - parsed[key] = val; - } + + if (key.isNotEmpty) { + var val = line.substring(i + 1).trim(); + parsed[key] = parsed.containsKey(key) ? "${parsed[key]}, $val" : val; } }); return parsed; @@ -666,7 +638,7 @@ class Http { * that the [Http] service is currently waiting for. */ Iterable > get pendingRequests => - _pendingRequests.values; + _pendingRequests.values; /** * DEPRECATED @@ -690,7 +662,7 @@ class Http { url = _buildUrl(config.url, config.params); } - if (cache is bool && cache == false) { + if (cache == false) { cache = null; } else if (cache == null) { cache = defaults.cache; @@ -699,9 +671,11 @@ class Http { if (cache != null && _pendingRequests.containsKey(url)) { return _pendingRequests[url]; } - var cachedValue = (cache != null && method == 'GET') ? cache.get(url) : null; - if (cachedValue != null) { - return new async.Future.value(new HttpResponse.copy(cachedValue)); + var cachedResponse = (cache != null && method == 'GET') + ? cache.get(url) + : null; + if (cachedResponse != null) { + return new async.Future.value(new HttpResponse.copy(cachedResponse)); } var result = _backend.request(url, @@ -715,19 +689,14 @@ class Http { // TODO: Uncomment after apps migrate off of this class. // assert(value.status >= 200 && value.status < 300); - var response = new HttpResponse( - value.status, value.responseText, parseHeaders(value), - config); + var response = new HttpResponse(value.status, value.responseText, + parseHeaders(value), config); - if (cache != null) { - cache.put(url, response); - } + if (cache != null) cache.put(url, response); _pendingRequests.remove(url); return response; }, onError: (error) { - if (error is! dom.ProgressEvent) { - throw error; - } + if (error is! dom.ProgressEvent) throw error; dom.ProgressEvent event = error; _pendingRequests.remove(url); dom.HttpRequest request = event.currentTarget; @@ -735,8 +704,7 @@ class Http { new HttpResponse(request.status, request.response, parseHeaders(request), config)); }); - _pendingRequests[url] = result; - return result; + return _pendingRequests[url] = result; } _buildUrl(String url, Map params) { @@ -749,21 +717,18 @@ class Http { if (value is! List) value = [value]; value.forEach((v) { - if (v is Map) { - v = JSON.encode(v); - } - parts.add(_encodeUriQuery(key) + '=' + - _encodeUriQuery("$v")); + if (v is Map) v = JSON.encode(v); + parts.add(_encodeUriQuery(key) + '=' + _encodeUriQuery("$v")); }); }); return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); } _encodeUriQuery(val, {bool pctEncodeSpaces: false}) => - Uri.encodeComponent(val) - .replaceAll('%40', '@') - .replaceAll('%3A', ':') - .replaceAll('%24', r'$') - .replaceAll('%2C', ',') - .replaceAll('%20', pctEncodeSpaces ? '%20' : '+'); + Uri.encodeComponent(val) + .replaceAll('%40', '@') + .replaceAll('%3A', ':') + .replaceAll('%24', r'$') + .replaceAll('%2C', ',') + .replaceAll('%20', pctEncodeSpaces ? '%20' : '+'); } diff --git a/lib/core_dom/module.dart b/lib/core_dom/module.dart index 71d66adc3..f360ba28f 100644 --- a/lib/core_dom/module.dart +++ b/lib/core_dom/module.dart @@ -8,6 +8,7 @@ import 'dart:mirrors'; import 'package:di/di.dart'; import 'package:perf_api/perf_api.dart'; +import 'package:angular/animate/module.dart'; import 'package:angular/core/module.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/utils.dart'; @@ -30,7 +31,6 @@ class NgCoreDomModule extends Module { NgCoreDomModule() { value(dom.Window, dom.window); - value(TextChangeListener, null); factory(TemplateCache, (_) => new TemplateCache(capacity: 0)); type(dom.NodeTreeSanitizer, implementedBy: NullTreeSanitizer); diff --git a/lib/core_dom/ng_mustache.dart b/lib/core_dom/ng_mustache.dart index 1dabaf917..cf7229b17 100644 --- a/lib/core_dom/ng_mustache.dart +++ b/lib/core_dom/ng_mustache.dart @@ -7,14 +7,16 @@ class NgTextMustacheDirective { String markup, Interpolate interpolate, Scope scope, - TextChangeListener listener) { + AstParser parser, + FilterMap filters) { Interpolation interpolation = interpolate(markup); - interpolation.setter = (text) { - element.text = text; - if (listener != null) listener.call(text); - }; - interpolation.setter(''); - scope.$watchSet(interpolation.watchExpressions, interpolation.call, markup.trim()); + interpolation.setter = (text) => element.text = text; + + List items = interpolation.expressions.map((exp) { + return parser(exp, filters:filters); + }).toList(); + AST ast = new PureFunctionAST('[[$markup]]', new ArrayFn(), items); + scope.watch(ast, interpolation.call, readOnly: true); } } @@ -22,14 +24,39 @@ class NgTextMustacheDirective { @NgDirective(selector: r'[*=/{{.*}}/]') class NgAttrMustacheDirective { // This Directive is special and does not go through injection. - NgAttrMustacheDirective(NodeAttrs attrs, String markup, Interpolate interpolate, Scope scope) { + NgAttrMustacheDirective(NodeAttrs attrs, + String markup, + Interpolate interpolate, + Scope scope, + AstParser parser, + FilterMap filters) { var eqPos = markup.indexOf('='); var attrName = markup.substring(0, eqPos); var attrValue = markup.substring(eqPos + 1); Interpolation interpolation = interpolate(attrValue); - interpolation.setter = (text) => attrs[attrName] = text; + var lastValue = markup; + interpolation.setter = (text) { + if (lastValue != text) { + lastValue = attrs[attrName] = text; + } + }; + // TODO(misko): figure out how to remove call to setter. It slows down + // Block instantiation interpolation.setter(''); - scope.$watchSet(interpolation.watchExpressions, interpolation.call, markup.trim()); + + List items = interpolation.expressions.map((exp) { + return parser(exp, filters:filters); + }).toList(); + AST ast = new PureFunctionAST('[[$markup]]', new ArrayFn(), items); + /* + Attribute bindings are tricky. They need to be resolved on digest + inline with components so that any bindings to component can + be resolved before the component attach method. But once the + component is attached we need to run on the flush cycle rather + then digest cycle. + */ + // TODO(misko): figure out how to get most of these on observe rather then watch. + scope.watch(ast, interpolation.call); } } diff --git a/lib/core_dom/selector.dart b/lib/core_dom/selector.dart index 14579d9b6..83c69ac66 100644 --- a/lib/core_dom/selector.dart +++ b/lib/core_dom/selector.dart @@ -32,7 +32,7 @@ class _Directive { final Type type; final NgAnnotation annotation; - _Directive(Type this.type, NgAnnotation this.annotation); + _Directive(this.type, this.annotation); toString() => annotation.selector; } @@ -42,13 +42,15 @@ class _ContainsSelector { final NgAnnotation annotation; final RegExp regexp; - _ContainsSelector(this.annotation, String regexp) : regexp = new RegExp(regexp); + _ContainsSelector(this.annotation, String regexp) + : regexp = new RegExp(regexp); } -RegExp _SELECTOR_REGEXP = new RegExp(r'^(?:([\w\-]+)|(?:\.([\w\-]+))|(?:\[([\w\-\*]+)(?:=([^\]]*))?\]))'); -RegExp _COMMENT_COMPONENT_REGEXP = new RegExp(r'^\[([\w\-]+)(?:\=(.*))?\]$'); -RegExp _CONTAINS_REGEXP = new RegExp(r'^:contains\(\/(.+)\/\)$'); // -RegExp _ATTR_CONTAINS_REGEXP = new RegExp(r'^\[\*=\/(.+)\/\]$'); // +var _SELECTOR_REGEXP = new RegExp(r'^(?:([\w\-]+)|(?:\.([\w\-]+))|' + r'(?:\[([\w\-\*]+)(?:=([^\]]*))?\]))'); +var _COMMENT_COMPONENT_REGEXP = new RegExp(r'^\[([\w\-]+)(?:\=(.*))?\]$'); +var _CONTAINS_REGEXP = new RegExp(r'^:contains\(\/(.+)\/\)$'); // +var _ATTR_CONTAINS_REGEXP = new RegExp(r'^\[\*=\/(.+)\/\]$'); // class _SelectorPart { final String element; @@ -56,14 +58,14 @@ class _SelectorPart { final String attrName; final String attrValue; - const _SelectorPart.fromElement(String this.element) + const _SelectorPart.fromElement(this.element) : className = null, attrName = null, attrValue = null; - const _SelectorPart.fromClass(String this.className) + const _SelectorPart.fromClass(this.className) : element = null, attrName = null, attrValue = null; - const _SelectorPart.fromAttribute(String this.attrName, String this.attrValue) + const _SelectorPart.fromAttribute(this.attrName, this.attrValue) : element = null, className = null; toString() => @@ -76,16 +78,16 @@ class _SelectorPart { class _ElementSelector { - String name; + final String name; - Map> elementMap = new Map>(); - Map elementPartialMap = new Map(); + var elementMap = >{}; + var elementPartialMap = {}; - Map> classMap = new Map>(); - Map classPartialMap = new Map(); + var classMap = >{}; + var classPartialMap = {}; - Map>> attrValueMap = new Map>>(); - Map> attrValuePartialMap = new Map>(); + var attrValueMap = >>{}; + var attrValuePartialMap = >{}; _ElementSelector(this.name); @@ -96,8 +98,8 @@ class _ElementSelector { if ((name = selectorPart.element) != null) { if (terminal) { elementMap - .putIfAbsent(name, () => []) - .add(directive); + .putIfAbsent(name, () => []) + .add(directive); } else { elementPartialMap .putIfAbsent(name, () => new _ElementSelector(name)) @@ -106,8 +108,8 @@ class _ElementSelector { } else if ((name = selectorPart.className) != null) { if (terminal) { classMap - .putIfAbsent(name, () => []) - .add(directive); + .putIfAbsent(name, () => []) + .add(directive); } else { classPartialMap .putIfAbsent(name, () => new _ElementSelector(name)) @@ -116,13 +118,14 @@ class _ElementSelector { } else if ((name = selectorPart.attrName) != null) { if (terminal) { attrValueMap - .putIfAbsent(name, () => new Map>()) + .putIfAbsent(name, () => >{}) .putIfAbsent(selectorPart.attrValue, () => []) .add(directive); } else { attrValuePartialMap - .putIfAbsent(name, () => new Map()) - .putIfAbsent(selectorPart.attrValue, () => new _ElementSelector(name)) + .putIfAbsent(name, () => {}) + .putIfAbsent(selectorPart.attrValue, () => + new _ElementSelector(name)) .addDirective(selectorParts, directive); } } else { @@ -130,37 +133,47 @@ class _ElementSelector { } } - _addRefs(List refs, List<_Directive> directives, dom.Node node, [String attrValue]) { + _addRefs(List refs, List<_Directive> directives, dom.Node node, + [String attrValue]) { directives.forEach((directive) => - refs.add(new DirectiveRef(node, directive.type, directive.annotation, attrValue))); + refs.add(new DirectiveRef(node, directive.type, directive.annotation, + attrValue))); } - List<_ElementSelector> selectNode(List refs, List<_ElementSelector> partialSelection, - dom.Node node, String nodeName) { + List<_ElementSelector> selectNode(List refs, + List<_ElementSelector> partialSelection, + dom.Node node, String nodeName) { if (elementMap.containsKey(nodeName)) { _addRefs(refs, elementMap[nodeName], node); } if (elementPartialMap.containsKey(nodeName)) { - if (partialSelection == null) partialSelection = new List<_ElementSelector>(); + if (partialSelection == null) { + partialSelection = new List<_ElementSelector>(); + } partialSelection.add(elementPartialMap[nodeName]); } return partialSelection; } - List<_ElementSelector> selectClass(List refs, List<_ElementSelector> partialSelection, - dom.Node node, String className) { + List<_ElementSelector> selectClass(List refs, + List<_ElementSelector> partialSelection, + dom.Node node, String className) { if (classMap.containsKey(className)) { _addRefs(refs, classMap[className], node); } if (classPartialMap.containsKey(className)) { - if (partialSelection == null) partialSelection = new List<_ElementSelector>(); + if (partialSelection == null) { + partialSelection = new List<_ElementSelector>(); + } partialSelection.add(classPartialMap[className]); } return partialSelection; } - List<_ElementSelector> selectAttr(List refs, List<_ElementSelector> partialSelection, - dom.Node node, String attrName, String attrValue) { + List<_ElementSelector> selectAttr(List refs, + List<_ElementSelector> partialSelection, + dom.Node node, String attrName, + String attrValue) { String matchingKey = _matchingKey(attrValueMap.keys, attrName); @@ -174,30 +187,34 @@ class _ElementSelector { } } if (attrValuePartialMap.containsKey(attrName)) { - Map valuesPartialMap = attrValuePartialMap[attrName]; + Map valuesPartialMap = + attrValuePartialMap[attrName]; if (valuesPartialMap.containsKey('')) { - if (partialSelection == null) partialSelection = new List<_ElementSelector>(); + if (partialSelection == null) { + partialSelection = new List<_ElementSelector>(); + } partialSelection.add(valuesPartialMap['']); } if (attrValue != '' && valuesPartialMap.containsKey(attrValue)) { - if (partialSelection == null) partialSelection = new List<_ElementSelector>(); + if (partialSelection == null) { + partialSelection = new List<_ElementSelector>(); + } partialSelection.add(valuesPartialMap[attrValue]); } } return partialSelection; } - String _matchingKey(Iterable keys, String attrName) { - return keys.firstWhere( - (key) => new RegExp('^${key.replaceAll('*', r'[\w\-]+')}\$').hasMatch(attrName), - orElse: () => null); - } + String _matchingKey(Iterable keys, String attrName) => + keys.firstWhere((key) => + new RegExp('^${key.replaceAll('*', r'[\w\-]+')}\$') + .hasMatch(attrName), orElse: () => null); toString() => 'ElementSelector($name)'; } List<_SelectorPart> _splitCss(String selector, Type type) { - List<_SelectorPart> parts = []; + var parts = <_SelectorPart>[]; var remainder = selector; var match; while (!remainder.isEmpty) { @@ -226,9 +243,9 @@ List<_SelectorPart> _splitCss(String selector, Type type) { */ DirectiveSelector directiveSelectorFactory(DirectiveMap directives) { - _ElementSelector elementSelector = new _ElementSelector(''); - List<_ContainsSelector> attrSelector = []; - List<_ContainsSelector> textSelector = []; + var elementSelector = new _ElementSelector(''); + var attrSelector = <_ContainsSelector>[]; + var textSelector = <_ContainsSelector>[]; directives.forEach((NgAnnotation annotation, Type type) { var match; var selector = annotation.selector; @@ -242,17 +259,18 @@ DirectiveSelector directiveSelectorFactory(DirectiveMap directives) { } else if ((match = _ATTR_CONTAINS_REGEXP.firstMatch(selector)) != null) { attrSelector.add(new _ContainsSelector(annotation, match[1])); } else if ((selectorParts = _splitCss(selector, type)) != null){ - elementSelector.addDirective(selectorParts, new _Directive(type, annotation)); + elementSelector.addDirective(selectorParts, + new _Directive(type, annotation)); } else { throw new ArgumentError('Unsupported Selector: $selector'); } }); return (dom.Node node) { - List directiveRefs = []; - List<_ElementSelector> partialSelection = null; - Map classes = new Map(); - Map attrs = new Map(); + var directiveRefs = []; + List<_ElementSelector> partialSelection; + var classes = {}; + var attrs = {}; switch(node.nodeType) { case 1: // Element @@ -266,25 +284,27 @@ DirectiveSelector directiveSelectorFactory(DirectiveMap directives) { } // Select node - partialSelection = elementSelector.selectNode(directiveRefs, partialSelection, element, nodeName); + partialSelection = elementSelector.selectNode(directiveRefs, + partialSelection, element, nodeName); // Select .name if ((element.classes) != null) { for(var name in element.classes) { classes[name] = true; - partialSelection = elementSelector.selectClass(directiveRefs, partialSelection, element, name); + partialSelection = elementSelector.selectClass(directiveRefs, + partialSelection, element, name); } } // Select [attributes] - element.attributes.forEach((attrName, value){ + element.attributes.forEach((attrName, value) { attrs[attrName] = value; - for(var k = 0, kk = attrSelector.length; k < kk; k++) { + for(var k = 0; k < attrSelector.length; k++) { _ContainsSelector selectorRegExp = attrSelector[k]; if (selectorRegExp.regexp.hasMatch(value)) { // this directive is matched on any attribute name, and so - // we need to pass the name to the directive by prefixing it to the - // value. Yes it is a bit of a hack. + // we need to pass the name to the directive by prefixing it to + // the value. Yes it is a bit of a hack. directives[selectorRegExp.annotation].forEach((type) { directiveRefs.add(new DirectiveRef( node, type, selectorRegExp.annotation, '$attrName=$value')); @@ -292,7 +312,8 @@ DirectiveSelector directiveSelectorFactory(DirectiveMap directives) { } } - partialSelection = elementSelector.selectAttr(directiveRefs, partialSelection, node, attrName, value); + partialSelection = elementSelector.selectAttr(directiveRefs, + partialSelection, node, attrName, value); }); while(partialSelection != null) { @@ -300,21 +321,25 @@ DirectiveSelector directiveSelectorFactory(DirectiveMap directives) { partialSelection = null; elementSelectors.forEach((_ElementSelector elementSelector) { classes.forEach((className, _) { - partialSelection = elementSelector.selectClass(directiveRefs, partialSelection, node, className); + partialSelection = elementSelector.selectClass(directiveRefs, + partialSelection, node, className); }); attrs.forEach((attrName, value) { - partialSelection = elementSelector.selectAttr(directiveRefs, partialSelection, node, attrName, value); + partialSelection = elementSelector.selectAttr(directiveRefs, + partialSelection, node, attrName, value); }); }); } break; case 3: // Text Node - for(var value = node.nodeValue, k = 0, kk = textSelector.length; k < kk; k++) { - var selectorRegExp = textSelector[k]; + var value = node.nodeValue; + for(var k = 0; k < textSelector.length; k++) { + var selectorRegExp = textSelector[k]; if (selectorRegExp.regexp.hasMatch(value)) { directives[selectorRegExp.annotation].forEach((type) { - directiveRefs.add(new DirectiveRef(node, type, selectorRegExp.annotation, value)); + directiveRefs.add(new DirectiveRef(node, type, + selectorRegExp.annotation, value)); }); } } diff --git a/lib/directive/input_select.dart b/lib/directive/input_select.dart index 4127fec40..d74e1ef94 100644 --- a/lib/directive/input_select.dart +++ b/lib/directive/input_select.dart @@ -61,19 +61,31 @@ class InputSelectDirective implements NgAttachAware { }); _selectElement.onChange.listen((event) => _mode.onViewChange(event)); - _model.render = (value) => _mode.onModelChange(value); + _model.render = (value) { + // TODO(misko): this hack need to delay the rendering until after domRead + // because the modelChange reads from the DOM. We should be able to render + // without DOM changes. + _scope.rootScope.domRead(() { + _scope.rootScope.domWrite(() => _mode.onModelChange(value)); + }); + }; } /** * This method invalidates the current state of the selector and forces a - * re-rendering of the options using the [Scope.$evalAsync]. + * re-rendering of the options using the [Scope.evalAsync]. */ dirty() { if (!_dirty) { _dirty = true; - _scope.$evalAsync(() { - _dirty = false; - _mode.onModelChange(_model.viewValue); + // TODO(misko): this hack need to delay the rendering until after domRead + // becouse the modelChange reads from the DOM. We should be able to render + // without DOM changes. + _scope.rootScope.domRead(() { + _scope.rootScope.domWrite(() { + _dirty = false; + _mode.onModelChange(_model.viewValue); + }); }); } } @@ -85,29 +97,21 @@ class InputSelectDirective implements NgAttachAware { * */ @NgDirective( - selector: 'option', - publishTypes: const [TextChangeListener], - map: const {'ng-value': '&ngValue'}) -class OptionValueDirective implements TextChangeListener, NgAttachAware, + selector: 'option') +class OptionValueDirective implements NgAttachAware, NgDetachAware { final InputSelectDirective _inputSelectDirective; - final NodeAttrs _attrs; + final dom.Element _element; - BoundGetter _ngValue; + NgValue _ngValue; - OptionValueDirective(this._attrs, this._inputSelectDirective) { + OptionValueDirective(this._element, this._inputSelectDirective, this._ngValue) { if (_inputSelectDirective != null) { - _inputSelectDirective.expando[_attrs.element] = this; + _inputSelectDirective.expando[_element] = this; } } attach() { - if (_inputSelectDirective != null) { - this._attrs.observe('value', (_) => _inputSelectDirective.dirty()); - } - } - - call(String text) { if (_inputSelectDirective != null) { _inputSelectDirective.dirty(); } @@ -116,14 +120,11 @@ class OptionValueDirective implements TextChangeListener, NgAttachAware, detach() { if (_inputSelectDirective != null) { _inputSelectDirective.dirty(); - _inputSelectDirective.expando[_attrs.element] = null; + _inputSelectDirective.expando[_element] = null; } } - set ngValue(BoundGetter value) => _ngValue = value; - get ngValue => _attrs['ng-value'] is String ? - _ngValue() : - (_attrs.element as dom.OptionElement).value; + get ngValue => _ngValue.readValue(_element); } class _SelectMode { @@ -204,8 +205,8 @@ class _SingleSelectMode extends _SelectMode { class _MultipleSelectionMode extends _SelectMode { _MultipleSelectionMode(Expando expando, dom.SelectElement select, - NgModel model - ): super(expando, select, model); + NgModel model) + : super(expando, select, model); onViewChange(event) { var selected = []; diff --git a/lib/directive/module.dart b/lib/directive/module.dart index 0e6004392..fa4d9dba3 100644 --- a/lib/directive/module.dart +++ b/lib/directive/module.dart @@ -2,12 +2,14 @@ library angular.directive; import 'package:di/di.dart'; import 'dart:html' as dom; -import 'dart:async' as async; import 'package:intl/intl.dart'; +import 'package:angular/animate/module.dart'; import 'package:angular/core/module.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/core_dom/module.dart'; import 'package:angular/utils.dart'; +import 'package:angular/change_detection/watch_group.dart'; +import 'package:angular/change_detection/change_detection.dart'; part 'ng_a.dart'; part 'ng_bind.dart'; @@ -49,15 +51,19 @@ class NgDirectiveModule extends Module { value(NgIncludeDirective, null); value(NgPluralizeDirective, null); value(NgRepeatDirective, null); - value(NgShalowRepeatDirective, null); + value(NgShallowRepeatDirective, null); value(NgShowDirective, null); value(InputTextLikeDirective, null); + value(InputNumberLikeDirective, null); value(InputRadioDirective, null); value(InputCheckboxDirective, null); value(InputSelectDirective, null); value(OptionValueDirective, null); value(ContentEditableDirective, null); value(NgModel, null); + value(NgValue, new NgValue(null)); + value(NgTrueValue, new NgTrueValue(null)); + value(NgFalseValue, new NgFalseValue(null)); value(NgSwitchDirective, null); value(NgSwitchWhenDirective, null); value(NgSwitchDefaultDirective, null); @@ -70,6 +76,7 @@ class NgDirectiveModule extends Module { value(NgStyleDirective, null); value(NgNonBindableDirective, null); value(NgTemplateDirective, null); + value(NgControl, new NgNullControl()); value(NgForm, new NgNullForm()); value(NgModelRequiredValidator, null); diff --git a/lib/directive/ng_a.dart b/lib/directive/ng_a.dart index ee4ea8458..91b7b5590 100644 --- a/lib/directive/ng_a.dart +++ b/lib/directive/ng_a.dart @@ -7,7 +7,7 @@ part of angular.directive; * * @description * Modifies the default behavior of the html A tag so that the default action is prevented when - * the href attribute is empty. + * the a href is empty or it contains `ng-click` directive. * * This change permits the easy creation of action links with the `ngClick` directive * without changing the location or causing page reloads, e.g.: @@ -18,7 +18,8 @@ class NgADirective { final dom.Element element; NgADirective(this.element) { - if (element.attributes["href"] == "") { + if (element.attributes["href"] == "" || + element.attributes.containsKey('ng-click')) { element.onClick.listen((event) { if (element.attributes["href"] == "") { event.preventDefault(); diff --git a/lib/directive/ng_bind_html.dart b/lib/directive/ng_bind_html.dart index 7fc318c61..5908c0225 100644 --- a/lib/directive/ng_bind_html.dart +++ b/lib/directive/ng_bind_html.dart @@ -16,7 +16,7 @@ part of angular.directive; */ @NgDirective( selector: '[ng-bind-html]', - map: const {'ngBindHtml': '=>value'}) + map: const {'ng-bind-html': '=>value'}) class NgBindHtmlDirective { final dom.Element element; final dom.NodeValidator validator; diff --git a/lib/directive/ng_class.dart b/lib/directive/ng_class.dart index 0aed9a7fc..6c77f7302 100644 --- a/lib/directive/ng_class.dart +++ b/lib/directive/ng_class.dart @@ -67,8 +67,8 @@ part of angular.directive; map: const {'ng-class': '@valueExpression'}, exportExpressionAttrs: const ['ng-class']) class NgClassDirective extends _NgClassBase { - NgClassDirective(dom.Element element, Scope scope, NodeAttrs attrs) - : super(element, scope, null, attrs); + NgClassDirective(dom.Element element, Scope scope, NodeAttrs attrs, AstParser parser) + : super(element, scope, null, attrs, parser); } /** @@ -102,8 +102,8 @@ class NgClassDirective extends _NgClassBase { map: const {'ng-class-odd': '@valueExpression'}, exportExpressionAttrs: const ['ng-class-odd']) class NgClassOddDirective extends _NgClassBase { - NgClassOddDirective(dom.Element element, Scope scope, NodeAttrs attrs) - : super(element, scope, 0, attrs); + NgClassOddDirective(dom.Element element, Scope scope, NodeAttrs attrs, AstParser parser) + : super(element, scope, 0, attrs, parser); } /** @@ -137,8 +137,8 @@ class NgClassOddDirective extends _NgClassBase { map: const {'ng-class-even': '@valueExpression'}, exportExpressionAttrs: const ['ng-class-even']) class NgClassEvenDirective extends _NgClassBase { - NgClassEvenDirective(dom.Element element, Scope scope, NodeAttrs attrs) - : super(element, scope, 1, attrs); + NgClassEvenDirective(dom.Element element, Scope scope, NodeAttrs attrs, AstParser parser) + : super(element, scope, 1, attrs, parser); } abstract class _NgClassBase { @@ -146,16 +146,17 @@ abstract class _NgClassBase { final Scope scope; final int mode; final NodeAttrs nodeAttrs; + final AstParser _parser; var previousSet = []; var currentSet = []; - _NgClassBase(this.element, this.scope, this.mode, this.nodeAttrs) { + _NgClassBase(this.element, this.scope, this.mode, this.nodeAttrs, this._parser) { var prevClass; nodeAttrs.observe('class', (String newValue) { if (prevClass != newValue) { prevClass = newValue; - _handleChange(scope[r'$index']); + _handleChange(scope.context[r'$index']); } }); } @@ -163,12 +164,16 @@ abstract class _NgClassBase { set valueExpression(currentExpression) { // this should be called only once, so we don't worry about cleaning up // watcher registrations. - scope.$watchCollection(currentExpression, (current) { - currentSet = _flatten(current); - _handleChange(scope[r'$index']); - }); + scope.watch( + _parser(currentExpression, collection: true), + (current, _) { + currentSet = _flatten(current); + _handleChange(scope.context[r'$index']); + }, + readOnly: true + ); if (mode != null) { - scope.$watch(r'$index', (index, oldIndex) { + scope.watch(_parser(r'$index'), (index, oldIndex) { var mod = index % 2; if (oldIndex == null || mod != oldIndex % 2) { if (mod == mode) { @@ -177,7 +182,7 @@ abstract class _NgClassBase { element.classes.removeAll(previousSet); } } - }); + }, readOnly: true); } } @@ -191,14 +196,20 @@ abstract class _NgClassBase { static List _flatten(classes) { if (classes == null) return []; + if (classes is CollectionChangeRecord) { + classes = (classes as CollectionChangeRecord).iterable.toList(); + } if (classes is List) { return classes.where((String e) => e != null && e.isNotEmpty) .toList(growable: false); } + if (classes is MapChangeRecord) { + classes = (classes as MapChangeRecord).map; + } if (classes is Map) { return classes.keys.where((key) => toBool(classes[key])).toList(); } if (classes is String) return classes.split(' '); - throw 'ng-class expects expression value to be List, Map or String.'; + throw 'ng-class expects expression value to be List, Map or String, got $classes'; } } diff --git a/lib/directive/ng_control.dart b/lib/directive/ng_control.dart index 56cc53e63..51628aaa7 100644 --- a/lib/directive/ng_control.dart +++ b/lib/directive/ng_control.dart @@ -1,10 +1,12 @@ part of angular.directive; -abstract class NgControl { - static const NG_VALID_CLASS = "ng-valid"; - static const NG_INVALID_CLASS = "ng-invalid"; - static const NG_PRISTINE_CLASS = "ng-pristine"; - static const NG_DIRTY_CLASS = "ng-dirty"; +abstract class NgControl implements NgDetachAware { + static const NG_VALID_CLASS = "ng-valid"; + static const NG_INVALID_CLASS = "ng-invalid"; + static const NG_PRISTINE_CLASS = "ng-pristine"; + static const NG_DIRTY_CLASS = "ng-dirty"; + static const NG_SUBMIT_VALID_CLASS = "ng-submit-valid"; + static const NG_SUBMIT_INVALID_CLASS = "ng-submit-invalid"; String _name; bool _dirty; @@ -12,10 +14,46 @@ abstract class NgControl { bool _valid; bool _invalid; - get element => null; + final Scope _scope; + final NgControl _parentControl; + dom.Element _element; + + final Map> errors = new Map>(); + final List _controls = new List(); + final Map _controlByName = new Map(); + + NgControl(Scope this._scope, dom.Element this._element, Injector injector) + : _parentControl = injector.parent.get(NgControl) + { + pristine = true; + _scope.on('submitNgControl').listen((e) => _onSubmit(e.data)); + } + + detach() { + for (int i = _controls.length - 1; i >= 0; --i) { + removeControl(_controls[i]); + } + } + + reset() { + _scope.broadcast('resetNgModel'); + } + + _onSubmit(bool valid) { + if (valid) { + element.classes..add(NG_SUBMIT_VALID_CLASS)..remove(NG_SUBMIT_INVALID_CLASS); + } else { + element.classes..add(NG_SUBMIT_INVALID_CLASS)..remove(NG_SUBMIT_VALID_CLASS); + } + } get name => _name; - set name(name) => _name = name; + set name(value) { + _name = value; + _parentControl.addControl(this); + } + + get element => _element; get pristine => _pristine; set pristine(value) { @@ -31,6 +69,10 @@ abstract class NgControl { _pristine = false; element.classes..remove(NG_PRISTINE_CLASS)..add(NG_DIRTY_CLASS); + + //as soon as one of the controls/models is modified + //then all of the parent controls are dirty as well + _parentControl.dirty = true; } get valid => _valid; @@ -49,4 +91,101 @@ abstract class NgControl { element.classes..remove(NG_VALID_CLASS)..add(NG_INVALID_CLASS); } + /** + * Registers a form control into the form for validation. + * + * * [control] - The form control which will be registered (see [ngControl]). + */ + addControl(NgControl control) { + _controls.add(control); + if (control.name != null) { + _controlByName[control.name] = control; + } + } + + /** + * De-registers a form control from the list of controls associated with the + * form. + * + * * [control] - The form control which will be de-registered (see + * [ngControl]). + */ + removeControl(NgControl control) { + _controls.remove(control); + if (control.name != null) { + _controlByName.remove(control.name); + } + } + + /** + * Sets the validity status of the given control/errorType pair within + * the list of controls registered on the form. Depending on the validation + * state of the existing controls, this will either change valid to true + * or invalid to true depending on if all controls are valid or if one + * or more of them is invalid. + * + * * [control] - The registered control object (see [ngControl]). + * * [errorType] - The error associated with the control (e.g. required, url, + * number, etc...). + * * [isValid] - Whether the given error is valid or not (false would mean the + * error is real). + */ + updateControlValidity(NgControl control, String errorType, bool isValid) { + List queue = errors[errorType]; + + if (isValid) { + if (queue != null) { + queue.remove(control); + if (queue.isEmpty) { + errors.remove(errorType); + _parentControl.updateControlValidity(this, errorType, true); + } + } + if (errors.isEmpty) { + valid = true; + } + } else { + if (queue == null) { + queue = new List(); + errors[errorType] = queue; + _parentControl.updateControlValidity(this, errorType, false); + } else if (queue.contains(control)) return; + + queue.add(control); + invalid = true; + } + } +} + +class NgNullControl implements NgControl { + var _name, _dirty, _valid, _invalid, _pristine, _element; + var _controls, _scope, _parentControl, _controlName; + var errors, _controlByName; + dom.Element element; + + NgNullControl() {} + _onSubmit(bool valid) {} + + addControl(control) {} + removeControl(control) {} + updateControlValidity(NgControl control, String errorType, bool isValid) {} + + get name => null; + set name(name) {} + + get pristine => null; + set pristine(value) {} + + get dirty => null; + set dirty(value) {} + + get valid => null; + set valid(value) {} + + get invalid => null; + set invalid(value) {} + + reset() => null; + detach() => null; + } diff --git a/lib/directive/ng_events.dart b/lib/directive/ng_events.dart index 402d6422e..9141b3067 100644 --- a/lib/directive/ng_events.dart +++ b/lib/directive/ng_events.dart @@ -25,7 +25,6 @@ part of angular.directive; * * The full list of supported handlers are: * - * - [ng-blur] * - [ng-abort] * - [ng-beforecopy] * - [ng-beforecut] @@ -152,7 +151,7 @@ class NgEventDirective { int key = stream.hashCode; if (!listeners.containsKey(key)) { listeners[key] = handler; - stream.listen((event) => scope.$apply(() {handler({r"$event": event});})); + stream.listen((event) => handler({r"$event": event})); } } diff --git a/lib/directive/ng_form.dart b/lib/directive/ng_form.dart index a9651e4f4..2712be93f 100644 --- a/lib/directive/ng_form.dart +++ b/lib/directive/ng_form.dart @@ -7,27 +7,21 @@ part of angular.directive; */ @NgDirective( selector: 'form', + publishTypes : const [NgControl], visibility: NgDirective.CHILDREN_VISIBILITY) @NgDirective( selector: 'fieldset', + publishTypes : const [NgControl], visibility: NgDirective.CHILDREN_VISIBILITY) @NgDirective( selector: '.ng-form', + publishTypes : const [NgControl], visibility: NgDirective.CHILDREN_VISIBILITY) @NgDirective( selector: '[ng-form]', + publishTypes : const [NgControl], visibility: NgDirective.CHILDREN_VISIBILITY) -class NgForm extends NgControl implements NgDetachAware, Map { - final NgForm _parentForm; - final dom.Element _element; - final Scope _scope; - - final Map> errors = - new Map>(); - - final List _controls = new List(); - final Map _controlByName = new Map(); - +class NgForm extends NgControl implements Map { /** * Instantiates a new instance of NgForm. Upon creation, the instance of the * class will be bound to the formName property on the scope (where formName @@ -38,73 +32,27 @@ class NgForm extends NgControl implements NgDetachAware, Map { * * [element] - The form DOM element. * * [injector] - An instance of Injector. */ - NgForm(this._scope, dom.Element this._element, Injector injector): - _parentForm = injector.parent.get(NgForm) - { - if(!_element.attributes.containsKey('action')) { - _element.onSubmit.listen((event) { + NgForm(Scope scope, dom.Element element, Injector injector) : + super(scope, element, injector) { + + if (!element.attributes.containsKey('action')) { + element.onSubmit.listen((event) { event.preventDefault(); + _scope.broadcast('submitNgControl', valid == null ? false : valid); }); } - - pristine = true; - } - - detach() { - for (int i = _controls.length - 1; i >= 0; --i) { - removeControl(_controls[i]); - } } - get element => _element; - @NgAttr('name') get name => _name; - set name(name) { - _name = name; - _scope[name] = this; - } - - /** - * Sets the validity status of the given control/errorType pair within - * the list of controls registered on the form. Depending on the validation - * state of the existing controls, this will either change valid to true - * or invalid to true depending on if all controls are valid or if one - * or more of them is invalid. - * - * * [control] - The registered control object (see [ngControl]). - * * [errorType] - The error associated with the control (e.g. required, url, - * number, etc...). - * * [isValid] - Whether the given error is valid or not (false would mean the - * error is real). - */ - setValidity(NgControl control, String errorType, bool isValid) { - List queue = errors[errorType]; - - if(isValid) { - if(queue != null) { - queue.remove(control); - if(queue.isEmpty) { - errors.remove(errorType); - if(errors.isEmpty) valid = true; - _parentForm.setValidity(this, errorType, true); - } - } - } else { - if(queue == null) { - queue = new List(); - errors[errorType] = queue; - _parentForm.setValidity(this, errorType, false); - } else if(queue.contains(control)) return; - - queue.add(control); - invalid = true; - } + set name(value) { + super.name = value; + _scope.context[name] = this; } //FIXME: fix this reflection bug that shows up when Map is implemented operator []=(String key, value) { - if(key == 'name'){ + if (key == 'name') { name = value; } else { _controlByName[key] = value; @@ -113,45 +61,15 @@ class NgForm extends NgControl implements NgDetachAware, Map { //FIXME: fix this reflection bug that shows up when Map is implemented operator[](name) { - if(name == 'valid') { + if (name == 'valid') { return valid; - } else if(name == 'invalid') { + } else if (name == 'invalid') { return invalid; } else { return _controlByName[name]; } } - /** - * Registers a form control into the form for validation. - * - * * [control] - The form control which will be registered (see [ngControl]). - */ - addControl(NgControl control) { - _controls.add(control); - if(control.name != null) { - _controlByName[control.name] = control; - } - } - - /** - * De-registers a form control from the list of controls associated with the - * form. - * - * * [control] - The form control which will be de-registered (see - * [ngControl]). - */ - removeControl(NgControl control) { - _controls.remove(control); - if(control.name != null) { - _controlByName.remove(control.name); - } - } - - set dirty(value) { - super.dirty = _parentForm.dirty = true; - } - bool get isEmpty => false; bool get isNotEmpty => !isEmpty; get values => null; @@ -166,32 +84,11 @@ class NgForm extends NgControl implements NgDetachAware, Map { putIfAbsent(_, __) => null; } -class NgNullForm implements NgForm { - var _name, _dirty, _valid, _invalid, _pristine, _element; - var _controls, _scope, _parentForm, _controlName; - var errors, _controlByName; - dom.Element element; +class NgNullForm extends NgNullControl implements NgForm { NgNullForm() {} + operator[](name) {} operator []=(String name, value) {} - addControl(control) {} - removeControl(control) {} - setValidity(control, String errorType, bool isValid) {} - - get name => null; - set name(name) {} - - get pristine => null; - set pristine(value) {} - - get dirty => null; - set dirty(value) {} - - get valid => null; - set valid(value) {} - - get invalid => null; - set invalid(value) {} bool get isEmpty => false; bool get isNotEmpty => true; @@ -205,6 +102,4 @@ class NgNullForm implements NgForm { addAll(_) => null; forEach(_) => null; putIfAbsent(_, __) => null; - - detach() => null; } diff --git a/lib/directive/ng_if.dart b/lib/directive/ng_if.dart index 255651af6..548c536e4 100644 --- a/lib/directive/ng_if.dart +++ b/lib/directive/ng_if.dart @@ -14,7 +14,7 @@ abstract class _NgUnlessIfAttrDirectiveBase { * The new child scope. This child scope is recreated whenever the `ng-if` * subtree is inserted into the DOM and destroyed when it's removed from the * DOM. Refer - * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance + * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-prototypical-Inheritance prototypical inheritance */ Scope _childScope; @@ -26,16 +26,22 @@ abstract class _NgUnlessIfAttrDirectiveBase { void _ensureBlockExists() { if (_block == null) { - _childScope = _scope.$new(); + _childScope = _scope.createChild(new PrototypeMap(_scope.context)); _block = _boundBlockFactory(_childScope); - _block.insertAfter(_blockHole); + var insertBlock = _block; + _scope.rootScope.domWrite(() { + insertBlock.insertAfter(_blockHole); + }); } } void _ensureBlockDestroyed() { if (_block != null) { - _block.remove(); - _childScope.$destroy(); + var removeBlock = _block; + _scope.rootScope.domWrite(() { + removeBlock.remove(); + }); + _childScope.destroy(); _block = null; _childScope = null; } @@ -57,7 +63,7 @@ abstract class _NgUnlessIfAttrDirectiveBase { * Whenever the subtree is inserted into the DOM, it always gets a new child * scope. This child scope is destroyed when the subtree is removed from the * DOM. Refer - * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance + * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-prototypical-Inheritance prototypical inheritance * * This has an important implication when `ng-model` is used inside an `ng-if` * to bind to a javascript primitive defined in the parent scope. In such a @@ -117,7 +123,7 @@ class NgIfDirective extends _NgUnlessIfAttrDirectiveBase { * Whenever the subtree is inserted into the DOM, it always gets a new child * scope. This child scope is destroyed when the subtree is removed from the * DOM. Refer - * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance + * https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-prototypical-Inheritance prototypical inheritance * * This has an important implication when `ng-model` is used inside an * `ng-unless` to bind to a javascript primitive defined in the parent scope. diff --git a/lib/directive/ng_include.dart b/lib/directive/ng_include.dart index 25c06866a..f0e35e078 100644 --- a/lib/directive/ng_include.dart +++ b/lib/directive/ng_include.dart @@ -35,7 +35,7 @@ class NgIncludeDirective { if (_previousBlock == null) return; _previousBlock.remove(); - _previousScope.$destroy(); + _previousScope.destroy(); element.innerHtml = ''; _previousBlock = null; @@ -44,7 +44,7 @@ class NgIncludeDirective { _updateContent(createBlock) { // create a new scope - _previousScope = scope.$new(); + _previousScope = scope.createChild(new PrototypeMap(scope.context)); _previousBlock = createBlock(injector.createChild([new Module() ..value(Scope, _previousScope)])); diff --git a/lib/directive/ng_model.dart b/lib/directive/ng_model.dart index 2eb46f672..b4dc3140b 100644 --- a/lib/directive/ng_model.dart +++ b/lib/directive/ng_model.dart @@ -11,59 +11,77 @@ part of angular.directive; * (to be implemented) */ @NgDirective(selector: '[ng-model]') -class NgModel extends NgControl { +class NgModel extends NgControl implements NgAttachAware { final NgForm _form; - final dom.Element _element; - final Scope _scope; + final AstParser _parser; BoundGetter getter = ([_]) => null; BoundSetter setter = (_, [__]) => null; + var _lastValue; String _exp; - String _name; + final _validators = []; - final List<_NgModelValidator> _validators = new List<_NgModelValidator>(); - final Map errors = new Map(); - - Function _removeWatch = () => null; + Watch _removeWatch; bool _watchCollection; - Function render = (value) => null; - NgModel(this._scope, NodeAttrs attrs, [dom.Element this._element, - NgForm this._form]) { - _exp = 'ng-model=${attrs["ng-model"]}'; + NgModel(Scope _scope, dom.Element _element, Injector injector, + NgForm this._form, this._parser, NodeAttrs attrs) + : super(_scope, _element, injector) + { + _exp = attrs["ng-model"]; watchCollection = false; + } + + process(value, [_]) { + validate(); + _scope.rootScope.domWrite(() => render(value)); + } - _form.addControl(this); - pristine = true; + attach() { + watchCollection = false; + _scope.on('resetNgModel').listen((e) => reset()); } - get element => _element; + reset() { + modelValue = _lastValue; + } @NgAttr('name') get name => _name; set name(value) { _name = value; - _form.addControl(this); + _parentControl.addControl(this); } + // TODO(misko): could we get rid of watch collection, and just always watch the collection? get watchCollection => _watchCollection; set watchCollection(value) { if (_watchCollection == value) return; _watchCollection = value; - _removeWatch(); + if (_removeWatch!=null) _removeWatch.remove(); if (_watchCollection) { - _removeWatch = _scope.$watchCollection((s) => getter(), (value) => render(value), _exp); - } else { - _removeWatch = _scope.$watch((s) => getter(), (value) => render(value), _exp); + _removeWatch = _scope.watch( + _parser(_exp, collection: true), + (changeRecord, _) { + var value = changeRecord is CollectionChangeRecord ? changeRecord.iterable: changeRecord; + process(value); + }); + } else if (_exp != null) { + _removeWatch = _scope.watch(_exp, process); } } + // TODO(misko): getters/setters need to go. We need AST here. @NgCallback('ng-model') set model(BoundExpression boundExpression) { getter = boundExpression; setter = boundExpression.assign; + + _scope.rootScope.runAsync(() { + _lastValue = modelValue; + }); } // TODO(misko): right now viewValue and modelValue are the same, @@ -80,46 +98,23 @@ class NgModel extends NgControl { * Executes a validation on the form against each of the validation present on the model. */ validate() { - if(validators.isNotEmpty) { + if (validators.isNotEmpty) { validators.forEach((validator) { - setValidity(validator.name, validator.isValid()); + setValidity(validator.name, validator.isValid(viewValue)); }); } else { valid = true; } } - /** - * Sets the validity status of the given errorType on the model. Depending on if - * valid or invalid, the matching CSS classes will be added/removed on the input - * element associated with the model. If any errors exist on the model then invalid - * will be set to true otherwise valid will be set to true. - * - * * [errorType] - The name of the error (e.g. required, url, number, etc...). - * * [isValid] - Whether or not the given error is valid or not (false would mean the error is real). - */ - setValidity(String errorType, bool isValid) { - if(isValid) { - if(errors.containsKey(errorType)) { - errors.remove(errorType); - } - if(valid != true && errors.isEmpty) { - valid = true; - } - } else if(!errors.containsKey(errorType)) { - errors[errorType] = true; - invalid = true; - } - - if(_form != null) { - _form.setValidity(this, errorType, isValid); - } + setValidity(String name, bool valid) { + this.updateControlValidity(this, name, valid); } /** * Registers a validator into the model to consider when running validate(). */ - addValidator(_NgModelValidator v) { + addValidator(NgValidatable v) { validators.add(v); validate(); } @@ -127,21 +122,10 @@ class NgModel extends NgControl { /** * De-registers a validator from the model. */ - removeValidator(_NgModelValidator v) { + removeValidator(NgValidatable v) { validators.remove(v); validate(); } - - set dirty(value) { - super.dirty = _form.dirty = true; - } - - /** - * Removes the model from the control/form. - */ - destroy() { - _form.removeControl(this); - } } /** @@ -154,22 +138,25 @@ class NgModel extends NgControl { * is falsy (i.e. one of `false`, `null`, and `0`), then the checkbox is * unchecked. Otherwise, it is checked.  Likewise, when the checkbox is checked, * the model value is set to true. When unchecked, it is set to false. - * - * The AngularJS style ng-true-value / ng-false-value is not supported. */ @NgDirective(selector: 'input[type=checkbox][ng-model]') class InputCheckboxDirective { final dom.InputElement inputElement; final NgModel ngModel; + final NgTrueValue ngTrueValue; + final NgFalseValue ngFalseValue; final Scope scope; InputCheckboxDirective(dom.Element this.inputElement, this.ngModel, - this.scope) { + this.scope, this.ngTrueValue, this.ngFalseValue) { ngModel.render = (value) { - inputElement.checked = value == null ? false : toBool(value); + inputElement.checked = ngTrueValue.isValue(inputElement, value); }; inputElement.onChange.listen((value) { - scope.$apply(() => ngModel.viewValue = inputElement.checked); + ngModel.dirty = true; + ngModel.viewValue = inputElement.checked + ? ngTrueValue.readValue(inputElement) + : ngFalseValue.readValue(inputElement); }); } } @@ -177,7 +164,7 @@ class InputCheckboxDirective { /** * Usage: * - * + * * * * This creates a two-way binding between any string-based input element @@ -192,7 +179,6 @@ class InputCheckboxDirective { @NgDirective(selector: 'input[type=password][ng-model]') @NgDirective(selector: 'input[type=url][ng-model]') @NgDirective(selector: 'input[type=email][ng-model]') -@NgDirective(selector: 'input[type=number][ng-model]') @NgDirective(selector: 'input[type=search][ng-model]') class InputTextLikeDirective { final dom.Element inputElement; @@ -215,18 +201,76 @@ class InputTextLikeDirective { typedValue = value; } }; + inputElement + ..onChange.listen(processValue) + ..onInput.listen(processValue); + } + + processValue([_]) { + var value = typedValue; + if (value != ngModel.viewValue) { + ngModel.dirty = true; + ngModel.viewValue = value; + } + ngModel.validate(); + } +} + +/** + * Usage: + * + * + * + * Model: + * + * num myModel; + * + * This creates a two-way binding between the input and the named model property + * (e.g., myModel in the example above). When processing the input, its value is + * read as a [num], via the [dom.InputElement.valueAsNumber] field. If the input + * text does not represent a number, then the model is appropriately set to + * [double.NAN]. Setting the model property to [null] will clear the input. + * Setting the model to [double.NAN] will have no effect (input will be left + * unchanged). + */ +@NgDirective(selector: 'input[type=number][ng-model]') +@NgDirective(selector: 'input[type=range][ng-model]') +class InputNumberLikeDirective { + final dom.InputElement inputElement; + final NgModel ngModel; + final Scope scope; + + num get typedValue => inputElement.valueAsNumber; + void set typedValue(num value) { + // [chalin, 2014-02-16] This post + // http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2010-January/024829.html + // suggests that setting `valueAsNumber` to null should clear the field, but + // it does not. [TODO: put BUG/ISSUE number here]. We implement a + // workaround by setting `value`. Clean-up once the bug is fixed. + if (value == null) { + inputElement.value = null; + } else { + inputElement.valueAsNumber = value; + } + } + + InputNumberLikeDirective(dom.Element this.inputElement, this.ngModel, this.scope) { + ngModel.render = (value) { + if (value != typedValue + && (value == null || value is num && !value.isNaN)) { + typedValue = value; + } + }; inputElement ..onChange.listen(relaxFnArgs(processValue)) - ..onInput.listen((e) { - processValue(); - }); + ..onInput.listen(relaxFnArgs(processValue)); } processValue() { - var value = typedValue; + num value = typedValue; if (value != ngModel.viewValue) { ngModel.dirty = true; - scope.$apply(() => ngModel.viewValue = value); + scope.eval(() => ngModel.viewValue = value); } ngModel.validate(); } @@ -259,6 +303,66 @@ class _UidCounter { final _uidCounter = new _UidCounter(); +/** + * Use `ng-value` directive with `` or `