diff --git a/.travis.yml b/.travis.yml index 885a33c00..aa0313577 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,40 +1,73 @@ language: node_js node_js: -- "0.11" +- '0.11' env: matrix: - JOB=unit-stable + CHANNEL=stable + TESTS=vm + BROWSERS=DartiumWithWebPlatform + - JOB=unit-g3stable CHANNEL=stable - TESTS=dart2js - BROWSERS=ChromeNoSandbox,Firefox + TESTS=vm + BROWSERS=DartiumWithWebPlatform + USE_G3=YES - JOB=unit-dev CHANNEL=dev + TESTS=vm + BROWSERS=DartiumWithWebPlatform + - JOB=unit-stable + CHANNEL=stable TESTS=dart2js - BROWSERS=ChromeNoSandbox,Firefox + BROWSERS=SL_Chrome,SL_Firefox - JOB=unit-dev CHANNEL=dev + TESTS=dart2js + BROWSERS=SL_Chrome,SL_Firefox + - JOB=e2e-g3stable + CHANNEL=stable TESTS=vm BROWSERS=DartiumWithWebPlatform - - JOB=unit-stable + USE_G3=YES + - JOB=e2e-stable CHANNEL=stable TESTS=vm BROWSERS=DartiumWithWebPlatform + - JOB=e2e-dev + CHANNEL=dev + TESTS=vm + BROWSERS=DartiumWithWebPlatform + - JOB=e2e-stable + CHANNEL=stable + TESTS=dart2js + BROWSERS=SL_Chrome,SL_Firefox + - JOB=e2e-dev + CHANNEL=dev + TESTS=dart2js + BROWSERS=SL_Chrome,SL_Firefox global: - - FIREFOX_VERSION="29.0" - - CHROME_BIN=/usr/bin/google-chrome - - secure: "AKoqpZ699egF0i4uT/FQ5b4jIc0h+KVbhtVCql0uFxwFIl2HjOYgDayrUCAf6USfpW0LghZxJJhBamWOl/505eNSe9HvEd8JLg/to+1Fo9xi9llsu5ehmNH31/5pue4EvsrVuEap1qqL6/BNwI2cAryayU0p5tV0g8gL5h4IxG8=" + - FIREFOX_VERSION="30.0" + - CHROME_VERSION="35.0" + - secure: AKoqpZ699egF0i4uT/FQ5b4jIc0h+KVbhtVCql0uFxwFIl2HjOYgDayrUCAf6USfpW0LghZxJJhBamWOl/505eNSe9HvEd8JLg/to+1Fo9xi9llsu5ehmNH31/5pue4EvsrVuEap1qqL6/BNwI2cAryayU0p5tV0g8gL5h4IxG8= + - LOGS_DIR=/tmp/angular-build/logs + - BROWSER_PROVIDER_READY_FILE=/tmp/sauce-connect-ready + - SAUCE_USERNAME=angular-ci + - SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987 -# Don't test these branches. branches: except: - - g3v1x - + - g3v1x-master before_install: - export DISPLAY=:99.0 - ./scripts/travis/install.sh before_script: +- mkdir -p $LOGS_DIR - ./scripts/travis/setup.sh +- ./scripts/sauce/sauce_connect_setup.sh +- ./scripts/sauce/sauce_connect_block.sh script: - ./scripts/travis/build.sh after_success: - ./scripts/travis/after-success.sh +after_script: +- ./scripts/travis/print-logs.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4a2945a..fb05e1a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,285 @@ + +# v0.13.0 tempus-fugitification (2014-07-25) + +## Highlights + +This release is focused on performance and significantly speeds up rendering. We optimized our +entire rendering pipeline and now component rendering is 2.8 times (those with inlined templates) +to 6.3 times faster (those with template files) than the previous 0.12.0 release. + +To accomplish these performance improvements, we +- fixed a number of performance bugs +- moved more compilation work out of the ViewFactories, which stamps out DOM nodes, and into the + Compiler, which sets up the ViewFactories. +- implemented a custom "directive injector" to optimize Dependency Injection calls from the + ViewFactories +- optimized Dependency Injection, eliminating slow APIs + +Also, we have given apps more knobs to tune performance +- The Http service now supports coalescing http requests. This means that all the HTTP responses + that arrive within a particular interval can all be processed in a single digest. +- The ElementProbe can be disabled for apps that do not use animation +- Along with the existing ScopeStats, Angular now exposes cache statistics through the ngCaches + global object. This also allows developers to clear the caches and measure memory usage +- Along with these changes, we have also added support for ProtractorDart. + +## Bug Fixes + +- **DynamicParser:** Correctly handle throwing exceptions from method. + ([82ca6bad](https://github.com/angular/angular.dart/commit/82ca6bad7d930d645e57e5d0f1d795e0b97501dc), + [#971](https://github.com/angular/angular.dart/issues/971), [#1064](https://github.com/angular/angular.dart/issues/1064)) +- **Http:** + - Auxiliary methods like get, put, post, etc. do not set type restriction on data being sent. + ([68c3e80a](https://github.com/angular/angular.dart/commit/68c3e80afa258f5307e1655beadcf85257b35b11), + [#1051](https://github.com/angular/angular.dart/issues/1051)) + - Fix NoSuchMethodError in Http when cache set to true and update documentation about cache usage. + ([5b483324](https://github.com/angular/angular.dart/commit/5b483324bd73955ed799b9ba363dbc2557e2b09a), + [#1066](https://github.com/angular/angular.dart/issues/1066)) +- **NgModel:** Read the view value in the flush phase + ([75c2f170](https://github.com/angular/angular.dart/commit/75c2f170e1d4de3f40666835c176cff73b08ee82)) +- **WatchGroup:** Handle watching elements of array that were removed. + ([8c271f78](https://github.com/angular/angular.dart/commit/8c271f78941438059b0e71577e363fa48146441f), + [#1046](https://github.com/angular/angular.dart/issues/1046)) +- **benchmark:** + - typo in element tag names + ([86de17c2](https://github.com/angular/angular.dart/commit/86de17c2432fb92cdc8efcb7d671d1830649b300), + [#1220](https://github.com/angular/angular.dart/issues/1220)) + - incorrect mirrors import + ([b74d5682](https://github.com/angular/angular.dart/commit/b74d5682b9437880029bdc456466ade9ba3e7722)) + - fix standard deviation calculation + ([1f59d114](https://github.com/angular/angular.dart/commit/1f59d1149f962e6b4008fd7ab6a050f9a39108cd)) +- **cache:** + - Make UnboundedCache extend Cache + ([08e73054](https://github.com/angular/angular.dart/commit/08e73054a4efc06861ea85481e7bbd4c07f502d3), + [#1174](https://github.com/angular/angular.dart/issues/1174)) + - Do not export the Cache symbol. + ([016d463c](https://github.com/angular/angular.dart/commit/016d463c1975e04e71139310c297399f5e6c14cb)) +- **change_detector:** fix NaN move detection in collections + ([f01e2867](https://github.com/angular/angular.dart/commit/f01e2867543cb439dd1ad1db41d9c616f418baab), + [#1136](https://github.com/angular/angular.dart/issues/1136), [#1149](https://github.com/angular/angular.dart/issues/1149)) +- **component factory:** Only create a single ShadowDomComponentFactory + ([707f701c](https://github.com/angular/angular.dart/commit/707f701c3cf8a96425888f61dd3d753dad5e28a7)) +- **dccd:** + - fix false positive for collection moves + ([ea3eb1e0](https://github.com/angular/angular.dart/commit/ea3eb1e03eea9033f9bc7177941e41bbb5a32c2a), + [#1105](https://github.com/angular/angular.dart/issues/1105)) + - fix removals reporting + ([8dbeefc4](https://github.com/angular/angular.dart/commit/8dbeefc4abb412706991f99f5c8d21d1bebd8237)) +- **di:** Remove deprecated calls to DI bind(Type, inject[]). +- **directive:** Support multiple directives with same selector. + ([01488977](https://github.com/angular/angular.dart/commit/014889771ae4a80f7f934b60f02cbb1d37d5ed41)) +- **element binder:** + - New-style Module.bind for AttrMustache + ([cfa11e05](https://github.com/angular/angular.dart/commit/cfa11e050fcbc43539e37b51571c40e3270c999a)) + - Use the new-style Module.bind(toFactory) syntax + ([bed6bcbe](https://github.com/angular/angular.dart/commit/bed6bcbec3a635b9193ac79a7d8249a13f67ea95)) +- **examples:** do not use shadow DOM + ([0cf209bb](https://github.com/angular/angular.dart/commit/0cf209bb091839b8e1192e2fd54a0ced1a057201)) +- **expression extractor:** Do not use implicit DI + ([105b41f4](https://github.com/angular/angular.dart/commit/105b41f4247dac8b59738ad740fff545320431a2)) +- **introspection:** + - getTestability should throw when ElementProbes are unavailable + ([158e9aa7](https://github.com/angular/angular.dart/commit/158e9aa70cf38141959a185b2c39e33ead09e543)) + - work around http://dartbug.com/17752 + ([384039a1](https://github.com/angular/angular.dart/commit/384039a1c07ee66ac2a886e58b3eb3c12bcba04d)) +- **karma:** remove saucelabs from default browser list. + ([989992de](https://github.com/angular/angular.dart/commit/989992deec9f5253ea9028eeb6b74022736fae36)) +- **ng-model:** Turn off failing test until Dart 1.6 is stable. + ([e8825165](https://github.com/angular/angular.dart/commit/e88251651fb466e48e6f1a208cd24df7fade59f1), + [#1234](https://github.com/angular/angular.dart/issues/1234)) +- **ng-view:** cleanup should not destroy an already destroyed scope + ([5cb46a16](https://github.com/angular/angular.dart/commit/5cb46a1608f41f8a1c7db1f473c7efccc265598f), + [#1182](https://github.com/angular/angular.dart/issues/1182)) +- **ng_repeat:** fix ng_repeat not moving views for elements that have not moved + ([559a685e](https://github.com/angular/angular.dart/commit/559a685e218c453eea158eee068d2e0afaf718eb), + [#1154](https://github.com/angular/angular.dart/issues/1154), [#1155](https://github.com/angular/angular.dart/issues/1155)) +- **parser:** ensure only one instance of dynamic parser + ([f2c45758](https://github.com/angular/angular.dart/commit/f2c457580de28eb250ddce455edd3ede4af6c847)) +- **pubspec:** Add missing upper bound version constraints + ([cee0d727](https://github.com/angular/angular.dart/commit/cee0d727d9b18e6d21350bf74e023565e3908f57)) +- **registry_dynamic:** Do not use HashMaps. + ([04624f21](https://github.com/angular/angular.dart/commit/04624f21813be8efc5bffaa8daf0029e777958a2)) +- **scope:** + - remove deprecation warning for scope.watch with context + ([1c7c0ba3](https://github.com/angular/angular.dart/commit/1c7c0ba3c8745a73a03c14845c70e83662c55d3e)) + - Use runAsync for microtasks in digest. + ([d1e745e0](https://github.com/angular/angular.dart/commit/d1e745e04ba1c9fdf23b416d299e193b77e3cc53)) +- **transcluding component:** Perfer getByKey over get + ([6f3587d2](https://github.com/angular/angular.dart/commit/6f3587d21d00da296a40c4879c7e8c248c0b06a8)) +- **travis:** Work around Travis breakages + ([be76be4f](https://github.com/angular/angular.dart/commit/be76be4fde537bf9c52ae307541f045e8b06c8f3)) +- **various:** Use the new-style Module.bind(toFactory) syntax + ([a30c0a57](https://github.com/angular/angular.dart/commit/a30c0a5726e71491646506b9306ce53138c47c44)) +- **watch_group:** fix for NaN !== NaN + ([d24ff897](https://github.com/angular/angular.dart/commit/d24ff8979e3b2c6a72f388f478ea0aab655f35cb), + [#1146](https://github.com/angular/angular.dart/issues/1146)) +- **web platform:** Do not barf on attribute selectors. + ([f2b83930](https://github.com/angular/angular.dart/commit/f2b83930f2473f7d6b1f1796a8e1f07ec4e0422e)) + + +## Features + +- **animate:** Allowed property to turn off all animations + ([b3f2e6ca](https://github.com/angular/angular.dart/commit/b3f2e6caa5bc4047af59f730c17fcb5a34f1bc9f)) +- **benchmark:** + - improve layout of data by moving averages to dedicated row + ([3330d657](https://github.com/angular/angular.dart/commit/3330d657cd5c592924816a4a88876881de2460da)) + - calculate coefficient of variation + ([fc09af2c](https://github.com/angular/angular.dart/commit/fc09af2c880bf741abe8c9284805a92bd63f8742)) + - add standard deviation to report + ([13e87e06](https://github.com/angular/angular.dart/commit/13e87e06fb3972b7c0b6d35b08351a639a329eba)) + - calculate relative margin of error and simplify report + ([4614cc06](https://github.com/angular/angular.dart/commit/4614cc061011b01809eb884d7ad127d89fe4e547)) + - add confidence and stability info to averages + ([bd17bbe7](https://github.com/angular/angular.dart/commit/bd17bbe7e61b83789f4cc3f36d6d4222b37f0279)) + - add statistical functions to calculate confidence interval + ([26f7defe](https://github.com/angular/angular.dart/commit/26f7defe4bc2b70c9fd746b42053e4fa9cefc63b)) + - add ability to profile memory usage per iteration + ([afe55814](https://github.com/angular/angular.dart/commit/afe558141802f84f8cd34d61bfd4dd1e7ca9eba4)) + - improve sampling UI + ([e1c17d90](https://github.com/angular/angular.dart/commit/e1c17d904177c186825d939742800d94e6bd6304)) + - change samples input type from range to text + ([387933d0](https://github.com/angular/angular.dart/commit/387933d03fa908d7fe29fc8d673847cd18d824cc)) + - record gc time for each test run + ([dfdf67b5](https://github.com/angular/angular.dart/commit/dfdf67b50921b6ff69d9fe2360fbb7b55bf6d1ec)) + - add ability to adjust sample quantity + ([a98663d9](https://github.com/angular/angular.dart/commit/a98663d9bd148839d14e1f95d887560b58bc63f2)) + - add automatic gc before each test + ([fe1f74d0](https://github.com/angular/angular.dart/commit/fe1f74d0d845bcfe3b416ada4bb8849a1ecba3ff), + [#1133](https://github.com/angular/angular.dart/issues/1133)) +- **cache:** + - Add a JS interface to CacheRegister + ([435d9987](https://github.com/angular/angular.dart/commit/435d9987beaf2965a75ed13a18f29bb672c32806), + [#1181](https://github.com/angular/angular.dart/issues/1181)) + - Add existing caches to CacheRegister + ([59003705](https://github.com/angular/angular.dart/commit/59003705f7225760151136951361d1e71a453db4), + [#1165](https://github.com/angular/angular.dart/issues/1165)) + - Move cache out of core, add a CacheRegister + ([be62c48e](https://github.com/angular/angular.dart/commit/be62c48e3b4c9dbe5a0b4cb7fcadec4e29255861)) +- **compiler:** + - Backport DirectiveBinder API from #1178 to allow gradual migration. + ([1f3cca42](https://github.com/angular/angular.dart/commit/1f3cca429e2074400ce7e0f476d972d6b54e2774)) +- **element binder:** Use a child scope instead of Scope.watch(context:o) + ([6051340b](https://github.com/angular/angular.dart/commit/6051340bb583382ff0157e255eafa537ab5564aa)) +- **form:** Add support for `input[type=color]` + ([0064ef5c](https://github.com/angular/angular.dart/commit/0064ef5c7db5ce2286cc7d626b2cc620429e4da3), + [#611](https://github.com/angular/angular.dart/issues/611), [#1080](https://github.com/angular/angular.dart/issues/1080)) +- **http:** + - support coalescing http requests + ([3e44a542](https://github.com/angular/angular.dart/commit/3e44a542faccd3f5e5f7861cff44e221279bcc42)) + - run interceptors synchronously until first non-sync interceptor + ([38d3cfd6](https://github.com/angular/angular.dart/commit/38d3cfd6ff680dd92ce3b34018fdb7b148a29ea9)) +- **mock:** Add timer queue checks in mock zone + ([98e61b77](https://github.com/angular/angular.dart/commit/98e61b77d91a58fe13406ded2b94167b595701d5), + [#1157](https://github.com/angular/angular.dart/issues/1157)) +- **router:** added vetoable preLeave event + ([ddd9e414](https://github.com/angular/angular.dart/commit/ddd9e4147c0c88a374345bf8b32df6cf57740ac4), + [#1070](https://github.com/angular/angular.dart/issues/1070)) +- **scope:** + - Expose Scope.watchAST as a public API + ([ecd75ce7](https://github.com/angular/angular.dart/commit/ecd75ce71460a3bb1e87e0c9371c8d8ac892c512)) + - Deprecate Scope.watch's context parameter. + ([e8a5ce73](https://github.com/angular/angular.dart/commit/e8a5ce734b00fd22f700e2fc1be5507dda195638)) + - Instrument Scope to use User tags in Dart Obervatory + ([c21ac7ea](https://github.com/angular/angular.dart/commit/c21ac7eaec3fd15bba7a124fdfb3e9680092a0f2), + [#1138](https://github.com/angular/angular.dart/issues/1138)) + - Use VmTurnZone.onScheduleMicrotask in Scope + ([81667aad](https://github.com/angular/angular.dart/commit/81667aad6a2da1efa39adaf333b8ebfe297c88b1), + [#984](https://github.com/angular/angular.dart/issues/984)) +- **testability:** + - whenStable replaces notifyWhenNoOutstandingRequests + ([5ef596d1](https://github.com/angular/angular.dart/commit/5ef596d10f50d2739eba6a7be9555f240f12abf6)) + - implement the testability for ProtractorDart + ([f2d1f2e9](https://github.com/angular/angular.dart/commit/f2d1f2e9e64d18fe5407f67555eb1c30110bd419)) + + +## Performance Improvements + +- **ChangeDetector:** + - create _EvalWatchRecord#namedArgs lazily + ([42e53b86](https://github.com/angular/angular.dart/commit/42e53b8663936b3637b2d13de4f3f56db27e94cf)) + - lazy initialize DuplicateMap + ([11629dee](https://github.com/angular/angular.dart/commit/11629deea92bb0f0b341ba6eb2f04a5072fbdcd2)) +- **View:** Improve View instantiation speed and memory consumption. + ([494deda5](https://github.com/angular/angular.dart/commit/494deda594f39b422e4c2f5def1a2cbaf749efba)) +- **cd:** fewer string concatenations (10% improvement) + ([a6526803](https://github.com/angular/angular.dart/commit/a6526803fb07f126414f0259d4d0baf8e89d70b1)) +- **compiler:** + - +6% Pre-compute ViewFactories, styles for components. + ([be3cdd41](https://github.com/angular/angular.dart/commit/be3cdd4147d7917dea5fcca301cb73057f0f604d), + [#1134](https://github.com/angular/angular.dart/issues/1134)) + - An option to disable the ElementProbe. + ([9f0c7bca](https://github.com/angular/angular.dart/commit/9f0c7bcab2915ffa7509a28eaff1d1762f1d9bf0), + [#1118](https://github.com/angular/angular.dart/issues/1118), [#1131](https://github.com/angular/angular.dart/issues/1131)) + - Pre-compile Scope.watch ASTs for attribute mustaches + ([90df4eb2](https://github.com/angular/angular.dart/commit/90df4eb2012a0bf21f33bf219be544bd535f765c), + [#1088](https://github.com/angular/angular.dart/issues/1088)) + - Precompute Scope.watch AST for TextMustache + ([daf8d5af](https://github.com/angular/angular.dart/commit/daf8d5afdc8f0f0f66eed5de6eea4a5df2280a9f)) +- **element binder:** Do not create tasklists when not needed + ([a33891ea](https://github.com/angular/angular.dart/commit/a33891ea915a4faf98caf78725dfc093b213744b)) +- **scope:** Cache the Scope.watch AST. + ([05e2c576](https://github.com/angular/angular.dart/commit/05e2c57625f1cc2c56c3cc91534420d066e41389), + [#1173](https://github.com/angular/angular.dart/issues/1173)) +- **various:** Avoid putIfAbsent + ([57da29d7](https://github.com/angular/angular.dart/commit/57da29d7ea0e20aecde60ac42788b30aedcaa3b6)) +- **view cache:** Avoid http.get + ([db72a4fc](https://github.com/angular/angular.dart/commit/db72a4fc99e1d2b9921b361198e0c57d93091f5e), + [#1108](https://github.com/angular/angular.dart/issues/1108)) +- **view factory:** 14% Precompute linking information for nodes + ([eac36d1d](https://github.com/angular/angular.dart/commit/eac36d1d26a283560742b3555886f5db99ee9c65), + [#1194](https://github.com/angular/angular.dart/issues/1194), [#1196](https://github.com/angular/angular.dart/issues/1196)) + + +## Breaking Changes + +- **Scope:** due to [81667aad](https://github.com/angular/angular.dart/commit/81667aad6a2da1efa39adaf333b8ebfe297c88b1), + + +Previously a micro task registered in flush phase would cause a new +digest cycle after the current digest cycle. The new behavior +will cause an error. + +Closes #984 +- **View:** due to [494deda5](https://github.com/angular/angular.dart/commit/494deda594f39b422e4c2f5def1a2cbaf749efba), + + +- Injector no longer supports visibility +- The Directive:module instead of returning Module now takes + DirectiveModule (which supports visibility) +- Application Injector and DirectiveInjector now have separate trees. + (The root if DirectiveInjector is ApplicationInjector) +- **scope:** due to [d1e745e0](https://github.com/angular/angular.dart/commit/d1e745e04ba1c9fdf23b416d299e193b77e3cc53), + + +Microtasks scheduled in flush will process in current cycle, but they +are not allowed to do model changes. + +Microtasks scheduled in digest will be executed in digest, counting +towards the ScopeDigestTTL. +- **testability:** due to [5ef596d1](https://github.com/angular/angular.dart/commit/5ef596d10f50d2739eba6a7be9555f240f12abf6), + + NOTE: This only affects you if you are calling this API directly. If + you are using ProtractorDart, then you are insulated from this change. + + To update your code, rename all references to the + notifyWhenNoOutstandingRequests(callback) method on the testability + object to whenStable(callback). + # v0.12.0 sprightly-argentinosaurus (2014-06-03) +## Highlights + +- A 20% performance improvement from caching interpolated expressions. +- Http service can make cross-site requests (get, post, put, etc.) which use credentials (such as + cookies or authorization headers). +- **Breaking change**: vetoing is no longer allowed on leave (RouteLeaveEvent). This change corrects + an issue with routes unable to recover from another route vetoing a leave event. +- **Breaking change**: Zone.defaultOnScheduleMicrotask is now named Zone.onScheduleMicrotask +- **Breaking change**: OneWayOneTime bindings will continue to accept value assignments until their + stabilized value is non-null. + ## Bug Fixes - **NgStyle:** make NgStyle export expressions @@ -90,13 +369,61 @@ - **VmTurnZone:** due to [a8699da0](https://github.com/angular/angular.dart/commit/a8699da016c754e08502ae24034a86bd8d6e0d8e), - `Zone.defaultOnScheduleMicrotask` is now named `Zone.onScheduleMicrotask` # v0.11.0 ungulate-funambulism (2014-05-06) +## Highlights + +### Breaking Change + +The breaking change first: `Http.getString()` is gone. + +If you said: `Http.getString('data.txt').then((String data) { ... })` before, now say +`Http.get('data.txt').then((HttpResponse resp) { var data = resp.data; ... });` + +### New Features + +- Shadow DOM-less components + +Shadow DOM is still enabled by default for components. Now, its use can be controlled through the +new `useShadowDom` option in the Component annotation. + +For example: + +```dart +@Component( + selector: 'my-comp', + templateUrl: 'my-comp.html', + useShadowDom: false) +class MyComp {} +``` + +will disable Shadow DOM for that component and construct the template in the "light" DOM. Either +omitting the `useShadowDom` option or explicitly setting it to `true` will cause Angular to +construct the template in the component's shadow DOM. + +Adding cssUrls to Components with Shadow DOM disabled is not allowed. Since they aren't using Shadow +DOM, there is no style encapsulation and per-component CSS doesn't make sense. The component has +access to the styles in the `documentFragment` where it was created. Style encapsulation is a +feature we are thinking about, so this design will likely change in the future. + +- bind-* syntax + +We have shipped an early "preview" of the upcoming bind-* syntax. In 0.11.0, you may bind an +expression to any mapped attribute, even if that attribute is a `@NgAttr` mapping which typically +takes a string. + +### Performance improvements + +There are two significant performance improvements: +- We now cache CSS as `StyleElement`s instead of string, saving a `setInnerHtml` call on each styled + component instantiation. In a benchmark where components used unminified Bootstrap styles (124kB), + this sped up component creation by 31%. +- Changes in the DI package sped up View instantiation by 200%. This change makes AngularDart + rendering significantly faster. ## Bug Fixes diff --git a/DEVELOPER.md b/DEVELOPER.md index 31071d120..8dda3c606 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -38,7 +38,7 @@ following products on your development machine: * [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. + source or as a pre-packaged bundle. You will need node v0.10.29+. ## Getting the Sources @@ -115,47 +115,28 @@ pub install NOTE: scripts are being written to embody the following steps. -To run base tests: +To run all unit 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 +# Run all tests +./scripts/run-test.sh ``` -To run Karma tests over Dartium, execute the following shell commands (which -will launch the Karma server): +To run Karma tests over Dartium, execute the following shell command: ```shell -. ./scripts/env.sh node "node_modules/karma/bin/karma" start karma.conf \ --reporters=junit,dots --port=8765 --runner-port=8766 \ - --browsers=Dartium + --browsers=Dartium --single-run ``` -In another shell window or tab, or from your favorite IDE, launch the Karma -tests proper by executing: +To make a persistent Karma server that watches for changes in your files and +runs tests on change, replace `--single-run` with `--auto-watch`. -```shell -. ./scripts/env.sh -./scripts/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 -``` - **Note**: 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. `xit` and `xdescribe` @@ -213,7 +194,7 @@ Set the parameters as follow: - **PATH**: `/path/to/dart-sdk/bin` - **DART_FLAGS**: `--checked` -Launch the server by selecting the "Karmer server" configuration in the toolbar +Launch the server by selecting the "Karma server" configuration in the toolbar and pressing the play icon. You should see the following message at the bottom of the run window: `INFO [Chrome 34.0.1847 (Linux)]: Connected on socket 97GpzQz-MfHFPHgHOVkc with id 10199707` @@ -221,7 +202,7 @@ of the run window: #### Running the tests You need to create a "Karma tests" run configuration. Start by copying the -"Karma server" run configuration and xhange the **Application parameters** to +"Karma server" run configuration and change the **Application parameters** to `run --port=8765`. To execute the test suite, you just need to run this "Karma tests" diff --git a/benchmark/karma.conf.js b/benchmark/karma.conf.js new file mode 100644 index 000000000..bdfdf4adc --- /dev/null +++ b/benchmark/karma.conf.js @@ -0,0 +1,68 @@ +// Karma configuration +// Generated on Tue Jun 10 2014 08:41:10 GMT-0700 (PDT) + +module.exports = function(config) { + config.set({ + + // base path, that will be used to resolve files and exclude + basePath: '', + + + // frameworks to use + frameworks: ['jasmine'], + + + // list of files / patterns to load in the browser + files: [ + 'web/*.js' + ], + + + // list of files to exclude + exclude: [ + + ], + + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: ['Chrome'], + + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false + }); +}; diff --git a/benchmark/launch_chrome.sh b/benchmark/launch_chrome.sh new file mode 100755 index 000000000..c9e7ea65b --- /dev/null +++ b/benchmark/launch_chrome.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +platform=`uname` +if [[ "$platform" == 'Linux' ]]; then + `google-chrome --js-flags="--expose-gc"` +elif [[ "$platform" == 'Darwin' ]]; then + `/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --enable-memory-info --enable-precise-memory-info --enable-memory-benchmarking --js-flags="--expose-gc"` +fi diff --git a/benchmark/pubspec.lock b/benchmark/pubspec.lock index d29fe4deb..70b9a773c 100644 --- a/benchmark/pubspec.lock +++ b/benchmark/pubspec.lock @@ -4,7 +4,7 @@ packages: analyzer: description: analyzer source: hosted - version: "0.13.6" + version: "0.15.7" angular: description: path: ".." @@ -30,15 +30,17 @@ packages: code_transformers: description: code_transformers source: hosted - version: "0.1.3" + version: "0.1.4+2" collection: description: collection source: hosted version: "0.9.2" di: - description: di - source: hosted - version: "0.0.40" + description: + path: "../../di.dart" + relative: true + source: path + version: "2.0.0-alpha.9" html5lib: description: html5lib source: hosted @@ -46,7 +48,7 @@ packages: intl: description: intl source: hosted - version: "0.9.9" + version: "0.9.10" logging: description: logging source: hosted @@ -62,7 +64,7 @@ packages: path: description: path source: hosted - version: "1.1.0" + version: "1.2.1" perf_api: description: perf_api source: hosted diff --git a/benchmark/pubspec.yaml b/benchmark/pubspec.yaml index 141c3f449..ead62b1e6 100644 --- a/benchmark/pubspec.yaml +++ b/benchmark/pubspec.yaml @@ -13,3 +13,4 @@ transformers: - $dart2js: minify: false checked: false + commandLineOptions: [--dump-info] diff --git a/benchmark/watch_group_perf.dart b/benchmark/watch_group_perf.dart index 4eb9c065f..99bf17f69 100644 --- a/benchmark/watch_group_perf.dart +++ b/benchmark/watch_group_perf.dart @@ -9,7 +9,7 @@ import 'package:benchmark_harness/benchmark_harness.dart'; @MirrorsUsed( targets: const [ - 'angular.perf.watch_group' + 'angular.benchmarks.watch_group' ], override: '*' ) diff --git a/benchmark/web/bootstrap.min.css b/benchmark/web/bootstrap.min.css new file mode 100644 index 000000000..679272d25 --- /dev/null +++ b/benchmark/web/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.1.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none!important;color:#000!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#999}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#999}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date]{line-height:34px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.has-feedback .form-control-feedback{position:absolute;top:25px;right:0;display:block;width:34px;height:34px;line-height:34px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:7px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#999}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:4px;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:gray}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;color:#fff;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5) 0),color-stop(rgba(0,0,0,.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001) 0),color-stop(rgba(0,0,0,.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/benchmark/web/bp.js b/benchmark/web/bp.js index c81c20366..518ef2999 100644 --- a/benchmark/web/bp.js +++ b/benchmark/web/bp.js @@ -1,136 +1,300 @@ -window.benchmarkSteps = []; +var bp = window.bp = { + steps: window.benchmarkSteps = [], + Statistics: { + //Taken from z-table where confidence is 95% + criticalValue: 1.96 + }, + Runner: { + runState: { + iterations: 0, + numSamples: 20, + recentResult: {} + } + }, + Document: {}, + Report: { + timesPerAction: {} + }, + Measure: { + characteristics: ['gcTime','testTime','garbageCount','retainedCount'] + } +}; -window.addEventListener('DOMContentLoaded', function() { - var container = document.querySelector('#benchmarkContainer'); +bp.Measure.numMilliseconds = function() { + if (window.performance != null && typeof window.performance.now == 'function') { + return window.performance.now(); + } else if (window.performance != null && typeof window.performance.webkitNow == 'function') { + return window.performance.webkitNow(); + } else { + console.log('using Date.now'); + return Date.now(); + } +}; - // Add links to everything - var linkDiv = document.createElement('div'); - linkDiv.style['margin-bottom'] = "1.5em"; - var linkHtml = [ - '', - 'Benchmark Versions: ' - ].join('\n'); +bp.Statistics.getMean = function (sample) { + var total = 0; + sample.forEach(function(x) { total += x; }); + return total / sample.length; +}; - [ - // Add new benchmark suites here - ['tree.html', 'TreeComponent'] - ].forEach((function (link) { - linkHtml += [ - '', - link[1], - '' - ].join(''); - })); +bp.Statistics.calculateConfidenceInterval = function(standardDeviation, sampleSize) { + var standardError = standardDeviation / Math.sqrt(sampleSize); + return bp.Statistics.criticalValue * standardError; +}; - linkDiv.innerHTML = linkHtml; - container.appendChild(linkDiv); - - - // Benchmark runner - var btn = document.createElement('button'); - btn.innerText = "Loop"; - var running = false; - btn.addEventListener('click', loopBenchmark); - - container.appendChild(btn); - - function loopBenchmark() { - if (running) { - btn.innerText = "Loop"; - running = false; - } else { - window.requestAnimationFrame(function() { - btn.innerText = "Pause"; - running = true; - var loopB = function() { - if (running) { - window.requestAnimationFrame(function() { - if (running) runBenchmarkSteps(loopB); - }); - } - }; - loopB(); - }); - } +bp.Statistics.calculateRelativeMarginOfError = function(marginOfError, mean) { + /* + * Converts absolute margin of error to a relative margin of error by + * converting it to a percentage of the mean. + */ + return (marginOfError / mean); +}; + +bp.Statistics.calculateCoefficientOfVariation = function(standardDeviation, mean) { + return standardDeviation / mean; +}; + +bp.Statistics.calculateStandardDeviation = function(sample, mean) { + var deviation = 0; + sample.forEach(function(x) { + deviation += Math.pow(x - mean, 2); + }); + deviation = deviation / (sample.length -1); + deviation = Math.sqrt(deviation); + return deviation; +}; + +bp.Runner.setIterations = function (iterations) { + bp.Runner.runState.iterations = iterations; +}; + +bp.Runner.resetIterations = function() { + bp.Runner.runState.iterations = 0; +}; + +bp.Runner.loopBenchmark = function () { + if (bp.Runner.runState.iterations <= -1) { + //Time to stop looping + bp.Runner.setIterations(0); + bp.Document.loopBtn.innerText = 'Loop'; + return; } + bp.Runner.setIterations(-1); + bp.Document.loopBtn.innerText = 'Pause'; + bp.Runner.runAllTests(); +}; + +bp.Runner.onceBenchmark = function() { + bp.Runner.setIterations(1); + bp.Document.onceBtn.innerText = '...'; + bp.Runner.runAllTests(function() { + bp.Document.onceBtn.innerText = 'Once'; + }); +}; +bp.Runner.twentyFiveBenchmark = function() { + var twentyFiveBtn = bp.Document.twentyFiveBtn; + bp.Runner.setIterations(25); + twentyFiveBtn.innerText = 'Looping...'; + bp.Runner.runAllTests(function() { + twentyFiveBtn.innerText = 'Loop 25x'; + }, 5); +}; - var onceBtn = document.createElement('button'); - onceBtn.innerText = "Once"; - onceBtn.addEventListener('click', function() { +bp.Runner.runAllTests = function (done) { + if (bp.Runner.runState.iterations--) { + bp.steps.forEach(function(bs) { + var testResults = bp.Runner.runTimedTest(bs); + bp.Runner.runState.recentResult[bs.name] = testResults; + }); + bp.Report.markup = bp.Report.calcStats(); + bp.Document.writeReport(bp.Report.markup); window.requestAnimationFrame(function() { - onceBtn.innerText = "..."; - window.requestAnimationFrame(function() { - runBenchmarkSteps(function() { - onceBtn.innerText = "Once"; - }); - }); + bp.Runner.runAllTests(done); }); - }); - container.appendChild(onceBtn); + } + else { + bp.Document.writeReport(bp.Report.markup); + bp.Runner.resetIterations(); + done && done(); + } +}; - var infoDiv = document.createElement('div'); - infoDiv.style['font-family'] = 'monospace'; - container.appendChild(infoDiv); +bp.Runner.runTimedTest = function (bs) { + var startTime, + endTime, + startGCTime, + endGCTime, + retainedMemory, + garbage, + beforeHeap, + afterHeap, + finalHeap; + if (typeof window.gc === 'function') { + window.gc(); + } + beforeHeap = performance.memory.usedJSHeapSize; + startTime = bp.Measure.numMilliseconds(); + bs.fn(); + endTime = bp.Measure.numMilliseconds() - startTime; + afterHeap = performance.memory.usedJSHeapSize; - var numMilliseconds; - var performance = window.performance; - if (performance != null && typeof performance.now == "function") { - numMilliseconds = function numMillisecondsWPN() { - return performance.now(); - } - } else if (performance != null && typeof performance.webkitNow == "function") { - numMilliseconds = function numMillisecondsWebkit() { - return performance.webkitNow(); - } - } else { - console.log('using Date.now'); - numMilliseconds = function numMillisecondsDateNow() { - return Date.now(); - }; + startGCTime = bp.Measure.numMilliseconds(); + if (typeof window.gc === 'function') { + window.gc(); } + endGCTime = bp.Measure.numMilliseconds() - startGCTime; + + finalHeap = performance.memory.usedJSHeapSize; + garbage = Math.abs(finalHeap - afterHeap); + retainedMemory = finalHeap - beforeHeap; + return { + testTime: endTime, + gcTime: endGCTime, + beforeHeap: beforeHeap, + garbageCount: garbage, + retainedCount: retainedMemory + }; +}; + +bp.Report.generatePartial = function(model) { + return bp.Document.infoTemplate(model); +}; - function runBenchmarkSteps(done) { - // Run all the steps; - var times = {}; - window.benchmarkSteps.forEach(function(bs) { - var startTime = numMilliseconds(); - bs.fn(); - times[bs.name] = numMilliseconds() - startTime; +bp.Document.writeReport = function(reportContent) { + bp.Document.infoDiv.innerHTML = reportContent; +}; + +bp.Report.getTimesPerAction = function(name) { + var tpa = bp.Report.timesPerAction[name]; + if (!tpa) { + tpa = bp.Report.timesPerAction[name] = { + name: name, + nextEntry: 0 + }; + _.each(bp.Measure.characteristics, function(c) { + tpa[c] = { + recent: undefined, + history: [], + avg: {}, + min: Number.MAX_VALUE, + max: Number.MIN_VALUE + }; }); - calcStats(times); + } + return tpa; +}; - done(); +bp.Report.rightSizeTimes = function(times) { + var delta = times.length - bp.Runner.runState.numSamples; + if (delta > 0) { + return times.slice(delta); } - var timesPerAction = {}; - - var NUM_SAMPLES = 10; - function calcStats(times) { - var iH = ''; - window.benchmarkSteps.forEach(function(bs) { - var tpa = timesPerAction[bs.name]; - if (!tpa) { - tpa = timesPerAction[bs.name] = { - times: [], // circular buffer - fmtTimes: [], - nextEntry: 0 - } - } - tpa.fmtTimes[tpa.nextEntry] = ('' + times[bs.name]).substr(0,6); - tpa.times[tpa.nextEntry++] = times[bs.name]; - tpa.nextEntry %= NUM_SAMPLES; - var avg = 0; - tpa.times.forEach(function(x) { avg += x; }); - avg /= Math.min(NUM_SAMPLES, tpa.times.length); - avg = ('' + avg).substr(0,6); - iH += '
' + (' ' + bs.name).slice(-10).replace(/ /g, ' ') + ': avg-' + NUM_SAMPLES + ':' + avg + 'ms [' + tpa.fmtTimes.join(', ') + ']ms
'; + return times; +}; + +bp.Report.updateTimes = function(tpa, index, reference, recentTime) { + var curTpa = tpa[reference]; + curTpa.recent = recentTime; + curTpa.history[index] = recentTime; + curTpa.history = bp.Report.rightSizeTimes(curTpa.history); + curTpa.min = Math.min(curTpa.min, recentTime); + curTpa.max = Math.max(curTpa.max, recentTime); +}; + +bp.Report.calcStats = function() { + var report = ''; + bp.steps.forEach(function(bs) { + var recentResult = bp.Runner.runState.recentResult[bs.name], + tpa = bp.Report.getTimesPerAction(bs.name); + + _.each(bp.Measure.characteristics, function(c) { + bp.Report.updateTimes(tpa, tpa.nextEntry, c, recentResult[c]); + var mean = bp.Statistics.getMean(tpa[c].history); + var stdDev = bp.Statistics.calculateStandardDeviation(tpa[c].history, mean); + tpa[c].avg = { + mean: mean, + stdDev: stdDev, + coefficientOfVariation: bp.Statistics.calculateCoefficientOfVariation(stdDev, mean) + }; }); - infoDiv.innerHTML = iH; + + tpa.nextEntry++; + tpa.nextEntry %= bp.Runner.runState.numSamples; + + report += bp.Report.generatePartial(tpa); + }); + return report; +}; + +bp.Document.addSampleRange = function() { + bp.Document.sampleRange = bp.Document.container().querySelector('#sampleRange'); + if (bp.Document.sampleRange) { + bp.Document.sampleRange.value = Math.max(bp.Runner.runState.numSamples, 1); + bp.Document.sampleRange.addEventListener('input', bp.Document.onSampleInputChanged); + bp.Document.sampleRangeValue = bp.Document.container().querySelector('#sampleRangeValue'); + bp.Document.sampleRangeValue.innerText = bp.Runner.runState.numSamples; + } + +}; + +bp.Document.onSampleInputChanged = function (evt) { + var value = evt.target.value; + bp.Runner.runState.numSamples = parseInt(value, 10); + if (bp.Document.sampleRangeValue) { + bp.Document.sampleRangeValue.innerText = value; + } +}; + +bp.Document.container = function() { + if (!bp.Document._container) { + bp.Document._container = document.querySelector('#benchmarkContainer'); } -}); + return bp.Document._container; +} + +bp.Document.addButton = function(reference, handler) { + var container = bp.Document.container(); + bp.Document[reference] = container.querySelector('button.' + reference); + if (bp.Document[reference]) { + bp.Document[reference].addEventListener('click', handler); + } +} + +bp.Document.addLinks = function() { + // Add links to everything + var linkDiv = bp.Document.container().querySelector('.versionContent'); + var linkHtml = ''; + + [ + // Add new benchmark suites here + ['tree.html', 'TreeComponent'] + ].forEach((function (link) { + linkHtml += ''+ link[1] +''; + })); + + if (linkDiv) { + linkDiv.innerHTML = linkHtml; + } +}; + +bp.Document.addInfo = function() { + bp.Document.infoDiv = bp.Document.container().querySelector('div.info'); + if (bp.Document.infoDiv) { + bp.Document.infoTemplate = _.template(bp.Document.container().querySelector('#infoTemplate').innerHTML); + } +}; + +bp.Document.onDOMContentLoaded = function() { + if (!bp.Document.container()) return; + bp.Document.addLinks(); + bp.Document.addButton('loopBtn', bp.Runner.loopBenchmark); + bp.Document.addButton('onceBtn', bp.Runner.onceBenchmark); + bp.Document.addButton('twentyFiveBtn', bp.Runner.twentyFiveBenchmark); + bp.Document.addSampleRange(); + bp.Document.addInfo(); +}; + +window.addEventListener('DOMContentLoaded', bp.Document.onDOMContentLoaded); diff --git a/benchmark/web/bp.spec.js b/benchmark/web/bp.spec.js new file mode 100644 index 000000000..447c47058 --- /dev/null +++ b/benchmark/web/bp.spec.js @@ -0,0 +1,429 @@ +//ugly +if (typeof bp !== 'undefined') { + window.removeEventListener('DOMContentLoaded', bp.onDOMContentLoaded); +} + +describe('bp', function() { + var bp = window.bp, + mockStep = { + fn: function() {}, + name: 'fakeStep' + }; + + beforeEach(function() { + bp.Document._container = document.createElement('div'); + bp.Document.infoTemplate = function(model) { + return JSON.stringify(model); + } + bp.Runner.runState = { + iterations: 0, + numSamples: 20, + recentResult: {} + }; + + bp.Report.timesPerAction = {}; + }); + + describe('.Statistics', function() { + describe('.calculateConfidenceInterval()', function() { + it('should provide the correct confidence interval', function() { + expect(bp.Statistics.calculateConfidenceInterval(30, 1000)).toBe(1.859419264179007); + }); + }); + + + describe('.calculateRelativeMarginOfError()', function() { + expect(bp.Statistics.calculateRelativeMarginOfError(1.85, 5)).toBe(0.37); + }); + + + describe('.getMean()', function() { + it('should return the mean for a given sample', function() { + expect(bp.Statistics.getMean([1,2,5,4])).toBe(3); + }); + }); + + + describe('.calculateStandardDeviation()', function() { + it('should provide the correct standardDeviation for the provided sample and mean', function() { + expect(bp.Statistics.calculateStandardDeviation([ + 2,4,4,4,5,5,7,9 + ], 5)).toBe(2.138089935299395); + }); + + + it('should provide the correct standardDeviation for the provided sample and mean', function() { + expect(bp.Statistics.calculateStandardDeviation([ + 674.64,701.78,668.33,662.15,663.34,677.32,664.25,1233.00,1100.80,716.15,681.52,671.23,702.70,686.89,939.39,830.28,695.46,695.66,675.15,667.48], 750.38)).toBe(158.57877026559186); + }); + }); + + + describe('.calculateCoefficientOfVariation()', function() { + expect(bp.Statistics.calculateCoefficientOfVariation(0.5, 5)).toBe(0.1); + }); + }); + + + describe('.Document', function() { + describe('.container()', function() { + it('should return bp.Document._container if set', function() { + expect(bp.Document.container() instanceof HTMLElement).toBe(true); + }); + }); + + + describe('.onSampleRangeChanged()', function() { + beforeEach(function() { + bp.Runner.resetIterations(); + }); + + + it('should change the numSamples property', function() { + expect(bp.Runner.runState.numSamples).toBe(20); + bp.Document.onSampleInputChanged({target: {value: '80'}}); + expect(bp.Runner.runState.numSamples).toBe(80); + }); + }); + + + describe('.writeReport()', function() { + it('should write the report to the infoDiv', function() { + bp.Document.infoDiv = document.createElement('div'); + bp.Document.writeReport('report!'); + expect(bp.Document.infoDiv.innerHTML).toBe('report!') + }); + }); + + + describe('.onDOMContentLoaded()', function() { + it('should call methods to write to the dom', function() { + var linksSpy = spyOn(bp.Document, 'addLinks'); + var buttonSpy = spyOn(bp.Document, 'addButton'); + var rangeSpy = spyOn(bp.Document, 'addSampleRange'); + var infoSpy = spyOn(bp.Document, 'addInfo'); + + bp.Document.onDOMContentLoaded(); + expect(linksSpy).toHaveBeenCalled(); + expect(buttonSpy.callCount).toBe(3); + expect(rangeSpy).toHaveBeenCalled(); + expect(infoSpy).toHaveBeenCalled(); + }); + }); + }); + + + describe('.Runner', function() { + describe('.setIterations()', function() { + it('should set provided arguments to runState object', function() { + bp.Runner.runState = {numSamples: 20}; + bp.Runner.setIterations(15); + expect(bp.Runner.runState.numSamples).toBe(20); + expect(bp.Runner.runState.iterations).toBe(15); + }); + }); + + + describe('.resetIterations()', function() { + it('should set runState object to defaults', function() { + bp.Runner.runState = { + numSamples: 99, + iterations: 100, + recentResult: { + fakeStep: { + testTime: 2 + } + } + } + bp.Report.timesPerAction = { + fakeStep: { + testTimes: [5] + } + }; + + bp.Runner.resetIterations(); + expect(bp.Runner.runState.numSamples).toBe(99); + expect(bp.Runner.runState.iterations).toBe(0); + expect(bp.Report.timesPerAction).toEqual({fakeStep: {testTimes: [5]}}); + expect(bp.Runner.runState.recentResult['fakeStep'].testTime).toEqual(2); + }); + }); + + + describe('.runTimedTest()', function() { + it('should call gc if available', function() { + window.gc = window.gc || function() {}; + var spy = spyOn(window, 'gc'); + bp.Runner.runTimedTest(mockStep, {}); + expect(spy).toHaveBeenCalled(); + }); + + + it('should return the time required to run the test', function() { + var times = {}; + expect(typeof bp.Runner.runTimedTest(mockStep, times).testTime).toBe('number'); + }); + }); + + + describe('.runAllTests()', function() { + beforeEach(function() { + bp.steps = [mockStep]; + bp.Document.infoDiv = document.createElement('div'); + bp.infoTemplate = jasmine.createSpy('infoTemplate'); + }); + + it('should call resetIterations before calling done', function() { + var spy = spyOn(bp.Runner, 'resetIterations'); + bp.Runner.runState.iterations = 0; + bp.Runner.runAllTests(); + expect(spy).toHaveBeenCalled(); + }); + + + it('should call done after running for the appropriate number of iterations', function() { + var spy = spyOn(mockStep, 'fn'); + var doneSpy = jasmine.createSpy('done'); + + runs(function() { + bp.Runner.setIterations(5, 5); + bp.Runner.runAllTests(doneSpy); + }); + + waitsFor(function() { + return doneSpy.callCount; + }, 'done to be called', 200); + + runs(function() { + expect(spy.callCount).toBe(5); + }); + }); + + + it('should add as many times to timePerStep as are specified by numSamples', function() { + var doneSpy = jasmine.createSpy('done'); + var resetSpy = spyOn(bp.Runner, 'resetIterations'); + runs(function() { + bp.Runner.runState.numSamples = 8; + bp.Runner.setIterations(10); + bp.Runner.runAllTests(doneSpy); + }); + + waitsFor(function() { + return doneSpy.callCount; + }, 'done to be called', 200); + + runs(function() { + expect(bp.Report.timesPerAction.fakeStep.testTime.history.length).toBe(8); + }); + }); + }); + + + describe('.loopBenchmark()', function() { + var runAllTestsSpy, btn; + beforeEach(function() { + runAllTestsSpy = spyOn(bp.Runner, 'runAllTests'); + bp.Document.loopBtn = document.createElement('button'); + }); + + it('should call runAllTests if iterations does not start at greater than -1', function() { + bp.Runner.runState.iterations = 0; + bp.Runner.loopBenchmark(); + expect(runAllTestsSpy).toHaveBeenCalled(); + expect(runAllTestsSpy.callCount).toBe(1); + }); + + + it('should not call runAllTests if iterations is already -1', function() { + runs(function() { + bp.Runner.runState.iterations = -1; + bp.Runner.loopBenchmark(); + }); + + waits(1); + + runs(function() { + expect(runAllTestsSpy).not.toHaveBeenCalled(); + }); + }); + + + it('should not call runAllTests if iterations is less than -1', function() { + runs(function() { + bp.Runner.runState.iterations = -50; + bp.Runner.loopBenchmark(); + }); + + waits(1); + + runs(function() { + expect(runAllTestsSpy).not.toHaveBeenCalled(); + }); + }); + + + it('should set the button text to "Pause" while iterating', function() { + bp.Runner.runState.iterations = 0; + bp.Runner.loopBenchmark(); + expect(bp.Document.loopBtn.innerText).toBe('Pause'); + }); + + + it('should set the button text to "Loop" while iterating', function() { + bp.Runner.runState.iterations = -1; + bp.Runner.loopBenchmark(); + expect(bp.Document.loopBtn.innerText).toBe('Loop'); + }); + + + it('should set the runState -1 iterations', function() { + var spy = spyOn(bp.Runner, 'setIterations'); + bp.Runner.runState.iterations = 0; + bp.Runner.loopBenchmark(); + expect(spy).toHaveBeenCalledWith(-1); + }); + + + it('should set the iterations to 0 if iterations is already -1', function() { + bp.Runner.runState.iterations = -1; + bp.Runner.loopBenchmark(); + expect(bp.Runner.runState.iterations).toBe(0); + }); + }); + + + describe('.onceBenchmark()', function() { + var runAllTestsSpy; + beforeEach(function() { + bp.Document.onceBtn = document.createElement('button'); + runAllTestsSpy = spyOn(bp.Runner, 'runAllTests'); + }); + + it('should call runAllTests', function() { + expect(runAllTestsSpy.callCount).toBe(0); + bp.Runner.onceBenchmark(); + expect(runAllTestsSpy).toHaveBeenCalled(); + }); + + + it('should set the button text to "..."', function() { + expect(runAllTestsSpy.callCount).toBe(0); + bp.Runner.onceBenchmark(); + expect(bp.Document.onceBtn.innerText).toBe('...'); + }); + + + it('should set the text back to Once when done running test', function() { + expect(bp.Document.onceBtn.innerText).not.toBe('Once'); + bp.Runner.onceBenchmark(); + var done = runAllTestsSpy.calls[0].args[0]; + done(); + expect(bp.Document.onceBtn.innerText).toBe('Once'); + }); + }); + + + describe('.twentyFiveBenchmark()', function() { + var runAllTestsSpy; + beforeEach(function() { + bp.Document.twentyFiveBtn = document.createElement('button'); + runAllTestsSpy = spyOn(bp.Runner, 'runAllTests'); + }); + + + it('should set the runState to25 iterations', function() { + var spy = spyOn(bp.Runner, 'setIterations'); + bp.Runner.twentyFiveBenchmark(); + expect(spy).toHaveBeenCalledWith(25); + }); + + + it('should change the button text to "Looping..."', function() { + expect(bp.Document.twentyFiveBtn.innerText).not.toBe('Looping...'); + bp.Runner.twentyFiveBenchmark(); + expect(bp.Document.twentyFiveBtn.innerText).toBe('Looping...'); + }); + + + it('should call runAllTests', function() { + bp.Runner.twentyFiveBenchmark(); + expect(runAllTestsSpy).toHaveBeenCalled(); + }); + + + it('should pass runAllTests a third argument specifying times to ignore', function() { + bp.Runner.twentyFiveBenchmark(); + expect(runAllTestsSpy.calls[0].args[1]).toBe(5); + }); + }); + }); + + + describe('.Report', function() { + describe('.calcStats()', function() { + beforeEach(function() { + bp.steps = [mockStep]; + bp.Runner.runState = { + numSamples: 5, + iterations: 5, + recentResult: { + fakeStep: { + testTime: 5, + gcTime: 2, + recentGarbagePerStep: 200, + recentRetainedMemoryPerStep: 100 + } + } + }; + bp.Report.timesPerAction = { + fakeStep: { + testTime: { + history: [3,7] + }, + garbageCount: { + history: [50,50] + }, + retainedCount: { + history: [25,25] + }, + gcTime: { + recent: 3, + history: [1,3] + }, + nextEntry: 2 + }, + }; + }); + + + it('should set the most recent time for each step to the next entry', function() { + bp.Report.calcStats(); + expect(bp.Report.timesPerAction.fakeStep.testTime.history[2]).toBe(5); + bp.Runner.runState.recentResult.fakeStep.testTime = 25; + bp.Report.calcStats(); + expect(bp.Report.timesPerAction.fakeStep.testTime.history[3]).toBe(25); + }); + + + it('should return an string report', function() { + expect(typeof bp.Report.calcStats()).toBe('string'); + }); + }); + + + describe('.rightSizeTimes()', function() { + it('should make remove the left side of the input if longer than numSamples', function() { + bp.Runner.runState.numSamples = 3; + expect(bp.Report.rightSizeTimes([0,1,2,3,4,5,6])).toEqual([4,5,6]); + }); + + + it('should return the whole list if shorter than or equal to numSamples', function() { + bp.Runner.runState.numSamples = 7; + expect(bp.Report.rightSizeTimes([0,1,2,3,4,5,6])).toEqual([0,1,2,3,4,5,6]); + expect(bp.Report.rightSizeTimes([0,1,2,3,4,5])).toEqual([0,1,2,3,4,5]); + }); + }); + }); +}); \ No newline at end of file diff --git a/benchmark/web/ng-class/baseline.html b/benchmark/web/ng-class/baseline.html new file mode 100644 index 000000000..6a1940d61 --- /dev/null +++ b/benchmark/web/ng-class/baseline.html @@ -0,0 +1,1011 @@ +
+. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +
diff --git a/benchmark/web/ng-class/classy.html b/benchmark/web/ng-class/classy.html new file mode 100644 index 000000000..0b5c09ca8 --- /dev/null +++ b/benchmark/web/ng-class/classy.html @@ -0,0 +1,1011 @@ +
+ . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . + . +
diff --git a/benchmark/web/ng-class/index.dart b/benchmark/web/ng-class/index.dart new file mode 100644 index 000000000..44175348c --- /dev/null +++ b/benchmark/web/ng-class/index.dart @@ -0,0 +1,90 @@ +import 'package:di/di.dart'; +import 'package:angular/angular.dart'; +import 'package:angular/core_dom/module_internal.dart'; +import 'package:angular/application_factory.dart'; +import 'package:angular/change_detection/ast_parser.dart'; + +import 'dart:html'; +import 'dart:math'; +import 'dart:js' as js; + +@Component( + selector: 'classy', + templateUrl: 'classy.html') +class Classy { +} + +@Component( + selector: 'baseline', + templateUrl: 'baseline.html') +class Baseline { +} + +@Decorator( + selector: '[silly-class]' +) +class SillyClass { + + final NgElement element; + + SillyClass(this.element); + + @NgOneWay('silly-class') + set className(value) { + element.addClass(value); + } +} + +// Main function runs the benchmark. +main() { + var cleanup, createDom; + + var module = new Module() + ..type(Classy) + ..type(Baseline) + ..type(SillyClass) + ..bind(CompilerConfig, toValue: new CompilerConfig.withOptions(elementProbeEnabled: false)); + + var injector = applicationFactory().addModule(module).run(); + assert(injector != null); + + // Set up ASTs + var parser = injector.get(ASTParser); + VmTurnZone zone = injector.get(VmTurnZone); + Scope scope = injector.get(Scope); + + scope.context['initData'] = { + "value": "top", + "right": { + "value": "right" + }, + "left": { + "value": "left" + } + }; + + buildTree(maxDepth, values, curDepth) { + if (maxDepth == curDepth) return {}; + return { + "value": values[curDepth], + "right": buildTree(maxDepth, values, curDepth+1), + "left": buildTree(maxDepth, values, curDepth+1) + + }; + } + cleanup = (_) => zone.run(() { + scope.context['running'] = false; + }); + + var count = 0; + createDom = (_) => zone.run(() { + scope.context['running'] = true; + }); + + js.context['benchmarkSteps'].add(new js.JsObject.jsify({ + "name": "cleanup", "fn": new js.JsFunction.withThis(cleanup) + })); + js.context['benchmarkSteps'].add(new js.JsObject.jsify({ + "name": "createDom", "fn": new js.JsFunction.withThis(createDom) + })); +} diff --git a/benchmark/web/ng-class/index.html b/benchmark/web/ng-class/index.html new file mode 100644 index 000000000..60c5fb97b --- /dev/null +++ b/benchmark/web/ng-class/index.html @@ -0,0 +1,138 @@ + + + TreeComponent Benchmark + + + + + + + + + +
+
+
+
+ +
+
+
+ + +
+
+
+ + + +
+
+ +
+
+
+
+ test time (ms) +
+
+ gc time (ms) +
+
+ garbage (KB) +
+
+ retained memory (KB) +
+
+
+
+
+ + +
+
+
+

+ NgClass benchmark +

+ +
NgClass:
+
Baseline:
+ + + + + + +
{{useNgClass}}
+
+ + diff --git a/benchmark/web/tree.dart b/benchmark/web/tree.dart index 81d3e8b13..2710b73f7 100644 --- a/benchmark/web/tree.dart +++ b/benchmark/web/tree.dart @@ -2,6 +2,7 @@ import 'package:di/di.dart'; import 'package:angular/angular.dart'; import 'package:angular/core_dom/module_internal.dart'; import 'package:angular/application_factory.dart'; +import 'package:angular/change_detection/ast_parser.dart'; import 'dart:html'; import 'dart:math'; @@ -53,7 +54,7 @@ class NgFreeTree implements ShadowRootAware { } Element newFreeTree(tree) { - var elt = new Element.tag('ng-fre-tree'); + var elt = new Element.tag('ng-free-tree'); var root = elt.createShadowRoot(); var s = new SpanElement(); @@ -74,12 +75,13 @@ class NgFreeTree implements ShadowRootAware { } updateElement(root, tree) { - // Not quite acurate + // Not quite accurate root.innerHtml = ''; root.append(newFreeTree(tree)); } } +var treeValueAST, treeRightNotNullAST, treeLeftNotNullAST, treeRightAST, treeLeftAST, treeAST; /** * A baseline version of TreeComponent which uses Angular's Scope to * manage data. This version is setting up data binding so arbitrary @@ -112,32 +114,32 @@ class NgFreeTreeScoped implements ShadowRootAware { } Element newFreeTree(parentScope, treeExpr) { - var elt = new Element.tag('ng-fre-tree'); + var elt = new Element.tag('ng-free-tree-scoped'); var root = elt.createShadowRoot(); var scope = parentScope.createChild({}); - parentScope.watch(treeExpr, (v, _) { + parentScope.watchAST(treeExpr, (v, _) { scope.context['tree'] = v; }); var s = new SpanElement(); root.append(s); - scope.watch('tree.value', (v, _) { + scope.watchAST(treeValueAST, (v, _) { if (v != null) { s.text = " $v"; } }); - scope.watch('tree.right != null', (v, _) { + scope.watchAST(treeRightNotNullAST, (v, _) { if (v != true) return; s.append(new SpanElement() - ..append(newFreeTree(scope, 'tree.right'))); + ..append(newFreeTree(scope, treeRightAST))); }); - scope.watch('tree.left != null', (v, _) { + scope.watchAST(treeLeftNotNullAST, (v, _) { if (v != true) return; s.append(new SpanElement() - ..append(newFreeTree(scope, 'tree.left'))); + ..append(newFreeTree(scope, treeLeftAST))); }); return elt; @@ -145,14 +147,14 @@ class NgFreeTreeScoped implements ShadowRootAware { Scope treeScope; updateElement(root, tree) { - // Not quite acurate + // Not quite accurate if (treeScope != null) { treeScope.destroy(); } treeScope = scope.createChild({}); treeScope.context['tree'] = tree; root.innerHtml = ''; - root.append(newFreeTree(treeScope, 'tree')); + root.append(newFreeTree(treeScope, treeAST)); } } @@ -170,34 +172,34 @@ class FreeTreeClass { Scope parentScope; FreeTreeClass(this.parentScope, treeExpr) { - parentScope.watch(treeExpr, (v, _) { + parentScope.watchAST(treeExpr, (v, _) { tree = v; }); } Element element() { - var elt = new Element.tag('ng-fre-tree'); + var elt = new Element.tag('ng-free-tree'); var root = elt.createShadowRoot(); var scope = parentScope.createChild(this); var s = new SpanElement(); root.append(s); - scope.watch('tree.value', (v, _) { + scope.watchAST(treeValueAST, (v, _) { if (v != null) { s.text = " $v"; } }); - scope.watch('tree.right != null', (v, _) { + scope.watchAST(treeRightNotNullAST, (v, _) { if (v != true) return; s.append(new SpanElement() - ..append(new FreeTreeClass(scope, 'tree.right').element())); + ..append(new FreeTreeClass(scope, treeRightAST).element())); }); - scope.watch('tree.left != null', (v, _) { + scope.watchAST(treeLeftNotNullAST, (v, _) { if (v != true) return; s.append(new SpanElement() - ..append(new FreeTreeClass(scope, 'tree.left').element())); + ..append(new FreeTreeClass(scope, treeLeftAST).element())); }); return elt; @@ -230,14 +232,14 @@ class NgFreeTreeClass implements ShadowRootAware { var treeScope; updateElement(root, tree) { - // Not quite acurate + // Not quite accurate if (treeScope != null) { treeScope.destroy(); } treeScope = scope.createChild({}); treeScope.context['tree'] = tree; root.innerHtml = ''; - root.append(new FreeTreeClass(treeScope, 'tree').element()); + root.append(new FreeTreeClass(treeScope, treeAST).element()); } } @@ -247,15 +249,26 @@ main() { var cleanup, createDom; var module = new Module() - ..type(TreeComponent) - ..type(TreeUrlComponent) - ..type(NgFreeTree) - ..type(NgFreeTreeScoped) - ..type(NgFreeTreeClass) - ..factory(ScopeDigestTTL, (i) => new ScopeDigestTTL.value(15)); + ..bind(TreeComponent) + ..bind(TreeUrlComponent) + ..bind(NgFreeTree) + ..bind(NgFreeTreeScoped) + ..bind(NgFreeTreeClass) + ..bind(ScopeDigestTTL, toFactory: () => new ScopeDigestTTL.value(15), inject: []) + ..bind(CompilerConfig, toValue: new CompilerConfig.withOptions(elementProbeEnabled: false)); var injector = applicationFactory().addModule(module).run(); assert(injector != null); + + // Set up ASTs + var parser = injector.get(ASTParser); + treeValueAST = parser('tree.value'); + treeRightNotNullAST = parser('tree.right != null'); + treeLeftNotNullAST = parser('tree.left != null'); + treeRightAST = parser('tree.right'); + treeLeftAST = parser('tree.left'); + treeAST = parser('tree'); + VmTurnZone zone = injector.get(VmTurnZone); Scope scope = injector.get(Scope); diff --git a/benchmark/web/tree.html b/benchmark/web/tree.html index b7040040d..e245db00e 100644 --- a/benchmark/web/tree.html +++ b/benchmark/web/tree.html @@ -1,34 +1,149 @@ TreeComponent Benchmark + + + -
-

- Render a 2^9 prefix tree with AngularDart -

+
+
+
+
+ +
+
+
+ + +
+
+
+ + + +
+
-
Default:
-
From URL:
-
Baseline:
-
Baseline + scope:
-
Baseline + class:
- +
+
+
+
+ test time (ms) +
+
+ gc time (ms) +
+
+ garbage (KB) +
+
+ retained memory (KB) +
+
+
+
+
- - + +
+
+
+

+ Render a 2^9 prefix tree with AngularDart +

- - - +
Default:
+
From URL:
+
Baseline:
+
Baseline + scope:
+
Baseline + class:
- -
{{tree}}
- + + + + + + + + +
{{tree}}
+
+ diff --git a/benchmark/web/underscore.js b/benchmark/web/underscore.js new file mode 100644 index 000000000..9a4cabecf --- /dev/null +++ b/benchmark/web/underscore.js @@ -0,0 +1,1343 @@ +// Underscore.js 1.6.0 +// http://underscorejs.org +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.6.0'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return obj; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, length = obj.length; i < length; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; + } + } + return obj; + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results.push(iterator.call(context, value, index, list)); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var result; + any(obj, function(value, index, list) { + if (predicate.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(predicate, context); + each(obj, function(value, index, list) { + if (predicate.call(context, value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, function(value, index, list) { + return !predicate.call(context, value, index, list); + }, context); + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + predicate || (predicate = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(predicate, context); + each(obj, function(value, index, list) { + if (!(result = result && predicate.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, predicate, context) { + predicate || (predicate = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(predicate, context); + each(obj, function(value, index, list) { + if (result || (result = predicate.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + return any(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matches(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matches(attrs)); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + var result = -Infinity, lastComputed = -Infinity; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + if (computed > lastComputed) { + result = value; + lastComputed = computed; + } + }); + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + var result = Infinity, lastComputed = Infinity; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + if (computed < lastComputed) { + result = value; + lastComputed = computed; + } + }); + return result; + }; + + // Shuffle an array, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (obj.length !== +obj.length) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + if (value == null) return _.identity; + if (_.isFunction(value)) return value; + return _.property(value); + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, iterator, context) { + iterator = lookupIterator(iterator); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iterator, context) { + var result = {}; + iterator = lookupIterator(iterator); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, key, value) { + _.has(result, key) ? result[key].push(value) : result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, key, value) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, key) { + _.has(result, key) ? result[key]++ : result[key] = 1; + }); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) return array[0]; + if (n < 0) return []; + return slice.call(array, 0, n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) return array[array.length - 1]; + return slice.call(array, Math.max(array.length - n, 0)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } + each(input, function(value) { + if (_.isArray(value) || _.isArguments(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Split an array into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(array, predicate) { + var pass = [], fail = []; + each(array, function(elem) { + (predicate(elem) ? pass : fail).push(elem); + }); + return [pass, fail]; + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(_.flatten(arguments, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.contains(other, item); + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var length = _.max(_.pluck(arguments, 'length').concat(0)); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(arguments, '' + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, length = list.length; i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, length = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < length; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(length); + + while(idx < length) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + return function() { + var position = 0; + var args = boundArgs.slice(); + for (var i = 0, length = args.length; i < length; i++) { + if (args[i] === _) args[i] = arguments[position++]; + } + while (position < arguments.length) args.push(arguments[position++]); + return func.apply(this, args); + }; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) throw new Error('bindAll must be passed function names'); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + options || (options = {}); + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = new Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = new Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor)) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + _.constant = function(value) { + return function () { + return value; + }; + }; + + _.property = function(key) { + return function(obj) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of `key:value` pairs. + _.matches = function(attrs) { + return function(obj) { + if (obj === attrs) return true; //avoid comparing an object to itself. + for (var key in attrs) { + if (attrs[key] !== obj[key]) + return false; + } + return true; + } + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(Math.max(0, n)); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { return new Date().getTime(); }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property) { + if (object == null) return void 0; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}).call(this); diff --git a/bin/parser_generator_for_spec.dart b/bin/parser_generator_for_spec.dart index 8aeca334c..9e8da6896 100644 --- a/bin/parser_generator_for_spec.dart +++ b/bin/parser_generator_for_spec.dart @@ -1,15 +1,19 @@ import 'dart:io' as io; import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; +import 'package:angular/cache/module.dart'; +import 'package:angular/core/parser/lexer.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/tools/parser_getter_setter/generator.dart'; main(arguments) { - Module module = new Module()..bind(Parser, toImplementation: DynamicParser); + Module module = new Module() + ..bind(Lexer) + ..bind(ParserGetterSetter) + ..bind(Parser, toImplementation: DynamicParser) + ..install(new CacheModule()); module.bind(ParserBackend, toImplementation: DartGetterSetterGen); - Injector injector = new DynamicInjector(modules: [module], - allowImplicitInjection: true); + Injector injector = new ModuleInjector([module]); // List generated using: // node node_modules/karma/bin/karma run | grep -Eo ":XNAY:.*:XNAY:" | sed -e 's/:XNAY://g' | sed -e "s/^/'/" | sed -e "s/$/',/" | sort | uniq > missing_expressions @@ -96,6 +100,8 @@ main(arguments) { 'map["square"] = 6', 'method', 'method()', + 'causeException', + 'causeException()', 'notAFn()', 'notmixed', 'obj[0].name=1', diff --git a/bugs.txt b/bugs.txt deleted file mode 100644 index 403e8516b..000000000 --- a/bugs.txt +++ /dev/null @@ -1,3 +0,0 @@ -dart-injector: should throw error rather then injecting null -dart: Stack trace is not propegated on mirror invocations. -karma: Dart syntax error causes the runner to hang. diff --git a/example/bower.json b/example/bower.json new file mode 100644 index 000000000..df727d27d --- /dev/null +++ b/example/bower.json @@ -0,0 +1,24 @@ +{ + "name": "paper-example", + "version": "0.0.1", + "homepage": "https://github.com/angular/angular.dart", + "authors": [ + "James deBoer " + ], + "description": "Paper with AngularDart", + "main": "web/index.html", + "license": "MIT", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "polymer": "Polymer/polymer#~0.3.4", + "paper-elements": "Polymer/paper-elements#~0.3.4", + "core-elements": "Polymer/core-elements#~0.3.4" + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 0f7d672c3..e83147353 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -4,7 +4,7 @@ packages: analyzer: description: analyzer source: hosted - version: "0.13.6" + version: "0.18.0" angular: description: path: ".." @@ -26,7 +26,7 @@ packages: code_transformers: description: code_transformers source: hosted - version: "0.1.3" + version: "0.1.6" collection: description: collection source: hosted @@ -34,7 +34,7 @@ packages: di: description: di source: hosted - version: "0.0.40" + version: "2.0.1" html5lib: description: html5lib source: hosted @@ -42,7 +42,7 @@ packages: intl: description: intl source: hosted - version: "0.9.9" + version: "0.8.10+4" logging: description: logging source: hosted @@ -51,6 +51,10 @@ packages: description: matcher source: hosted version: "0.10.0" + meta: + description: meta + source: hosted + version: "0.8.8" mock: description: mock source: hosted @@ -58,7 +62,7 @@ packages: path: description: path source: hosted - version: "1.1.0" + version: "1.2.1" perf_api: description: perf_api source: hosted @@ -66,7 +70,7 @@ packages: route_hierarchical: description: route_hierarchical source: hosted - version: "0.4.20" + version: "0.4.21" source_maps: description: source_maps source: hosted @@ -75,6 +79,10 @@ packages: description: stack_trace source: hosted version: "0.9.3+1" + typed_mock: + description: typed_mock + source: hosted + version: "0.0.4" unittest: description: unittest source: hosted diff --git a/example/web/animation/repeat_demo.dart b/example/web/animation/repeat_demo.dart index 5857d72fe..6c0f9b446 100644 --- a/example/web/animation/repeat_demo.dart +++ b/example/web/animation/repeat_demo.dart @@ -2,6 +2,7 @@ part of animation; @Component( selector: 'repeat-demo', + useShadowDom: false, template: '''
diff --git a/example/web/animation/visibility_demo.dart b/example/web/animation/visibility_demo.dart index 5dee51f2a..2d1a7c688 100644 --- a/example/web/animation/visibility_demo.dart +++ b/example/web/animation/visibility_demo.dart @@ -17,7 +17,7 @@ part of animation;
''', publishAs: 'ctrl', - applyAuthorStyles: true) + useShadowDom: false) class VisibilityDemo { bool visible = false; } diff --git a/example/web/bower_components b/example/web/bower_components new file mode 120000 index 000000000..84cb58a3f --- /dev/null +++ b/example/web/bower_components @@ -0,0 +1 @@ +../bower_components/ \ No newline at end of file diff --git a/example/web/hello_world.dart b/example/web/hello_world.dart index ab8953f72..871164ace 100644 --- a/example/web/hello_world.dart +++ b/example/web/hello_world.dart @@ -6,6 +6,7 @@ import 'package:angular/application_factory.dart'; publishAs: 'ctrl') class HelloWorld { String name = "world"; + String color = "#aaaaaa"; } main() { diff --git a/example/web/hello_world.html b/example/web/hello_world.html index 705607d48..addcb05b2 100644 --- a/example/web/hello_world.html +++ b/example/web/hello_world.html @@ -5,8 +5,9 @@ -

Hello {{ctrl.name}}!

-name: +

Hello {{ctrl.name}}!

+Name: +Color: diff --git a/example/web/index.html b/example/web/index.html index c7a6b6860..760fd9e06 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -6,6 +6,7 @@
  • hello_world.html
  • todo.html
  • shadow_dom_components.html
  • +
  • paper.html
  • diff --git a/example/web/paper.dart b/example/web/paper.dart new file mode 100644 index 000000000..039629dad --- /dev/null +++ b/example/web/paper.dart @@ -0,0 +1,13 @@ +import 'package:angular/angular.dart'; +import 'package:angular/application_factory.dart'; + + +main() { + var injector = applicationFactory() + .run(); + var scope = injector.get(Scope); + scope.context['text'] = "Hello future"; + scope.context['max'] = 20; + scope.context['curValue'] = 12; + scope.apply(); +} diff --git a/example/web/paper.html b/example/web/paper.html new file mode 100644 index 000000000..167b712fb --- /dev/null +++ b/example/web/paper.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + +

    Polymer components inside a AngularDart app

    +

    Property binding: paper-progress

    +

    This is a simple component that doesn't generate events; the only things that is required is property binding

    +

    The max ({{max}}) and value ({{curValue}}) properties are bound through bind-* semantics

    + +

    Text from Angular: {{text}}

    + +
    + +
    + +

    + +

    + +

    Events: paper-checkbox

    +

    The checkbox will generate an event every time the value is changed

    +

    AngularDart can listen to these events through the on-* syntax

    + +
    + +
    +

    Every the value changes, the curValue ({{curValue}}) scope variable will update

    + +

    Two-way binding: paper-slider

    +

    The slide is bound to the curValue scope variable ({{curValue}})

    + +
    + +
    + + diff --git a/karma-perf.conf.js b/karma-perf.conf.js index 174dee106..e3a3b4e8f 100644 --- a/karma-perf.conf.js +++ b/karma-perf.conf.js @@ -19,12 +19,13 @@ module.exports = function(config) { autoWatch: false, - // If browser does not capture in given timeout [ms], kill it - captureTimeout: 5000, + // If browser does not capture in given timeout [ms], kill it. Note long time is for dart2js. + captureTimeout: 120000, plugins: [ 'karma-dart', 'karma-chrome-launcher', + 'karma-sauce-launcher', 'karma-script-launcher', 'karma-junit-reporter', '../../../karma-parser-getter-setter' diff --git a/karma.conf.js b/karma.conf.js index e1a1ed09f..3797fa676 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -10,10 +10,14 @@ module.exports = function(config) { files: [ 'packages/web_components/platform.js', 'packages/web_components/dart_support.js', + 'test/core_dom/web_components_support.js', 'test/*.dart', 'test/**/*_spec.dart', 'test/config/init_guinness.dart', - {pattern: '**/*.dart', watched: true, included: false, served: true}, + {pattern: 'packages/**/*.dart', watched: true, included: false, served: true}, + {pattern: 'test/**/*.dart', watched: true, included: false, served: true}, + {pattern: 'bin/**/*.dart', watched: true, included: false, served: true}, + {pattern: 'lib/**/*.dart', watched: true, included: false, served: true}, 'packages/browser/dart.js' ], @@ -26,13 +30,14 @@ module.exports = function(config) { autoWatch: false, // If browser does not capture in given timeout [ms], kill it - captureTimeout: 20000, + captureTimeout: 120000, // Time for dart2js to run on Travis... [ms] browserNoActivityTimeout: 1500000, plugins: [ 'karma-dart', 'karma-chrome-launcher', + 'karma-sauce-launcher', 'karma-firefox-launcher', 'karma-script-launcher', 'karma-junit-reporter', @@ -44,8 +49,16 @@ module.exports = function(config) { }, customLaunchers: { - ChromeNoSandbox: { base: 'Chrome', flags: ['--no-sandbox'] }, - // Only needed for Chrome 34. These features are enabled by default in Chrome 35. + 'SL_Chrome': { + base: 'SauceLabs', + browserName: 'chrome', + version: '35' + }, + 'SL_Firefox': { + base: 'SauceLabs', + browserName: 'firefox', + version: '30' + }, DartiumWithWebPlatform: { base: 'Dartium', flags: ['--enable-experimental-web-platform-features'] } @@ -60,6 +73,15 @@ module.exports = function(config) { junitReporter: { outputFile: 'test_out/unit.xml', suite: 'unit' + }, + sauceLabs: { + testName: 'AngularDart', + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, + startConnect: false, + options: { + 'selenium-version': '2.41.0', + 'max-duration': 2700 + } } }); }; diff --git a/lib/angular.dart b/lib/angular.dart index 8919c4262..d79b93b3d 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -5,9 +5,10 @@ export 'package:angular/application.dart'; export 'package:angular/core/module.dart'; export 'package:angular/directive/module.dart'; export 'package:angular/core/annotation.dart'; -export 'package:angular/introspection.dart'; +export 'package:angular/introspection.dart' hide + elementExpando, publishToJavaScript; export 'package:angular/formatter/module.dart'; export 'package:angular/routing/module.dart'; -export 'package:di/di.dart' hide lastKeyId; +export 'package:di/di.dart'; +export 'package:di/annotations.dart'; export 'package:route_hierarchical/client.dart' hide childRoute; - diff --git a/lib/animate/animation_optimizer.dart b/lib/animate/animation_optimizer.dart index 843e21947..5f0c54560 100644 --- a/lib/animate/animation_optimizer.dart +++ b/lib/animate/animation_optimizer.dart @@ -13,6 +13,10 @@ class AnimationOptimizer { final Map _animations = new Map(); + /** + * Toggle to disable all animations through this optimizer. + */ + bool animationsAllowed = true; final Map _alwaysAnimate = new Map(); final Map _alwaysAnimateChildren = new Map(); @@ -103,6 +107,9 @@ class AnimationOptimizer { * and [false] if the optimizer thinks that it should not execute. */ bool shouldAnimate(dom.Node node) { + if (!animationsAllowed) { + return false; + } bool alwaysAnimate = _alwaysAnimate[node]; if (alwaysAnimate != null) { return alwaysAnimate; diff --git a/lib/animate/css_animate.dart b/lib/animate/css_animate.dart index db5bebaee..ec4204b5a 100644 --- a/lib/animate/css_animate.dart +++ b/lib/animate/css_animate.dart @@ -25,6 +25,11 @@ class CssAnimate implements Animate { CssAnimate(this._runner, this._animationMap, this._optimizer); + bool get animationsAllowed => _optimizer.animationsAllowed; + void set animationsAllowed(bool allowed) { + _optimizer.animationsAllowed = allowed; + } + Animation addClass(dom.Element element, String cssClass) { if (!_optimizer.shouldAnimate(element)) { element.classes.add(cssClass); @@ -134,11 +139,11 @@ class CssAnimate implements Animate { @Injectable() class CssAnimationMap { final Map> cssAnimations - = new Map>(); + = new HashMap>(); void track(CssAnimation animation) { var animations = cssAnimations.putIfAbsent(animation.element, - () => {}); + () => new HashMap()); animations[animation.eventClass] = animation; } diff --git a/lib/animate/module.dart b/lib/animate/module.dart index 44a700613..5552cc72a 100644 --- a/lib/animate/module.dart +++ b/lib/animate/module.dart @@ -108,11 +108,13 @@ import 'package:angular/core_dom/dom_util.dart' as util; import 'package:logging/logging.dart'; import 'package:perf_api/perf_api.dart'; import 'package:di/di.dart'; +import 'package:di/annotations.dart'; @MirrorsUsed(targets: const [ 'angular.animate' ]) import 'dart:mirrors' show MirrorsUsed; +import 'dart:collection'; part 'animations.dart'; part 'animation_loop.dart'; diff --git a/lib/application.dart b/lib/application.dart index 2f80a8e4d..5c6cf10bf 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -73,15 +73,18 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:di/di.dart'; import 'package:angular/angular.dart'; import 'package:angular/perf/module.dart'; +import 'package:angular/cache/module.dart'; +import 'package:angular/cache/js_cache_register.dart'; import 'package:angular/core/module_internal.dart'; import 'package:angular/core/registry.dart'; import 'package:angular/core_dom/module_internal.dart'; import 'package:angular/directive/module.dart'; import 'package:angular/formatter/module_internal.dart'; import 'package:angular/routing/module.dart'; -import 'package:angular/introspection_js.dart'; +import 'package:angular/introspection.dart'; import 'package:angular/core_dom/static_keys.dart'; +import 'package:angular/core_dom/directive_injector.dart'; /** * This is the top level module which describes all Angular components, @@ -94,14 +97,16 @@ import 'package:angular/core_dom/static_keys.dart'; */ class AngularModule extends Module { AngularModule() { + DirectiveInjector.initUID(); + install(new CacheModule()); install(new CoreModule()); install(new CoreDomModule()); install(new DirectiveModule()); install(new FormatterModule()); + install(new JsCacheModule()); install(new PerfModule()); install(new RoutingModule()); - bind(MetadataExtractor); bind(Expando, toValue: elementExpando); } } @@ -148,7 +153,7 @@ abstract class Application { modules.add(ngModule); ngModule..bind(VmTurnZone, toValue: zone) ..bind(Application, toValue: this) - ..bind(dom.Node, toFactory: (i) => i.getByKey(new Key(Application)).element); + ..bind(dom.Node, toFactory: (Application app) => app.element, inject: [Application]); } /** @@ -167,11 +172,15 @@ abstract class Application { var rootElements = [element]; Injector injector = createInjector(); ExceptionHandler exceptionHandler = injector.getByKey(EXCEPTION_HANDLER_KEY); + // Publish cache register interface + injector.getByKey(JS_CACHE_REGISTER_KEY); initializeDateFormatting(null, null).then((_) { try { - var compiler = injector.getByKey(COMPILER_KEY); - var viewFactory = compiler(rootElements, injector.getByKey(DIRECTIVE_MAP_KEY)); - viewFactory(injector, rootElements); + Compiler compiler = injector.getByKey(COMPILER_KEY); + DirectiveMap directiveMap = injector.getByKey(DIRECTIVE_MAP_KEY); + RootScope rootScope = injector.getByKey(ROOT_SCOPE_KEY); + ViewFactory viewFactory = compiler(rootElements, directiveMap); + viewFactory(rootScope, injector.get(DirectiveInjector), rootElements); } catch (e, s) { exceptionHandler(e, s); } @@ -184,5 +193,5 @@ abstract class Application { * Creates an injector function that can be used for retrieving services as well as for * dependency injection. */ - Injector createInjector(); + Injector createInjector() => new ModuleInjector(modules); } diff --git a/lib/application_factory.dart b/lib/application_factory.dart index 25b95cd40..0baa76921 100644 --- a/lib/application_factory.dart +++ b/lib/application_factory.dart @@ -9,7 +9,6 @@ */ library angular.app.factory; -import 'package:di/dynamic_injector.dart'; import 'package:angular/angular.dart'; import 'package:angular/core/registry.dart'; import 'package:angular/core/parser/parser.dart' show ClosureMap; @@ -64,8 +63,6 @@ class _DynamicApplication extends Application { ..bind(FieldGetterFactory, toImplementation: DynamicFieldGetterFactory) ..bind(ClosureMap, toImplementation: DynamicClosureMap); } - - Injector createInjector() => new DynamicInjector(modules: modules); } /** diff --git a/lib/application_factory_static.dart b/lib/application_factory_static.dart index 3379177ff..a44c3f490 100644 --- a/lib/application_factory_static.dart +++ b/lib/application_factory_static.dart @@ -30,8 +30,6 @@ */ library angular.app.factory.static; -import 'package:di/static_injector.dart'; -import 'package:di/di.dart' show TypeFactory, Injector; import 'package:angular/application.dart'; import 'package:angular/core/registry.dart'; import 'package:angular/core/parser/parser.dart'; @@ -46,9 +44,7 @@ export 'package:angular/change_detection/change_detection.dart' show FieldSetter; class _StaticApplication extends Application { - final Map typeFactories; - - _StaticApplication(Map this.typeFactories, + _StaticApplication( Map metadata, Map fieldGetters, Map fieldSetters, @@ -58,9 +54,6 @@ class _StaticApplication extends Application { ..bind(FieldGetterFactory, toValue: new StaticFieldGetterFactory(fieldGetters)) ..bind(ClosureMap, toValue: new StaticClosureMap(fieldGetters, fieldSetters, symbols)); } - - Injector createInjector() => - new StaticInjector(modules: modules, typeFactories: typeFactories); } /** @@ -81,20 +74,19 @@ class _StaticApplication extends Application { * becomes: * * main() { - * staticApplication(generated_static_injector.factories, - * generated_static_metadata.typeAnnotations, - * generated_static_expressions.getters, - * generated_static_expressions.setters, - * generated_static_expressions.symbols) - * .addModule(new Module()..bind(HelloWorldController)) - * .run(); + * staticApplication( + * generated_static_metadata.typeAnnotations, + * generated_static_expressions.getters, + * generated_static_expressions.setters, + * generated_static_expressions.symbols) + * .addModule(new Module()..bind(HelloWorldController)) + * .run(); * */ Application staticApplicationFactory( - Map typeFactories, Map metadata, Map fieldGetters, Map fieldSetters, Map symbols) { - return new _StaticApplication(typeFactories, metadata, fieldGetters, fieldSetters, symbols); + return new _StaticApplication(metadata, fieldGetters, fieldSetters, symbols); } diff --git a/lib/core/cache.dart b/lib/cache/cache.dart similarity index 90% rename from lib/core/cache.dart rename to lib/cache/cache.dart index 196a6a4ef..d6bfb2ef6 100644 --- a/lib/core/cache.dart +++ b/lib/cache/cache.dart @@ -1,4 +1,4 @@ -part of angular.core_internal; +part of angular.cache; class CacheStats { final int capacity; @@ -32,18 +32,23 @@ abstract class Cache { /** * Removes all entries from the cache. */ + @Deprecated('Use clear() instead') void removeAll(); int get capacity; + @Deprecated('Use length instead') int get size; CacheStats stats(); + + void clear() => removeAll(); + int get length => size; } /** * An unbounded cache. */ -class UnboundedCache implements Cache { - Map _entries = {}; +class UnboundedCache extends Cache { + Map _entries = new HashMap(); int _hits = 0; int _misses = 0; @@ -64,6 +69,8 @@ class UnboundedCache implements Cache { CacheStats stats() => new CacheStats(capacity, size, _hits, _misses); // Debugging helper. String toString() => "[$runtimeType: size=${_entries.length}, items=$_entries]"; + int get length => -1; + void clear() {} } diff --git a/lib/cache/cache_register.dart b/lib/cache/cache_register.dart new file mode 100644 index 000000000..713338a3a --- /dev/null +++ b/lib/cache/cache_register.dart @@ -0,0 +1,64 @@ +part of angular.cache; + +class CacheRegisterStats { + final String name; + int length; + + CacheRegisterStats(this.name); +} + +@Injectable() +class CacheRegister { + Map _caches = {}; + List _stats = null; + + /** + * Registers a cache with the CacheRegister. The [name] is used for in the stats as + * well as a key for [clear]. + */ + void registerCache(String name, cache) { + if (_caches.containsKey(name)) { + throw "Cache [$name] already registered"; + } + _caches[name] = cache; + + // The stats object needs to be updated. + _stats = null; + + } + + /** + * A list of caches and their sizes. + */ + List get stats { + if (_stats == null) { + _stats = []; + _caches.forEach((k, v) { + _stats.add(new CacheRegisterStats(k)); + }); + } + + _stats.forEach((CacheRegisterStats stat) { + stat.length = _caches[stat.name].length; + }); + return _stats; + } + + /** + * Clears one or all the caches. If [name] is omitted, all caches will be cleared. + * Otherwise, only the cache named [name] will be cleared. + */ + void clear([String name]) { + if (name == null) { + _caches.forEach((k, v) { + v.clear(); + }); + return; + } + var cache = _caches[name]; + if (cache == null) { + return; + } + _caches[name].clear(); + } +} diff --git a/lib/cache/js_cache_register.dart b/lib/cache/js_cache_register.dart new file mode 100644 index 000000000..ce54a499f --- /dev/null +++ b/lib/cache/js_cache_register.dart @@ -0,0 +1,54 @@ +library angular.cache.js; + +// This is a separate module since it depends on dart:js + +import 'dart:js' as js; +import 'package:di/di.dart'; +import 'package:di/annotations.dart'; +import 'package:angular/core/annotation_src.dart'; +import 'package:angular/cache/module.dart'; + +Key JS_CACHE_REGISTER_KEY = new Key(JsCacheRegister); + +/** + * Publishes an interface to the CacheRegister in Javascript. When installed, + * a 'ngCaches' object will be available in Javascript. + * + * ngCaches.sizes() returns a map of cache name -> number of entries in the cache + * ngCaches.dump() prints the cache information to the console + * ngCaches.clear(name) clears the cache named 'name', or if name is omitted, all caches. + */ +@Injectable() +class JsCacheRegister { + CacheRegister _caches; + + JsCacheRegister(CacheRegister this._caches) { + js.context['ngCaches'] = new js.JsObject.jsify({ + "sizes": new js.JsFunction.withThis(sizesAsMap), + "clear": new js.JsFunction.withThis((_, [name]) => _caches.clear(name)), + "dump": new js.JsFunction.withThis(dump) + }); + } + + void dump(_) { + var toPrint = ['Angular Cache Sizes:']; + _caches.stats.forEach((CacheRegisterStats stat) { + toPrint.add('${stat.name.padLeft(35)} ${stat.length}'); + }); + print(toPrint.join('\n')); + } + + js.JsObject sizesAsMap(_) { + var map = {}; + _caches.stats.forEach((CacheRegisterStats stat) { + map[stat.name] = stat.length; + }); + return new js.JsObject.jsify(map); + } +} + +class JsCacheModule extends Module { + JsCacheModule() { + bind(JsCacheRegister); + } +} diff --git a/lib/cache/module.dart b/lib/cache/module.dart new file mode 100644 index 000000000..6296700fd --- /dev/null +++ b/lib/cache/module.dart @@ -0,0 +1,19 @@ +library angular.cache; + +import 'dart:collection'; +import 'dart:async'; + +import 'package:di/di.dart'; +import 'package:di/annotations.dart'; + +part "cache.dart"; +part "cache_register.dart"; + +class CacheModule extends Module { + CacheModule() { + bind(CacheRegister); + } + CacheModule.withReflector(reflector): super.withReflector(reflector) { + bind(CacheRegister); + } +} diff --git a/lib/change_detection/ast.dart b/lib/change_detection/ast.dart index 80653a0ba..a18bc5b30 100644 --- a/lib/change_detection/ast.dart +++ b/lib/change_detection/ast.dart @@ -91,8 +91,7 @@ class PureFunctionAST extends AST { /** * SYNTAX: fn(arg0, arg1, ...) * - * Invoke a pure function. Pure means that the function has no state, and - * therefore it needs to be re-computed only if its args change. + * Invoke a (non-pure) function. */ class ClosureAST extends AST { final String name; diff --git a/lib/change_detection/ast_parser.dart b/lib/change_detection/ast_parser.dart index 9235b0b17..898faa9c3 100644 --- a/lib/change_detection/ast_parser.dart +++ b/lib/change_detection/ast_parser.dart @@ -2,6 +2,7 @@ library angular.change_detection.ast_parser; import 'dart:collection'; +import 'package:di/annotations.dart'; import 'package:angular/core/parser/syntax.dart' as syntax; import 'package:angular/core/parser/parser.dart'; import 'package:angular/core/formatter.dart'; @@ -193,7 +194,14 @@ _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]; +_operation_bracket(obj, key) { + if (obj != null && ( + obj is! List || (key is int && key >= 0 && key < obj.length))) { + return obj[key]; + } else { + return null; + } +} class ArrayFn extends FunctionApply { // TODO(misko): figure out why do we need to make a copy? diff --git a/lib/change_detection/dirty_checking_change_detector.dart b/lib/change_detection/dirty_checking_change_detector.dart index 306248e00..6d7c3e4f5 100644 --- a/lib/change_detection/dirty_checking_change_detector.dart +++ b/lib/change_detection/dirty_checking_change_detector.dart @@ -175,12 +175,12 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { DirtyCheckingRecord _recordAdd(DirtyCheckingRecord record) { DirtyCheckingRecord previous = _recordTail; - DirtyCheckingRecord next = previous == null ? null : previous._nextRecord; + DirtyCheckingRecord next = previous._nextRecord; record._nextRecord = next; record._prevRecord = previous; - if (previous != null) previous._nextRecord = record; + previous._nextRecord = record; if (next != null) next._prevRecord = record; _recordTail = record; @@ -194,7 +194,8 @@ class DirtyCheckingChangeDetectorGroup implements ChangeDetectorGroup { DirtyCheckingRecord previous = record._prevRecord; DirtyCheckingRecord next = record._nextRecord; - if (record == _recordHead && record == _recordTail) { + if (_recordHead == _recordTail) { + assert(record == _recordHead); // we are the last one, must leave marker behind. _recordHead = _recordTail = _marker; _marker._nextRecord = next; @@ -514,19 +515,10 @@ class DirtyCheckingRecord implements Record, WatchRecord { } var last = currentValue; - if (!identical(last, current)) { - if (last is String && current is String && - last == current) { - // 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 - currentValue = current; - } else if (last is num && last.isNaN && current is num && current.isNaN) { - // we need this for the compiled JavaScript since in JS NaN !== NaN. - } else { - previousValue = last; - currentValue = current; - return true; - } + if (!_looseIdentical(last, current)) { + previousValue = currentValue; + currentValue = current; + return true; } return false; } @@ -543,7 +535,7 @@ class DirtyCheckingRecord implements Record, WatchRecord { final Object _INITIAL_ = new Object(); class _MapChangeRecord implements MapChangeRecord { - final _records = new Map(); + final _records = new HashMap(); Map _map; Map get map => _map; @@ -620,14 +612,10 @@ class _MapChangeRecord implements MapChangeRecord { var newSeqRecord; if (oldSeqRecord != null && key == oldSeqRecord.key) { newSeqRecord = oldSeqRecord; - if (!identical(value, oldSeqRecord._currentValue)) { + if (!_looseIdentical(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); - } + _addToChanges(oldSeqRecord); } } else { seqChanged = true; @@ -871,11 +859,11 @@ class _CollectionChangeRecord implements CollectionChangeRecord { Iterable _iterable; int _length; - /// Keeps track of moved items. - DuplicateMap _movedItems = new DuplicateMap(); + /// Keeps track of the used records at any point in time (during & across `_check()` calls) + DuplicateMap _linkedRecords; - /// Keeps track of removed items. - DuplicateMap _removedItems = new DuplicateMap(); + /// Keeps track of the removed records at any point in time during `_check()` calls. + DuplicateMap _unlinkedRecords; ItemRecord _previousItHead; ItemRecord _itHead, _itTail; @@ -886,7 +874,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { void _revertToPreviousState() { if (!isDirty) return; - _movedItems.clear(); + if (_linkedRecords != null) _linkedRecords.clear(); ItemRecord prev; int i = 0; @@ -896,7 +884,9 @@ class _CollectionChangeRecord implements CollectionChangeRecord { record.currentIndex = record.previousIndex = i; record._prev = prev; if (prev != null) prev._next = prev._nextPrevious = record; - _movedItems.put(record); + + if (_linkedRecords == null) _linkedRecords = new DuplicateMap(); + _linkedRecords.put(record); } prev._next = null; @@ -953,7 +943,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { _length = list.length; for (int index = 0; index < _length; index++) { var item = list[index]; - if (record == null || !identical(item, record.item)) { + if (record == null || !_looseIdentical(record.item, item)) { record = mismatch(record, item, index); maybeDirty = true; } else if (maybeDirty) { @@ -965,7 +955,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } else { int index = 0; for (var item in collection) { - if (record == null || !identical(item, record.item)) { + if (record == null || !_looseIdentical(record.item, item)) { record = mismatch(record, item, index); maybeDirty = true; } else if (maybeDirty) { @@ -1035,19 +1025,6 @@ class _CollectionChangeRecord implements CollectionChangeRecord { * - [index] is the position of the item in the collection */ ItemRecord mismatch(ItemRecord record, item, int index) { - 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 as num).isNaN && record.item is num && (record.item as num).isNaN) { - // we need this for JavaScript since in JS NaN !== NaN. - return record; - } - } - // The previous record after which we will append the current one. ItemRecord previousRecord; @@ -1060,13 +1037,13 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } // Attempt to see if we have seen the item before. - record = _movedItems.get(item, index); + record = _linkedRecords == null ? null : _linkedRecords.get(item, index); if (record != null) { // We have seen this before, we need to move it forward in the collection. _moveAfter(record, previousRecord, index); } else { // Never seen it, check evicted list. - record = _removedItems.get(item); + record = _unlinkedRecords == null ? null : _unlinkedRecords.get(item); if (record != null) { // It is an item which we have evicted earlier: reinsert it back into the list. _reinsertAfter(record, previousRecord, index); @@ -1105,12 +1082,12 @@ class _CollectionChangeRecord implements CollectionChangeRecord { * of 'b' rather then switch 'a' with 'b' and then add 'a' at the end. */ ItemRecord verifyReinsertion(ItemRecord record, item, int index) { - ItemRecord reinsertRecord = _removedItems.get(item); + ItemRecord reinsertRecord = _unlinkedRecords == null ? null : _unlinkedRecords.get(item); if (reinsertRecord != null) { record = _reinsertAfter(reinsertRecord, record._prev, index); } else if (record.currentIndex != index) { record.currentIndex = index; - _addToMoves(record); + _addToMoves(record, index); } return record; } @@ -1127,7 +1104,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { _addToRemovals(_unlink(record)); record = nextRecord; } - _removedItems.clear(); + if (_unlinkedRecords != null) _unlinkedRecords.clear(); if (_additionsTail != null) _additionsTail._nextAdded = null; if (_movesTail != null) _movesTail._nextMoved = null; @@ -1136,7 +1113,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } ItemRecord _reinsertAfter(ItemRecord record, ItemRecord prevRecord, int index) { - _removedItems.remove(record); + if (_unlinkedRecords != null) _unlinkedRecords.remove(record); var prev = record._prevRemoved; var next = record._nextRemoved; @@ -1152,14 +1129,14 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } _insertAfter(record, prevRecord, index); - _addToMoves(record); + _addToMoves(record, index); return record; } ItemRecord _moveAfter(ItemRecord record, ItemRecord prevRecord, int index) { _unlink(record); _insertAfter(record, prevRecord, index); - _addToMoves(record); + _addToMoves(record, index); return record; } @@ -1198,7 +1175,9 @@ class _CollectionChangeRecord implements CollectionChangeRecord { prevRecord._next = record; } - _movedItems.put(record); + if (_linkedRecords == null) _linkedRecords = new DuplicateMap(); + _linkedRecords.put(record); + record.currentIndex = index; return record; } @@ -1206,7 +1185,7 @@ class _CollectionChangeRecord implements CollectionChangeRecord { ItemRecord _remove(ItemRecord record) => _addToRemovals(_unlink(record)); ItemRecord _unlink(ItemRecord record) { - _movedItems.remove(record); + if (_linkedRecords != null) _linkedRecords.remove(record); var prev = record._prev; var next = record._next; @@ -1228,8 +1207,11 @@ class _CollectionChangeRecord implements CollectionChangeRecord { return record; } - ItemRecord _addToMoves(ItemRecord record) { + ItemRecord _addToMoves(ItemRecord record, int toIndex) { assert(record._nextMoved == null); + + if (record.previousIndex == toIndex) return record; + if (_movesTail == null) { assert(_movesHead == null); _movesTail = _movesHead = record; @@ -1242,7 +1224,8 @@ class _CollectionChangeRecord implements CollectionChangeRecord { } ItemRecord _addToRemovals(ItemRecord record) { - _removedItems.put(record); + if (_unlinkedRecords == null) _unlinkedRecords = new DuplicateMap(); + _unlinkedRecords.put(record); record.currentIndex = null; record._nextRemoved = null; @@ -1320,39 +1303,22 @@ class _DuplicateItemRecordList { ItemRecord _head, _tail; /** - * Add the [record] before the [insertBefore] in the list of duplicates or at the end of the list - * when no [insertBefore] is specified. + * Append the [record] to the list of duplicates. * * Note: by design all records in the list of duplicates hold the save value in [record.item]. */ - void add(ItemRecord record, ItemRecord insertBefore) { - assert(insertBefore == null || insertBefore.item == record.item); + void add(ItemRecord record) { if (_head == null) { - /// pushing the first [ItemRecord] to the list - assert(insertBefore == null); _head = _tail = record; record._nextDup = null; record._prevDup = null; } else { - // adding a duplicate [ItemRecord] to the list - assert(record.item == _head.item); - if (insertBefore == null) { - _tail._nextDup = record; - record._prevDup = _tail; - record._nextDup = null; - _tail = record; - } else { - var prev = insertBefore._prevDup; - var next = insertBefore; - record._prevDup = prev; - record._nextDup = next; - if (prev == null) { - _head = record; - } else { - prev._nextDup = record; - } - next._prevDup = record; - } + assert(record.item == _head.item || + record.item is num && record.item.isNaN && _head.item is num && _head.item.isNaN); + _tail._nextDup = record; + record._prevDup = _tail; + record._nextDup = null; + _tail = record; } } @@ -1362,10 +1328,10 @@ class _DuplicateItemRecordList { ItemRecord record; for (record = _head; record != null; record = record._nextDup) { if ((afterIndex == null || afterIndex < record.currentIndex) && - identical(record.item, item)) { - return record; + _looseIdentical(record.item, item)) { + return record; + } } - } return null; } @@ -1406,10 +1372,16 @@ class _DuplicateItemRecordList { * The list of duplicates is implemented by [_DuplicateItemRecordList]. */ class DuplicateMap { - final map = {}; + static final _nanKey = const Object(); + final map = new HashMap(); - void put(ItemRecord record, [ItemRecord insertBefore = null]) { - map.putIfAbsent(record.item, () => new _DuplicateItemRecordList()).add(record, insertBefore); + void put(ItemRecord record) { + var key = _getKey(record.item); + _DuplicateItemRecordList duplicates = map[key]; + if (duplicates == null) { + duplicates = map[key] = new _DuplicateItemRecordList(); + } + duplicates.add(record); } /** @@ -1419,9 +1391,10 @@ class DuplicateMap { * Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a` then asking if we * have any more `a`s needs to return the last `a` not the first or second. */ - ItemRecord get(key, [int afterIndex]) { + ItemRecord get(value, [int afterIndex]) { + var key = _getKey(value); _DuplicateItemRecordList recordList = map[key]; - return recordList == null ? null : recordList.get(key, afterIndex); + return recordList == null ? null : recordList.get(value, afterIndex); } /** @@ -1430,10 +1403,11 @@ class DuplicateMap { * The list of duplicates also is removed from the map if it gets empty. */ ItemRecord remove(ItemRecord record) { - assert(map.containsKey(record.item)); - _DuplicateItemRecordList recordList = map[record.item]; + var key = _getKey(record.item); + assert(map.containsKey(key)); + _DuplicateItemRecordList recordList = map[key]; // Remove the list of duplicates when it gets empty - if (recordList.remove(record)) map.remove(record.item); + if (recordList.remove(record)) map.remove(key); return record; } @@ -1443,5 +1417,32 @@ class DuplicateMap { map.clear(); } + /// Required to handle num.NAN as a Map value + dynamic _getKey(value) => value is num && value.isNaN ? _nanKey : value; + String toString() => "DuplicateMap($map)"; } + +/** + * Returns whether the [dst] and [src] are loosely identical: + * * true when the value are identical, + * * true when both values are equal strings, + * * true when both values are NaN + * + * If both values are equal string, src is assigned to dst. + */ +bool _looseIdentical(dst, src) { + if (identical(dst, src)) return true; + + if (dst is String && src is String && dst == src) { + // 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 + dst = src; + return true; + } + + // we need this for JavaScript since in JS NaN !== NaN. + if (dst is num && (dst as num).isNaN && src is num && (src as num).isNaN) return true; + + return false; +} diff --git a/lib/change_detection/prototype_map.dart b/lib/change_detection/prototype_map.dart index 130444184..b324a12a1 100644 --- a/lib/change_detection/prototype_map.dart +++ b/lib/change_detection/prototype_map.dart @@ -2,7 +2,7 @@ part of angular.watch_group; class PrototypeMap implements Map { final Map prototype; - final Map self = new Map(); + final Map self = new HashMap(); PrototypeMap(this.prototype); @@ -34,4 +34,6 @@ class PrototypeMap implements Map { } // todo(vbe) include prototype ? V putIfAbsent(key, fn) => self.putIfAbsent(key, fn); + + toString() => self.toString(); } diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index 884491d06..49e3e7360 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -1,6 +1,7 @@ library angular.watch_group; import 'package:angular/change_detection/change_detection.dart'; +import 'dart:collection'; part 'linked_list.dart'; part 'ast.dart'; @@ -119,7 +120,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { : id = '', _rootGroup = null, _parentWatchGroup = null, - _cache = new Map>() + _cache = new HashMap>() { _marker.watchGrp = this; _evalWatchTail = _evalWatchHead = _marker; @@ -138,9 +139,10 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { } Watch watch(AST expression, ReactionFn reactionFn) { - WatchRecord<_Handler> watchRecord = - _cache.putIfAbsent(expression.expression, - () => expression.setupWatch(this)); + WatchRecord<_Handler> watchRecord = _cache[expression.expression]; + if (watchRecord == null) { + _cache[expression.expression] = watchRecord = expression.setupWatch(this); + } return watchRecord.handler.addReactionFn(reactionFn); } @@ -159,8 +161,10 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { _fieldCost++; fieldHandler.watchRecord = watchRecord; - WatchRecord<_Handler> lhsWR = _cache.putIfAbsent(lhs.expression, - () => lhs.setupWatch(this)); + WatchRecord<_Handler> lhsWR = _cache[lhs.expression]; + if (lhsWR == null) { + lhsWR = _cache[lhs.expression] = lhs.setupWatch(this); + } // We set a field forwarding handler on LHS. This will allow the change // objects to propagate to the current WatchRecord. @@ -176,8 +180,10 @@ 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[ast.expression]; + if (astWR == null) { + astWR = _cache[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. @@ -228,8 +234,10 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { invokeHandler.watchRecord = evalWatchRecord; if (lhsAST != null) { - var lhsWR = _cache.putIfAbsent(lhsAST.expression, - () => lhsAST.setupWatch(this)); + var lhsWR = _cache[lhsAST.expression]; + if (lhsWR == null) { + lhsWR = _cache[lhsAST.expression] = lhsAST.setupWatch(this); + } lhsWR.handler.addForwardHandler(invokeHandler); invokeHandler.acceptValue(lhsWR.currentValue); } @@ -237,8 +245,10 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { // Convert the args from AST to WatchRecords for (var i = 0; i < argsAST.length; i++) { var ast = argsAST[i]; - WatchRecord<_Handler> record = - _cache.putIfAbsent(ast.expression, () => ast.setupWatch(this)); + WatchRecord<_Handler> record = _cache[ast.expression]; + if (record == null) { + record = _cache[ast.expression] = ast.setupWatch(this); + } _ArgHandler handler = new _PositionalArgHandler(this, evalWatchRecord, i); _ArgHandlerList._add(invokeHandler, handler); record.handler.addForwardHandler(handler); @@ -246,8 +256,10 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { } namedArgsAST.forEach((Symbol name, AST ast) { - WatchRecord<_Handler> record = _cache.putIfAbsent(ast.expression, - () => ast.setupWatch(this)); + WatchRecord<_Handler> record = _cache[ast.expression]; + if (record == null) { + record = _cache[ast.expression] = ast.setupWatch(this); + } _ArgHandler handler = new _NamedArgHandler(this, evalWatchRecord, name); _ArgHandlerList._add(invokeHandler, handler); record.handler.addForwardHandler(handler); @@ -289,7 +301,7 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { this, _changeDetector.newGroup(), context == null ? this.context : context, - >{}, + new HashMap>(), _rootGroup == null ? this : _rootGroup); _WatchGroupList._add(this, childGroup); var marker = childGroup._marker; @@ -660,10 +672,11 @@ abstract class _ArgHandler extends _Handler { } class _PositionalArgHandler extends _ArgHandler { + static final List _ARGS = new List.generate(20, (index) => 'arg[$index]'); final int index; _PositionalArgHandler(WatchGroup watchGrp, _EvalWatchRecord record, int index) : this.index = index, - super(watchGrp, 'arg[$index]', record); + super(watchGrp, _ARGS[index], record); void acceptValue(object) { watchRecord.dirtyArgs = true; @@ -672,13 +685,22 @@ class _PositionalArgHandler extends _ArgHandler { } class _NamedArgHandler extends _ArgHandler { + static final Map _NAMED_ARG = new HashMap(); + static String _GET_NAMED_ARG(Symbol symbol) { + String name = _NAMED_ARG[symbol]; + if (name == null) name = _NAMED_ARG[symbol] = 'namedArg[$name]'; + return name; + } final Symbol name; _NamedArgHandler(WatchGroup watchGrp, _EvalWatchRecord record, Symbol name) : this.name = name, - super(watchGrp, 'namedArg[$name]', record); + super(watchGrp, _GET_NAMED_ARG(name), record); void acceptValue(object) { + if (watchRecord.namedArgs == null) { + watchRecord.namedArgs = new HashMap(); + } watchRecord.dirtyArgs = true; watchRecord.namedArgs[name] = object; } @@ -728,7 +750,7 @@ class _EvalWatchRecord implements WatchRecord<_Handler> { WatchGroup watchGrp; final _Handler handler; final List args; - final Map namedArgs = new Map(); + Map namedArgs = null; final String name; int mode; Function fn; @@ -849,6 +871,8 @@ class _EvalWatchRecord implements WatchRecord<_Handler> { if (value is String && current is String && value == current) { // it is really the same, recover and save so next time identity is same current = value; + } else if (value is num && value.isNaN && current is num && current.isNaN) { + // we need this for the compiled JavaScript since in JS NaN !== NaN. } else { previousValue = current; currentValue = value; diff --git a/lib/core/annotation.dart b/lib/core/annotation.dart index 768def9aa..cee2ad10f 100644 --- a/lib/core/annotation.dart +++ b/lib/core/annotation.dart @@ -11,12 +11,14 @@ export "package:angular/core/annotation_src.dart" show ShadowRootAware, Formatter, - Injectable, + DirectiveBinder, + DirectiveBinderFn, Directive, Component, Controller, Decorator, + Visibility, DirectiveAnnotation, NgAttr, diff --git a/lib/core/annotation_src.dart b/lib/core/annotation_src.dart index 2458fd8d9..d40b126be 100644 --- a/lib/core/annotation_src.dart +++ b/lib/core/annotation_src.dart @@ -1,36 +1,32 @@ library angular.core.annotation_src; -import "package:di/di.dart" show Injector, Visibility; - -RegExp _ATTR_NAME = new RegExp(r'\[([^\]]+)\]$'); - -const String SHADOW_DOM_INJECTOR_NAME = 'SHADOW_INJECTOR'; - -skipShadow(Injector injector) - => injector.name == SHADOW_DOM_INJECTOR_NAME ? injector.parent : injector; +import "package:di/di.dart" show Injector, Visibility, Factory; + +abstract class DirectiveBinder { + void bind(key, {dynamic toValue, + Function toFactory, + Type toImplementation, + toInstanceOf, + inject: const[], + Visibility visibility: Visibility.LOCAL}); +} -localVisibility (Injector requesting, Injector defining) - => identical(skipShadow(requesting), defining); +typedef void DirectiveBinderFn(DirectiveBinder module); -directChildrenVisibility(Injector requesting, Injector defining) { - requesting = skipShadow(requesting); - return identical(requesting.parent, defining) || localVisibility(requesting, defining); -} +RegExp _ATTR_NAME = new RegExp(r'\[([^\]]+)\]$'); -Directive cloneWithNewMap(Directive annotation, map) - => annotation._cloneWithNewMap(map); +Directive cloneWithNewMap(Directive annotation, map) => annotation._cloneWithNewMap(map); String mappingSpec(DirectiveAnnotation annotation) => annotation._mappingSpec; +class Visibility { + static const LOCAL = const Visibility._('LOCAL'); + static const CHILDREN = const Visibility._('CHILDREN'); + static const DIRECT_CHILD = const Visibility._('DIRECT_CHILD'); -/** - * An annotation when applied to a class indicates that the class (service) will - * be instantiated by di injector. This annotation is also used to designate which - * classes need to have a static factory generated when using static angular, and - * therefore is required on any injectable class. - */ -class Injectable { - const Injectable(); + final String name; + const Visibility._(this.name); + toString() => 'Visibility: $name'; } /** @@ -39,16 +35,19 @@ class Injectable { abstract class Directive { /// The directive can only be injected to other directives on the same element. - static const Visibility LOCAL_VISIBILITY = localVisibility; + @deprecated // ('Use Visibility.LOCAL instead') + static const Visibility LOCAL_VISIBILITY = Visibility.LOCAL; /// The directive can be injected to other directives on the same or child elements. - static const Visibility CHILDREN_VISIBILITY = null; + @deprecated// ('Use Visibility.CHILDREN instead') + static const Visibility CHILDREN_VISIBILITY = Visibility.CHILDREN; /** * The directive on this element can only be injected to other directives * declared on elements which are direct children of the current element. */ - static const Visibility DIRECT_CHILDREN_VISIBILITY = directChildrenVisibility; + @deprecated// ('Use Visibility.DIRECT_CHILD instead') + static const Visibility DIRECT_CHILDREN_VISIBILITY = Visibility.DIRECT_CHILD; /** * CSS selector which will trigger this component/directive. @@ -73,25 +72,21 @@ abstract class Directive { * * [TRANSCLUDE_CHILDREN] * * [IGNORE_CHILDREN] */ - @deprecated final String children; /** * Compile the child nodes of the element. This is the default. */ - @deprecated static const String COMPILE_CHILDREN = 'compile'; /** * Compile the child nodes for transclusion and makes available * [BoundViewFactory], [ViewFactory] and [ViewPort] for injection. */ - @deprecated static const String TRANSCLUDE_CHILDREN = 'transclude'; /** * Do not compile/visit the child nodes. Angular markup on descendant nodes * will not be processed. */ - @deprecated static const String IGNORE_CHILDREN = 'ignore'; /** @@ -122,8 +117,8 @@ abstract class Directive { * selector: '[foo]', * module: Foo.moduleFactory) * class Foo { - * static moduleFactory() => new Module() - * ..bind(SomeTypeA, visibility: Directive.LOCAL_VISIBILITY); + * static moduleFactory(DirectiveBinder binder) => + * binder.bind(SomeTypeA, visibility: Directive.LOCAL_VISIBILITY); * } * * When specifying types, factories or values in the module, notice that @@ -132,7 +127,7 @@ abstract class Directive { * * [Directive.CHILDREN_VISIBILITY] * * [Directive.DIRECT_CHILDREN_VISIBILITY] */ - final Function module; + final DirectiveBinderFn module; /** * Use map to define the mapping of DOM attributes to fields. @@ -215,18 +210,15 @@ abstract class Directive { const Directive({ this.selector, - this.children: Directive.COMPILE_CHILDREN, - this.visibility: Directive.LOCAL_VISIBILITY, + this.children, + this.visibility, this.module, this.map: const {}, this.exportExpressions: const [], this.exportExpressionAttrs: const [] }); - toString() => selector; - get hashCode => selector.hashCode; - operator==(other) => - other is Directive && selector == other.selector; + String toString() => selector; Directive _cloneWithNewMap(newMap); } @@ -306,7 +298,6 @@ class Component extends Directive { * published into. This allows the expressions in the template to be referring * to controller instance and its properties. */ - @deprecated final String publishAs; /** @@ -328,7 +319,7 @@ class Component extends Directive { applyAuthorStyles, resetStyleInheritance, this.publishAs, - module, + DirectiveBinderFn module, map, selector, visibility, @@ -386,7 +377,7 @@ class Decorator extends Directive { const Decorator({children: Directive.COMPILE_CHILDREN, map, selector, - module, + DirectiveBinderFn module, visibility, exportExpressions, exportExpressionAttrs}) @@ -439,7 +430,7 @@ class Controller extends Decorator { children: Directive.COMPILE_CHILDREN, this.publishAs, map, - module, + DirectiveBinderFn module, selector, visibility, exportExpressions, @@ -482,7 +473,6 @@ abstract class DirectiveAnnotation { * The value of the attribute to be treated as a string, equivalent * to `@` specification. */ -@deprecated class NgAttr extends DirectiveAnnotation { final _mappingSpec = '@'; const NgAttr(String attrName) : super(attrName); @@ -494,7 +484,6 @@ class NgAttr extends DirectiveAnnotation { * The value of the attribute to be treated as a one-way expression, equivalent * to `=>` specification. */ -@deprecated class NgOneWay extends DirectiveAnnotation { final _mappingSpec = '=>'; const NgOneWay(String attrName) : super(attrName); @@ -506,7 +495,6 @@ class NgOneWay extends DirectiveAnnotation { * The value of the attribute to be treated as a one time one-way expression, * equivalent to `=>!` specification. */ -@deprecated class NgOneWayOneTime extends DirectiveAnnotation { final _mappingSpec = '=>!'; const NgOneWayOneTime(String attrName) : super(attrName); @@ -518,7 +506,6 @@ class NgOneWayOneTime extends DirectiveAnnotation { * The value of the attribute to be treated as a two-way expression, * equivalent to `<=>` specification. */ -@deprecated class NgTwoWay extends DirectiveAnnotation { final _mappingSpec = '<=>'; const NgTwoWay(String attrName) : super(attrName); @@ -530,7 +517,6 @@ class NgTwoWay extends DirectiveAnnotation { * The value of the attribute to be treated as a callback expression, * equivalent to `&` specification. */ -@deprecated class NgCallback extends DirectiveAnnotation { final _mappingSpec = '&'; const NgCallback(String attrName) : super(attrName); @@ -588,8 +574,5 @@ class Formatter { const Formatter({this.name}); - int get hashCode => name.hashCode; - bool operator==(other) => name == other.name; - toString() => 'Formatter: $name'; } diff --git a/lib/core/formatter.dart b/lib/core/formatter.dart index 25f317414..0bbcb2e99 100644 --- a/lib/core/formatter.dart +++ b/lib/core/formatter.dart @@ -1,6 +1,8 @@ library angular.core_internal.formatter_map; +import 'dart:collection'; import 'package:di/di.dart'; +import 'package:di/annotations.dart'; import 'package:angular/core/annotation_src.dart'; import 'package:angular/core/registry.dart'; @@ -8,16 +10,30 @@ import 'package:angular/core/registry.dart'; * Registry of formatters at runtime. */ @Injectable() -class FormatterMap extends AnnotationMap { - Injector _injector; - FormatterMap(Injector injector, MetadataExtractor extractMetadata) - : this._injector = injector, - super(injector, extractMetadata); +class FormatterMap { + final Map _map = new HashMap(); + final Injector _injector; - call(String name) { - var formatter = new Formatter(name: name); - var formatterType = this[formatter]; - return _injector.get(formatterType); + FormatterMap(this._injector, MetadataExtractor extractMetadata) { + (_injector as ModuleInjector).types.forEach((type) { + extractMetadata(type) + .where((annotation) => annotation is Formatter) + .forEach((Formatter formatter) { + _map[formatter.name] = type; + }); + }); + } + + Function call(String name) => _injector.get(this[name]); + + Type operator[](String name) { + Type formatterType = _map[name]; + if (formatterType == null) throw "No formatter '$name' found!"; + return formatterType; + } + + void forEach(fn(K, Type)) { + _map.forEach(fn); } } diff --git a/lib/core/interpolate.dart b/lib/core/interpolate.dart index 8c74a2c23..2e043c08f 100644 --- a/lib/core/interpolate.dart +++ b/lib/core/interpolate.dart @@ -10,7 +10,11 @@ part of angular.core_internal; */ @Injectable() class Interpolate implements Function { - var _cache = {}; + var _cache = new HashMap(); + + Interpolate(CacheRegister cacheRegister) { + cacheRegister.registerCache("Interpolate", _cache); + } /** * Compiles markup text into expression. * @@ -57,8 +61,7 @@ class Interpolate implements Function { // formatter expParts.add(_wrapInQuotes(template.substring(index, startIdx))); } - expParts.add('(' + template.substring(startIdx + startLen, endIdx) + - '|stringify)'); + expParts.add('(' + template.substring(startIdx + startLen, endIdx) + '|stringify)'); index = endIdx + endLen; hasInterpolation = true; diff --git a/lib/core/module.dart b/lib/core/module.dart index d4b3d6a24..41ec0a6ad 100644 --- a/lib/core/module.dart +++ b/lib/core/module.dart @@ -22,12 +22,19 @@ export "package:angular/change_detection/change_detection.dart" show AvgStopwatch, FieldGetterFactory; +export "package:angular/cache/module.dart" show + CacheRegister, + CacheRegisterStats; + +export "package:angular/core_dom/directive_injector.dart" show + DirectiveInjector; + export "package:angular/core_dom/module_internal.dart" show Animation, AnimationResult, BrowserCookies, - Cache, Compiler, + CompilerConfig, Cookies, BoundViewFactory, DirectiveMap, @@ -35,6 +42,7 @@ export "package:angular/core_dom/module_internal.dart" show EventHandler, Http, HttpBackend, + HttpConfig, HttpDefaultHeaders, HttpDefaults, HttpInterceptor, diff --git a/lib/core/module_internal.dart b/lib/core/module_internal.dart index 04ed84507..5b98206b0 100644 --- a/lib/core/module_internal.dart +++ b/lib/core/module_internal.dart @@ -6,6 +6,7 @@ import 'dart:math'; import 'package:intl/intl.dart'; import 'package:di/di.dart'; +import 'package:di/annotations.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/core/parser/lexer.dart'; @@ -13,6 +14,7 @@ import 'package:angular/utils.dart'; import 'package:angular/core/annotation_src.dart'; +import 'package:angular/cache/module.dart'; import 'package:angular/change_detection/watch_group.dart'; export 'package:angular/change_detection/watch_group.dart'; import 'package:angular/change_detection/ast_parser.dart'; @@ -24,7 +26,6 @@ import 'package:angular/core/parser/utils.dart'; import 'package:angular/core/registry.dart'; import 'package:angular/core/static_keys.dart'; -part "cache.dart"; part "exception_handler.dart"; part "interpolate.dart"; part "scope.dart"; @@ -35,22 +36,19 @@ class CoreModule extends Module { CoreModule() { bind(ScopeDigestTTL); - bind(MetadataExtractor); - bind(Cache); bind(ExceptionHandler); bind(FormatterMap); bind(Interpolate); bind(RootScope); - bind(Scope, toFactory: (injector) => injector.getByKey(ROOT_SCOPE_KEY)); - bind(ClosureMap, toFactory: (_) => throw "Must provide dynamic/static ClosureMap."); + bind(Scope, toInstanceOf: RootScope); + bind(ClosureMap, toFactory: () => throw "Must provide dynamic/static ClosureMap.", inject: []); bind(ScopeStats); bind(ScopeStatsEmitter); - bind(ScopeStatsConfig, toFactory: (i) => new ScopeStatsConfig()); + bind(ScopeStatsConfig); bind(Object, toValue: {}); // RootScope context - bind(VmTurnZone); - bind(Parser, toImplementation: DynamicParser); - bind(ParserBackend, toImplementation: DynamicParserBackend); + bind(Parser, toInstanceOf: DynamicParser); + bind(ParserBackend, toInstanceOf: DynamicParserBackend); bind(DynamicParser); bind(DynamicParserBackend); bind(Lexer); diff --git a/lib/core/parser/dynamic_parser.dart b/lib/core/parser/dynamic_parser.dart index 041ebb33d..d1caf80f2 100644 --- a/lib/core/parser/dynamic_parser.dart +++ b/lib/core/parser/dynamic_parser.dart @@ -1,5 +1,7 @@ library angular.core.parser.dynamic_parser; +import 'package:di/annotations.dart'; +import 'package:angular/cache/module.dart'; import 'package:angular/core/annotation_src.dart' hide Formatter; import 'package:angular/core/module_internal.dart' show FormatterMap; @@ -24,7 +26,9 @@ class DynamicParser implements Parser { final Lexer _lexer; final ParserBackend _backend; final Map _cache = {}; - DynamicParser(this._lexer, this._backend); + DynamicParser(this._lexer, this._backend, CacheRegister cacheRegister) { + cacheRegister.registerCache("DynamicParser", _cache); + } Expression call(String input) { if (input == null) input = ''; diff --git a/lib/core/parser/lexer.dart b/lib/core/parser/lexer.dart index 41c96c3a8..46abe864a 100644 --- a/lib/core/parser/lexer.dart +++ b/lib/core/parser/lexer.dart @@ -1,5 +1,6 @@ library angular.core.parser.lexer; +import 'package:di/annotations.dart'; import 'package:angular/core/annotation_src.dart'; import 'package:angular/core/parser/characters.dart'; diff --git a/lib/core/parser/parser_dynamic.dart b/lib/core/parser/parser_dynamic.dart index 967db7bed..2657731c3 100644 --- a/lib/core/parser/parser_dynamic.dart +++ b/lib/core/parser/parser_dynamic.dart @@ -45,11 +45,7 @@ class DynamicClosureMap implements ClosureMap { throw "Property '$name' is not of type function."; } } else { - try { - return reflect(o).invoke(symbol, posArgs, sNamedArgs).reflectee; - } on NoSuchMethodError catch (e) { - throw 'Undefined function $name'; - } + return reflect(o).invoke(symbol, posArgs, sNamedArgs).reflectee; } }; } diff --git a/lib/core/parser/static_parser.dart b/lib/core/parser/static_parser.dart index ac097c67a..e1e38ad79 100644 --- a/lib/core/parser/static_parser.dart +++ b/lib/core/parser/static_parser.dart @@ -1,5 +1,6 @@ library angular.core.parser.static_parser; +import 'package:angular/cache/module.dart' show CacheRegister; import 'package:angular/core/annotation_src.dart' show Injectable; import 'package:angular/core/module_internal.dart' show FormatterMap; import 'package:angular/core/parser/parser.dart'; @@ -16,8 +17,10 @@ class StaticParserFunctions { class StaticParser implements Parser { final StaticParserFunctions _functions; final DynamicParser _fallbackParser; - final _cache = {}; - StaticParser(this._functions, this._fallbackParser); + final _cache = new HashMap(); + StaticParser(this._functions, this._fallbackParser, CacheRegister cacheRegister) { + cacheRegister.registerCache("StaticParser", _cache); + } Expression call(String input) { if (input == null) input = ''; diff --git a/lib/core/parser/syntax.dart b/lib/core/parser/syntax.dart index e4393860c..de2ebc70e 100644 --- a/lib/core/parser/syntax.dart +++ b/lib/core/parser/syntax.dart @@ -205,5 +205,4 @@ class _DefaultFormatterMap implements FormatterMap { call(name) => throw 'No Formatter: $name found!'; Type operator[](annotation) => null; forEach(fn) { } - annotationsFor(type) => null; } diff --git a/lib/core/registry.dart b/lib/core/registry.dart index 9d66de3ed..1db9cf61c 100644 --- a/lib/core/registry.dart +++ b/lib/core/registry.dart @@ -1,75 +1,5 @@ library angular.core.registry; -import 'package:di/di.dart' show Injector; - -abstract class AnnotationMap { - final Map _map = {}; - - AnnotationMap(Injector injector, MetadataExtractor extractMetadata) { - injector.types.forEach((type) { - extractMetadata(type) - .where((annotation) => annotation is K) - .forEach((annotation) { - _map[annotation] = type; - }); - }); - } - - Type operator[](K annotation) { - var value = _map[annotation]; - if (value == null) throw 'No $annotation found!'; - return value; - } - - void forEach(fn(K, Type)) { - _map.forEach(fn); - } - - List annotationsFor(Type type) { - final res = []; - forEach((ann, annType) { - if (annType == type) res.add(ann); - }); - return res; - } -} - -abstract class AnnotationsMap { - final Map> map = {}; - - AnnotationsMap(Injector injector, MetadataExtractor extractMetadata) { - injector.types.forEach((type) { - extractMetadata(type) - .where((annotation) => annotation is K) - .forEach((annotation) { - map.putIfAbsent(annotation, () => []).add(type); - }); - }); - } - - List operator[](K annotation) { - var value = map[annotation]; - if (value == null) throw 'No $annotation found!'; - return value; - } - - void forEach(fn(K, Type)) { - map.forEach((annotation, types) { - types.forEach((type) { - fn(annotation, type); - }); - }); - } - - List annotationsFor(Type type) { - var res = []; - forEach((ann, annType) { - if (annType == type) res.add(ann); - }); - return res; - } -} - abstract class MetadataExtractor { Iterable call(Type type); } diff --git a/lib/core/registry_dynamic.dart b/lib/core/registry_dynamic.dart index 54ea6573d..1f24e0772 100644 --- a/lib/core/registry_dynamic.dart +++ b/lib/core/registry_dynamic.dart @@ -3,11 +3,12 @@ library angular.core_dynamic; import 'dart:mirrors'; import 'package:angular/core/annotation_src.dart'; import 'package:angular/core/registry.dart'; +import 'dart:collection'; export 'package:angular/core/registry.dart' show MetadataExtractor; -var _fieldMetadataCache = new Map>(); +var _fieldMetadataCache = new HashMap>(); class DynamicMetadataExtractor implements MetadataExtractor { final _fieldAnnotations = [ diff --git a/lib/core/registry_static.dart b/lib/core/registry_static.dart index fcb287b13..426b26aa6 100644 --- a/lib/core/registry_static.dart +++ b/lib/core/registry_static.dart @@ -1,6 +1,6 @@ library angular.core_static; -import 'package:angular/core/annotation_src.dart' show Injectable; +import 'package:di/annotations.dart'; import 'package:angular/core/registry.dart'; @Injectable() diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 4a05a1da0..978abaa5d 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -4,78 +4,63 @@ 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]. + * 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. - */ final String name; - /** - * The origin scope that triggered the event (via broadcast or emit). - */ + /// The origin scope that triggered the event (via [Scope.broadcast] or [Scope.emit]). final Scope targetScope; /** - * 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. + * The destination scope that intercepted the event. As the event traverses the scope hierarchy + * the event instance stays the same, but the [currentScope] reflects the scope of the listener + * which is firing. */ Scope get currentScope => _currentScope; Scope _currentScope; - /** - * true or false depending on if [stopPropagation] was executed. - */ + /// true or false depending on if [stopPropagation] was executed. bool get propagationStopped => _propagationStopped; bool _propagationStopped = false; - /** - * true or false depending on if [preventDefault] was executed. - */ + /// true or false depending on if [preventDefault] was executed. bool get defaultPrevented => _defaultPrevented; bool _defaultPrevented = false; /** - * [name] - The name of the scope event. - * [targetScope] - The destination scope that is listening on the event. + * [name]: The name of the scope event. + * [targetScope]: The scope that triggers the event. + * [data]: Arbitrary data attached to the event. */ ScopeEvent(this.name, this.targetScope, this.data); - /** - * Prevents the intercepted event from propagating further to successive - * scopes. - */ + /// Prevents the intercepted event from propagating further void stopPropagation () { _propagationStopped = true; } - /** - * Sets the defaultPrevented flag to true. - */ + /// Sets the defaultPrevented flag to true. void preventDefault() { _defaultPrevented = true; } } /** - * 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 iterations then - * the digest is stopped and an exception is thrown. + * 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. The 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 iterations then the + * digest is stopped and an exception is thrown. */ @Injectable() class ScopeDigestTTL { @@ -125,31 +110,23 @@ class ScopeLocals implements Map { } /** - * [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 [View]s are bound to each other. - * As scopes are created and destroyed by [ViewFactory] they are responsible - * for change detection, change processing and memory management. + * [Scope] represents a collection of [watch]es [observer]s, and a [context] for the watchers, + * observers and [eval]uations. Scopes structure loosely mimics the DOM structure. Scopes and + * [View]s are bound to each other. As scopes are created and destroyed by [ViewFactory] they are + * responsible for change detection, change processing and memory management. */ class Scope { final String id; int _childScopeNextId = 0; - /** - * The default execution context for [watch]es [observe]ers, and [eval]uation. - */ + /// The default execution context for [watch]es [observe]ers, and [eval]uation. final context; - /** - * The [RootScope] of the application. - */ + /// The [RootScope] of the application. final RootScope rootScope; Scope _parentScope; - /** - * The parent [Scope]. - */ Scope get parentScope => _parentScope; final ScopeStats _stats; @@ -167,9 +144,7 @@ class Scope { return true; } - /** - * Returns true if the scope is still attached to the [RootScope]. - */ + /// true when the scope is still attached to the [RootScope]. bool get isAttached => !isDestroyed; // TODO(misko): WatchGroup should be private. @@ -218,17 +193,6 @@ class Scope { // TODO(deboer): Temporary shim until all uses are fixed. if (context != null) { - assert(() { - try { - throw []; - } catch (e, s) { - var msg = "WARNING: The Scope.watch's context parameter " - "is deprecated.\nScope.watch was called from:\n$s"; - _oneTimeWarnings.putIfAbsent(msg, () => print(msg)); - } - return true; - }); - // Create a child scope instead. return createChild(context) .watch(expression, reactionFn, @@ -253,13 +217,19 @@ class Scope { } else if (expression.startsWith(':')) { expression = expression.substring(1); fn = (value, last) { - if (value != null) reactionFn(value, last); + if (value != null) reactionFn(value, last); }; } } - AST ast = rootScope._astParser(expression, - formatters: formatters, collection: collection); + String astKey = + "${collection ? "C" : "."}${formatters == null ? "." : formatters.hashCode}$expression"; + AST ast = rootScope.astCache[astKey]; + if (ast == null) { + ast = rootScope.astCache[astKey] = + rootScope._astParser(expression, + formatters: formatters, collection: collection); + } return watch = watchAST(ast, fn, canChangeModel: canChangeModel); } @@ -267,7 +237,7 @@ class Scope { /** * Use [watch] to set up change detection on an pre-parsed AST. * - * * [ast] The pre-parsed AST. + * * [ast]: The pre-parsed AST. * * [reactionFn]: The function executed when a change is detected. * * [canChangeModel]: Whether or not the [reactionFn] can change the model. */ @@ -275,8 +245,18 @@ class Scope { WatchGroup group = canChangeModel ? _readWriteGroup : _readOnlyGroup; return group.watch(ast, reactionFn); } - static Map _oneTimeWarnings = {}; + /** + * 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. + * + * * [expression]: The expression that will be evaluated. This can be either a Function or a + * String. + * * [locals]: A Map that will override any matching context members for the purposes of the + * evaluation. + */ dynamic eval(expression, [Map locals]) { assert(isAttached); assert(expression == null || @@ -293,6 +273,12 @@ class Scope { return null; } + /** + * Triggers a digest cycle. It accepts an optional [expression] to evaluate before the digest + * operation. The result of that expression will be returned afterwards. + * + * [apply] should only be called from the within unit tests to simulate the life cycle of a scope. + */ dynamic apply([expression, Map locals]) { _assertInternalStateConsistency(); rootScope._transitionState(null, RootScope.STATE_APPLY); @@ -307,21 +293,42 @@ class Scope { } } + /** + * Triggers a [ScopeEvent] 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] method), then the event listener callback function will be + * executed. + * + * The triggered [ScopeEvent] references the [data] so that they can be retrieve in the listener. + */ ScopeEvent emit(String name, [data]) { assert(isAttached); return _Streams.emit(this, name, data); } + /** + * Triggers a [ScopeEvent] referenced by the [name] parameters downards 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] method), then the event listener callback function will be + * executed. + * + * The triggered [ScopeEvent] references the [data] so that they can be retrieve in the listener. + */ ScopeEvent broadcast(String name, [data]) { assert(isAttached); return _Streams.broadcast(this, name, data); } + /** + * 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]. + */ ScopeStream on(String name) { assert(isAttached); return _Streams.on(this, rootScope._exceptionHandler, name); } + /// Creates a child [Scope] with the given [childContext] Scope createChild(Object childContext) { assert(isAttached); var child = new Scope(childContext, rootScope, this, @@ -337,6 +344,17 @@ class Scope { return child; } + /** + * 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 nor its children. + * + * 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 [ScopeEvent.DESTROY] event is broadcasted from this scope. + * This allows for child scopes (such as shared directives) to perform any necessary cleanup + * before the scope is removed from the application. + */ void destroy() { assert(isAttached); broadcast(ScopeEvent.DESTROY); @@ -369,8 +387,8 @@ class Scope { Map _verifyStreams(parentScope, prefix, log) { assert(_parentScope == parentScope); - var counts = {}; - var typeCounts = _streams == null ? {} : _streams._typeCounts; + var counts = new HashMap(); + var typeCounts = _streams == null ? new HashMap() : _streams._typeCounts; var connection = _streams != null && _streams._scope == this ? '=' : '-'; log..add(prefix)..add(hashCode)..add(connection)..add(typeCounts)..add('\n'); if (_streams == null) { @@ -486,10 +504,7 @@ class ScopeStats { } } -/** - * ScopeStatsEmitter is in charge of formatting the [ScopeStats] and outputting - * a message. - */ +/// ScopeStatsEmitter is in charge of formatting the [ScopeStats] and outputting a message. @Injectable() class ScopeStatsEmitter { static String _PAD_ = ' '; @@ -503,9 +518,9 @@ class ScopeStatsEmitter { static pad(String str, int size) => _PAD_.substring(0, max(size - str.length, 0)) + str; - _ms(num value) => '${pad(_nfDec.format(value), 9)} ms'; - _us(num value) => _ms(value / 1000); - _tally(num value) => '${pad(_nfInt.format(value), 6)}'; + String _ms(num value) => '${pad(_nfDec.format(value), 9)} ms'; + String _us(num value) => _ms(value / 1000); + String _tally(num value) => '${pad(_nfInt.format(value), 6)}'; /** * Emit a message based on the phase and state of stopwatches. @@ -538,16 +553,14 @@ class ScopeStatsEmitter { * ScopeStatsConfig is used to modify behavior of [ScopeStats]. You can use this * object to modify behavior at runtime too. */ +@Injectable() class ScopeStatsConfig { var emit = false; ScopeStatsConfig(); - ScopeStatsConfig.enabled() { - emit = true; - } + ScopeStatsConfig.enabled(): emit = true; } /** - * * Every Angular application has exactly one RootScope. RootScope extends Scope, adding * services related to change detection, async unit-of-work processing, and DOM read/write queues. * The RootScope can not be destroyed. @@ -556,7 +569,6 @@ class ScopeStatsConfig { * * All work in Angular must be done within a context of a VmTurnZone. VmTurnZone detects the end * of the VM turn, and calls the Apply method to process the changes at the end of VM turn. - * */ @Injectable() class RootScope extends Scope { @@ -571,6 +583,9 @@ class RootScope extends Scope { final ScopeDigestTTL _ttl; final VmTurnZone _zone; + // For Scope.watch(). + final Map astCache = new HashMap(); + _FunctionChain _runAsyncHead, _runAsyncTail; _FunctionChain _domWriteHead, _domWriteTail; _FunctionChain _domReadHead, _domReadTail; @@ -580,28 +595,27 @@ class RootScope extends Scope { String _state; /** - * * While processing data bindings, Angular passes through multiple states. When testing or * debugging, it can be useful to access the current `state`, which is one of the following: * - * * null - * * apply - * * digest - * * flush - * * assert + * * `null` + * * `STATE_APPLY` + * * `STATE_DIGEST` + * * `STATE_FLUSH` + * * `STATE_FLUSH_ASSERT` * - * ##null + * ## `null` * * Angular is not currently processing changes * - * ##apply + * ## `STATE_APPLY` * * The apply state begins by executing the optional expression within the context of * angular change detection mechanism. Any exceptions are delegated to [ExceptionHandler]. At the * end of apply state RootScope enters the digest followed by flush phase (optionally if asserts * enabled run assert phase.) * - * ##digest + * ## `STATE_DIGEST` * * The apply state begins by processing the async queue, * followed by change detection @@ -610,7 +624,7 @@ class RootScope extends Scope { * iterations the model is considered unstable and angular exists with an exception. (See * ScopeDigestTTL) * - * ##flush + * ## `STATE_FLUSH` * * The flush phase consists of these steps: * @@ -620,17 +634,16 @@ class RootScope extends Scope { * 3. processing the DOM read queue * 4. repeat steps 1 and 3 (not 2) until queues are empty * - * ##assert + * ## `STATE_FLUSH_ASSERT` * * Optionally if Dart assert is on, verify that flush reaction functions did not make any changes * to model and throw error if changes detected. - * */ String get state => _state; RootScope(Object context, Parser parser, ASTParser astParser, FieldGetterFactory fieldGetterFactory, FormatterMap formatters, this._exceptionHandler, this._ttl, this._zone, - ScopeStats _scopeStats) + ScopeStats _scopeStats, CacheRegister cacheRegister) : _scopeStats = _scopeStats, _parser = parser, _astParser = astParser, @@ -644,12 +657,14 @@ class RootScope extends Scope { { _zone.onTurnDone = apply; _zone.onError = (e, s, ls) => _exceptionHandler(e, s); + _zone.onScheduleMicrotask = runAsync; + cacheRegister.registerCache("ScopeWatchASTs", astCache); } RootScope get rootScope => this; bool get isAttached => true; -/** + /** * Propagates changes between different parts of the application model. Normally called by * [VMTurnZone] right before DOM rendering to initiate data binding. May also be called directly * for unit testing. @@ -679,15 +694,8 @@ class RootScope extends Scope { ChangeLog changeLog; _scopeStats.digestStart(); do { - while (_runAsyncHead != null) { - try { - _runAsyncHead.fn(); - } catch (e, s) { - _exceptionHandler(e, s); - } - _runAsyncHead = _runAsyncHead._next; - } - _runAsyncTail = null; + + int asyncCount = _runAsyncFns(); digestTTL--; count = rootWatchGroup.detectChanges( @@ -703,7 +711,7 @@ class RootScope extends Scope { digestLog = []; changeLog = (e, c, p) => digestLog.add('$e: $c <= $p'); } else { - log.add(digestLog.join(', ')); + log.add("${asyncCount > 0 ? 'async:$asyncCount' : ''}${digestLog.join(', ')}"); digestLog.clear(); } } @@ -712,7 +720,7 @@ class RootScope extends Scope { 'Last $LOG_COUNT iterations:\n${log.join('\n')}'; } _scopeStats.digestLoop(count); - } while (count > 0); + } while (count > 0 || _runAsyncHead != null); } finally { _scopeStats.digestEnd(); _transitionState(STATE_DIGEST, null); @@ -755,7 +763,8 @@ class RootScope extends Scope { if (_domReadHead == null) _stats.domReadEnd(); } _domReadTail = null; - } while (_domWriteHead != null || _domReadHead != null); + _runAsyncFns(); + } while (_domWriteHead != null || _domReadHead != null || _runAsyncHead != null); _stats.flushEnd(); assert((() { _stats.flushAssertStart(); @@ -787,6 +796,9 @@ class RootScope extends Scope { // QUEUES void runAsync(fn()) { + if (_state == STATE_FLUSH_ASSERT) { + throw "Scheduling microtasks not allowed in $state state."; + } var chain = new _FunctionChain(fn); if (_runAsyncHead == null) { _runAsyncHead = _runAsyncTail = chain; @@ -795,6 +807,21 @@ class RootScope extends Scope { } } + _runAsyncFns() { + var count = 0; + while (_runAsyncHead != null) { + try { + count++; + _runAsyncHead.fn(); + } catch (e, s) { + _exceptionHandler(e, s); + } + _runAsyncHead = _runAsyncHead._next; + } + _runAsyncTail = null; + return count; + } + void domWrite(fn()) { var chain = new _FunctionChain(fn); if (_domWriteHead == null) { @@ -823,40 +850,36 @@ class RootScope extends Scope { } /** - * 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. + * 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. + * 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 + * 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. + * 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. + * 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(); + final _streams = new HashMap(); /// Child [Scope] event counts. final Map _typeCounts; _Streams(this._scope, this._exceptionHandler, _Streams inheritStreams) : _typeCounts = inheritStreams == null - ? {} - : new Map.from(inheritStreams._typeCounts); + ? new HashMap() + : new HashMap.from(inheritStreams._typeCounts); static ScopeEvent emit(Scope scope, String name, data) { var event = new ScopeEvent(name, scope, data); diff --git a/lib/core/static_keys.dart b/lib/core/static_keys.dart index 8f810b894..fe29b88ee 100644 --- a/lib/core/static_keys.dart +++ b/lib/core/static_keys.dart @@ -1,11 +1,12 @@ library angular.static_keys; import 'package:di/di.dart'; -import 'module_internal.dart'; +import 'package:angular/core/module_internal.dart'; -Key EXCEPTION_HANDLER_KEY = new Key(ExceptionHandler); -Key ROOT_SCOPE_KEY = new Key(RootScope); -Key SCOPE_KEY = new Key(Scope); -Key SCOPE_STATS_CONFIG_KEY = new Key(ScopeStatsConfig); -Key FORMATTER_MAP_KEY = new Key(FormatterMap); -Key INTERPOLATE_KEY = new Key(Interpolate); \ No newline at end of file +final Key INJECTOR_KEY = new Key(Injector); +final Key EXCEPTION_HANDLER_KEY = new Key(ExceptionHandler); +final Key ROOT_SCOPE_KEY = new Key(RootScope); +final Key SCOPE_KEY = new Key(Scope); +final Key SCOPE_STATS_CONFIG_KEY = new Key(ScopeStatsConfig); +final Key FORMATTER_MAP_KEY = new Key(FormatterMap); +final Key INTERPOLATE_KEY = new Key(Interpolate); diff --git a/lib/core/zone.dart b/lib/core/zone.dart index 68953859b..4fe973463 100644 --- a/lib/core/zone.dart +++ b/lib/core/zone.dart @@ -60,6 +60,7 @@ class VmTurnZone { /// an "inner" [Zone], which is a child of the outer [Zone]. async.Zone _innerZone; + /** * Associates with this * @@ -87,7 +88,8 @@ class VmTurnZone { bool _errorThrownFromOnRun = false; var _currentlyInTurn = false; - _onRunBase(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) { + + dynamic _onRunBase(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) { _runningInTurn++; try { if (!_currentlyInTurn) { @@ -104,28 +106,29 @@ class VmTurnZone { if (_runningInTurn == 0) _finishTurn(zone, delegate); } } + // Called from the parent zone. - _onRun(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) => + dynamic _onRun(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) => _onRunBase(self, delegate, zone, () => delegate.run(zone, fn)); - _onRunUnary(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, - fn(args), args) => + dynamic _onRunUnary(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, + fn(args), args) => _onRunBase(self, delegate, zone, () => delegate.runUnary(zone, fn, args)); - _onScheduleMicrotask(async.Zone self, async.ZoneDelegate delegate, - async.Zone zone, fn()) { + void _onScheduleMicrotask(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) { onScheduleMicrotask(() => delegate.run(zone, fn)); if (_runningInTurn == 0 && !_inFinishTurn) _finishTurn(zone, delegate); } - _uncaughtError(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, - e, StackTrace s) { + void _uncaughtError(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, + e, StackTrace s) { if (!_errorThrownFromOnRun) onError(e, s, _longStacktrace); _errorThrownFromOnRun = false; } var _inFinishTurn = false; - _finishTurn(zone, delegate) { + + void _finishTurn(zone, delegate) { if (_inFinishTurn) return; _inFinishTurn = true; try { @@ -217,15 +220,27 @@ class VmTurnZone { /** * Runs [body] in the inner zone and returns whatever it returns. + * + * In a typical app where the inner zone is the Angular zone, this allows one to make use of the + * Angular's auto digest mechanism. + * + * VmTurnZone zone = ; + * + * void functionCalledFromJS() { + * zone.run(() { + * // auto-digest will run after this function is called from JS + * }) + * } */ dynamic run(body()) => _innerZone.run(body); /** * Runs [body] in the outer zone and returns whatever it returns. - * In a typical app where the inner zone is the Angular zone, this allows - * one to escape Angular's auto-digest mechanism. * - * myFunction(VmTurnZone zone, Element element) { + * In a typical app where the inner zone is the Angular zone, this allows one to escape Angular's + * auto-digest mechanism. + * + * void myFunction(VmTurnZone zone, Element element) { * element.onClick.listen(() { * // auto-digest will run after element click. * }); diff --git a/lib/core_dom/animation.dart b/lib/core_dom/animation.dart index 2a6c99b1e..0629e3901 100644 --- a/lib/core_dom/animation.dart +++ b/lib/core_dom/animation.dart @@ -9,6 +9,12 @@ part of angular.core.dom_internal; */ @Injectable() class Animate { + /** + * When set to false, all animations are disabled. When true, animations are + * allowed. + */ + bool animationsAllowed = true; + /** * Add the [cssClass] to the classes on [element] after running any * defined animations. diff --git a/lib/core_dom/common.dart b/lib/core_dom/common.dart index 53a2c6c59..1dc25fd04 100644 --- a/lib/core_dom/common.dart +++ b/lib/core_dom/common.dart @@ -1,8 +1,6 @@ part of angular.core.dom_internal; -List cloneElements(elements) { - return elements.map((el) => el.clone(true)).toList(); -} +List cloneElements(elements) => elements.map((el) => el.clone(true)).toList(); class MappingParts { final String attrName; @@ -11,19 +9,24 @@ class MappingParts { final AST dstAST; final String originalValue; - const MappingParts(this.attrName, this.attrValueAST, this.mode, this.dstAST, this.originalValue); + MappingParts(this.attrName, this.attrValueAST, this.mode, this.dstAST, this.originalValue); } class DirectiveRef { final dom.Node element; final Type type; + final Function factory; + final List paramKeys; final Key typeKey; final Directive annotation; final String value; final AST valueAST; - final mappings = new List(); + final mappings = []; - DirectiveRef(this.element, this.type, this.annotation, this.typeKey, [ this.value, this.valueAST ]); + DirectiveRef(this.element, type, this.annotation, this.typeKey, [ this.value, this.valueAST ]) + : type = type, + factory = Module.DEFAULT_REFLECTOR.factoryFor(type), + paramKeys = Module.DEFAULT_REFLECTOR.parameterKeysFor(type); String toString() { var html = element is dom.Element @@ -39,13 +42,17 @@ class DirectiveRef { * Creates a child injector that allows loading new directives, formatters and * services from the provided modules. */ -Injector forceNewDirectivesAndFormatters(Injector injector, List modules) { +Injector forceNewDirectivesAndFormatters(Injector injector, DirectiveInjector dirInjector, + List modules) { modules.add(new Module() - ..bind(Scope, toFactory: (i) { - var scope = i.parent.getByKey(SCOPE_KEY); - return scope.createChild(new PrototypeMap(scope.context)); - })); - - return injector.createChild(modules, - forceNewInstances: [DirectiveMap, FormatterMap]); + ..bind(Scope, toFactory: (Injector injector) { + var scope = injector.parent.getByKey(SCOPE_KEY); + return scope.createChild(new PrototypeMap(scope.context)); + }, inject: [INJECTOR_KEY]) + ..bind(DirectiveMap) + ..bind(FormatterMap) + ..bind(DirectiveInjector, + toFactory: () => new DefaultDirectiveInjector.newAppInjector(dirInjector, injector))); + + return new ModuleInjector(modules, injector); } diff --git a/lib/core_dom/compiler_config.dart b/lib/core_dom/compiler_config.dart new file mode 100644 index 000000000..3ef2b8895 --- /dev/null +++ b/lib/core_dom/compiler_config.dart @@ -0,0 +1,15 @@ +part of angular.core.dom_internal; + +/** + * Global configuration options for the Compiler + */ +@Injectable() +class CompilerConfig { + /** + * True if the compiler should add ElementProbes to the elements. + */ + final bool elementProbeEnabled; + + CompilerConfig() : elementProbeEnabled = true; + CompilerConfig.withOptions({this.elementProbeEnabled: true}); +} diff --git a/lib/core_dom/directive.dart b/lib/core_dom/directive.dart index ec846f7ca..8738fe273 100644 --- a/lib/core_dom/directive.dart +++ b/lib/core_dom/directive.dart @@ -18,7 +18,7 @@ class NodeAttrs { final dom.Element element; Map> _observers; - final _mustacheAttrs = {}; + final _mustacheAttrs = new HashMap(); NodeAttrs(this.element); @@ -49,7 +49,7 @@ class NodeAttrs { * [:true:] */ observe(String attrName, notifyFn(String value)) { - if (_observers == null) _observers = >{}; + if (_observers == null) _observers = new HashMap>(); _observers.putIfAbsent(attrName, () => <_AttributeChanged>[]) .add(notifyFn); diff --git a/lib/core_dom/directive_injector.dart b/lib/core_dom/directive_injector.dart new file mode 100644 index 000000000..7e10f74b2 --- /dev/null +++ b/lib/core_dom/directive_injector.dart @@ -0,0 +1,437 @@ +library angular.node_injector; + +import 'dart:collection'; +import 'dart:html' show Node, Element, ShadowRoot; +import 'dart:profiler'; + +import 'package:di/di.dart'; +import 'package:di/annotations.dart'; +import 'package:di/src/module.dart' show DEFAULT_VALUE, Binding; +import 'package:angular/core/static_keys.dart'; +import 'package:angular/core_dom/static_keys.dart'; + +import 'package:angular/core/module.dart' show Scope, RootScope; +import 'package:angular/core/annotation.dart' show Visibility, DirectiveBinder; +import 'package:angular/core_dom/module_internal.dart' + show Animate, View, ViewFactory, BoundViewFactory, ViewPort, NodeAttrs, ElementProbe, + NgElement, ContentPort, TemplateLoader, ShadowRootEventHandler, EventHandler; + +var _TAG_GET = new UserTag('DirectiveInjector.get()'); +var _TAG_INSTANTIATE = new UserTag('DirectiveInjector.instantiate()'); + +final DIRECTIVE_INJECTOR_KEY = new Key(DirectiveInjector); +final CONTENT_PORT_KEY = new Key(ContentPort); +final TEMPLATE_LOADER_KEY = new Key(TemplateLoader); +final SHADOW_ROOT_KEY = new Key(ShadowRoot); + +const int VISIBILITY_LOCAL = -1; +const int VISIBILITY_DIRECT_CHILD = -2; +const int VISIBILITY_CHILDREN = -3; +const int VISIBILITY_COMPONENT_OFFSET = VISIBILITY_CHILDREN; +const int VISIBILITY_COMPONENT_LOCAL = VISIBILITY_LOCAL + VISIBILITY_COMPONENT_OFFSET; +const int VISIBILITY_COMPONENT_DIRECT_CHILD = VISIBILITY_DIRECT_CHILD + VISIBILITY_COMPONENT_OFFSET; +const int VISIBILITY_COMPONENT_CHILDREN = VISIBILITY_CHILDREN + VISIBILITY_COMPONENT_OFFSET; + +const int UNDEFINED_ID = 0; +const int INJECTOR_KEY_ID = 1; +const int DIRECTIVE_INJECTOR_KEY_ID = 2; +const int NODE_KEY_ID = 3; +const int ELEMENT_KEY_ID = 4; +const int NODE_ATTRS_KEY_ID = 5; +const int ANIMATE_KEY_ID = 6; +const int SCOPE_KEY_ID = 7; +const int VIEW_KEY_ID = 8; +const int VIEW_PORT_KEY_ID = 9; +const int VIEW_FACTORY_KEY_ID = 10; +const int NG_ELEMENT_KEY_ID = 11; +const int BOUND_VIEW_FACTORY_KEY_ID = 12; +const int ELEMENT_PROBE_KEY_ID = 13; +const int TEMPLATE_LOADER_KEY_ID = 14; +const int SHADOW_ROOT_KEY_ID = 15; +const int CONTENT_PORT_KEY_ID = 16; +const int EVENT_HANDLER_KEY_ID = 17; +const int KEEP_ME_LAST = 18; + +class DirectiveInjector implements DirectiveBinder { + static bool _isInit = false; + static initUID() { + if (_isInit) return; + _isInit = true; + INJECTOR_KEY.uid = INJECTOR_KEY_ID; + DIRECTIVE_INJECTOR_KEY.uid = DIRECTIVE_INJECTOR_KEY_ID; + NODE_KEY.uid = NODE_KEY_ID; + ELEMENT_KEY.uid = ELEMENT_KEY_ID; + NODE_ATTRS_KEY.uid = NODE_ATTRS_KEY_ID; + SCOPE_KEY.uid = SCOPE_KEY_ID; + VIEW_KEY.uid = VIEW_KEY_ID; + VIEW_PORT_KEY.uid = VIEW_PORT_KEY_ID; + VIEW_FACTORY_KEY.uid = VIEW_FACTORY_KEY_ID; + NG_ELEMENT_KEY.uid = NG_ELEMENT_KEY_ID; + BOUND_VIEW_FACTORY_KEY.uid = BOUND_VIEW_FACTORY_KEY_ID; + ELEMENT_PROBE_KEY.uid = ELEMENT_PROBE_KEY_ID; + TEMPLATE_LOADER_KEY.uid = TEMPLATE_LOADER_KEY_ID; + SHADOW_ROOT_KEY.uid = SHADOW_ROOT_KEY_ID; + CONTENT_PORT_KEY.uid = CONTENT_PORT_KEY_ID; + EVENT_HANDLER_KEY.uid = EVENT_HANDLER_KEY_ID; + ANIMATE_KEY.uid = ANIMATE_KEY_ID; + for(var i = 1; i < KEEP_ME_LAST; i++) { + if (_KEYS[i].uid != i) throw 'MISSORDERED KEYS ARRAY: ${_KEYS} at $i'; + } + } + static List _KEYS = + [ UNDEFINED_ID + , INJECTOR_KEY + , DIRECTIVE_INJECTOR_KEY + , NODE_KEY + , ELEMENT_KEY + , NODE_ATTRS_KEY + , ANIMATE_KEY + , SCOPE_KEY + , VIEW_KEY + , VIEW_PORT_KEY + , VIEW_FACTORY_KEY + , NG_ELEMENT_KEY + , BOUND_VIEW_FACTORY_KEY + , ELEMENT_PROBE_KEY + , TEMPLATE_LOADER_KEY + , SHADOW_ROOT_KEY + , CONTENT_PORT_KEY + , EVENT_HANDLER_KEY + , KEEP_ME_LAST + ]; + + final DirectiveInjector parent; + final Injector appInjector; + final Node _node; + final NodeAttrs _nodeAttrs; + final Animate _animate; + final EventHandler _eventHandler; + Scope scope; //TODO(misko): this should be final after we get rid of controller + + NgElement _ngElement; + ElementProbe _elementProbe; + + Key _key0 = null; dynamic _obj0; List _pKeys0; Function _factory0; + Key _key1 = null; dynamic _obj1; List _pKeys1; Function _factory1; + Key _key2 = null; dynamic _obj2; List _pKeys2; Function _factory2; + Key _key3 = null; dynamic _obj3; List _pKeys3; Function _factory3; + Key _key4 = null; dynamic _obj4; List _pKeys4; Function _factory4; + Key _key5 = null; dynamic _obj5; List _pKeys5; Function _factory5; + Key _key6 = null; dynamic _obj6; List _pKeys6; Function _factory6; + Key _key7 = null; dynamic _obj7; List _pKeys7; Function _factory7; + Key _key8 = null; dynamic _obj8; List _pKeys8; Function _factory8; + Key _key9 = null; dynamic _obj9; List _pKeys9; Function _factory9; + + static _toVisId(Visibility v) => identical(v, Visibility.LOCAL) + ? VISIBILITY_LOCAL + : (identical(v, Visibility.CHILDREN) ? VISIBILITY_CHILDREN : VISIBILITY_DIRECT_CHILD); + + static _toVis(int id) { + switch (id) { + case VISIBILITY_LOCAL: return Visibility.LOCAL; + case VISIBILITY_DIRECT_CHILD: return Visibility.DIRECT_CHILD; + case VISIBILITY_CHILDREN: return Visibility.CHILDREN; + case VISIBILITY_COMPONENT_LOCAL: return Visibility.LOCAL; + case VISIBILITY_COMPONENT_DIRECT_CHILD: return Visibility.DIRECT_CHILD; + case VISIBILITY_COMPONENT_CHILDREN: return Visibility.CHILDREN; + default: return null; + } + } + + static Binding _tempBinding = new Binding(); + + DirectiveInjector(parent, appInjector, this._node, this._nodeAttrs, this._eventHandler, + this.scope, this._animate) + : appInjector = appInjector, + parent = parent == null ? new DefaultDirectiveInjector(appInjector) : parent; + + DirectiveInjector._default(this.parent, this.appInjector) + : _node = null, + _nodeAttrs = null, + _eventHandler = null, + scope = null, + _animate = null; + + void bind(key, {dynamic toValue: DEFAULT_VALUE, + Function toFactory: DEFAULT_VALUE, + Type toImplementation, + toInstanceOf, + inject: const[], + Visibility visibility: Visibility.LOCAL}) { + if (key == null) throw 'Key is required'; + if (key is! Key) key = new Key(key); + if (inject is! List) inject = [inject]; + + _tempBinding.bind(key, Module.DEFAULT_REFLECTOR, toValue: toValue, toFactory: toFactory, + toImplementation: toImplementation, inject: inject, toInstanceOf: toInstanceOf); + + bindByKey(key, _tempBinding.factory, _tempBinding.parameterKeys, visibility); + } + + void bindByKey(Key key, Function factory, List parameterKeys, [Visibility visibility]) { + if (visibility == null) visibility = Visibility.CHILDREN; + int visibilityId = _toVisId(visibility); + int keyVisId = key.uid; + if (keyVisId != visibilityId) { + if (keyVisId == null) { + key.uid = visibilityId; + } else { + throw "Can not set $visibility on $key, it alread has ${_toVis(keyVisId)}"; + } + } + if (_key0 == null || identical(_key0, key)) { _key0 = key; _pKeys0 = parameterKeys; _factory0 = factory; } + else if (_key1 == null || identical(_key1, key)) { _key1 = key; _pKeys1 = parameterKeys; _factory1 = factory; } + else if (_key2 == null || identical(_key2, key)) { _key2 = key; _pKeys2 = parameterKeys; _factory2 = factory; } + else if (_key3 == null || identical(_key3, key)) { _key3 = key; _pKeys3 = parameterKeys; _factory3 = factory; } + else if (_key4 == null || identical(_key4, key)) { _key4 = key; _pKeys4 = parameterKeys; _factory4 = factory; } + else if (_key5 == null || identical(_key5, key)) { _key5 = key; _pKeys5 = parameterKeys; _factory5 = factory; } + else if (_key6 == null || identical(_key6, key)) { _key6 = key; _pKeys6 = parameterKeys; _factory6 = factory; } + else if (_key7 == null || identical(_key7, key)) { _key7 = key; _pKeys7 = parameterKeys; _factory7 = factory; } + else if (_key8 == null || identical(_key8, key)) { _key8 = key; _pKeys8 = parameterKeys; _factory8 = factory; } + else if (_key9 == null || identical(_key9, key)) { _key9 = key; _pKeys9 = parameterKeys; _factory9 = factory; } + else { throw 'Maximum number of directives per element reached.'; } + } + + Object get(Type type) => getByKey(new Key(type)); + + Object getByKey(Key key) { + var oldTag = _TAG_GET.makeCurrent(); + try { + return _getByKey(key); + } on ResolvingError catch (e, s) { + e.appendKey(key); + rethrow; + } finally { + oldTag.makeCurrent(); + } + } + + Object _getByKey(Key key) { + int uid = key.uid; + if (uid == null || uid == UNDEFINED_ID) return appInjector.getByKey(key); + bool isDirective = uid < 0; + return isDirective ? _getDirectiveByKey(key, uid, appInjector) : _getById(uid); + } + + Object _getDirectiveByKey(Key k, int visType, Injector i) { + do { + if (_key0 == null) break; if (identical(_key0, k)) return _obj0 == null ? _obj0 = _new(_pKeys0, _factory0) : _obj0; + if (_key1 == null) break; if (identical(_key1, k)) return _obj1 == null ? _obj1 = _new(_pKeys1, _factory1) : _obj1; + if (_key2 == null) break; if (identical(_key2, k)) return _obj2 == null ? _obj2 = _new(_pKeys2, _factory2) : _obj2; + if (_key3 == null) break; if (identical(_key3, k)) return _obj3 == null ? _obj3 = _new(_pKeys3, _factory3) : _obj3; + if (_key4 == null) break; if (identical(_key4, k)) return _obj4 == null ? _obj4 = _new(_pKeys4, _factory4) : _obj4; + if (_key5 == null) break; if (identical(_key5, k)) return _obj5 == null ? _obj5 = _new(_pKeys5, _factory5) : _obj5; + if (_key6 == null) break; if (identical(_key6, k)) return _obj6 == null ? _obj6 = _new(_pKeys6, _factory6) : _obj6; + if (_key7 == null) break; if (identical(_key7, k)) return _obj7 == null ? _obj7 = _new(_pKeys7, _factory7) : _obj7; + if (_key8 == null) break; if (identical(_key8, k)) return _obj8 == null ? _obj8 = _new(_pKeys8, _factory8) : _obj8; + if (_key9 == null) break; if (identical(_key9, k)) return _obj9 == null ? _obj9 = _new(_pKeys9, _factory9) : _obj9; + } while (false); + switch (visType) { + case VISIBILITY_LOCAL: return appInjector.getByKey(k); + case VISIBILITY_DIRECT_CHILD: return parent._getDirectiveByKey(k, VISIBILITY_LOCAL, i); + case VISIBILITY_CHILDREN: return parent._getDirectiveByKey(k, VISIBILITY_CHILDREN, i); + // SHADOW + case VISIBILITY_COMPONENT_LOCAL: return parent._getDirectiveByKey(k, VISIBILITY_LOCAL, i); + case VISIBILITY_COMPONENT_DIRECT_CHILD: return parent._getDirectiveByKey(k, VISIBILITY_DIRECT_CHILD, i); + case VISIBILITY_COMPONENT_CHILDREN: return parent._getDirectiveByKey(k, VISIBILITY_CHILDREN, i); + default: throw null; + } + } + + List get directives { + var directives = []; + if (_obj0 != null) directives.add(_obj0); + if (_obj1 != null) directives.add(_obj1); + if (_obj2 != null) directives.add(_obj2); + if (_obj3 != null) directives.add(_obj3); + if (_obj4 != null) directives.add(_obj4); + if (_obj5 != null) directives.add(_obj5); + if (_obj6 != null) directives.add(_obj6); + if (_obj7 != null) directives.add(_obj7); + if (_obj8 != null) directives.add(_obj8); + if (_obj9 != null) directives.add(_obj9); + return directives; + } + + Object _getById(int keyId) { + switch(keyId) { + case INJECTOR_KEY_ID: return appInjector; + case DIRECTIVE_INJECTOR_KEY_ID: return this; + case NODE_KEY_ID: return _node; + case ELEMENT_KEY_ID: return _node; + case NODE_ATTRS_KEY_ID: return _nodeAttrs; + case ANIMATE_KEY_ID: return _animate; + case SCOPE_KEY_ID: return scope; + case ELEMENT_PROBE_KEY_ID: return elementProbe; + case NG_ELEMENT_KEY_ID: return ngElement; + case EVENT_HANDLER_KEY_ID: return _eventHandler; + case CONTENT_PORT_KEY_ID: return parent._getById(keyId); + default: new NoProviderError(_KEYS[keyId]); + } + } + + dynamic _new(List paramKeys, Function fn) { + var oldTag = _TAG_GET.makeCurrent(); + int size = paramKeys.length; + var obj; + if (size > 15) { + var params = new List(paramKeys.length); + for(var i = 0; i < paramKeys.length; i++) { + params[i] = _getByKey(paramKeys[i]); + } + _TAG_INSTANTIATE.makeCurrent(); + obj = Function.apply(fn, params); + } else { + var a01 = size >= 01 ? _getByKey(paramKeys[00]) : null; + var a02 = size >= 02 ? _getByKey(paramKeys[01]) : null; + var a03 = size >= 03 ? _getByKey(paramKeys[02]) : null; + var a04 = size >= 04 ? _getByKey(paramKeys[03]) : null; + var a05 = size >= 05 ? _getByKey(paramKeys[04]) : null; + var a06 = size >= 06 ? _getByKey(paramKeys[05]) : null; + var a07 = size >= 07 ? _getByKey(paramKeys[06]) : null; + var a08 = size >= 08 ? _getByKey(paramKeys[07]) : null; + var a09 = size >= 09 ? _getByKey(paramKeys[08]) : null; + var a10 = size >= 10 ? _getByKey(paramKeys[09]) : null; + var a11 = size >= 11 ? _getByKey(paramKeys[10]) : null; + var a12 = size >= 12 ? _getByKey(paramKeys[11]) : null; + var a13 = size >= 13 ? _getByKey(paramKeys[12]) : null; + var a14 = size >= 14 ? _getByKey(paramKeys[13]) : null; + var a15 = size >= 15 ? _getByKey(paramKeys[14]) : null; + _TAG_INSTANTIATE.makeCurrent(); + switch(size) { + case 00: obj = fn(); break; + case 01: obj = fn(a01); break; + case 02: obj = fn(a01, a02); break; + case 03: obj = fn(a01, a02, a03); break; + case 04: obj = fn(a01, a02, a03, a04); break; + case 05: obj = fn(a01, a02, a03, a04, a05); break; + case 06: obj = fn(a01, a02, a03, a04, a05, a06); break; + case 07: obj = fn(a01, a02, a03, a04, a05, a06, a07); break; + case 08: obj = fn(a01, a02, a03, a04, a05, a06, a07, a08); break; + case 09: obj = fn(a01, a02, a03, a04, a05, a06, a07, a08, a09); break; + case 10: obj = fn(a01, a02, a03, a04, a05, a06, a07, a08, a09, a10); break; + case 11: obj = fn(a01, a02, a03, a04, a05, a06, a07, a08, a09, a10, a11); break; + case 12: obj = fn(a01, a02, a03, a04, a05, a06, a07, a08, a09, a10, a11, a12); break; + case 13: obj = fn(a01, a02, a03, a04, a05, a06, a07, a08, a09, a10, a11, a12, a13); break; + case 14: obj = fn(a01, a02, a03, a04, a05, a06, a07, a08, a09, a10, a11, a12, a13, a14); break; + case 15: obj = fn(a01, a02, a03, a04, a05, a06, a07, a08, a09, a10, a11, a12, a13, a14, a15); + } + } + oldTag.makeCurrent(); + return obj; + } + + + ElementProbe get elementProbe { + if (_elementProbe == null) { + ElementProbe parentProbe = parent is DirectiveInjector ? parent.elementProbe : null; + _elementProbe = new ElementProbe(parentProbe, _node, this, scope); + } + return _elementProbe; + } + + NgElement get ngElement { + if (_ngElement == null) { + _ngElement = new NgElement(_node, scope, _animate); + } + return _ngElement; + } +} + +class TemplateDirectiveInjector extends DirectiveInjector { + final ViewFactory _viewFactory; + ViewPort _viewPort; + BoundViewFactory _boundViewFactory; + + TemplateDirectiveInjector(DirectiveInjector parent, Injector appInjector, + Node node, NodeAttrs nodeAttrs, EventHandler eventHandler, + Scope scope, Animate animate, this._viewFactory) + : super(parent, appInjector, node, nodeAttrs, eventHandler, scope, animate); + + + Object _getById(int keyId) { + switch(keyId) { + case VIEW_FACTORY_KEY_ID: return _viewFactory; + case VIEW_PORT_KEY_ID: return ((_viewPort) == null) ? + _viewPort = new ViewPort(this, scope, _node, _animate) : _viewPort; + case BOUND_VIEW_FACTORY_KEY_ID: return (_boundViewFactory == null) ? + _boundViewFactory = _viewFactory.bind(this.parent) : _boundViewFactory; + default: return super._getById(keyId); + } + } + +} + +abstract class ComponentDirectiveInjector extends DirectiveInjector { + + final TemplateLoader _templateLoader; + final ShadowRoot _shadowRoot; + + ComponentDirectiveInjector(DirectiveInjector parent, Injector appInjector, + EventHandler eventHandler, Scope scope, + this._templateLoader, this._shadowRoot) + : super(parent, appInjector, parent._node, parent._nodeAttrs, eventHandler, scope, + parent._animate); + + Object _getById(int keyId) { + switch(keyId) { + case TEMPLATE_LOADER_KEY_ID: return _templateLoader; + case SHADOW_ROOT_KEY_ID: return _shadowRoot; + default: return super._getById(keyId); + } + } + + _getDirectiveByKey(Key k, int visType, Injector i) => + super._getDirectiveByKey(k, visType + VISIBILITY_COMPONENT_OFFSET, i); +} + +class ShadowlessComponentDirectiveInjector extends ComponentDirectiveInjector { + final ContentPort _contentPort; + + ShadowlessComponentDirectiveInjector(DirectiveInjector parent, Injector appInjector, + EventHandler eventHandler, Scope scope, + templateLoader, shadowRoot, this._contentPort) + : super(parent, appInjector, eventHandler, scope, templateLoader, shadowRoot); + + Object _getById(int keyId) { + switch(keyId) { + case CONTENT_PORT_KEY_ID: return _contentPort; + default: return super._getById(keyId); + } + } +} + +class ShadowDomComponentDirectiveInjector extends ComponentDirectiveInjector { + ShadowDomComponentDirectiveInjector(DirectiveInjector parent, Injector appInjector, + Scope scope, templateLoader, shadowRoot) + : super(parent, appInjector, new ShadowRootEventHandler(shadowRoot, + parent.getByKey(EXPANDO_KEY), + parent.getByKey(EXCEPTION_HANDLER_KEY)), + scope, templateLoader, shadowRoot); + + ElementProbe get elementProbe { + if (_elementProbe == null) { + ElementProbe parentProbe = + parent is DirectiveInjector ? parent.elementProbe : parent.getByKey(ELEMENT_PROBE_KEY); + _elementProbe = new ElementProbe(parentProbe, _shadowRoot, this, scope); + } + return _elementProbe; + } +} + +@Injectable() +class DefaultDirectiveInjector extends DirectiveInjector { + DefaultDirectiveInjector(Injector appInjector): super._default(null, appInjector); + DefaultDirectiveInjector.newAppInjector(DirectiveInjector parent, Injector appInjector) + : super._default(parent, appInjector); + + Object getByKey(Key key) => appInjector.getByKey(key); + _getDirectiveByKey(Key key, int visType, Injector i) => + parent == null ? i.getByKey(key) : parent._getDirectiveByKey(key, visType, i); + _getById(int keyId) { + switch (keyId) { + case CONTENT_PORT_KEY_ID: return null; + default: throw new NoProviderError(DirectiveInjector._KEYS[keyId]); + } + } +} diff --git a/lib/core_dom/directive_map.dart b/lib/core_dom/directive_map.dart index d1728f909..576587763 100644 --- a/lib/core_dom/directive_map.dart +++ b/lib/core_dom/directive_map.dart @@ -1,18 +1,48 @@ part of angular.core.dom_internal; +class DirectiveTypeTuple { + final Directive directive; + final Type type; + DirectiveTypeTuple(this.directive, this.type); + String toString() => '@$directive#$type'; +} + @Injectable() -class DirectiveMap extends AnnotationsMap { - DirectiveSelectorFactory _directiveSelectorFactory; +class DirectiveMap { + final Map> map = new HashMap>(); + final DirectiveSelectorFactory _directiveSelectorFactory; FormatterMap _formatters; DirectiveSelector _selector; + + DirectiveMap(Injector injector, + this._formatters, + MetadataExtractor metadataExtractor, + this._directiveSelectorFactory) { + (injector as ModuleInjector).types.forEach((type) { + metadataExtractor(type) + .where((annotation) => annotation is Directive) + .forEach((Directive dir) { + map.putIfAbsent(dir.selector, () => []).add(new DirectiveTypeTuple(dir, type)); + }); + }); + } + DirectiveSelector get selector { if (_selector != null) return _selector; return _selector = _directiveSelectorFactory.selector(this, _formatters); } - DirectiveMap(Injector injector, - this._formatters, - MetadataExtractor metadataExtractor, - this._directiveSelectorFactory) - : super(injector, metadataExtractor); + List operator[](String key) { + var value = map[key]; + if (value == null) throw 'No Directive selector $key found!'; + return value; + } + + void forEach(fn(K, Type)) { + map.forEach((_, types) { + types.forEach((tuple) { + fn(tuple.directive, tuple.type); + }); + }); + } } diff --git a/lib/core_dom/element_binder.dart b/lib/core_dom/element_binder.dart index 5f04f7e36..814bd9148 100644 --- a/lib/core_dom/element_binder.dart +++ b/lib/core_dom/element_binder.dart @@ -14,28 +14,15 @@ class TemplateElementBinder extends ElementBinder { return _directiveCache = [template]; } - TemplateElementBinder(perf, expando, parser, componentFactory, - transcludingComponentFactory, shadowDomComponentFactory, + TemplateElementBinder(perf, expando, parser, config, this.template, this.templateBinder, onEvents, bindAttrs, childMode) - : super(perf, expando, parser, componentFactory, - transcludingComponentFactory, shadowDomComponentFactory, + : super(perf, expando, parser, config, null, null, onEvents, bindAttrs, childMode); String toString() => "[TemplateElementBinder template:$template]"; - - _registerViewFactory(node, parentInjector, nodeModule) { - assert(templateViewFactory != null); - nodeModule - ..bindByKey(VIEW_PORT_KEY, toFactory: (_) => - new ViewPort(node, parentInjector.getByKey(ANIMATE_KEY))) - ..bindByKey(VIEW_FACTORY_KEY, toValue: templateViewFactory) - ..bindByKey(BOUND_VIEW_FACTORY_KEY, toFactory: (Injector injector) => - templateViewFactory.bind(injector)); - } } - /** * ElementBinder is created by the Selector and is responsible for instantiating * individual directives and binding element properties. @@ -45,27 +32,21 @@ class ElementBinder { final Profiler _perf; final Expando _expando; final Parser _parser; + final CompilerConfig _config; - // The default component factory - final ComponentFactory _componentFactory; - final TranscludingComponentFactory _transcludingComponentFactory; - final ShadowDomComponentFactory _shadowDomComponentFactory; final Map onEvents; final Map bindAttrs; // Member fields final decorators; - final DirectiveRef component; + final BoundComponentData componentData; // Can be either COMPILE_CHILDREN or IGNORE_CHILDREN final String childMode; - ElementBinder(this._perf, this._expando, this._parser, - this._componentFactory, - this._transcludingComponentFactory, - this._shadowDomComponentFactory, - this.component, this.decorators, + ElementBinder(this._perf, this._expando, this._parser, this._config, + this.componentData, this.decorators, this.onEvents, this.bindAttrs, this.childMode); final bool hasTemplate = false; @@ -76,16 +57,16 @@ class ElementBinder { var _directiveCache; List get _usableDirectiveRefs { if (_directiveCache != null) return _directiveCache; - if (component != null) return _directiveCache = new List.from(decorators)..add(component); + if (componentData != null) return _directiveCache = new List.from(decorators)..add(componentData.ref); return _directiveCache = decorators; } bool get hasDirectivesOrEvents => - _usableDirectiveRefs.isNotEmpty || onEvents.isNotEmpty; + _usableDirectiveRefs.isNotEmpty || onEvents.isNotEmpty || bindAttrs.isNotEmpty; void _bindTwoWay(tasks, AST ast, scope, directiveScope, controller, AST dstAST) { - var taskId = tasks.registerTask(); + var taskId = (tasks != null) ? tasks.registerTask() : 0; var viewOutbound = false; var viewInbound = false; @@ -94,7 +75,7 @@ class ElementBinder { viewOutbound = true; scope.rootScope.runAsync(() => viewOutbound = false); var value = dstAST.parsedExp.assign(controller, inboundValue); - tasks.completeTask(taskId); + if (tasks != null) tasks.completeTask(taskId); return value; } }); @@ -104,18 +85,18 @@ class ElementBinder { viewInbound = true; scope.rootScope.runAsync(() => viewInbound = false); ast.parsedExp.assign(scope.context, outboundValue); - tasks.completeTask(taskId); + if (tasks != null) tasks.completeTask(taskId); } }); } } - _bindOneWay(tasks, ast, scope, AST dstAST, controller) { - var taskId = tasks.registerTask(); + void _bindOneWay(tasks, ast, scope, AST dstAST, controller) { + var taskId = (tasks != null) ? tasks.registerTask() : 0; scope.watchAST(ast, (v, _) { dstAST.parsedExp.assign(controller, v); - tasks.completeTask(taskId); + if (tasks != null) tasks.completeTask(taskId); }); } @@ -126,7 +107,8 @@ class ElementBinder { void _createAttrMappings(directive, scope, List mappings, nodeAttrs, tasks) { Scope directiveScope; // Only created if there is a two-way binding in the element. - mappings.forEach((MappingParts p) { + for(var i = 0; i < mappings.length; i++) { + MappingParts p = mappings[i]; var attrName = p.attrName; var attrValueAST = p.attrValueAST; AST dstAST = p.dstAST; @@ -137,7 +119,7 @@ class ElementBinder { } // Check if there is a bind attribute for this mapping. - var bindAttr = bindAttrs["bind-${p.attrName}"]; + var bindAttr = bindAttrs[p.attrName]; if (bindAttr != null) { if (p.mode == '<=>') { if (directiveScope == null) { @@ -150,20 +132,20 @@ class ElementBinder { } else { _bindOneWay(tasks, bindAttr, scope, dstAST, directive); } - return; + continue; } switch (p.mode) { case '@': // string - var taskId = tasks.registerTask(); + var taskId = (tasks != null) ? tasks.registerTask() : 0; nodeAttrs.observe(attrName, (value) { dstAST.parsedExp.assign(directive, value); - tasks.completeTask(taskId); + if (tasks != null) tasks.completeTask(taskId); }); break; case '<=>': // two-way - if (nodeAttrs[attrName] == null) return; + if (nodeAttrs[attrName] == null) continue; if (directiveScope == null) { directiveScope = scope.createChild(directive); } @@ -172,13 +154,12 @@ class ElementBinder { break; case '=>': // one-way - if (nodeAttrs[attrName] == null) return; - _bindOneWay(tasks, attrValueAST, scope, - dstAST, directive); + if (nodeAttrs[attrName] == null) continue; + _bindOneWay(tasks, attrValueAST, scope, dstAST, directive); break; case '=>!': // one-way, one-time - if (nodeAttrs[attrName] == null) return; + if (nodeAttrs[attrName] == null) continue; var watch; var lastOneTimeValue; @@ -201,21 +182,23 @@ class ElementBinder { _bindCallback(dstAST.parsedExp, directive, nodeAttrs[attrName], scope); break; } - }); + } } - void _link(nodeInjector, probe, scope, nodeAttrs) { - _usableDirectiveRefs.forEach((DirectiveRef ref) { - var directive = nodeInjector.getByKey(ref.typeKey); - probe.directives.add(directive); + void _link(DirectiveInjector directiveInjector, Scope scope, nodeAttrs) { + for(var i = 0; i < _usableDirectiveRefs.length; i++) { + DirectiveRef ref = _usableDirectiveRefs[i]; + var key = ref.typeKey; + if (identical(key, TEXT_MUSTACHE_KEY) || identical(key, ATTR_MUSTACHE_KEY)) continue; + var directive = directiveInjector.getByKey(ref.typeKey); if (ref.annotation is Controller) { - scope.context[(ref.annotation as Controller).publishAs] = directive; + scope.parentScope.context[(ref.annotation as Controller).publishAs] = directive; } - var tasks = new _TaskList(directive is AttachAware ? () { + var tasks = directive is AttachAware ? new _TaskList(() { if (scope.isAttached) directive.attach(); - } : null); + }) : null; if (ref.mappings.isNotEmpty) { if (nodeAttrs == null) nodeAttrs = new _AnchorAttrs(ref); @@ -223,108 +206,109 @@ class ElementBinder { } if (directive is AttachAware) { - var taskId = tasks.registerTask(); + var taskId = (tasks != null) ? tasks.registerTask() : 0; Watch watch; watch = scope.watch('1', // Cheat a bit. (_, __) { watch.remove(); - tasks.completeTask(taskId); + if (tasks != null) tasks.completeTask(taskId); }); } - tasks.doneRegistering(); + if (tasks != null) tasks.doneRegistering(); if (directive is DetachAware) { scope.on(ScopeEvent.DESTROY).listen((_) => directive.detach()); } - }); + } } - void _createDirectiveFactories(DirectiveRef ref, nodeModule, node, nodesAttrsDirectives, nodeAttrs, - visibility) { - if (ref.type == TextMustache) { - nodeModule.bind(TextMustache, toFactory: (Injector injector) { - return new TextMustache(node, ref.valueAST, injector.getByKey(SCOPE_KEY)); - }); - } else if (ref.type == AttrMustache) { - if (nodesAttrsDirectives.isEmpty) { - nodeModule.bind(AttrMustache, toFactory: (Injector injector) { - var scope = injector.getByKey(SCOPE_KEY); - for (var ref in nodesAttrsDirectives) { - new AttrMustache(nodeAttrs, ref.value, ref.valueAST, scope); - } - }); - } - nodesAttrsDirectives.add(ref); + void _createDirectiveFactories(DirectiveRef ref, DirectiveInjector nodeInjector, node, + nodeAttrs) { + if (ref.typeKey == TEXT_MUSTACHE_KEY) { + new TextMustache(node, ref.valueAST, nodeInjector.scope); + } else if (ref.typeKey == ATTR_MUSTACHE_KEY) { + new AttrMustache(nodeAttrs, ref.value, ref.valueAST, nodeInjector.scope); } else if (ref.annotation is Component) { - var factory; - var annotation = ref.annotation as Component; - if (annotation.useShadowDom == true) { - factory = _shadowDomComponentFactory; - } else if (annotation.useShadowDom == false) { - factory = _transcludingComponentFactory; - } else { - factory = _componentFactory; - } - nodeModule.bindByKey(ref.typeKey, toFactory: factory.call(node, ref), visibility: visibility); + assert(ref == componentData.ref); + + BoundComponentFactory boundComponentFactory = componentData.factory; + Function componentFactory = boundComponentFactory.call(node); + nodeInjector.bindByKey(ref.typeKey, componentFactory, + boundComponentFactory.callArgs, ref.annotation.visibility); } else { - nodeModule.bindByKey(ref.typeKey, visibility: visibility); + nodeInjector.bindByKey(ref.typeKey, ref.factory, ref.paramKeys, ref.annotation.visibility); } } - // Overridden in TemplateElementBinder - void _registerViewFactory(node, parentInjector, nodeModule) { - nodeModule..bindByKey(VIEW_PORT_KEY, toValue: null) - ..bindByKey(VIEW_FACTORY_KEY, toValue: null) - ..bindByKey(BOUND_VIEW_FACTORY_KEY, toValue: null); - } - - - Injector bind(View view, Injector parentInjector, dom.Node node) { - Injector nodeInjector; - Scope scope = parentInjector.getByKey(SCOPE_KEY); + DirectiveInjector bind(View view, Scope scope, + DirectiveInjector parentInjector, + dom.Node node, EventHandler eventHandler, Animate animate) { var nodeAttrs = node is dom.Element ? new NodeAttrs(node) : null; - ElementProbe probe; var directiveRefs = _usableDirectiveRefs; if (!hasDirectivesOrEvents) return parentInjector; - var nodesAttrsDirectives = []; - var nodeModule = new Module() - ..bindByKey(NG_ELEMENT_KEY) - ..bindByKey(VIEW_KEY, toValue: view) - ..bindByKey(ELEMENT_KEY, toValue: node) - ..bindByKey(NODE_KEY, toValue: node) - ..bindByKey(NODE_ATTRS_KEY, toValue: nodeAttrs) - ..bindByKey(ELEMENT_PROBE_KEY, toFactory: (_) => probe); + DirectiveInjector nodeInjector; + if (this is TemplateElementBinder) { + nodeInjector = new TemplateDirectiveInjector(parentInjector, parentInjector.appInjector, + node, nodeAttrs, eventHandler, scope, animate, + (this as TemplateElementBinder).templateViewFactory); + } else { + nodeInjector = new DirectiveInjector(parentInjector, parentInjector.appInjector, + node, nodeAttrs, eventHandler, scope, animate); + } - directiveRefs.forEach((DirectiveRef ref) { + for(var i = 0; i < directiveRefs.length; i++) { + DirectiveRef ref = directiveRefs[i]; Directive annotation = ref.annotation; - var visibility = ref.annotation.visibility; if (ref.annotation is Controller) { - scope = scope.createChild(new PrototypeMap(scope.context)); - nodeModule.bind(Scope, toValue: scope); + scope = nodeInjector.scope = scope.createChild(new PrototypeMap(scope.context)); } - - _createDirectiveFactories(ref, nodeModule, node, nodesAttrsDirectives, nodeAttrs, - visibility); + _createDirectiveFactories(ref, nodeInjector, node, nodeAttrs); if (ref.annotation.module != null) { - nodeModule.install(ref.annotation.module()); + DirectiveBinderFn config = ref.annotation.module; + if (config != null) config(nodeInjector); } - }); + if (_config.elementProbeEnabled && ref.valueAST != null) { + nodeInjector.elementProbe.bindingExpressions.add(ref.valueAST.expression); + } + } - _registerViewFactory(node, parentInjector, nodeModule); + if (_config.elementProbeEnabled) { + _expando[node] = nodeInjector.elementProbe; + // TODO(misko): pretty sure that clearing Expando is not necessary. Remove? + scope.on(ScopeEvent.DESTROY).listen((_) => _expando[node] = null); + } - nodeInjector = parentInjector.createChild([nodeModule]); - probe = _expando[node] = new ElementProbe( - parentInjector.getByKey(ELEMENT_PROBE_KEY), node, nodeInjector, scope); - scope.on(ScopeEvent.DESTROY).listen((_) {_expando[node] = null;}); + _link(nodeInjector, scope, nodeAttrs); - _link(nodeInjector, probe, scope, nodeAttrs); + var jsNode; + List bindAssignableProps = []; + bindAttrs.forEach((String prop, AST ast) { + if (jsNode == null) jsNode = new js.JsObject.fromBrowserObject(node); + scope.watchAST(ast, (v, _) { + jsNode[prop] = v; + }); - onEvents.forEach((event, value) { - view.registerEvent(EventHandler.attrNameToEventName(event)); + if (ast.parsedExp.isAssignable) { + bindAssignableProps.add([prop, ast.parsedExp]); + } }); + + if (bindAssignableProps.isNotEmpty) { + node.addEventListener('change', (_) { + bindAssignableProps.forEach((propAndExp) { + propAndExp[1].assign(scope.context, jsNode[propAndExp[0]]); + }); + }); + } + + if (onEvents.isNotEmpty) { + onEvents.forEach((event, value) { + view.registerEvent(EventHandler.attrNameToEventName(event)); + }); + } return nodeInjector; } diff --git a/lib/core_dom/element_binder_builder.dart b/lib/core_dom/element_binder_builder.dart index 9f02551c0..dfa070d71 100644 --- a/lib/core_dom/element_binder_builder.dart +++ b/lib/core_dom/element_binder_builder.dart @@ -4,28 +4,27 @@ part of angular.core.dom_internal; class ElementBinderFactory { final Parser _parser; final Profiler _perf; + final CompilerConfig _config; final Expando _expando; - final ComponentFactory _componentFactory; - final TranscludingComponentFactory _transcludingComponentFactory; - final ShadowDomComponentFactory _shadowDomComponentFactory; - final ASTParser _astParser; + final ASTParser astParser; + final ComponentFactory componentFactory; + final ShadowDomComponentFactory shadowDomComponentFactory; + final TranscludingComponentFactory transcludingComponentFactory; - ElementBinderFactory(this._parser, this._perf, this._expando, this._componentFactory, - this._transcludingComponentFactory, this._shadowDomComponentFactory, - this._astParser); + ElementBinderFactory(this._parser, this._perf, this._config, this._expando, + this.astParser, this.componentFactory, this.shadowDomComponentFactory, this.transcludingComponentFactory); // TODO: Optimize this to re-use a builder. - ElementBinderBuilder builder(FormatterMap formatters) => - new ElementBinderBuilder(this, _astParser, formatters); + ElementBinderBuilder builder(FormatterMap formatters, DirectiveMap directives) => + new ElementBinderBuilder(this,formatters, directives); ElementBinder binder(ElementBinderBuilder b) => - new ElementBinder(_perf, _expando, _parser, _componentFactory, - _transcludingComponentFactory, _shadowDomComponentFactory, - b.component, b.decorators, b.onEvents, b.bindAttrs, b.childMode); + + new ElementBinder(_perf, _expando, _parser, _config, + b.componentData, b.decorators, b.onEvents, b.bindAttrs, b.childMode); TemplateElementBinder templateBinder(ElementBinderBuilder b, ElementBinder transclude) => - new TemplateElementBinder(_perf, _expando, _parser, _componentFactory, - _transcludingComponentFactory, _shadowDomComponentFactory, + new TemplateElementBinder(_perf, _expando, _parser, _config, b.template, transclude, b.onEvents, b.bindAttrs, b.childMode); } @@ -36,23 +35,23 @@ class ElementBinderFactory { class ElementBinderBuilder { static final RegExp _MAPPING = new RegExp(r'^(@|=>!|=>|<=>|&)\s*(.*)$'); - ElementBinderFactory _factory; - ASTParser _astParser; - FormatterMap _formatters; + final ElementBinderFactory _factory; + final DirectiveMap _directives; + final FormatterMap _formatters; /// "on-*" attribute names and values, added by a [DirectiveSelector] - final onEvents = {}; + final onEvents = new HashMap(); /// "bind-*" attribute names and values, added by a [DirectiveSelector] - final bindAttrs = {}; + final bindAttrs = new HashMap(); final decorators = []; DirectiveRef template; - DirectiveRef component; + BoundComponentData componentData; // Can be either COMPILE_CHILDREN or IGNORE_CHILDREN String childMode = Directive.COMPILE_CHILDREN; - ElementBinderBuilder(this._factory, this._astParser, this._formatters); + ElementBinderBuilder(this._factory, this._formatters, this._directives); /** * Adds [DirectiveRef]s to this [ElementBinderBuilder]. @@ -70,7 +69,17 @@ class ElementBinderBuilder { if (annotation.children == Directive.TRANSCLUDE_CHILDREN) { template = ref; } else if (annotation is Component) { - component = ref; + ComponentFactory factory; + var annotation = ref.annotation as Component; + if (annotation.useShadowDom == true) { + factory = _factory.shadowDomComponentFactory; + } else if (annotation.useShadowDom == false) { + factory = _factory.transcludingComponentFactory; + } else { + factory = _factory.componentFactory; + } + + componentData = new BoundComponentData(ref, () => factory.bind(ref, _directives)); } else { decorators.add(ref); } @@ -89,14 +98,14 @@ class ElementBinderBuilder { var dstPath = match[2]; String dstExpression = dstPath.isEmpty ? attrName : dstPath; - AST dstAST = _astParser(dstExpression); // no formatters + AST dstAST = _factory.astParser(dstExpression); // no formatters // Look up the value of attrName and compute an AST AST ast; if (mode != '@' && mode != '&') { var value = attrName == "." ? ref.value : (ref.element as dom.Element).attributes[attrName]; if (value == null || value.isEmpty) { value = "''"; } - ast = _astParser(value, formatters: _formatters); + ast = _factory.astParser(value, formatters: _formatters); } ref.mappings.add(new MappingParts(attrName, ast, mode, dstAST, mapping)); @@ -110,3 +119,30 @@ class ElementBinderBuilder { return template == null ? elBinder : _factory.templateBinder(this, elBinder); } } + +/** + * Data used by the ComponentFactory to construct components. + */ +class BoundComponentData { + final DirectiveRef ref; + BoundComponentFactory _instance; + Function _gen; + BoundComponentFactory get factory { + if (_instance != null) return _instance; + _instance = _gen(); + _gen = null; // Clear the gen function for GC. + return _instance; + } + + Component get component => ref.annotation as Component; + @Deprecated('Use typeKey instead') + Type get type => ref.type; + Key get typeKey => ref.typeKey; + + + /** + * * [ref]: The components directive ref + * * [_gen]: A function which returns a [BoundComponentFactory]. Called lazily. + */ + BoundComponentData(this.ref, this._gen); +} diff --git a/lib/core_dom/event_handler.dart b/lib/core_dom/event_handler.dart index 1a0f5d9b5..b47fb59b7 100644 --- a/lib/core_dom/event_handler.dart +++ b/lib/core_dom/event_handler.dart @@ -31,7 +31,7 @@ class EventHandler { dom.Node _rootNode; final Expando _expando; final ExceptionHandler _exceptionHandler; - final _listeners = {}; + final _listeners = new HashMap(); EventHandler(this._rootNode, this._expando, this._exceptionHandler); diff --git a/lib/core_dom/http.dart b/lib/core_dom/http.dart index 67fa4f0ea..6f211205f 100644 --- a/lib/core_dom/http.dart +++ b/lib/core_dom/http.dart @@ -38,6 +38,11 @@ typedef RequestInterceptor(HttpResponseConfig); typedef RequestErrorInterceptor(dynamic); typedef Response(HttpResponse); typedef ResponseError(dynamic); +typedef _CompleteResponse(HttpResponse); +typedef _RunCoaleced(fn()); + +_runNow(fn()) => fn(); +_identity(x) => x; /** * HttpInterceptors are used to modify the Http request. They can be added to @@ -69,7 +74,7 @@ class HttpInterceptor { class DefaultTransformDataHttpInterceptor implements HttpInterceptor { Function request = (HttpResponseConfig config) { if (config.data != null && config.data is! String && - config.data is! dom.File) { + config.data is! dom.File && config.data is! dom.FormData) { config.data = JSON.encode(config.data); } return config; @@ -244,23 +249,35 @@ class HttpDefaultHeaders { 'PATCH' : {'Content-Type': _defaultContentType} }; - _applyHeaders(method, ucHeaders, headers) { + _applyHeaders(method, ucHeaders, headers, data) { if (!_headers.containsKey(method)) return; + final removeContentTypeHeader = data != null && data is dom.FormData; + String contentTypeHeader; _headers[method].forEach((k, v) { - if (!ucHeaders.contains(k.toUpperCase())) { + final kUpper = k.toUpperCase(); + if(removeContentTypeHeader && kUpper == 'CONTENT-TYPE') { + contentTypeHeader = k; + } + + if (!ucHeaders.contains(kUpper) + && !(kUpper == 'CONTENT-TYPE' && removeContentTypeHeader)) { headers[k] = v; } }); + + if(removeContentTypeHeader && contentTypeHeader != null) { + headers.remove(contentTypeHeader); + } } /** * Called from [Http], this method sets default headers on [headers] */ - setHeaders(Map headers, String method) { + setHeaders(Map headers, String method, dynamic data) { assert(headers != null); var ucHeaders = headers.keys.map((x) => x.toUpperCase()).toSet(); - _applyHeaders('COMMON', ucHeaders, headers); - _applyHeaders(method.toUpperCase(), ucHeaders, headers); + _applyHeaders('COMMON', ucHeaders, headers, data); + _applyHeaders(method.toUpperCase(), ucHeaders, headers, data); } /** @@ -344,8 +361,9 @@ class HttpDefaults { * can be configured using the [HttpDefaultHeaders] object. The defaults are: * * - For all requests: `Accept: application/json, text/plain, * / *` - * - For POST, PUT, PATCH requests: `Content-Type: application/json` - * + * - For POST, PUT, PATCH requests that supply [dom.FormData]: `Content-Type: multipart/form-data` + * - For all other POST, PUT, PATCH requests: `Content-Type: application/json` + * * # Caching * * To enable caching, pass a [Cache] object into the [call] method. The [Http] @@ -369,23 +387,29 @@ class HttpDefaults { */ @Injectable() class Http { - var _pendingRequests = >{}; - BrowserCookies _cookies; - LocationWrapper _location; - UrlRewriter _rewriter; - HttpBackend _backend; - HttpInterceptors _interceptors; + final _pendingRequests = new HashMap>(); + final BrowserCookies _cookies; + final LocationWrapper _location; + final UrlRewriter _rewriter; + final HttpBackend _backend; + final HttpInterceptors _interceptors; + final RootScope _rootScope; + final HttpConfig _httpConfig; + final VmTurnZone _zone; + + final _responseQueue = []; + async.Timer _responseQueueTimer; /** * The defaults for [Http] */ - HttpDefaults defaults; + final HttpDefaults defaults; /** * 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, this._rootScope, this._httpConfig, this._zone); /** * Parse a [requestUrl] and determine whether this is a same-origin request as @@ -442,7 +466,7 @@ class Http { method = method.toUpperCase(); if (headers == null) headers = {}; - defaults.headers.setHeaders(headers, method); + defaults.headers.setHeaders(headers, method, data); var xsrfValue = _urlIsSameOrigin(url) ? _cookies[xsrfCookieName != null ? xsrfCookieName : defaults.xsrfCookieName] : @@ -457,7 +481,7 @@ class Http { if (v is Function) headers[k] = v(); }); - var serverRequest = (HttpResponseConfig config) { + serverRequest(HttpResponseConfig config) { // Strip content-type if data is undefined if (config.data == null) { new List.from(headers.keys) @@ -482,38 +506,34 @@ class Http { return new async.Future.value(new HttpResponse.copy(cachedResponse)); } - var result = _backend.request(url, - method: method, - requestHeaders: config.headers, - sendData: config.data, - withCredentials: withCredentials).then((dom.HttpRequest value) { - // 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); - - if (cache != null) cache.put(url, response); - _pendingRequests.remove(url); - return response; - }, onError: (error) { - if (error is! dom.ProgressEvent) throw error; - dom.ProgressEvent event = error; - _pendingRequests.remove(url); - dom.HttpRequest request = event.currentTarget; - return new async.Future.error( - new HttpResponse(request.status, request.response, parseHeaders(request), config)); - }); - return _pendingRequests[url] = result; + requestFromBackend(runCoalesced, onComplete, onError) => _backend.request( + url, + method: method, + requestHeaders: config.headers, + sendData: config.data, + withCredentials: withCredentials + ).then((dom.HttpRequest req) => _onResponse(req, runCoalesced, onComplete, config, cache, url), + onError: (e) => _onError(e, runCoalesced, onError, config, url)); + + async.Future responseFuture; + if (_httpConfig.coalesceDuration != null) { + async.Completer completer = new async.Completer(); + responseFuture = completer.future; + _zone.runOutsideAngular(() => requestFromBackend( + _coalesce, completer.complete, completer.completeError)); + } else { + responseFuture = requestFromBackend(_runNow, _identity, _identity); + } + return _pendingRequests[url] = responseFuture; }; var chain = [[serverRequest, null]]; - var future = new async.Future.value(new HttpResponseConfig( + var initialInput = new HttpResponseConfig( url: url, params: params, headers: headers, - data: data)); + data: data); _interceptors.constructChain(chain); @@ -525,11 +545,18 @@ class Http { interceptors.constructChain(chain); } - chain.forEach((chainFns) { - future = future.then(chainFns[0], onError: chainFns[1]); - }); - - return future; + // Try to run interceptors synchronously until one of them returns a Future. This + // makes sure that in common cases the HTTP backend sends the HTTP request immediately + // saving dozens of millis of RPC latency. + var chainResult = chain.fold(initialInput, (prev, chainFns) => prev is async.Future + ? prev.then(chainFns[0], onError: chainFns[1]) + : chainFns[0](prev)); + + // Depending on the implementation of HttpBackend (e.g. with a local cache) the entire + // chain could finish synchronously with a non-Future result. + return chainResult is async.Future + ? chainResult + : new async.Future.value(chainResult); } /** @@ -643,13 +670,52 @@ class Http { xsrfCookieName: xsrfCookieName, interceptors: interceptors, cache: cache, timeout: timeout); + _onResponse(dom.HttpRequest request, _RunCoaleced runCoalesced, _CompleteResponse onComplete, + HttpResponseConfig config, cache, String url) { + // TODO: Uncomment after apps migrate off of this class. + // assert(request.status >= 200 && request.status < 300); + + var response = new HttpResponse( + request.status, request.responseText, parseHeaders(request), config); + + if (cache != null) cache.put(url, response); + _pendingRequests.remove(url); + return runCoalesced(() => onComplete(response)); + } + + _onError(error, _RunCoaleced runCoalesced, _CompleteResponse onError, + HttpResponseConfig config, String url) { + if (error is! dom.ProgressEvent) throw error; + dom.ProgressEvent event = error; + _pendingRequests.remove(url); + dom.HttpRequest request = event.currentTarget; + var response = new HttpResponse( + request.status, request.response, parseHeaders(request), config); + return runCoalesced(() => onError(new async.Future.error(response))); + } + + _coalesce(fn()) { + _responseQueue.add(fn); + if (_responseQueueTimer == null) { + _responseQueueTimer = new async.Timer(_httpConfig.coalesceDuration, _flushResponseQueue); + } + } + + _flushResponseQueue() => _zone.run(_flushResponseQueueSync); + + _flushResponseQueueSync() { + _responseQueueTimer = null; + _responseQueue.forEach(_runNow); + _responseQueue.clear(); + } + /** * Parse raw headers into key-value object */ - static Map parseHeaders(dom.HttpRequest value) { - var headers = value.getAllResponseHeaders(); + static Map parseHeaders(dom.HttpRequest request) { + var headers = request.getAllResponseHeaders(); - var parsed = {}; + var parsed = new HashMap(); if (headers == null) return parsed; @@ -697,3 +763,10 @@ class Http { .replaceAll('%2C', ',') .replaceAll('%20', pctEncodeSpaces ? '%20' : '+'); } + +@Injectable() +class HttpConfig { + final Duration coalesceDuration; + + HttpConfig({this.coalesceDuration}); +} diff --git a/lib/core_dom/module_internal.dart b/lib/core_dom/module_internal.dart index 01b9a05cd..e599d30ae 100644 --- a/lib/core_dom/module_internal.dart +++ b/lib/core_dom/module_internal.dart @@ -6,27 +6,31 @@ import 'dart:html' as dom; import 'dart:js' as js; import 'package:di/di.dart'; +import 'package:di/annotations.dart'; import 'package:perf_api/perf_api.dart'; +import 'package:angular/cache/module.dart'; + import 'package:angular/core/annotation.dart'; -import 'package:angular/core/annotation_src.dart' show SHADOW_DOM_INJECTOR_NAME; import 'package:angular/core/module_internal.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/core_dom/dom_util.dart' as util; import 'package:angular/core_dom/static_keys.dart'; +import 'package:angular/core_dom/directive_injector.dart'; +export 'package:angular/core_dom/directive_injector.dart' show DirectiveInjector; import 'package:angular/change_detection/watch_group.dart' show Watch, PrototypeMap; import 'package:angular/change_detection/ast_parser.dart'; import 'package:angular/core/registry.dart'; import 'package:angular/directive/module.dart' show NgBaseCss; +import 'dart:collection'; part 'animation.dart'; -part 'view.dart'; -part 'view_factory.dart'; part 'cookies.dart'; part 'common.dart'; part 'compiler.dart'; +part 'compiler_config.dart'; part 'directive.dart'; part 'directive_map.dart'; part 'element_binder.dart'; @@ -34,8 +38,8 @@ part 'element_binder_builder.dart'; part 'event_handler.dart'; part 'http.dart'; part 'mustache.dart'; +part 'ng_element.dart'; part 'node_cursor.dart'; -part 'web_platform.dart'; part 'selector.dart'; part 'shadow_dom_component_factory.dart'; part 'shadowless_shadow_root.dart'; @@ -44,8 +48,10 @@ part 'tagging_view_factory.dart'; part 'template_cache.dart'; part 'transcluding_component_factory.dart'; part 'tree_sanitizer.dart'; +part 'view.dart'; +part 'view_factory.dart'; part 'walking_compiler.dart'; -part 'ng_element.dart'; +part 'web_platform.dart'; class CoreDomModule extends Module { CoreDomModule() { @@ -53,28 +59,34 @@ class CoreDomModule extends Module { bind(ElementProbe, toValue: null); // Default to a unlimited-sized TemplateCache - bind(TemplateCache, toFactory: (_) => new TemplateCache()); + bind(TemplateCache, toFactory: (CacheRegister register) { + var templateCache = new TemplateCache(); + register.registerCache("TemplateCache", templateCache); + return templateCache; + }, inject: [CACHE_REGISTER_KEY]); bind(dom.NodeTreeSanitizer, toImplementation: NullTreeSanitizer); bind(TextMustache); bind(AttrMustache); bind(Compiler, toImplementation: TaggingCompiler); + bind(CompilerConfig); - bind(ComponentFactory, toImplementation: ShadowDomComponentFactory); + bind(ComponentFactory, toInstanceOf: SHADOW_DOM_COMPONENT_FACTORY_KEY); bind(ShadowDomComponentFactory); bind(TranscludingComponentFactory); bind(Content); bind(ContentPort, toValue: null); bind(ComponentCssRewriter); bind(WebPlatform); - + bind(Http); bind(UrlRewriter); bind(HttpBackend); bind(HttpDefaultHeaders); bind(HttpDefaults); bind(HttpInterceptors); + bind(HttpConfig, toValue: new HttpConfig()); bind(Animate); bind(ViewCache); bind(BrowserCookies); diff --git a/lib/core_dom/mustache.dart b/lib/core_dom/mustache.dart index b478e0205..920cbdea1 100644 --- a/lib/core_dom/mustache.dart +++ b/lib/core_dom/mustache.dart @@ -26,9 +26,9 @@ class AttrMustache { // This Directive is special and does not go through injection. AttrMustache(this._attrs, - String this._attrName, - AST valueAST, - Scope scope) { + String this._attrName, + AST valueAST, + Scope scope) { _updateMarkup('', 'INITIAL-VALUE'); _attrs.listenObserverChanges(_attrName, (hasObservers) { diff --git a/lib/core_dom/ng_element.dart b/lib/core_dom/ng_element.dart index b6d12d022..08de548d2 100644 --- a/lib/core_dom/ng_element.dart +++ b/lib/core_dom/ng_element.dart @@ -8,8 +8,8 @@ class NgElement { final Scope _scope; final Animate _animate; - final _classesToUpdate = {}; - final _attributesToUpdate = {}; + final _classesToUpdate = new HashMap(); + final _attributesToUpdate = new HashMap(); bool _writeScheduled = false; diff --git a/lib/core_dom/node_cursor.dart b/lib/core_dom/node_cursor.dart index 5b6109a8b..ae71a7252 100644 --- a/lib/core_dom/node_cursor.dart +++ b/lib/core_dom/node_cursor.dart @@ -45,5 +45,5 @@ class NodeCursor { NodeCursor remove() => new NodeCursor([elements.removeAt(index)..remove()]); - toString() => "[NodeCursor: $elements $index]"; + String toString() => "[NodeCursor: $elements $index]"; } diff --git a/lib/core_dom/selector.dart b/lib/core_dom/selector.dart index cc8d948e2..1bb1b9977 100644 --- a/lib/core_dom/selector.dart +++ b/lib/core_dom/selector.dart @@ -21,6 +21,9 @@ part of angular.core.dom_internal; * * [*=/abc/] */ class DirectiveSelector { + static String BIND_PREFIX = "bind-"; + static int BIND_PREFIX_LENGTH = 5; + ElementBinderFactory _binderFactory; DirectiveMap _directives; Interpolate _interpolate; @@ -41,9 +44,9 @@ class DirectiveSelector { } if ((match = _CONTAINS_REGEXP.firstMatch(selector)) != null) { - textSelector.add(new _ContainsSelector(annotation, match[1])); + textSelector.add(new _ContainsSelector(selector, match[1])); } else if ((match = _ATTR_CONTAINS_REGEXP.firstMatch(selector)) != null) { - attrSelector.add(new _ContainsSelector(annotation, match[1])); + attrSelector.add(new _ContainsSelector(selector, match[1])); } else if ((selectorParts = _splitCss(selector, type)) != null){ elementSelector.addDirective(selectorParts, new _Directive(type, annotation)); } else { @@ -59,10 +62,10 @@ class DirectiveSelector { ElementBinder matchElement(dom.Node node) { assert(node is dom.Element); - ElementBinderBuilder builder = _binderFactory.builder(_formatters); + ElementBinderBuilder builder = _binderFactory.builder(_formatters, _directives); List<_ElementSelector> partialSelection; final classes = new Set(); - final attrs = {}; + final attrs = new HashMap(); dom.Element element = node; String nodeName = element.tagName.toLowerCase(); @@ -86,8 +89,9 @@ class DirectiveSelector { if (attrName.startsWith("on-")) { builder.onEvents[attrName] = value; - } else if (attrName.startsWith("bind-")) { - builder.bindAttrs[attrName] = _astParser(value, formatters: _formatters); + } else if (attrName.startsWith(BIND_PREFIX)) { + builder.bindAttrs[attrName.substring(BIND_PREFIX_LENGTH)] = + _astParser(value, formatters: _formatters); } attrs[attrName] = value; @@ -97,12 +101,12 @@ class DirectiveSelector { // 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. - _directives[selectorRegExp.annotation].forEach((type) { + _directives[selectorRegExp.selector].forEach((DirectiveTypeTuple tuple) { // Pre-compute the AST to watch this value. String expression = _interpolate(value); AST valueAST = _astParser(expression, formatters: _formatters); builder.addDirective(new DirectiveRef( - node, type, selectorRegExp.annotation, new Key(type), attrName, valueAST)); + node, tuple.type, tuple.directive, new Key(tuple.type), attrName, valueAST)); }); } } @@ -129,26 +133,26 @@ class DirectiveSelector { } ElementBinder matchText(dom.Node node) { - ElementBinderBuilder builder = _binderFactory.builder(_formatters); + ElementBinderBuilder builder = _binderFactory.builder(_formatters, _directives); 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) { + _directives[selectorRegExp.selector].forEach((tuple) { // Pre-compute the AST to watch this value. String expression = _interpolate(value); var valueAST = _astParser(expression, formatters: _formatters); - builder.addDirective(new DirectiveRef(node, type, - selectorRegExp.annotation, new Key(type), value, valueAST)); + builder.addDirective(new DirectiveRef(node, tuple.type, + tuple.directive, new Key(tuple.type), value, valueAST)); }); } } return builder.binder; } - ElementBinder matchComment(dom.Node node) => _binderFactory.builder(null).binder; + ElementBinder matchComment(dom.Node node) => _binderFactory.builder(null, null).binder; } /** @@ -187,10 +191,10 @@ class _Directive { } class _ContainsSelector { - final Directive annotation; + final String selector; final RegExp regexp; - _ContainsSelector(this.annotation, String regexp) + _ContainsSelector(this.selector, String regexp) : regexp = new RegExp(regexp); } @@ -234,14 +238,14 @@ _addRefs(ElementBinderBuilder builder, List<_Directive> directives, dom.Node nod class _ElementSelector { final String _name; - final _elementMap = >{}; - final _elementPartialMap = {}; + final _elementMap = new HashMap>(); + final _elementPartialMap = new HashMap(); - final _classMap = >{}; - final _classPartialMap = {}; + final _classMap = new HashMap>(); + final _classPartialMap = new HashMap(); - final _attrValueMap = >>{}; - final _attrValuePartialMap = >{}; + final _attrValueMap = new HashMap>>(); + final _attrValuePartialMap = new HashMap>(); _ElementSelector(this._name); @@ -268,12 +272,12 @@ class _ElementSelector { } } else if ((name = part.attrName) != null) { if (terminal) { - elSelector._attrValueMap.putIfAbsent(name, () => >{}) + elSelector._attrValueMap.putIfAbsent(name, () => new HashMap>()) .putIfAbsent(part.attrValue, () => []) .add(directive); } else { elSelector = elSelector._attrValuePartialMap - .putIfAbsent(name, () => {}) + .putIfAbsent(name, () => new HashMap()) .putIfAbsent(part.attrValue, () => new _ElementSelector(name)); } } else { @@ -349,7 +353,7 @@ class _ElementSelector { // A global cache for the _matchingKey RegExps. The size is bounded by // the number of attribute directive selectors used in the application. - static var _matchingKeyCache = {}; + static var _matchingKeyCache = new HashMap(); String _matchingKey(Iterable keys, String attrName) => keys.firstWhere((key) => diff --git a/lib/core_dom/shadow_dom_component_factory.dart b/lib/core_dom/shadow_dom_component_factory.dart index 3ffa26970..7c8a48c9a 100644 --- a/lib/core_dom/shadow_dom_component_factory.dart +++ b/lib/core_dom/shadow_dom_component_factory.dart @@ -1,7 +1,15 @@ part of angular.core.dom_internal; abstract class ComponentFactory { - FactoryFn call(dom.Node node, DirectiveRef ref); + BoundComponentFactory bind(DirectiveRef ref, directives); +} + +/** + * A Component factory with has been bound to a specific component type. + */ +abstract class BoundComponentFactory { + List get callArgs; + Function call(dom.Element element); static async.Future _viewFuture( Component component, ViewCache viewCache, DirectiveMap directives) { @@ -27,173 +35,150 @@ abstract class ComponentFactory { @Injectable() class ShadowDomComponentFactory implements ComponentFactory { - final Expando _expando; - - ShadowDomComponentFactory(this._expando); - - final Map<_ComponentAssetKey, async.Future> _styleElementCache = {}; - - - - FactoryFn call(dom.Node node, DirectiveRef ref) { - return (Injector injector) { - var component = ref.annotation as Component; - Scope scope = injector.getByKey(SCOPE_KEY); - ViewCache viewCache = injector.getByKey(VIEW_CACHE_KEY); - Http http = injector.getByKey(HTTP_KEY); - TemplateCache templateCache = injector.getByKey(TEMPLATE_CACHE_KEY); - DirectiveMap directives = injector.getByKey(DIRECTIVE_MAP_KEY); - NgBaseCss baseCss = component.useNgBaseCss ? injector.getByKey(NG_BASE_CSS_KEY) : null; - // This is a bit of a hack since we are returning different type then we are. - var componentFactory = new _ComponentFactory(node, - ref.typeKey, - component, - injector.getByKey(NODE_TREE_SANITIZER_KEY), - injector.getByKey(WEB_PLATFORM_KEY), - injector.getByKey(COMPONENT_CSS_REWRITER_KEY), - _expando, - baseCss, - _styleElementCache); - var controller = componentFactory.call(injector, scope, viewCache, http, templateCache, - directives); - - componentFactory.shadowScope.context[component.publishAs] = controller; - return controller; - }; + final ViewCache viewCache; + final Http http; + final TemplateCache templateCache; + final WebPlatform platform; + final ComponentCssRewriter componentCssRewriter; + final dom.NodeTreeSanitizer treeSanitizer; + final Expando expando; + final CompilerConfig config; + + final Map<_ComponentAssetKey, async.Future> styleElementCache = {}; + + ShadowDomComponentFactory(this.viewCache, this.http, this.templateCache, this.platform, + this.componentCssRewriter, this.treeSanitizer, this.expando, + this.config, CacheRegister cacheRegister) { + cacheRegister.registerCache("ShadowDomComponentFactoryStyles", styleElementCache); } + + bind(DirectiveRef ref, directives) => + new BoundShadowDomComponentFactory(this, ref, directives); } +class BoundShadowDomComponentFactory implements BoundComponentFactory { -/** - * ComponentFactory is responsible for setting up components. This includes - * the shadowDom, fetching template, importing styles, setting up attribute - * mappings, publishing the controller, and compiling and caching the template. - */ -class _ComponentFactory implements Function { + final ShadowDomComponentFactory _f; + final DirectiveRef _ref; + final DirectiveMap _directives; - final dom.Element element; - final Key typeKey; - final Component component; - final dom.NodeTreeSanitizer treeSanitizer; - final Expando _expando; - final NgBaseCss _baseCss; - final Map<_ComponentAssetKey, async.Future> - _styleElementCache; - final ComponentCssRewriter componentCssRewriter; - final WebPlatform platform; + Component get _component => _ref.annotation as Component; + + String _tag; + async.Future> _styleElementsFuture; + async.Future _viewFuture; + + BoundShadowDomComponentFactory(this._f, this._ref, this._directives) { + _tag = _component.selector.toLowerCase(); + _styleElementsFuture = async.Future.wait(_component.cssUrls.map(_styleFuture)); + + _viewFuture = BoundComponentFactory._viewFuture( + _component, + new PlatformViewCache(_f.viewCache, _tag, _f.platform), + _directives); + } + + async.Future _styleFuture(cssUrl) { + Http http = _f.http; + TemplateCache templateCache = _f.templateCache; + WebPlatform platform = _f.platform; + ComponentCssRewriter componentCssRewriter = _f.componentCssRewriter; + dom.NodeTreeSanitizer treeSanitizer = _f.treeSanitizer; - dom.ShadowRoot shadowDom; - Scope shadowScope; - Injector shadowInjector; - var controller; - - _ComponentFactory(this.element, this.typeKey, this.component, this.treeSanitizer, - this.platform, this.componentCssRewriter, this._expando, - this._baseCss, this._styleElementCache); - - dynamic call(Injector injector, Scope scope, - ViewCache viewCache, Http http, TemplateCache templateCache, - DirectiveMap directives) { - shadowDom = element.createShadowRoot() - ..applyAuthorStyles = component.applyAuthorStyles - ..resetStyleInheritance = component.resetStyleInheritance; - - 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, - // so change back to using @import once Chrome bug is fixed or a - // better work around is found. - Iterable> cssFutures; - var cssUrls = _baseCss != null ? - ([]..addAll(_baseCss.urls)..addAll(component.cssUrls)) : - component.cssUrls; - var tag = element.tagName.toLowerCase(); - if (cssUrls.isNotEmpty) { - cssFutures = cssUrls.map((cssUrl) => _styleElementCache.putIfAbsent( - new _ComponentAssetKey(tag, cssUrl), () => + return _f.styleElementCache.putIfAbsent( + new _ComponentAssetKey(_tag, cssUrl), () => http.get(cssUrl, cache: templateCache) - .then((resp) => resp.responseText, - onError: (e) => '/*\n$e\n*/\n') - .then((String css) { + .then((resp) => resp.responseText, + onError: (e) => '/*\n$e\n*/\n') + .then((String css) { - // Shim CSS if required - if (platform.cssShimRequired) { - css = platform.shimCss(css, selector: tag, cssUrl: cssUrl); - } + // Shim CSS if required + if (platform.cssShimRequired) { + css = platform.shimCss(css, selector: _tag, cssUrl: cssUrl); + } - // If a css rewriter is installed, run the css through a rewriter - var styleElement = new dom.StyleElement() - ..appendText(componentCssRewriter(css, selector: tag, - cssUrl: cssUrl)); + // If a css rewriter is installed, run the css through a rewriter + var styleElement = new dom.StyleElement() + ..appendText(componentCssRewriter(css, selector: _tag, + cssUrl: cssUrl)); - // ensure there are no invalid tags or modifications - treeSanitizer.sanitizeTree(styleElement); + // ensure there are no invalid tags or modifications + treeSanitizer.sanitizeTree(styleElement); - // If the css shim is required, it means that scoping does not - // work, and adding the style to the head of the document is - // preferrable. - if (platform.cssShimRequired) { - dom.document.head.append(styleElement); - } + // If the css shim is required, it means that scoping does not + // work, and adding the style to the head of the document is + // preferrable. + if (platform.cssShimRequired) { + dom.document.head.append(styleElement); + return null; + } - return styleElement; - }) - )).toList(); - } else { - cssFutures = [new async.Future.value(null)]; - } + return styleElement; + }) + ); + } - var platformViewCache = new PlatformViewCache(viewCache, tag, platform); + List get callArgs => _CALL_ARGS; + static final _CALL_ARGS = [DIRECTIVE_INJECTOR_KEY, SCOPE_KEY, NG_BASE_CSS_KEY, + EVENT_HANDLER_KEY]; + Function call(dom.Element element) { + return (DirectiveInjector injector, Scope scope, NgBaseCss baseCss, + EventHandler eventHandler) { + var shadowDom = element.createShadowRoot() + ..applyAuthorStyles = _component.applyAuthorStyles + ..resetStyleInheritance = _component.resetStyleInheritance; + + var shadowScope = scope.createChild(new HashMap()); // Isolate + + async.Future> cssFuture; + if (_component.useNgBaseCss == true) { + cssFuture = async.Future.wait( + [async.Future.wait(baseCss.urls.map(_styleFuture)), _styleElementsFuture]) + .then((twoLists) { + assert(twoLists.length == 2); + return []..addAll(twoLists[0])..addAll(twoLists[1]); + }); + } else { + cssFuture = _styleElementsFuture; + } - var viewFuture = ComponentFactory._viewFuture(component, platformViewCache, - directives); + ComponentDirectiveInjector shadowInjector; - TemplateLoader templateLoader = new TemplateLoader( - async.Future.wait(cssFutures).then((Iterable cssList) { - // This prevents style duplication by only adding css to the shadow - // root if there is a native implementation of shadow dom. - if (!platform.cssShimRequired) { - cssList.where((styleElement) => styleElement != null) + TemplateLoader templateLoader = new TemplateLoader( + cssFuture.then((Iterable cssList) { + cssList + .where((styleElement) => styleElement != null) .forEach((styleElement) { shadowDom.append(styleElement.clone(true)); }); - } - if (viewFuture != null) { - return viewFuture.then((ViewFactory viewFactory) { - return (!shadowScope.isAttached) ? - shadowDom : - attachViewToShadowDom(viewFactory); - }); - } - return shadowDom; - })); - controller = createShadowInjector(injector, templateLoader).getByKey(typeKey); - ComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); - return controller; - } + if (_viewFuture != null) { + return _viewFuture.then((ViewFactory viewFactory) { + if (shadowScope.isAttached) { + shadowDom.nodes.addAll( + viewFactory.call(shadowInjector.scope, shadowInjector).nodes); + } + return shadowDom; + }); + } + return shadowDom; + })); - dom.ShadowRoot attachViewToShadowDom(ViewFactory viewFactory) { - var view = viewFactory(shadowInjector); - shadowDom.nodes.addAll(view.nodes); - return shadowDom; - } + var probe; + shadowInjector = new ShadowDomComponentDirectiveInjector(injector, injector.appInjector, + shadowScope, templateLoader, shadowDom); + shadowInjector.bindByKey(_ref.typeKey, _ref.factory, _ref.paramKeys, _ref.annotation.visibility); + + if (_f.config.elementProbeEnabled) { + probe = _f.expando[shadowDom] = shadowInjector.elementProbe; + shadowScope.on(ScopeEvent.DESTROY).listen((ScopeEvent) => _f.expando[shadowDom] = null); + } + + var controller = shadowInjector.getByKey(_ref.typeKey); + BoundComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); + shadowScope.context[_component.publishAs] = controller; - Injector createShadowInjector(injector, TemplateLoader templateLoader) { - var probe; - var shadowModule = new Module() - ..bindByKey(typeKey) - ..bindByKey(NG_ELEMENT_KEY) - ..bindByKey(EVENT_HANDLER_KEY, toImplementation: ShadowRootEventHandler) - ..bindByKey(SCOPE_KEY, toValue: shadowScope) - ..bindByKey(TEMPLATE_LOADER_KEY, toValue: templateLoader) - ..bindByKey(SHADOW_ROOT_KEY, toValue: shadowDom) - ..bindByKey(ELEMENT_PROBE_KEY, toFactory: (_) => probe); - shadowInjector = injector.createChild([shadowModule], name: SHADOW_DOM_INJECTOR_NAME); - probe = _expando[shadowDom] = new ElementProbe( - injector.getByKey(ELEMENT_PROBE_KEY), shadowDom, shadowInjector, shadowScope); - shadowScope.on(ScopeEvent.DESTROY).listen((ScopeEvent) {_expando[shadowDom] = null;}); - return shadowInjector; + return controller; + }; } } diff --git a/lib/core_dom/shadowless_shadow_root.dart b/lib/core_dom/shadowless_shadow_root.dart index d4994a458..ae4a87b62 100644 --- a/lib/core_dom/shadowless_shadow_root.dart +++ b/lib/core_dom/shadowless_shadow_root.dart @@ -1,12 +1,65 @@ part of angular.core.dom_internal; - -@proxy class ShadowlessShadowRoot implements dom.ShadowRoot { dom.Element _element; - ShadowlessShadowRoot(this._element); - - noSuchMethod(Invocation invocation) { - throw new UnimplementedError("Not yet implemented in ShadowlessShadowRoot."); - } + _notSupported() { throw new UnsupportedError("Not supported"); } + dom.Element get activeElement => _notSupported(); + dom.Element get host => _notSupported(); + String get innerHtml => _notSupported(); + void set innerHtml(String value) => _notSupported(); + dom.ShadowRoot get olderShadowRoot => _notSupported(); + bool get _resetStyleInheritance => _notSupported(); + void set _resetStyleInheritance(bool value) => _notSupported(); + List get styleSheets => _notSupported(); + dom.Node clone(bool deep) => _notSupported(); + dom.Element elementFromPoint(int x, int y) => _notSupported(); + dom.Element getElementById(String elementId) => _notSupported(); + List getElementsByClassName(String className) => _notSupported(); + List getElementsByTagName(String tagName) => _notSupported(); + dom.Selection getSelection() => _notSupported(); + bool get resetStyleInheritance { _notSupported(); } + void set resetStyleInheritance(bool value) { _notSupported(); } + bool get applyAuthorStyles { _notSupported(); } + void set applyAuthorStyles(bool value) { _notSupported(); } + List get children => _notSupported(); + void set children(List value) { _notSupported(); } + dom.ElementList querySelectorAll(String selectors) => _notSupported(); + void setInnerHtml(String html, {dom.NodeValidator validator, dom.NodeTreeSanitizer treeSanitizer}) { _notSupported(); } + void appendText(String text) { _notSupported(); } + void appendHtml(String text) { _notSupported(); } + dom.Element query(String relativeSelectors) { _notSupported(); } + dom.ElementList queryAll(String relativeSelectors) { _notSupported(); } + dom.Element querySelector(String selectors) => _notSupported(); + List get nodes => _notSupported(); + void set nodes(Iterable value) { _notSupported(); } + void remove() { _notSupported(); } + dom.Node replaceWith(dom.Node otherNode) { _notSupported(); } + dom.Node insertAllBefore(Iterable newNodes, dom.Node refChild) { _notSupported(); } + void _clearChildren() { _notSupported(); } + String get baseUri => _notSupported(); + List get childNodes => _notSupported(); + dom.Node get firstChild => _notSupported(); + dom.Node get lastChild => _notSupported(); + String get _localName => _notSupported(); + String get _namespaceUri => _notSupported(); + dom.Node get nextNode => _notSupported(); + String get nodeName => _notSupported(); + int get nodeType => _notSupported(); + String get nodeValue => _notSupported(); + dom.Document get ownerDocument => _notSupported(); + dom.Element get parent => _notSupported(); + dom.Node get parentNode => _notSupported(); + dom.Node get previousNode => _notSupported(); + String get text => _notSupported(); + void set text(String value) => _notSupported(); + dom.Node append(dom.Node newChild) => _notSupported(); + bool contains(dom.Node other) => _notSupported(); + bool hasChildNodes() => _notSupported(); + dom.Node insertBefore(dom.Node newChild, dom.Node refChild) => _notSupported(); + dom.Node _removeChild(dom.Node oldChild) => _notSupported(); + dom.Node _replaceChild(dom.Node newChild, dom.Node oldChild) => _notSupported(); + dom.Events get on => _notSupported(); + void addEventListener(String type, dom.EventListener listener, [bool useCapture]) => _notSupported(); + bool dispatchEvent(dom.Event event) => _notSupported(); + void removeEventListener(String type, dom.EventListener listener, [bool useCapture]) => _notSupported(); } diff --git a/lib/core_dom/static_keys.dart b/lib/core_dom/static_keys.dart index 7f3253c40..8c770c9f7 100644 --- a/lib/core_dom/static_keys.dart +++ b/lib/core_dom/static_keys.dart @@ -2,6 +2,7 @@ library angular.core_dom.static_keys; import 'dart:html' as dom; import 'package:di/di.dart'; +import 'package:angular/cache/module.dart'; import 'package:angular/core/static_keys.dart'; import 'package:angular/core_dom/module_internal.dart'; @@ -10,26 +11,30 @@ export 'package:angular/core/static_keys.dart'; // Keys used to call Injector.getByKey and Module.bindByKey -Key ANIMATE_KEY = new Key(Animate); -Key BOUND_VIEW_FACTORY_KEY = new Key(BoundViewFactory); -Key COMPILER_KEY = new Key(Compiler); -Key COMPONENT_CSS_REWRITER_KEY = new Key(ComponentCssRewriter); -Key DIRECTIVE_MAP_KEY = new Key(DirectiveMap); -Key ELEMENT_KEY = new Key(dom.Element); -Key ELEMENT_PROBE_KEY = new Key(ElementProbe); -Key EVENT_HANDLER_KEY = new Key(EventHandler); -Key HTTP_KEY = new Key(Http); -Key NG_ELEMENT_KEY = new Key(NgElement); -Key NODE_ATTRS_KEY = new Key(NodeAttrs); -Key NODE_KEY = new Key(dom.Node); -Key NODE_TREE_SANITIZER_KEY = new Key(dom.NodeTreeSanitizer); -Key SHADOW_ROOT_KEY = new Key(dom.ShadowRoot); -Key TEMPLATE_CACHE_KEY = new Key(TemplateCache); -Key TEMPLATE_LOADER_KEY = new Key(TemplateLoader); -Key TEXT_MUSTACHE_KEY = new Key(TextMustache); -Key VIEW_CACHE_KEY = new Key(ViewCache); -Key VIEW_FACTORY_KEY = new Key(ViewFactory); -Key VIEW_KEY = new Key(View); -Key VIEW_PORT_KEY = new Key(ViewPort); -Key WEB_PLATFORM_KEY = new Key(WebPlatform); -Key WINDOW_KEY = new Key(dom.Window); +final Key ANIMATE_KEY = new Key(Animate); +final Key BOUND_VIEW_FACTORY_KEY = new Key(BoundViewFactory); +final Key CACHE_REGISTER_KEY = new Key(CacheRegister); +final Key COMPILER_KEY = new Key(Compiler); +final Key COMPONENT_CSS_REWRITER_KEY = new Key(ComponentCssRewriter); +final Key DIRECTIVE_MAP_KEY = new Key(DirectiveMap); +final Key ELEMENT_KEY = new Key(dom.Element); +final Key ELEMENT_PROBE_KEY = new Key(ElementProbe); +final Key EVENT_HANDLER_KEY = new Key(EventHandler); +final Key HTTP_KEY = new Key(Http); +final Key NG_ELEMENT_KEY = new Key(NgElement); +final Key NODE_ATTRS_KEY = new Key(NodeAttrs); +final Key NODE_KEY = new Key(dom.Node); +final Key NODE_TREE_SANITIZER_KEY = new Key(dom.NodeTreeSanitizer); +final Key SHADOW_DOM_COMPONENT_FACTORY_KEY = new Key(ShadowDomComponentFactory); +final Key SHADOW_ROOT_KEY = new Key(dom.ShadowRoot); +final Key TEMPLATE_CACHE_KEY = new Key(TemplateCache); +final Key TEMPLATE_LOADER_KEY = new Key(TemplateLoader); +final Key TEXT_MUSTACHE_KEY = new Key(TextMustache); +final Key ATTR_MUSTACHE_KEY = new Key(AttrMustache); +final Key VIEW_CACHE_KEY = new Key(ViewCache); +final Key VIEW_FACTORY_KEY = new Key(ViewFactory); +final Key VIEW_KEY = new Key(View); +final Key VIEW_PORT_KEY = new Key(ViewPort); +final Key WEB_PLATFORM_KEY = new Key(WebPlatform); +final Key WINDOW_KEY = new Key(dom.Window); +final Key EXPANDO_KEY = new Key(Expando); diff --git a/lib/core_dom/tagging_view_factory.dart b/lib/core_dom/tagging_view_factory.dart index 00123aa93..c5a928ae0 100644 --- a/lib/core_dom/tagging_view_factory.dart +++ b/lib/core_dom/tagging_view_factory.dart @@ -1,57 +1,123 @@ part of angular.core.dom_internal; +class NodeLinkingInfo { + /** + * True if the Node has a 'ng-binding' class. + */ + final bool containsNgBinding; + + /** + * True if the Node is a [dom.Element], otherwise it is a Text or Comment node. + * No other nodeTypes are allowed. + */ + final bool isElement; + + /** + * If true, some child has a 'ng-binding' class and the ViewFactory must search + * for these children. + */ + final bool ngBindingChildren; + + NodeLinkingInfo(this.containsNgBinding, this.isElement, this.ngBindingChildren); +} + +computeNodeLinkingInfos(List nodeList) { + List list = new List(nodeList.length); + + for (int i = 0; i < nodeList.length; i++) { + dom.Node node = nodeList[i]; + + assert(node.nodeType == dom.Node.ELEMENT_NODE || + node.nodeType == dom.Node.TEXT_NODE || + node.nodeType == dom.Node.COMMENT_NODE); + + bool isElement = node.nodeType == dom.Node.ELEMENT_NODE; + + list[i] = new NodeLinkingInfo( + isElement && (node as dom.Element).classes.contains('ng-binding'), + isElement, + isElement && (node as dom.Element).querySelectorAll('.ng-binding').length > 0); + } + return list; +} + class TaggingViewFactory implements ViewFactory { final List elementBinders; final List templateNodes; + final List nodeLinkingInfos; final Profiler _perf; - TaggingViewFactory(this.templateNodes, this.elementBinders, this._perf); + TaggingViewFactory(templateNodes, this.elementBinders, this._perf) : + nodeLinkingInfos = computeNodeLinkingInfos(templateNodes), + this.templateNodes = templateNodes; - BoundViewFactory bind(Injector injector) => new BoundViewFactory(this, injector); + @deprecated + BoundViewFactory bind(DirectiveInjector directiveInjector) => + new BoundViewFactory(this, directiveInjector); static Key _EVENT_HANDLER_KEY = new Key(EventHandler); - View call(Injector injector, [List nodes /* TODO: document fragment */]) { + View call(Scope scope, DirectiveInjector directiveInjector, + [List nodes /* TODO: document fragment */]) { + assert(scope != null); if (nodes == null) { nodes = cloneElements(templateNodes); } var timerId; try { assert((timerId = _perf.startTimer('ng.view')) != false); - var view = new View(nodes, injector.getByKey(_EVENT_HANDLER_KEY)); - _link(view, nodes, injector); + Animate animate = directiveInjector.getByKey(ANIMATE_KEY); + EventHandler eventHandler = directiveInjector.getByKey(EVENT_HANDLER_KEY); + var view = new View(nodes, scope, eventHandler); + _link(view, scope, nodes, eventHandler, animate, directiveInjector); return view; } finally { assert(_perf.stopTimer(timerId) != false); } } - void _bindTagged(TaggedElementBinder tagged, int elementBinderIndex, Injector rootInjector, - List elementInjectors, View view, boundNode) { + void _bindTagged(TaggedElementBinder tagged, int elementBinderIndex, + DirectiveInjector rootInjector, + List elementInjectors, View view, boundNode, Scope scope, + EventHandler eventHandler, Animate animate) { var binder = tagged.binder; - var parentInjector = tagged.parentBinderOffset == -1 ? - rootInjector : - elementInjectors[tagged.parentBinderOffset]; - assert(parentInjector != null); + DirectiveInjector parentInjector = + tagged.parentBinderOffset == -1 ? rootInjector : elementInjectors[tagged.parentBinderOffset]; - var elementInjector = elementInjectors[elementBinderIndex] = - binder != null ? binder.bind(view, parentInjector, boundNode) : parentInjector; + var elementInjector; + if (binder == null) { + elementInjector = parentInjector; + } else { + // TODO(misko): Remove this after we remove controllers. No controllers -> 1to1 Scope:View. + if (parentInjector != rootInjector && parentInjector.scope != null) { + scope = parentInjector.scope; + } + elementInjector = binder.bind(view, scope, parentInjector, boundNode, eventHandler, animate); + } + // TODO(misko): Remove this after we remove controllers. No controllers -> 1to1 Scope:View. + if (elementInjector != rootInjector && elementInjector.scope != null) { + scope = elementInjector.scope; + } + elementInjectors[elementBinderIndex] = elementInjector; if (tagged.textBinders != null) { for (var k = 0; k < tagged.textBinders.length; k++) { TaggedTextBinder taggedText = tagged.textBinders[k]; - taggedText.binder.bind(view, elementInjector, boundNode.childNodes[taggedText.offsetIndex]); + var childNode = boundNode.childNodes[taggedText.offsetIndex]; + taggedText.binder.bind(view, scope, elementInjector, childNode, eventHandler, animate); } } } - View _link(View view, List nodeList, Injector rootInjector) { - var elementInjectors = new List(elementBinders.length); + View _link(View view, Scope scope, List nodeList, EventHandler eventHandler, + Animate animate, DirectiveInjector rootInjector) { + var elementInjectors = new List(elementBinders.length); var directiveDefsByName = {}; var elementBinderIndex = 0; for (int i = 0; i < nodeList.length; i++) { - var node = nodeList[i]; + dom.Node node = nodeList[i]; + NodeLinkingInfo linkingInfo = nodeLinkingInfos[i]; // if node isn't attached to the DOM, create a parent for it. var parentNode = node.parentNode; @@ -62,29 +128,30 @@ class TaggingViewFactory implements ViewFactory { parentNode.append(node); } - if (node.nodeType == dom.Node.ELEMENT_NODE) { - var elts = node.querySelectorAll('.ng-binding'); - // querySelectorAll doesn't return the node itself - if (node.classes.contains('ng-binding')) { + if (linkingInfo.isElement) { + if (linkingInfo.containsNgBinding) { var tagged = elementBinders[elementBinderIndex]; - _bindTagged(tagged, elementBinderIndex, rootInjector, elementInjectors, view, node); + _bindTagged(tagged, elementBinderIndex, rootInjector, + elementInjectors, view, node, scope, eventHandler, animate); elementBinderIndex++; } - for (int j = 0; j < elts.length; j++, elementBinderIndex++) { - TaggedElementBinder tagged = elementBinders[elementBinderIndex]; - _bindTagged(tagged, elementBinderIndex, rootInjector, elementInjectors, view, elts[j]); + if (linkingInfo.ngBindingChildren) { + var elts = (node as dom.Element).querySelectorAll('.ng-binding'); + for (int j = 0; j < elts.length; j++, elementBinderIndex++) { + TaggedElementBinder tagged = elementBinders[elementBinderIndex]; + _bindTagged(tagged, elementBinderIndex, rootInjector, elementInjectors, + view, elts[j], scope, eventHandler, animate); + } } - } else if (node.nodeType == dom.Node.TEXT_NODE || - node.nodeType == dom.Node.COMMENT_NODE) { + } else { TaggedElementBinder tagged = elementBinders[elementBinderIndex]; assert(tagged.binder != null || tagged.isTopLevel); if (tagged.binder != null) { - _bindTagged(tagged, elementBinderIndex, rootInjector, elementInjectors, view, node); + _bindTagged(tagged, elementBinderIndex, rootInjector, + elementInjectors, view, node, scope, eventHandler, animate); } elementBinderIndex++; - } else { - throw "nodeType sadness ${node.nodeType}}"; } if (fakeParent) { diff --git a/lib/core_dom/transcluding_component_factory.dart b/lib/core_dom/transcluding_component_factory.dart index 2a534aab8..29f8f2663 100644 --- a/lib/core_dom/transcluding_component_factory.dart +++ b/lib/core_dom/transcluding_component_factory.dart @@ -70,59 +70,90 @@ class ContentPort { @Injectable() class TranscludingComponentFactory implements ComponentFactory { - final Expando _expando; - TranscludingComponentFactory(this._expando); + final Expando expando; + final ViewCache viewCache; + final CompilerConfig config; - FactoryFn call(dom.Node node, DirectiveRef ref) { + TranscludingComponentFactory(this.expando, this.viewCache, this.config); + + bind(DirectiveRef ref, directives) => + new BoundTranscludingComponentFactory(this, ref, directives); +} + +class BoundTranscludingComponentFactory implements BoundComponentFactory { + final TranscludingComponentFactory _f; + final DirectiveRef _ref; + final DirectiveMap _directives; + + Component get _component => _ref.annotation as Component; + async.Future _viewFuture; + + BoundTranscludingComponentFactory(this._f, this._ref, this._directives) { + _viewFuture = BoundComponentFactory._viewFuture( + _component, + _f.viewCache, + _directives); + } + + List get callArgs => _CALL_ARGS; + static var _CALL_ARGS = [ DIRECTIVE_INJECTOR_KEY, SCOPE_KEY, + VIEW_CACHE_KEY, HTTP_KEY, TEMPLATE_CACHE_KEY, + DIRECTIVE_MAP_KEY, NG_BASE_CSS_KEY, EVENT_HANDLER_KEY]; + Function call(dom.Node node) { // CSS is not supported. - assert((ref.annotation as Component).cssUrls == null || - (ref.annotation as Component).cssUrls.isEmpty); + assert(_component.cssUrls == null || + _component.cssUrls.isEmpty); var element = node as dom.Element; - return (Injector injector) { - var childInjector; - var component = ref.annotation as Component; - Scope scope = injector.getByKey(SCOPE_KEY); - ViewCache viewCache = injector.getByKey(VIEW_CACHE_KEY); - Http http = injector.getByKey(HTTP_KEY); - TemplateCache templateCache = injector.getByKey(TEMPLATE_CACHE_KEY); - DirectiveMap directives = injector.getByKey(DIRECTIVE_MAP_KEY); - NgBaseCss baseCss = injector.getByKey(NG_BASE_CSS_KEY); + return (DirectiveInjector injector, Scope scope, + ViewCache viewCache, Http http, TemplateCache templateCache, + DirectiveMap directives, NgBaseCss baseCss, EventHandler eventHandler) { + + DirectiveInjector childInjector; + var childInjectorCompleter; // Used if the ViewFuture is available before the childInjector. + var component = _component; var contentPort = new ContentPort(element); // Append the component's template as children - var viewFuture = ComponentFactory._viewFuture(component, viewCache, directives); var elementFuture; - if (viewFuture != null) { - elementFuture = viewFuture.then((ViewFactory viewFactory) { + if (_viewFuture != null) { + elementFuture = _viewFuture.then((ViewFactory viewFactory) { contentPort.pullNodes(); - element.nodes.addAll(viewFactory(childInjector).nodes); - return element; + if (childInjector != null) { + element.nodes.addAll( + viewFactory.call(childInjector.scope, childInjector).nodes); + return element; + } else { + childInjectorCompleter = new async.Completer(); + return childInjectorCompleter.future.then((childInjector) { + element.nodes.addAll( + viewFactory.call(childInjector.scope, childInjector).nodes); + return element; + }); + } }); } else { elementFuture = new async.Future.microtask(() => contentPort.pullNodes()); } TemplateLoader templateLoader = new TemplateLoader(elementFuture); - Scope shadowScope = scope.createChild({}); + Scope shadowScope = scope.createChild(new HashMap()); - var probe; - var childModule = new Module() - ..bind(ref.type) - ..bind(NgElement) - ..bind(ContentPort, toValue: contentPort) - ..bind(Scope, toValue: shadowScope) - ..bind(TemplateLoader, toValue: templateLoader) - ..bind(dom.ShadowRoot, toValue: new ShadowlessShadowRoot(element)) - ..bind(ElementProbe, toFactory: (_) => probe); - childInjector = injector.createChild([childModule], name: SHADOW_DOM_INJECTOR_NAME); + childInjector = new ShadowlessComponentDirectiveInjector(injector, injector.appInjector, + eventHandler, shadowScope, templateLoader, new ShadowlessShadowRoot(element), + contentPort); + childInjector.bindByKey(_ref.typeKey, _ref.factory, _ref.paramKeys, _ref.annotation.visibility); + + if (childInjectorCompleter != null) { + childInjectorCompleter.complete(childInjector); + } - var controller = childInjector.get(ref.type); + var controller = childInjector.getByKey(_ref.typeKey); shadowScope.context[component.publishAs] = controller; - ComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); + BoundComponentFactory._setupOnShadowDomAttach(controller, templateLoader, shadowScope); return controller; }; } diff --git a/lib/core_dom/view.dart b/lib/core_dom/view.dart index c53309f7d..1195fbe8c 100644 --- a/lib/core_dom/view.dart +++ b/lib/core_dom/view.dart @@ -12,10 +12,11 @@ part of angular.core.dom_internal; * */ class View { + final Scope scope; final List nodes; final EventHandler eventHandler; - View(this.nodes, this.eventHandler); + View(this.nodes, this.scope, this.eventHandler); void registerEvent(String eventName) { eventHandler.register(eventName); @@ -27,32 +28,46 @@ class View { * [placeholder] node that is used as the insertion point for view nodes. */ class ViewPort { + final DirectiveInjector directiveInjector; + final Scope scope; final dom.Node placeholder; final Animate _animate; final _views = []; - ViewPort(this.placeholder, this._animate); + ViewPort(this.directiveInjector, this.scope, this.placeholder, this._animate); - void insert(View view, { View insertAfter }) { - dom.Node previousNode = _lastNode(insertAfter); - _viewsInsertAfter(view, insertAfter); + View insertNew(ViewFactory viewFactory, { View insertAfter, Scope viewScope}) { + if (viewScope == null) viewScope = scope.createChild(new PrototypeMap(scope.context)); + View view = viewFactory.call(viewScope, directiveInjector); + return insert(view, insertAfter: insertAfter); + } - _animate.insert(view.nodes, placeholder.parentNode, - insertBefore: previousNode.nextNode); + View insert(View view, { View insertAfter }) { + scope.rootScope.domWrite(() { + dom.Node previousNode = _lastNode(insertAfter); + _viewsInsertAfter(view, insertAfter); + _animate.insert(view.nodes, placeholder.parentNode, insertBefore: previousNode.nextNode); + }); + return view; } - void remove(View view) { + View remove(View view) { + view.scope.destroy(); _views.remove(view); - _animate.remove(view.nodes); + scope.rootScope.domWrite(() { + _animate.remove(view.nodes); + }); + return view; } - void move(View view, { View moveAfter }) { + View move(View view, { View moveAfter }) { dom.Node previousNode = _lastNode(moveAfter); _views.remove(view); _viewsInsertAfter(view, moveAfter); - - _animate.move(view.nodes, placeholder.parentNode, - insertBefore: previousNode.nextNode); + scope.rootScope.domWrite(() { + _animate.move(view.nodes, placeholder.parentNode, insertBefore: previousNode.nextNode); + }); + return view; } void _viewsInsertAfter(View view, View insertAfter) { diff --git a/lib/core_dom/view_factory.dart b/lib/core_dom/view_factory.dart index 2fec262ba..2f79624f5 100644 --- a/lib/core_dom/view_factory.dart +++ b/lib/core_dom/view_factory.dart @@ -9,20 +9,21 @@ part of angular.core.dom_internal; * * The BoundViewFactory needs [Scope] to be created. */ +@deprecated class BoundViewFactory { ViewFactory viewFactory; - Injector injector; + DirectiveInjector directiveInjector; - BoundViewFactory(this.viewFactory, this.injector); + BoundViewFactory(this.viewFactory, this.directiveInjector); - View call(Scope scope) => - viewFactory(injector.createChild([new Module()..bindByKey(SCOPE_KEY, toValue: scope)])); + View call(Scope scope) => viewFactory(scope, directiveInjector); } abstract class ViewFactory implements Function { - BoundViewFactory bind(Injector injector); + @deprecated + BoundViewFactory bind(DirectiveInjector directiveInjector); - View call(Injector injector, [List elements]); + View call(Scope scope, DirectiveInjector directiveInjector, [List elements]); } /** @@ -41,24 +42,28 @@ class WalkingViewFactory implements ViewFactory { eb is ElementBinderTreeRef)); } - BoundViewFactory bind(Injector injector) => - new BoundViewFactory(this, injector); + BoundViewFactory bind(DirectiveInjector directiveInjector) => + new BoundViewFactory(this, directiveInjector); - View call(Injector injector, [List nodes]) { + View call(Scope scope, DirectiveInjector directiveInjector, [List nodes]) { + assert(directiveInjector != null); if (nodes == null) nodes = cloneElements(templateElements); var timerId; try { assert((timerId = _perf.startTimer('ng.view')) != false); - var view = new View(nodes, injector.getByKey(EVENT_HANDLER_KEY)); - _link(view, nodes, elementBinders, injector); + EventHandler eventHandler = directiveInjector.getByKey(EVENT_HANDLER_KEY); + Animate animate = directiveInjector.getByKey(ANIMATE_KEY); + var view = new View(nodes, scope, eventHandler); + _link(view, scope, nodes, elementBinders, eventHandler, animate, directiveInjector); return view; } finally { assert(_perf.stopTimer(timerId) != false); } } - View _link(View view, List nodeList, List elementBinders, - Injector parentInjector) { + View _link(View view, Scope scope, List nodeList, List elementBinders, + EventHandler eventHandler, Animate animate, + DirectiveInjector directiveInjector) { var preRenderedIndexOffset = 0; var directiveDefsByName = {}; @@ -90,17 +95,22 @@ class WalkingViewFactory implements ViewFactory { parentNode = new dom.DivElement()..append(node); } - var childInjector = binder != null ? - binder.bind(view, parentInjector, node) : - parentInjector; + DirectiveInjector childInjector; + if (binder == null) { + childInjector = directiveInjector; + } else { + childInjector = binder.bind(view, scope, directiveInjector, node, eventHandler, animate); + // TODO(misko): Remove this after we remove controllers. No controllers -> 1to1 Scope:View. + if (childInjector != directiveInjector) scope = childInjector.scope; + } if (fakeParent) { // extract the node from the parentNode. nodeList[nodeListIndex] = parentNode.nodes[0]; } if (tree.subtrees != null) { - _link(view, node.nodes, tree.subtrees, childInjector); + _link(view, scope, node.nodes, tree.subtrees, eventHandler, animate, childInjector); } } finally { assert(_perf.stopTimer(timerId) != false); @@ -125,7 +135,9 @@ class ViewCache { final Compiler compiler; final dom.NodeTreeSanitizer treeSanitizer; - ViewCache(this.http, this.templateCache, this.compiler, this.treeSanitizer); + ViewCache(this.http, this.templateCache, this.compiler, this.treeSanitizer, CacheRegister cacheRegister) { + cacheRegister.registerCache('viewCache', viewFactoryCache); + } ViewFactory fromHtml(String html, DirectiveMap directives) { ViewFactory viewFactory = viewFactoryCache.get(html); @@ -188,9 +200,11 @@ String _html(obj) { class ElementProbe { final ElementProbe parent; final dom.Node element; - final Injector injector; + final DirectiveInjector injector; final Scope scope; - final directives = []; + List get directives => injector.directives; + final bindingExpressions = []; + final modelExpressions = []; ElementProbe(this.parent, this.element, this.injector, this.scope); } diff --git a/lib/core_dom/web_platform.dart b/lib/core_dom/web_platform.dart index bd615cea3..15f4ce50d 100644 --- a/lib/core_dom/web_platform.dart +++ b/lib/core_dom/web_platform.dart @@ -9,23 +9,25 @@ part of angular.core.dom_internal; */ @Injectable() class WebPlatform { - js.JsObject _platformJs; js.JsObject _shadowCss; bool get cssShimRequired => _shadowCss != null; bool get shadowDomShimRequired => _shadowCss != null; WebPlatform() { - var _platformJs = js.context['Platform']; - if (_platformJs != null) { - _shadowCss = _platformJs['ShadowCSS']; - - if (_shadowCss != null) { - _shadowCss['strictStyling'] = true; - } + var platformJs = js.context['Platform']; + if (platformJs != null) { + _shadowCss = platformJs['ShadowCSS']; + if (_shadowCss != null) _shadowCss['strictStyling'] = true; } } + /** + * Because this code uses `strictStyling` for the polymer css shim, it is required to add the + * custom element’s name as an attribute on all DOM nodes in the shadowRoot (e.g. ). + * + * See http://www.polymer-project.org/docs/polymer/styling.html#strictstyling + */ String shimCss(String css, { String selector, String cssUrl }) { if (!cssShimRequired) return css; @@ -35,11 +37,15 @@ class WebPlatform { void shimShadowDom(dom.Element root, String selector) { if (shadowDomShimRequired) { - // This adds an empty attribute with the name of the component tag onto // each element in the shadow root. - root.querySelectorAll("*") - .forEach((n) => n.attributes[selector] = ""); + // + // TODO Remove the try-catch once https://github.com/angular/angular.dart/issues/1189 is fixed. + try { + root.querySelectorAll("*").forEach((n) => n.attributes[selector] = ""); + } catch (e, s) { + print("WARNING: Failed to set up Shadow DOM shim for $selector.\n$e\n$s"); + } } } } @@ -60,14 +66,10 @@ class PlatformViewCache implements ViewCache { ViewFactory fromHtml(String html, DirectiveMap directives) { ViewFactory viewFactory; - if (selector != null && selector != "" - && platform.shadowDomShimRequired) { - - // By adding a comment with the tag name we ensure the template html is - // unique per selector name when used as a key in the view factory - // cache. - viewFactory = viewFactoryCache.get( - "$html"); + if (selector != null && selector != "" && platform.shadowDomShimRequired) { + // By adding a comment with the tag name we ensure the template html is unique per selector + // name when used as a key in the view factory cache. + viewFactory = viewFactoryCache.get("$html"); } else { viewFactory = viewFactoryCache.get(html); } @@ -76,11 +78,9 @@ class PlatformViewCache implements ViewCache { var div = new dom.DivElement(); div.setInnerHtml(html, treeSanitizer: treeSanitizer); - if (selector != null && selector != "" - && platform.shadowDomShimRequired) { - // This MUST happen before the compiler is called so that every dom - // element gets touched before the compiler removes them for - // transcluding directives like ng-if. + if (selector != null && selector != "" && platform.shadowDomShimRequired) { + // This MUST happen before the compiler is called so that every dom element gets touched + // before the compiler removes them for transcluding directives like `ng-if` platform.shimShadowDom(div, selector); } diff --git a/lib/di/module.dart b/lib/di/module.dart new file mode 100644 index 000000000..903c96c79 --- /dev/null +++ b/lib/di/module.dart @@ -0,0 +1,3 @@ +library di; + +export 'package:di/di.dart' hide lastKeyId; \ No newline at end of file diff --git a/lib/directive/module.dart b/lib/directive/module.dart index a374ff1bb..29b040ced 100644 --- a/lib/directive/module.dart +++ b/lib/directive/module.dart @@ -24,10 +24,12 @@ import 'package:angular/core/annotation.dart'; import 'package:angular/core/module_internal.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/core_dom/module_internal.dart'; +import 'package:angular/core_dom/directive_injector.dart'; import 'package:angular/utils.dart'; import 'package:angular/change_detection/watch_group.dart'; import 'package:angular/change_detection/change_detection.dart'; import 'package:angular/directive/static_keys.dart'; +import 'dart:collection'; part 'a_href.dart'; part 'ng_base_css.dart'; @@ -62,12 +64,14 @@ part 'ng_model_options.dart'; */ class DirectiveModule extends Module { DirectiveModule() { + bind(DirectiveInjector, toImplementation: DefaultDirectiveInjector); + bind(AHref, toValue: null); bind(NgBaseCss); // The root injector should have an empty NgBaseCss bind(NgBind, toValue: null); bind(NgBindTemplate, toValue: null); bind(NgBindHtml, toValue: null); - bind(dom.NodeValidator, toFactory: (_) => new dom.NodeValidatorBuilder.common()); + bind(dom.NodeValidator, toFactory: () => new dom.NodeValidatorBuilder.common(), inject: []); bind(NgClass, toValue: null); bind(NgClassOdd, toValue: null); bind(NgClassEven, toValue: null); @@ -117,5 +121,6 @@ class DirectiveModule extends Module { bind(NgModelPatternValidator, toValue: null); bind(NgModelMinLengthValidator, toValue: null); bind(NgModelMaxLengthValidator, toValue: null); + bind(NgModelColorValidator, toValue: null); } } diff --git a/lib/directive/ng_base_css.dart b/lib/directive/ng_base_css.dart index 60afe0adc..74e41406d 100644 --- a/lib/directive/ng_base_css.dart +++ b/lib/directive/ng_base_css.dart @@ -8,7 +8,9 @@ part of angular.directive; * # Example *
    */ -@Decorator(selector: '[ng-base-css]') +@Decorator( + selector: '[ng-base-css]', + visibility: Visibility.CHILDREN) class NgBaseCss { List _urls = const []; diff --git a/lib/directive/ng_bind.dart b/lib/directive/ng_bind.dart index 65cb194f2..7d49c1755 100644 --- a/lib/directive/ng_bind.dart +++ b/lib/directive/ng_bind.dart @@ -20,7 +20,12 @@ part of angular.directive; class NgBind { final dom.Element element; - NgBind(this.element); + NgBind(this.element, ElementProbe probe) { + // TODO(chirayu): Generalize this. + if (probe != null) { + probe.bindingExpressions.add(element.attributes['ng-bind']); + } + } set value(value) => element.text = value == null ? '' : value.toString(); } diff --git a/lib/directive/ng_class.dart b/lib/directive/ng_class.dart index 1ece01593..4f607d0e2 100644 --- a/lib/directive/ng_class.dart +++ b/lib/directive/ng_class.dart @@ -82,25 +82,17 @@ class NgClass extends _NgClassBase { } /** - * Dynamically style only odd rows in a list via data. + * Dynamically style only odd rows in an `ng-repeat` list. `Selector: [ng-class-odd]` * - * The `ngClassOdd` and `ngClassEven` directives work exactly as - * {@link ng.directive:ngClass ngClass}, except it works in - * conjunction with `ngRepeat` and takes affect only on odd (even) rows. + * This directive works exactly as [ngClass] with regard to String, Array, + * and Map syntax for associating CSS classes with an element, but only affects odd rows in a + * list. * - * This directive can be applied only within a scope of an `ngRepeat`. + * Also see [ngClassEven], which applies CSS classes to even rows. * - * ##Examples - * - * index.html: - * - *
  • - * - * {{name}} - * - *
  • - * - * style.css: + * ##Example + * Let's assume that we have a simple stylesheet that defines two CSS classes for the following + * example. * * .odd { * color: red; @@ -108,6 +100,14 @@ class NgClass extends _NgClassBase { * .even { * color: blue; * } + * + * The following template applies these classes to the odd and even rows respectively: + * + *
  • + * + * {{name}} + * + *
  • */ @Decorator( selector: '[ng-class-odd]', @@ -119,23 +119,17 @@ class NgClassOdd extends _NgClassBase { } /** - * The `ngClassOdd` and `ngClassEven` directives work exactly as - * {@link ng.directive:ngClass ngClass}, except it works in - * conjunction with `ngRepeat` and takes affect only on odd (even) rows. + * Dynamically style only even rows in an `ng-repeat` list. `Selector: [ng-class-even]` * - * This directive can be applied only within a scope of an `ngRepeat`. + * This directive works exactly as [ngClass] with regard to String, Array, + * and Map syntax for associating CSS classes with an element, but only affects even rows in a + * list. * - * ##Examples + * Also see [ngClassEven], which applies CSS classes to even rows. * - * index.html: - * - *
  • - * - * {{name}} - * - *
  • - * - * style.css: + * ##Example + * Let's assume that we have a simple stylesheet that defines two CSS classes for the following + * example. * * .odd { * color: red; @@ -143,6 +137,14 @@ class NgClassOdd extends _NgClassBase { * .even { * color: blue; * } + * + * The following template applies these classes to the odd and even rows respectively: + * + *
  • + * + * {{name}} + * + *
  • */ @Decorator( selector: '[ng-class-even]', diff --git a/lib/directive/ng_control.dart b/lib/directive/ng_control.dart index d49316ded..36350f73b 100644 --- a/lib/directive/ng_control.dart +++ b/lib/directive/ng_control.dart @@ -39,7 +39,7 @@ abstract class NgControl implements AttachAware, DetachAware { */ final infoStates = new Map>(); - NgControl(NgElement this._element, Injector injector, + NgControl(NgElement this._element, DirectiveInjector injector, Animate this._animate) : _parentControl = injector.parent.getByKey(NG_CONTROL_KEY); diff --git a/lib/directive/ng_events.dart b/lib/directive/ng_events.dart index 14c422d3f..e032f2388 100644 --- a/lib/directive/ng_events.dart +++ b/lib/directive/ng_events.dart @@ -133,7 +133,7 @@ class NgEvent { // Is it better to use a map of listeners or have 29 properties on this // object? One would pretty much only assign to one or two of those // properties. I'm opting for the map since it's less boilerplate code. - var listeners = {}; + var listeners = new HashMap(); final dom.Element element; final Scope scope; diff --git a/lib/directive/ng_form.dart b/lib/directive/ng_form.dart index f86237009..595d547a2 100644 --- a/lib/directive/ng_form.dart +++ b/lib/directive/ng_form.dart @@ -1,9 +1,9 @@ part of angular.directive; /** - * The form directive listens on submission requests and, depending, - * on if an action is set, the form will automatically either allow - * or prevent the default browser submission from occurring. + * Listens on form submission requests and if an action is set, either allows or + * prevents the default browser form submission action from occurring. `Selector: [ng-form]` or + * `.ng-form` or `form` or `fieldset` */ @Decorator( selector: 'form', @@ -19,9 +19,8 @@ part of angular.directive; module: NgForm.module, map: const { 'ng-form': '@name' }) class NgForm extends NgControl { - static final Module _module = new Module() - ..bind(NgControl, toFactory: (i) => i.getByKey(NG_FORM_KEY)); - static module() => _module; + static module(DirectiveBinder binder) => + binder.bind(NgControl, toInstanceOf: NG_FORM_KEY, visibility: Visibility.CHILDREN); final Scope _scope; @@ -35,7 +34,7 @@ class NgForm extends NgControl { * * [element] - The form DOM element. * * [injector] - An instance of Injector. */ - NgForm(this._scope, NgElement element, Injector injector, Animate animate) : + NgForm(this._scope, NgElement element, DirectiveInjector injector, Animate animate) : super(element, injector, animate) { if (!element.node.attributes.containsKey('action')) { diff --git a/lib/directive/ng_if.dart b/lib/directive/ng_if.dart index 562bfa181..981bf9a3b 100644 --- a/lib/directive/ng_if.dart +++ b/lib/directive/ng_if.dart @@ -4,46 +4,29 @@ part of angular.directive; * Base class for NgIf and NgUnless. */ abstract class _NgUnlessIfAttrDirectiveBase { - final BoundViewFactory _boundViewFactory; + final ViewFactory _viewFactory; final ViewPort _viewPort; final Scope _scope; View _view; - /** - * 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-prototypical-Inheritance prototypical inheritance - */ - Scope _childScope; - - _NgUnlessIfAttrDirectiveBase(this._boundViewFactory, this._viewPort, - this._scope); + _NgUnlessIfAttrDirectiveBase(this._viewFactory, this._viewPort, this._scope) { + assert(_viewFactory != null); + } // Override in subclass. void set condition(value); void _ensureViewExists() { if (_view == null) { - _childScope = _scope.createChild(new PrototypeMap(_scope.context)); - _view = _boundViewFactory(_childScope); - var view = _view; - _scope.rootScope.domWrite(() { - _viewPort.insert(view); - }); + _view = _viewPort.insertNew(_viewFactory); } } void _ensureViewDestroyed() { if (_view != null) { - var view = _view; - _scope.rootScope.domWrite(() { - _viewPort.remove(view); - }); - _childScope.destroy(); + _viewPort.remove(_view); _view = null; - _childScope = null; } } } @@ -94,9 +77,8 @@ abstract class _NgUnlessIfAttrDirectiveBase { selector:'[ng-if]', map: const {'.': '=>condition'}) class NgIf extends _NgUnlessIfAttrDirectiveBase { - NgIf(BoundViewFactory boundViewFactory, - ViewPort viewPort, - Scope scope): super(boundViewFactory, viewPort, scope); + NgIf(ViewFactory viewFactory, ViewPort viewPort, Scope scope) + : super(viewFactory, viewPort, scope); void set condition(value) { if (toBool(value)) { @@ -156,9 +138,8 @@ class NgIf extends _NgUnlessIfAttrDirectiveBase { map: const {'.': '=>condition'}) class NgUnless extends _NgUnlessIfAttrDirectiveBase { - NgUnless(BoundViewFactory boundViewFactory, - ViewPort viewPort, - Scope scope): super(boundViewFactory, viewPort, scope); + NgUnless(ViewFactory viewFactory, ViewPort viewPort, Scope scope) + : super(viewFactory, viewPort, scope); void set condition(value) { if (!toBool(value)) { diff --git a/lib/directive/ng_include.dart b/lib/directive/ng_include.dart index be450524f..2f4a0f2ce 100644 --- a/lib/directive/ng_include.dart +++ b/lib/directive/ng_include.dart @@ -23,13 +23,14 @@ class NgInclude { final dom.Element element; final Scope scope; final ViewCache viewCache; - final Injector injector; + final DirectiveInjector directiveInjector; final DirectiveMap directives; View _view; Scope _scope; - NgInclude(this.element, this.scope, this.viewCache, this.injector, this.directives); + NgInclude(this.element, this.scope, this.viewCache, + this.directiveInjector, this.directives); _cleanUp() { if (_view == null) return; @@ -42,11 +43,10 @@ class NgInclude { _scope = null; } - _updateContent(createView) { + _updateContent(ViewFactory viewFactory) { // create a new scope _scope = scope.createChild(new PrototypeMap(scope.context)); - _view = createView(injector.createChild([new Module() - ..bind(Scope, toValue: _scope)])); + _view = viewFactory(_scope, directiveInjector); _view.nodes.forEach((node) => element.append(node)); } diff --git a/lib/directive/ng_model.dart b/lib/directive/ng_model.dart index 2ce09b0e8..db307d439 100644 --- a/lib/directive/ng_model.dart +++ b/lib/directive/ng_model.dart @@ -44,11 +44,14 @@ class NgModel extends NgControl implements AttachAware { Watch _watch; bool _watchCollection; - NgModel(this._scope, NgElement element, Injector injector, NodeAttrs attrs, - Animate animate) + NgModel(this._scope, NgElement element, DirectiveInjector injector, NodeAttrs attrs, + Animate animate, ElementProbe probe) : super(element, injector, animate) { _expression = attrs["ng-model"]; + if (probe != null) { + probe.modelExpressions.add(_expression); + } watchCollection = false; //Since the user will never be editing the value of a select element then @@ -265,7 +268,7 @@ class NgModel extends NgControl implements AttachAware { /** * Creates a two-way databinding between the `ng-model` expression - * and the checkbox input element state. + * and the checkbox input element state. `Selector: input[type=checkbox][ng-model]` * * **Usage** * @@ -276,21 +279,25 @@ class NgModel extends NgControl implements AttachAware { * > * * If the optional `ng-true-value` is absent, - * - if the model expression evaluates to true or to a nonzero [:num:], + * + * * if the model expression evaluates to true or to a nonzero [:num:], * then the checkbox is checked - * - otherwise, the checkbox is unchecked + * * otherwise, the checkbox is unchecked * * If `ng-true-value="t_expr"` is present, - * - if the model expression evaluates to the same value as `t_expr`, then the checkbox is checked - * - otherwise, it is unchecked. + * + * * if the model expression evaluates to the same value as `t_expr`, then the checkbox is checked + * * otherwise, it is unchecked. * * When the checkbox is checked, - * - the model is set to the value of `t_expr` if present - * - otherwise, the model is set to `true` + * + * * the model is set to the value of `t_expr` if present + * * otherwise, the model is set to `true` * * When the checkbox is unchecked, - * - the model is set to the value of `f_expr` if present - * - otherwise, the model is set to false. + * + * * the model is set to the value of `f_expr` if present + * * otherwise, the model is set to false. * * Also see [NgTrueValue] and [NgFalseValue]. */ @@ -325,11 +332,12 @@ class InputCheckbox { /** * Creates a two-way databinding between the `ng-model` expression - * and the `` or ` * * When the `ng-model` attribute is present on the input element, @@ -345,6 +353,7 @@ class InputCheckbox { @Decorator(selector: 'input[type=email][ng-model]') @Decorator(selector: 'input[type=search][ng-model]') @Decorator(selector: 'input[type=tel][ng-model]') +@Decorator(selector: 'input[type=color][ng-model]') class InputTextLike { final dom.Element inputElement; final NgModel ngModel; @@ -362,12 +371,8 @@ class InputTextLike { ngModel.render = (value) { scope.rootScope.domWrite(() { if (value == null) value = ''; - var currentValue = typedValue; - if (value != currentValue && !(value is num && currentValue is num && - value.isNaN && currentValue.isNaN)) { - typedValue = value; - } + if (!eqOrNaN(value, currentValue)) typedValue = value; }); }; @@ -390,7 +395,7 @@ class InputTextLike { /** * Creates a two-way databinding between the `ng-model` expression - * and a numeric input element. + * and a numeric input element. `Selector:input[type=number|range][ng-model]` * * **Usage** * @@ -462,24 +467,24 @@ class InputNumberLike { } /** - * This directive affects which IDL attribute will be used to read the value of - * date/time related input directives. Recognized values for this directive are: + * Subordinate directive to [InputDateLike] that specifies the type for date/time related values. + * `Selector: input[type=date|time|datetime|datetime-local|month|week][ng-model][ng-bind-type]` + * + * This directive controls which IDL attribute is read and thus sets the type. This allows an app + * to support browsers that deviate from the HTML5 standard for date/time. * - * - [DATE]: [dom.InputElement].valueAsDate will be read. - * - [NUMBER]: [dom.InputElement].valueAsNumber will be read. - * - [STRING]: [dom.InputElement].value will be read. + * Recognized values for this directive are: * - * The default is [DATE]. Use other settings, e.g., when an app needs to support - * browsers that treat date-like inputs as text (in such a case the [STRING] - * kind would be appropriate) or, for browsers that fail to conform to the - * HTML5 standard in their processing of date-like inputs. + * - [DATE]: `dom.InputElement.valueAsDate` is read. (This is the default.) + * - [NUMBER]: `dom.InputElement.valueAsNumber` is read. + * - [STRING]: `dom.InputElement.value` is read. */ -@Decorator(selector: 'input[type=date][ng-model][ng-bind-type]') -@Decorator(selector: 'input[type=time][ng-model][ng-bind-type]') -@Decorator(selector: 'input[type=datetime][ng-model][ng-bind-type]') -@Decorator(selector: 'input[type=datetime-local][ng-model][ng-bind-type]') -@Decorator(selector: 'input[type=month][ng-model][ng-bind-type]') -@Decorator(selector: 'input[type=week][ng-model][ng-bind-type]') +@Decorator(selector: 'input[type=date][ng-model][ng-bind-type]', visibility: Visibility.LOCAL) +@Decorator(selector: 'input[type=time][ng-model][ng-bind-type]', visibility: Visibility.LOCAL) +@Decorator(selector: 'input[type=datetime][ng-model][ng-bind-type]', visibility: Visibility.LOCAL) +@Decorator(selector: 'input[type=datetime-local][ng-model][ng-bind-type]', visibility: Visibility.LOCAL) +@Decorator(selector: 'input[type=month][ng-model][ng-bind-type]', visibility: Visibility.LOCAL) +@Decorator(selector: 'input[type=week][ng-model][ng-bind-type]', visibility: Visibility.LOCAL) class NgBindTypeForDateLike { static const DATE = 'date'; static const NUMBER = 'number'; @@ -543,15 +548,16 @@ class NgBindTypeForDateLike { /** * Controls the IDL attribute that reads the value of a date/time input, - * to support browsers that deviate from the HTML5 standard for date/time. + * to support browsers that deviate from the HTML5 standard for date/time. `Selector: + * input[type=date|datetime|datetime-local|month|time|week][ng-model]` * * The [HTML5 Standard](http://www.w3.org/TR/html5/forms.html#the-input-element) for date/time - * related inputs specifies that the [dom.InputElement.valueAsDate] and - * [dom.InputElement.valueAsNumber] IDL attributes should be available for all date/time related - * input types, except for `datetime-local` which is limited to [dom.InputElement.valueNumber]. + * related inputs specifies that the `dom.InputElement.valueAsDate` and + * `dom.InputElement.valueAsNumber` IDL attributes should be available for all date/time related + * input types, except for `datetime-local` which is limited to `dom.InputElement.valueNumber`. * * This directive creates a two-way binding between the input and a model - * property. The subordinate 'ng-bind-type' directive determines which input + * property. The subordinate `ng-bind-type` directive determines which input * IDL attribute is read (see [NgBindTypeForDateLike] for details) and * hence the type of the read values. * @@ -599,8 +605,9 @@ class NgBindTypeForDateLike { @Decorator(selector: 'input[type=week][ng-model]', module: InputDateLike.moduleFactory) class InputDateLike { - static Module moduleFactory() => new Module()..bind(NgBindTypeForDateLike, - toFactory: (Injector i) => new NgBindTypeForDateLike(i.getByKey(ELEMENT_KEY))); + static void moduleFactory(DirectiveBinder binder) + => binder.bind(NgBindTypeForDateLike, + toFactory: (dom.Element e) => new NgBindTypeForDateLike(e), inject: [ELEMENT_KEY]); final dom.InputElement inputElement; final NgModel ngModel; final NgModelOptions ngModelOptions; @@ -691,11 +698,10 @@ final _uidCounter = new _UidCounter(); * the `ng-model` property when the corresponding radio element or option is * selected. */ -@Decorator(selector: 'input[type=radio][ng-model][ng-value]') -@Decorator(selector: 'option[ng-value]') +@Decorator(selector: 'input[type=radio][ng-model][ng-value]', visibility: Visibility.LOCAL) +@Decorator(selector: 'option[ng-value]', visibility: Visibility.LOCAL) class NgValue { - static Module _module = new Module()..bind(NgValue); - static Module moduleFactory() => _module; + static module(DirectiveBinder binder) => binder.bind(NgValue, visibility: Visibility.LOCAL); final dom.Element element; var _value; @@ -704,14 +710,14 @@ class NgValue { @NgOneWay('ng-value') void set value(val) { - this._value = val; + _value = val; } dynamic get value => _value == null ? (element as dynamic).value : _value; } /** * Assigns the value of a bound expression to the model when an input checkbox is - * checked. + * checked. `Selector: input[type=checkbox][ng-model][ng-true-value]` * * **Usage** * @@ -735,9 +741,9 @@ class NgTrueValue { /** * Assigns the value of a bound expression to the model when an input checkbox is - * unchecked. + * unchecked. `Selector: input[type=checkbox][ng-model][ng-false-value]` * - * **Usage** + * **Usage** * * ` element to perform data binding between the + * `option.value` attribute and the model. `Selector: select[ng-model]` * - * The [NgModel] will receive the currently selected item. The binding - * is performed on the [OPTION].[value] property. An empty [OPTION].[value] is - * treated as null. + * An empty `option.value` is treated as null. If the model specifies a value which does not map + * to an existing option, a new unknown option is inserted into the list. Once the model again + * points to an existing option, the unknown option is removed. * - * If you the model contains value which does not map to any [OPTION] then a new - * unknown [OPTION] is inserted into the list. Once the model points to an - * existing [OPTION] the unknown [OPTION] is removed. - * - * Because [OPTION].[value] attribute is a string, the model is bound to a - * string. If there is need to bind to an object then [OptionValue] - * should be used. + * # Example + * * + * Note: The `option.value` attribute for the ` + * + * * + * Note: See [InputSelect] for the simpler case where `option.value` is a string. */ -@Decorator(selector: 'option', module: NgValue.moduleFactory) +@Decorator(selector: 'option', module: NgValue.module) class OptionValue implements AttachAware, DetachAware { final InputSelect _inputSelectDirective; @@ -129,10 +139,10 @@ class _SelectMode { destroy() {} get _options => select.querySelectorAll('option'); - _forEachOption(fn, [quiteOnReturn = false]) { + _forEachOption(fn, [quitOnReturn = false]) { for (var i = 0; i < _options.length; i++) { var retValue = fn(_options[i], i); - if (quiteOnReturn && retValue != null) return retValue; + if (quitOnReturn && retValue != null) return retValue; } return null; } diff --git a/lib/directive/ng_model_validators.dart b/lib/directive/ng_model_validators.dart index b0537399f..ed051ab3f 100644 --- a/lib/directive/ng_model_validators.dart +++ b/lib/directive/ng_model_validators.dart @@ -68,6 +68,22 @@ class NgModelUrlValidator implements NgValidator { modelValue == null || modelValue.isEmpty || URL_REGEXP.hasMatch(modelValue); } +/** + * Validates the model to see if its contents match a valid color pattern. + */ +@Decorator(selector: 'input[type=color][ng-model]') +class NgModelColorValidator implements NgValidator { + static final COLOR_REGEXP = new RegExp(r'^#[0-9a-f]{6}$', caseSensitive: false); + final String name = 'ng-color'; + + NgModelColorValidator(NgModel ngModel) { + ngModel.addValidator(this); + } + + bool isValid(modelValue) => + modelValue == null || modelValue.isEmpty || COLOR_REGEXP.hasMatch(modelValue); +} + /** * Validates the model to see if its contents match a valid email pattern. */ diff --git a/lib/directive/ng_repeat.dart b/lib/directive/ng_repeat.dart index b6f92c646..6bf161776 100644 --- a/lib/directive/ng_repeat.dart +++ b/lib/directive/ng_repeat.dart @@ -87,8 +87,7 @@ class NgRepeat { Function _generateId = (key, value, index) => value; Watch _watch; - NgRepeat(this._viewPort, this._boundViewFactory, this._scope, - this._parser, this.formatters); + NgRepeat(this._viewPort, this._boundViewFactory, this._scope, this._parser, this.formatters); set expression(value) { assert(value != null); @@ -107,7 +106,7 @@ class NgRepeat { if (trackByExpr != null) { Expression trackBy = _parser(trackByExpr); _generateId = ((key, value, index) { - final context = {} + final context = new HashMap() ..[_valueIdentifier] = value ..[r'$index'] = index ..[r'$id'] = (obj) => obj; @@ -132,15 +131,7 @@ class NgRepeat { _watch = _scope.watch( _listExpr, (changes, _) { - if (changes is CollectionChangeRecord && changes != null) { - _onCollectionChange(changes); - } else if (_rows != null) { - _rows.forEach((row) { - row.scope.destroy(); - _viewPort.remove(row.view); - }); - _rows = null; - } + _onChange((changes is CollectionChangeRecord) ? changes : null); }, collection: true, formatters: formatters @@ -148,8 +139,9 @@ class NgRepeat { } // Computes and executes DOM changes when the item list changes - void _onCollectionChange(CollectionChangeRecord changes) { - final int length = changes.length; + void _onChange(CollectionChangeRecord changes) { + final iterable = (changes == null) ? const [] : changes.iterable; + final int length = (changes == null) ? 0 : changes.length; final rows = new List<_Row>(length); final changeFunctions = new List(length); final removedIndexes = []; @@ -157,62 +149,70 @@ class NgRepeat { final leftInDom = new List.generate(domLength, (i) => domLength - 1 - i); var domIndex; - Function addFn, moveFn, removeFn; + var addRow = (int index, value, View previousView) { + var childContext = _updateContext(new PrototypeMap(_scope.context), index, + length)..[_valueIdentifier] = value; + var childScope = _scope.createChild(childContext); + var view = _boundViewFactory(childScope); + var nodes = view.nodes; + rows[index] = new _Row(_generateId(index, value, index)) + ..view = view + ..scope = childScope + ..nodes = nodes + ..startNode = nodes.first + ..endNode = nodes.last; + _viewPort.insert(view, insertAfter: previousView); + }; + // todo(vicb) refactor once GH-774 gets fixed if (_rows == null) { - addFn = changes.forEachItem; - moveFn = (_) {}; - removeFn = (_) {}; + _rows = new List<_Row>(length); + for (var i = 0; i < length; i++) { + changeFunctions[i] = (index, previousView) { + addRow(index, iterable.elementAt(i), previousView); + }; + } } else { - addFn = changes.forEachAddition; - moveFn = changes.forEachMove; - removeFn = changes.forEachRemoval; - } - - removeFn((CollectionChangeItem removal) { - var index = removal.previousIndex; - var row = _rows[index]; - row.scope.destroy(); - _viewPort.remove(row.view); - leftInDom.removeAt(domLength - 1 - index); - }); + if (changes == null) { + _rows.forEach((row) { + _viewPort.remove(row.view); + }); + leftInDom.clear(); + } else { + changes.forEachRemoval((CollectionChangeItem removal) { + var index = removal.previousIndex; + var row = _rows[index]; + _viewPort.remove(row.view); + leftInDom.removeAt(domLength - 1 - index); + }); - addFn((CollectionChangeItem addition) { - changeFunctions[addition.currentIndex] = (index, previousView) { - var childContext = _updateContext(new PrototypeMap(_scope.context), index,length) - ..[_valueIdentifier] = addition.item; - var childScope = _scope.createChild(childContext); - var view = _boundViewFactory(childScope); - var nodes = view.nodes; - rows[index] = new _Row(_generateId(index, addition.item, index)) - ..view = view - ..scope = childScope - ..nodes = nodes - ..startNode = nodes.first - ..endNode = nodes.last; - _viewPort.insert(view, insertAfter: previousView); - }; - }); + changes.forEachAddition((CollectionChangeItem addition) { + changeFunctions[addition.currentIndex] = (index, previousView) { + addRow(index, addition.item, previousView); + }; + }); - moveFn((CollectionChangeItem move) { - var previousIndex = move.previousIndex; - var value = move.item; - changeFunctions[move.currentIndex] = (index, previousView) { - var previousRow = _rows[previousIndex]; - var childScope = previousRow.scope; - var childContext = _updateContext(childScope.context, index, length); - if (!identical(childScope.context[_valueIdentifier], value)) { - childContext[_valueIdentifier] = value; - } - rows[index] = _rows[previousIndex]; - // Only move the DOM node when required - if (domIndex < 0 || leftInDom[domIndex] != previousIndex) { - _viewPort.move(previousRow.view, moveAfter: previousView); - leftInDom.remove(previousIndex); - } - domIndex--; - }; - }); + changes.forEachMove((CollectionChangeItem move) { + var previousIndex = move.previousIndex; + var value = move.item; + changeFunctions[move.currentIndex] = (index, previousView) { + var previousRow = _rows[previousIndex]; + var childScope = previousRow.scope; + var childContext = _updateContext(childScope.context, index, length); + if (!identical(childScope.context[_valueIdentifier], value)) { + childContext[_valueIdentifier] = value; + } + rows[index] = _rows[previousIndex]; + // Only move the DOM node when required + if (domIndex < 0 || leftInDom[domIndex] != previousIndex) { + _viewPort.move(previousRow.view, moveAfter: previousView); + leftInDom.remove(previousIndex); + } + domIndex--; + }; + }); + } + } var previousView = null; domIndex = leftInDom.length - 1; @@ -220,9 +220,12 @@ class NgRepeat { var changeFn = changeFunctions[targetIndex]; if (changeFn == null) { rows[targetIndex] = _rows[targetIndex]; + if (domIndex < 0 || leftInDom[domIndex] != targetIndex) { + _viewPort.move(rows[targetIndex].view, moveAfter: previousView); + leftInDom.remove(targetIndex); + } domIndex--; - // The element has not moved but `$last` and `$middle` might still need - // to be updated + // The element has not moved but `$last` and `$middle` might still need to be updated _updateContext(rows[targetIndex].scope.context, targetIndex, length); } else { changeFn(targetIndex, previousView); diff --git a/lib/directive/ng_src_boolean.dart b/lib/directive/ng_src_boolean.dart index 48853d2b1..a9542febd 100644 --- a/lib/directive/ng_src_boolean.dart +++ b/lib/directive/ng_src_boolean.dart @@ -1,26 +1,21 @@ part of angular.directive; /** - * Allows adding and removing the boolean attributes from the element. + * Sets boolean HTML attributes as true or false. `Selector: + * [ng-checked]` or `[ng-disabled]` or `[ng-multiple]` or `[ng-open]` or `[ng-readonly]` or + * `[ng-required]` or `[ng-selected]` * - * Using ` */ @Decorator(selector: '[ng-checked]', map: const {'ng-checked': '=>checked'}) @Decorator(selector: '[ng-disabled]', map: const {'ng-disabled': '=>disabled'}) @@ -82,14 +77,15 @@ class NgSource { } /** - * In SVG some attributes have a specific syntax. Placing `{{interpolation}}` in - * those attributes will break the attribute syntax, and browser will clear the - * attribute. + * Provides a generic way to use `{{ }}` interpolation for attributes within validated SVG + * elements. `Selector: [ng-attr-*]` * - * The `ng-attr-*` is a generic way to use interpolation without breaking the - * attribute syntax validator. The `ng-attr-` part get stripped. + * Because the browser validates SVG syntax, using `{{interpolation}}` inside some validated + * `` elements causes the browser to ignore the interpolated value. The `ng-attr-*` selector + * inserts `{{ }}` into the element without breaking validation. (The `ng-attr-` part is stripped + * out during rendering.) * - * @example + * #Example * * * diff --git a/lib/directive/ng_switch.dart b/lib/directive/ng_switch.dart index c9aec1275..94028231d 100644 --- a/lib/directive/ng_switch.dart +++ b/lib/directive/ng_switch.dart @@ -75,7 +75,6 @@ class NgSwitch { currentViews ..forEach((_ViewScopePair pair) { pair.port.remove(pair.view); - pair.scope.destroy(); }) ..clear(); diff --git a/lib/introspection.dart b/lib/introspection.dart index 7293dd45d..f130c9397 100644 --- a/lib/introspection.dart +++ b/lib/introspection.dart @@ -3,11 +3,61 @@ */ library angular.introspection; +import 'dart:async' as async; import 'dart:html' as dom; +import 'dart:js' as js; import 'package:di/di.dart'; -import 'package:angular/introspection_js.dart'; +import 'package:angular/animate/module.dart'; import 'package:angular/core/module_internal.dart'; import 'package:angular/core_dom/module_internal.dart'; +import 'package:angular/core_dom/directive_injector.dart' show DirectiveInjector; +import 'package:angular/core/static_keys.dart'; + + +/** + * A global write only variable which keeps track of objects attached to the + * elements. This is useful for debugging AngularDart application from the + * browser's REPL. + */ +var elementExpando = new Expando('element'); + + +ElementProbe _findProbeWalkingUp(dom.Node node, [dom.Node ascendUntil]) { + while (node != null && node != ascendUntil) { + var probe = elementExpando[node]; + if (probe != null) return probe; + node = node.parent; + } + return null; +} + + +_walkProbesInTree(dom.Node node, Function walker) { + var probe = elementExpando[node]; + if (probe == null || walker(probe) != true) { + for (var child in node.childNodes) { + _walkProbesInTree(child, walker); + } + } +} + + +ElementProbe _findProbeInTree(dom.Node node, [dom.Node ascendUntil]) { + var probe; + _walkProbesInTree(node, (_probe) { + probe = _probe; + return true; + }); + return (probe != null) ? probe : _findProbeWalkingUp(node, ascendUntil); +} + + +List _findAllProbesInTree(dom.Node node) { + List probes = []; + _walkProbesInTree(node, probes.add); + return probes; +} + /** * Return the [ElementProbe] object for the closest [Element] in the hierarchy. @@ -21,25 +71,21 @@ import 'package:angular/core_dom/module_internal.dart'; * function is not intended to be called from Angular application. */ ElementProbe ngProbe(nodeOrSelector) { - var errorMsg; - var node; if (nodeOrSelector == null) throw "ngProbe called without node"; + var node = nodeOrSelector; if (nodeOrSelector is String) { var nodes = ngQuery(dom.document, nodeOrSelector); - if (nodes.isNotEmpty) node = nodes.first; - errorMsg = "Could not find a probe for the selector '$nodeOrSelector' nor its parents"; - } else { - node = nodeOrSelector; - errorMsg = "Could not find a probe for the node '$node' nor its parents"; + node = (nodes.isNotEmpty) ? nodes.first : null; } - while (node != null) { - var probe = elementExpando[node]; - if (probe != null) return probe; - node = node.parent; + var probe = _findProbeWalkingUp(node); + if (probe != null) { + return probe; } - throw errorMsg; + var forWhat = (nodeOrSelector is String) ? "selector" : "node"; + throw "Could not find a probe for the $forWhat '$nodeOrSelector' nor its parents"; } + /** * Return the [Injector] associated with a current [Element]. * @@ -47,7 +93,7 @@ ElementProbe ngProbe(nodeOrSelector) { * application from the browser's REPL, unit or end-to-end tests. The function * is not intended to be called from Angular application. */ -Injector ngInjector(nodeOrSelector) => ngProbe(nodeOrSelector).injector; +DirectiveInjector ngInjector(nodeOrSelector) => ngProbe(nodeOrSelector).injector; /** @@ -79,6 +125,7 @@ List ngQuery(dom.Node element, String selector, return list; } + /** * Return a List of directives associated with a current [Element]. * @@ -88,3 +135,239 @@ List ngQuery(dom.Node element, String selector, */ List ngDirectives(nodeOrSelector) => ngProbe(nodeOrSelector).directives; + + +js.JsObject _jsProbe(ElementProbe probe) { + return _jsify({ + "element": probe.element, + "injector": _jsInjector(probe.injector), + "scope": _jsScopeFromProbe(probe), + "directives": probe.directives.map((directive) => _jsDirective(directive)), + "bindings": probe.bindingExpressions, + "models": probe.modelExpressions + })..['_dart_'] = probe; +} + + +js.JsObject _jsInjector(DirectiveInjector injector) => + _jsify({"get": injector.get})..['_dart_'] = injector; + + +js.JsObject _jsScopeFromProbe(ElementProbe probe) => + _jsScope(probe.scope, probe.injector.getByKey(SCOPE_STATS_CONFIG_KEY)); + + + +// Work around http://dartbug.com/17752 +// Proxies a Dart function that accepts up to 10 parameters. +js.JsFunction _jsFunction(Function fn) { + const Object X = __varargSentinel; + Function fnCopy = fn; // workaround a bug. + return new js.JsFunction.withThis( + (thisArg, [o1=X, o2=X, o3=X, o4=X, o5=X, o6=X, o7=X, o8=X, o9=X, o10=X]) { + // Work around a bug in dart 1.4.0 where the closurized variable, fn, + // gets mysteriously replaced with our own closure function leading to a + // stack overflow. + fn = fnCopy; + if (o10 == null && identical(o9, X)) { + // Work around another bug in dart 1.4.0. This bug is not present in + // dart 1.5.0-dev.2.0. + // In dart 1.4.0, when running in Dartium (not dart2js), if you invoke + // a JsFunction from Dart code (either by calling .apply([args]) on it + // or by calling .callMethod(jsFuncName, [args]) on a JsObject + // containing the JsFunction, regardless of whether you specified the + // thisArg keyword parameter, the Dart function is called with the + // first argument in the thisArg param causing all the arguments to be + // shifted by one. We can detect this by the fact that o10 is null + // but o9 is X (should only happen when o9 got a default value) and + // work around it by using thisArg as the first parameter. + return __invokeFn(fn, thisArg, o1, o2, o3, o4, o5, o6, o7, o8, o9); + } else { + return __invokeFn(fn, o1, o2, o3, o4, o5, o6, o7, o8, o9, o10); + } + } + ); +} + + +const Object __varargSentinel = const Object(); + + +__invokeFn(fn, o1, o2, o3, o4, o5, o6, o7, o8, o9, o10) { + var args = [o1, o2, o3, o4, o5, o6, o7, o8, o9, o10]; + while (args.length > 0 && identical(args.last, __varargSentinel)) { + args.removeLast(); + } + return _jsify(Function.apply(fn, args)); +} + + +// Helper function to JSify a Dart object. While this is *required* to JSify +// the result of a scope.eval(), other uses are not required and are used to +// work around http://dartbug.com/17752 in a convenient way (that bug affects +// dart2js in checked mode.) +_jsify(var obj) { + if (obj == null || obj is js.JsObject) { + return obj; + } + if (obj is _JsObjectProxyable) { + return obj._toJsObject(); + } + if (obj is Function) { + return _jsFunction(obj); + } + if ((obj is Map) || (obj is Iterable)) { + var mappedObj = (obj is Map) ? + new Map.fromIterables(obj.keys, obj.values.map(_jsify)) : obj.map(_jsify); + if (obj is List) { + return new js.JsArray.from(mappedObj); + } else { + return new js.JsObject.jsify(mappedObj); + } + } + return obj; +} + + +js.JsObject _jsScope(Scope scope, ScopeStatsConfig config) { + return _jsify({ + "apply": scope.apply, + "broadcast": scope.broadcast, + "context": scope.context, + "destroy": scope.destroy, + "digest": scope.rootScope.digest, + "emit": scope.emit, + "flush": scope.rootScope.flush, + "get": (name) => scope.context[name], + "isAttached": scope.isAttached, + "isDestroyed": scope.isDestroyed, + "set": (name, value) => scope.context[name] = value, + "scopeStatsEnable": () => config.emit = true, + "scopeStatsDisable": () => config.emit = false, + r"$eval": (expr) => _jsify(scope.eval(expr)), + })..['_dart_'] = scope; +} + + +_jsDirective(directive) => directive; + + +abstract class _JsObjectProxyable { + js.JsObject _toJsObject(); +} + + +typedef List _GetExpressionsFromProbe(ElementProbe probe); + + +/** + * Returns the "$testability service" object for JS / Protractor use. + * + * JS code expects to get a hold of this object in the following way: + * + * // Prereq: There is an "angular" object on window accessible via JS. + * var testability = angular.element(document).injector().get('$testability'); + */ +class _Testability implements _JsObjectProxyable { + final dom.Node node; + final ElementProbe probe; + + _Testability(this.node, this.probe); + + whenStable(callback) { + probe.injector.get(VmTurnZone).run( + () => new async.Timer(Duration.ZERO, callback)); + } + + /** + * Returns a list of all nodes in the selected tree that have an `ng-model` + * binding specified by the [modelString]. If the optional [exactMatch] + * parameter is provided and true, it restricts the searches to bindings that + * are exact matches for [modelString]. + */ + List findModels(String modelString, [bool exactMatch]) => _findByExpression( + modelString, exactMatch, (ElementProbe probe) => probe.modelExpressions); + + /** + * Returns a list of all nodes in the selected tree that have `ng-bind` or + * mustache bindings specified by the [bindingString]. If the optional + * [exactMatch] parameter is provided and true, it restricts the searches to + * bindings that are exact matches for [bindingString]. + */ + List findBindings(String bindingString, [bool exactMatch]) => _findByExpression( + bindingString, exactMatch, (ElementProbe probe) => probe.bindingExpressions); + + List _findByExpression(String query, bool exactMatch, _GetExpressionsFromProbe getExpressions) { + List probes = _findAllProbesInTree(node); + if (probes.length == 0) { + probes.add(_findProbeWalkingUp(node)); + } + List results = []; + for (ElementProbe probe in probes) { + for (String expression in getExpressions(probe)) { + if(exactMatch == true ? expression == query : expression.indexOf(query) >= 0) { + results.add(probe.element); + } + } + } + return results; + } + + allowAnimations(bool allowed) { + Animate animate = probe.injector.get(Animate); + bool previous = animate.animationsAllowed; + animate.animationsAllowed = (allowed == true); + return previous; + } + + js.JsObject _toJsObject() { + return _jsify({ + 'allowAnimations': allowAnimations, + 'findBindings': (bindingString, [exactMatch]) => + findBindings(bindingString, exactMatch), + 'findModels': (modelExpressions, [exactMatch]) => + findModels(modelExpressions, exactMatch), + 'whenStable': (callback) => + whenStable(() => callback.apply([])), + 'notifyWhenNoOutstandingRequests': (callback) { + print("DEPRECATED: notifyWhenNoOutstandingRequests has been renamed to whenStable"); + whenStable(() => callback.apply([])); + }, + 'probe': () => _jsProbe(probe), + 'scope': () => _jsScopeFromProbe(probe), + 'eval': (expr) => probe.scope.eval(expr), + 'query': (String selector, [String containsText]) => + ngQuery(node, selector, containsText), + })..['_dart_'] = this; + } +} + + +_Testability getTestability(dom.Node node) { + ElementProbe probe = _findProbeInTree(node); + if (probe == null) { + throw ("Could not find an ElementProbe for $node.  This might happen " + "either because there is no Angular directive for that node OR " + "because your application is running with ElementProbes disabled " + "(CompilerConfig.elementProbeEnabled = false)."); + } + return new _Testability(node, probe); +} + + +void publishToJavaScript() { + var D = {}; + D['ngProbe'] = (nodeOrSelector) => _jsProbe(ngProbe(nodeOrSelector)); + D['ngInjector'] = (nodeOrSelector) => _jsInjector(ngInjector(nodeOrSelector)); + D['ngScope'] = (nodeOrSelector) => _jsScopeFromProbe(ngProbe(nodeOrSelector)); + D['ngQuery'] = (dom.Node node, String selector, [String containsText]) => + ngQuery(node, selector, containsText); + D['angular'] = { + 'resumeBootstrap': ([arg]) {}, + 'getTestability': getTestability, + }; + js.JsObject J = _jsify(D); + for (String key in D.keys) { + js.context[key] = J[key]; + } +} diff --git a/lib/introspection_js.dart b/lib/introspection_js.dart index b3b99f867..87ed9e7c3 100644 --- a/lib/introspection_js.dart +++ b/lib/introspection_js.dart @@ -38,7 +38,7 @@ js.JsObject _jsProbe(ElementProbe probe) { })..['_dart_'] = probe; } -js.JsObject _jsInjector(Injector injector) => +js.JsObject _jsInjector(injector) => new js.JsObject.jsify({"get": injector.get})..['_dart_'] = injector; js.JsObject _jsScope(Scope scope, ScopeStatsConfig config) { diff --git a/lib/metadata.dart b/lib/metadata.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/mock/module.dart b/lib/mock/module.dart index ea3407337..6978540d4 100644 --- a/lib/mock/module.dart +++ b/lib/mock/module.dart @@ -21,6 +21,7 @@ import 'dart:js' as js; import 'package:angular/angular.dart'; import 'package:angular/core/module_internal.dart'; import 'package:angular/core_dom/module_internal.dart'; +import 'package:angular/core_dom/directive_injector.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/mock/static_keys.dart'; import 'package:di/di.dart'; @@ -62,11 +63,11 @@ class AngularMockModule extends Module { bind(MockHttpBackend); bind(Element, toValue: document.body); bind(Node, toValue: document.body); - bind(HttpBackend, toFactory: (Injector i) => i.getByKey(MOCK_HTTP_BACKEND_KEY)); - bind(VmTurnZone, toFactory: (_) { + bind(HttpBackend, toInstanceOf: MOCK_HTTP_BACKEND_KEY); + bind(VmTurnZone, toFactory: () { return new VmTurnZone() ..onError = (e, s, LongStackTrace ls) => dump('EXCEPTION: $e\n$s\n$ls'); - }); + }, inject: []); bind(Window, toImplementation: MockWindow); var mockPlatform = new MockWebPlatform(); bind(MockWebPlatform, toValue: mockPlatform); diff --git a/lib/mock/probe.dart b/lib/mock/probe.dart index 09bc60a81..681549b84 100644 --- a/lib/mock/probe.dart +++ b/lib/mock/probe.dart @@ -10,9 +10,10 @@ part of angular.mock; * rootScope.myProbe.directive(SomeAttrDirective); */ @Decorator(selector: '[probe]') +@deprecated class Probe implements DetachAware { final Scope scope; - final Injector injector; + final DirectiveInjector injector; final Element element; String _probeName; diff --git a/lib/mock/test_bed.dart b/lib/mock/test_bed.dart index 54578868b..e0cf5064d 100644 --- a/lib/mock/test_bed.dart +++ b/lib/mock/test_bed.dart @@ -8,6 +8,7 @@ part of angular.mock; */ class TestBed { final Injector injector; + final DirectiveInjector directiveInjector; final Scope rootScope; final Compiler compiler; final Parser _parser; @@ -17,7 +18,11 @@ class TestBed { List rootElements; View rootView; - TestBed(this.injector, this.rootScope, this.compiler, this._parser, this.expando); + TestBed(this.injector, this.directiveInjector, this.rootScope, this.compiler, this._parser, this.expando); + + TestBed.fromInjector(Injector i) : + this(i, i.get(DirectiveInjector), i.get(RootScope), i.get(Compiler), + i.get(Parser), i.get(Expando)); /** @@ -35,10 +40,7 @@ class TestBed { * An option [scope] parameter can be supplied to link it with non root scope. */ Element compile(html, {Scope scope, DirectiveMap directives}) { - var injector = this.injector; - if (scope != null) { - injector = injector.createChild([new Module()..bind(Scope, toValue: scope)]); - } + if (scope == null) scope = rootScope; if (html is String) { rootElements = toNodeList(html); } else if (html is Node) { @@ -52,7 +54,7 @@ class TestBed { if (directives == null) { directives = injector.getByKey(DIRECTIVE_MAP_KEY); } - rootView = compiler(rootElements, directives)(injector, rootElements); + rootView = compiler(rootElements, directives)(scope, injector.get(DirectiveInjector), rootElements); return rootElement; } diff --git a/lib/mock/test_injection.dart b/lib/mock/test_injection.dart index 33d7b8f2f..79aea419b 100644 --- a/lib/mock/test_injection.dart +++ b/lib/mock/test_injection.dart @@ -3,26 +3,26 @@ library angular.mock.test_injection; import 'package:angular/application_factory.dart'; import 'package:angular/mock/module.dart'; import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; +import 'dart:mirrors'; _SpecInjector _currentSpecInjector = null; class _SpecInjector { - DynamicInjector moduleInjector; - DynamicInjector injector; + Injector moduleInjector; + Injector injector; dynamic injectiorCreateLocation; final modules = []; final initFns = []; _SpecInjector() { var moduleModule = new Module() - ..bind(Module, toFactory: (Injector injector) => addModule(new Module())); - moduleInjector = new DynamicInjector(modules: [moduleModule]); + ..bind(Module, toFactory: () => addModule(new Module())); + moduleInjector = new ModuleInjector([moduleModule]); } addModule(module) { if (injector != null) { - throw ["Injector already crated, can not add more modules."]; + throw ["Injector already created, can not add more modules."]; } modules.add(module); return module; @@ -34,7 +34,7 @@ class _SpecInjector { } try { if (fnOrModule is Function) { - var initFn = moduleInjector.invoke(fnOrModule); + var initFn = _invoke(moduleInjector, fnOrModule); if (initFn is Function) initFns.add(initFn); } else if (fnOrModule is Module) { addModule(fnOrModule); @@ -50,12 +50,12 @@ class _SpecInjector { try { if (injector == null) { injectiorCreateLocation = declarationStack; - injector = new DynamicInjector(modules: modules); // Implicit injection is disabled. + injector = new ModuleInjector(modules); // Implicit injection is disabled. initFns.forEach((fn) { - injector.invoke(fn); + _invoke(injector, fn); }); } - injector.invoke(fn); + _invoke(injector, fn); } catch (e, s) { throw "$e\n$s\nDECLARED AT:$declarationStack"; } @@ -65,6 +65,20 @@ class _SpecInjector { injector = null; injectiorCreateLocation = null; } + + _invoke(Injector injector, Function fn) { + ClosureMirror cm = reflect(fn); + MethodMirror mm = cm.function; + List args = mm.parameters.map((ParameterMirror parameter) { + var metadata = parameter.metadata; + Key key = new Key( + (parameter.type as ClassMirror).reflectedType, + metadata.isEmpty ? null : metadata.first.type.reflectedType); + return injector.getByKey(key); + }).toList(); + + return cm.apply(args).reflectee; + } } /** diff --git a/lib/mock/zone.dart b/lib/mock/zone.dart index ab8c3868c..153539a57 100644 --- a/lib/mock/zone.dart +++ b/lib/mock/zone.dart @@ -68,6 +68,25 @@ microLeap() { */ isAsyncQueueEmpty() => _asyncQueue.isEmpty; +/** + * Returns whether there are outstanding timers. + */ +isTimerQueueEmpty() => _timerQueue.isEmpty; + +/** + * Returns whether there are outstanding non-periodic timers. + */ +isNonPeriodicTimerQueueEmpty() => _timerQueue + .where((_TimerSpec spec) => !spec.periodic) + .isEmpty; + +/** + * Returns whether there are outstanding periodic timers. + */ +isPeriodicTimerQueueEmpty() => _timerQueue + .where((_TimerSpec spec) => spec.periodic) + .isEmpty; + /** * Simulates a clock tick by running any scheduled timers. Can only be used * in [async] tests.Clock tick will call [microLeap] to process the microtask diff --git a/lib/perf/module.dart b/lib/perf/module.dart index 01de8a563..7ee7b4072 100644 --- a/lib/perf/module.dart +++ b/lib/perf/module.dart @@ -22,6 +22,6 @@ part 'dev_tools_timeline.dart'; class PerfModule extends Module { PerfModule() { - bind(Profiler, toImplementation: Profiler); + bind(Profiler, toFactory: () => new Profiler(), inject: []); } } diff --git a/lib/playback/playback_http.dart b/lib/playback/playback_http.dart index 7efe1d2f6..2522a3605 100644 --- a/lib/playback/playback_http.dart +++ b/lib/playback/playback_http.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:convert' show JSON; import 'dart:html'; +import 'package:di/annotations.dart'; import 'package:angular/core_dom/module_internal.dart'; import 'package:angular/core/annotation_src.dart'; import 'package:angular/mock/http_backend.dart' as mock; diff --git a/lib/routing/module.dart b/lib/routing/module.dart index 624c1ed0c..7430de4cd 100644 --- a/lib/routing/module.dart +++ b/lib/routing/module.dart @@ -126,6 +126,7 @@ import 'dart:async'; import 'dart:html'; import 'package:di/di.dart'; +import 'package:di/annotations.dart'; import 'package:angular/application.dart'; import 'package:angular/core/annotation_src.dart'; import 'package:angular/core/module_internal.dart'; @@ -133,6 +134,7 @@ import 'package:angular/core_dom/module_internal.dart'; import 'package:route_hierarchical/client.dart'; import 'package:angular/routing/static_keys.dart'; +import 'dart:collection'; part 'routing.dart'; part 'ng_view.dart'; @@ -141,10 +143,10 @@ part 'ng_bind_route.dart'; class RoutingModule extends Module { RoutingModule({bool usePushState: true}) { bind(NgRoutingUsePushState); - bind(Router, toFactory: (injector) { - var useFragment = !injector.getByKey(NG_ROUTING_USE_PUSH_STATE_KEY).usePushState; - return new Router(useFragment: useFragment, windowImpl: injector.getByKey(WINDOW_KEY)); - }); + bind(Router, toFactory: (NgRoutingUsePushState state, Window window) { + var useFragment = !state.usePushState; + return new Router(useFragment: useFragment, windowImpl: window); + }, inject: [NG_ROUTING_USE_PUSH_STATE_KEY, WINDOW_KEY]); bind(NgRoutingHelper); bind(RouteProvider, toValue: null); bind(RouteInitializer, toValue: null); diff --git a/lib/routing/ng_bind_route.dart b/lib/routing/ng_bind_route.dart index 935a65ee2..0956d7c98 100644 --- a/lib/routing/ng_bind_route.dart +++ b/lib/routing/ng_bind_route.dart @@ -29,13 +29,10 @@ part of angular.routing; class NgBindRoute implements RouteProvider { String routeName; final Router _router; - final Injector _injector; + final DirectiveInjector _injector; - static final Module _module = new Module() - ..bind(RouteProvider, toFactory: (i) => i.getByKey(NG_BIND_ROUTE_KEY), - visibility: Directive.CHILDREN_VISIBILITY); - - static Module module() => _module; + static void module(DirectiveBinder binder) => + binder.bind(RouteProvider, toInstanceOf: NG_BIND_ROUTE_KEY, visibility: Visibility.CHILDREN); // We inject NgRoutingHelper to force initialization of routing. NgBindRoute(this._router, this._injector, NgRoutingHelper _); diff --git a/lib/routing/ng_view.dart b/lib/routing/ng_view.dart index a9c489d15..3a60fc05f 100644 --- a/lib/routing/ng_view.dart +++ b/lib/routing/ng_view.dart @@ -53,16 +53,16 @@ part of angular.routing; */ @Decorator( selector: 'ng-view', - module: NgView.module) + module: NgView.module, + visibility: Visibility.CHILDREN) class NgView implements DetachAware, RouteProvider { - static final Module _module = new Module() - ..bind(RouteProvider, toFactory: (i) => i.getByKey(NG_VIEW_KEY)); - - static Module module() => _module; + static void module(DirectiveBinder binder) => + binder.bind(RouteProvider, toInstanceOf: NG_VIEW_KEY, visibility: Visibility.CHILDREN); final NgRoutingHelper _locationService; final ViewCache _viewCache; - final Injector _injector; + final Injector _appInjector; + final DirectiveInjector _dirInjector; final Element _element; final Scope _scope; RouteHandle _route; @@ -71,11 +71,12 @@ class NgView implements DetachAware, RouteProvider { Scope _childScope; Route _viewRoute; - NgView(this._element, this._viewCache, Injector injector, Router router, this._scope) - : _injector = injector, - _locationService = injector.getByKey(NG_ROUTING_HELPER_KEY) + NgView(this._element, this._viewCache, DirectiveInjector dirInjector, this._appInjector, + Router router, this._scope) + : _dirInjector = dirInjector, + _locationService = dirInjector.getByKey(NG_ROUTING_HELPER_KEY) { - RouteProvider routeProvider = injector.parent.getByKey(NG_VIEW_KEY); + RouteProvider routeProvider = dirInjector.parent.getByKey(NG_VIEW_KEY); _route = routeProvider != null ? routeProvider.route.newHandle() : router.root.newHandle(); @@ -90,6 +91,7 @@ class NgView implements DetachAware, RouteProvider { void detach() { _route.discard(); _locationService._unregisterPortal(this); + _cleanUp(); } void _show(_View viewDef, Route route, List modules) { @@ -106,19 +108,22 @@ class NgView implements DetachAware, RouteProvider { _cleanUp(); }); - var viewInjector = modules == null ? - _injector : - forceNewDirectivesAndFormatters(_injector, modules); + Injector viewInjector = _appInjector; + DirectiveInjector directiveInjector = _dirInjector; + + if (modules != null) { + viewInjector = forceNewDirectivesAndFormatters(_appInjector, _dirInjector, modules); + directiveInjector = viewInjector.get(DirectiveInjector); + } var newDirectives = viewInjector.getByKey(DIRECTIVE_MAP_KEY); var viewFuture = viewDef.templateHtml != null ? new Future.value(_viewCache.fromHtml(viewDef.templateHtml, newDirectives)) : _viewCache.fromUrl(viewDef.template, newDirectives); - viewFuture.then((viewFactory) { + viewFuture.then((ViewFactory viewFactory) { _cleanUp(); _childScope = _scope.createChild(new PrototypeMap(_scope.context)); - _view = viewFactory( - viewInjector.createChild([new Module()..bind(Scope, toValue: _childScope)])); + _view = viewFactory(_childScope, directiveInjector); _view.nodes.forEach((elm) => _element.append(elm)); }); } @@ -128,7 +133,6 @@ class NgView implements DetachAware, RouteProvider { _view.nodes.forEach((node) => node.remove()); _childScope.destroy(); - _view = null; _childScope = null; } @@ -138,7 +142,7 @@ class NgView implements DetachAware, RouteProvider { String get routeName => _viewRoute.name; Map get parameters { - var res = {}; + var res = new HashMap(); var p = _viewRoute; while (p != null) { res.addAll(p.parameters); diff --git a/lib/routing/routing.dart b/lib/routing/routing.dart index c40f9d29e..91a57c0b5 100644 --- a/lib/routing/routing.dart +++ b/lib/routing/routing.dart @@ -55,6 +55,11 @@ class RouteViewFactory { cfg.preEnter(e); } }, + preLeave: (RoutePreLeaveEvent e) { + if (cfg.preLeave != null) { + cfg.preLeave(e); + } + }, leave: cfg.leave, mount: (Route mountRoute) { if (cfg.mount != null) { @@ -68,10 +73,10 @@ class RouteViewFactory { NgRouteCfg ngRoute({String path, String view, String viewHtml, Map mount, modules(), bool defaultRoute: false, RoutePreEnterEventHandler preEnter, RouteEnterEventHandler enter, - RouteLeaveEventHandler leave}) => + RoutePreLeaveEventHandler preLeave, RouteLeaveEventHandler leave}) => new NgRouteCfg(path: path, view: view, viewHtml: viewHtml, mount: mount, modules: modules, defaultRoute: defaultRoute, preEnter: preEnter, - enter: enter, leave: leave); + preLeave: preLeave, enter: enter, leave: leave); class NgRouteCfg { final String path; @@ -82,10 +87,11 @@ class NgRouteCfg { final bool defaultRoute; final RouteEnterEventHandler enter; final RoutePreEnterEventHandler preEnter; + final RoutePreLeaveEventHandler preLeave; final RouteLeaveEventHandler leave; NgRouteCfg({this.view, this.viewHtml, this.path, this.mount, this.modules, - this.defaultRoute, this.enter, this.preEnter, this.leave}); + this.defaultRoute, this.enter, this.preEnter, this.preLeave, this.leave}); } /** diff --git a/lib/tools/expression_extractor.dart b/lib/tools/expression_extractor.dart index 006ba83e1..acc231aa5 100644 --- a/lib/tools/expression_extractor.dart +++ b/lib/tools/expression_extractor.dart @@ -10,9 +10,10 @@ import 'package:angular/tools/io_impl.dart'; import 'package:angular/tools/common.dart'; import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; import 'package:angular/core/parser/parser.dart'; +import 'package:angular/core/parser/lexer.dart'; +import 'package:angular/cache/module.dart'; import 'package:angular/tools/parser_getter_setter/generator.dart'; main(args) { @@ -52,10 +53,14 @@ main(args) { printer.write('// Found ${expressions.length} expressions\n'); Module module = new Module() - ..bind(Parser, toImplementation: DynamicParser) - ..bind(ParserBackend, toImplementation: DartGetterSetterGen); - Injector injector = - new DynamicInjector(modules: [module], allowImplicitInjection: true); + ..bind(ParserGetterSetter) + ..bind(Lexer) + ..bind(DynamicParser) + ..bind(DartGetterSetterGen) + ..bind(CacheRegister) + ..bind(Parser, toInstanceOf: DynamicParser) + ..bind(ParserBackend, toInstanceOf: DartGetterSetterGen); + Injector injector = new ModuleInjector([module]); runZoned(() { // Run the generator. diff --git a/lib/tools/transformer/expression_generator.dart b/lib/tools/transformer/expression_generator.dart index 020e5d585..4bd1fdb74 100644 --- a/lib/tools/transformer/expression_generator.dart +++ b/lib/tools/transformer/expression_generator.dart @@ -2,7 +2,9 @@ library angular.tools.transformer.expression_generator; import 'dart:async'; import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/cache/module.dart'; import 'package:angular/core/parser/parser.dart'; +import 'package:angular/core/parser/lexer.dart'; import 'package:angular/tools/html_extractor.dart'; import 'package:angular/tools/parser_getter_setter/generator.dart'; import 'package:angular/tools/source_crawler.dart'; @@ -12,7 +14,7 @@ import 'package:angular/tools/transformer/referenced_uris.dart'; import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; +import 'package:di/src/reflector_dynamic.dart'; import 'package:path/path.dart' as path; /** @@ -45,11 +47,13 @@ class ExpressionGenerator extends Transformer with ResolverTransformer { return _getHtmlSources(transform, resolver) .forEach(htmlExtractor.parseHtml) .then((_) { - var module = new Module() + var module = new Module.withReflector(getReflector()) + ..install(new CacheModule.withReflector(getReflector())) ..bind(Parser, toImplementation: DynamicParser) - ..bind(ParserBackend, toImplementation: DartGetterSetterGen); - var injector = - new DynamicInjector(modules: [module], allowImplicitInjection: true); + ..bind(ParserBackend, toImplementation: DartGetterSetterGen) + ..bind(Lexer) + ..bind(_ParserGetterSetter); + var injector = new ModuleInjector([module]); injector.get(_ParserGetterSetter).generateParser( htmlExtractor.expressions.toList(), outputBuffer); diff --git a/lib/tools/transformer/options.dart b/lib/tools/transformer/options.dart index 3a730075a..fc6560a17 100644 --- a/lib/tools/transformer/options.dart +++ b/lib/tools/transformer/options.dart @@ -1,6 +1,6 @@ library angular.tools.transformer.options; -import 'package:di/transformer/options.dart' as di; +import 'package:di/transformer.dart' as di show TransformOptions; /** Options used by Angular transformers */ class TransformOptions { diff --git a/lib/tools/transformer/static_angular_generator.dart b/lib/tools/transformer/static_angular_generator.dart index 284b9924f..69f130eea 100644 --- a/lib/tools/transformer/static_angular_generator.dart +++ b/lib/tools/transformer/static_angular_generator.dart @@ -51,9 +51,6 @@ class StaticAngularGenerator extends Transformer with ResolverTransformer { _addImport(transaction, unit, '${generatedFilePrefix}_static_metadata.dart', 'generated_static_metadata'); - _addImport(transaction, unit, - '${generatedFilePrefix}_static_injector.dart', - 'generated_static_injector'); var printer = transaction.commit(); var url = id.path.startsWith('lib/') @@ -82,7 +79,6 @@ class _NgDynamicToStaticVisitor extends GeneralizingAstVisitor { var args = m.argumentList; transaction.edit(args.beginToken.offset + 1, args.end - 1, - 'generated_static_injector.factories, ' 'generated_static_metadata.typeAnnotations, ' 'generated_static_expressions.getters, ' 'generated_static_expressions.setters, ' diff --git a/lib/transformer.dart b/lib/transformer.dart index 0d8668203..5c68cbb91 100644 --- a/lib/transformer.dart +++ b/lib/transformer.dart @@ -9,8 +9,7 @@ import 'package:angular/tools/transformer/html_dart_references_generator.dart'; import 'package:angular/tools/transformer/options.dart'; import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; -import 'package:di/transformer/injector_generator.dart' show InjectorGenerator; -import 'package:di/transformer/options.dart' as di; +import 'package:di/transformer.dart' as di; import 'package:path/path.dart' as path; @@ -34,7 +33,7 @@ class AngularTransformerGroup implements TransformerGroup { TransformOptions _parseSettings(Map args) { // Default angular annotations for injectable types var annotations = [ - 'angular.core.annotation_src.Injectable', + 'di.annotations.Injectable', 'angular.core.annotation_src.Decorator', 'angular.core.annotation_src.Controller', 'angular.core.annotation_src.Component', @@ -48,10 +47,7 @@ TransformOptions _parseSettings(Map args) { injectedTypes.addAll(_readStringListValue(args, 'injected_types')); var sdkDir = _readStringValue(args, 'dart_sdk', required: false); - if (sdkDir == null) { - // Assume the Pub executable is always coming from the SDK. - sdkDir = path.dirname(path.dirname(Platform.executable)); - } + if (sdkDir == null) sdkDir = dartSdkDirectory; var diOptions = new di.TransformOptions( injectableAnnotations: annotations, @@ -123,10 +119,10 @@ Map _readStringMapValue(Map args, String name) { List> _createPhases(TransformOptions options) { var resolvers = new Resolvers(options.sdkDirectory); return [ - [new HtmlDartReferencesGenerator(options)], - [new _SerialTransformer([ + [ new HtmlDartReferencesGenerator(options) ], + [ new di.InjectorGenerator(options.diOptions, resolvers) ], + [ new _SerialTransformer([ new ExpressionGenerator(options, resolvers), - new InjectorGenerator(options.diOptions, resolvers), new MetadataGenerator(options, resolvers), new StaticAngularGenerator(options, resolvers) ])] diff --git a/package.json b/package.json index a3c4e429c..e8f12c6c0 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,14 @@ "devDependencies": { "karma" : "~0.12.0", "karma-dart" : "~0.2.6", + "karma-sauce-launcher": "^0.2.3", "karma-script-launcher": "*", "karma-chrome-launcher": "~0.1.3", "karma-firefox-launcher": "*", "karma-junit-reporter": "~0.2.1", "jasmine-node": "*", + "jasmine-reporters": "~0.4.1", + "protractor-dart": "0.0.5", "qq": "*" }, "licenses": [ diff --git a/pubspec.lock b/pubspec.lock index 3cdc8c6dd..1660722b2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -4,7 +4,7 @@ packages: analyzer: description: analyzer source: hosted - version: "0.13.6" + version: "0.18.0" args: description: args source: hosted @@ -12,7 +12,7 @@ packages: barback: description: barback source: hosted - version: "0.13.0" + version: "0.14.1+3" benchmark_harness: description: benchmark_harness source: hosted @@ -24,19 +24,19 @@ packages: code_transformers: description: code_transformers source: hosted - version: "0.1.3+1" + version: "0.1.5" collection: description: collection source: hosted - version: "0.9.2" + version: "0.9.4" di: description: di source: hosted - version: "1.0.0" + version: "2.0.1" guinness: description: guinness source: hosted - version: "0.1.5" + version: "0.1.9" html5lib: description: html5lib source: hosted @@ -44,7 +44,11 @@ packages: intl: description: intl source: hosted - version: "0.9.9" + version: "0.8.10+4" + js: + description: js + source: hosted + version: "0.2.2" logging: description: logging source: hosted @@ -52,19 +56,27 @@ packages: matcher: description: matcher source: hosted - version: "0.10.0" + version: "0.11.0" + meta: + description: meta + source: hosted + version: "0.8.8" mock: description: mock source: hosted - version: "0.11.0+1" + version: "0.11.0+2" path: description: path source: hosted - version: "1.2.0" + version: "1.2.1" perf_api: description: perf_api source: hosted version: "0.0.8" + protractor: + description: protractor + source: hosted + version: "0.0.5" route_hierarchical: description: route_hierarchical source: hosted @@ -72,15 +84,19 @@ packages: source_maps: description: source_maps source: hosted - version: "0.9.0" + version: "0.9.3" stack_trace: description: stack_trace source: hosted - version: "0.9.3+2" + version: "1.0.2" + typed_mock: + description: typed_mock + source: hosted + version: "0.0.4" unittest: description: unittest source: hosted - version: "0.11.0" + version: "0.11.0+3" utf: description: utf source: hosted @@ -88,4 +104,4 @@ packages: web_components: description: web_components source: hosted - version: "0.3.4" + version: "0.3.5+1" diff --git a/pubspec.yaml b/pubspec.yaml index fe4f30bc1..148f9a1d2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angular -version: 0.12.0 +version: 0.13.0 authors: - Misko Hevery - Pavel Jbanov @@ -13,20 +13,21 @@ homepage: https://angulardart.org environment: sdk: '>=1.4.0' dependencies: - args: '>=0.10.0 < 0.11.0' - analyzer: '>=0.13.0 <0.14.0' - barback: '>=0.11.1 <0.14.0' + args: '>=0.10.0 <0.11.0' + analyzer: '>=0.15.0 <0.19.0' + barback: '>=0.13.0 <0.17.0' browser: '>=0.10.0 <0.11.0' - code_transformers: '>=0.1.3 <0.2.0' + code_transformers: '>=0.1.4+2 <0.2.0' collection: '>=0.9.1 <1.0.0' - di: '>=1.0.0 <2.0.0' + di: '>=2.0.1 <3.0.0' html5lib: '>=0.10.0 <0.11.0' - intl: '>=0.8.7 <0.10.0' + intl: '>=0.8.7 <0.12.0' perf_api: '>=0.0.8 <0.1.0' route_hierarchical: '>=0.4.21 <0.5.0' web_components: '>=0.3.3 <0.4.0' dev_dependencies: - benchmark_harness: '>=1.0.0' - unittest: '>=0.10.1 <0.12.0' + benchmark_harness: '>=1.0.0 <2.0.0' + guinness: '>=0.1.9 <0.2.0' mock: '>=0.10.0 <0.12.0' - guinness: '>=0.1.3 <0.2.0' + protractor: '0.0.5' + unittest: '>=0.10.1 <0.12.0' diff --git a/renderedText.dart b/renderedText.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/analyze.sh b/scripts/analyze.sh index 5b99839c7..2cf759e73 100755 --- a/scripts/analyze.sh +++ b/scripts/analyze.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -e -o pipefail . $(dirname $0)/env.sh diff --git a/scripts/env.sh b/scripts/env.sh index 750e2b577..823605a1e 100755 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -1,54 +1,82 @@ #!/bin/false -set -e +set -e -o pipefail if [[ -z $ENV_SET ]]; then export ENV_SET=1 - if [ -n "$DART_SDK" ]; then - DARTSDK=$DART_SDK + # Map DART_SDK and DARTSDK to each other if only one is specified. + # + # TODO(chirayu): Remove this legacy DARTSDK variable support. Check with Misko + # to see if he's using it on this Mac. + if [[ -z "$DART_SDK" ]]; then + : "${DARTSDK:=$DART_SDK}" else - echo "sdk=== $DARTSDK" - DART=`which dart|cat` # pipe to cat to ignore the exit code - DARTSDK=`which dart | sed -e 's/\/dart\-sdk\/.*$/\/dart-sdk/'` - - if [ "$DARTSDK" = "/Applications/dart/dart-sdk" ]; then - # Assume we are a mac machine with standard dart setup - export DARTIUM="/Applications/dart/chromium/Chromium.app/Contents/MacOS/Chromium" - else - DARTSDK="`pwd`/dart-sdk" - case $( uname -s ) in - Darwin) - export DARTIUM=${DARTIUM:-./dartium/Chromium.app/Contents/MacOS/Chromium} - ;; - Linux) - export DARTIUM=${DARTIUM:-./dartium/chrome} - ;; - esac - fi + : "${DART_SDK:=$DARTSDK}" fi - case $( uname -s ) in - Darwin) + unset DART + PLATFORM="$(uname -s)" + + case "$PLATFORM" in + (Darwin) path=$(readlink ${BASH_SOURCE[0]}||echo './scripts/env.sh') export NGDART_SCRIPT_DIR=$(dirname $path) ;; - Linux) + (Linux) export NGDART_SCRIPT_DIR=$(dirname $(readlink -f ${BASH_SOURCE[0]})) ;; + (*) + echo Unsupported platform $PLATFORM. Exiting ... >&2 + exit 3 + ;; esac + export NGDART_BASE_DIR=$(dirname $NGDART_SCRIPT_DIR) - export DART_SDK="$DARTSDK" + # Try to find the SDK alongside the dart command first. + if [[ -z "$DART_SDK" ]]; then + DART=$(which dart) || true + if [[ -x "$DART" ]]; then + DART_SDK="${DART/dart-sdk\/*/dart-sdk}" + if [[ ! -e "$DART_SDK" ]]; then + unset DART DART_SDK + fi + fi + fi + # Fallback: Assume it's alongside the current directory (e.g. Travis). + if [[ -z "$DART_SDK" ]]; then + DART_SDK="$(pwd)/dart-sdk" + fi + + : "${DART:=$DART_SDK/bin/dart}" + + if [[ ! -x "$DART" ]]; then + echo Unable to locate the dart binary / SDK. Exiting >&2 + exit 3 + fi + + if [[ -z "$DARTIUM" ]]; then + dartiumRoot="$DART_SDK/../chromium" + if [[ -e "$dartiumRoot" ]]; then + case "$PLATFORM" in + (Linux) export DARTIUM="$dartiumRoot/chrome" ;; + (Darwin) export DARTIUM="$dartiumRoot/Chromium.app/Contents/MacOS/Chromium" ;; + (*) echo Unsupported platform $PLATFORM. Exiting ... >&2 ; exit 3 ;; + esac + fi + fi + + export DART_SDK export DARTSDK - export DART=${DART:-"$DARTSDK/bin/dart"} - export PUB=${PUB:-"$DARTSDK/bin/pub"} - export DARTANALYZER=${DARTANALYZER:-"$DARTSDK/bin/dartanalyzer"} - export DARTDOC=${DARTDOC:-"$DARTSDK/bin/dartdoc"} - export DART_DOCGEN=${DART_DOCGEN:-"$DARTSDK/bin/docgen"} + export DART + export PUB=${PUB:-"$DART_SDK/bin/pub"} + export DARTANALYZER=${DARTANALYZER:-"$DART_SDK/bin/dartanalyzer"} + export DARTDOC=${DARTDOC:-"$DART_SDK/bin/dartdoc"} + export DART_DOCGEN=${DART_DOCGEN:-"$DART_SDK/bin/docgen"} export DART_VM_OPTIONS="--old_gen_heap_size=2048" export DARTIUM_BIN=${DARTIUM_BIN:-"$DARTIUM"} export CHROME_BIN=${CHROME_BIN:-"google-chrome"} - export PATH=$PATH:$DARTSDK/bin + export PATH=$PATH:$DART_SDK/bin echo '*********' echo '** ENV **' @@ -66,4 +94,4 @@ if [[ -z $ENV_SET ]]; then echo NGDART_SCRIPT_DIR=$NGDART_SCRIPT_DIR $DART --version 2>&1 -fi \ No newline at end of file +fi diff --git a/scripts/generate-documentation.sh b/scripts/generate-documentation.sh index ffcee0301..a3da72af6 100755 --- a/scripts/generate-documentation.sh +++ b/scripts/generate-documentation.sh @@ -17,8 +17,8 @@ cat README-orig.md | sed "1s/^AngularDart.*/AngularDart/" > README.md --start-page=angular \ --exclude-lib=js,metadata,meta,mirrors,intl,number_symbols,number_symbol_data,intl_helpers,date_format_internal,date_symbols,angular.util \ --no-include-sdk \ + --include-dependent-packages \ --package-root=packages \ - lib/angular.dart \ lib/application_factory.dart \ lib/application_factory_static.dart \ lib/application.dart lib/introspection.dart \ @@ -30,6 +30,7 @@ cat README-orig.md | sed "1s/^AngularDart.*/AngularDart/" > README.md lib/routing/module.dart \ lib/mock/module.dart \ lib/perf/module.dart \ + lib/di/module.dart ) # Revert the temp copy of the README.md file @@ -54,7 +55,7 @@ fi; # Create a version file from the current build version doc_version=`head CHANGELOG.md | awk 'NR==2' | sed 's/^# //'` -dartsdk_version=`cat $DARTSDK/version` +dartsdk_version=`cat $DART_SDK/version` head_sha=`git rev-parse --short HEAD` echo $doc_version at $head_sha \(with Dart SDK $dartsdk_version\) > docs/VERSION diff --git a/scripts/git/validate-commit-msg.js b/scripts/git/validate-commit-msg.js index b00b6bf59..a8fd3cbca 100755 --- a/scripts/git/validate-commit-msg.js +++ b/scripts/git/validate-commit-msg.js @@ -23,7 +23,8 @@ var TYPES = { refactor: true, test: true, chore: true, - revert: true + revert: true, + perf: true }; diff --git a/scripts/run-e2e-test.sh b/scripts/run-e2e-test.sh new file mode 100755 index 000000000..6a932ed6d --- /dev/null +++ b/scripts/run-e2e-test.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# Run E2E / Protractor tests. + +set -e -o pipefail + +. $(dirname $0)/env.sh + +SIGNALS=(ERR HUP INT QUIT PIPE TERM) + +_onSignal() { + EXIT_CODE=$? + # Kill all child processes (running servers.) + kill 0 + # Need to explicitly kill ourselves to let the caller know we died from a + # signal. Ref: http://www.cons.org/cracauer/sigint.html + sig=$1 + trap - "${SIGNALS[@]}" # disable signals so we don't capture them again. + if [[ "$sig" == "ERR" ]]; then + exit $EXIT_CODE + else + kill -$sig $$ + fi +} + +for s in "${SIGNALS[@]}" ; do + trap "_onSignal $s" $s +done + + +install_deps() {( + SELENIUM_VER="2.42" + SELENIUM_ZIP="selenium-server-standalone-$SELENIUM_VER.0.jar" + CHROMEDRIVER_VER="2.10" + # chromedriver + case "$(uname -s)" in + (Darwin) CHROMEDRIVER_ZIP="chromedriver_mac32.zip" ;; + (Linux) CHROMEDRIVER_ZIP="chromedriver_linux64.zip" ;; + (*) echo Unsupported OS >&2; exit 2 ;; + esac + mkdir -p e2e_bin && cd e2e_bin + if [[ ! -e "$SELENIUM_ZIP" ]]; then + curl -O "http://selenium-release.storage.googleapis.com/$SELENIUM_VER/$SELENIUM_ZIP" + fi + if [[ ! -e "$CHROMEDRIVER_ZIP" ]]; then + curl -O "http://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VER/$CHROMEDRIVER_ZIP" + unzip "$CHROMEDRIVER_ZIP" + fi +)} + + +start_servers() { + # Run examples. + ( + cd example + pub install + pub build + rsync -rl --exclude packages web/ build/web/ + rm -rf build/web/packages + ln -s $PWD/packages build/web/packages + ) + PORT=28000 + (cd example/build/web && python -m SimpleHTTPServer $PORT) >/dev/null 2>&1 & + export NGDART_EXAMPLE_BASEURL=http://127.0.0.1:$PORT + + # Allow chromedriver to be found on the system path. + export PATH=$PATH:$PWD/e2e_bin + + # Start selenium. Kill all output - selenium is extremely noisy. + java -jar ./e2e_bin/selenium-server-standalone-2.42.0.jar >/dev/null 2>&1 & + + sleep 4 # wait for selenium startup +} + + +# Main +install_deps +start_servers +(cd test_e2e && pub install) +./node_modules/.bin/protractor_dart test_e2e/examplesConf.js diff --git a/scripts/run-test.sh b/scripts/run-test.sh index f60e1a613..beef3c293 100755 --- a/scripts/run-test.sh +++ b/scripts/run-test.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -e -o pipefail . $(dirname $0)/env.sh @@ -33,3 +33,5 @@ $NGDART_SCRIPT_DIR/analyze.sh && --reporters=junit,dots --port=8765 --runner-port=8766 \ --browsers=Dartium,Chrome,Firefox --single-run --no-colors +# Run E2E tests +$NGDART_BASE_DIR/scripts/run-e2e-test.sh diff --git a/scripts/sauce/sauce_connect_block.sh b/scripts/sauce/sauce_connect_block.sh new file mode 100755 index 000000000..d6fb539c8 --- /dev/null +++ b/scripts/sauce/sauce_connect_block.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Wait for Connect to be ready before exiting +while [ ! -f $BROWSER_PROVIDER_READY_FILE ]; do + echo "..."; + sleep .5; #dart2js takes longer than the travis 10 min timeout to complete +done \ No newline at end of file diff --git a/scripts/sauce/sauce_connect_setup.sh b/scripts/sauce/sauce_connect_setup.sh new file mode 100755 index 000000000..faed4e07c --- /dev/null +++ b/scripts/sauce/sauce_connect_setup.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -e -o pipefail + +# Setup and start Sauce Connect for your TravisCI build +# This script requires your .travis.yml to include the following two private env variables: +# SAUCE_USERNAME +# SAUCE_ACCESS_KEY +# Follow the steps at https://saucelabs.com/opensource/travis to set that up. +# +# Curl and run this script as part of your .travis.yml before_script section: +# before_script: +# - curl https://gist.github.com/santiycr/5139565/raw/sauce_connect_setup.sh | bash + +CONNECT_URL="https://saucelabs.com/downloads/sc-4.3-linux.tar.gz" +CONNECT_DIR="/tmp/sauce-connect-$RANDOM" +CONNECT_DOWNLOAD="sc-latest-linux.tar.gz" + +CONNECT_LOG="$LOGS_DIR/sauce-connect" +CONNECT_STDOUT="$LOGS_DIR/sauce-connect.stdout" +CONNECT_STDERR="$LOGS_DIR/sauce-connect.stderr" + +# Get Connect and start it +mkdir -p $CONNECT_DIR +cd $CONNECT_DIR +curl $CONNECT_URL -o $CONNECT_DOWNLOAD 2> /dev/null 1> /dev/null +mkdir sauce-connect +tar --extract --file=$CONNECT_DOWNLOAD --strip-components=1 --directory=sauce-connect > /dev/null +rm $CONNECT_DOWNLOAD + +SAUCE_ACCESS_KEY=`echo $SAUCE_ACCESS_KEY | rev` + +ARGS="" + +# Set tunnel-id only on Travis, to make local testing easier. +if [ ! -z "$TRAVIS_JOB_NUMBER" ]; then + ARGS="$ARGS --tunnel-identifier $TRAVIS_JOB_NUMBER" +fi +if [ ! -z "$BROWSER_PROVIDER_READY_FILE" ]; then + ARGS="$ARGS --readyfile $BROWSER_PROVIDER_READY_FILE" +fi + + +echo "Starting Sauce Connect in the background, logging into:" +echo " $CONNECT_LOG" +echo " $CONNECT_STDOUT" +echo " $CONNECT_STDERR" +sauce-connect/bin/sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY $ARGS \ + --logfile $CONNECT_LOG 2> $CONNECT_STDERR 1> $CONNECT_STDOUT & diff --git a/scripts/test-expression-extractor.sh b/scripts/test-expression-extractor.sh index 9c2ada1fb..0721a9812 100755 --- a/scripts/test-expression-extractor.sh +++ b/scripts/test-expression-extractor.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -e -o pipefail . $(dirname $0)/env.sh diff --git a/scripts/travis/after-success.sh b/scripts/travis/after-success.sh index 21415f3e3..87a7da859 100755 --- a/scripts/travis/after-success.sh +++ b/scripts/travis/after-success.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -e -o pipefail echo '*******************' echo '** AFTER_SUCCESS **' diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index a366cdd0e..6ed159732 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -e -o pipefail . "$(dirname $0)/../env.sh" echo '===========' @@ -9,6 +9,8 @@ echo '===========' SIZE_TOO_BIG_COUNT=0 +export SAUCE_ACCESS_KEY=`echo $SAUCE_ACCESS_KEY | rev` + function checkSize() { file=$1 if [[ ! -e $file ]]; then @@ -24,6 +26,17 @@ function checkSize() { fi } + +# E2E tests only? +if [[ $JOB == e2e-* ]]; then + echo '---------------------------' + echo '-- E2E TEST: AngularDart --' + echo '---------------------------' + $NGDART_BASE_DIR/scripts/run-e2e-test.sh + exit 0 +fi + + if [[ $TESTS == "dart2js" ]]; then # skip auxiliary tests if we are only running dart2js echo '------------------------' @@ -98,7 +111,19 @@ echo BROWSER=$BROWSERS $NGDART_BASE_DIR/node_modules/jasmine-node/bin/jasmine-node playback_middleware/spec/ && node "node_modules/karma/bin/karma" start karma.conf \ --reporters=junit,dots --port=8765 --runner-port=8766 \ - --browsers=$BROWSERS --single-run --no-colors + --browsers=$BROWSERS --single-run --no-colors 2>&1 | tee karma-output.log + +if grep -q "WARN: iit" karma-output.log; then + echo "ERROR: iit caused some tests to be excluded" + exit 1 +fi + +if grep -q "WARN: ddescribe" karma-output.log; then + echo "ERROR: ddescribe caused some tests to be excluded" + exit 1 +fi + + echo '-------------------------' echo '-- DOCS: Generate Docs --' diff --git a/scripts/travis/install.sh b/scripts/travis/install.sh index 91ed1014b..926697051 100755 --- a/scripts/travis/install.sh +++ b/scripts/travis/install.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -e -o pipefail sh -e /etc/init.d/xvfb start @@ -16,25 +16,5 @@ if [[ $BROWSERS =~ "dartium" ]]; then unzip dartium.zip > /dev/null rm -rf dartium rm dartium.zip - mv dartium-* dartium + mv dartium-* chromium fi - -if [[ $BROWSERS =~ "chrome" ]]; then - echo "Installing Chrome from $CHROME_DEB" - wget $CHROME_DEB - dpkg -I google-chrome*.deb - sudo dpkg -i google-chrome*.deb - sudo chmod u+s /opt -fi - -if [[ $BROWSERS =~ "firefox" ]]; then - echo "Installing Firefox from $FF_TAR" - sudo mkdir -p /usr/local/firefox-$FIREFOX_VERSION - sudo chmod u+s /usr/local/firefox-$FIREFOX_VERSION - wget -O /tmp/firefox.tar.bz2 $FF_TAR - cd /usr/local/firefox-$FIREFOX_VERSION - sudo tar xf /tmp/firefox.tar.bz2 - sudo ln -sf /usr/local/firefox-$FIREFOX_VERSION/firefox/firefox /usr/local/bin/firefox - sudo ln -sf /usr/local/firefox-$FIREFOX_VERSION/firefox/firefox-bin /usr/local/bin/firefox-bin - export FIREFOX_BIN=/usr/local/bin/firefox -fi \ No newline at end of file diff --git a/scripts/travis/presubmit.sh b/scripts/travis/presubmit.sh index adab21725..321f865d0 100755 --- a/scripts/travis/presubmit.sh +++ b/scripts/travis/presubmit.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -e -o pipefail # If we're on the presubmit branch, the dev Dart release, and all unit # tests pass, merge the presubmit branch into master and push it. diff --git a/scripts/travis/print-logs.sh b/scripts/travis/print-logs.sh new file mode 100755 index 000000000..9ebf30022 --- /dev/null +++ b/scripts/travis/print-logs.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +LOG_FILES=$LOGS_DIR/* + +for FILE in $LOG_FILES; do + echo -e "\n\n\n" + echo "================================================================================" + echo " $FILE" + echo "================================================================================" + cat $FILE +done \ No newline at end of file diff --git a/scripts/travis/publish-docs.sh b/scripts/travis/publish-docs.sh index c0943734a..91483a2cb 100755 --- a/scripts/travis/publish-docs.sh +++ b/scripts/travis/publish-docs.sh @@ -16,8 +16,8 @@ if [ "$TRAVIS_REPO_SLUG" = "angular/angular.dart" ]; then rm -Rf docs.angulardart.org git clone https://github.com/angular/docs.angulardart.org.git - echo "Removing old stable docs..." - rm -rf docs.angulardart.org/docs +# echo "Removing old stable docs..." +# rm -rf docs.angulardart.org/docs echo "Copying new docs into stable folder..." rsync -a dartdoc-viewer/client/build/web/* docs.angulardart.org/ diff --git a/scripts/travis/setup.sh b/scripts/travis/setup.sh index ba9b13c29..06e4db907 100755 --- a/scripts/travis/setup.sh +++ b/scripts/travis/setup.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -e -o pipefail echo Fetch Dart channel: $CHANNEL @@ -13,7 +13,20 @@ echo Fetched new dart version $(unzip -p $DART_SDK_ZIP dart-sdk/version) rm -rf dart-sdk unzip $DART_SDK_ZIP > /dev/null +if [ "$USE_G3" = "YES" ]; then + echo ============================================================================= + echo "Rebasing onto g3v1x branch" + git config --global user.email "travis@travis" + git config --global user.name "AngularDart on Travis" + git remote add upstream https://github.com/angular/angular.dart.git + git fetch upstream + git rebase --onto upstream/g3v1x upstream/g3v1x-master +fi + echo ============================================================================= . ./scripts/env.sh $DART --version $PUB install + +# Record dart version for tests. +echo $'import "dart:js";\n\nmain() {\n context["DART_VERSION"] = \''"$($DART --version 2>&1)"$'\';\n}' > test/dart_version.dart diff --git a/test/_specs.dart b/test/_specs.dart index 038593d17..f6c024973 100644 --- a/test/_specs.dart +++ b/test/_specs.dart @@ -14,10 +14,11 @@ export 'package:guinness/guinness_html.dart'; export 'package:mock/mock.dart'; export 'package:di/di.dart'; -export 'package:di/dynamic_injector.dart'; export 'package:angular/angular.dart'; export 'package:angular/application.dart'; export 'package:angular/introspection.dart'; +export 'package:angular/cache/module.dart'; +export 'package:angular/cache/js_cache_register.dart'; export 'package:angular/core/annotation.dart'; export 'package:angular/core/registry.dart'; export 'package:angular/core/module_internal.dart'; diff --git a/test/angular_spec.dart b/test/angular_spec.dart index def7303c1..f3ee12120 100644 --- a/test/angular_spec.dart +++ b/test/angular_spec.dart @@ -94,6 +94,8 @@ main() { var ALLOWED_NAMES = [ "angular.app.AngularModule", "angular.app.Application", + "angular.cache.CacheRegister", + "angular.cache.CacheRegisterStats", "angular.core.annotation.ShadowRootAware", "angular.core.annotation_src.AttachAware", "angular.core.annotation_src.Component", @@ -102,25 +104,29 @@ main() { "angular.core.annotation_src.DetachAware", "angular.core.annotation_src.Directive", "angular.core.annotation_src.DirectiveAnnotation", + "angular.core.annotation_src.DirectiveBinder", + "angular.core.annotation_src.DirectiveBinderFn", "angular.core.annotation_src.Formatter", - "angular.core.annotation_src.Injectable", "angular.core.annotation_src.NgAttr", "angular.core.annotation_src.NgCallback", "angular.core.annotation_src.NgOneWay", "angular.core.annotation_src.NgOneWayOneTime", "angular.core.annotation_src.NgTwoWay", + "angular.core.annotation_src.Visibility", "angular.core.dom_internal.Animate", "angular.core.dom_internal.Animation", "angular.core.dom_internal.AnimationResult", "angular.core.dom_internal.BoundViewFactory", "angular.core.dom_internal.BrowserCookies", "angular.core.dom_internal.Compiler", + "angular.core.dom_internal.CompilerConfig", "angular.core.dom_internal.Cookies", "angular.core.dom_internal.DirectiveMap", "angular.core.dom_internal.ElementProbe", "angular.core.dom_internal.EventHandler", "angular.core.dom_internal.Http", "angular.core.dom_internal.HttpBackend", + "angular.core.dom_internal.HttpConfig", "angular.core.dom_internal.HttpDefaultHeaders", "angular.core.dom_internal.HttpDefaults", "angular.core.dom_internal.HttpInterceptor", @@ -141,7 +147,6 @@ main() { "angular.core.dom_internal.ViewCache", "angular.core.dom_internal.ViewFactory", "angular.core.dom_internal.ViewPort", - "angular.core_internal.CacheStats", "angular.core_internal.ExceptionHandler", "angular.core_internal.Interpolate", "angular.core_internal.RootScope", @@ -183,6 +188,7 @@ main() { "angular.directive.NgInclude", "angular.directive.NgModel", "angular.directive.NgModelOptions", + "angular.directive.NgModelColorValidator", "angular.directive.NgModelConverter", "angular.directive.NgModelEmailValidator", "angular.directive.NgModelMaxLengthValidator", @@ -223,11 +229,13 @@ main() { "angular.formatter_internal.OrderBy", "angular.formatter_internal.Stringify", "angular.formatter_internal.Uppercase", + "angular.introspection.getTestability", "angular.introspection.ngDirectives", "angular.introspection.ngInjector", "angular.introspection.ngProbe", "angular.introspection.ngQuery", "angular.introspection.ngScope", + "angular.node_injector.DirectiveInjector", "angular.routing.NgBindRoute", "angular.routing.ngRoute", "angular.routing.NgRouteCfg", @@ -244,15 +252,21 @@ main() { "angular.watch_group.Watch", "change_detection.AvgStopwatch", "change_detection.FieldGetterFactory", - "di.CircularDependencyError", - "di.FactoryFn", - "di.Injector", - "di.InvalidBindingError", "di.key.Key", - "di.Module", - "di.NoProviderError", - "di.TypeFactory", - "di.Visibility", + "di.key.key", + "di.injector.Injector", + "di.injector.ModuleInjector", + "di.module.Module", + "di.module.Binding", + "di.module.DEFAULT_VALUE", + "di.reflector.TypeReflector", + "di.errors.ResolvingError", + "di.errors.CircularDependencyError", + "di.errors.NoProviderError", + "di.errors.DynamicReflectorError", + "di.errors.NoGeneratedTypeFactoryError", + "di.annotations.Injectable", + "di.annotations.Injectables", "route.client.Routable", "route.client.Route", "route.client.RouteEnterEvent", @@ -260,12 +274,14 @@ main() { "route.client.RouteEvent", "route.client.RouteHandle", "route.client.RouteImpl", - "route.client.RoutePreLeaveEvent", - "route.client.RoutePreLeaveEventHandler", "route.client.RouteLeaveEvent", "route.client.RouteLeaveEventHandler", + "route.client.RoutePreLeaveEvent", + "route.client.RoutePreLeaveEventHandler", "route.client.RoutePreEnterEvent", "route.client.RoutePreEnterEventHandler", + "route.client.RoutePreLeaveEvent", + "route.client.RoutePreLeaveEventHandler", "route.client.Router", "route.client.RouteStartEvent", "url_matcher.UrlMatch", diff --git a/test/animate/animation_loop_spec.dart b/test/animate/animation_loop_spec.dart index df79dcb0a..d2b06603f 100644 --- a/test/animate/animation_loop_spec.dart +++ b/test/animate/animation_loop_spec.dart @@ -8,11 +8,11 @@ main() { TestBed _; AnimationLoop runner; MockAnimationFrame frame; - beforeEach(async(inject((TestBed tb, VmTurnZone zone) { + beforeEach(async((TestBed tb, VmTurnZone zone) { _ = tb; frame = new MockAnimationFrame(); runner = new AnimationLoop(frame, new Profiler(), zone); - }))); + })); it('should play animations with window animation frames', async(() { var animation = new MockAnimation(); diff --git a/test/animate/animation_optimizer_spec.dart b/test/animate/animation_optimizer_spec.dart index d1be072c0..29ac85f1c 100644 --- a/test/animate/animation_optimizer_spec.dart +++ b/test/animate/animation_optimizer_spec.dart @@ -1,26 +1,27 @@ library animation_optimizer_spec; import '../_specs.dart'; +import 'dart:js' as js; -main() { - describe('AnimationLoop', () { +_run({bool animationsAllowed}) { + describe('animationsAllowed=$animationsAllowed', () { TestBed _; AnimationOptimizer optimizer; - beforeEach(inject((TestBed tb, Expando expand) { + beforeEach((TestBed tb, Expando expand) { _ = tb; - optimizer = new AnimationOptimizer(expand); - })); + optimizer = new AnimationOptimizer(expand)..animationsAllowed = animationsAllowed; + }); it('should prevent animations on child elements', () { var animation = new NoOpAnimation(); _.compile('
    '); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); optimizer.track(animation, _.rootElement); expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); optimizer.forget(animation); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); }); it('should allow multiple animations on the same element', () { @@ -28,16 +29,16 @@ main() { var animation2 = new NoOpAnimation(); _.compile('
    '); - expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement)).toBe(animationsAllowed); optimizer.track(animation1, _.rootElement); - expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement)).toBe(animationsAllowed); optimizer.track(animation2, _.rootElement); - expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement)).toBe(animationsAllowed); expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); optimizer.forget(animation1); expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); optimizer.forget(animation2); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); }); it('should always animate an element', () { @@ -45,22 +46,22 @@ main() { optimizer.alwaysAnimate(_.rootElement.children[0], "never"); expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); optimizer.alwaysAnimate(_.rootElement.children[0], "always"); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); optimizer.alwaysAnimate(_.rootElement.children[0], "auto"); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); }); it('alwaysAnimate should not affect children', () { _.compile('
    '); optimizer.alwaysAnimate(_.rootElement, "never"); expect(optimizer.shouldAnimate(_.rootElement)).toBeFalsy(); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); optimizer.alwaysAnimate(_.rootElement, "always"); - expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement)).toBe(animationsAllowed); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); optimizer.alwaysAnimate(_.rootElement, "auto"); - expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement)).toBe(animationsAllowed); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); }); @@ -68,16 +69,16 @@ main() { _.compile('
    '); optimizer.alwaysAnimateChildren(_.rootElement, "never"); - expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement)).toBe(animationsAllowed); expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); optimizer.alwaysAnimateChildren(_.rootElement, "always"); - expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement)).toBe(animationsAllowed); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); optimizer.alwaysAnimateChildren(_.rootElement, "auto"); - expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement)).toBe(animationsAllowed); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); }); it('alwaysAnimate should take priority over alwaysAnimateChildren', () { @@ -85,7 +86,7 @@ main() { optimizer.alwaysAnimateChildren(_.rootElement, "never"); optimizer.alwaysAnimate(_.rootElement.children[0], "always"); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); optimizer.alwaysAnimateChildren(_.rootElement, "always"); optimizer.alwaysAnimate(_.rootElement.children[0], "never"); @@ -100,13 +101,13 @@ main() { expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); optimizer.alwaysAnimate(_.rootElement.children[0], "always"); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); optimizer.alwaysAnimate(_.rootElement.children[0], "auto"); expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); optimizer.forget(animation); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); }); it('alwaysAnimateChildren should take priority over running animations', @@ -118,13 +119,13 @@ main() { expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); optimizer.alwaysAnimateChildren(_.rootElement, "always"); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); optimizer.alwaysAnimateChildren(_.rootElement, "auto"); expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); optimizer.forget(animation); - expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBe(animationsAllowed); optimizer.alwaysAnimateChildren(_.rootElement, "never"); expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); @@ -138,7 +139,7 @@ main() { optimizer.alwaysAnimateChildren(_.rootElement, "always"); expect(optimizer.shouldAnimate(_.rootElement.children[0].children[0])) - .toBeTruthy(); + .toBe(animationsAllowed); optimizer.alwaysAnimateChildren(_.rootElement.children[0], "never"); expect(optimizer.shouldAnimate(_.rootElement.children[0].children[0])) @@ -148,7 +149,20 @@ main() { optimizer.alwaysAnimateChildren(_.rootElement, "never"); optimizer.alwaysAnimateChildren(_.rootElement.children[0], "always"); expect(optimizer.shouldAnimate(_.rootElement.children[0].children[0])) - .toBeTruthy(); + .toBe(animationsAllowed); }); }); } + +main() { + describe('AnimationLoop', () { + _run(animationsAllowed: true); + if (!identical(1, 1.0) && js.context['DART_VERSION'].toString().contains("version: 1.5.")) { + // Remove this block when issue #1219 is fixed. + // In Dart 1.5's Dartium, running both describes in any order causes + // ng_model_spec to fails. This is not the case in Dart 1.4 or Dart 1.6. + return; + } + _run(animationsAllowed: false); + }); +} diff --git a/test/animate/css_animate_spec.dart b/test/animate/css_animate_spec.dart index 74dc7a99a..3b7d6535b 100644 --- a/test/animate/css_animate_spec.dart +++ b/test/animate/css_animate_spec.dart @@ -1,21 +1,23 @@ library css_animate_spec; import 'dart:async'; +import 'dart:js' as js; import '../_specs.dart'; -main() { - describe('CssAnimate', () { +_run({bool animationsAllowed}) { + describe('animationsAllowed=$animationsAllowed', () { TestBed _; Animate animate; MockAnimationLoop runner; - beforeEach(inject((TestBed tb, Expando expand) { + beforeEach((TestBed tb, Expando expand) { _ = tb; - runner = new MockAnimationLoop(); + runner = new MockAnimationLoop(animationsAllowed); animate = new CssAnimate(runner, new CssAnimationMap(), new AnimationOptimizer(expand)); - })); + animate.animationsAllowed = animationsAllowed; + }); it('should add a css class to an element node', async(() { _.compile('
    '); @@ -91,7 +93,9 @@ main() { _.compile('
    '); animate.addClass(_.rootElement, 'test'); runner.start(); - expect(_.rootElement).toHaveClass('test-add'); + if (animationsAllowed) { + expect(_.rootElement).toHaveClass('test-add'); + } var spans = es('AB'); animate.insert(spans, _.rootElement); runner.start(); @@ -101,8 +105,11 @@ main() { } class MockAnimationLoop extends Mock implements AnimationLoop { + bool animationsAllowed; num time = 0.0; + MockAnimationLoop(this.animationsAllowed); + Future get onCompleted { var cmp = new Completer(); cmp.complete(AnimationResult.COMPLETED); @@ -129,3 +136,16 @@ class MockAnimationLoop extends Mock implements AnimationLoop { noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } + +main() { + describe('CssAnimate', () { + _run(animationsAllowed: true); + if (!identical(1, 1.0) && js.context['DART_VERSION'].toString().contains("version: 1.5.")) { + // Remove this block when issue #1219 is fixed. + // In Dart 1.5's Dartium, running both describes in any order causes + // ng_model_spec to fails. This is not the case in Dart 1.4 or Dart 1.6. + return; + } + _run(animationsAllowed: false); + }); +} diff --git a/test/animate/css_animation_spec.dart b/test/animate/css_animation_spec.dart index 23c73f0e4..3f2989f2c 100644 --- a/test/animate/css_animation_spec.dart +++ b/test/animate/css_animation_spec.dart @@ -6,7 +6,7 @@ main() { describe('CssAnimation', () { TestBed _; - beforeEach(inject((TestBed tb) => _ = tb)); + beforeEach((TestBed tb) => _ = tb); afterEach(() => _.rootElements.forEach((e) => e.remove())); it('should correctly respond to an animation lifecycle', async(() { diff --git a/test/cache/cache_register_spec.dart b/test/cache/cache_register_spec.dart new file mode 100644 index 000000000..a797385ea --- /dev/null +++ b/test/cache/cache_register_spec.dart @@ -0,0 +1,49 @@ +library cache_register_spec; + +import '../_specs.dart'; + +main() => describe('CacheRegister', () { + it('should clear caches', (CacheRegister register) { + var map = {'a': 2}; + var map2 = {'b': 3}; + expect(map.length).toEqual(1); + expect(map2.length).toEqual(1); + + register.registerCache('a', map); + register.registerCache('b', map2); + register.clear('a'); + expect(map.length).toEqual(0); + expect(map2.length).toEqual(1); + + map['a'] = 2; + register.clear(); + expect(map.length).toEqual(0); + expect(map2.length).toEqual(0); + + + }); + + it('should return stats when empty', (CacheRegister register) { + expect(register.stats).toEqual([]); + }); + + it('should return correct stats', (CacheRegister register) { + var map = {'a': 2}; + var map2 = {'b': 3, 'c': 4}; + register.registerCache('a', map); + register.registerCache('b', map2); + + expect(register.stats.length).toEqual(2); + if (register.stats[0].name == 'a') { + expect(register.stats[0].length).toEqual(1); + expect(register.stats[1].name).toEqual('b'); + expect(register.stats[1].length).toEqual(2); + } else { + expect(register.stats[0].name).toEqual('b'); + expect(register.stats[0].length).toEqual(2); + expect(register.stats[1].name).toEqual('a'); + expect(register.stats[1].length).toEqual(1); + } + + }); +}); diff --git a/test/core/cache_spec.dart b/test/cache/cache_spec.dart similarity index 91% rename from test/core/cache_spec.dart rename to test/cache/cache_spec.dart index 79a4afdaf..3e4ac550c 100644 --- a/test/core/cache_spec.dart +++ b/test/cache/cache_spec.dart @@ -67,17 +67,17 @@ void main() { describe('put, get & remove', () { - it('should add cache entries via add and retrieve them via get', inject(() { + it('should add cache entries via add and retrieve them via get', () { var obj = {'bar':'baz'}; cache.put('key1', 'bar'); cache.put('key2', obj); expect(cache.get('key2')).toBe(obj); expect(cache.get('key1')).toBe('bar'); - })); + }); - it('should remove entries via remove', inject(() { + it('should remove entries via remove', () { cache.put('k1', 'foo'); cache.put('k2', 'bar'); @@ -90,22 +90,22 @@ void main() { expect(cache.get('k1')).toBeNull(); expect(cache.get('k2')).toBeNull(); - })); + }); - it('should return null when entry does not exist', inject(() { + it('should return null when entry does not exist', () { expect(cache.remove('non-existent')).toBeNull(); - })); + }); - it("should return value from put", inject(() { + it("should return value from put", () { var obj = {}; expect(cache.put('k1', obj)).toBe(obj); - })); + }); }); describe('removeAll', () { - it('should blow away all data', inject(() { + it('should blow away all data', () { cache.put('id1', 1); cache.put('id2', 2); cache.put('id3', 3); @@ -115,13 +115,13 @@ void main() { expect(cache.get('id1')).toBeNull(); expect(cache.get('id2')).toBeNull(); expect(cache.get('id3')).toBeNull(); - })); + }); }); }); // TODO(chirayu): Add a lot more tests and tests and don't rely on toString() describe('LRU cache', () { - it('should have LRU behavior with ordering keys and eviction', inject(() { + it('should have LRU behavior with ordering keys and eviction', () { var cache = new LruCache(capacity: 4); cache.put(1, 10); cache.put(2, 20); @@ -146,7 +146,7 @@ void main() { expect(stats.size).toEqual(0); expect(stats.hits).toEqual(2); expect(stats.misses).toEqual(1); - })); + }); it('should hold nothing if capacity is zero', () { var cache = new LruCache(capacity: 0); diff --git a/test/cache/js_cache_register_spec.dart b/test/cache/js_cache_register_spec.dart new file mode 100644 index 000000000..667456eec --- /dev/null +++ b/test/cache/js_cache_register_spec.dart @@ -0,0 +1,43 @@ +library js_cache_register_spec; + +import '../_specs.dart'; +import 'dart:js' as js; +import 'package:angular/application_factory.dart'; + +main() => describe('JsCacheRegister', () { + s() => js.context['ngCaches']['sizes'].apply([]); + + // Create some caches in the system + beforeEach((JsCacheRegister js, DynamicParser dp, ViewCache vc) { }); + + it('should publish a JS interface', () { + expect(js.context['ngCaches']).toBeDefined(); + }); + + it('should return a map of caches', () { + expect(js.context['Object']['keys'].apply([s()]).length > 0).toBeTruthy(); + }); + + it('should clear one cache', (DynamicParser p) { + p('1'); + + expect(s()['DynamicParser'] > 0).toBeTruthy(); + + js.context['ngCaches']['clear'].apply(['DynamicParser']); + expect(s()['DynamicParser']).toEqual(0); + }); + + it('should clear all caches', (DynamicParser p) { + p('1'); + + var stats = s(); + var caches = js.context['Object']['keys'].apply([stats]); + expect(caches.length > 0).toBeTruthy(); + js.context['ngCaches']['clear'].apply([]); + + var clearedStats = s(); + caches.forEach((cacheName) { + expect(clearedStats[cacheName]).toEqual(0); + }); + }); +}); diff --git a/test/change_detection/dirty_checking_change_detector_spec.dart b/test/change_detection/dirty_checking_change_detector_spec.dart index 06aba9673..656eea566 100644 --- a/test/change_detection/dirty_checking_change_detector_spec.dart +++ b/test/change_detection/dirty_checking_change_detector_spec.dart @@ -301,7 +301,7 @@ void testWithGetterFactory(FieldGetterFactory getterFactory) { collection: ['b[1 -> 0]', 'a[0 -> 1]', 'c'], previous: ['a[0 -> 1]', 'b[1 -> 0]', 'c'], additions: [], - moves: ['b[1 -> 0]', 'a[0 -> 1]', 'c'], + moves: ['b[1 -> 0]', 'a[0 -> 1]'], removals: [])); list..clear()..addAll(['b', 'c', 'a']); @@ -390,6 +390,21 @@ void testWithGetterFactory(FieldGetterFactory getterFactory) { expect(detector.collectChanges().moveNext()).toEqual(false); }); + it('should detect [NaN] moves', () { + var list = [double.NAN, double.NAN]; + detector..watch(list, null, null)..collectChanges(); + + list..clear()..addAll(['foo', double.NAN, double.NAN]); + var iterator = detector.collectChanges(); + expect(iterator.moveNext()).toEqual(true); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['foo[null -> 0]', 'NaN[0 -> 1]', 'NaN[1 -> 2]'], + previous: ['NaN[0 -> 1]', 'NaN[1 -> 2]'], + additions: ['foo[null -> 0]'], + moves: ['NaN[0 -> 1]', 'NaN[1 -> 2]'], + removals: [])); + }); + it('should remove and add same item', () { var list = ['a', 'b', 'c']; var record = detector.watch(list, null, 'handler'); @@ -532,6 +547,21 @@ void testWithGetterFactory(FieldGetterFactory getterFactory) { moves: ['(1)a-a[0 -> 1]'], removals: [])); }); + + it('should not report unnecessary moves', () { + var list = ['a', 'b', 'c']; + var record = detector.watch(list, null, null); + var iterator = detector.collectChanges()..moveNext(); + + list..clear()..addAll(['b', 'a', 'c']); + iterator = detector.collectChanges()..moveNext(); + expect(iterator.current.currentValue, toEqualCollectionRecord( + collection: ['b[1 -> 0]', 'a[0 -> 1]', 'c'], + previous: ['a[0 -> 1]', 'b[1 -> 0]', 'c'], + additions: [], + moves: ['b[1 -> 0]', 'a[0 -> 1]'], + removals: [])); + }); }); describe('map watching', () { diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index ba5dcdfbd..07df50859 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -23,7 +23,7 @@ void main() { Parser parser; ASTParser astParser; - beforeEach(inject((Logger _logger, Parser _parser, ASTParser _astParser) { + beforeEach((Logger _logger, Parser _parser, ASTParser _astParser) { context = {}; var getterFactory = new DynamicFieldGetterFactory(); changeDetector = new DirtyCheckingChangeDetector(getterFactory); @@ -31,7 +31,7 @@ void main() { logger = _logger; parser = _parser; astParser = _astParser; - })); + }); AST parse(String expression) => astParser(expression); @@ -64,13 +64,13 @@ void main() { expect(logger).toEqual(list); } - beforeEach(inject((Logger _logger) { + beforeEach((Logger _logger) { context = {}; var getterFactory = new DynamicFieldGetterFactory(); changeDetector = new DirtyCheckingChangeDetector(getterFactory); watchGrp = new RootWatchGroup(getterFactory, changeDetector, context); logger = _logger; - })); + }); it('should have a toString for debugging', () { watchGrp.watch(parse('a'), (v, p) {}); @@ -528,6 +528,27 @@ void main() { expect(logger).toEqual([]); }); + it('should ignore NaN != NaN', () { + watchGrp.watch(new ClosureAST('NaN', () => double.NAN, []), (_, __) => logger('NaN')); + + watchGrp.detectChanges(); + expect(logger).toEqual(['NaN']); + + logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual([]); + }) ; + + it('should test string by value', () { + watchGrp.watch(new ClosureAST('String', () => 'value', []), (v, _) => logger(v)); + + watchGrp.detectChanges(); + expect(logger).toEqual(['value']); + + logger.clear(); + watchGrp.detectChanges(); + expect(logger).toEqual([]); + }); it('should eval method', () { var obj = new MyClass(logger); diff --git a/test/config/init_guinness.dart b/test/config/init_guinness.dart index 2c8a9e6ab..6e4b868e1 100644 --- a/test/config/init_guinness.dart +++ b/test/config/init_guinness.dart @@ -4,5 +4,22 @@ import 'package:unittest/unittest.dart' as unit; main() { unit.filterStacks = true; unit.formatStacks = false; + + _printWarnings(); + + guinness.autoInit = false; guinness.initSpecs(); } + +_printWarnings () { + final info = guinness.suiteInfo(); + + if (info.activeIts.any((it) => it.exclusive)) { + print("WARN: iit caused some tests to be excluded"); + } + + if (info.exclusiveDescribes.isNotEmpty) { + print("WARN: ddescribe caused some tests to be excluded"); + } +} + diff --git a/test/core/annotation_src_spec.dart b/test/core/annotation_src_spec.dart index d8664fb5d..dec1b2c9b 100644 --- a/test/core/annotation_src_spec.dart +++ b/test/core/annotation_src_spec.dart @@ -30,9 +30,7 @@ List nullFields(x) { return ret; } -// TODO(deboer): These tests are disabled due to dartbug.com/19177 -// That bug should be fixed soon -void main() => xdescribe('annotations', () { +void main() => describe('annotations', () { describe('component', () { it('should set all fields on clone when all the fields are set', () { var component = new Component( @@ -42,7 +40,7 @@ void main() => xdescribe('annotations', () { applyAuthorStyles: true, resetStyleInheritance: true, publishAs: '', - module: (){}, + module: (i){}, map: {}, selector: '', visibility: Directive.LOCAL_VISIBILITY, @@ -65,7 +63,7 @@ void main() => xdescribe('annotations', () { children: 'xxx', map: {}, selector: '', - module: (){}, + module: (i){}, visibility: Directive.LOCAL_VISIBILITY, exportExpressions: [], exportExpressionAttrs: [] @@ -86,7 +84,7 @@ void main() => xdescribe('annotations', () { children: 'xxx', map: {}, selector: '', - module: (){}, + module: (i){}, visibility: Directive.LOCAL_VISIBILITY, exportExpressions: [], exportExpressionAttrs: [] diff --git a/test/core/core_directive_spec.dart b/test/core/core_directive_spec.dart index ab4cd3149..12632dc14 100644 --- a/test/core/core_directive_spec.dart +++ b/test/core/core_directive_spec.dart @@ -11,11 +11,11 @@ void main() { }); it('should extract attr map from annotated component', (DirectiveMap directives) { - var annotations = directives.annotationsFor(AnnotatedIoComponent); - expect(annotations.length).toEqual(1); - expect(annotations[0] is Component).toBeTruthy(); + var tuples = directives['annotated-io']; + expect(tuples.length).toEqual(1); + expect(tuples[0].directive is Component).toBeTruthy(); - Component annotation = annotations[0]; + Component annotation = tuples[0].directive; expect(annotation.selector).toEqual('annotated-io'); expect(annotation.visibility).toEqual(Directive.LOCAL_VISIBILITY); expect(annotation.exportExpressions).toEqual(['exportExpressions']); @@ -77,11 +77,11 @@ void main() { }); it("should extract attr map from annotated component which inherits other component", (DirectiveMap directives) { - var annotations = directives.annotationsFor(Sub); - expect(annotations.length).toEqual(1); - expect(annotations[0] is Directive).toBeTruthy(); + var tupls = directives['[sub]']; + expect(tupls.length).toEqual(1); + expect(tupls[0].directive is Directive).toBeTruthy(); - Directive annotation = annotations[0]; + Directive annotation = tupls[0].directive; expect(annotation.selector).toEqual('[sub]'); expect(annotation.map).toEqual({ "foo": "=>foo", @@ -112,7 +112,7 @@ class NullParser implements Parser { 'foo': '=>foo' }) class AnnotatedIoComponent { - static module() => new Module()..bind(String, toFactory: (i) => i.get(AnnotatedIoComponent), + static module(i) => i.bind(String, toFactory: (i) => i.get(AnnotatedIoComponent), visibility: Directive.LOCAL_VISIBILITY); AnnotatedIoComponent(Scope scope) { diff --git a/test/core/parser/generated_getter_setter_spec.dart b/test/core/parser/generated_getter_setter_spec.dart index 3bcd6e5e1..74a4d93e4 100644 --- a/test/core/parser/generated_getter_setter_spec.dart +++ b/test/core/parser/generated_getter_setter_spec.dart @@ -7,7 +7,7 @@ import 'generated_getter_setter.dart' as gen; main() { describe('hybrid getter-setter', () { beforeEachModule((Module module) { - module..bind(Parser, toImplementation: DynamicParser) + module..bind(Parser, toInstanceOf: DynamicParser) ..bind(ClosureMap, toValue: gen.closureMap); }); parser_spec.main(); diff --git a/test/core/parser/parser_spec.dart b/test/core/parser/parser_spec.dart index 60ecd68dc..6ef4d5036 100644 --- a/test/core/parser/parser_spec.dart +++ b/test/core/parser/parser_spec.dart @@ -10,6 +10,7 @@ class TestData { set str(x) => _str = x; method() => "testMethod"; + causeException() => this.x(); sub1(a, {b: 0}) => a - b; sub2({a: 0, b: 0}) => a - b; } @@ -56,6 +57,17 @@ main() { module.bind(SubstringFormatter); }); + describe('DynamicParser', () { + // This is important because the DynamicParser expects to be a singleton + // to share its cache. It therefore registers with the CacheRegister and + // having more than one instance will result in a duplicate registration. + it('should be identical to Parser in dynamic mode', (Parser p, DynamicParser dp) { + if (p is DynamicParser) { + expect(identical(p, dp)).toBeTruthy();; + } + }); + }); + beforeEach((Parser injectedParser, FormatterMap injectedFormatters) { parser = injectedParser; formatters = injectedFormatters; @@ -435,6 +447,12 @@ main() { expect(context['obj'].field['key']).toEqual(4); }); + it('should rethrow an error from a function', () { + expect(() { + parser("causeException()").eval(new TestData()); + }).toThrow('NoSuchMethodError'); + }); + xit('should throw a nice error for type mismatch', () { context['obj'] = new SetterObject(); @@ -445,7 +463,7 @@ main() { }); xdescribe('reserved words', () { - iit('should support reserved words in member get access', () { + it('should support reserved words in member get access', () { for (String reserved in RESERVED_WORDS) { expect(parser("o.$reserved").eval({ 'o': new Object() })).toEqual(null); expect(parser("o.$reserved").eval({ 'o': { reserved: reserved }})).toEqual(reserved); @@ -1128,10 +1146,10 @@ main() { it('should parse formatters', () { expect(() { eval("1|nonexistent"); - }).toThrow('No Formatter: nonexistent found!'); + }).toThrow('No formatter \'nonexistent\' found!'); expect(() { eval("1|nonexistent", formatters); - }).toThrow('No Formatter: nonexistent found!'); + }).toThrow('No formatter \'nonexistent\' found!'); context['offset'] = 3; expect(eval("'abcd'|substring:1:offset")).toEqual("bc"); @@ -1142,12 +1160,12 @@ main() { var expression = parser("'World'|hello"); expect(() { expression.eval({}, formatters); - }).toThrow('No Formatter: hello found!'); + }).toThrow('No formatter \'hello\' found!'); var module = new Module() + ..bind(FormatterMap) ..bind(HelloFormatter); - var childInjector = injector.createChild([module], - forceNewInstances: [FormatterMap]); + var childInjector = injector.createChild([module]); var newFormatters = childInjector.get(FormatterMap); expect(expression.eval({}, newFormatters)).toEqual('Hello, World!'); diff --git a/test/core/registry_spec.dart b/test/core/registry_spec.dart deleted file mode 100644 index 1a00d2af0..000000000 --- a/test/core/registry_spec.dart +++ /dev/null @@ -1,64 +0,0 @@ -library registry_spec; - -import '../_specs.dart'; -import 'package:angular/application_factory.dart'; - -main() { - describe('RegistryMap', () { - it('should allow for multiple registry keys to be added', () { - var module = new Module() - ..bind(MyMap) - ..bind(A1) - ..bind(A2); - - var injector = applicationFactory().addModule(module).createInjector(); - expect(() { - injector.get(MyMap); - }).not.toThrow(); - }); - - it('should iterate over all types', () { - var module = new Module() - ..bind(MyMap) - ..bind(A1); - - var injector = applicationFactory().addModule(module).createInjector(); - var keys = []; - var types = []; - var map = injector.get(MyMap); - map.forEach((k, t) { keys.add(k); types.add(t); }); - expect(keys).toEqual([new MyAnnotation('A'), new MyAnnotation('B')]); - expect(types).toEqual([A1, A1]); - }); - - it('should safely ignore typedefs', () { - var module = new Module() - ..bind(MyMap) - ..bind(MyTypedef, toValue: (String _) => null); - - var injector = applicationFactory().addModule(module).createInjector(); - expect(() => injector.get(MyMap), isNot(throws)); - }); - }); -} - -typedef void MyTypedef(String arg); - -class MyMap extends AnnotationMap { - MyMap(Injector injector, MetadataExtractor metadataExtractor) - : super(injector, metadataExtractor); -} - - -class MyAnnotation { - final String name; - - const MyAnnotation(String this.name); - - toString() => name; - get hashCode => name.hashCode; - operator==(other) => this.name == other.name; -} - -@MyAnnotation('A') @MyAnnotation('B') class A1 {} -@MyAnnotation('A') class A2 {} diff --git a/test/core/scope_spec.dart b/test/core/scope_spec.dart index aa1f62d21..f94aa6891 100644 --- a/test/core/scope_spec.dart +++ b/test/core/scope_spec.dart @@ -654,7 +654,7 @@ void main() { it('should skip scopes which dont have given event', - inject((RootScope rootScope, Logger log) { + (RootScope rootScope, Logger log) { var child1 = rootScope.createChild('A'); rootScope.createChild('A1'); rootScope.createChild('A2'); @@ -663,7 +663,7 @@ void main() { child2.on('event').listen((e) => log(e.data)); rootScope.broadcast('event', 'OK'); expect(log).toEqual(['OK']); - })); + }); }); @@ -847,14 +847,14 @@ void main() { }); - it(r'should execute and return value and update', inject( - (RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler exceptionHandler = e; - rootScope.context['name'] = 'abc'; - expect(rootScope.apply((context) => context['name'])).toEqual('abc'); - expect(log).toEqual('digest;digest;'); - exceptionHandler.assertEmpty(); - })); + it(r'should execute and return value and update', + (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e; + rootScope.context['name'] = 'abc'; + expect(rootScope.apply((context) => context['name'])).toEqual('abc'); + expect(log).toEqual('digest;digest;'); + exceptionHandler.assertEmpty(); + }); it(r'should execute and return value and update', (RootScope rootScope) { @@ -924,14 +924,14 @@ void main() { exceptionHandler.assertEmpty(); }); - it(r'should execute and return value and update', inject( - (RootScope rootScope, ExceptionHandler e) { - LoggingExceptionHandler exceptionHandler = e; - rootScope.context['name'] = 'abc'; - expect(rootScope.apply((context) => context['name'])).toEqual('abc'); - expect(log).toEqual('digest;digest;'); - exceptionHandler.assertEmpty(); - })); + it(r'should execute and return value and update', + (RootScope rootScope, ExceptionHandler e) { + LoggingExceptionHandler exceptionHandler = e; + rootScope.context['name'] = 'abc'; + expect(rootScope.apply((context) => context['name'])).toEqual('abc'); + expect(log).toEqual('digest;digest;'); + exceptionHandler.assertEmpty(); + }); it(r'should execute and return value and update', (RootScope rootScope) { rootScope.context['name'] = 'abc'; @@ -1092,23 +1092,22 @@ void main() { }); - it(r'should run digest multiple times', inject( - (RootScope rootScope) { - // tests a traversal edge case which we originally missed - var log = []; - var childA = rootScope.createChild({'log': log}); - var childB = rootScope.createChild({'log': log}); + it(r'should run digest multiple times', (RootScope rootScope) { + // tests a traversal edge case which we originally missed + var log = []; + var childA = rootScope.createChild({'log': log}); + var childB = rootScope.createChild({'log': log}); - rootScope.context['log'] = log; + rootScope.context['log'] = log; - rootScope.watch("log.add('r')", (_, __) => null); - childA.watch("log.add('a')", (_, __) => null); - childB.watch("log.add('b')", (_, __) => null); + rootScope.watch("log.add('r')", (_, __) => null); + childA.watch("log.add('a')", (_, __) => null); + childB.watch("log.add('b')", (_, __) => null); - // init - rootScope.digest(); - expect(log.join('')).toEqual('rabrab'); - })); + // init + rootScope.digest(); + expect(log.join('')).toEqual('rabrab'); + }); it(r'should repeat watch cycle while model changes are identified', (RootScope rootScope) { @@ -1165,7 +1164,7 @@ void main() { }); - it(r'should return a function that allows listeners to be unregistered', inject( + it(r'should return a function that allows listeners to be unregistered', (RootScope rootScope) { var listener = guinness.createSpy('watch listener'); var watch; @@ -1185,7 +1184,7 @@ void main() { watch.remove(); rootScope.digest(); //trigger expect(listener).not.toHaveBeenCalled(); - })); + }); it(r'should be possible to remove every watch', @@ -1227,8 +1226,22 @@ void main() { }); + it(r'should detect infinite digest through runAsync', (RootScope rootScope) { + rootScope.context['value'] = () { rootScope.runAsync(() {}); return 'a'; }; + rootScope.watch('value()', (_, __) {}); + + expect(() { + rootScope.digest(); + }).toThrow('Model did not stabilize in 5 digests. ' + 'Last 3 iterations:\n' + 'async:1\n' + 'async:1\n' + 'async:1'); + }); + + it(r'should always call the watchr with newVal and oldVal equal on the first run', - inject((RootScope rootScope) { + (RootScope rootScope) { var log = []; var logger = (newVal, oldVal) { var val = (newVal == oldVal || (newVal != oldVal && oldVal != newVal)) ? newVal : 'xxx'; @@ -1253,7 +1266,7 @@ void main() { log = []; rootScope.digest(); expect(log).toEqual([]); - })); + }); it('should properly watch constants', (RootScope rootScope, Logger log) { @@ -1440,6 +1453,105 @@ void main() { }); }); + describe('microtask processing', () { + beforeEach((VmTurnZone zone, RootScope scope, Logger log) { + var onTurnDone = zone.onTurnDone; + zone.onTurnDone = () { + log('['); + onTurnDone(); + log(']'); + }; + var onScheduleMicrotask = zone.onScheduleMicrotask; + zone.onScheduleMicrotask = (fn) { + log('('); + try { + onScheduleMicrotask(fn); + } catch (e) { + log('CATCH: $e'); + } + log(')'); + }; + }); + + it('should schedule apply after future resolution', + async((Logger log, VmTurnZone zone, RootScope scope) { + Completer completer; + zone.run(() { + completer = new Completer(); + completer.future.then((value) { + log('then($value)'); + }); + }); + + scope.runAsync(() => log('before')); + log.clear(); + completer.complete('OK'); // this one causes APPLY which processe 'before' + // This one schedules work but apply already run so it does not execute. + scope.runAsync(() => log('NOT_EXECUTED')); + + expect(log).toEqual(['(', ')', '[', 'before', 'then(OK)', ']']); + }) + ); + + it('should schedule microtask to runAsync queue during digest', + async((Logger log, VmTurnZone zone, RootScope scope) { + Completer completer; + zone.run(() { + completer = new Completer(); + completer.future. + then((value) { + scope.runAsync(() => log('in(${scope.state})')); + return new Future.value(value); + }). + then((value) { + log('then($value)'); + }); + }); + log.clear(); + completer.complete('OK'); + expect(log).toEqual(['(', ')', '[', '(', ')', 'in(digest)', 'then(OK)', ']']); + }) + ); + + it('should allow microtasks in flush phase and process them immediatly', + async((Logger log, VmTurnZone zone, RootScope scope) { + scope.watch('g()', (_, __) {}); + scope.context['g'] = () { + log('!'); + return 0; + }; + + zone.run(() { + scope.domWrite(() { + log('domWriteA'); + return new Future.value(null).then((_) => scope.domWrite(() => log('domWriteB'))); + }); + }); + expect(log).toEqual( + ['[', '!', '!', 'domWriteA', '(', ')', 'domWriteB', /* assert */'!', ']']); + }) + ); + + it('should allow creation of Completers in flush phase', + async((Logger log, VmTurnZone zone, RootScope scope) { + Completer completer; + zone.run(() { + scope.domWrite(() { + log('new Completer'); + completer = new Completer(); + completer.future.then((value) { + log('then($value)'); + }); + }); + }); + log('='); + completer.complete('OK'); + log(';'); + expect(log).toEqual( + ['[', 'new Completer', ']', '=', '(', ')', '[', 'then(OK)', ']', ';']); + }) + ); + }); describe('domRead/domWrite', () { beforeEachModule((Module module) { diff --git a/test/core/templateurl_spec.dart b/test/core/templateurl_spec.dart index 693127947..6f28f62d3 100644 --- a/test/core/templateurl_spec.dart +++ b/test/core/templateurl_spec.dart @@ -53,7 +53,7 @@ void main() { ..bind(UrlRewriter, toImplementation: PrefixedUrlRewriter); }); - it('should use the UrlRewriter for both HTML and CSS URLs', async(inject( + it('should use the UrlRewriter for both HTML and CSS URLs', async( (Http http, Compiler compile, Scope rootScope, Logger log, Injector injector, VmTurnZone zone, MockHttpBackend backend, DirectiveMap directives) { @@ -64,7 +64,7 @@ void main() { var element = e('
    ignore
    '); zone.run(() { - compile([element], directives)(injector, [element]); + compile([element], directives)(rootScope, injector.get(DirectiveInjector), [element]); }); backend.flush(); @@ -74,7 +74,7 @@ void main() { expect(element.children[0].shadowRoot).toHaveHtml( '
    Simple!
    ' ); - }))); + })); }); @@ -88,13 +88,13 @@ void main() { ..bind(InlineWithCssComponent); }); - it('should replace element with template from url', async(inject( + it('should replace element with template from url', async( (Http http, Compiler compile, Scope rootScope, Logger log, Injector injector, MockHttpBackend backend, DirectiveMap directives) { backend.expectGET('simple.html').respond(200, '
    Simple!
    '); var element = es('
    ignore
    '); - compile(element, directives)(injector, element); + compile(element, directives)(rootScope, injector.get(DirectiveInjector), element); microLeap(); backend.flush(); @@ -104,9 +104,9 @@ void main() { rootScope.apply(); // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; SIMPLE'); - }))); + })); - it('should load template from URL once', async(inject( + it('should load template from URL once', async( (Http http, Compiler compile, Scope rootScope, Logger log, Injector injector, MockHttpBackend backend, DirectiveMap directives) { backend.whenGET('simple.html').respond(200, '
    Simple!
    '); @@ -116,7 +116,7 @@ void main() { 'ignore' 'ignore' '
    '); - compile(element, directives)(injector, element); + compile(element, directives)(rootScope, injector.get(DirectiveInjector), element); microLeap(); backend.flush(); @@ -127,9 +127,9 @@ void main() { // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; LOG; SIMPLE; SIMPLE'); - }))); + })); - it('should load a CSS file into a style', async(inject( + it('should load a CSS file into a style', async( (Http http, Compiler compile, Scope rootScope, Logger log, Injector injector, MockHttpBackend backend, DirectiveMap directives) { backend @@ -137,7 +137,7 @@ void main() { ..expectGET('simple.html').respond(200, '
    Simple!
    '); var element = e('
    ignore
    '); - compile([element], directives)(injector, [element]); + compile([element], directives)(rootScope, injector.get(DirectiveInjector), [element]); microLeap(); backend.flush(); @@ -150,27 +150,27 @@ void main() { rootScope.apply(); // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; SIMPLE'); - }))); + })); - it('should load a CSS file with a \$template', async(inject( + it('should load a CSS file with a \$template', async( (Http http, Compiler compile, Scope rootScope, Injector injector, MockHttpBackend backend, DirectiveMap directives) { var element = es('
    ignore
    '); backend.expectGET('simple.css').respond(200, '.hello{}'); - compile(element, directives)(injector, element); + compile(element, directives)(rootScope, injector.get(DirectiveInjector), element); microLeap(); backend.flush(); microLeap(); expect(element[0]).toHaveText('.hello{}inline!'); - }))); + })); - it('should ignore CSS load errors ', async(inject( + it('should ignore CSS load errors ', async( (Http http, Compiler compile, Scope rootScope, Injector injector, MockHttpBackend backend, DirectiveMap directives) { var element = es('
    ignore
    '); backend.expectGET('simple.css').respond(500, 'some error'); - compile(element, directives)(injector, element); + compile(element, directives)(rootScope, injector.get(DirectiveInjector), element); microLeap(); backend.flush(); @@ -180,22 +180,22 @@ void main() { 'HTTP 500: some error\n' '*/\n' 'inline!'); - }))); + })); - it('should load a CSS with no template', async(inject( + it('should load a CSS with no template', async( (Http http, Compiler compile, Scope rootScope, Injector injector, MockHttpBackend backend, DirectiveMap directives) { var element = es('
    ignore
    '); backend.expectGET('simple.css').respond(200, '.hello{}'); - compile(element, directives)(injector, element); + compile(element, directives)(rootScope, injector.get(DirectiveInjector), element); microLeap(); backend.flush(); microLeap(); expect(element[0]).toHaveText('.hello{}'); - }))); + })); - it('should load the CSS before the template is loaded', async(inject( + it('should load the CSS before the template is loaded', async( (Http http, Compiler compile, Scope rootScope, Injector injector, MockHttpBackend backend, DirectiveMap directives) { backend @@ -203,13 +203,13 @@ void main() { ..expectGET('simple.html').respond(200, '
    Simple!
    '); var element = es('ignore'); - compile(element, directives)(injector, element); + compile(element, directives)(rootScope, injector.get(DirectiveInjector), element); microLeap(); backend.flush(); microLeap(); expect(element.first).toHaveText('.hello{}Simple!'); - }))); + })); }); describe('multiple css loading', () { @@ -219,7 +219,7 @@ void main() { ..bind(HtmlAndMultipleCssComponent); }); - it('should load multiple CSS files into a style', async(inject( + it('should load multiple CSS files into a style', async( (Http http, Compiler compile, Scope rootScope, Logger log, Injector injector, MockHttpBackend backend, DirectiveMap directives) { backend @@ -228,7 +228,7 @@ void main() { ..expectGET('simple.html').respond(200, '
    Simple!
    '); var element = e('
    ignore
    '); - compile([element], directives)(injector, [element]); + compile([element], directives)(rootScope, injector.get(DirectiveInjector), [element]); microLeap(); backend.flush(); @@ -241,7 +241,7 @@ void main() { rootScope.apply(); // Note: There is no ordering. It is who ever comes off the wire first! expect(log.result()).toEqual('LOG; SIMPLE'); - }))); + })); }); describe('style cache', () { @@ -251,15 +251,15 @@ void main() { ..bind(TemplateCache, toValue: new TemplateCache(capacity: 0)); }); - it('should load css from the style cache for the second component', async(inject( - (Http http, Compiler compile, MockHttpBackend backend, + it('should load css from the style cache for the second component', async( + (Http http, Compiler compile, MockHttpBackend backend, RootScope rootScope, DirectiveMap directives, Injector injector) { backend ..expectGET('simple.css').respond(200, '.hello{}') ..expectGET('simple.html').respond(200, '
    Simple!
    '); var element = e('
    ignore
    '); - compile([element], directives)(injector, [element]); + compile([element], directives)(rootScope, injector.get(DirectiveInjector), [element]); microLeap(); backend.flush(); @@ -270,14 +270,14 @@ void main() { ); var element2 = e('
    ignore
    '); - compile([element2], directives)(injector, [element2]); + compile([element2], directives)(rootScope, injector.get(DirectiveInjector), [element2]); microLeap(); expect(element2.children[0].shadowRoot).toHaveHtml( '
    Simple!
    ' ); - }))); + })); }); }); } diff --git a/test/core_dom/compiler_spec.dart b/test/core_dom/compiler_spec.dart index fe9130549..6be107436 100644 --- a/test/core_dom/compiler_spec.dart +++ b/test/core_dom/compiler_spec.dart @@ -1,6 +1,7 @@ library compiler_spec; import '../_specs.dart'; +import 'package:angular/core_dom/directive_injector.dart'; forBothCompilers(fn) { @@ -9,7 +10,7 @@ forBothCompilers(fn) { m.bind(Compiler, toImplementation: WalkingCompiler); return m; }); - fn(); + fn('walking'); }); describe('tagging compiler', () { @@ -17,7 +18,16 @@ forBothCompilers(fn) { m.bind(Compiler, toImplementation: TaggingCompiler); return m; }); - fn(); + fn('tagging'); + }); + + describe('tagging compiler with ElementProbe disabled', () { + beforeEachModule((Module m) { + m.bind(Compiler, toImplementation: TaggingCompiler); + m.bind(CompilerConfig, toValue: new CompilerConfig.withOptions(elementProbeEnabled: false)); + return m; + }); + fn('tagging-no-elementProbe'); }); } @@ -31,12 +41,12 @@ forAllCompilersAndComponentFactories(fn) { return m; }); - fn(); + fn('transcluding'); }); } void main() { - forBothCompilers(() => + forBothCompilers((compilerType) => describe('TranscludingComponentFactory', () { TestBed _; @@ -46,7 +56,7 @@ void main() { ..bind(SimpleComponent); }); - beforeEach(inject((TestBed tb) => _ = tb)); + beforeEach((TestBed tb) => _ = tb); it('should correctly detach transcluded content when scope destroyed', async(() { var scope = _.rootScope.createChild({}); @@ -59,7 +69,7 @@ void main() { })); })); - forAllCompilersAndComponentFactories(() => + forAllCompilersAndComponentFactories((compilerType) => describe('dte.compiler', () { TestBed _; @@ -76,10 +86,13 @@ void main() { ..bind(TwoOfTwoDirectives) ..bind(MyController) ..bind(MyParentController) - ..bind(MyChildController); + ..bind(MyChildController) + ..bind(MyScopeModifyingController) + ..bind(SameNameDecorator) + ..bind(SameNameTransclude); }); - beforeEach(inject((TestBed tb) => _ = tb)); + beforeEach((TestBed tb) => _ = tb); it('should compile basic hello world', () { var element = _.compile('
    '); @@ -357,18 +370,20 @@ void main() { })); it('should store ElementProbe with Elements', async(() { + if (compilerType == 'tagging-no-elementProbe') return; + _.compile('
    innerText
    '); microLeap(); _.rootScope.apply(); var simpleElement = _.rootElement.querySelector('simple'); expect(simpleElement).toHaveText('INNER(innerText)'); var simpleProbe = ngProbe(simpleElement); - var simpleComponent = simpleProbe.injector.get(SimpleComponent); + var simpleComponent = simpleProbe.injector.getByKey(new Key(SimpleComponent)); expect(simpleComponent.scope.context['name']).toEqual('INNER'); var shadowRoot = simpleElement.shadowRoot; // If there is no shadow root, skip this. - if (shadowRoot != null) { + if (compilerType != 'transcluding') { var shadowProbe = ngProbe(shadowRoot); expect(shadowProbe).toBeNotNull(); expect(shadowProbe.element).toEqual(shadowRoot); @@ -376,6 +391,32 @@ void main() { } })); + describe('elementProbeEnabled option', () { + beforeEachModule((Module m) { + m.bind(CompilerConfig, toValue: + new CompilerConfig.withOptions(elementProbeEnabled: false)); + }); + + it('should not store ElementProbe with Elements', async(() { + _.compile('
    innerText
    '); + microLeap(); + _.rootScope.apply(); + var simpleElement = _.rootElement.querySelector('simple'); + expect(simpleElement).toHaveText('INNER(innerText)'); + + expect(() => ngProbe(simpleElement)) + .toThrow("Could not find a probe for the node 'simple' nor its parents"); + + var shadowRoot = simpleElement.shadowRoot; + + // If there is no shadow root, skip this. + if (compilerType != 'transcluding') { + expect(() => ngProbe(shadowRoot)) + .toThrow("Could not find a probe for the node 'Instance of 'ShadowRoot'' nor its parents"); + } + })); + }); + it('should create a simple component', async((VmTurnZone zone) { _.rootScope.context['name'] = 'OUTTER'; var element = _.compile(r'
    {{name}}:{{name}}
    '); @@ -592,7 +633,7 @@ void main() { it('should expose PublishModuleDirectiveSuperType as PublishModuleDirectiveSuperType', () { _.compile(r'
    '); - var probe = _.rootScope.context['publishModuleProbe']; + Probe probe = _.rootScope.context['publishModuleProbe']; var directive = probe.injector.get(PublishModuleDirectiveSuperType); expect(directive is PublishModuleAttrDirective).toBeTruthy(); }); @@ -621,7 +662,7 @@ void main() { scope.context['logger'] = logger; scope.context['once'] = null; var elts = es('{{logger("inner")}}'); - compile(elts, _.injector.get(DirectiveMap))(_.injector.createChild([new Module()..bind(Scope, toValue: scope)]), elts); + compile(elts, _.injector.get(DirectiveMap))(scope, _.directiveInjector, elts); expect(logger).toEqual(['new']); expect(logger).toEqual(['new']); @@ -653,7 +694,7 @@ void main() { backend.whenGET('foo.html').respond('
    WORKED
    '); var elts = es(''); var scope = _.rootScope.createChild({}); - compile(elts, _.injector.get(DirectiveMap))(_.injector.createChild([new Module()..bind(Scope, toValue: scope)]), elts); + compile(elts, _.injector.get(DirectiveMap))(scope, _.directiveInjector, elts); expect(logger).toEqual(['SimpleAttachComponent']); scope.destroy(); @@ -685,8 +726,9 @@ void main() { describe('invalid components', () { it('should throw a useful error message for missing selectors', () { Module module = new Module() + ..bind(DirectiveMap) ..bind(MissingSelector); - var injector = _.injector.createChild([module], forceNewInstances: [Compiler, DirectiveMap]); + var injector = _.injector.createChild([module]); var c = injector.get(Compiler); var directives = injector.get(DirectiveMap); expect(() { @@ -697,8 +739,9 @@ void main() { it('should throw a useful error message for invalid selector', () { Module module = new Module() + ..bind(DirectiveMap) ..bind(InvalidSelector); - var injector = _.injector.createChild([module], forceNewInstances: [Compiler, DirectiveMap]); + var injector = _.injector.createChild([module]); var c = injector.get(Compiler); var directives = injector.get(DirectiveMap); @@ -737,9 +780,11 @@ void main() { })); describe('expando memory', () { + if (compilerType == 'tagging-no-elementProbe') return; + Expando expando; - beforeEach(inject((Expando _expando) => expando = _expando)); + beforeEach((Expando _expando) => expando = _expando); ['shadowy', 'shadowless'].forEach((selector) { it('should release expando when a node is freed ($selector)', async(() { @@ -837,7 +882,24 @@ void main() { expect(log.result()).toEqual('Ignore; TabComponent-0; LocalAttrDirective-0; PaneComponent-1; LocalAttrDirective-0'); })); - it('should reuse controllers for transclusions', async((Logger log) { + /* + This test is dissabled becouse I (misko) thinks it has no real use case. It is easier + to understand in terms of ng-repeat + + + + + + + Should pane be allowed to get a hold of ng-repeat? Right now ng-repeat injector is + to the side and is not in any of the parents of the pane. Making an injector a + parent of pane would put the ng-repeat between tabs and pane and it would break + the DirectChild between tabs and pane. + + It is not clear to me (misko) that there is a use case for getting hold of the + tranrscluding directive such a ng-repeat. + */ + xit('should reuse controllers for transclusions', async((Logger log) { _.compile('
    view
    '); microLeap(); @@ -855,6 +917,16 @@ void main() { expect(element.text).toContain('my data'); }); + it('should pass the right scope into inner mustache', (TestBed _) { + var element = _.compile('
    ' + '
    {{ data }}
    ' + '
    '); + + _.rootScope.apply(); + + expect(element.text).toContain('my data'); + }); + it('should expose a ancestor controller to the scope of its children thru a undecorated element', (TestBed _) { var element = _.compile( '
    ' @@ -876,12 +948,30 @@ void main() { _.compile('
    {{name}}
    '); _.rootScope.apply(); expect(_.rootScope.context['name']).toEqual('cover me'); + expect(_.rootScope.context['myCtrl'] is MyController).toEqual(true); expect(_.rootElement.text).toEqual('MyController'); }); + + it('should allow multiple directives with the same selector of different type', (DirectiveMap map) { + _.compile('
    '); + _.rootScope.apply(); + SameNameTransclude transclude = _.rootScope.context['sameTransclude']; + SameNameDecorator decorator = _.rootScope.context['sameDecorator']; + + expect(transclude.valueTransclude).toEqual('worked'); + expect(decorator.valueDecorator).toEqual('worked'); + }); }); })); } +@Controller( + selector: '[my-scope-modifying-controller]') +class MyScopeModifyingController { + MyScopeModifyingController(Scope s) { + s.context['data'] = 'my data'; + } +} @Controller( selector: '[my-parent-controller]', @@ -935,7 +1025,8 @@ class LocalAttrDirective { @Decorator( selector: '[simple-transclude-in-attach]', - visibility: Directive.CHILDREN_VISIBILITY, children: Directive.TRANSCLUDE_CHILDREN) + visibility: Visibility.LOCAL, + children: Directive.TRANSCLUDE_CHILDREN) class SimpleTranscludeInAttachAttrDirective { SimpleTranscludeInAttachAttrDirective(ViewPort viewPort, BoundViewFactory boundViewFactory, Logger log, RootScope scope) { scope.runAsync(() { @@ -984,12 +1075,11 @@ class PublishModuleDirectiveSuperType { selector: '[publish-types]', module: PublishModuleAttrDirective.module) class PublishModuleAttrDirective implements PublishModuleDirectiveSuperType { - static Module _module = new Module() - ..bind(PublishModuleDirectiveSuperType, toFactory: (i) => i.get(PublishModuleAttrDirective)); - static module() => _module; + static module(i) => + i.bind(PublishModuleDirectiveSuperType, toInstanceOf: PublishModuleAttrDirective); - static Injector _injector; - PublishModuleAttrDirective(Injector injector) { + static DirectiveInjector _injector; + PublishModuleAttrDirective(DirectiveInjector injector) { _injector = injector; } } @@ -1289,3 +1379,27 @@ class OneTimeDecorator { OneTimeDecorator(this.log); set value(v) => log(v); } + +@Decorator( + selector: '[same-name]', + children: Directive.TRANSCLUDE_CHILDREN, + map: const { '.': '@valueTransclude' } +) +class SameNameTransclude { + var valueTransclude; + SameNameTransclude(ViewPort port, ViewFactory factory, RootScope scope) { + port.insertNew(factory); + scope.context['sameTransclude'] = this; + } +} + +@Decorator( + selector: '[same-name]', + map: const { 'same-name': '@valueDecorator' } +) +class SameNameDecorator { + var valueDecorator; + SameNameDecorator(RootScope scope) { + scope.context['sameDecorator'] = this; + } +} diff --git a/test/core_dom/directive_injector_spec.dart b/test/core_dom/directive_injector_spec.dart new file mode 100644 index 000000000..59d0aadbb --- /dev/null +++ b/test/core_dom/directive_injector_spec.dart @@ -0,0 +1,122 @@ +library directive_injector_spec; + +import '../_specs.dart'; +import 'package:angular/core_dom/directive_injector.dart'; + +void main() { + describe('DirectiveInjector', () { + + var appInjector = new ModuleInjector([new Module()..bind(_Root)]); + var div = new DivElement(); + var span = new SpanElement(); + var eventHandler = new EventHandler(null, null, null); + + describe('base', () { + DirectiveInjector injector; + Scope scope; + Animate animate; + + addDirective(Type type, [Visibility visibility]) { + if (visibility == null) visibility = Visibility.LOCAL; + var reflector = Module.DEFAULT_REFLECTOR; + injector.bindByKey( + new Key(type), + reflector.factoryFor(type), + reflector.parameterKeysFor(type), + visibility); + } + + beforeEach((Scope _scope, Animate _animate) { + scope = _scope; + animate = _animate; + injector = new DirectiveInjector(null, appInjector, div, new NodeAttrs(div), eventHandler, scope, animate); + }); + + it('should return basic types', () { + expect(injector.parent is DefaultDirectiveInjector).toBe(true); + expect(injector.appInjector).toBe(appInjector); + expect(injector.scope).toBe(scope); + expect(injector.get(Injector)).toBe(appInjector); + expect(injector.get(DirectiveInjector)).toBe(injector); + expect(injector.get(Scope)).toBe(scope); + expect(injector.get(Node)).toBe(div); + expect(injector.get(Element)).toBe(div); + expect((injector.get(NodeAttrs) as NodeAttrs).element).toBe(div); + expect(injector.get(EventHandler)).toBe(eventHandler); + expect(injector.get(Animate)).toBe(animate); + expect((injector.get(ElementProbe) as ElementProbe).element).toBe(div); + }); + + it('should instantiate types', () { + addDirective(_Type9); + addDirective(_Type8); + addDirective(_Type7); + addDirective(_Type5); + addDirective(_Type6); + addDirective(_Type0); + addDirective(_Type1); + addDirective(_Type2); + addDirective(_Type3); + addDirective(_Type4); + expect(() => addDirective(_TypeA)) + .toThrow('Maximum number of directives per element reached.'); + var root = injector.get(_Root); + expect((injector.get(_Type9) as _Type9).type8.type7.type6.type5.type4.type3.type2.type1.type0.root) + .toBe(root); + expect(() => injector.get(_TypeA)).toThrow('No provider found for _TypeA'); + }); + + describe('Visibility', () { + DirectiveInjector childInjector; + DirectiveInjector leafInjector; + + beforeEach(() { + childInjector = new DirectiveInjector(injector, appInjector, span, null, null, null, null); + leafInjector = new DirectiveInjector(childInjector, appInjector, span, null, null, null, null); + }); + + it('should not allow reseting visibility', () { + addDirective(_Type0, Visibility.LOCAL); + expect(() => addDirective(_Type0, Visibility.DIRECT_CHILD)).toThrow( + 'Can not set Visibility: DIRECT_CHILD on _Type0, it alread has Visibility: LOCAL'); + }); + + it('should allow child injector to see types declared at parent injector', () { + addDirective(_Children, Visibility.CHILDREN); + _Children t = injector.get(_Children); + expect(childInjector.get(_Children)).toBe(t); + expect(leafInjector.get(_Children)).toBe(t); + }); + + it('should hide parent injector types when local visibility', () { + addDirective(_Local, Visibility.LOCAL); + _Local t = injector.getByKey(_LOCAL); + expect(() => childInjector.get(_LOCAL)).toThrow(); + expect(() => leafInjector.get(_LOCAL)).toThrow(); + }); + }); + }); + }); +} + +var _CHILDREN = new Key(_Local); +var _LOCAL = new Key(_Local); +var _TYPE0 = new Key(_Local); + +class _Children{} +class _Local{} +class _Direct{} +class _Any{} +class _Root{ } +class _Type0{ final _Root root; _Type0(this.root); } +class _Type1{ final _Type0 type0; _Type1(this.type0); } +class _Type2{ final _Type1 type1; _Type2(this.type1); } +class _Type3{ final _Type2 type2; _Type3(this.type2); } +class _Type4{ final _Type3 type3; _Type4(this.type3); } +class _Type5{ final _Type4 type4; _Type5(this.type4); } +class _Type6{ final _Type5 type5; _Type6(this.type5); } +class _Type7{ final _Type6 type6; _Type7(this.type6); } +class _Type8{ final _Type7 type7; _Type8(this.type7); } +class _Type9{ final _Type8 type8; _Type9(this.type8); } +class _TypeA{ final _Type9 type9; _TypeA(this.type9); } + diff --git a/test/core_dom/element_binder_builder_spec.dart b/test/core_dom/element_binder_builder_spec.dart index 12c7033b8..72a45eb8e 100644 --- a/test/core_dom/element_binder_builder_spec.dart +++ b/test/core_dom/element_binder_builder_spec.dart @@ -32,7 +32,7 @@ main() => describe('ElementBinderBuilder', () { beforeEach((DirectiveMap d, ElementBinderFactory f) { directives = d; - b = f.builder(null); + b = f.builder(null, null); }); addDirective(selector) { @@ -49,17 +49,17 @@ main() => describe('ElementBinderBuilder', () { addDirective('[directive]'); expect(b.decorators.length).toEqual(1); - expect(b.component).toBeNull(); + expect(b.componentData).toBeNull(); expect(b.childMode).toEqual(Directive.COMPILE_CHILDREN); }); - it('should add a component', () { + it('should add a component', async(() { addDirective('component'); expect(b.decorators.length).toEqual(0); - expect(b.component).toBeNotNull(); - }); + expect(b.componentData).toBeNotNull(); + })); it('should add a template', () { addDirective('[structural]'); @@ -71,7 +71,7 @@ main() => describe('ElementBinderBuilder', () { addDirective('[ignore-children]'); expect(b.decorators.length).toEqual(1); - expect(b.component).toBeNull(); + expect(b.componentData).toBeNull(); expect(b.childMode).toEqual(Directive.IGNORE_CHILDREN); }); }); diff --git a/test/core_dom/event_handler_spec.dart b/test/core_dom/event_handler_spec.dart index 43b3b9149..68a6784a6 100644 --- a/test/core_dom/event_handler_spec.dart +++ b/test/core_dom/event_handler_spec.dart @@ -19,7 +19,7 @@ class FooController { class BarComponent { var invoked = false; BarComponent(RootScope scope) { - scope.context['ctrl'] = this; + scope.context['barComponent'] = this; } } @@ -45,7 +45,7 @@ main() { return ngAppElement.firstChild; } - it('should register and handle event', inject((TestBed _) { + it('should register and handle event', (TestBed _) { var e = compile(_, '''
    @@ -53,9 +53,9 @@ main() { _.triggerEvent(e.querySelector('[on-abc]'), 'abc'); expect(_.getScope(e).context['ctrl'].invoked).toEqual(true); - })); + }); - it('shoud register and handle event with long name', inject((TestBed _) { + it('shoud register and handle event with long name', (TestBed _) { var e = compile(_, '''
    @@ -64,9 +64,9 @@ main() { _.triggerEvent(e.querySelector('[on-my-new-event]'), 'myNewEvent'); var fooScope = _.getScope(e); expect(fooScope.context['ctrl'].invoked).toEqual(true); - })); + }); - it('shoud have model updates applied correctly', inject((TestBed _) { + it('shoud have model updates applied correctly', (TestBed _) { var e = compile(_, '''
    {{ctrl.description}}
    @@ -75,7 +75,7 @@ main() { el.dispatchEvent(new Event('abc')); _.rootScope.apply(); expect(el.text).toEqual("new description"); - })); + }); it('shoud register event when shadow dom is used', async((TestBed _) { var e = compile(_,''); @@ -85,11 +85,11 @@ main() { var shadowRoot = e.shadowRoot; var span = shadowRoot.querySelector('span'); span.dispatchEvent(new CustomEvent('abc')); - var ctrl = _.rootScope.context['ctrl']; + BarComponent ctrl = _.rootScope.context['barComponent']; expect(ctrl.invoked).toEqual(true); })); - it('shoud handle event within content only once', async(inject((TestBed _) { + it('shoud handle event within content only once', async((TestBed _) { var e = compile(_, '''
    @@ -102,10 +102,11 @@ main() { document.querySelector('[on-abc]').dispatchEvent(new Event('abc')); var shadowRoot = document.querySelector('bar').shadowRoot; var shadowRootScope = _.getScope(shadowRoot); - expect(shadowRootScope.context['ctrl'].invoked).toEqual(false); + BarComponent ctrl = shadowRootScope.context['ctrl']; + expect(ctrl.invoked).toEqual(false); var fooScope = _.getScope(document.querySelector('[foo]')); expect(fooScope.context['ctrl'].invoked).toEqual(true); - }))); + })); }); } diff --git a/test/core_dom/http_spec.dart b/test/core_dom/http_spec.dart index 9a32f69f8..95a3a2805 100644 --- a/test/core_dom/http_spec.dart +++ b/test/core_dom/http_spec.dart @@ -8,9 +8,10 @@ var VALUE = 'val'; var CACHED_VALUE = 'cached_value'; class FakeCache extends UnboundedCache { - get(x) => x == 'f' ? new HttpResponse(200, CACHED_VALUE) : null; - put(_,__) => null; - + HttpResponse get(x) => x == 'f' ? new HttpResponse(200, CACHED_VALUE) : null; + HttpResponse put(_,__) => null; + void clear() {} + int get length => 0; } class SubstringRewriter extends UrlRewriter { @@ -88,9 +89,8 @@ void main() { // we don't care about the data field. backend.expect('POST', '/url', 'null').respond(''); - http(url: '/url', method: 'POST'); expect(() { - flush(); + http(url: '/url', method: 'POST'); }).toThrow('with different data'); // satisfy the expectation for our afterEach's assert. @@ -952,6 +952,60 @@ void main() { flush(); })); }); + + describe('request interceptors', () { + bool interceptorCalled; + + beforeEach(() { + interceptorCalled = false; + }); + + describe('synchronous', () { + beforeEachModule((Module module) { + module.bind(HttpInterceptors, toValue: new HttpInterceptors() + // The first interceptor is sync, causing the second interceptor to be called synchronously + ..add(new HttpInterceptor(request: (cfg) => cfg)) + ..add(new HttpInterceptor(request: (cfg) { + interceptorCalled = true; + return cfg; + }))); + }); + + it('should call backend synchronously if request interceptor chain is ' + 'synchronous', async(() { + backend.expect('POST', '/url', '').respond(''); + http(url: '/url', method: 'POST', data: ''); + expect(interceptorCalled).toBe(true); + expect(backend.responses.isEmpty).toBe(false); // request made immediately + flush(); + })); + }); + + describe('asynchronous', () { + beforeEachModule((Module module) { + module.bind(HttpInterceptors, toValue: new HttpInterceptors() + // The first interceptor is async, causing the second interceptor to be + // called in a microtask + ..add(new HttpInterceptor(request: (cfg) => new Future.value(cfg))) + ..add(new HttpInterceptor(request: (cfg) { + interceptorCalled = true; + return cfg; + }))); + }); + + it('should call backend asynchronously if request interceptor chain is ' + 'asynchronous', async(() { + backend.expect('POST', '/url', '').respond(''); + http(url: '/url', method: 'POST', data: ''); + expect(interceptorCalled).toBe(false); + expect(backend.expectations.isEmpty).toBe(false); + backend.verifyNoOutstandingRequest(); + + flush(); + expect(interceptorCalled).toBe(true); + })); + }); + }); }); describe('url rewriting', () { @@ -1369,6 +1423,39 @@ void main() { }); }); }); + + describe('coalesce', () { + beforeEachModule((Module module) { + var coalesceDuration = new Duration(milliseconds: 100); + module.bind(HttpConfig, toValue: new HttpConfig(coalesceDuration: coalesceDuration)); + }); + + it('should coalesce requests', async((Http http) { + backend.expect('GET', '/foo').respond(200, 'foo'); + backend.expect('GET', '/bar').respond(200, 'bar'); + + var fooResp, barResp; + http.get('/foo').then((HttpResponse resp) => fooResp = resp.data); + http.get('/bar').then((HttpResponse resp) => barResp = resp.data); + + microLeap(); + backend.flush(); + microLeap(); + expect(fooResp).toBeNull(); + expect(barResp).toBeNull(); + + clockTick(milliseconds: 99); + microLeap(); + expect(fooResp).toBeNull(); + expect(barResp).toBeNull(); + + clockTick(milliseconds: 1); + microLeap(); + expect(fooResp).toEqual('foo'); + expect(barResp).toEqual('bar'); + })); + + }); }); } diff --git a/test/core_dom/mustache_spec.dart b/test/core_dom/mustache_spec.dart index ba47f09b6..92ce00d12 100644 --- a/test/core_dom/mustache_spec.dart +++ b/test/core_dom/mustache_spec.dart @@ -9,25 +9,25 @@ main() { module.bind(_HelloFormatter); module.bind(_FooDirective); }); - beforeEach(inject((TestBed tb) => _ = tb)); + beforeEach((TestBed tb) => _ = tb); - it('should replace {{}} in text', inject((Compiler compile, - Scope rootScope, Injector injector, DirectiveMap directives) + it('should replace {{}} in text', (Compiler compile, Scope rootScope, + Injector injector, DirectiveMap directives) { var element = es('
    {{name}}!
    '); var template = compile(element, directives); rootScope.context['name'] = 'OK'; - var view = template(injector); + var view = template(rootScope, injector.get(DirectiveInjector)); element = view.nodes; rootScope.apply(); expect(element).toHaveText('OK!'); - })); + }); describe('observe/flush phase', () { - it('should first only when then value has settled', async((Logger log) { + it('should fire only when then value has settled', async((Logger log) { _.compile('
    '); _.rootScope.apply(); @@ -44,62 +44,59 @@ main() { })); }); - it('should replace {{}} in attribute', inject((Compiler compile, - Scope rootScope, Injector injector, DirectiveMap directives) - { + it('should replace {{}} in attribute', (Compiler compile, Scope rootScope, + Injector injector, DirectiveMap directives) { Element element = e('
    '); var template = compile([element], directives); rootScope.context['name'] = 'OK'; rootScope.context['age'] = 23; - var view = template(injector); + var view = template(rootScope, injector.get(DirectiveInjector)); element = view.nodes[0]; rootScope.apply(); expect(element.attributes['some-attr']).toEqual('OK'); expect(element.attributes['other-attr']).toEqual('23'); - })); + }); - it('should allow newlines in attribute', inject((Compiler compile, - RootScope rootScope, Injector injector, DirectiveMap directives) - { + it('should allow newlines in attribute', (Compiler compile, + RootScope rootScope, Injector injector, DirectiveMap directives) { Element element = e('
    '); var template = compile([element], directives); rootScope.context['line1'] = 'L1'; rootScope.context['line2'] = 'L2'; - var view = template(injector); + var view = template(rootScope, injector.get(DirectiveInjector)); element = view.nodes[0]; rootScope.apply(); expect(element.attributes['multiline-attr']) .toEqual('line1: L1\nline2: L2'); - })); + }); - it('should handle formatters', inject((Compiler compile, RootScope rootScope, - Injector injector, DirectiveMap directives) - { + it('should handle formatters', (Compiler compile, RootScope rootScope, + Injector injector, DirectiveMap directives) { var element = es('
    {{"World" | hello}}
    '); var template = compile(element, directives); - var view = template(injector); + var view = template(rootScope, injector.get(DirectiveInjector)); rootScope.apply(); element = view.nodes; expect(element).toHaveHtml('Hello, World!'); - })); + }); }); describe('NgShow', () { TestBed _; - beforeEach(inject((TestBed tb) => _ = tb)); + beforeEach((TestBed tb) => _ = tb); it('should add/remove ng-hide class', () { var element = _.compile('
    '); diff --git a/test/core_dom/selector_spec.dart b/test/core_dom/selector_spec.dart index 9d63d516d..856c33b45 100644 --- a/test/core_dom/selector_spec.dart +++ b/test/core_dom/selector_spec.dart @@ -2,34 +2,47 @@ library angular.dom.selector_spec; import '../_specs.dart'; -@Decorator(selector:'b') class _BElement{} -@Decorator(selector:'.b') class _BClass{} -@Decorator(selector:'[directive]') class _DirectiveAttr{} -@Decorator(selector:'[wildcard-*]') class _WildcardDirectiveAttr{} -@Decorator(selector:'[directive=d][foo=f]') class _DirectiveFooAttr{} -@Decorator(selector:'b[directive]') class _BElementDirectiveAttr{} -@Decorator(selector:'[directive=value]') class _DirectiveValueAttr{} -@Decorator(selector:'b[directive=value]') class _BElementDirectiveValue{} -@Decorator(selector:':contains(/abc/)') class _ContainsAbc{} -@Decorator(selector:'[*=/xyz/]') class _AttributeContainsXyz{} - -@Component(selector:'component') class _Component{} -@Decorator(selector:'[attribute]') class _Attribute{} -@Decorator(selector:'[structural]', - children: Directive.TRANSCLUDE_CHILDREN) - class _Structural{} - -@Decorator(selector:'[ignore-children]', - children: Directive.IGNORE_CHILDREN) - class _IgnoreChildren{} - -@Decorator(selector: '[my-model][required]') -@Decorator(selector: '[my-model][my-required]') - class _TwoDirectives {} - -@Decorator(selector: '[two-directives]') class _OneOfTwoDirectives {} -@Decorator(selector: '[two-directives]') class _TwoOfTwoDirectives {} - +const _aBElement = const Decorator(selector:'b'); +const _aBClass = const Decorator(selector:'.b'); +const _aDirectiveAttr = const Decorator(selector:'[directive]'); +const _aWildcardDirectiveAttr = const Decorator(selector:'[wildcard-*]'); +const _aDirectiveFooAttr = const Decorator(selector:'[directive=d][foo=f]'); +const _aBElementDirectiveAttr = const Decorator(selector:'b[directive]'); +const _aDirectiveValueAttr = const Decorator(selector:'[directive=value]'); +const _aBElementDirectiveValue = const Decorator(selector:'b[directive=value]'); +const _aContainsAbc = const Decorator(selector:':contains(/abc/)'); +const _aAttributeContainsXyz = const Decorator(selector:'[*=/xyz/]'); +const _aAttribute = const Decorator(selector:'[attribute]'); +const _aCComponent = const Component(selector:'component'); +const _aStructural = const Decorator(selector:'[structural]', + children: Directive.TRANSCLUDE_CHILDREN); +const _aIgnoreChildren = const Decorator(selector:'[ignore-children]', + children: Directive.IGNORE_CHILDREN); +const _aTwoDirectives0 = const Decorator(selector: '[my-model][required]'); +const _aTwoDirectives1 = const Decorator(selector: '[my-model][my-required]'); +const _aOneOfTwoDirectives = const Decorator(selector: '[two-directives]'); +const _aTwoOfTwoDirectives = const Decorator(selector: '[two-directives]'); + + +@_aBElement class _BElement{} +@_aBClass class _BClass{} +@_aDirectiveAttr class _DirectiveAttr{} +@_aWildcardDirectiveAttr class _WildcardDirectiveAttr{} +@_aDirectiveFooAttr class _DirectiveFooAttr{} +@_aBElementDirectiveAttr class _BElementDirectiveAttr{} +@_aDirectiveValueAttr class _DirectiveValueAttr{} +@_aBElementDirectiveValue class _BElementDirectiveValue{} +@_aContainsAbc class _ContainsAbc{} +@_aAttributeContainsXyz class _AttributeContainsXyz{} +@_aCComponent class _CComponent{} +@_aAttribute class _Attribute{} +@_aStructural class _Structural{} +@_aIgnoreChildren class _IgnoreChildren{} +@_aOneOfTwoDirectives class _OneOfTwoDirectives {} +@_aTwoOfTwoDirectives class _TwoOfTwoDirectives {} + +@_aTwoDirectives0 +@_aTwoDirectives1 class _TwoDirectives {} main() { describe('Selector', () { @@ -51,7 +64,7 @@ main() { ..bind(_BElementDirectiveValue) ..bind(_ContainsAbc) ..bind(_AttributeContainsXyz) - ..bind(_Component) + ..bind(_CComponent) ..bind(_Attribute) ..bind(_Structural) ..bind(_IgnoreChildren) @@ -69,14 +82,14 @@ main() { expect( selector(element = e('')), toEqualsDirectiveInfos([ - { "selector": 'b', "value": null, "element": element} + { "selector": 'b', "value": null, "element": element, "annotation": _aBElement} ])); }); it('should match directive on class', () { expect(selector(element = e('
    ')), toEqualsDirectiveInfos([ - { "selector": '.b', "value": null, "element": element} + { "selector": '.b', "value": null, "element": element, "annotation": _aBClass} ])); }); @@ -84,39 +97,39 @@ main() { expect(selector(element = e('
    ')), toEqualsDirectiveInfos([ { "selector": '[directive]', "value": 'abc', "element": element, - "name": 'directive' }])); + "name": 'directive', "annotation": _aDirectiveAttr }])); expect(selector(element = e('
    ')), toEqualsDirectiveInfos([ { "selector": '[directive]', "value": '', "element": element, - "name": 'directive' }])); + "name": 'directive', "annotation": _aDirectiveAttr }])); }); it('should match directive on element[attribute]', () { expect(selector(element = e('')), toEqualsDirectiveInfos([ - { "selector": 'b', "value": null, "element": element}, - { "selector": '[directive]', "value": 'abc', "element": element}, - { "selector": 'b[directive]', "value": 'abc', "element": element} + { "selector": 'b', "value": null, "element": element, "annotation": _aBElement}, + { "selector": '[directive]', "value": 'abc', "element": element, "annotation": _aDirectiveAttr}, + { "selector": 'b[directive]', "value": 'abc', "element": element, "annotation": _aBElementDirectiveAttr} ])); }); it('should match directive on [attribute=value]', () { expect(selector(element = e('
    ')), toEqualsDirectiveInfos([ - { "selector": '[directive]', "value": 'value', "element": element}, - { "selector": '[directive=value]', "value": 'value', "element": element} + { "selector": '[directive]', "value": 'value', "element": element, "annotation": _aDirectiveAttr}, + { "selector": '[directive=value]', "value": 'value', "element": element, "annotation": _aDirectiveValueAttr} ])); }); it('should match directive on element[attribute=value]', () { expect(selector(element = e('
    ')), toEqualsDirectiveInfos([ - { "selector": 'b', "value": null, "element": element, "name": null}, - { "selector": '[directive]', "value": 'value', "element": element}, - { "selector": '[directive=value]', "value": 'value', "element": element}, - { "selector": 'b[directive]', "value": 'value', "element": element}, - { "selector": 'b[directive=value]', "value": 'value', "element": element} + { "selector": 'b', "value": null, "element": element, "name": null, "annotation": _aBElement}, + { "selector": '[directive]', "value": 'value', "element": element, "annotation": _aDirectiveAttr}, + { "selector": '[directive=value]', "value": 'value', "element": element, "annotation": _aDirectiveValueAttr}, + { "selector": 'b[directive]', "value": 'value', "element": element, "annotation": _aBElementDirectiveAttr}, + { "selector": 'b[directive=value]', "value": 'value', "element": element, "annotation": _aBElementDirectiveValue} ])); }); @@ -126,7 +139,9 @@ main() { { "selector": '[*=/xyz/]', "value": 'attr', "ast": '"before-xyz-after"', - "element": element, "name": 'attr'} + "element": element, + "name": 'attr', + "annotation": _aAttributeContainsXyz} ])); }); @@ -134,33 +149,33 @@ main() { expect(selector(element = e('
    ')), toEqualsDirectiveInfos([ { "selector": '[wildcard-*]', "value": 'ignored', - "element": element, "name": 'wildcard-match'} + "element": element, "name": 'wildcard-match', "annotation": _aWildcardDirectiveAttr} ])); }); - it('should sort by priority', () { + it('should sort by priority', async(() { TemplateElementBinder eb = selector(element = e( '')); expect(eb, toEqualsDirectiveInfos( null, - template: {"selector": "[structural]", "value": "", "element": element})); + template: {"selector": "[structural]", "value": "", "element": element, "annotation": _aStructural})); expect(eb.templateBinder, toEqualsDirectiveInfos( [ - { "selector": "[attribute]", "value": "", "element": element }, - { "selector": "[ignore-children]", "value": "", "element": element } + { "selector": "[attribute]", "value": "", "element": element, "annotation": _aAttribute }, + { "selector": "[ignore-children]", "value": "", "element": element, "annotation": _aIgnoreChildren } ], - component: { "selector": "component", "value": null, "element": element })); - }); + component: { "selector": "component", "value": null, "element": element, "annotation": _aCComponent })); + })); it('should match on multiple directives', () { expect(selector(element = e('
    ')), toEqualsDirectiveInfos([ - { "selector": '[directive]', "value": 'd', "element": element}, - { "selector": '[directive=d][foo=f]', "value": 'f', "element": element} + { "selector": '[directive]', "value": 'd', "element": element, "annotation": _aDirectiveAttr}, + { "selector": '[directive=d][foo=f]', "value": 'f', "element": element, "annotation": _aDirectiveFooAttr} ])); }); @@ -187,8 +202,8 @@ main() { it('should match an two directives with the same selector', () { expect(selector(element = e('
    ')), toEqualsDirectiveInfos([ - { "selector": '[two-directives]', "value": '', "element": element}, - { "selector": '[two-directives]', "value": '', "element": element} + { "selector": '[two-directives]', "value": '', "element": element, "annotation": _aOneOfTwoDirectives}, + { "selector": '[two-directives]', "value": '', "element": element, "annotation": _aTwoOfTwoDirectives} ])); }); @@ -200,8 +215,8 @@ main() { it('should collect bind-* attributes', () { ElementBinder binder = selector(e('')); expect(binder.bindAttrs.keys.length).toEqual(2); - expect(binder.bindAttrs['bind-x'].expression).toEqual('y'); - expect(binder.bindAttrs['bind-z'].expression).toEqual('yy'); + expect(binder.bindAttrs['x'].expression).toEqual('y'); + expect(binder.bindAttrs['z'].expression).toEqual('yy'); }); }); @@ -252,10 +267,11 @@ class DirectiveInfosMatcher extends Matcher { Description describe(Description description) => description..add(expected.toString()); - bool _refMatches(directiveRef, expectedMap) => + bool _refMatches(DirectiveRef directiveRef, Map expectedMap) => directiveRef.element == expectedMap['element'] && directiveRef.annotation.selector == expectedMap['selector'] && directiveRef.value == expectedMap['value'] && + (expectedMap['annotation'] == null || directiveRef.annotation == expectedMap['annotation']) && (directiveRef.valueAST == null || directiveRef.valueAST.expression == expectedMap['ast']); @@ -275,7 +291,7 @@ class DirectiveInfosMatcher extends Matcher { pass = pass && _refMatches((binder as TemplateElementBinder).template, expectedTemplate); } if (pass && expectedComponent != null) { - pass = pass && _refMatches(binder.component, expectedComponent); + pass = pass && _refMatches(binder.componentData.ref, expectedComponent); } return pass; } diff --git a/test/core_dom/view_spec.dart b/test/core_dom/view_spec.dart index 7fae0a95e..3be620b3e 100644 --- a/test/core_dom/view_spec.dart +++ b/test/core_dom/view_spec.dart @@ -63,7 +63,7 @@ class BFormatter { main() { var viewFactoryFactory = (a,b,c,d) => new WalkingViewFactory(a,b,c,d); describe('View', () { - var anchor; + ViewPort viewPort; Element rootElement; var viewCache; @@ -77,32 +77,36 @@ main() { beforeEach((Injector injector, Profiler perf) { rootElement.innerHtml = ''; - anchor = new ViewPort(rootElement.childNodes[0], + var scope = injector.get(Scope); + viewPort = new ViewPort(injector.get(DirectiveInjector), scope, rootElement.childNodes[0], injector.get(Animate)); - a = (viewFactoryFactory(es('Aa'), [], perf, expando))(injector); - b = (viewFactoryFactory(es('Bb'), [], perf, expando))(injector); + a = (viewFactoryFactory(es('Aa'), [], perf, expando))(scope, injector.get(DirectiveInjector)); + b = (viewFactoryFactory(es('Bb'), [], perf, expando))(scope, injector.get(DirectiveInjector)); }); describe('insertAfter', () { - it('should insert block after anchor view', () { - anchor.insert(a); + it('should insert block after anchor view', (RootScope scope) { + viewPort.insert(a); + scope.flush(); expect(rootElement).toHaveHtml('Aa'); }); - it('should insert multi element view after another multi element view', () { - anchor.insert(a); - anchor.insert(b, insertAfter: a); + it('should insert multi element view after another multi element view', (RootScope scope) { + viewPort.insert(a); + viewPort.insert(b, insertAfter: a); + scope.flush(); expect(rootElement).toHaveHtml('AaBb'); }); - it('should insert multi element view before another multi element view', () { - anchor.insert(b); - anchor.insert(a); + it('should insert multi element view before another multi element view', (RootScope scope) { + viewPort.insert(b); + viewPort.insert(a); + scope.flush(); expect(rootElement).toHaveHtml('AaBb'); }); @@ -110,20 +114,23 @@ main() { describe('remove', () { - beforeEach(() { - anchor.insert(a); - anchor.insert(b, insertAfter: a); + beforeEach((RootScope scope) { + viewPort.insert(a); + viewPort.insert(b, insertAfter: a); + scope.flush(); expect(rootElement.text).toEqual('AaBb'); }); - it('should remove the last view', () { - anchor.remove(b); + it('should remove the last view', (RootScope scope) { + viewPort.remove(b); + scope.flush(); expect(rootElement).toHaveHtml('Aa'); }); - it('should remove child views from parent pseudo black', () { - anchor.remove(a); + it('should remove child views from parent pseudo black', (RootScope scope) { + viewPort.remove(a); + scope.flush(); expect(rootElement).toHaveHtml('Bb'); }); @@ -176,16 +183,18 @@ main() { describe('moveAfter', () { - beforeEach(() { - anchor.insert(a); - anchor.insert(b, insertAfter: a); + beforeEach((RootScope scope) { + viewPort.insert(a); + viewPort.insert(b, insertAfter: a); + scope.flush(); expect(rootElement.text).toEqual('AaBb'); }); - it('should move last to middle', () { - anchor.move(a, moveAfter: b); + it('should move last to middle', (RootScope scope) { + viewPort.move(a, moveAfter: b); + scope.flush(); expect(rootElement).toHaveHtml('BbAa'); }); }); @@ -193,13 +202,13 @@ main() { describe('deferred', () { - it('should load directives/formatters from the child injector', () { + it('should load directives/formatters from the child injector', (RootScope scope) { Module rootModule = new Module() ..bind(Probe) ..bind(Log) ..bind(AFormatter) ..bind(ADirective) - ..bind(Node, toFactory: (injector) => document.body); + ..bind(Node, toFactory: () => document.body, inject: []); Injector rootInjector = applicationFactory() .addModule(rootModule) @@ -209,7 +218,7 @@ main() { Compiler compiler = rootInjector.get(Compiler); DirectiveMap directives = rootInjector.get(DirectiveMap); - compiler(es('{{\'a\' | formatterA}}'), directives)(rootInjector); + compiler(es('{{\'a\' | formatterA}}'), directives)(rootScope, rootInjector.get(DirectiveInjector)); rootScope.apply(); expect(log.log, equals(['AFormatter', 'ADirective'])); @@ -219,11 +228,12 @@ main() { ..bind(BFormatter) ..bind(BDirective); - var childInjector = forceNewDirectivesAndFormatters(rootInjector, [childModule]); + var childInjector = forceNewDirectivesAndFormatters(rootInjector, null, [childModule]); DirectiveMap newDirectives = childInjector.get(DirectiveMap); + var scope = childInjector.get(Scope); compiler(es('{{\'a\' | formatterA}}' - '{{\'b\' | formatterB}}'), newDirectives)(childInjector); + '{{\'b\' | formatterB}}'), newDirectives)(scope, childInjector.get(DirectiveInjector)); rootScope.apply(); expect(log.log, equals(['AFormatter', 'ADirective', 'BFormatter', 'ADirective', 'BDirective'])); diff --git a/test/core_dom/web_components_spec.dart b/test/core_dom/web_components_spec.dart new file mode 100644 index 000000000..6cad2a403 --- /dev/null +++ b/test/core_dom/web_components_spec.dart @@ -0,0 +1,93 @@ +library angular.dom.web_components_spec; + +import '../_specs.dart'; +import 'dart:js' as js; + +registerElement(String name, prototype) { + js.context['angularTestsRegisterElement'].apply( + [name, new js.JsObject.jsify(prototype)]); +} + + + +main() { + describe('WebComponent support', () { + TestBed _; + + /** + * Returns the property [prop] as read through the JS interface. + * [elt] is optional and defaults to the [TestBed]'s rootElement. + */ + customProp(String prop, [Element elt]) { + if (elt == null) elt = _.rootElement; + return (new js.JsObject.fromBrowserObject(elt))[prop]; + } + + /** + * Sets the property [prop] to [value] through the JS interface. + * [elt] is optional and defaults to the [TestBed]'s rootElement. + */ + void setCustomProp(String prop, value, [Element elt]) { + if (elt == null) elt = _.rootElement; + (new js.JsObject.fromBrowserObject(_.rootElement))[prop] = value; + } + + compileAndUpgrade(String html) { + _.compile(html); + var CustomElements = js.context['CustomElements']; + if (CustomElements != null) { + CustomElements['upgradeAll'].apply([new js.JsObject.fromBrowserObject(_.rootElement)]); + } + } + + beforeEach((TestBed tb) { + _ = tb; + }); + + it('should create custom elements', () { + registerElement('tests-basic', {'prop-x': 6}); + + // Create a web component + compileAndUpgrade(''); + expect(customProp('prop-x')).toEqual(6); + }); + + + it('should bind to Custom Element properties', () { + registerElement('tests-bound', {'prop-y': 10}); + compileAndUpgrade(''); + + // Scope has not been digested yet + expect(customProp('prop-y')).toEqual(10); + + _.rootScope.apply(); + expect(customProp('prop-y')).toEqual(27); + }); + + + it('should bind to a non-existent property', () { + registerElement('tests-empty', {}); + compileAndUpgrade(''); + _.rootScope.apply(); + expect(customProp('new-prop')).toEqual(27); + }); + + it('should bind to both directives and properties', () { + registerElement('tests-double', {}); + compileAndUpgrade(''); + _.rootScope.apply(); + expect(customProp('ng-bind')).toEqual("hello"); + expect(_.rootElement).toHaveText('hello'); + }); + + it('should support two-way bindings for components that trigger a change event', () { + registerElement('tests-twoway', {}); + compileAndUpgrade(''); + + setCustomProp('prop', 6); + _.rootElement.dispatchEvent(new Event.eventType('CustomEvent', 'change')); + + expect(_.rootScope.context['x']).toEqual(6); + }); + }); +} diff --git a/test/core_dom/web_components_support.js b/test/core_dom/web_components_support.js new file mode 100644 index 000000000..4f09d1190 --- /dev/null +++ b/test/core_dom/web_components_support.js @@ -0,0 +1,10 @@ +/** + * Used to create Javascript Web Components from Dart tests + */ +function angularTestsRegisterElement(name, prototype) { + // Polymer requires that all prototypes are chained to HTMLElement + // https://github.com/Polymer/CustomElements/issues/121 + prototype.__proto__ = HTMLElement.prototype; + prototype.createdCallback = function() {}; + document.registerElement(name, {prototype: prototype}); +} diff --git a/test/core_dom/web_platform_spec.dart b/test/core_dom/web_platform_spec.dart index 647e34c39..5984f06a2 100644 --- a/test/core_dom/web_platform_spec.dart +++ b/test/core_dom/web_platform_spec.dart @@ -10,6 +10,7 @@ main() { beforeEachModule((Module module) { module ..bind(WebPlatformTestComponent) + ..bind(WebPlatformTestComponentWithAttribute) ..bind(InnerComponent) ..bind(OuterComponent) ..bind(WebPlatform, toValue: new WebPlatform()); @@ -54,6 +55,24 @@ main() { } })); + it('should not crash with an attribute selector; but wont work either..', + async((TestBed _, MockHttpBackend backend, WebPlatform platform) { + + backend + ..expectGET('style.css').respond(200, 'span { background-color: red; ' + '}') + ..expectGET('template.html').respond(200, 'foo'); + + Element element = e('ignore' + ''); + + _.compile(element); + + microLeap(); + backend.flush(); + microLeap(); + })); + it('should scope :host styles to the primary element.', async((TestBed _, MockHttpBackend backend, WebPlatform platform) { @@ -176,6 +195,14 @@ main() { class WebPlatformTestComponent { } +@Component( + selector: "test-wptca[a]", + publishAs: "ctrl", + templateUrl: "template.html", + cssUrl: "style.css") +class WebPlatformTestComponentWithAttribute { +} + @Component( selector: "my-inner", publishAs: "ctrl", diff --git a/test/directive/ng_base_css_spec.dart b/test/directive/ng_base_css_spec.dart index 5a6bde427..bc56214be 100644 --- a/test/directive/ng_base_css_spec.dart +++ b/test/directive/ng_base_css_spec.dart @@ -25,9 +25,9 @@ main() => describe('NgBaseCss', () { it('should load css urls from ng-base-css', async((TestBed _, MockHttpBackend backend) { backend - ..expectGET('base.css').respond(200, '.base{}') ..expectGET('simple.css').respond(200, '.simple{}') - ..expectGET('simple.html').respond(200, '
    Simple!
    '); + ..expectGET('simple.html').respond(200, '
    Simple!
    ') + ..expectGET('base.css').respond(200, '.base{}'); var element = e('
    ignore
    '); _.compile(element); @@ -43,9 +43,9 @@ main() => describe('NgBaseCss', () { it('ng-base-css should overwrite parent ng-base-csses', async((TestBed _, MockHttpBackend backend) { backend - ..expectGET('base.css').respond(200, '.base{}') ..expectGET('simple.css').respond(200, '.simple{}') - ..expectGET('simple.html').respond(200, '
    Simple!
    '); + ..expectGET('simple.html').respond(200, '
    Simple!
    ') + ..expectGET('base.css').respond(200, '.base{}'); var element = e('
    ignore
    '); _.compile(element); @@ -83,9 +83,9 @@ main() => describe('NgBaseCss', () { it('ng-base-css should be available from the injector', async((TestBed _, MockHttpBackend backend) { backend - ..expectGET('injected.css').respond(200, '.injected{}') ..expectGET('simple.css').respond(200, '.simple{}') - ..expectGET('simple.html').respond(200, '
    Simple!
    '); + ..expectGET('simple.html').respond(200, '
    Simple!
    ') + ..expectGET('injected.css').respond(200, '.injected{}'); var element = e('
    ignore
    '); _.compile(element); diff --git a/test/directive/ng_bind_html_spec.dart b/test/directive/ng_bind_html_spec.dart index e1a0ad665..019987b04 100644 --- a/test/directive/ng_bind_html_spec.dart +++ b/test/directive/ng_bind_html_spec.dart @@ -9,7 +9,7 @@ main() { it('should sanitize and set innerHtml and sanitize and set html', (Scope scope, Injector injector, Compiler compiler, DirectiveMap directives) { var element = es('
    '); - compiler(element, directives)(injector, element); + compiler(element, directives)(scope, injector.get(DirectiveInjector), element); scope.context['htmlVar'] = 'Google!'; scope.apply(); // Sanitization removes the href attribute on the tag. @@ -18,7 +18,7 @@ main() { describe('injected NodeValidator', () { beforeEachModule((Module module) { - module.bind(dom.NodeValidator, toFactory: (_) { + module.bind(dom.NodeValidator, toFactory: () { final validator = new NodeValidatorBuilder(); validator.allowNavigation(new AnyUriPolicy()); validator.allowTextElements(); @@ -28,7 +28,7 @@ main() { it('should use injected NodeValidator and override default sanitize behavior', (Scope scope, Injector injector, Compiler compiler, DirectiveMap directives) { var element = es('
    '); - compiler(element, directives)(injector, element); + compiler(element, directives)(scope, injector.get(DirectiveInjector), element); scope.context['htmlVar'] = '
    Google!'; scope.apply(); // Sanitation allows href attributes per injected sanitizer. diff --git a/test/directive/ng_bind_spec.dart b/test/directive/ng_bind_spec.dart index af65f7f9d..3d7468acf 100644 --- a/test/directive/ng_bind_spec.dart +++ b/test/directive/ng_bind_spec.dart @@ -10,7 +10,7 @@ main() { it('should set.text', (Scope scope, Injector injector, Compiler compiler, DirectiveMap directives) { var element = e('
    '); - compiler([element], directives)(injector, [element]); + compiler([element], directives)(scope, injector.get(DirectiveInjector), [element]); scope.context['a'] = "abc123"; scope.apply(); expect(element.text).toEqual('abc123'); diff --git a/test/directive/ng_form_spec.dart b/test/directive/ng_form_spec.dart index 1821f217a..a0cb7624e 100644 --- a/test/directive/ng_form_spec.dart +++ b/test/directive/ng_form_spec.dart @@ -722,9 +722,9 @@ void main() { }); it('should be resolvable by injector if configured by user.', - (Injector injector, Compiler compiler, DirectiveMap directives) { + (Scope scope, Injector injector, Compiler compiler, DirectiveMap directives) { var element = es('
    '); - expect(() => compiler(element, directives)(injector, element)) + expect(() => compiler(element, directives)(scope, injector.get(DirectiveInjector), element)) .not.toThrow(); }); }); diff --git a/test/directive/ng_if_spec.dart b/test/directive/ng_if_spec.dart index 0f84d59d8..ff016a9a9 100644 --- a/test/directive/ng_if_spec.dart +++ b/test/directive/ng_if_spec.dart @@ -28,7 +28,7 @@ main() { logger = _logger; compile = (html, [applyFn]) { element = e(html); - compiler([element], _directives)(injector, [element]); + compiler([element], _directives)(scope, injector.get(DirectiveInjector), [element]); scope.apply(applyFn); }; directives = _directives; diff --git a/test/directive/ng_model_spec.dart b/test/directive/ng_model_spec.dart index 20b87665b..e7418dd61 100644 --- a/test/directive/ng_model_spec.dart +++ b/test/directive/ng_model_spec.dart @@ -36,6 +36,7 @@ bool isBrowser(String pattern) => dom.window.navigator.userAgent.indexOf(pattern void main() { describe('ng-model', () { TestBed _; + DirectiveInjector dirInjector; beforeEachModule((Module module) { module @@ -44,7 +45,10 @@ void main() { ..bind(CountingValidator); }); - beforeEach((TestBed tb) => _ = tb); + beforeEach((TestBed tb) { + _ = tb; + dirInjector = new DirectiveInjector(null, _.injector, null, null, null, null, null); + }); describe('type="text" like', () { it('should update input value from model', () { @@ -94,8 +98,8 @@ void main() { var ngModelOptions = new NgModelOptions(); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, ngElement, i.createChild([new Module()]), - nodeAttrs, new Animate()); + var model = new NgModel(scope, ngElement, dirInjector, + nodeAttrs, new Animate(), null); dom.querySelector('body').append(element); var input = new InputTextLike(element, model, scope, ngModelOptions); @@ -374,8 +378,8 @@ void main() { var ngModelOptions = new NgModelOptions(); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, ngElement, i.createChild([new Module()]), - nodeAttrs, new Animate()); + var model = new NgModel(scope, ngElement, dirInjector, + nodeAttrs, new Animate(), null); dom.querySelector('body').append(element); var input = new InputTextLike(element, model, scope, ngModelOptions); @@ -466,8 +470,8 @@ void main() { var ngModelOptions = new NgModelOptions(); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, ngElement, i.createChild([new Module()]), - nodeAttrs, new Animate()); + var model = new NgModel(scope, ngElement, dirInjector, + nodeAttrs, new Animate(), null); dom.querySelector('body').append(element); var input = new InputTextLike(element, model, scope, ngModelOptions); @@ -512,7 +516,12 @@ void main() { }); }); - describe('no type attribute', () { + // Test temporarily turned off. + // Typeless input fields work fine (tested manually). + // Also the test passes if ran by itself with ddescribe. + // TODO(radokirov): Find out the reason why this test fails when ran + // together with the whole testsuite. + xdescribe('no type attribute', () { it('should be set "text" as default value for "type" attribute', () { _.compile(''); _.rootScope.apply(); @@ -566,8 +575,8 @@ void main() { var ngModelOptions = new NgModelOptions(); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, ngElement, i.createChild([new Module()]), - nodeAttrs, new Animate()); + var model = new NgModel(scope, ngElement, dirInjector, + nodeAttrs, new Animate(), null); dom.querySelector('body').append(element); var input = new InputTextLike(element, model, scope, ngModelOptions); @@ -777,8 +786,8 @@ void main() { var ngModelOptions = new NgModelOptions(); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, ngElement, i.createChild([new Module()]), - nodeAttrs, new Animate()); + var model = new NgModel(scope, ngElement, dirInjector, + nodeAttrs, new Animate(), null); dom.querySelector('body').append(element); var input = new InputTextLike(element, model, scope, ngModelOptions); @@ -1096,6 +1105,60 @@ void main() { }); }); + describe('type="color"', () { + it('should update input value from model', () { + _.compile(''); + _.rootScope.apply(); + + expect((_.rootElement as dom.InputElement).value).toEqual('#000000'); + + _.rootScope.apply('model = "#123456"'); + expect((_.rootElement as dom.InputElement).value).toEqual('#123456'); + }); + + it('should render as #000000 on default and when a null value is present', () { + _.compile(''); + _.rootScope.apply(); + + expect((_.rootElement as dom.InputElement).value).toEqual('#000000'); + + _.rootScope.apply('model = null'); + expect((_.rootElement as dom.InputElement).value).toEqual('#000000'); + }); + + it('should update model from the input value', () { + _.compile(''); + Probe probe = _.rootScope.context['p']; + var ngModel = probe.directive(NgModel); + InputElement inputElement = probe.element; + + inputElement.value = '#000000'; + _.triggerEvent(inputElement, 'change'); + expect(_.rootScope.context['model']).toEqual('#000000'); + + inputElement.value = '#ffffff'; + var input = probe.directive(InputTextLike); + input.processValue(); + expect(_.rootScope.context['model']).toEqual('#ffffff'); + }); + + it('should only render the input value upon the next digest', (Scope scope) { + _.compile(''); + Probe probe = _.rootScope.context['p']; + var ngModel = probe.directive(NgModel); + InputElement inputElement = probe.element; + + ngModel.render('#aabbcc'); + scope.context['model'] = '#aabbcc'; + + expect(inputElement.value).not.toEqual('#aabbcc'); + + scope.apply(); + + expect(inputElement.value).toEqual('#aabbcc'); + }); + }); + describe('contenteditable', () { it('should update content from model', () { _.compile('

    '); diff --git a/test/directive/ng_model_validators_spec.dart b/test/directive/ng_model_validators_spec.dart index 3aa9d54ee..93fbeede6 100644 --- a/test/directive/ng_model_validators_spec.dart +++ b/test/directive/ng_model_validators_spec.dart @@ -111,6 +111,31 @@ void main() { }); }); + describe('[type="color"]', () { + it('should validate the input field given a valid or invalid color', (RootScope scope) { + _.compile(''); + Probe probe = _.rootScope.context['i']; + var model = probe.directive(NgModel); + + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + + _.rootScope.apply(() { + _.rootScope.context['val'] = 'red'; + }); + + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + + _.rootScope.apply(() { + _.rootScope.context['val'] = '#ff0000'; + }); + + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + }); + }); + describe('[type="email"]', () { it('should validate the input field given a valid or invalid email address', (RootScope scope) { _.compile(''); @@ -627,6 +652,6 @@ void main() { expect(model.valid).toBe(true); }); - }); + }); }); } diff --git a/test/directive/ng_non_bindable_spec.dart b/test/directive/ng_non_bindable_spec.dart index a2ec3fb3d..1a2817360 100644 --- a/test/directive/ng_non_bindable_spec.dart +++ b/test/directive/ng_non_bindable_spec.dart @@ -19,7 +19,7 @@ main() { ' {{a}}' + ' ' + '

    '); - compiler([element], directives)(injector, [element]); + compiler([element], directives)(scope, injector.get(DirectiveInjector), [element]); scope.context['a'] = "one"; scope.context['b'] = "two"; scope.apply(); diff --git a/test/directive/ng_repeat_spec.dart b/test/directive/ng_repeat_spec.dart index 6f2594867..c4da255ad 100644 --- a/test/directive/ng_repeat_spec.dart +++ b/test/directive/ng_repeat_spec.dart @@ -20,12 +20,12 @@ main() { scope = rootScope; compile = (html, [scope]) { element = e(html); - var viewFactory = compiler([element], _directives); - var blockInjector = injector; + ViewFactory viewFactory = compiler([element], _directives); + Injector blockInjector = injector; if (scope != null) { - viewFactory.bind(injector)(scope); + viewFactory.bind(injector.get(DirectiveInjector))(scope); } else { - viewFactory(injector, [element]); + viewFactory(rootScope, injector.get(DirectiveInjector), [element]); } return element; }; @@ -35,7 +35,7 @@ main() { it(r'should set create a list of items', (Scope scope, Compiler compiler, Injector injector) { var element = es('
    {{item}}
    '); ViewFactory viewFactory = compiler(element, directives); - View view = viewFactory(injector, element); + View view = viewFactory(scope, injector.get(DirectiveInjector), element); scope.context['items'] = ['a', 'b']; scope.apply(); expect(element).toHaveText('ab'); @@ -50,7 +50,7 @@ main() { }); var element = es('
    {{item}}
    '); ViewFactory viewFactory = compiler(element, directives); - View view = viewFactory(injector, element); + View view = viewFactory(scope, injector.get(DirectiveInjector), element); scope.apply(); expect(element).toHaveText('ab'); }); @@ -60,7 +60,7 @@ main() { (Scope scope, Compiler compiler, Injector injector) { var element = es('
    {{item}}
    '); ViewFactory viewFactory = compiler(element, directives); - View view = viewFactory(injector, element); + View view = viewFactory(scope, injector.get(DirectiveInjector), element); scope.context['items'] = ['a', 'b'].map((i) => i); // makes an iterable scope.apply(); expect(element).toHaveText('ab'); @@ -327,7 +327,6 @@ main() { expect(element.text).toEqual('misko:0|shyam:1|frodo:2|'); }); - it(r'should expose iterator position as $first, $middle and $last when iterating over arrays', () { element = compile( @@ -401,6 +400,53 @@ main() { expect(element.text).toEqual('a|b|Xc|d|X'); }); + describe('nested watching', () { + it('should not error when the first watched item is removed', () { + element = compile( + '
      ' + '
    • ' + r' ' + '
    • ' + '
    '); + scope.context['items'] = ['misko', 'shyam', 'frodo']; + scope.apply(); + expect(element.children.length).toEqual(3); + scope.context['items'].remove('misko'); + scope.apply(); + expect(element.children.length).toEqual(2); + }); + + it('should not error when the last watched item is removed', () { + element = compile( + '
      ' + '
    • ' + r' ' + '
    • ' + '
    '); + scope.context['items'] = ['misko', 'shyam', 'frodo']; + scope.apply(); + expect(element.children.length).toEqual(3); + scope.context['items'].remove('frodo'); + scope.apply(); + expect(element.children.length).toEqual(2); + }); + + it('should not error when multiple watched items are removed at the same time', () { + element = compile( + '
      ' + '
    • ' + r' ' + '
    • ' + '
    '); + scope.context['items'] = ['misko', 'shyam', 'frodo', 'igor']; + scope.apply(); + expect(element.children.length).toEqual(4); + scope.context['items'].remove('shyam'); + scope.context['items'].remove('frodo'); + scope.apply(); + expect(element.children.length).toEqual(2); + }); + }); describe('stability', () { var a, b, c, d, lis; @@ -420,6 +466,14 @@ main() { lis = element.querySelectorAll('li'); }); + it(r'should correctly update rows orders - gh1154', () { + scope.context['items'] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + scope.apply(); + expect(element).toHaveText('0123456789'); + scope.context['items'] = [1, 2, 6, 7, 4, 3, 5, 8, 9, 0]; + scope.apply(); + expect(element).toHaveText('0123456789'); + }); it(r'should preserve the order of elements', () { scope.context['items'] = [a, c, d]; @@ -486,23 +540,20 @@ main() { }); it(r'should not move blocks when elements only added or removed', - inject((Injector injector) { + (Injector injector, Scope rootScope, Compiler compiler, + DirectiveMap _directives, ExceptionHandler exceptionHandler) { var throwOnMove = new MockAnimate(); var child = injector.createChild( [new Module()..bind(Animate, toValue: throwOnMove)]); - child.invoke((Injector injector, Scope rootScope, Compiler compiler, - DirectiveMap _directives) { - exceptionHandler = injector.get(ExceptionHandler); - scope = rootScope; - compile = (html) { - element = e(html); - var viewFactory = compiler([element], _directives); - viewFactory(injector, [element]); - return element; - }; - directives = _directives; - }); + scope = rootScope; + compile = (html) { + element = e(html); + var viewFactory = compiler([element], _directives); + viewFactory(scope, child.get(DirectiveInjector), [element]); + return element; + }; + directives = _directives; element = compile( '
      ' @@ -521,6 +572,6 @@ main() { ..apply(); expect(element).toHaveText('bc'); - })); + }); }); } diff --git a/test/directive/ng_switch_spec.dart b/test/directive/ng_switch_spec.dart index e289113aa..2c26666ca 100644 --- a/test/directive/ng_switch_spec.dart +++ b/test/directive/ng_switch_spec.dart @@ -94,8 +94,7 @@ void main() { }); - it('should always display the elements that do not match a switch', - inject(() { + it('should always display the elements that do not match a switch', () { var element = _.compile( '
        ' + '
      • always
      • ' + @@ -109,7 +108,7 @@ void main() { _.rootScope.context['select'] = 1; _.rootScope.apply(); expect(element.text).toEqual('always one '); - })); + }); it('should display the elements that do not have ngSwitchWhen nor ' + @@ -137,8 +136,7 @@ void main() { it('should display the elements that do not have ngSwitchWhen nor ' + 'ngSwitchDefault at the position specified in the template when the ' + - 'first and last elements in the ngSwitch have a ngSwitch* directive', - inject(() { + 'first and last elements in the ngSwitch have a ngSwitch* directive', () { var element = _.compile( '
          ' + '
        • 2
        • ' + @@ -153,7 +151,7 @@ void main() { _.rootScope.context['select'] = 1; _.rootScope.apply(); expect(element.text).toEqual('236'); - })); + }); it('should call change on switch', () { diff --git a/test/formatter/currency_spec.dart b/test/formatter/currency_spec.dart index a488c107f..f747959fa 100644 --- a/test/formatter/currency_spec.dart +++ b/test/formatter/currency_spec.dart @@ -8,7 +8,7 @@ void main() { var currency; beforeEach((FormatterMap map, Injector injector) { - currency = injector.get(map[new Formatter(name: 'currency')]); + currency = injector.get(map['currency']); }); diff --git a/test/formatter/date_spec.dart b/test/formatter/date_spec.dart index 124dcb2e4..895532d84 100644 --- a/test/formatter/date_spec.dart +++ b/test/formatter/date_spec.dart @@ -13,7 +13,7 @@ void main() { var date; beforeEach((FormatterMap map, Injector injector) { - date = injector.get(map[new Formatter(name: 'date')]); + date = injector.get(map['date']); }); it('should ignore falsy inputs', () { @@ -47,7 +47,13 @@ void main() { }); it('should accept various locales', async(() { - expect(Intl.withLocale('de', () => date(noon, "medium"))).toEqual('3. Sep 2010 12:05:08'); + // Angular's pubspec for intl spans versions with breaking changes. This + // is ok for Angular, because they are not breaking changes for us (e.g. + // date formatting changes would be breaking changes, but CLDR updates are + // not.) The tests below use SV and FR which are mostly stable across + // different versions of the intl package (as opposed to DE which received + // CLDR updates.) + expect(Intl.withLocale('sv', () => date(noon, "medium"))).toEqual('3 sep 2010 12:05:08'); expect(Intl.withLocale('fr', () => date(noon, "medium"))).toEqual('3 sept. 2010 12:05:08'); })); }); diff --git a/test/formatter/filter_spec.dart b/test/formatter/filter_spec.dart index 0c614912a..dfc168979 100644 --- a/test/formatter/filter_spec.dart +++ b/test/formatter/filter_spec.dart @@ -52,7 +52,7 @@ main() { var filter; beforeEach((Injector injector, FormatterMap filterMap) { - filter = injector.get(filterMap[new Formatter(name: 'filter')]); + filter = injector.get(filterMap['filter']); }); it('should formatter by string', () { diff --git a/test/formatter/json_spec.dart b/test/formatter/json_spec.dart index c3c62597c..5dff8886b 100644 --- a/test/formatter/json_spec.dart +++ b/test/formatter/json_spec.dart @@ -4,9 +4,9 @@ import '../_specs.dart'; void main() { describe('json', () { - it('should convert primitives, array, map to json', inject((Scope scope, Parser parser, FormatterMap formatters) { + it('should convert primitives, array, map to json', (Scope scope, Parser parser, FormatterMap formatters) { scope.context['foo'] = [{"string":'foo', "number": 123, "bool": false}]; expect(parser('foo | json').eval(scope.context, formatters)).toEqual('[{"string":"foo","number":123,"bool":false}]'); - })); + }); }); } diff --git a/test/formatter/lowercase_spec.dart b/test/formatter/lowercase_spec.dart index 03d465b90..2370cae08 100644 --- a/test/formatter/lowercase_spec.dart +++ b/test/formatter/lowercase_spec.dart @@ -4,9 +4,9 @@ import '../_specs.dart'; void main() { describe('lowercase', () { - it('should convert string to lowercase', inject((Parser parse, FormatterMap formatters) { + it('should convert string to lowercase', (Parser parse, FormatterMap formatters) { expect(parse('null | lowercase').eval(null, formatters)).toEqual(null); expect(parse('"FOO" | lowercase').eval(null, formatters)).toEqual('foo'); - })); + }); }); } diff --git a/test/formatter/number_spec.dart b/test/formatter/number_spec.dart index cf4889bdd..782418724 100644 --- a/test/formatter/number_spec.dart +++ b/test/formatter/number_spec.dart @@ -8,7 +8,7 @@ void main() { var number; beforeEach((FormatterMap map, Injector injector) { - number = injector.get(map[new Formatter(name: 'number')]); + number = injector.get(map['number']); }); diff --git a/test/introspection_spec.dart b/test/introspection_spec.dart index d8ff35c74..a034d7073 100644 --- a/test/introspection_spec.dart +++ b/test/introspection_spec.dart @@ -10,8 +10,8 @@ void main() { it('should retrieve ElementProbe', (TestBed _) { _.compile('
          '); ElementProbe probe = ngProbe(_.rootElement); - expect(probe.injector.parent).toBe(_.injector); - expect(ngInjector(_.rootElement).parent).toBe(_.injector); + expect(probe.injector.appInjector).toBe(_.injector); + expect(ngInjector(_.rootElement).appInjector).toBe(_.injector); expect(probe.directives[0] is NgBind).toBe(true); expect(ngDirectives(_.rootElement)[0] is NgBind).toBe(true); expect(probe.scope).toBe(_.rootScope); @@ -53,23 +53,152 @@ void main() { expect(toHtml(ngQuery(div, 'li'))).toEqual('
        • stash
        • secret
        • '); }); - // Does not work in dart2js. deboer is investigating. - it('should be available from Javascript', () { - // The probe only works if there is a directive. - var elt = e('
          '); - // Make it possible to find the element from JS - document.body.append(elt); - (applicationFactory()..element = elt).run(); + describe('getTestability', () { + for (bool elementProbeEnabled in [false, true]) { + describe('elementProbeEnabled=$elementProbeEnabled', () { + var elt; - expect(js.context['ngProbe']).toBeDefined(); - expect(js.context['ngScope']).toBeDefined(); - expect(js.context['ngInjector']).toBeDefined(); - expect(js.context['ngQuery']).toBeDefined(); + beforeEachModule((Module m) { + m.bind(CompilerConfig, toValue: + new CompilerConfig.withOptions(elementProbeEnabled: elementProbeEnabled)); + }); + beforeEach((TestBed _) { + elt = _.compile('
          '); + document.body.append(elt); + }); - // Polymer does not support accessing named elements directly (e.g. window.ngtop) - // so we need to use getElementById to support Polymer's shadow DOM polyfill. - expect(js.context['ngProbe'].apply([document.getElementById('ngtop')])).toBeDefined(); + afterEach(() { + elt.remove(); + }); + + if (elementProbeEnabled) { + it('should return a Testability object', () { + expect(getTestability(elt)).toBeDefined(); + }); + } else { + it('should throw an exception', () { + expect(() => getTestability(elt)).toThrow( + "Could not find an ElementProbe for div.  This might happen " + "either because there is no Angular directive for that node OR " + "because your application is running with ElementProbes " + "disabled (CompilerConfig.elementProbeEnabled = false)."); + }); + } + }); + } + }); + + describe('JavaScript bindings', () { + var elt, angular, ngtop; + + beforeEach(() { + elt = e('
          ' + '
          ' + '
          ' + '
          {{textMustache}}
          ' + '
          '); + // Make it possible to find the element from JS + document.body.append(elt); + (applicationFactory()..element = elt).run(); + angular = js.context['angular']; + // Polymer does not support accessing named elements directly (e.g. window.ngtop) + // so we need to use getElementById to support Polymer's shadow DOM polyfill. + ngtop = document.getElementById('ngtop'); + }); + + afterEach(() { + elt.remove(); + elt = angular = ngtop = null; + }); + + // Does not work in dart2js. deboer is investigating. + it('should be available from Javascript', () { + expect(js.context['ngProbe']).toBeDefined(); + expect(js.context['ngInjector']).toBeDefined(); + expect(js.context['ngScope']).toBeDefined(); + expect(js.context['ngQuery']).toBeDefined(); + expect(angular).toBeDefined(); + expect(angular['resumeBootstrap']).toBeDefined(); + expect(angular['getTestability']).toBeDefined(); + + expect(js.context['ngProbe'].apply([ngtop])).toBeDefined(); + }); + + // Issue #1219 + if (identical(1, 1.0) || !js.context['DART_VERSION'].toString().contains("version: 1.5.")) { + describe(r'testability', () { + + var testability; + + beforeEach(() { + testability = angular['getTestability'].apply([ngtop]); + }); + + it('should be available from Javascript', () { + expect(testability).toBeDefined(); + }); + + it('should expose allowAnimations', () { + allowAnimations(allowed) => testability['allowAnimations'].apply([allowed]); + expect(allowAnimations(false)).toEqual(true); + expect(allowAnimations(false)).toEqual(false); + expect(allowAnimations(true)).toEqual(false); + expect(allowAnimations(true)).toEqual(true); + }); + + describe('bindings', () { + it('should find exact bindings', () { + // exactMatch should fail. + var bindingNodes = testability['findBindings'].apply(['introspection', true]); + expect(bindingNodes.length).toEqual(0); + + // substring search (default) should succeed. + // exactMatch should default to false. + bindingNodes = testability['findBindings'].apply(['introspection']); + expect(bindingNodes.length).toEqual(1); + bindingNodes = testability['findBindings'].apply(['introspection', false]); + expect(bindingNodes.length).toEqual(1); + + // and so should exact search with the correct query. + bindingNodes = testability['findBindings'].apply(["'introspection FTW'", true]); + expect(bindingNodes.length).toEqual(1); + }); + + _assertBinding(String query) { + var bindingNodes = testability['findBindings'].apply([query]); + expect(bindingNodes.length).toEqual(1); + var node = bindingNodes[0]; + var probe = js.context['ngProbe'].apply([node]); + expect(probe).toBeDefined(); + var bindings = probe['bindings']; + expect(bindings['length']).toEqual(1); + expect(bindings[0].contains(query)).toBe(true); + } + + it('should find ng-bind bindings', () => _assertBinding('introspection FTW')); + it('should find attribute mustache bindings', () => _assertBinding('attrMustache')); + it('should find text mustache bindings', () => _assertBinding('textMustache')); + }); + + it('should find models', () { + // exactMatch should fail. + var modelNodes = testability['findModels'].apply(['my', true]); + expect(modelNodes.length).toEqual(0); + + // substring search (default) should succeed. + modelNodes = testability['findModels'].apply(['my']); + expect(modelNodes.length).toEqual(1); + var divElement = modelNodes[0]; + expect(divElement is DivElement).toEqual(true); + var probe = js.context['ngProbe'].apply([divElement]); + expect(probe).toBeDefined(); + var models = probe['models']; + expect(models['length']).toEqual(1); + expect(models[0]).toEqual('myModel'); + }); + }); + } }); }); } diff --git a/test/io/expression_extractor_spec.dart b/test/io/expression_extractor_spec.dart index f9ce6a0c4..d8b09da74 100644 --- a/test/io/expression_extractor_spec.dart +++ b/test/io/expression_extractor_spec.dart @@ -1,7 +1,6 @@ library ng.tool.expression_extractor_spec; import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; import 'package:angular/tools/common.dart'; import 'package:angular/tools/io.dart'; import 'package:angular/tools/io_impl.dart'; @@ -17,8 +16,7 @@ void main() { Iterable _extractExpressions(file) { Module module = new Module(); - Injector injector = new DynamicInjector(modules: [module], - allowImplicitInjection: true); + Injector injector = new ModuleInjector([module]); IoService ioService = new IoServiceImpl(); var sourceCrawler = new SourceCrawlerImpl(['packages/']); diff --git a/test/mock/test_bed_spec.dart b/test/mock/test_bed_spec.dart index a35872f5f..661c5f4ac 100644 --- a/test/mock/test_bed_spec.dart +++ b/test/mock/test_bed_spec.dart @@ -14,20 +14,17 @@ void main() { return (TestBed tb) => _ = tb; }); - it('should allow for a scope-based compile', () { + it('should allow for a scope-based compile', (Scope scope) { + Scope childScope = scope.createChild({}); - inject((Scope scope) { - Scope childScope = scope.createChild({}); + _.compile('
          ', scope: childScope); - _.compile('
          ', scope: childScope); + Probe probe = _.rootScope.context['i']; + var directiveInst = probe.directive(MyTestBedDirective); - Probe probe = _.rootScope.context['i']; - var directiveInst = probe.directive(MyTestBedDirective); + childScope.destroy(); - childScope.destroy(); - - expect(directiveInst.destroyed).toBe(true); - }); + expect(directiveInst.destroyed).toBe(true); }); }); diff --git a/test/mock/zone_spec.dart b/test/mock/zone_spec.dart index b1aaf9c83..00454cafd 100644 --- a/test/mock/zone_spec.dart +++ b/test/mock/zone_spec.dart @@ -332,6 +332,28 @@ void main() { (_) => dump("i never run")); })).toThrow('1 active timer(s) are still in the queue.'); }); + + it('should report no timers when there are none', async(() { + expect(isTimerQueueEmpty()).toBe(true); + expect(isNonPeriodicTimerQueueEmpty()).toBe(true); + expect(isPeriodicTimerQueueEmpty()).toBe(true); + })); + + it('should report remaining non-periodic timers', async(() { + new Future(() => null); + expect(isTimerQueueEmpty()).toBe(false); + expect(isNonPeriodicTimerQueueEmpty()).toBe(false); + expect(isPeriodicTimerQueueEmpty()).toBe(true); + clockTick(); + })); + + it('should report remaining periodic timers', async(() { + var t = new Timer.periodic(new Duration(seconds: 1), (_) => null); + expect(isTimerQueueEmpty()).toBe(false); + expect(isNonPeriodicTimerQueueEmpty()).toBe(true); + expect(isPeriodicTimerQueueEmpty()).toBe(false); + t.cancel(); + })); }); }); }); diff --git a/test/routing/ng_view_spec.dart b/test/routing/ng_view_spec.dart index f026b2c41..34bffa8f8 100644 --- a/test/routing/ng_view_spec.dart +++ b/test/routing/ng_view_spec.dart @@ -5,7 +5,7 @@ import '../_specs.dart'; import 'package:angular/routing/module.dart'; import 'package:angular/mock/module.dart'; -main() { +main() => describe('ngView', () { describe('Flat ngView', () { TestBed _; Router router; @@ -49,8 +49,9 @@ main() { router.route('/foo'); microLeap(); _.rootScope.apply(); + Probe probe = _.rootScope.context['p']; - expect(_.rootScope.context['p'].injector.get(RouteProvider) is NgView).toBeTruthy(); + expect(probe.injector.get(RouteProvider) is NgView).toBeTruthy(); })); @@ -98,42 +99,65 @@ main() { beforeEach((TestBed tb, Router _router, TemplateCache templates) { _ = tb; router = _router; + _.rootScope.context['flag'] = true; templates.put('library.html', new HttpResponse(200, '

          Library

          ' - '
          ')); + '
    ')); templates.put('book_list.html', new HttpResponse(200, '

    Books

    ')); templates.put('book_overview.html', new HttpResponse(200, '

    Book 1234

    ')); templates.put('book_read.html', new HttpResponse(200, '

    Read Book 1234

    ')); + templates.put('alt.html', new HttpResponse(200, 'alt')); }); it('should switch nested templates', async(() { Element root = _.compile(''); + microLeap(); _.rootScope.apply(); microLeap(); expect(root.text).toEqual(''); router.route('/library/all'); - microLeap(); + microLeap(); _.rootScope.apply(); microLeap(); expect(root.text).toEqual('LibraryBooks'); router.route('/library/1234'); - microLeap(); + microLeap(); _.rootScope.apply(); microLeap(); expect(root.text).toEqual('LibraryBook 1234'); // nothing should change here router.route('/library/1234/overview'); - microLeap(); + microLeap(); _.rootScope.apply(); microLeap(); expect(root.text).toEqual('LibraryBook 1234'); // nothing should change here router.route('/library/1234/read'); - microLeap(); + microLeap(); _.rootScope.apply(); microLeap(); expect(root.text).toEqual('LibraryRead Book 1234'); })); - }); + it('should not attempt to destroy and already destroyed childscope', async(() { + // This can happen with nested ng-views. Refer + // https://github.com/angular/angular.dart/issues/1182 + // and repro case + // https://github.com/chirayuk/sample/tree/issue_1182_leaving_a_nested_ng_view + Element root = _.compile(''); + microLeap(); _.rootScope.apply(); microLeap(); + + router.route('/library/1234'); + microLeap(); _.rootScope.apply(); microLeap(); + + expect(root.text).toEqual('LibraryBook 1234'); + + _.rootScope.context['flag'] = false; + microLeap(); _.rootScope.apply(); microLeap(); + router.route('/alt'); + microLeap(); _.rootScope.apply(); microLeap(); + + expect(root.text).toEqual('alt'); + })); + }); describe('Inline template ngView', () { TestBed _; @@ -165,7 +189,7 @@ main() { expect(root.text).toEqual('Hello'); })); }); -} +}); class FlatRouteInitializer implements Function { void call(Router router, RouteViewFactory views) { @@ -193,7 +217,10 @@ class NestedRouteInitializer implements Function { 'read': ngRoute(path: '/read', view: 'book_read.html'), 'admin': ngRoute(path: '/admin', view: 'admin.html'), }) - }) + }), + 'alt': ngRoute( + path: '/alt', + view: 'alt.html'), }); } } diff --git a/test/routing/routing_spec.dart b/test/routing/routing_spec.dart index bbc63cd31..8bb3c9538 100644 --- a/test/routing/routing_spec.dart +++ b/test/routing/routing_spec.dart @@ -16,7 +16,7 @@ main() { router = new Router(useFragment: false, windowImpl: new MockWindow()); m ..install(new AngularMockModule()) - ..bind(RouteInitializerFn, toFactory: (_) => initRoutes) + ..bind(RouteInitializerFn, toFactory: () => initRoutes, inject: []) ..bind(Router, toValue: router); }); @@ -169,6 +169,66 @@ main() { })); + it('should call preEnter callback and be able to veto', async(() { + int preEnterCount = 0; + initRouter((Router router, RouteViewFactory views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + preEnter: (RoutePreEnterEvent e) { + preEnterCount++; + e.allowEnter(new Future.value(false)); + }, + view: 'foo.html' + ), + }); + }); + + Element root = _.compile(''); + expect(root.text).toEqual(''); + + router.route('/foo'); + microLeap(); + + expect(preEnterCount).toBe(1); + expect(root.text).toEqual(''); // didn't enter. + })); + + + it('should call preLeave callback and be able to veto', async(() { + int preLeaveCount = 0; + initRouter((Router router, RouteViewFactory views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + preLeave: (RoutePreLeaveEvent e) { + preLeaveCount++; + e.allowLeave(new Future.value(false)); + }, + view: 'foo.html' + ), + }); + }); + _.injector.get(TemplateCache) + .put('foo.html', new HttpResponse(200, '

    Foo

    ')); + + Element root = _.compile(''); + expect(root.text).toEqual(''); + + router.route('/foo'); + microLeap(); + + expect(preLeaveCount).toBe(0); + expect(root.text).toEqual('Foo'); + + router.route(''); + microLeap(); + + expect(preLeaveCount).toBe(1); + expect(root.text).toEqual('Foo'); // didn't leave. + })); + + it('should call preEnter callback and load modules', async(() { int preEnterCount = 0; int modulesCount = 0; diff --git a/test/tools/transformer/static_angular_generator_spec.dart b/test/tools/transformer/static_angular_generator_spec.dart index 8346c6a34..506467c75 100644 --- a/test/tools/transformer/static_angular_generator_spec.dart +++ b/test/tools/transformer/static_angular_generator_spec.dart @@ -43,12 +43,11 @@ import 'package:angular/application_factory_static.dart'; import 'package:di/di.dart' show Module; import 'main_static_expressions.dart' as generated_static_expressions; import 'main_static_metadata.dart' as generated_static_metadata; -import 'main_static_injector.dart' as generated_static_injector; class MyModule extends Module {} main() { - var app = staticApplicationFactory(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, generated_static_expressions.setters, generated_static_expressions.symbols) + var app = staticApplicationFactory(generated_static_metadata.typeAnnotations, generated_static_expressions.getters, generated_static_expressions.setters, generated_static_expressions.symbols) .addModule(new MyModule()) .run(); } @@ -80,12 +79,11 @@ import 'package:angular/application_factory_static.dart' as ng; import 'package:di/di.dart' show Module; import 'main_static_expressions.dart' as generated_static_expressions; import 'main_static_metadata.dart' as generated_static_metadata; -import 'main_static_injector.dart' as generated_static_injector; class MyModule extends Module {} main() { - var app = ng.staticApplicationFactory(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, generated_static_expressions.setters, generated_static_expressions.symbols) + var app = ng.staticApplicationFactory(generated_static_metadata.typeAnnotations, generated_static_expressions.getters, generated_static_expressions.setters, generated_static_expressions.symbols) .addModule(new MyModule()) .run(); } diff --git a/test_e2e/animation_ng_repeat_spec.dart b/test_e2e/animation_ng_repeat_spec.dart new file mode 100644 index 000000000..51064160b --- /dev/null +++ b/test_e2e/animation_ng_repeat_spec.dart @@ -0,0 +1,91 @@ +part of angular.example.animation_spec; + +class NgRepeatAppState extends AppState { + var addBtn = element(by.buttonText("Add Thing")); + var removeBtn = element(by.buttonText("Remove Thing")); + var rows = element.all(by.repeater("outer in ctrl.items")); + var thingId = 0; // monotonically increasing. + var things = []; + + addThing() { + things.add(thingId++); + addBtn.click(); + } + + removeThing() { + if (things.length > 0) { + things.removeLast(); + } + removeBtn.click(); + } + + cell(x, y) => rows.get(x).findElements(by.tagName("li")) + .then((e) => toDartArray(e)[y].getText()); + + assertState() { + expect(rows.count()).toBe(things.length); + for (int y = 0; y < things.length; y++) { + for (int x = 0; x < things.length; x++) { + expect(cell(x, y)).toEqual("Thing ${things[y]}"); + } + } + } +} + +animation_ng_repeat_spec() { + describe('ng-repeat', () { + var S; + + beforeEach(() { + S = new NgRepeatAppState(); + S.ngRepeatBtn.click(); + }); + + it('should switch to the ng-repeat example', () { + expect(S.heading.getText()).toEqual("ng-repeat Demo"); + S.assertState(); + }); + + it('should add row', () { + S.addThing(); + S.assertState(); + S.addThing(); + S.assertState(); + S.removeThing(); + S.addThing(); + S.assertState(); + }); + + it('should remove rows', () { + S.addThing(); + S.addThing(); + S.assertState(); + + S.removeThing(); + S.assertState(); + + S.removeThing(); + S.assertState(); + }); + + it('should not remove rows that do not exist', () { + S.removeThing(); + S.assertState(); + + S.addThing(); + S.removeThing(); + S.removeThing(); + S.assertState(); + }); + + // TODO(chirayu): Disabled because this times out on Travis + SauceLabs. + xit('should add things with monotonically increasing numbers', () { + S.addThing(); + S.addThing(); S.removeThing(); S.addThing(); + S.addThing(); S.removeThing(); S.addThing(); + S.addThing(); + expect(S.things).toEqual([0, 2, 4, 5]); + S.assertState(); + }); + }); +} diff --git a/test_e2e/animation_spec.dart b/test_e2e/animation_spec.dart new file mode 100644 index 000000000..db81e65f1 --- /dev/null +++ b/test_e2e/animation_spec.dart @@ -0,0 +1,33 @@ +library angular.example.animation_spec; + +import 'dart:html'; +import 'package:js/js.dart'; +import 'package:protractor/protractor_api.dart'; + +part 'animation_ng_repeat_spec.dart'; +part 'animation_visibility_spec.dart'; + +class AppState { + var ngRepeatBtn = element(by.buttonText("ng-repeat")); + var visibilityBtn = element(by.buttonText("Visibility")); + + var heading = element(by.css(".demo h2")); +} + + +main() { + describe('animation example', () { + beforeEach(() { + protractor.getInstance().get('animation.html'); + element(by.tagName("body")).allowAnimations(false); + }); + + it('should start in about page', () { + var S = new AppState(); + expect(S.heading.getText()).toEqual("About"); + }); + + animation_ng_repeat_spec(); + animation_visibility_spec(); + }); +} diff --git a/test_e2e/animation_visibility_spec.dart b/test_e2e/animation_visibility_spec.dart new file mode 100644 index 000000000..7baef14fc --- /dev/null +++ b/test_e2e/animation_visibility_spec.dart @@ -0,0 +1,46 @@ +part of angular.example.animation_spec; + +class VisibilityAppState extends AppState { + var toggleBtn = element(by.buttonText("Toggle Visibility")); + var visibleIf = element(by.css(".visible-if")); + var visibleHide = element(by.css(".visible-hide")); + + hasClass(var element, String expectedClass) { + return element.getAttribute("class").then((_class) => + "$_class".split(" ").contains(expectedClass)); + } + + assertState({bool toggled: false}) { + expect(hasClass(visibleHide, "ng-hide")).toEqual(toggled); + expect(visibleIf.isPresent()).toEqual(toggled); + } +} + +animation_visibility_spec() { + var S; + + describe('visibility', () { + beforeEach(() { + S = new VisibilityAppState(); + S.visibilityBtn.click(); + }); + + it('should switch to the visibility example in initial state', () { + expect(S.heading.getText()).toEqual("Visibility Demo"); + expect(S.visibleHide.getText()).toEqual( + "Hello World. ng-hide will add and remove the .ng-hide class " + "from me to show and hide this view of text."); + S.assertState(toggled: false); + }); + + it('should toggle ng-hide and ng-if', () { + S.toggleBtn.click(); + S.assertState(toggled: true); + S.toggleBtn.click(); + S.assertState(toggled: false); + S.toggleBtn.click(); + S.assertState(toggled: true); + }); + + }); +} diff --git a/test_e2e/configQuery.js b/test_e2e/configQuery.js new file mode 100644 index 000000000..980e232f8 --- /dev/null +++ b/test_e2e/configQuery.js @@ -0,0 +1,72 @@ +var env = process.env, + fs = require('fs'), + path = require('path'); + + +var runningOnTravis = (env.TRAVIS !== undefined); + + +function getBaseUrl() { + if (env.NGDART_EXAMPLE_BASEURL) { + return env.NGDART_EXAMPLE_BASEURL; + } else if (env.USER == 'chirayu') { + return 'http://example.ngdart.localhost'; + } else { + // Default host:port when you run "pub serve" from the example + // subdirectory of the AngularDart repo. + return 'http://localhost:8080'; + } +} + + +function getDartiumBinary() { + var ensure = function(condition) { + if (!condition) throw "Unable to locate Dartium. Please set the DARTIUM environment variable."; + }; + + if (env.DARTIUM) { + return env.DARTIUM; + } + var platform = require('os').platform(); + var DART_SDK = env.DART_SDK; + if (DART_SDK) { + // Locate the chromium directory as a sibling of the DART_SDK + // directory. (It's there if you unpacked the full Dart distribution.) + var chromiumRoot = path.join(DART_SDK, "../chromium"); + ensure(fs.existsSync(chromiumRoot)); + var binary = path.join(chromiumRoot, + (platform == 'darwin') ? 'Chromium.app/Contents/MacOS/Chromium' : 'chrome'); + ensure(fs.existsSync(binary)); + return binary; + } + // Last resort: Try the standard location on Macs for the AngularDart team. + var binary = '/Applications/dart/chromium/Chromium.app/Contents/MacOS/Chromium'; + ensure(platform == 'darwin' && fs.existsSync(binary)); + return binary; +} + + +function getChromeOptions() { + if (!runningOnTravis) { + return {'binary': getDartiumBinary()}; + } + // In Travis, the list of browsers to test is specified as a CSV in the + // BROWSERS environment variable. + // TODO(chirayu): Parse the BROWSERS csv so we also test on Firefox. + if (env.TESTS == "vm") { + return {'binary': env.DARTIUM_BIN}; + } + if (env.TESTS == "dart2js") { + return { + 'binary': env.CHROME_BIN, + // Ref: https://github.com/travis-ci/travis-ci/issues/938 + // https://sites.google.com/a/chromium.org/chromedriver/help/chrome-doesn-t-start + 'args': ['no-sandbox=true'] + }; + } + throw new Error("Unknown Travis configuration specified by TESTS variable"); +} + + +exports.getBaseUrl = getBaseUrl; +exports.getChromeOptions = getChromeOptions; diff --git a/test_e2e/examplesConf.js b/test_e2e/examplesConf.js new file mode 100644 index 000000000..b295e0b51 --- /dev/null +++ b/test_e2e/examplesConf.js @@ -0,0 +1,55 @@ +/** + * Environment Variables affecting this config. + * -------------------------------------------- + * + * DARTIUM: The full path to the Dartium binary. + * + * NGDART_EXAMPLE_BASEURL: Overrides the default baseUrl to one of your + * choosing. (The default is http://localhost:8080 which is the + * correct if you simply run "pub serve" inside the example folder + * of the AngularDart repo.) + */ + +var configQuery = require('./configQuery.js'); + +var config = { + seleniumAddress: 'http://127.0.0.1:4444/wd/hub', + + specs: [ + 'animation_spec.dart', + 'hello_world_spec.dart', + 'todo_spec.dart' + ], + + splitTestsBetweenCapabilities: true, + + multiCapabilities: [{ + 'browserName': 'chrome', + 'chromeOptions': configQuery.getChromeOptions(), + count: 4 + }], + + baseUrl: configQuery.getBaseUrl(), + + jasmineNodeOpts: { + isVerbose: true, // display spec names. + showColors: true, // print colors to the terminal. + includeStackTrace: true, // include stack traces in failures. + defaultTimeoutInterval: 80000 // wait time in ms before failing a test. + }, +}; + +// Saucelabs case. +if (process.env.SAUCE_USERNAME != null) { + config.sauceUser = process.env.SAUCE_USERNAME; + config.sauceKey = process.env.SAUCE_ACCESS_KEY; + config.seleniumAddress = null; + + config.multiCapabilities.forEach(function(capability) { + capability['tunnel-identifier'] = process.env.TRAVIS_JOB_NUMBER; + capability['build'] = process.env.TRAVIS_BUILD_NUMBER; + capability['name'] = 'AngularDart E2E Suite'; + }); +} + +exports.config = config; diff --git a/test_e2e/hello_world_spec.dart b/test_e2e/hello_world_spec.dart new file mode 100644 index 000000000..d27f51191 --- /dev/null +++ b/test_e2e/hello_world_spec.dart @@ -0,0 +1,25 @@ +import 'dart:html'; +import 'package:js/js.dart'; +import 'package:protractor/protractor_api.dart'; + +main() { + describe('hello_world example', () { + var nameByModel, nameByBinding; + + beforeEach(() { + protractor.getInstance().get('hello_world.html'); + nameByModel = element(by.model('ctrl.name')); + nameByBinding = element(by.binding('ctrl.name')); + }); + + it('should set initial value for input element', () { + expect(nameByModel.getAttribute('value')).toEqual('world'); + }); + + it('should set mustache value to initial value of model', () { + nameByBinding = element(by.binding('ctrl.name')); + expect(nameByBinding.getText()).toEqual('Hello world!'); + }); + + }); +} diff --git a/test_e2e/pubspec.lock b/test_e2e/pubspec.lock new file mode 100644 index 000000000..af81ca797 --- /dev/null +++ b/test_e2e/pubspec.lock @@ -0,0 +1,15 @@ +# Generated by pub +# See http://pub.dartlang.org/doc/glossary.html#lockfile +packages: + browser: + description: browser + source: hosted + version: "0.10.0+2" + js: + description: js + source: hosted + version: "0.2.2" + protractor: + description: protractor + source: hosted + version: "0.0.5" diff --git a/test_e2e/pubspec.yaml b/test_e2e/pubspec.yaml new file mode 100644 index 000000000..3cb1d08a2 --- /dev/null +++ b/test_e2e/pubspec.yaml @@ -0,0 +1,10 @@ +name: angulardart-specs +version: 0.0.1 +authors: +- Chirayu Krishnappa +description: Specs for AngularDart examples. +environment: + sdk: '>=1.3.0' +dependencies: + js: '>=0.2.0 <0.3.0' + protractor: '0.0.5' diff --git a/test_e2e/todo_spec.dart b/test_e2e/todo_spec.dart new file mode 100644 index 000000000..583fabcd6 --- /dev/null +++ b/test_e2e/todo_spec.dart @@ -0,0 +1,151 @@ +import 'dart:html'; +import 'package:js/js.dart'; +import 'package:protractor/protractor_api.dart'; + + +class AppState { + var items = element.all(by.repeater('item in todo.items')); + var remaining = element(by.binding('todo.remaining')); + var total = element(by.binding('todo.items.length')); + + var markAllDoneBtn = element(by.buttonText("mark all done")); + var archiveDoneBtn = element(by.buttonText("archive done")); + var addBtn = element(by.buttonText("add")); + var clearBtn = element(by.buttonText("clear")); + + var newItemInput = element(by.model("todo.newItem.text")); + get newItemText => newItemInput.getAttribute('value'); + + todo(i) => items.get(i).getText(); + input(i) => items.get(i).findElement(by.tagName("input")); + + // Initial state. + var todos = ['Write Angular in Dart', + 'Write Dart in Angular', + 'Do something useful']; + var checks = [true, false, false]; + + get numTodos => todos.length; + get numChecked => checks.where((i) => i).length; + + assertTodos() { + expect(remaining.getText()).toEqual('${numTodos - numChecked}'); + expect(total.getText()).toEqual('${numTodos}'); + expect(items.count()).toBe(numTodos); + for (int i = 0; i < todos.length; i++) { + expect(todo(i)).toEqual(todos[i]); + expect(input(i).isSelected()).toEqual(checks[i]); + } + } + + assertNewItem([String text]) { + text = (text == null) ? '' : text; + expect(addBtn.isEnabled()).toEqual(text.length > 0); + expect(clearBtn.isEnabled()).toEqual(text.length > 0); + // input field and model value should contain the typed text. + expect(newItemText).toEqual(text); + expect(newItemInput.evaluate('todo.newItem.text')).toEqual(text); + } +} + + +main() { + describe('todo example', () { + var S; + + beforeEach(() { + protractor.getInstance().get('todo.html'); + S = new AppState(); + }); + + it('should set initial values for elements', () { + S.assertTodos(); + }); + + it('should update model when checkbox is toggled', () { + S.input(0).click(); + S.checks[0] = false; + S.assertTodos(); + + S.input(1).click(); + S.checks[1] = true; + S.assertTodos(); + }); + + it('should mark all done with a button', () { + S.markAllDoneBtn.click(); + S.checks = new List.filled(S.todos.length, true); + S.assertTodos(); + }); + + it('should archive done items', () { + S.archiveDoneBtn.click(); + // the first todo should disappear. + S.todos.removeAt(0); + S.checks = new List.filled(S.todos.length, false); + S.assertTodos(); + }); + + it('should enable/disable add and clear buttons when input is empty/has text', () { + S.assertNewItem(''); + + // type a character + S.newItemInput.sendKeys('a'); + S.assertNewItem('a'); + + // backspace + S.newItemInput.sendKeys('\x08'); // backspace + S.assertNewItem(''); + + // type a character again + S.newItemInput.sendKeys('a'); + S.assertNewItem('a'); + }); + + it('should reflect new item text changes in model', () { + expect(S.newItemText).toEqual(''); + var text = 'Typing something ...'; + S.newItemInput.sendKeys(text); + // input field and model value should contain the typed text. + expect(S.newItemText).toEqual(text); + expect(S.newItemInput.evaluate('todo.newItem.text')).toEqual(text); + S.assertTodos(); + }); + + it('should clear input with clear button', () { + S.newItemInput.sendKeys('Typing something ...'); + S.clearBtn.click(); + // input field should be clear. + expect(S.newItemText).toEqual(''); + S.assertTodos(); + }); + + // TODO(chirayu): Disabled because this times out on Travis + SauceLabs. + xit('should add a new item and clear the input field', () { + var text = 'Test using Protractor'; + S.newItemInput.sendKeys(text); + S.addBtn.click(); + S.assertNewItem(''); + S.todos.add(text); + S.checks.add(false); + S.assertTodos(); + + // This time, use the key instead of clicking the add + // button. + text = 'Pressed enter in the input field'; + S.newItemInput.sendKeys(text + '\n'); + S.addBtn.click(); + S.assertNewItem(''); + S.todos.add(text); + S.checks.add(false); + S.assertTodos(); + }); + + it('should have empty list when all items are done', () { + S.markAllDoneBtn.click(); + S.archiveDoneBtn.click(); + S.todos = S.checks = []; + S.assertTodos(); + }); + }); +} diff --git a/travis.md b/travis.md index 72ff5c7f7..789325032 100644 --- a/travis.md +++ b/travis.md @@ -1,6 +1,6 @@ -= Travis-CI Setup Instructions +# Travis-CI Setup Instructions -== Set Up Instructions +## Set Up Instructions 1. Go to https://travis-ci.org/ and 'Sign in with Github' 2. Visit https://travis-ci.org/profile to trigger a Github sync (this may take a minute) @@ -9,26 +9,23 @@ 5. Scroll down to Travis and click it 6. Click the 'Test Hook' button. -== What does it do? +## What does it do? - Every time you push to your repo, Travis will grab the changes from your repo and run a build on it. -- The build runs on Chrome and Dartium stable browsers. +- The build runs on Chrome, Firefox and Dartium browsers. +## How does it work. -== How does it work. +All scripts can be found in `.travis.yml` and `scripts/travis` folder. -All scripts can be found it `.travis.yml` and `./scripts/travis` folder. - -1. Install latest google-chrome -2. Install Dart stable or dev channel (currently only running on stable, see `.travis.yml` `matrix`) +1. Install the browsers (Chrome, Firefox and Dartium) +2. Install Dart stable or dev channel (see `CHANNEL` in the `.travis.yml` matrix) 3. Run analyzer on the code -4. Run karma on both Dartium (dart) and Chrome (dart2js) +4. Run karma 5. Generate documentation +6. Merge "presubmit" branches into master when the build is successful +## What needs to be done -== What needs to be done - -- Publish generated documentation to a URL -- If the branch is `presubmit`, then `github push upstream presubmit:master` - Collect/publish the test runs/times to some dashboard/graphing service diff --git a/webstorm-user-settings.jar b/webstorm-user-settings.jar deleted file mode 100644 index 4f507f66d..000000000 Binary files a/webstorm-user-settings.jar and /dev/null differ