Compare commits

...

95 Commits

Author SHA1 Message Date
Pete Bacon Darwin c903b76f6c docs(changelog): add 1.7.5 release notes 2018-10-04 14:59:37 +01:00
George Kalpakas f3a565872d fix(ngClass): do not break on invalid values
Previously, when an `ngClass` expression evaluated to something that was
not a string, array or object (and was truthy), an error would be thrown
while trying to call `.split()` on a non-string value. This error was
not very helpful for the user to identify the root cause of the problem.

This commit fixes it by ensuring such values are converted to string.

Fixes #16697

Closes #16699
2018-09-20 12:20:50 +03:00
Sibiraj 582b03b983 docs(*): update LTS information in README.md and docs landing page
Closes #16688
2018-09-10 21:00:30 +03:00
Pete Bacon Darwin be240b1176 docs(changelog): add 1.7.4 release notes 2018-09-07 09:57:37 +01:00
Martin Staffa 61b33543ff fix(ngAria.ngClick): preventDefault on space/enter only on non-interactive elements
Fixes #16664
Closes #16680
2018-09-06 15:59:19 +02:00
Martin Staffa 1dcba9cd88 chore(benchpress): add ngRepeat animation benchmark
Closes #13976
2018-09-06 15:46:25 +02:00
Michał Gołębiowski-Owczarek 8cd54d7794 docs(version-support-status): remove outdated info, resolve inconsistencies
Closes #16684
2018-09-06 15:42:24 +02:00
Martin Staffa 3105b2c26a fix(ngAnimate): remove prepare classes with multiple structural animations
Closes #16681
Closes #16677
2018-09-06 15:42:18 +02:00
Craig Johnson bc5a48d4a4 docs(guide): grammar correction in security guide
Closes #16683
2018-09-06 15:42:00 +02:00
George Kalpakas 2bbc7c464f refactor(ngMocks): simplify routeToRegExp by assuming path has query/hash stripped off
Closes #16672
2018-08-26 00:00:03 +03:00
George Kalpakas e85f91d582 refactor(ngMocks): clean up MockHttpExpectation 2018-08-26 00:00:01 +03:00
George Kalpakas 862a78dfd2 refactor(ngMocks): ignore query/hash when extracting path params for MockHttpExpectation 2018-08-25 23:59:59 +03:00
George Kalpakas bd772abf34 refactor(ngMocks): clean up MockHttpExpectation#params() 2018-08-25 23:59:57 +03:00
George Kalpakas b074d719ae refactor(ngRoute): do not unnecessarily return originalPath in routeToRegExp 2018-08-25 23:59:56 +03:00
George Kalpakas a43a40b778 test(ngMocks): use correct method name in $httpBackend test 2018-08-25 23:59:54 +03:00
Susisu 2ceeb739f3 fix($route): correctly extract path params if path contains question mark or hash
The `routeToRegExp()` function, introduced by 840b5f0, could not extract
path params if the path contained question mark or hash. Although these
characters would normally be encoded in the path, they are decoded by
`$location.path()`, before being passed to the RegExp returned by
`routeToRegExp()`.

`routeToRegExp()` has to be able to deal with both encoded URL and
decoded path, because it is being shared between `ngRoute` and
`ngMocks`.

This commit fixes the issue, by introducing an `isUrl` option that
allows creating an appropriate RegExp for each usecase.
2018-08-25 23:59:53 +03:00
George Kalpakas fa715abf45 chore(doc-gen): upgrade dgeni-packages to 0.26.5
Related: #16671, angular/dgeni-packages#271
2018-08-23 15:08:29 +03:00
George Kalpakas f943e377e8 docs(angular.copy): fix formatting
Using `<br>` messes formatting (due to a bug in
`dgeni`/`dgeni-packages`). This started breaking in c387e0d79.

Fixes #16671
2018-08-21 11:46:06 +03:00
Martin Staffa 30084c1369 fix(ngHref): allow numbers and other objects in interpolation
Interpolated content in ngHref must be stringified before being passed to $$sanitizeUri by $sce. Before 1.7.x, the sanitization had happened on the already interpolated value inside $compile.

Closes #16652
Fixes #16626
2018-08-20 20:05:20 +02:00
Martin Staffa 668a33da34 fix(select): allow to select first option with value undefined
Previously, the value observer incorrectly assumed a value had changed even if
it was the first time it was set, which caused it to remove an option with
the value `undefined` from the internal option map.

Fixes #16653
Closes #16656
2018-08-08 09:18:10 +02:00
John Mantas 19e2347759 docs(ngRepeat): redundant "and" on line 77
Closes #16657
2018-08-07 16:21:28 +03:00
George Kalpakas f17292e1b5 docs(guide/migration): fix typos, format inline code 2018-08-06 15:52:32 +03:00
Martin Staffa 77b4330011 docs(select): remove solved known issue
The issue in question has been resolved some time in 2017.
The bug report is still open, but the behavior has changed:
https://bugzilla.mozilla.org/show_bug.cgi?id=126379

Let's hope they have tests for this!

Related #9134
2018-08-03 18:20:53 +02:00
Martin Staffa 7717de4c51 docs(CHANGELOG.md): add changes for 1.7.3 2018-08-03 13:35:40 +02:00
Jason Bedard e68697e2e3 fix($location): fix infinite recursion/digest on URLs with special characters
Some characters are treated differently by `$location` compared to `$browser` and
the native browser. When comparing URLs across these two services this must be
taken into account.

Fixes #16592
Closes #16611
2018-08-03 13:12:28 +02:00
Martin Staffa a02ed88279 docs($compile): add docs for ngProp and ngOn bindings
The docs are written as if ngProp and ngOn were regular directives,
which makes them easier to find.

Closes #16627
2018-08-03 12:28:27 +02:00
Jason Bedard a5914c94a8 feat($compile): add support for arbitrary DOM property and event bindings
Properties:

Previously only arbitrary DOM attribute bindings were supported via interpolation such as
`my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of
distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM
properties. For example `ng-prop-foo="x"` will assign the value of the expression `x` to the
`foo` property, and re-assign whenever the expression `x` changes.

Events:

Previously only a distinct set of DOM events could be bound using directives such as `ng-click`,
`ng-blur` etc. `ng-on-*` adds support for binding expressions to any DOM event. For example
`ng-on-bar="barOccured($event)"` will add a listener to the "bar" event and invoke the
`barOccured($event)` expression.

Since HTML attributes are case-insensitive, property and event names are specified in snake_case
for `ng-prop-*` and `ng-on-*`. For example, to bind property `fooBar` use `ng-prop-foo_bar`, to
listen to event `fooBar` use `ng-on-foo_bar`.

Fixes #16428
Fixes #16235
Closes #16614
2018-08-03 12:28:17 +02:00
Jason Bedard 63c9c9e8d7 refactor($compile): move img[srcset] sanitizing to helper method 2018-08-03 12:28:08 +02:00
Georgii Dolzhykov 4adbf82a84 fix(ngMock): pass failed HTTP expectations to $exceptionHandler
This was only partially fixed in f18dd2957.

Closes #16644
2018-07-31 14:36:33 +03:00
George Kalpakas 1144b1eccb fix($location): avoid unnecessary $locationChange* events due to empty hash
Fixes #16632

Closes #16636
2018-07-30 23:39:46 +03:00
George Kalpakas 8970087e58 test($location): add assertion 2018-07-30 23:39:45 +03:00
George Kalpakas 131e62a819 refactor($browser): correctly export helper used in specs
The helper is used in `fakeWindow.location.hash`. ATM, no test is using
the `hash` getter, so there were no errors.
2018-07-30 23:39:44 +03:00
George Kalpakas 2fad638237 refactor($location): remove unnecessary capturing group in RegExp 2018-07-30 23:39:44 +03:00
George Kalpakas a0940895a2 refactor($location): minor changes (unused deps, exported globals, unused deps, etc) 2018-07-30 23:39:43 +03:00
Martin Staffa 0a1db2ad5f fix(Angular): add workaround for Safari / Webdriver problem
Closes #16645
2018-07-28 13:50:25 +03:00
George Kalpakas 4bd4246906 fix($animate): avoid memory leak with $animate.enabled(element, enabled)
When disabling/enabling animations on a specific element (via
`$animate.enabled(element, enabled)`), the element is added in a map to
track its state. Previously, the element was never removed from the map,
causing AngularJS to hold on to the element even after it is removed
from the DOM, thus preventing it from being garbage collected.

This commit fixes it by removing the element from the map on `$destroy`.

Fixes #16637.

Closes #16649
2018-07-27 20:48:38 +03:00
Martin Staffa 05ac702bc7 fix($compile): use correct parent element when requiring on html element
Fixes #16535
Closes #16647
2018-07-27 17:11:16 +02:00
Martin Staffa 6882113bc1 fix(ngEventDirs): pass error in handler to $exceptionHandler when event was triggered in a digest
This ensures that the error handling is the same for events triggered inside and outside a digest.
2018-07-25 13:39:33 +02:00
Mark Gardner 535ee32a0b fix(ngEventDirs): don't wrap the event handler in $apply if already in $digest
Digest cycle already in progress error can inadvertently be caused when triggering an
element's click event while within an active digest cycle. This is due to the ngEventsDirs
event handler always calling $rootScope.$apply regardless of the status of $rootScope.$$phase.
Checking the phase and calling the function immediately if within an active digest cycle
will prevent the problem without reducing current functionality.

Closes #14673
Closes #14674
2018-07-25 13:39:29 +02:00
George Kalpakas 7cf4a2933c fix(angular.element): do not break on cleanData() if _data() returns undefined
This shouldn't happen in supported jQuery versions (2+), but if someone
uses the unsupported 1.x version the app will break. The change that
causes this new behavior was introduced in b7d396b8b.

Even though jQuery 1.x is not supported, it is worth avoiding the
unnecessary breakage (given how simple).

Fixes #16641

Closes #16642
2018-07-23 14:22:11 +03:00
George Kalpakas 7dd6c87eec docs(browserTrigger): document eventData.data property 2018-07-22 19:09:50 +03:00
George Kalpakas 17f963c5d8 docs($route): fix typo (inluding --> including) 2018-07-22 18:45:49 +03:00
Jason Bedard ba09ba5344 refactor($location): move repeated path normalization code into helper method (#16618)
Closes #16618
2018-07-22 18:01:03 +03:00
George Kalpakas 8c36a43e91 docs(ngMock/$interval.flush): fix param type (not optional)
Closes #16640
2018-07-22 17:33:16 +03:00
George Kalpakas af14d67b84 chore(saucelabs): upgrade sauce-connect to latest version
Closes #16639
2018-07-21 13:40:35 +03:00
George Kalpakas c8acff1cdc chore(karma): upgrade karma and related dependencies to latest versions 2018-07-21 13:40:34 +03:00
George Kalpakas 876e9842a2 docs(ngMocks): fix type for $flushPendingTasks/$verifyPendingsTasks
Closes #16638
2018-07-21 10:47:01 +03:00
George Kalpakas 5cb9465093 docs(ngMock/$timeout): deprecate flush() and verifyNoPendingTasks()
Closes #16603
2018-07-13 13:37:22 +03:00
George Kalpakas 58f9413ad3 docs(ngMock/$timeout): recommend $verifyNoPendingTasks() when appropriate
For historical reasons, `$timeout.verifyNoPendingTasks()` throws if
there is any type of task pending (even if it is not a timeout). When
calling `$timeout.verifyNoPendingTasks()`, the user is most likely
interested in verifying pending timeouts only, which is now possible
with `$verifyNoPendingTasks('$timeout')`.

To raise awareness of `$verifyNoPendingTasks()`, it is mentioned in the
error message thrown by `$timeoutverifyNoPendingTasks()` if none of the
pending tasks is a timeout.
2018-07-13 13:37:20 +03:00
George Kalpakas 6f7674a7d0 feat(ngMock): add $flushPendingTasks() and $verifyNoPendingTasks()
`$flushPendingTasks([delay])` allows flushing all pending tasks (or up
to a specific delay). This includes `$timeout`s, `$q` promises and tasks
scheduled via `$rootScope.$applyAsync()` and `$rootScope.$evalAsync()`.
(ATM, it only flushes tasks scheduled via `$browser.defer()`, which does
not include `$http` requests and `$route` transitions.)

`$verifyNoPendingTasks([taskType])` allows verifying that there are no
pending tasks (in general or of a specific type). This includes tasks
flushed by `$flushPendingTasks()` as well as pending `$http` requests
and in-progress `$route` transitions.

Background:
`ngMock/$timeout` has `flush()` and `verifyNoPendingTasks()` methods,
but they take all kinds of tasks into account which is confusing. For
example, `$timeout.verifyNoPendingTasks()` can fail (even if there are
no pending timeouts) because of an unrelated pending `$http` request.

This behavior is retained for backwards compatibility, but the new
methods are more generic (and thus less confusing) and also allow
more fine-grained control (when appropriate).

Closes #14336
2018-07-13 13:37:17 +03:00
George Kalpakas 8dc153db75 refactor($browser): share task-tracking code between $browser and ngMock/$browser
This avoids code/logic duplication and helps the implementations stay
in-sync.
2018-07-13 13:37:15 +03:00
George Kalpakas 4a6f0996f6 refactor($interval): share code between $interval and ngMock/$interval
This avoids code/logic duplication and helps the implementations stay
in-sync.
2018-07-13 13:37:12 +03:00
George Kalpakas 522d581fc9 refactor(ngMock/$interval): more closely follow actual $interval's internal implementation 2018-07-13 13:36:38 +03:00
George Kalpakas 17b139f107 feat(*): implement more granular pending task tracking
Previously, all pending async tasks (tracked via `$browser`) are treated
the same. I.e. things like `$$testability.whenStable()` and
`ngMock#$timeout.verifyNoPendingTasks()` take all tasks into account.

Yet, in some cases we might be interested in specific tasks only. For
example, if one wants to verify there are no pending `$timeout`s, they
don't care if there are other pending tasks, such as `$http` requests.
Similarly, one might want to get notified when all `$http` requests have
completed and does not care about pending promises.

This commit adds support for more granular task tracking, by enabling
callers to specify the type of task that is being added/removed from the
queue and enabling listeners to be triggered when specific types of
tasks are completed (even if there are more pending tasks of different
types).

The change is backwards compatible. I.e. calling the affected methods
with no explicit task-type, behaves the same as before.

Related to #14336.
2018-07-13 13:35:50 +03:00
George Kalpakas 10973c3366 fix($compile): work around Firefox DocumentFragment bug
DOM nodes passed to `compilationGenerator()` will eventually be wrapped
in `jqLite`, when the compilation actually happens. In Firefox 60+,
there seems to be a `DocumentFragment`-related bug that sometimes causes
the `childNodes` to be empty at the time the compilation happens.

This commit works around this bug by eagerly wrapping `childNodes` in
`jqLite`.

NOTE:
The wrapped nodes have references to their `DocumentFragment` container.
This is "by design", since we want to be able to traverse the nodes via
`nextSibling` (in order to correctly handle multi-element directives).

Once the nodes are compiled, they will be either moved to a new
container element or the `jqLite` wrapper is release making them
eligible for garbage collection. In both cases, the original
`DocumentFragment` container should be eligible for garbage collection
too.

Fixes #16607

Closes #16615
2018-07-09 21:27:26 +03:00
Martin Staffa fc64e68076 feat($animate): add option data to event callbacks
Closes #12697
Closes #13059
2018-07-09 17:11:24 +02:00
Martin Staffa ac5e92de9b docs($animate): clarify possible options and fired events 2018-07-09 17:11:19 +02:00
Matias Niemelä 0936353e9a perf(ngAnimate): avoid repeated calls to addClass/removeClass when animation has no duration
Background:
ngAnimate writes helper classes to DOM elements to see if animations are defined on them. If many
elements have the same definition, and the same parent, we can cache the definition and skip the
application of the helper classes altogether. This helps particularly with large ngRepeat
collections.

Closes #14165
Closes #14166
Closes #16613
2018-07-05 19:46:11 +02:00
Martin Staffa ed22d2fe7b docs(changelog, guide/Migration): add info about $sce BC in 1.7
Closes #16593
Closes #16622
2018-07-05 19:46:10 +02:00
Martin Staffa a5cfa88630 docs(guide/Using Location): change / remove obsolete information 2018-07-05 19:46:09 +02:00
Martin Staffa bbf74f9994 docs(*): fix headlines 2018-07-05 19:46:09 +02:00
Atef Ben Ali 62ad450d60 docs(guide/component): add missing :
Add `:` to `Components have a well-defined lifecycle` title.

Closes #16620
2018-06-29 11:10:55 +03:00
Martin Staffa faa4b17c86 chore: fix eslint error 2018-06-23 17:57:24 +02:00
Pete Bacon Darwin 4d980a8771 chore(docs-app): ensure ToC links contain the path
Without the path the link is always pointing to the
root page, rather than the current page, which means
that copying the link address or opening the page in
a new tab is broken.

Closes #16608
2018-06-22 21:59:46 +01:00
Ilia Choly 369469b4f3 fix(grunt-utils): correctly detect java 32bit support
Closes #16605
2018-06-20 21:14:19 +03:00
Georgii Dolzhykov be417f2854 fix(ngMock/$httpBackend): correctly ignore query params in {expect,when}Route
Previously, a route definition such as
`$httpBackend.whenRoute('GET', '/route/:id')` matched against a URL with
query params, for example `/route/1?q=foo`, would incorrectly include
the query params in `id`: `{id: '1?q=foo', q: 'foo'}`.

This commit fixes it, so that the extracted `params` will now be:
`{id: '1', q: 'foo'}`.

Fixes #14173

Closes #16589
2018-06-18 19:50:10 +03:00
George Kalpakas 3a517c25f6 fix(ngAria): do not scroll when pressing spacebar on custom buttons
By default, pressing spacebar causes the browser to scroll down.
However, when a native button is focused, the button is clicked instead.

`ngAria`'s `ngClick` directive, sets elements up to behave like buttons.
For example, it adds `role="button"` and forwards `ENTER` and `SPACEBAR`
keypresses to the `click` handler (to emulate the behavior of native
buttons).

Yet, pressing spacebar on such an element, still invokes the default
browser behavior of scrolling down.

This commit fixes this, by calling `preventDefault()` on the keyboard
event, thus preventing the default scrolling behavior and making custom
buttons behave closer to native ones.

Closes #14665

Closes #16604
2018-06-18 18:49:50 +03:00
George Kalpakas 29b8dcf387 refactor(ngAria): clean up accessible actions tests 2018-06-18 18:49:21 +03:00
Martin Staffa c9d1e690aa feat(form.FormController): add $getControls()
Closes #16601
Fixes #14749
Closes #14517
Closes #13202
2018-06-18 16:05:39 +02:00
Martin Staffa a47247b5e0 docs(downloading.ngdoc): remove link to Google CDN overview page
AngularJS is no longer listed on the CDN page, because the available versions
were almost always out of date due to the need to manually update the list
2018-06-15 16:18:14 +02:00
Martin Staffa b682213d72 feat(ngModelOptions): add timeStripZeroSeconds and timeSecondsFormat
Closes #10721
Closes #16510
Closes #16584
2018-06-13 13:20:37 +02:00
George Kalpakas 223cf2b5bb docs(CHANGELOG): add release notes for 1.7.2 2018-06-12 16:34:38 +03:00
George Kalpakas c387e0d79d chore(doc-gen): error on unmatched links
Closes #16597
2018-06-12 14:45:22 +03:00
George Kalpakas cbf74d5d64 docs(*): fix dangling or ambiguous links 2018-06-12 14:45:06 +03:00
George Kalpakas a812327acd revert: refactor($compile): remove preAssignBindingsEnabled leftovers
This reverts commit 8e104ee508.

This internal clean-up turned out to break popular UI libraries (e.g.
`ngMaterial`, `ui-bootstrap`) and cause pain to developers.

Fixes #16594

Closes #16595
2018-06-11 17:09:48 +03:00
George Kalpakas 35fce310e9 docs(CHANGELOG): fix links to issues/PRs 2018-06-08 17:10:08 +03:00
George Kalpakas 93a754a490 docs(CHANGELOG): add release notes for 1.7.1 2018-06-08 16:26:22 +03:00
Georgios Kalpakas 7d9d387195 feat(ngAria): add support for ignoring a specific element
Fixes #14602
Fixes #14672

Closes #14833
2018-06-08 11:23:37 +03:00
Georgios Kalpakas feac52d840 refactor(ngAria): move test helpers inside of closure 2018-06-08 11:23:26 +03:00
Georgios Kalpakas f4f571efdf feat($route): add support for the reloadOnUrl configuration option
Enables users to specify that a particular route should not be reloaded after a
URL change (including a change in `$location.path()`), if the new URL maps to
the same route.
The default behavior is still to always load the matched route when any part of
the URL changes.

Related to #1699, #5860, #14999 (potentially closing the first two).

Fixes #7925

Closes #15002
2018-06-08 11:01:35 +03:00
Martin Staffa a8c263c194 feat(ngMessages): add support for default message
add support for showing default message when a truthy value is not matched by an ng-message directive.

Closes #12008
Closes #12213
Closes #16587
2018-06-06 17:54:59 +02:00
Martin Staffa 3d6c45d76e feat(errorHandlingConfig): add option to exclude error params from url
Specific errors, such as those during nested module loading, can create very long
error urls because the error message includes the error stack. These urls create visual
clutter in the browser console, are often not clickable, and may be rejected
by the docs page because they are simply too long.

We've already made improvements to the error display in #16283, which excludes
the error url from error parameters, which results in cleaner error messages.

Further, modern browsers restrict console message length intelligently.

This option can still be useful for older browsers like Internet Explorer, or
in general to reduce visual clutter in the console.

Closes #14744
Closes #15707
Closes #16283
Closes #16299 
Closes #16591
2018-06-06 17:54:57 +02:00
Martin Staffa bf841d3512 feat(ngRef): add directive to publish controller, or element into scope
Thanks to @drpicox for the original implementation: PR #14080 

Closes #16511
2018-06-06 17:54:52 +02:00
Christian Oliff a1d88457de docs(.editorconfig): change link to use https 2018-06-06 17:54:40 +02:00
Jakub Freisler b011ae9544 docs(ngAnimate): add "animating between value changes" section
Add a section which covers use case when users need to animate upon
a variable's value changes (not between two states).

Refers #16561

Closes #16582
2018-06-05 23:24:52 +03:00
Georgii Dolzhykov 63fdee6b9c docs($httpBackend): headers param of expect* can be function
Closes #16588
2018-06-04 15:18:34 +03:00
Jakub Freisler 257ebbb514 docs(ngAnimate): refactor of ngAnimate docs
- Synced "animation aware" directives tables in API docs and "Animation"
  guide.
- Sorted tables alphabetically.
- Added info about `ngAnimateSwap` directive.

References #16561

Closes #16581
2018-06-02 11:49:37 +03:00
George Kalpakas 2b6c986736 fix(ngModel): do not throw if view value changes on destroyed scope
This could for example happen if updating the value is debounced (either
by asynchronously calling `$setViewValue()` or via `ngModelOptions`).

Fixes #16583

Closes #16585
2018-05-31 12:31:22 +03:00
Georgii Dolzhykov 2da4950406 refactor($compile): remove preAssignBindingsEnabled leftovers
Now that we don't need to support `preAssignBindingsEnabled` (removed in #15782),
complexity introduced in `$controller` by #7645 can be removed.

One difference with the previous implementation is that all non-ES2015-class controller instances
were available on the element before calling their constructors. Now it depends on the relative
order of controllers. Controller constructors shouldn't be used to access other controllers
(e.g. via `$element.controller(directiveName)`). The recommended way is to use the `require`
property of the directive definition object and the life cycle hooks `$onChanges` or `$onInit`.

See
https://docs.angularjs.org/api/ng/service/$compile#-require-
https://docs.angularjs.org/api/ng/service/$compile#life-cycle-hooks

Closes #16580
2018-05-30 23:05:29 +03:00
George Kalpakas 789db83a8a fix($compile): support transcluding multi-element directives
Previously, transcluding multi-element directives (e.g. `foo-start`/`foo-end`)
was not supported on elements with multi-slot transclusion (a `uterdir` error
was thrown).
This commit fixes it by putting the transcluded nodes into a DocumentFragment,
where they can be traversed via `.nextSibling`.

Fixes #15554
Closes #15555
2018-05-26 17:29:46 +02:00
Martin Staffa ce443792c4 docs(ngMockE2E): add docs for $httpBackend.matchLatestDefinitionEnabled()
Closes #16577
2018-05-26 17:29:43 +02:00
Martin Staffa dd47867bfb chore: try ios 11.2 2018-05-26 11:05:27 +02:00
George Kalpakas f2ebb82ba5 refactor(ngModelOptions): fix ng-closure-runner warning
Without this fix `grunt minall` emits the following warning:
> WARNING - Parse error. Non-JSDoc comment has annotations.
> Did you mean to start it with '/**'?

Closes #16575
2018-05-24 13:02:28 +03:00
Martin Staffa 12698755be chore: set ios 11 in saucelabs to 11.3 2018-05-24 00:02:17 +02:00
Martin Staffa f7d7954904 chore: add platform to ios saucelabs 2018-05-24 00:02:13 +02:00
101 changed files with 8631 additions and 1874 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# http://editorconfig.org
# https://editorconfig.org
root = true
+230 -2
View File
@@ -1,3 +1,202 @@
<a name="1.7.5"></a>
# 1.7.5 anti-prettification (2018-10-04)
## Bug Fixes
- **ngClass:** do not break on invalid values
([f3a565](https://github.com/angular/angular.js/commit/f3a565872d802c94bb213944791b11b483d52f73),
[#16697](https://github.com/angular/angular.js/issues/16697),
[#16699](https://github.com/angular/angular.js/issues/16699))
<a name="1.7.4"></a>
# 1.7.4 interstellar-exploration (2018-09-07)
## Bug Fixes
- **ngAria.ngClick:** prevent default event on space/enter only for non-interactive elements
([61b335](https://github.com/angular/angular.js/commit/61b33543ff8e7f32464dec98a46bf0a35e9b03a4),
[#16664](https://github.com/angular/angular.js/issues/16664),
[#16680](https://github.com/angular/angular.js/issues/16680))
- **ngAnimate:** remove the "prepare" classes with multiple structural animations
([3105b2](https://github.com/angular/angular.js/commit/3105b2c26a71594c4e7904efc18f4b2e9da25b1b),
[#16681](https://github.com/angular/angular.js/issues/16681),
[#16677](https://github.com/angular/angular.js/issues/16677))
- **$route:** correctly extract path params if the path contains a question mark or a hash
([2ceeb7](https://github.com/angular/angular.js/commit/2ceeb739f35e01fcebcabac4beeeb7684ae9f86d))
- **ngHref:** allow numbers and other objects in interpolation
([30084c](https://github.com/angular/angular.js/commit/30084c13699c814ff6703d7aa2d3947a9b2f7067),
[#16652](https://github.com/angular/angular.js/issues/16652),
[#16626](https://github.com/angular/angular.js/issues/16626))
- **select:** allow to select first option with value `undefined`
([668a33](https://github.com/angular/angular.js/commit/668a33da3439f17e61dfa8f6d9b114ebde8c9d87),
[#16653](https://github.com/angular/angular.js/issues/16653),
[#16656](https://github.com/angular/angular.js/issues/16656))
<a name="1.7.3"></a>
# 1.7.3 eventful-proposal (2018-08-03)
## Bug Fixes
- **$location:**
- fix infinite recursion/digest on URLs with special characters
([e68697](https://github.com/angular/angular.js/commit/e68697e2e30695f509e6c2c1e43c2c02b7af41f0),
[#16592](https://github.com/angular/angular.js/issues/16592),
[#16611](https://github.com/angular/angular.js/issues/16611))
- avoid unnecessary `$locationChange*` events due to empty hash
([1144b1](https://github.com/angular/angular.js/commit/1144b1eccb886ea0e4a80bcb07d38a305c3263b4),
[#16632](https://github.com/angular/angular.js/issues/16632),
[#16636](https://github.com/angular/angular.js/issues/16636))
- **ngMock.$httpBackend:**
- pass failed HTTP expectations to `$exceptionHandler`
([4adbf8](https://github.com/angular/angular.js/commit/4adbf82a84a564a8d3f0982c17a64c6163200bcd),
[#16644](https://github.com/angular/angular.js/issues/16644))
- correctly ignore query params in {expect,when}Route
([be417f](https://github.com/angular/angular.js/commit/be417f28549e184fbc3c7f74251ac21fca965ae8),
[#14173](https://github.com/angular/angular.js/issues/14173),
[#16589](https://github.com/angular/angular.js/issues/16589))
- **Angular:** add workaround for Safari / Webdriver problem
([0a1db2](https://github.com/angular/angular.js/commit/0a1db2ad5f8da6902b1711a738ae4177ce9685fa),
[#16645](https://github.com/angular/angular.js/issues/16645))
- **$animate:** avoid memory leak with `$animate.enabled(element, enabled)`
([4bd424](https://github.com/angular/angular.js/commit/4bd424690612885ca06028e9b27de585edc3d3c3),
[#16649](https://github.com/angular/angular.js/issues/16649))
- **$compile:**
- use correct parent element when requiring on html element
([05ac70](https://github.com/angular/angular.js/commit/05ac702bc7edae5f89c363ea661774910735ea8b),
[#16535](https://github.com/angular/angular.js/issues/16535),
[#16647](https://github.com/angular/angular.js/issues/16647))
- work around Firefox `DocumentFragment` bug
([10973c](https://github.com/angular/angular.js/commit/10973c3366676ac8e5b2728b1e006cdef4ea197e),
[#16607](https://github.com/angular/angular.js/issues/16607),
[#16615](https://github.com/angular/angular.js/issues/16615))
- **ngEventDirs:**
- pass error in handler to $exceptionHandler when event was triggered in a digest
([688211](https://github.com/angular/angular.js/commit/6882113bc194fb10081db9bab3dd7d69dd59f311))
- don't wrap the event handler in $apply if already in $digest
([535ee3](https://github.com/angular/angular.js/commit/535ee32a0b4881c9fd526fb5e0ffc10919ba1800),
[#14673](https://github.com/angular/angular.js/issues/14673),
[#14674](https://github.com/angular/angular.js/issues/14674))
- **angular.element:** do not break on `cleanData()` if `_data()` returns undefined
([7cf4a2](https://github.com/angular/angular.js/commit/7cf4a2933cb017e45b0c97b0a836cbbd905ee31a),
[#16641](https://github.com/angular/angular.js/issues/16641),
[#16642](https://github.com/angular/angular.js/issues/16642))
- **ngAria:** do not scroll when pressing spacebar on custom buttons
([3a517c](https://github.com/angular/angular.js/commit/3a517c25f677294a7a9eca1660654a3edcc9e103),
[#14665](https://github.com/angular/angular.js/issues/14665),
[#16604](https://github.com/angular/angular.js/issues/16604))
## New Features
- **$compile:** add support for arbitrary DOM property and event bindings
([a5914c](https://github.com/angular/angular.js/commit/a5914c94a8fa5b1eceeab9e4e6849cbf467bc26d),
[#16428](https://github.com/angular/angular.js/issues/16428),
[#16235](https://github.com/angular/angular.js/issues/16235),
[#16614](https://github.com/angular/angular.js/issues/16614))
- **ngMock:** add `$flushPendingTasks()` and `$verifyNoPendingTasks()`
([6f7674](https://github.com/angular/angular.js/commit/6f7674a7d063d434205f75f5b861f167e8125999),
[#14336](https://github.com/angular/angular.js/issues/14336))
- **core:** implement more granular pending task tracking
([17b139](https://github.com/angular/angular.js/commit/17b139f107e5471a9351af638093a8e13a69e42a))
- **$animate:** add option data to event callbacks
([fc64e6](https://github.com/angular/angular.js/commit/fc64e6807642512b567deb52b497bd2bff570a1f),
[#12697](https://github.com/angular/angular.js/issues/12697),
[#13059](https://github.com/angular/angular.js/issues/13059))
- **form.FormController:** add $getControls()
([c9d1e6](https://github.com/angular/angular.js/commit/c9d1e690aa597283373b78e646676fa8f1ba1b4d),
[#16601](https://github.com/angular/angular.js/issues/16601),
[#14749](https://github.com/angular/angular.js/issues/14749),
[#14517](https://github.com/angular/angular.js/issues/14517),
[#13202](https://github.com/angular/angular.js/issues/13202))
- **ngModelOptions:** add `timeStripZeroSeconds` and `timeSecondsFormat`
([b68221](https://github.com/angular/angular.js/commit/b682213d72d65c996a6a31ea57b79d4c4f4e3c98),
[#10721](https://github.com/angular/angular.js/issues/10721),
[#16510](https://github.com/angular/angular.js/issues/16510),
[#16584](https://github.com/angular/angular.js/issues/16584))
## Performance Improvements
- **ngAnimate:** avoid repeated calls to addClass/removeClass when animation has no duration
([093635](https://github.com/angular/angular.js/commit/0936353e9a03f072bc3c4056888fd154a96530ef),
[#14165](https://github.com/angular/angular.js/issues/14165),
[#14166](https://github.com/angular/angular.js/issues/14166),
[#16613](https://github.com/angular/angular.js/issues/16613))
<a name="1.7.2"></a>
# 1.7.2 extreme-compatiplication (2018-06-12)
In the previous release, we removed a private, undocumented API that was no longer used by
AngularJS. It turned out that several popular UI libraries (such as
[AngularJS Material](https://material.angularjs.org/),
[UI Bootstrap](https://angular-ui.github.io/bootstrap/),
[ngDialog](http://likeastore.github.io/ngDialog/) and probably others) relied on that API.
In order to avoid unnecessary pain for developers, this release reverts the removal of the private
API and restores compatibility of the aforementioned libraries with the latest AngularJS.
## Reverts
- **$compile:** remove `preAssignBindingsEnabled` leftovers
([2da495](https://github.com/angular/angular.js/commit/2da49504065e9e2b71a7a5622e45118d8abbe87e),
[#16580](https://github.com/angular/angular.js/pull/16580),
[a81232](https://github.com/angular/angular.js/commit/a812327acda8bc890a4c4e809f0debb761c29625),
[#16595](https://github.com/angular/angular.js/pull/16595))
<a name="1.7.1"></a>
# 1.7.1 momentum-defiance (2018-06-08)
## Bug Fixes
- **$compile:** support transcluding multi-element directives
([789db8](https://github.com/angular/angular.js/commit/789db83a8ae0e2db5db13289b2c29e56093d967a),
[#15554](https://github.com/angular/angular.js/issues/15554),
[#15555](https://github.com/angular/angular.js/issues/15555))
- **ngModel:** do not throw if view value changes on destroyed scope
([2b6c98](https://github.com/angular/angular.js/commit/2b6c9867369fd3ef1ddb687af1153478ab62ee1b),
[#16583](https://github.com/angular/angular.js/issues/16583),
[#16585](https://github.com/angular/angular.js/issues/16585))
## New Features
- **$compile:** add one-way collection bindings
([f9d1ca](https://github.com/angular/angular.js/commit/f9d1ca20c38f065f15769fbe23aee5314cb58bd4),
[#14039](https://github.com/angular/angular.js/issues/14039),
[#16553](https://github.com/angular/angular.js/issues/16553),
[#15874](https://github.com/angular/angular.js/issues/15874))
- **ngRef:** add directive to publish controller, or element into scope
([bf841d](https://github.com/angular/angular.js/commit/bf841d35120bf3c4655fde46af4105c85a0f1cdc),
[#16511](https://github.com/angular/angular.js/issues/16511))
- **errorHandlingConfig:** add option to exclude error params from url
([3d6c45](https://github.com/angular/angular.js/commit/3d6c45d76e30b1b3c4eb9672cf4a93e5251c06b3),
[#14744](https://github.com/angular/angular.js/issues/14744),
[#15707](https://github.com/angular/angular.js/issues/15707),
[#16283](https://github.com/angular/angular.js/issues/16283),
[#16299](https://github.com/angular/angular.js/issues/16299),
[#16591](https://github.com/angular/angular.js/issues/16591))
- **ngAria:** add support for ignoring a specific element
([7d9d38](https://github.com/angular/angular.js/commit/7d9d387195292cb5e04984602b752d31853cfea6),
[#14602](https://github.com/angular/angular.js/issues/14602),
[#14672](https://github.com/angular/angular.js/issues/14672),
[#14833](https://github.com/angular/angular.js/issues/14833))
- **ngCookies:** support samesite option
([10a229](https://github.com/angular/angular.js/commit/10a229ce1befdeaf6295d1635dc11391c252a91a),
[#16543](https://github.com/angular/angular.js/issues/16543),
[#16544](https://github.com/angular/angular.js/issues/16544))
- **ngMessages:** add support for default message
([a8c263](https://github.com/angular/angular.js/commit/a8c263c1947cc85ee60b4732f7e4bcdc7ba463e8),
[#12008](https://github.com/angular/angular.js/issues/12008),
[#12213](https://github.com/angular/angular.js/issues/12213),
[#16587](https://github.com/angular/angular.js/issues/16587))
- **ngMock, ngMockE2E:** add option to match latest definition for `$httpBackend` request
([773f39](https://github.com/angular/angular.js/commit/773f39c9345479f5f8b6321236ce6ad96f77aa92),
[#16251](https://github.com/angular/angular.js/issues/16251),
[#11637](https://github.com/angular/angular.js/issues/11637),
[#16560](https://github.com/angular/angular.js/issues/16560))
- **$route:** add support for the `reloadOnUrl` configuration option
([f4f571](https://github.com/angular/angular.js/commit/f4f571efdf86d6acbcd5c6b1de66b4b33a259125),
[#7925](https://github.com/angular/angular.js/issues/7925),
[#15002](https://github.com/angular/angular.js/issues/15002))
<a name="1.7.0"></a>
# 1.7.0 nonexistent-physiology (2018-05-11)
@@ -372,8 +571,8 @@ This in turn affects how dirty checking treats objects that prototypally
inherit from `Array` (e.g. MobX observable arrays). AngularJS will now
be able to handle these objects better when copying or watching.
### **$sce** due to:
- **[1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**: handle URL sanitization through the `$sce` service
### **$sce** :
- due to **[1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**: handle URL sanitization through the `$sce` service
If you use `attrs.$set` for URL attributes (a[href] and img[src]) there will no
longer be any automated sanitization of the value. This is in line with other
@@ -387,6 +586,35 @@ Note that values that have been passed through the `$interpolate` service within
`URL` or `MEDIA_URL` will have already been sanitized, so you would not need to sanitize
these values again.
- due to **[1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**: handle URL sanitization through the `$sce` service
binding `trustAs()` and the short versions (`trustAsResourceUrl()` et al.) to
`ngSrc`, `ngSrcset`, and `ngHref` will now raise an infinite digest error:
```js
$scope.imgThumbFn = function(id) {
return $sce.trustAsResourceUrl(someService.someUrl(id));
};
```
```html
<img ng-src="{{imgThumbFn(imgId)}}">
```
This is because the `$interpolate` service is now responsible for sanitizing
the attribute value, and its watcher receives a new object from `trustAs()`
on every digest.
To migrate, compute the trusted value only when the input value changes:
```js
$scope.$watch('imgId', function(id) {
$scope.imgThumb = $sce.trustAsResourceUrl(someService.someUrl(id));
});
```
```html
<img ng-src="{{imgThumb}}">
```
### **orderBy** due to:
- **[1d8046](https://github.com/angular/angular.js/commit/1d804645f7656d592c90216a0355b4948807f6b8)**: consider `null` and `undefined` greater than other values
+2 -2
View File
@@ -14,9 +14,9 @@ piece of cake. Best of all? It makes development fun!
--------------------
##### AngularJS will be moving to Long Term Support (LTS) mode on July 1st 2018: [Find out more](https://docs.angularjs.org/misc/version-support-status)
**On July 1, 2018 AngularJS entered a 3 year Long Term Support period:** [Find out more](https://docs.angularjs.org/misc/version-support-status)
##### Looking for the new Angular? Go here: https://github.com/angular/angular
**Looking for the new Angular? Go here:** https://github.com/angular/angular
--------------------
+6
View File
@@ -28,6 +28,7 @@ var angularFiles = {
'src/ng/httpBackend.js',
'src/ng/interpolate.js',
'src/ng/interval.js',
'src/ng/intervalFactory.js',
'src/ng/jsonpCallbacks.js',
'src/ng/locale.js',
'src/ng/location.js',
@@ -40,6 +41,7 @@ var angularFiles = {
'src/ng/sanitizeUri.js',
'src/ng/sce.js',
'src/ng/sniffer.js',
'src/ng/taskTrackerFactory.js',
'src/ng/templateRequest.js',
'src/ng/testability.js',
'src/ng/timeout.js',
@@ -74,6 +76,7 @@ var angularFiles = {
'src/ng/directive/ngNonBindable.js',
'src/ng/directive/ngOptions.js',
'src/ng/directive/ngPluralize.js',
'src/ng/directive/ngRef.js',
'src/ng/directive/ngRepeat.js',
'src/ng/directive/ngShowHide.js',
'src/ng/directive/ngStyle.js',
@@ -103,6 +106,7 @@ var angularFiles = {
'src/ngAnimate/animateJs.js',
'src/ngAnimate/animateJsDriver.js',
'src/ngAnimate/animateQueue.js',
'src/ngAnimate/animateCache.js',
'src/ngAnimate/animation.js',
'src/ngAnimate/ngAnimateSwap.js',
'src/ngAnimate/module.js'
@@ -130,6 +134,7 @@ var angularFiles = {
],
'ngRoute': [
'src/shallowCopy.js',
'src/routeToRegExp.js',
'src/ngRoute/route.js',
'src/ngRoute/routeParams.js',
'src/ngRoute/directive/ngView.js'
@@ -139,6 +144,7 @@ var angularFiles = {
'src/ngSanitize/filter/linky.js'
],
'ngMock': [
'src/routeToRegExp.js',
'src/ngMock/angular-mocks.js',
'src/ngMock/browserTrigger.js'
],
@@ -0,0 +1,9 @@
'use strict';
angular.module('repeatAnimateBenchmark', ['ngAnimate'])
.config(function($animateProvider) {
$animateProvider.classNameFilter(/animate-/);
})
.run(function($rootScope) {
$rootScope.fileType = 'classfilter';
});
@@ -0,0 +1,6 @@
'use strict';
angular.module('repeatAnimateBenchmark', [])
.run(function($rootScope) {
$rootScope.fileType = 'noanimate';
});
+7
View File
@@ -0,0 +1,7 @@
'use strict';
angular.module('repeatAnimateBenchmark', ['ngAnimate'])
.run(function($rootScope) {
$rootScope.fileType = 'default';
});
+24
View File
@@ -0,0 +1,24 @@
/* eslint-env node */
'use strict';
module.exports = function(config) {
config.set({
scripts: [
{
id: 'angular',
src: '/build/angular.js'
},
{
id: 'angular-animate',
src: '/build/angular-animate.js'
},
{
id: 'app',
src: 'app.js'
},
{
src: 'common.js'
}]
});
};
+120
View File
@@ -0,0 +1,120 @@
'use strict';
(function() {
var app = angular.module('repeatAnimateBenchmark');
app.config(function($compileProvider, $animateProvider) {
if ($compileProvider.debugInfoEnabled) {
$compileProvider.debugInfoEnabled(false);
}
});
app.run(function($animate) {
if ($animate.enabled) {
$animate.enabled(true);
}
});
app.controller('DataController', function($scope, $rootScope, $animate) {
var totalRows = 500;
var totalColumns = 20;
var data = $scope.data = [];
function fillData() {
if ($animate.enabled) {
$animate.enabled($scope.benchmarkType !== 'globallyDisabled');
}
for (var i = 0; i < totalRows; i++) {
data[i] = [];
for (var j = 0; j < totalColumns; j++) {
data[i][j] = {
i: i
};
}
}
}
benchmarkSteps.push({
name: 'enter',
fn: function() {
$scope.$apply(function() {
fillData();
});
}
});
benchmarkSteps.push({
name: 'leave',
fn: function() {
$scope.$apply(function() {
data = $scope.data = [];
});
}
});
});
app.directive('disableAnimations', function($animate) {
return {
link: {
pre: function(s, e) {
$animate.enabled(e, false);
}
}
};
});
app.directive('noop', function($animate) {
return {
link: {
pre: angular.noop
}
};
});
app.directive('baseline', function($document) {
return {
restrict: 'E',
link: function($scope, $element) {
var document = $document[0];
var i, j, row, cell, comment;
var template = document.createElement('span');
template.setAttribute('ng-repeat', 'foo in foos');
template.classList.add('ng-scope');
template.appendChild(document.createElement('span'));
template.appendChild(document.createTextNode(':'));
function createList() {
for (i = 0; i < $scope.data.length; i++) {
row = document.createElement('div');
$element[0].appendChild(row);
for (j = 0; j < $scope.data[i].length; j++) {
cell = template.cloneNode(true);
row.appendChild(cell);
cell.childNodes[0].textContent = i;
cell.ng339 = 'xxx';
comment = document.createComment('ngRepeat end: bar in foo');
row.appendChild(comment);
}
comment = document.createComment('ngRepeat end: foo in foos');
$element[0].appendChild(comment);
}
}
$scope.$watch('data.length', function(newVal) {
if (newVal === 0) {
while ($element[0].firstChild) {
$element[0].removeChild($element[0].firstChild);
}
} else {
createList();
}
});
}
};
});
})();
+70
View File
@@ -0,0 +1,70 @@
<div ng-app="repeatAnimateBenchmark" ng-cloak>
<div ng-controller="DataController">
<div class="container-fluid">
<p>
Tests rendering of an ngRepeat with 500 elements.<br>
Animations can be enabled / disabled in different ways.<br>
Two tests require reloading the app with different module / app configurations.
</p>
<div><label><input type="radio" ng-model="benchmarkType" value="none">none: </label></div>
<div><label><input type="radio" ng-model="benchmarkType" value="baseline">baseline (vanilla Javascript): </label></div>
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default'" value="enabled">enabled : </label> (requires <a href="./">app.js</a>)</div>
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default' && fileType !== 'classfilter'" value="globallyDisabled">globally disabled:</label> (requires <a href="./">app.js</a> or <a href="?app=app-classfilter.js">app-classfilter.js</a>)</div>
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default'" value="disabledParentElement">disabled by $animate.enabled() on parent element: </label> (requires <a href="./">app.js</a>)</div>
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'noanimate'" value="noanimate">Without ngAnimate:</label> (requires <a href="?app=app-noanimate.js">app-noanimate.js</a>)</div>
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'classfilter'" value="disabledClassFilter">disabled by classNameFilter on element:</label> (requires <a href="?app=app-classfilter.js">app-classfilter.js</a>)</div>
<ng-switch on="benchmarkType">
<baseline ng-switch-when="baseline">
</baseline>
<div ng-switch-when="noanimate">
<div noop>
<div ng-repeat="row in data">
<span ng-repeat="column in row">
<span>{{column.i}}</span>
</span>
</div>
</div>
</div>
<div ng-switch-when="enabled">
<div noop>
<div ng-repeat="row in data">
<span ng-repeat="column in row">
<span>{{column.i}}</span>
</span>
</div>
</div>
</div>
<div ng-switch-when="globallyDisabled">
<div noop>
<div ng-repeat="row in data">
<span ng-repeat="column in row">
<span>{{column.i}}</span>
</span>
</div>
</div>
</div>
<div ng-switch-when="disabledClassFilter">
<div noop>
<div ng-repeat="row in data">
<span class="disable-animations" ng-repeat="column in row">
<span>{{column.i}}</span>
</span>
</div>
</div>
</div>
<div ng-switch-when="disabledParentElement">
<div disable-animations>
<div ng-repeat="row in data">
<span ng-repeat="column in row">
<span>{{column.i}}</span>
</span>
</div>
</div>
</div>
</ng-switch>
</div>
</div>
</div>
+5 -2
View File
@@ -91,13 +91,16 @@ directivesModule
.component('tocTree', {
template: '<ul>' +
'<li ng-repeat="item in $ctrl.items">' +
'<a ng-href="#{{item.fragment}}">{{item.title}}</a>' +
'<a ng-href="{{ $ctrl.path }}#{{item.fragment}}">{{item.title}}</a>' +
'<toc-tree ng-if="::item.children.length > 0" items="item.children"></toc-tree>' +
'</li>' +
'</ul>',
bindings: {
items: '<'
}
},
controller: ['$location', /** @this */ function($location) {
this.path = $location.path().replace(/^\/?(.+?)(\/index)?\/?$/, '$1');
}]
})
.directive('tocContainer', function() {
return {
+1
View File
@@ -148,6 +148,7 @@ module.exports = new Package('angularjs', [
.config(function(checkAnchorLinksProcessor) {
checkAnchorLinksProcessor.base = '/';
checkAnchorLinksProcessor.errorOnUnmatchedLinks = true;
// We are only interested in docs that have an area (i.e. they are pages)
checkAnchorLinksProcessor.checkDoc = function(doc) { return doc.area; };
})
+1 -1
View File
@@ -5,7 +5,7 @@
# AngularJS API Docs
<div class="alert alert-warning">
**AngularJS will be moving to Long Term Support (LTS) mode on July 1st 2018.**: [Find out more](misc/version-support-status).
**On July 1, 2018 AngularJS entered a 3 year Long Term Support period:** [Find out more](misc/version-support-status).
</div>
## Welcome to the AngularJS API docs page.
@@ -0,0 +1,13 @@
@ngdoc error
@name $compile:ctxoverride
@fullName DOM Property Security Context Override
@description
This error occurs when the security context for a property is defined via {@link ng.$compileProvider#addPropertySecurityContext addPropertySecurityContext()} multiple times under different security contexts.
For example:
```js
$compileProvider.addPropertySecurityContext("my-element", "src", $sce.MEDIA_URL);
$compileProvider.addPropertySecurityContext("my-element", "src", $sce.RESOURCE_URL); //throws
```
@@ -1,12 +1,12 @@
@ngdoc error
@name $compile:nodomevents
@fullName Interpolated Event Attributes
@fullName Event Attribute/Property Binding
@description
This error occurs when one tries to create a binding for event handler attributes like `onclick`, `onload`, `onsubmit`, etc.
This error occurs when one tries to create a binding for event handler attributes or properties like `onclick`, `onload`, `onsubmit`, etc.
There is no practical value in binding to these attributes and doing so only exposes your application to security vulnerabilities like XSS.
For these reasons binding to event handler attributes (all attributes that start with `on` and `formaction` attribute) is not supported.
There is no practical value in binding to these attributes/properties and doing so only exposes your application to security vulnerabilities like XSS.
For these reasons binding to event handler attributes and properties (`formaction` and all starting with `on`) is not supported.
An example code that would allow XSS vulnerability by evaluating user input in the window context could look like this:
@@ -17,4 +17,4 @@ An example code that would allow XSS vulnerability by evaluating user input in t
Since the `onclick` evaluates the value as JavaScript code in the window context, setting the `username` model to a value like `javascript:alert('PWND')` would result in script injection when the `div` is clicked.
Please use the `ng-*` or `ng-on-*` versions instead (such as `ng-click` or `ng-on-click` rather than `onclick`).
+17
View File
@@ -0,0 +1,17 @@
@ngdoc error
@name ngRef:noctrl
@fullName A controller for the value of `ngRefRead` could not be found on the element.
@description
This error occurs when the {@link ng.ngRef ngRef directive} specifies
a value in `ngRefRead` that cannot be resolved to a directive / component controller.
Causes for this error can be:
1. Your `ngRefRead` value has a typo.
2. You have a typo in the *registered* directive / component name.
3. The directive / component does not have a controller.
Note that `ngRefRead` takes the name of the component / directive, not the name of controller, and
also not the combination of directive and 'Controller'. For example, for a directive called 'myDirective',
the correct declaration is `<div ng-ref="$ctrl.ref" ng-ref-read="myDirective">`.
+27
View File
@@ -0,0 +1,27 @@
@ngdoc error
@name ngRef:nonassign
@fullName Non-Assignable Expression
@description
This error occurs when ngRef defines an expression that is not-assignable.
In order for ngRef to work, it must be possible to write the reference into the path defined with the expression.
For example, the following expressions are non-assignable:
```
<my-directive ng-ref="{}"></my-directive>
<my-directive ng-ref="myFn()"></my-directive>
<!-- missing attribute value is also invalid -->
<my-directive ng-ref></my-directive>
```
To resolve this error, use a path expression that is assignable:
```
<my-directive ng-ref="$ctrl.reference"></my-directive>
```
+32 -116
View File
@@ -3,7 +3,7 @@
@sortOrder 500
@description
# What does it do?
# Using the `$location` service
The `$location` service parses the URL in the browser address bar (based on [`window.location`](https://developer.mozilla.org/en/window.location)) and makes the URL available to
your application. Changes to the URL in the address bar are reflected into the `$location` service and
@@ -76,7 +76,7 @@ the current URL in the browser.
It does not cause a full page reload when the browser URL is changed. To reload the page after
changing the URL, use the lower-level API, `$window.location.href`.
# General overview of the API
## General overview of the API
The `$location` service can behave differently, depending on the configuration that was provided to
it when it was instantiated. The default configuration is suitable for many applications, for
@@ -85,7 +85,7 @@ others customizing the configuration can enable new features.
Once the `$location` service is instantiated, you can interact with it via jQuery-style getter and
setter methods that allow you to get or change the current URL in the browser.
## `$location` service configuration
### `$location` service configuration
To configure the `$location` service, retrieve the
{@link ng.$locationProvider $locationProvider} and set the parameters as follows:
@@ -113,12 +113,12 @@ To configure the `$location` service, retrieve the
Prefix used for Hashbang URLs (used in Hashbang mode or in legacy browsers in HTML5 mode).<br />
Default: `'!'`
### Example configuration
#### Example configuration
```js
$locationProvider.html5Mode(true).hashPrefix('*');
```
## Getter and setter methods
### Getter and setter methods
`$location` service provides getter methods for read-only parts of the URL (absUrl, protocol, host,
port) and getter / setter methods for url, path, search, hash:
@@ -137,7 +137,7 @@ change multiple segments in one go, chain setters like this:
$location.path('/newValue').search({key: value});
```
## Replace method
### Replace method
There is a special `replace` method which can be used to tell the $location service that the next
time the $location service is synced with the browser, the last history record should be replaced
@@ -173,7 +173,7 @@ encoded.
`/path?search=a&b=c#hash`. The segments are encoded as well.
# Hashbang and HTML5 Modes
## Hashbang and HTML5 Modes
`$location` service has two configuration modes which control the format of the URL in the browser
address bar: **Hashbang mode** (the default) and the **HTML5 mode** which is based on using the
@@ -221,7 +221,7 @@ facilitate the browser URL change and history management.
</tbody>
</table>
## Hashbang mode (default mode)
### Hashbang mode (default mode)
In this mode, `$location` uses Hashbang URLs in all browsers.
AngularJS also does not intercept and rewrite links in this mode. I.e. links work
@@ -229,7 +229,7 @@ as expected and also perform full page reloads when other parts of the url
than the hash fragment was changed.
### Example
#### Example
```js
it('should show example', function() {
@@ -255,7 +255,7 @@ it('should show example', function() {
});
```
## HTML5 mode
### HTML5 mode
In HTML5 mode, the `$location` service getters and setters interact with the browser URL address
through the HTML5 history API. This allows for use of regular URL path and search segments,
@@ -271,7 +271,7 @@ Note that in this mode, AngularJS intercepts all links (subject to the "Html lin
and updates the url in a way that never performs a full page reload.
### Example
#### Example
```js
it('should show example', function() {
@@ -320,14 +320,14 @@ it('should show example (when browser doesn\'t support HTML5 mode', function() {
});
```
### Fallback for legacy browsers
#### Fallback for legacy browsers
For browsers that support the HTML5 history API, `$location` uses the HTML5 history API to write
path and search. If the history API is not supported by a browser, `$location` supplies a Hashbang
URL. This frees you from having to worry about whether the browser viewing your app supports the
history API or not; the `$location` service makes this transparent to you.
### HTML link rewriting
#### HTML link rewriting
When you use HTML5 history API mode, you will not need special hashbang links. All you have to do
is specify regular URL links, such as: `<a href="/some?foo=bar">link</a>`
@@ -361,7 +361,7 @@ Note that [attribute name normalization](guide/directive#normalization) does not
`'internalLink'` will **not** match `'internal-link'`.
### Relative links
#### Relative links
Be sure to check all relative links, images, scripts etc. AngularJS requires you to specify the url
base in the head of your main html file (`<base href="/my-base/index.html">`) unless `html5Mode.requireBase`
@@ -374,14 +374,14 @@ will only change `$location.hash()` and not modify the url otherwise. This is us
to anchors on the same page without needing to know on which page the user currently is.
### Server side
#### Server side
Using this mode requires URL rewriting on server side, basically you have to rewrite all your links
to entry point of your application (e.g. index.html). Requiring a `<base>` tag is also important for
this case, as it allows AngularJS to differentiate between the part of the url that is the application
base and the path that should be handled by the application.
### Base href constraints
#### Base href constraints
The `$location` service is not able to function properly if the current URL is outside the URL given
as the base href. This can have subtle confusing consequences...
@@ -403,7 +403,7 @@ legacy browsers and hashbang links in modern browser:
- Modern browser will rewrite hashbang URLs to regular URLs.
- Older browsers will redirect regular URLs to hashbang URLs.
### Example
#### Example
Here you can see two `$location` instances that show the difference between **Html5 mode** and **Html5 Fallback mode**.
Note that to simulate different levels of browser support, the `$location` instances are connected to
@@ -415,7 +415,7 @@ redirect to regular / hashbang url, as this conversion happens only during parsi
In these examples we use `<base href="/base/index.html" />`. The inputs represent the address bar of the browser.
#### Browser in HTML5 mode
##### Browser in HTML5 mode
<example module="html5-mode" name="location-html5-mode">
<file name="index.html">
<div ng-controller="LocationController">
@@ -565,7 +565,7 @@ In these examples we use `<base href="/base/index.html" />`. The inputs represen
</example>
#### Browser in HTML5 Fallback mode (Hashbang mode)
##### Browser in HTML5 Fallback mode (Hashbang mode)
<example module="hashbang-mode" name="location-hashbang-mode">
<file name="index.html">
<div ng-controller="LocationController">
@@ -718,15 +718,15 @@ In these examples we use `<base href="/base/index.html" />`. The inputs represen
</example>
# Caveats
## Caveats
## Page reload navigation
### Page reload navigation
The `$location` service allows you to change only the URL; it does not allow you to reload the
page. When you need to change the URL and reload the page or navigate to a different page, please
use a lower level API, {@link ng.$window $window.location.href}.
## Using $location outside of the scope life-cycle
### Using $location outside of the scope life-cycle
`$location` knows about AngularJS's {@link ng.$rootScope.Scope scope} life-cycle. When a URL changes in
the browser it updates the `$location` and calls `$apply` so that all
@@ -738,7 +738,7 @@ propagate this change into browser and will notify all the {@link ng.$rootScope.
When you want to change the `$location` from outside AngularJS (for example, through a DOM Event or
during testing) - you must call `$apply` to propagate the changes.
## $location.path() and ! or / prefixes
### $location.path() and ! or / prefixes
A path should always begin with forward slash (`/`); the `$location.path()` setter will add the
forward slash if it is missing.
@@ -746,22 +746,17 @@ forward slash if it is missing.
Note that the `!` prefix in the hashbang mode is not part of `$location.path()`; it is actually
`hashPrefix`.
## Crawling your app
### Crawling your app
To allow indexing of your AJAX application, you have to add special meta tag in the head section of
your document:
Most modern search engines are able to crawl AJAX applications with dynamic content, provided all
included resources are available to the crawler bots.
```html
<meta name="fragment" content="!" />
```
There also exists a special
[AJAX crawling scheme](http://code.google.com/web/ajaxcrawling/docs/specification.html) developed by
Google that allows bots to crawl the static equivalent of a dynamically generated page,
but this schema has been deprecated, and support for it may vary by search engine.
This will cause crawler bot to request links with `_escaped_fragment_` param so that your server
can recognize the crawler and serve a HTML snapshots. For more information about this technique,
see [Making AJAX Applications
Crawlable](http://code.google.com/web/ajaxcrawling/docs/specification.html).
# Testing with the $location service
## Testing with the $location service
When using `$location` service during testing, you are outside of the angular's {@link
ng.$rootScope.Scope scope} life-cycle. This means it's your responsibility to call `scope.$apply()`.
@@ -784,85 +779,6 @@ describe('serviceUnderTest', function() {
});
```
# Migrating from earlier AngularJS releases
In earlier releases of AngularJS, `$location` used `hashPath` or `hashSearch` to process path and
search methods. With this release, the `$location` service processes path and search methods and
then uses the information it obtains to compose hashbang URLs (such as
`http://server.com/#!/path?search=a`), when necessary.
## Changes to your code
<table class="table">
<thead>
<tr class="head">
<th>Navigation inside the app</th>
<th>Change to</th>
</tr>
</thead>
<tbody>
<tr>
<td>$location.href = value<br />$location.hash = value<br />$location.update(value)<br
/>$location.updateHash(value)</td>
<td>$location.path(path).search(search)</td>
</tr>
<tr>
<td>$location.hashPath = path</td>
<td>$location.path(path)</td>
</tr>
<tr>
<td>$location.hashSearch = search</td>
<td>$location.search(search)</td>
</tr>
<tr class="head">
<th>Navigation outside the app</td>
<th>Use lower level API</td>
</tr>
<tr>
<td>$location.href = value<br />$location.update(value)</td>
<td>$window.location.href = value</td>
</tr>
<tr>
<td>$location[protocol | host | port | path | search]</td>
<td>$window.location[protocol | host | port | path | search]</td>
</tr>
<tr class="head">
<th>Read access</td>
<th>Change to</td>
</tr>
<tr>
<td>$location.hashPath</td>
<td>$location.path()</td>
</tr>
<tr>
<td>$location.hashSearch</td>
<td>$location.search()</td>
</tr>
<tr>
<td>$location.href<br />$location.protocol<br />$location.host<br />$location.port<br
/>$location.hash</td>
<td>$location.absUrl()<br />$location.protocol()<br />$location.host()<br />$location.port()<br
/>$location.path() + $location.search()</td>
</tr>
<tr>
<td>$location.path<br />$location.search</td>
<td>$window.location.path<br />$window.location.search</td>
</tr>
</tbody>
</table>
## Two-way binding to $location
Because `$location` uses getters/setters, you can use `ng-model-options="{ getterSetter: true }"`
@@ -884,6 +800,6 @@ angular.module('locationExample', [])
</file>
</example>
# Related API
## Related API
* {@link ng.$location `$location` API}
+13 -10
View File
@@ -222,23 +222,26 @@ triggered:
| Directive | Supported Animations |
|-------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move |
| {@link ng.directive:ngIf#animations ngIf} | enter and leave |
| {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
| {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
| {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
| {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
| {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
| {@link ngAnimate.directive:ngAnimateSwap#animations ngAnimateSwap} | enter and leave |
| {@link ng.directive:ngClass#animations ngClass / {{class&#125;&#8203;&#125;} | add and remove |
| {@link ng.directive:ngClassEven#animations ngClassEven} | add and remove |
| {@link ng.directive:ngClassOdd#animations ngClassOdd} | add and remove |
| {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) |
| {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
| {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
| {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
| {@link ng.directive:ngIf#animations ngIf} | enter and leave |
| {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
| {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
| {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) |
| {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
| {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move |
| {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
| {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
| {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
(More information can be found by visiting the documentation associated with each directive.)
For a full breakdown of the steps involved during each animation event, refer to the
{@link ng.$animate API docs}.
{@link ng.$animate `$animate` API docs}.
## How do I use animations in my own directives?
+1 -1
View File
@@ -143,7 +143,7 @@ components should follow a few simple conventions:
}
```
- **Components have a well-defined lifecycle**
- **Components have a well-defined lifecycle:**
Each component can implement "lifecycle hooks". These are methods that will be called at certain points in the life
of the component. The following hook methods can be implemented:
+146 -100
View File
@@ -25,7 +25,7 @@ Additionally, we have removed some long-deprecated modules and APIs.
The most notable changes are:
- $resource has now support for request and requestError interceptors
- `$resource` has now support for request and requestError interceptors
- Several deprecated features have been removed:
- the `$controllerProvider.allowGlobals()` flag
@@ -36,8 +36,8 @@ The most notable changes are:
- the complete `ngScenario` module
Please note that feature development (without breaking changes) has happened in parallel on the
1.6.x branch, so 1.7 doesn't contain many new features, but you may still benefit from those features
that were added (with possible BCs), bugfixes, and a few smaller performance improvements.
1.6.x branch, so 1.7 doesn't contain many new features, but you may still benefit from those
features that were added (with possible BCs), bugfixes, and a few smaller performance improvements.
<br />
@@ -48,11 +48,11 @@ Below is the full list of breaking changes:
<a name="migrate1.6to1.7-ng-directives"></a>
### Core: _Directives_
<a name="migrate1.6to1.7-ng-directives-form"></a>
#### **form**
**Due to [223de5](https://github.com/angular/angular.js/commit/223de59e988dc0cc8b4ec3a045b7c0735eba1c77)**,
forms will now set $submitted on child forms when they are submitted.
forms will now set `$submitted` on child forms when they are submitted.
For example:
```
<form name="parentform" ng-submit="$ctrl.submit()">
@@ -63,15 +63,16 @@ For example:
</form>
```
Submitting this form will set $submitted on "parentform" and "childform".
Submitting this form will set `$submitted` on "parentform" and "childform".
Previously, it was only set on "parentform".
This change was introduced because mixing form and ngForm does not create
This change was introduced because mixing `form` and `ngForm` does not create
logically separate forms, but rather something like input groups.
Therefore, child forms should inherit the submission state from their parent form.
#### **input[radio]** and **input[checkbox]**
**Due to [656c8f](https://github.com/angular/angular.js/commit/656c8fa8f23b1277cc5c214c4d0237f3393afa1e)**,
`input[radio]` and `input[checkbox]` now listen to the "change" event instead of the "click" event.
Most apps should not be affected, as "change" is automatically fired by browsers after "click"
@@ -84,10 +85,10 @@ Two scenarios might need migration:
Before this change, custom click event listeners on radio / checkbox would be called after the
input element and `ngModel` had been updated, unless they were specifically registered before
the built-in click handlers.
After this change, they are called before the input is updated, and can call event.preventDefault()
to prevent the input from updating.
After this change, they are called before the input is updated, and can call
`event.preventDefault()` to prevent the input from updating.
If an app uses a click event listener that expects ngModel to be updated when it is called, it now
If an app uses a click event listener that expects `ngModel` to be updated when it is called, it now
needs to register a change event listener instead.
- Triggering click events:
@@ -95,50 +96,52 @@ needs to register a change event listener instead.
Conventional trigger functions:
The change event might not be fired when the input element is not attached to the document. This
can happen in **tests** that compile input elements and
trigger click events on them. Depending on the browser (Chrome and Safari) and the trigger method,
the change event will not be fired when the input isn't attached to the document.
can happen in **tests** that compile input elements and trigger click events on them. Depending on
the browser (Chrome and Safari) and the trigger method, the change event will not be fired when the
input isn't attached to the document.
Before:
```js
it('should update the model', inject(function($compile, $rootScope) {
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
it('should update the model', inject(function($compile, $rootScope) {
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
expect($rootScope.checkbox).toBe(true);
});
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
expect($rootScope.checkbox).toBe(true);
});
```
With this patch, `$rootScope.checkbox` might not be true, because the click event
hasn't triggered the change event. To make the test, work append the inputElm to the app's
`$rootElement`, and the `$rootElement` to the `$document`.
With this patch, `$rootScope.checkbox` might not be true, because the click event hasn't triggered
the change event. To make the test, work append `inputElm` to the app's `$rootElement`, and the
`$rootElement` to the `$document`.
After:
```js
it('should update the model', inject(function($compile, $rootScope, $rootElement, $document) {
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
it('should update the model', inject(function($compile, $rootScope, $rootElement, $document) {
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
$rootElement.append(inputElm);
$document.append($rootElement);
$rootElement.append(inputElm);
$document.append($rootElement);
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
expect($rootScope.checkbox).toBe(true);
});
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
expect($rootScope.checkbox).toBe(true);
});
```
#### **input\[number\]**
**Due to [aa3f95](https://github.com/angular/angular.js/commit/aa3f951330ec7b10b43ea884d9b5754e296770ec)**,
`input[type=number]` with `ngModel` now validates the input for the `max`/`min` restriction against
the `ngModelController.$viewValue` instead of against the `ngModelController.$modelValue`.
This affects apps that use `$parsers` or `$formatters` to transform the input / model value.
If you rely on the $modelValue validation, you can overwrite the `min`/`max` validator from a custom directive, as seen in the following example directive definition object:
If you rely on the `$modelValue` validation, you can overwrite the `min`/`max` validator from a
custom directive, as seen in the following example directive definition object:
```
```js
{
restrict: 'A',
require: 'ngModel',
@@ -154,11 +157,11 @@ If you rely on the $modelValue validation, you can overwrite the `min`/`max` val
#### **ngModel, input**
**Due to [74b04c](https://github.com/angular/angular.js/commit/74b04c9403af4fc7df5b6420f22c9f45a3e84140)**,
*Custom* parsers that fail to parse on input types "email", "url", "number", "date", "month",
"time", "datetime-local", "week", no longer set `ngModelController.$error[inputType]`, and
the `ng-invalid-[inputType]` class. Also, custom parsers on input type "range" do no
longer set `ngModelController.$error.number` and the `ng-invalid-number` class.
the `ng-invalid-[inputType]` class. Also, custom parsers on input type "range" no longer set `ngModelController.$error.number` and the `ng-invalid-number` class.
Instead, any custom parsers on these inputs set `ngModelController.$error.parse` and
`ng-invalid-parse`. This change was made to make distinguishing errors from built-in parsers
@@ -166,6 +169,7 @@ and custom parsers easier.
#### **ngModelOptions**
**Due to [55ba44](https://github.com/angular/angular.js/commit/55ba44913e02650b56410aa9ab5eeea5d3492b68)**,
the 'default' key in 'debounce' now only debounces the default event, i.e. the event that is added
as an update trigger by the different input directives automatically.
@@ -179,24 +183,24 @@ See the following example:
Pre-1.7:
'mouseup' is also debounced by 500 milliseconds because 'default' is applied:
```
```html
ng-model-options="{
updateOn: 'default blur mouseup',
debounce: { 'default': 500, 'blur': 0 }
}
}"
```
1.7:
The pre-1.7 behavior can be re-created by setting '*' as a catch-all debounce value:
```
```html
ng-model-options="{
updateOn: 'default blur mouseup',
debounce: { '*': 500, 'blur': 0 }
}
}"
```
In contrast, when only 'default' is used, 'blur' and 'mouseup' are not debounced:
```
```html
ng-model-options="{
updateOn: 'default blur mouseup',
debounce: { 'default': 500 }
@@ -207,14 +211,15 @@ ng-model-options="{
#### **ngStyle**
**Due to [15bbd3](https://github.com/angular/angular.js/commit/15bbd3e18cd89b91f7206a06c73d40e54a8a48a0)**,
previously the use of deep watch by ng-style would trigger styles to be
re-applied when nested state changed. Now only changes to direct
properties of the watched object will trigger changes.
the use of deep-watching in `ngStyle` has changed. Previously, `ngStyle` would trigger styles to be
re-applied whenever nested state changed. Now, only changes to direct properties of the watched
object will trigger changes.
<a name="migrate1.6to1.7-ng-services"></a>
### Core: _Services_
#### **$compile**
**Due to [38f8c9](https://github.com/angular/angular.js/commit/38f8c97af74649ce224b6dd45f433cc665acfbfb)**,
@@ -238,16 +243,16 @@ migrating to AngularJS 1.7.0 shouldn't require any further action.
3. If you specified `$compileProvider.preAssignBindingsEnabled(true)` you need
to first migrate your code so that the flag can be flipped to `false`. The
instructions on how to do that are available in the "Migrating from 1.5 to 1.6"
guide:
https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6
guide: https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6
Afterwards, remove the `$compileProvider.preAssignBindingsEnabled(true)`
statement.
<hr />
**Due to [6ccbfa](https://github.com/angular/angular.js/commit/6ccbfa65d60a3dc396d0cf6da21b993ad74653fd)**,
the `xlink:href` security context for SVG's `a` and `image` elements has been lowered.
In the unlikely case that an app relied on RESOURCE_URL whitelisting for the
In the unlikely case that an app relied on `RESOURCE_URL` whitelisting for the
purpose of binding to the `xlink:href` property of SVG's `<a>` or `<image>`
elements and if the values do not pass the regular URL sanitization, they will
break.
@@ -258,37 +263,39 @@ To fix this you need to ensure that the values used for binding to the affected
`imgSrcSanitizationWhitelist` (for `<image>` elements).
<hr />
**Due to [fd4f01](https://github.com/angular/angular.js/commit/fd4f0111188b62773b99ab6eab38b4d2b5d8d727)**,
deepWatch is no longer used in in literal one-way bindings.
Previously when a literal value was passed into a directive/component via
**Due to [fd4f01](https://github.com/angular/angular.js/commit/fd4f0111188b62773b99ab6eab38b4d2b5d8d727)**,
deep-watching is no longer used in literal one-way bindings.
Previously, when a literal value was passed into a directive/component via
one-way binding it would be watched with a deep watcher.
For example, for `<my-component input="[a]">`, a new instance of the array
would be passed into the directive/component (and trigger $onChanges) not
would be passed into the directive/component (and trigger `$onChanges`) not
only if `a` changed but also if any sub property of `a` changed such as
`a.b` or `a.b.c.d.e` etc.
This also means a new but equal value for `a` would NOT trigger such a
change.
Now literal values use an input-based watch similar to other directive/component
Now, literal values use an input-based watch similar to other directive/component
one-way bindings. In this context inputs are the non-constant parts of the
literal. In the example above the input would be `a`. Changes are only
triggered when the inputs to the literal change.
literal. In the example above, the input would be `a`. Changes are only
triggered, when the inputs to the literal change.
<hr />
**Due to [1cf728](https://github.com/angular/angular.js/commit/1cf728e209a9e0016068fac2769827e8f747760e)**,
`base[href]` was added to the list of RESOURCE_URL context attributes.
`base[href]` was added to the list of `RESOURCE_URL` context attributes.
Previously, `<base href="{{ $ctrl.baseUrl }}" />` would not require `baseUrl` to
be trusted as a RESOURCE_URL. Now, `baseUrl` will be sent to `$sce`'s
RESOURCE_URL checks. By default, it will break unless `baseUrl` is of the same
be trusted as a `RESOURCE_URL`. Now, `baseUrl` will be sent to `$sce`'s
`RESOURCE_URL` checks. By default, it will break unless `baseUrl` is of the same
origin as the application document.
Refer to the
[`$sce` API docs](https://code.angularjs.org/snapshot/docs/api/ng/service/$sce)
for more info on how to trust a value in a RESOURCE_URL context.
for more info on how to trust a value in a `RESOURCE_URL` context.
Also, concatenation in trusted contexts is not allowed, which means that the
following won't work: `<base href="/something/{{ $ctrl.partialPath }}" />`.
@@ -315,10 +322,10 @@ except for the simplest of cases):
**Due to ([c2b8fa](https://github.com/angular/angular.js/commit/c2b8fab0a480204374d561d6b9b3d47347ac5570))**,
the arguments of `$watchGroup` callbacks have changed.
Previously when using `$watchGroup` the entries in `newValues` and
Previously, when using `$watchGroup`, the entries in `newValues` and
`oldValues` represented the *most recent change of each entry*.
Now the entries in `oldValues` will always equal the `newValues` of the previous
Now, the entries in `oldValues` will always equal the `newValues` of the previous
call of the listener. This means comparing the entries in `newValues` and
`oldValues` can be used to determine which individual expressions changed.
@@ -343,7 +350,7 @@ Now the `oldValue` will always equal the previous `newValue`:
Note the last call now shows `a === 2` in the `oldValues` array.
This also makes the `oldValue` of one-time watchers more clear. Previously
This also makes the `oldValue` of one-time watchers more clear. Previously,
the `oldValue` of a one-time watcher would remain `undefined` forever. For
example `$scope.$watchGroup(['a', '::b'], fn)` would previously:
@@ -367,7 +374,7 @@ Where now the `oldValue` will always equal the previous `newValue`:
#### **$interval**
**Due to [a8bef9](https://github.com/angular/angular.js/commit/a8bef95127775d83d80daa4617c33227c4b443d4)**,
`$interval.cancel() will throw an error if called with a promise that was not generated by
`$interval.cancel()` will throw an error if called with a promise that was not generated by
`$interval()`. Previously, it would silently do nothing.
Before:
@@ -393,7 +400,7 @@ $interval.cancel(promise); // Interval canceled.
#### **$timeout**
**Due to [336525](https://github.com/angular/angular.js/commit/3365256502344970f86355d3ace1cb4251ae9828)**,
`$timeout.cancel() will throw an error if called with a promise that was not generated by
`$timeout.cancel()` will throw an error if called with a promise that was not generated by
`$timeout()`. Previously, it would silently do nothing.
Before:
@@ -417,10 +424,11 @@ $timeout.cancel(promise); // Timeout canceled.
#### **$cookies**
**Due to [73c646](https://github.com/angular/angular.js/commit/73c6467f1468353215dc689c019ed83aa4993c77)**,
the `$cookieStore`service has been removed. Migrate to the $cookies service. Note that
for object values you need to use the `putObject` & `getObject` methods as
`get`/`put` will not correctly save/retrieve them.
the `$cookieStore`service has been removed. Migrate to the `$cookies` service. Note that
for object values you need to use the `putObject` & `getObject` methods, as
`get`/`put` will not correctly save/retrieve the object values.
Before:
```js
@@ -433,29 +441,31 @@ $cookieStore.remove('name');
#### **$templateRequest**
**Due to [c617d6](https://github.com/angular/angular.js/commit/c617d6dceee5b000bfceda44ced22fc16b48b18b)**,
give tpload error namespace has changed. Previously the `tpload` error was namespaced to `$compile`.
If you have code that matches errors of the form `[$compile:tpload]` it will no
longer run. You should change the code to match
`[$templateRequest:tpload]`.
the `tpload` error namespace has changed. Previously, the `tpload` error was namespaced to
`$compile`. If you have code that matches errors of the form `[$compile:tpload]` it will no longer
run. You should change the code to match `[$templateRequest:tpload]`.
<hr />
**Due to ([fb0099](https://github.com/angular/angular.js/commit/fb00991460cf69ae8bc7f1f826363d09c73c0d5e)**,
the service now returns the result of `$templateCache.put()` when making a server request to the
template. Previously it would return the content of the response directly.
This now means if you are decorating `$templateCache.put()` to manipulate the template, you will
now get this manipulated result also on the first `$templateRequest` rather than only on subsequent
calls (when the template is retrived from the cache).
In practice this should not affect any apps, as it is unlikely that they rely on the template being
`$templateRequest()` now returns the result of `$templateCache.put()` when making a server request
for a template. Previously, it would return the content of the response directly.
This means that if you are decorating `$templateCache.put()` to manipulate the template, you will
now get this manipulated result also on the first `$templateRequest()` call rather than only on
subsequent calls (when the template is retrieved from the cache).
In practice, this should not affect any apps, as it is unlikely that they rely on the template being
different in the first and subsequent calls.
#### **$animate**
**Due to [16b82c](https://github.com/angular/angular.js/commit/16b82c6afe0ab916fef1d6ca78053b00bf5ada83)**,
$animate.cancel(runner) now rejects the underlying
promise and calls the catch() handler on the runner
returned by $animate functions (enter, leave, move,
addClass, removeClass, setClass, animate).
Previously it would resolve the promise as if the animation
had ended successfully.
`$animate.cancel(runner)` now rejects the underlying promise and calls the `catch()` handler on the
runner returned by `$animate` functions (`enter`, `leave`, `move`, `addClass`, `removeClass`,
`setClass`, `animate`).
Previously, it would resolve the promise as if the animation had ended successfully.
Example:
@@ -468,7 +478,7 @@ runner.cancel();
```
Pre-1.7.0, this logs 'success', 1.7.0 and later it logs 'cancelled'.
To migrate, add a catch() handler to your animation runners.
To migrate, add a `catch()` handler to your animation runners.
#### **$controller**
@@ -479,38 +489,74 @@ has been removed. Likewise, the deprecated `$controllerProvider.allowGlobals()`
method that could enable this behavior, has been removed.
This behavior had been deprecated since AngularJS v1.3.0, because polluting the global scope
is bad. To migrate, remove the call to $controllerProvider.allowGlobals() in the config, and
register your controller via the Module API or the $controllerProvider, e.g.
is considered bad practice. To migrate, remove the call to `$controllerProvider.allowGlobals()` in
the config, and register your controller via the Module API or the `$controllerProvider`, e.g.:
```
```js
angular.module('myModule', []).controller('myController', function() {...});
// or
angular.module('myModule', []).config(function($controllerProvider) {
$controllerProvider.register('myController', function() {...});
});
```
#### **$sce**
**Due to [1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
if you use `attrs.$set` for URL attributes (a[href] and img[src]) there will no
if you use `attrs.$set` for URL attributes (`a[href]` and `img[src]`) there will no
longer be any automated sanitization of the value. This is in line with other
programmatic operations, such as writing to the innerHTML of an element.
programmatic operations, such as writing to the `innerHTML` of an element.
If you are programmatically writing URL values to attributes from untrusted
input then you must sanitize it yourself. You could write your own sanitizer or copy
input, then you must sanitize it yourself. You could write your own sanitizer or copy
the private `$$sanitizeUri` service.
Note that values that have been passed through the `$interpolate` service within the
`URL` or `MEDIA_URL` will have already been sanitized, so you would not need to sanitize
these values again.
<hr/>
**Due to [1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
binding {@link ng.$sce#trustAs trustAs()} and the short versions
({@link ng.$sce#trustAsResourceUrl trustAsResourceUrl()} et al.) to {@link ng.ngSrc},
{@link ng.ngSrcset}, and {@link ng.ngHref} will now raise an infinite digest error:
```js
$scope.imgThumbFn = function(id) {
return $sce.trustAsResourceUrl(someService.someUrl(id));
};
```
```html
<img ng-src="{{ imgThumbFn(imgId) }}" />
```
This is because {@link ng.$interpolate} is now responsible for sanitizing
the attribute value, and its watcher receives a new object from `trustAs()`
on every digest.
To migrate, compute the trusted value only when the input value changes:
```js
$scope.$watch('imgId', function(id) {
$scope.imgThumb = $sce.trustAsResourceUrl(someService.someUrl(id));
});
```
```html
<img ng-src="{{ imgThumb }}" />
```
<a name="migrate1.6to1.7-ng-filters"></a>
### Core: _Filters_
#### **orderBy**
**Due to [1d8046](https://github.com/angular/angular.js/commit/1d804645f7656d592c90216a0355b4948807f6b8)**,
when using `orderBy` to sort arrays containing `null` values, the `null` values
will be considered "greater than" all other values, except for `undefined`.
@@ -535,8 +581,9 @@ orderByFilter(['a', undefined, 'o', null, 'z']);
#### **jqLite**
**Due to [b7d396](https://github.com/angular/angular.js/commit/b7d396b8b6e8f27a1f4556d58fc903321e8d532a)**,
removeData() no longer removes event handlers.
`removeData()` no longer removes event handlers.
Before this commit `removeData()` invoked on an element removed its event
handlers as well. If you want to trigger a full cleanup of an element, change:
@@ -561,22 +608,20 @@ elem.remove();
will remove event handlers as well.
#### **Angular**
#### **Helpers**
**Due to [1daa4f](https://github.com/angular/angular.js/commit/1daa4f2231a89ee88345689f001805ffffa9e7de)**,
the helper functions `angular.lowercase` `and angular.uppercase` have been removed.
the helper functions `angular.lowercase` and `angular.uppercase` have been removed.
These functions have been deprecated since 1.5.0. They are internally
used, but should not be exposed as they contain special locale handling
(for Turkish) to maintain internal consistency regardless of user-set locale.
Developers should generally use the built-ins `toLowerCase` and `toUpperCase`
Developers should generally use the built-in methods `toLowerCase` and `toUpperCase`
or `toLocaleLowerCase` and `toLocaleUpperCase` for special cases.
Further, we generally discourage using the angular.x helpers in application code.
<hr />
**Due to [e3ece2](https://github.com/angular/angular.js/commit/e3ece2fad9e1e6d47b5f06815ff186d7e6f44948)**,
`angular.isArray()` now supports Array subclasses.
@@ -597,18 +642,19 @@ be able to handle these objects better when copying or watching.
### ngAria
**Due to [6d5ef3](https://github.com/angular/angular.js/commit/6d5ef34fc6a974cde73157ba94f9706723dd8f5b)**,
the ngAria directive no longer sets aria-* attributes on input[type="hidden"] with ngModel.
This can affect apps that test for the presence of aria attributes on hidden inputs.
`ngAria` no longer sets `aria-*` attributes on `input[type="hidden"]` with `ngModel`.
This can affect apps that test for the presence of ARIA attributes on hidden inputs.
To migrate, remove these assertions.
In actual apps, this should not have a user-facing effect, as the previous behavior
was incorrect, and the new behavior is correct for accessibility.
<a name="migrate1.6to1.7-ngResource"></a>
### ngResource
#### **$resource**
**Due to [ea0585](https://github.com/angular/angular.js/commit/ea0585773bb93fd891576e2271254a17e15f1ddd)**,
the behavior of interceptors and success/error callbacks has changed.
@@ -690,6 +736,7 @@ User.get({id: 2}, onSuccess, onError);
```
<hr />
**Due to [240a3d](https://github.com/angular/angular.js/commit/240a3ddbf12a9bb79754031be95dae4b6bd2dded)**,
`$http` will be called asynchronously from `$resource` methods
(regardless if a `request`/`requestError` interceptor has been defined).
@@ -728,7 +775,6 @@ it('...', function() {
```
<a name="migrate1.6to1.7-ngScenario"></a>
### ngScenario
@@ -736,7 +782,7 @@ it('...', function() {
the angular scenario runner end-to-end test framework has been
removed from the project and will no longer be available on npm
or bower starting with 1.7.0.
It was deprecated and removed from the documentation in 2014.
It has been deprecated and removed from the documentation since 2014.
Applications that still use it should migrate to
[Protractor](http://www.protractortest.org).
Technically, it should also be possible to continue using an
@@ -748,10 +794,10 @@ not changed. However, we do not guarantee future compatibility.
### ngTouch
**Due to [11d9ad](https://github.com/angular/angular.js/commit/11d9ad1eb25eaf5967195e424108207427835d50)**,
the `ngClick` directive from the ngTouch module has been removed, and with it the
the `ngClick` directive of the `ngTouch` module has been removed, and with it the
corresponding `$touchProvider` and `$touch` service.
If you have included ngTouch v1.5.0 or higher in your application, and have not
If you have included `ngTouch` v1.5.0 or higher in your application, and have not
changed the value of `$touchProvider.ngClickOverrideEnabled()`, or injected and used the `$touch`
service, then there are no migration steps for your code. Otherwise you must remove references to
the provider and service.
+1 -1
View File
@@ -102,7 +102,7 @@ For more information please visit {@link $http#json-vulnerability-protection JSO
Bear in mind that calling `$http.jsonp` gives the remote server (and, if the request is not secured, any Man-in-the-Middle attackers)
instant remote code execution in your application: the result of these requests is handed off
to the browser as regular `<script>` tag.
to the browser as a regular `<script>` tag.
## Strict Contextual Escaping
+1 -2
View File
@@ -3,8 +3,7 @@
@description
# Including AngularJS scripts from the Google CDN
The quickest way to get started is to point your html `<script>` tag to a
[Google CDN](https://developers.google.com/speed/libraries/#angularjs) URL.
The quickest way to get started is to point your html `<script>` tag to a Google CDN URL.
This way, you don't have to download anything or maintain a local copy.
There are two types of AngularJS script URLs you can point to, one for development and one for
+21 -28
View File
@@ -7,48 +7,41 @@
This page describes the support status of the significant versions of AngularJS.
<div class="alert alert-info">
AngularJS is planning one more significant release, version 1.7, and on July 1, 2018 it will enter a 3 year Long Term Support period.
On July 1, 2018 AngularJS entered a 3 year Long Term Support period.
</div>
### Until July 1st 2018
Any version branch not shown in the following table (e.g. 1.5.x) is no longer being developed.
<table class="dev-status table table-bordered">
<thead>
<tr><th>Version</th><th>Status</th><th>Comments</th></tr>
</thead>
<tbody>
<tr class="security"><td><span>1.2.x</span></td><td>Security patches only</td><td>Last version to provide IE 8 support</td></tr>
<tr class="stable"><td><span>1.6.x</span></td><td>Patch Releases</td><td>Minor features, bug fixes, security patches - no breaking changes</td></tr>
<tr class="current"><td><span>1.7.x</span></td><td>Active Development</td><td>1.7.0 (not yet released) will be the last release of AngularJS to contain breaking changes</td></tr>
</tbody>
</table>
### After July 1st 2018
Any version branch not shown in the following table (e.g. 1.6.x) is no longer being developed.
<table class="dev-status table table-bordered">
<thead>
<tr><th>Version</th><th>Status</th><th>Comments</th></tr>
</thead>
<tbody>
<tr class="security"><td><span>1.2.x</span></td><td>Long Term Support</td><td>Last version to provide IE 8 support</td></tr>
<tr class="stable"><td><span>1.7.x</span></td><td>Long Term Support</td><td>See [Long Term Support](#long-term-support) section below.</td></tr>
</tbody>
<thead>
<tr><th>Version</th><th>Status</th><th>Comments</th></tr>
</thead>
<tbody>
<tr class="security">
<td><span>1.2.x</span></td>
<td>Security patches only</td>
<td>Last version to provide IE 8 support</td>
</tr>
<tr class="stable">
<td><span>1.7.x</span></td>
<td>Long Term Support</td>
<td>See {@link version-support-status#long-term-support Long Term Support} section below.</td>
</tr>
</tbody>
</table>
### Long Term Support
On July 1st 2018, we will enter a Long Term Support period for AngularJS.
On July 1st 2018, AngularJS entered a Long Term Support period for AngularJS.
At this time we will focus exclusively on providing fixes to bugs that satisfy at least one of the following criteria:
We now focus exclusively on providing fixes to bugs that satisfy at least one of the following criteria:
* A security flaw is detected in the 1.7.x branch of the framework
* One of the major browsers releases a version that will cause current production applications using AngularJS 1.7.x to stop working
* The jQuery library releases a version that will cause current production applications using AngularJS 1.7.x to stop working.
AngularJS 1.2.x will get a new version if and only if a new severe security issue is discovered.
### Blog Post
You can read more about these plans in our [blog post announcement](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c).
You can read more about these plans in our [blog post announcement](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c).
+3 -1
View File
@@ -107,12 +107,14 @@ module.exports = function(config, specificOptions) {
'SL_iOS_10': {
base: 'SauceLabs',
browserName: 'iphone',
platform: 'OS X 10.12',
version: '10.3'
},
'SL_iOS_11': {
base: 'SauceLabs',
browserName: 'iphone',
version: '11'
platform: 'OS X 10.12',
version: '11.2'
},
'BS_Chrome': {
+1 -1
View File
@@ -221,7 +221,7 @@ module.exports = {
//returns the 32-bit mode force flags for java compiler if supported, this makes the build much faster
java32flags: function() {
if (process.platform === 'win32') return '';
if (shell.exec('java -version -d32 2>&1', {silent: true}).code !== 0) return '';
if (shell.exec('java -d32 -version 2>&1', {silent: true}).code !== 0) return '';
return ' -d32 -client';
},
+1 -1
View File
@@ -11,7 +11,7 @@ set -e
# 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
SC_VERSION="4.4.1"
SC_VERSION="4.4.12"
CONNECT_URL="https://saucelabs.com/downloads/sc-$SC_VERSION-linux.tar.gz"
CONNECT_DIR="/tmp/sauce-connect-$RANDOM"
CONNECT_DOWNLOAD="sc-$SC_VERSION-linux.tar.gz"
+8 -8
View File
@@ -32,8 +32,8 @@
"commitplease": "^2.7.10",
"cross-spawn": "^4.0.0",
"cz-conventional-changelog": "1.1.4",
"dgeni": "^0.4.0",
"dgeni-packages": "^0.16.4",
"dgeni": "^0.4.9",
"dgeni-packages": "^0.26.5",
"eslint-plugin-promise": "^3.6.0",
"event-stream": "~3.1.0",
"glob": "^6.0.1",
@@ -63,18 +63,18 @@
"jquery": "3.2.1",
"jquery-2.1": "npm:jquery@2.1.4",
"jquery-2.2": "npm:jquery@2.2.4",
"karma": "^2.0.0",
"karma-browserstack-launcher": "^1.2.0",
"karma-chrome-launcher": "^2.1.1",
"karma": "^2.0.4",
"karma-browserstack-launcher": "^1.3.0",
"karma-chrome-launcher": "^2.2.0",
"karma-edge-launcher": "^0.4.2",
"karma-firefox-launcher": "^1.0.1",
"karma-firefox-launcher": "^1.1.0",
"karma-ie-launcher": "^1.0.0",
"karma-jasmine": "^1.1.0",
"karma-jasmine": "^1.1.2",
"karma-junit-reporter": "^1.2.0",
"karma-safari-launcher": "^1.0.0",
"karma-sauce-launcher": "^1.2.0",
"karma-script-launcher": "^1.0.0",
"karma-spec-reporter": "^0.0.31",
"karma-spec-reporter": "^0.0.32",
"load-grunt-tasks": "^3.5.0",
"lodash": "~2.4.1",
"log4js": "^0.6.27",
+6
View File
@@ -171,9 +171,15 @@
/* ng/q.js */
"markQExceptionHandled": false,
/* sce.js */
"SCE_CONTEXTS": false,
/* ng/directive/directives.js */
"ngDirective": false,
/* ng/directive/ngEventDirs.js */
"createEventDirective": false,
/* ng/directive/input.js */
"VALID_CLASS": false,
"INVALID_CLASS": false,
+12 -6
View File
@@ -792,15 +792,16 @@ function arrayRemove(array, value) {
* * If `source` is identical to `destination` an exception will be thrown.
*
* <br />
*
* <div class="alert alert-warning">
* Only enumerable properties are taken into account. Non-enumerable properties (both on `source`
* and on `destination`) will be ignored.
* </div>
*
* @param {*} source The source that will be used to make a copy.
* Can be any type, including primitives, `null`, and `undefined`.
* @param {(Object|Array)=} destination Destination into which the source is copied. If
* provided, must be of the same type as `source`.
* @param {*} source The source that will be used to make a copy. Can be any type, including
* primitives, `null`, and `undefined`.
* @param {(Object|Array)=} destination Destination into which the source is copied. If provided,
* must be of the same type as `source`.
* @returns {*} The copy or updated `destination`, if `destination` was specified.
*
* @example
@@ -1695,8 +1696,13 @@ function angularInit(element, bootstrap) {
});
if (appElement) {
if (!isAutoBootstrapAllowed) {
window.console.error('AngularJS: disabling automatic bootstrap. <script> protocol indicates ' +
try {
window.console.error('AngularJS: disabling automatic bootstrap. <script> protocol indicates ' +
'an extension, document.location.href does not match.');
} catch (e) {
// Support: Safari 11 w/ Webdriver
// The console.error will throw and make the test fail
}
return;
}
config.strictDi = getNgAttribute(appElement, 'strict-di') !== null;
@@ -1909,7 +1915,7 @@ function bindJQuery() {
jqLite.cleanData = function(elems) {
var events;
for (var i = 0, elem; (elem = elems[i]) != null; i++) {
events = jqLite._data(elem).events;
events = (jqLite._data(elem) || {}).events;
if (events && events.$destroy) {
jqLite(elem).triggerHandler('$destroy');
}
+6
View File
@@ -28,6 +28,7 @@
ngInitDirective,
ngNonBindableDirective,
ngPluralizeDirective,
ngRefDirective,
ngRepeatDirective,
ngShowDirective,
ngStyleDirective,
@@ -69,6 +70,7 @@
$FilterProvider,
$$ForceReflowProvider,
$InterpolateProvider,
$$IntervalFactoryProvider,
$IntervalProvider,
$HttpProvider,
$HttpParamSerializerProvider,
@@ -87,6 +89,7 @@
$SceProvider,
$SceDelegateProvider,
$SnifferProvider,
$$TaskTrackerFactoryProvider,
$TemplateCacheProvider,
$TemplateRequestProvider,
$$TestabilityProvider,
@@ -194,6 +197,7 @@ function publishExternalAPI(angular) {
ngInit: ngInitDirective,
ngNonBindable: ngNonBindableDirective,
ngPluralize: ngPluralizeDirective,
ngRef: ngRefDirective,
ngRepeat: ngRepeatDirective,
ngShow: ngShowDirective,
ngStyle: ngStyleDirective,
@@ -239,6 +243,7 @@ function publishExternalAPI(angular) {
$$forceReflow: $$ForceReflowProvider,
$interpolate: $InterpolateProvider,
$interval: $IntervalProvider,
$$intervalFactory: $$IntervalFactoryProvider,
$http: $HttpProvider,
$httpParamSerializer: $HttpParamSerializerProvider,
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
@@ -254,6 +259,7 @@ function publishExternalAPI(angular) {
$sce: $SceProvider,
$sceDelegate: $SceDelegateProvider,
$sniffer: $SnifferProvider,
$$taskTrackerFactory: $$TaskTrackerFactoryProvider,
$templateCache: $TemplateCacheProvider,
$templateRequest: $TemplateRequestProvider,
$$testability: $$TestabilityProvider,
+8 -4
View File
@@ -45,11 +45,10 @@ function NgMapShim() {
}
NgMapShim.prototype = {
_idx: function(key) {
if (key === this._lastKey) {
return this._lastIndex;
if (key !== this._lastKey) {
this._lastKey = key;
this._lastIndex = this._keys.indexOf(key);
}
this._lastKey = key;
this._lastIndex = this._keys.indexOf(key);
return this._lastIndex;
},
_transformKey: function(key) {
@@ -62,6 +61,11 @@ NgMapShim.prototype = {
return this._values[idx];
}
},
has: function(key) {
key = this._transformKey(key);
var idx = this._idx(key);
return idx !== -1;
},
set: function(key, value) {
key = this._transformKey(key);
var idx = this._idx(key);
+16 -3
View File
@@ -7,7 +7,8 @@
*/
var minErrConfig = {
objectMaxDepth: 5
objectMaxDepth: 5,
urlErrorParamsEnabled: true
};
/**
@@ -30,12 +31,21 @@ var minErrConfig = {
* * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a
* non-positive or non-numeric value, removes the max depth limit.
* Default: 5
*
* * `urlErrorParamsEnabled` **{Boolean}** - Specifies wether the generated error url will
* contain the parameters of the thrown error. Disabling the parameters can be useful if the
* generated error url is very long.
*
* Default: true. When used without argument, it returns the current value.
*/
function errorHandlingConfig(config) {
if (isObject(config)) {
if (isDefined(config.objectMaxDepth)) {
minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN;
}
if (isDefined(config.urlErrorParamsEnabled) && isBoolean(config.urlErrorParamsEnabled)) {
minErrConfig.urlErrorParamsEnabled = config.urlErrorParamsEnabled;
}
} else {
return minErrConfig;
}
@@ -50,6 +60,7 @@ function isValidObjectMaxDepth(maxDepth) {
return isNumber(maxDepth) && maxDepth > 0;
}
/**
* @description
*
@@ -113,8 +124,10 @@ function minErr(module, ErrorConstructor) {
message += '\n' + url + (module ? module + '/' : '') + code;
for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
if (minErrConfig.urlErrorParamsEnabled) {
for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
}
}
return new ErrorConstructor(message);
+28 -5
View File
@@ -369,14 +369,39 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* );
* ```
*
* <div class="alert alert-warning">
* **Note**: Generally, the events that are fired correspond 1:1 to `$animate` method names,
* e.g. {@link ng.$animate#addClass addClass()} will fire `addClass`, and {@link ng.ngClass}
* will fire `addClass` if classes are added, and `removeClass` if classes are removed.
* However, there are two exceptions:
*
* <ul>
* <li>if both an {@link ng.$animate#addClass addClass()} and a
* {@link ng.$animate#removeClass removeClass()} action are performed during the same
* animation, the event fired will be `setClass`. This is true even for `ngClass`.</li>
* <li>an {@link ng.$animate#animate animate()} call that adds and removes classes will fire
* the `setClass` event, but if it either removes or adds classes,
* it will fire `animate` instead.</li>
* </ul>
*
* </div>
*
* @param {string} event the animation event that will be captured (e.g. enter, leave, move, addClass, removeClass, etc...)
* @param {DOMElement} container the container element that will capture each of the animation events that are fired on itself
* as well as among its children
* @param {Function} callback the callback function that will be fired when the listener is triggered
* @param {Function} callback the callback function that will be fired when the listener is triggered.
*
* The arguments present in the callback function are:
* * `element` - The captured DOM element that the animation was fired on.
* * `phase` - The phase of the animation. The two possible phases are **start** (when the animation starts) and **close** (when it ends).
* * `data` - an object with these properties:
* * addClass - `{string|null}` - space-separated CSS classes to add to the element
* * removeClass - `{string|null}` - space-separated CSS classes to remove from the element
* * from - `{Object|null}` - CSS properties & values at the beginning of the animation
* * to - `{Object|null}` - CSS properties & values at the end of the animation
*
* Note that the callback does not trigger a scope digest. Wrap your call into a
* {@link $rootScope.Scope#$apply scope.$apply} to propagate changes to the scope.
*/
on: $$animateQueue.on,
@@ -644,9 +669,8 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* @param {object=} options an optional collection of options/styles that will be applied to the element.
* The object can have the following properties:
*
* - **addClass** - `{string}` - space-separated CSS classes to add to element
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Runner} animationRunner the animation runner
@@ -676,7 +700,6 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
*
* - **addClass** - `{string}` - space-separated CSS classes to add to element
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Runner} the animation runner
@@ -706,8 +729,8 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* The object can have the following properties:
*
* - **addClass** - `{string}` - space-separated CSS classes to add to element
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Runner} the animation runner
+47 -64
View File
@@ -1,5 +1,14 @@
'use strict';
/* global stripHash: true */
/* global getHash: true, stripHash: false */
function getHash(url) {
var index = url.indexOf('#');
return index === -1 ? '' : url.substr(index);
}
function trimEmptyHash(url) {
return url.replace(/#$/, '');
}
/**
* ! This is a private undocumented service !
@@ -22,61 +31,27 @@
* @param {object} $log window.console or an object with the same interface.
* @param {object} $sniffer $sniffer service
*/
function Browser(window, document, $log, $sniffer) {
function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) {
var self = this,
location = window.location,
history = window.history,
setTimeout = window.setTimeout,
clearTimeout = window.clearTimeout,
pendingDeferIds = {};
pendingDeferIds = {},
taskTracker = $$taskTrackerFactory($log);
self.isMock = false;
var outstandingRequestCount = 0;
var outstandingRequestCallbacks = [];
//////////////////////////////////////////////////////////////
// Task-tracking API
//////////////////////////////////////////////////////////////
// TODO(vojta): remove this temporary api
self.$$completeOutstandingRequest = completeOutstandingRequest;
self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; };
self.$$completeOutstandingRequest = taskTracker.completeTask;
self.$$incOutstandingRequestCount = taskTracker.incTaskCount;
/**
* Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks`
* counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed.
*/
function completeOutstandingRequest(fn) {
try {
fn.apply(null, sliceArgs(arguments, 1));
} finally {
outstandingRequestCount--;
if (outstandingRequestCount === 0) {
while (outstandingRequestCallbacks.length) {
try {
outstandingRequestCallbacks.pop()();
} catch (e) {
$log.error(e);
}
}
}
}
}
function getHash(url) {
var index = url.indexOf('#');
return index === -1 ? '' : url.substr(index);
}
/**
* @private
* TODO(vojta): prefix this method with $$ ?
* @param {function()} callback Function that will be called when no outstanding request
*/
self.notifyWhenNoOutstandingRequests = function(callback) {
if (outstandingRequestCount === 0) {
callback();
} else {
outstandingRequestCallbacks.push(callback);
}
};
// TODO(vojta): prefix this method with $$ ?
self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks;
//////////////////////////////////////////////////////////////
// URL API
@@ -101,20 +76,21 @@ function Browser(window, document, $log, $sniffer) {
*
* @description
* GETTER:
* Without any argument, this method just returns current value of location.href.
* Without any argument, this method just returns current value of `location.href` (with a
* trailing `#` stripped of if the hash is empty).
*
* SETTER:
* With at least one argument, this method sets url to new value.
* If html5 history api supported, pushState/replaceState is used, otherwise
* location.href/location.replace is used.
* Returns its own instance to allow chaining
* If html5 history api supported, `pushState`/`replaceState` is used, otherwise
* `location.href`/`location.replace` is used.
* Returns its own instance to allow chaining.
*
* NOTE: this api is intended for use only by the $location service. Please use the
* NOTE: this api is intended for use only by the `$location` service. Please use the
* {@link ng.$location $location service} to change url.
*
* @param {string} url New url (when used as setter)
* @param {boolean=} replace Should new url replace current history record?
* @param {object=} state object to use with pushState/replaceState
* @param {object=} state State object to use with `pushState`/`replaceState`
*/
self.url = function(url, replace, state) {
// In modern browsers `history.state` is `null` by default; treating it separately
@@ -172,7 +148,7 @@ function Browser(window, document, $log, $sniffer) {
// - pendingLocation is needed as browsers don't allow to read out
// the new location.href if a reload happened or if there is a bug like in iOS 9 (see
// https://openradar.appspot.com/22186109).
return pendingLocation || location.href;
return trimEmptyHash(pendingLocation || location.href);
}
};
@@ -307,7 +283,8 @@ function Browser(window, document, $log, $sniffer) {
/**
* @name $browser#defer
* @param {function()} fn A function, who's execution should be deferred.
* @param {number=} [delay=0] of milliseconds to defer the function execution.
* @param {number=} [delay=0] Number of milliseconds to defer the function execution.
* @param {string=} [taskType=DEFAULT_TASK_TYPE] The type of task that is deferred.
* @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`.
*
* @description
@@ -318,14 +295,19 @@ function Browser(window, document, $log, $sniffer) {
* via `$browser.defer.flush()`.
*
*/
self.defer = function(fn, delay) {
self.defer = function(fn, delay, taskType) {
var timeoutId;
outstandingRequestCount++;
delay = delay || 0;
taskType = taskType || taskTracker.DEFAULT_TASK_TYPE;
taskTracker.incTaskCount(taskType);
timeoutId = setTimeout(function() {
delete pendingDeferIds[timeoutId];
completeOutstandingRequest(fn);
}, delay || 0);
pendingDeferIds[timeoutId] = true;
taskTracker.completeTask(fn, taskType);
}, delay);
pendingDeferIds[timeoutId] = taskType;
return timeoutId;
};
@@ -341,10 +323,11 @@ function Browser(window, document, $log, $sniffer) {
* canceled.
*/
self.defer.cancel = function(deferId) {
if (pendingDeferIds[deferId]) {
if (pendingDeferIds.hasOwnProperty(deferId)) {
var taskType = pendingDeferIds[deferId];
delete pendingDeferIds[deferId];
clearTimeout(deferId);
completeOutstandingRequest(noop);
taskTracker.completeTask(noop, taskType);
return true;
}
return false;
@@ -354,8 +337,8 @@ function Browser(window, document, $log, $sniffer) {
/** @this */
function $BrowserProvider() {
this.$get = ['$window', '$log', '$sniffer', '$document',
function($window, $log, $sniffer, $document) {
return new Browser($window, $document, $log, $sniffer);
}];
this.$get = ['$window', '$log', '$sniffer', '$document', '$$taskTrackerFactory',
function($window, $log, $sniffer, $document, $$taskTrackerFactory) {
return new Browser($window, $document, $log, $sniffer, $$taskTrackerFactory);
}];
}
+580 -85
View File
@@ -1030,8 +1030,7 @@
*
* See issue [#2573](https://github.com/angular/angular.js/issues/2573).
*
* #### `transclude: element` in the replace template root can have
* unexpected effects
* #### `transclude: element` in the replace template root can have unexpected effects
*
* When the replace template has a directive at the root node that uses
* {@link $compile#-transclude- `transclude: element`}, e.g.
@@ -1045,6 +1044,325 @@
*
*/
/**
* @ngdoc directive
* @name ngProp
* @restrict A
* @element ANY
*
* @usage
*
* ```html
* <ANY ng-prop-propname="expression">
* </ANY>
* ```
*
* or with uppercase letters in property (e.g. "propName"):
*
*
* ```html
* <ANY ng-prop-prop_name="expression">
* </ANY>
* ```
*
*
* @description
* The `ngProp` directive binds an expression to a DOM element property.
* `ngProp` allows writing to arbitrary properties by including
* the property name in the attribute, e.g. `ng-prop-value="'my value'"` binds 'my value' to
* the `value` property.
*
* Usually, it's not necessary to write to properties in AngularJS, as the built-in directives
* handle the most common use cases (instead of the above example, you would use {@link ngValue}).
*
* However, [custom elements](https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements)
* often use custom properties to hold data, and `ngProp` can be used to provide input to these
* custom elements.
*
* ## Binding to camelCase properties
*
* Since HTML attributes are case-insensitive, camelCase properties like `innerHTML` must be escaped.
* AngularJS uses the underscore (_) in front of a character to indicate that it is uppercase, so
* `innerHTML` must be written as `ng-prop-inner_h_t_m_l="expression"` (Note that this is just an
* example, and for binding HTML {@link ngBindHtml} should be used.
*
* ## Security
*
* Binding expressions to arbitrary properties poses a security risk, as properties like `innerHTML`
* can insert potentially dangerous HTML into the application, e.g. script tags that execute
* malicious code.
* For this reason, `ngProp` applies Strict Contextual Escaping with the {@link ng.$sce $sce service}.
* This means vulnerable properties require their content to be "trusted", based on the
* context of the property. For example, the `innerHTML` is in the `HTML` context, and the
* `iframe.src` property is in the `RESOURCE_URL` context, which requires that values written to
* this property are trusted as a `RESOURCE_URL`.
*
* This can be set explicitly by calling $sce.trustAs(type, value) on the value that is
* trusted before passing it to the `ng-prop-*` directive. There are exist shorthand methods for
* each context type in the form of {@link ng.$sce#trustAsResourceUrl $sce.trustAsResourceUrl()} et al.
*
* In some cases you can also rely upon automatic sanitization of untrusted values - see below.
*
* Based on the context, other options may exist to mark a value as trusted / configure the behavior
* of {@link ng.$sce}. For example, to restrict the `RESOURCE_URL` context to specific origins, use
* the {@link $sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist()}
* and {@link $sceDelegateProvider#resourceUrlBlacklist resourceUrlBlacklist()}.
*
* {@link ng.$sce#what-trusted-context-types-are-supported- Find out more about the different context types}.
*
* ### HTML Sanitization
*
* By default, `$sce` will throw an error if it detects untrusted HTML content, and will not bind the
* content.
* However, if you include the {@link ngSanitize ngSanitize module}, it will try to sanitize the
* potentially dangerous HTML, e.g. strip non-whitelisted tags and attributes when binding to
* `innerHTML`.
*
* @example
* ### Binding to different contexts
*
* <example name="ngProp" module="exampleNgProp">
* <file name="app.js">
* angular.module('exampleNgProp', [])
* .component('main', {
* templateUrl: 'main.html',
* controller: function($sce) {
* this.safeContent = '<strong>Safe content</strong>';
* this.unsafeContent = '<button onclick="alert(\'Hello XSS!\')">Click for XSS</button>';
* this.trustedUnsafeContent = $sce.trustAsHtml(this.unsafeContent);
* }
* });
* </file>
* <file name="main.html">
* <div>
* <div class="prop-unit">
* Binding to a property without security context:
* <div class="prop-binding" ng-prop-inner_text="$ctrl.safeContent"></div>
* <span class="prop-note">innerText</span> (safeContent)
* </div>
*
* <div class="prop-unit">
* "Safe" content that requires a security context will throw because the contents could potentially be dangerous ...
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.safeContent"></div>
* <span class="prop-note">innerHTML</span> (safeContent)
* </div>
*
* <div class="prop-unit">
* ... so that actually dangerous content cannot be executed:
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.unsafeContent"></div>
* <span class="prop-note">innerHTML</span> (unsafeContent)
* </div>
*
* <div class="prop-unit">
* ... but unsafe Content that has been trusted explicitly works - only do this if you are 100% sure!
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.trustedUnsafeContent"></div>
* <span class="prop-note">innerHTML</span> (trustedUnsafeContent)
* </div>
* </div>
* </file>
* <file name="index.html">
* <main></main>
* </file>
* <file name="styles.css">
* .prop-unit {
* margin-bottom: 10px;
* }
*
* .prop-binding {
* min-height: 30px;
* border: 1px solid blue;
* }
*
* .prop-note {
* font-family: Monospace;
* }
* </file>
* </example>
*
*
* @example
* ### Binding to innerHTML with ngSanitize
*
* <example name="ngProp" module="exampleNgProp" deps="angular-sanitize.js">
* <file name="app.js">
* angular.module('exampleNgProp', ['ngSanitize'])
* .component('main', {
* templateUrl: 'main.html',
* controller: function($sce) {
* this.safeContent = '<strong>Safe content</strong>';
* this.unsafeContent = '<button onclick="alert(\'Hello XSS!\')">Click for XSS</button>';
* this.trustedUnsafeContent = $sce.trustAsHtml(this.unsafeContent);
* }
* });
* </file>
* <file name="main.html">
* <div>
* <div class="prop-unit">
* "Safe" content will be sanitized ...
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.safeContent"></div>
* <span class="prop-note">innerHTML</span> (safeContent)
* </div>
*
* <div class="prop-unit">
* ... as will dangerous content:
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.unsafeContent"></div>
* <span class="prop-note">innerHTML</span> (unsafeContent)
* </div>
*
* <div class="prop-unit">
* ... and content that has been trusted explicitly works the same as without ngSanitize:
* <div class="prop-binding" ng-prop-inner_h_t_m_l="$ctrl.trustedUnsafeContent"></div>
* <span class="prop-note">innerHTML</span> (trustedUnsafeContent)
* </div>
* </div>
* </file>
* <file name="index.html">
* <main></main>
* </file>
* <file name="styles.css">
* .prop-unit {
* margin-bottom: 10px;
* }
*
* .prop-binding {
* min-height: 30px;
* border: 1px solid blue;
* }
*
* .prop-note {
* font-family: Monospace;
* }
* </file>
* </example>
*
*/
/** @ngdoc directive
* @name ngOn
* @restrict A
* @element ANY
*
* @usage
*
* ```html
* <ANY ng-on-eventname="expression">
* </ANY>
* ```
*
* or with uppercase letters in property (e.g. "eventName"):
*
*
* ```html
* <ANY ng-on-event_name="expression">
* </ANY>
* ```
*
* @description
* The `ngOn` directive adds an event listener to a DOM element via
* {@link angular.element angular.element().on()}, and evaluates an expression when the event is
* fired.
* `ngOn` allows adding listeners for arbitrary events by including
* the event name in the attribute, e.g. `ng-on-drop="onDrop()"` executes the 'onDrop()' expression
* when the `drop` event is fired.
*
* AngularJS provides specific directives for many events, such as {@link ngClick}, so in most
* cases it is not necessary to use `ngOn`. However, AngularJS does not support all events
* (e.g. the `drop` event in the example above), and new events might be introduced in later DOM
* standards.
*
* Another use-case for `ngOn` is listening to
* [custom events](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
* fired by
* [custom elements](https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements).
*
* ## Binding to camelCase properties
*
* Since HTML attributes are case-insensitive, camelCase properties like `myEvent` must be escaped.
* AngularJS uses the underscore (_) in front of a character to indicate that it is uppercase, so
* `myEvent` must be written as `ng-on-my_event="expression"`.
*
* @example
* ### Bind to built-in DOM events
*
* <example name="ngOn" module="exampleNgOn">
* <file name="app.js">
* angular.module('exampleNgOn', [])
* .component('main', {
* templateUrl: 'main.html',
* controller: function() {
* this.clickCount = 0;
* this.mouseoverCount = 0;
*
* this.loadingState = 0;
* }
* });
* </file>
* <file name="main.html">
* <div>
* This is equivalent to `ngClick` and `ngMouseover`:<br>
* <button
* ng-on-click="$ctrl.clickCount = $ctrl.clickCount + 1"
* ng-on-mouseover="$ctrl.mouseoverCount = $ctrl.mouseoverCount + 1">Click or mouseover</button><br>
* clickCount: {{$ctrl.clickCount}}<br>
* mouseover: {{$ctrl.mouseoverCount}}
*
* <hr>
*
* For the `error` and `load` event on images no built-in AngularJS directives exist:<br>
* <img src="thisimagedoesnotexist.png" ng-on-error="$ctrl.loadingState = -1" ng-on-load="$ctrl.loadingState = 1"><br>
* <div ng-switch="$ctrl.loadingState">
* <span ng-switch-when="0">Image is loading</span>
* <span ng-switch-when="-1">Image load error</span>
* <span ng-switch-when="1">Image loaded successfully</span>
* </div>
* </div>
* </file>
* <file name="index.html">
* <main></main>
* </file>
* </example>
*
*
* @example
* ### Bind to custom DOM events
*
* <example name="ngOnCustom" module="exampleNgOn">
* <file name="app.js">
* angular.module('exampleNgOn', [])
* .component('main', {
* templateUrl: 'main.html',
* controller: function() {
* this.eventLog = '';
*
* this.listener = function($event) {
* this.eventLog = 'Event with type "' + $event.type + '" fired at ' + $event.detail;
* };
* }
* })
* .component('childComponent', {
* templateUrl: 'child.html',
* controller: function($element) {
* this.fireEvent = function() {
* var event = new CustomEvent('customtype', { detail: new Date()});
*
* $element[0].dispatchEvent(event);
* };
* }
* });
* </file>
* <file name="main.html">
* <child-component ng-on-customtype="$ctrl.listener($event)"></child-component><br>
* <span>Event log: {{$ctrl.eventLog}}</span>
* </file>
* <file name="child.html">
<button ng-click="$ctrl.fireEvent()">Fire custom event</button>
* </file>
* <file name="index.html">
* <main></main>
* </file>
* </example>
*/
var $compileMinErr = minErr('$compile');
function UNINITIALIZED_VALUE() {}
@@ -1587,6 +1905,91 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return cssClassDirectivesEnabledConfig;
};
/**
* The security context of DOM Properties.
* @private
*/
var PROP_CONTEXTS = createMap();
/**
* @ngdoc method
* @name $compileProvider#addPropertySecurityContext
* @description
*
* Defines the security context for DOM properties bound by ng-prop-*.
*
* @param {string} elementName The element name or '*' to match any element.
* @param {string} propertyName The DOM property name.
* @param {string} ctx The {@link $sce} security context in which this value is safe for use, e.g. `$sce.URL`
* @returns {object} `this` for chaining
*/
this.addPropertySecurityContext = function(elementName, propertyName, ctx) {
var key = (elementName.toLowerCase() + '|' + propertyName.toLowerCase());
if (key in PROP_CONTEXTS && PROP_CONTEXTS[key] !== ctx) {
throw $compileMinErr('ctxoverride', 'Property context \'{0}.{1}\' already set to \'{2}\', cannot override to \'{3}\'.', elementName, propertyName, PROP_CONTEXTS[key], ctx);
}
PROP_CONTEXTS[key] = ctx;
return this;
};
/* Default property contexts.
*
* Copy of https://github.com/angular/angular/blob/6.0.6/packages/compiler/src/schema/dom_security_schema.ts#L31-L58
* Changing:
* - SecurityContext.* => SCE_CONTEXTS/$sce.*
* - STYLE => CSS
* - various URL => MEDIA_URL
* - *|formAction, form|action URL => RESOURCE_URL (like the attribute)
*/
(function registerNativePropertyContexts() {
function registerContext(ctx, values) {
forEach(values, function(v) { PROP_CONTEXTS[v.toLowerCase()] = ctx; });
}
registerContext(SCE_CONTEXTS.HTML, [
'iframe|srcdoc',
'*|innerHTML',
'*|outerHTML'
]);
registerContext(SCE_CONTEXTS.CSS, ['*|style']);
registerContext(SCE_CONTEXTS.URL, [
'area|href', 'area|ping',
'a|href', 'a|ping',
'blockquote|cite',
'body|background',
'del|cite',
'input|src',
'ins|cite',
'q|cite'
]);
registerContext(SCE_CONTEXTS.MEDIA_URL, [
'audio|src',
'img|src', 'img|srcset',
'source|src', 'source|srcset',
'track|src',
'video|src', 'video|poster'
]);
registerContext(SCE_CONTEXTS.RESOURCE_URL, [
'*|formAction',
'applet|code', 'applet|codebase',
'base|href',
'embed|src',
'frame|src',
'form|action',
'head|profile',
'html|manifest',
'iframe|src',
'link|href',
'media|src',
'object|codebase', 'object|data',
'script|src'
]);
})();
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
'$controller', '$rootScope', '$sce', '$animate',
@@ -1632,6 +2035,57 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
function sanitizeSrcset(value, invokeType) {
if (!value) {
return value;
}
if (!isString(value)) {
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `{0}`: "{1}"', invokeType, value.toString());
}
// Such values are a bit too complex to handle automatically inside $sce.
// Instead, we sanitize each of the URIs individually, which works, even dynamically.
// It's not possible to work around this using `$sce.trustAsMediaUrl`.
// If you want to programmatically set explicitly trusted unsafe URLs, you should use
// `$sce.trustAsHtml` on the whole `img` tag and inject it into the DOM using the
// `ng-bind-html` directive.
var result = '';
// first check if there are spaces because it's not the same pattern
var trimmedSrcset = trim(value);
// ( 999x ,| 999w ,| ,|, )
var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/;
var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/;
// split srcset into tuple of uri and descriptor except for the last item
var rawUris = trimmedSrcset.split(pattern);
// for each tuples
var nbrUrisWith2parts = Math.floor(rawUris.length / 2);
for (var i = 0; i < nbrUrisWith2parts; i++) {
var innerIdx = i * 2;
// sanitize the uri
result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
// add the descriptor
result += ' ' + trim(rawUris[innerIdx + 1]);
}
// split the last item into uri and descriptor
var lastTuple = trim(rawUris[i * 2]).split(/\s/);
// sanitize the last uri
result += $sce.getTrustedMediaUrl(trim(lastTuple[0]));
// and add the last descriptor if any
if (lastTuple.length === 2) {
result += (' ' + trim(lastTuple[1]));
}
return result;
}
function Attributes(element, attributesToCopy) {
if (attributesToCopy) {
var keys = Object.keys(attributesToCopy);
@@ -1768,51 +2222,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeName = nodeName_(this.$$element);
// Sanitize img[srcset] values.
if (nodeName === 'img' && key === 'srcset' && value) {
if (!isString(value)) {
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `$set(\'srcset\', value)`: "{0}"', value.toString());
}
// Such values are a bit too complex to handle automatically inside $sce.
// Instead, we sanitize each of the URIs individually, which works, even dynamically.
// It's not possible to work around this using `$sce.trustAsMediaUrl`.
// If you want to programmatically set explicitly trusted unsafe URLs, you should use
// `$sce.trustAsHtml` on the whole `img` tag and inject it into the DOM using the
// `ng-bind-html` directive.
var result = '';
// first check if there are spaces because it's not the same pattern
var trimmedSrcset = trim(value);
// ( 999x ,| 999w ,| ,|, )
var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/;
var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/;
// split srcset into tuple of uri and descriptor except for the last item
var rawUris = trimmedSrcset.split(pattern);
// for each tuples
var nbrUrisWith2parts = Math.floor(rawUris.length / 2);
for (var i = 0; i < nbrUrisWith2parts; i++) {
var innerIdx = i * 2;
// sanitize the uri
result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
// add the descriptor
result += ' ' + trim(rawUris[innerIdx + 1]);
}
// split the last item into uri and descriptor
var lastTuple = trim(rawUris[i * 2]).split(/\s/);
// sanitize the last uri
result += $sce.getTrustedMediaUrl(trim(lastTuple[0]));
// and add the last descriptor if any
if (lastTuple.length === 2) {
result += (' ' + trim(lastTuple[1]));
}
this[key] = value = result;
if (nodeName === 'img' && key === 'srcset') {
this[key] = value = sanitizeSrcset(value, '$set(\'srcset\', value)');
}
if (writeAttr !== false) {
@@ -1909,7 +2320,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
: function denormalizeTemplate(template) {
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
},
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
NG_PREFIX_BINDING = /^ng(Attr|Prop|On)([A-Z].*)$/;
var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
@@ -2245,43 +2656,66 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective);
// iterate over the attributes
for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes,
for (var attr, name, nName, value, ngPrefixMatch, nAttrs = node.attributes,
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
var attrStartName = false;
var attrEndName = false;
var isNgAttr = false, isNgProp = false, isNgEvent = false;
var multiElementMatch;
attr = nAttrs[j];
name = attr.name;
value = attr.value;
// support ngAttr attribute binding
ngAttrName = directiveNormalize(name);
isNgAttr = NG_ATTR_BINDING.test(ngAttrName);
if (isNgAttr) {
nName = directiveNormalize(name.toLowerCase());
// Support ng-attr-*, ng-prop-* and ng-on-*
if ((ngPrefixMatch = nName.match(NG_PREFIX_BINDING))) {
isNgAttr = ngPrefixMatch[1] === 'Attr';
isNgProp = ngPrefixMatch[1] === 'Prop';
isNgEvent = ngPrefixMatch[1] === 'On';
// Normalize the non-prefixed name
name = name.replace(PREFIX_REGEXP, '')
.substr(8).replace(/_(.)/g, function(match, letter) {
.toLowerCase()
.substr(4 + ngPrefixMatch[1].length).replace(/_(.)/g, function(match, letter) {
return letter.toUpperCase();
});
}
var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
// Support *-start / *-end multi element directives
} else if ((multiElementMatch = nName.match(MULTI_ELEMENT_DIR_RE)) && directiveIsMultiElement(multiElementMatch[1])) {
attrStartName = name;
attrEndName = name.substr(0, name.length - 5) + 'end';
name = name.substr(0, name.length - 6);
}
nName = directiveNormalize(name.toLowerCase());
attrsMap[nName] = name;
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
if (isNgProp || isNgEvent) {
attrs[nName] = value;
attrsMap[nName] = attr.name;
if (isNgProp) {
addPropertyDirective(node, directives, nName, name);
} else {
addEventDirective(directives, nName, name);
}
} else {
// Update nName for cases where a prefix was removed
// NOTE: the .toLowerCase() is unnecessary and causes https://github.com/angular/angular.js/issues/16624 for ng-attr-*
nName = directiveNormalize(name.toLowerCase());
attrsMap[nName] = name;
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
attrs[nName] = value;
if (getBooleanAttrName(node, nName)) {
attrs[nName] = true; // presence means true
}
}
addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
attrEndName);
}
addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
attrEndName);
}
if (nodeName === 'input' && node.getAttribute('type') === 'hidden') {
@@ -2575,7 +3009,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// We have transclusion slots,
// collect them up, compile them and store their transclusion functions
$template = [];
$template = window.document.createDocumentFragment();
var slotMap = createMap();
var filledSlots = createMap();
@@ -2603,10 +3037,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var slotName = slotMap[directiveNormalize(nodeName_(node))];
if (slotName) {
filledSlots[slotName] = true;
slots[slotName] = slots[slotName] || [];
slots[slotName].push(node);
slots[slotName] = slots[slotName] || window.document.createDocumentFragment();
slots[slotName].appendChild(node);
} else {
$template.push(node);
$template.appendChild(node);
}
});
@@ -2620,9 +3054,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
for (var slotName in slots) {
if (slots[slotName]) {
// Only define a transclusion function if the slot was filled
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
var slotCompileNodes = jqLite(slots[slotName].childNodes);
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slotCompileNodes, transcludeFn);
}
}
$template = jqLite($template.childNodes);
}
$compileNode.empty(); // clear contents
@@ -2958,7 +3395,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!value) {
var dataName = '$' + name + 'Controller';
value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
if (inheritType === '^^' && $element[0] && $element[0].nodeType === NODE_TYPE_DOCUMENT) {
// inheritedData() uses the documentElement when it finds the document, so we would
// require from the element itself.
value = null;
} else {
value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
}
}
if (!value && !optional) {
@@ -3315,42 +3759,95 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
function getTrustedContext(node, attrNormalizedName) {
function getTrustedAttrContext(nodeName, attrNormalizedName) {
if (attrNormalizedName === 'srcdoc') {
return $sce.HTML;
}
var tag = nodeName_(node);
// All tags with src attributes require a RESOURCE_URL value, except for
// img and various html5 media tags, which require the MEDIA_URL context.
// All nodes with src attributes require a RESOURCE_URL value, except for
// img and various html5 media nodes, which require the MEDIA_URL context.
if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') {
if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) {
if (['img', 'video', 'audio', 'source', 'track'].indexOf(nodeName) === -1) {
return $sce.RESOURCE_URL;
}
return $sce.MEDIA_URL;
} else if (attrNormalizedName === 'xlinkHref') {
// Some xlink:href are okay, most aren't
if (tag === 'image') return $sce.MEDIA_URL;
if (tag === 'a') return $sce.URL;
if (nodeName === 'image') return $sce.MEDIA_URL;
if (nodeName === 'a') return $sce.URL;
return $sce.RESOURCE_URL;
} else if (
// Formaction
(tag === 'form' && attrNormalizedName === 'action') ||
(nodeName === 'form' && attrNormalizedName === 'action') ||
// If relative URLs can go where they are not expected to, then
// all sorts of trust issues can arise.
(tag === 'base' && attrNormalizedName === 'href') ||
(nodeName === 'base' && attrNormalizedName === 'href') ||
// links can be stylesheets or imports, which can run script in the current origin
(tag === 'link' && attrNormalizedName === 'href')
(nodeName === 'link' && attrNormalizedName === 'href')
) {
return $sce.RESOURCE_URL;
} else if (tag === 'a' && (attrNormalizedName === 'href' ||
} else if (nodeName === 'a' && (attrNormalizedName === 'href' ||
attrNormalizedName === 'ngHref')) {
return $sce.URL;
}
}
function getTrustedPropContext(nodeName, propNormalizedName) {
var prop = propNormalizedName.toLowerCase();
return PROP_CONTEXTS[nodeName + '|' + prop] || PROP_CONTEXTS['*|' + prop];
}
function sanitizeSrcsetPropertyValue(value) {
return sanitizeSrcset($sce.valueOf(value), 'ng-prop-srcset');
}
function addPropertyDirective(node, directives, attrName, propName) {
if (EVENT_HANDLER_ATTR_REGEXP.test(propName)) {
throw $compileMinErr('nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
}
var nodeName = nodeName_(node);
var trustedContext = getTrustedPropContext(nodeName, propName);
var sanitizer = identity;
// Sanitize img[srcset] + source[srcset] values.
if (propName === 'srcset' && (nodeName === 'img' || nodeName === 'source')) {
sanitizer = sanitizeSrcsetPropertyValue;
} else if (trustedContext) {
sanitizer = $sce.getTrusted.bind($sce, trustedContext);
}
directives.push({
priority: 100,
compile: function ngPropCompileFn(_, attr) {
var ngPropGetter = $parse(attr[attrName]);
var ngPropWatch = $parse(attr[attrName], function sceValueOf(val) {
// Unwrap the value to compare the actual inner safe value, not the wrapper object.
return $sce.valueOf(val);
});
return {
pre: function ngPropPreLinkFn(scope, $element) {
function applyPropValue() {
var propValue = ngPropGetter(scope);
$element.prop(propName, sanitizer(propValue));
}
applyPropValue();
scope.$watch(ngPropWatch, applyPropValue);
}
};
}
});
}
function addEventDirective(directives, attrName, eventName) {
directives.push(
createEventDirective($parse, $rootScope, $exceptionHandler, attrName, eventName, /*forceAsync=*/false)
);
}
function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) {
var trustedContext = getTrustedContext(node, name);
var nodeName = nodeName_(node);
var trustedContext = getTrustedAttrContext(nodeName, name);
var mustHaveExpression = !isNgAttr;
var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr;
@@ -3359,16 +3856,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// no interpolation found -> ignore
if (!interpolateFn) return;
if (name === 'multiple' && nodeName_(node) === 'select') {
if (name === 'multiple' && nodeName === 'select') {
throw $compileMinErr('selmulti',
'Binding to the \'multiple\' attribute is not supported. Element: {0}',
startingTag(node));
}
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
throw $compileMinErr('nodomevents',
'Interpolations for HTML DOM event attributes are disallowed. Please use the ' +
'ng- versions (such as ng-click instead of onclick) instead.');
throw $compileMinErr('nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
}
directives.push({
+25
View File
@@ -4,6 +4,7 @@
*/
var nullFormCtrl = {
$addControl: noop,
$getControls: valueFn([]),
$$renameControl: nullFormRenameControl,
$removeControl: noop,
$setValidity: noop,
@@ -159,6 +160,30 @@ FormController.prototype = {
control.$$parentForm = this;
},
/**
* @ngdoc method
* @name form.FormController#$getControls
* @returns {Array} the controls that are currently part of this form
*
* @description
* This method returns a **shallow copy** of the controls that are currently part of this form.
* The controls can be instances of {@link form.FormController `FormController`}
* ({@link ngForm "child-forms"}) and of {@link ngModel.NgModelController `NgModelController`}.
* If you need access to the controls of child-forms, you have to call `$getControls()`
* recursively on them.
* This can be used for example to iterate over all controls to validate them.
*
* The controls can be accessed normally, but adding to, or removing controls from the array has
* no effect on the form. Instead, use {@link form.FormController#$addControl `$addControl()`} and
* {@link form.FormController#$removeControl `$removeControl()`} for this use-case.
* Likewise, adding a control to, or removing a control from the form is not reflected
* in the shallow copy. That means you should get a fresh copy from `$getControls()` every time
* you need access to the controls.
*/
$getControls: function() {
return shallowCopy(this.$$controls);
},
// Private API: rename a form control
$$renameControl: function(control, newName) {
var oldName = control.$name;
+33 -2
View File
@@ -255,6 +255,10 @@ var inputType = {
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
* The format of the displayed time can be adjusted with the
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
* and `timeStripZeroSeconds`.
*
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
@@ -356,7 +360,12 @@ var inputType = {
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions}. By default,
* this is the timezone of the browser.
*
* The format of the displayed time can be adjusted with the
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
* and `timeStripZeroSeconds`.
*
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
@@ -1491,6 +1500,8 @@ function createDateInputType(type, regexp, parseDate, format) {
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
badInputChecker(scope, element, attr, ctrl, type);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
var isTimeType = type === 'time' || type === 'datetimelocal';
var previousDate;
var previousTimezone;
@@ -1514,11 +1525,13 @@ function createDateInputType(type, regexp, parseDate, format) {
if (isValidDate(value)) {
previousDate = value;
var timezone = ctrl.$options.getOption('timezone');
if (timezone) {
previousTimezone = timezone;
previousDate = convertTimezoneToLocal(previousDate, timezone, true);
}
return $filter('date')(value, format, timezone);
return formatter(value, timezone);
} else {
previousDate = null;
previousTimezone = null;
@@ -1573,6 +1586,24 @@ function createDateInputType(type, regexp, parseDate, format) {
}
return parsedDate;
}
function formatter(value, timezone) {
var targetFormat = format;
if (isTimeType && isString(ctrl.$options.getOption('timeSecondsFormat'))) {
targetFormat = format
.replace('ss.sss', ctrl.$options.getOption('timeSecondsFormat'))
.replace(/:$/, '');
}
var formatted = $filter('date')(value, targetFormat, timezone);
if (isTimeType && ctrl.$options.getOption('timeStripZeroSeconds')) {
formatted = formatted.replace(/(?::00)?(?:\.000)?$/, '');
}
return formatted;
}
};
}
+5
View File
@@ -125,6 +125,8 @@ function classDirective(name, selector) {
}
function toClassString(classValue) {
if (!classValue) return classValue;
var classString = classValue;
if (isArray(classValue)) {
@@ -133,6 +135,8 @@ function classDirective(name, selector) {
classString = Object.keys(classValue).
filter(function(key) { return classValue[key]; }).
join(' ');
} else if (!isString(classValue)) {
classString = classValue + '';
}
return classString;
@@ -178,6 +182,7 @@ function classDirective(name, selector) {
* |----------------------------------|-------------------------------------|
* | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
* | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
* | {@link ng.$animate#setClass setClass} | just before classes are added and classes are removed from the element at the same time |
*
* ### ngClass and pre-existing CSS3 Transitions/Animations
The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure.
+34 -23
View File
@@ -50,33 +50,44 @@ forEach(
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(eventName) {
var directiveName = directiveNormalize('ng-' + eventName);
ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
return {
restrict: 'A',
compile: function($element, attr) {
// NOTE:
// We expose the powerful `$event` object on the scope that provides access to the Window,
// etc. This is OK, because expressions are not sandboxed any more (and the expression
// sandbox was never meant to be a security feature anyway).
var fn = $parse(attr[directiveName]);
return function ngEventHandler(scope, element) {
element.on(eventName, function(event) {
var callback = function() {
fn(scope, {$event: event});
};
if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
scope.$evalAsync(callback);
} else {
scope.$apply(callback);
}
});
};
}
};
ngEventDirectives[directiveName] = ['$parse', '$rootScope', '$exceptionHandler', function($parse, $rootScope, $exceptionHandler) {
return createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsyncEvents[eventName]);
}];
}
);
function createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsync) {
return {
restrict: 'A',
compile: function($element, attr) {
// NOTE:
// We expose the powerful `$event` object on the scope that provides access to the Window,
// etc. This is OK, because expressions are not sandboxed any more (and the expression
// sandbox was never meant to be a security feature anyway).
var fn = $parse(attr[directiveName]);
return function ngEventHandler(scope, element) {
element.on(eventName, function(event) {
var callback = function() {
fn(scope, {$event: event});
};
if (!$rootScope.$$phase) {
scope.$apply(callback);
} else if (forceAsync) {
scope.$evalAsync(callback);
} else {
try {
callback();
} catch (error) {
$exceptionHandler(error);
}
}
});
};
}
};
}
/**
* @ngdoc directive
* @name ngDblclick
+2 -1
View File
@@ -287,6 +287,7 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
this.$$currentValidationRunId = 0;
this.$$scope = $scope;
this.$$rootScope = $scope.$root;
this.$$attr = $attr;
this.$$element = $element;
this.$$animate = $animate;
@@ -864,7 +865,7 @@ NgModelController.prototype = {
this.$$pendingDebounce = this.$$timeout(function() {
that.$commitViewValue();
}, debounceDelay);
} else if (this.$$scope.$root.$$phase) {
} else if (this.$$rootScope.$$phase) {
this.$commitViewValue();
} else {
this.$$scope.$apply(function() {
+86 -8
View File
@@ -41,7 +41,7 @@ ModelOptions.prototype = {
options = extend({}, options);
// Inherit options from the parent if specified by the value `"$inherit"`
forEach(options, /* @this */ function(option, key) {
forEach(options, /** @this */ function(option, key) {
if (option === '$inherit') {
if (key === '*') {
inheritAll = true;
@@ -406,12 +406,6 @@ defaultModelOptions = new ModelOptions({
* </example>
*
*
* ## Specifying timezones
*
* You can specify the timezone that date/time input directives expect by providing its name in the
* `timezone` property.
*
*
* ## Programmatically changing options
*
* The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not
@@ -423,8 +417,70 @@ defaultModelOptions = new ModelOptions({
* Default events, extra triggers, and catch-all debounce values}.
*
*
* ## Specifying timezones
*
* You can specify the timezone that date/time input directives expect by providing its name in the
* `timezone` property.
*
*
* ## Formatting the value of `time` and `datetime-local`
*
* With the options `timeSecondsFormat` and `timeStripZeroSeconds` it is possible to adjust the value
* that is displayed in the control. Note that browsers may apply their own formatting
* in the user interface.
*
<example name="ngModelOptions-time-format" module="timeExample">
<file name="index.html">
<time-example></time-example>
</file>
<file name="script.js">
angular.module('timeExample', [])
.component('timeExample', {
templateUrl: 'timeExample.html',
controller: function() {
this.time = new Date(1970, 0, 1, 14, 57, 0);
this.options = {
timeSecondsFormat: 'ss',
timeStripZeroSeconds: true
};
this.optionChange = function() {
this.timeForm.timeFormatted.$overrideModelOptions(this.options);
this.time = new Date(this.time);
};
}
});
</file>
<file name="timeExample.html">
<form name="$ctrl.timeForm">
<strong>Default</strong>:
<input type="time" ng-model="$ctrl.time" step="any" /><br>
<strong>With options</strong>:
<input type="time" name="timeFormatted" ng-model="$ctrl.time" step="any" ng-model-options="$ctrl.options" />
<br>
Options:<br>
<code>timeSecondsFormat</code>:
<input
type="text"
ng-model="$ctrl.options.timeSecondsFormat"
ng-change="$ctrl.optionChange()">
<br>
<code>timeStripZeroSeconds</code>:
<input
type="checkbox"
ng-model="$ctrl.options.timeStripZeroSeconds"
ng-change="$ctrl.optionChange()">
</form>
</file>
* </example>
*
* @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and
* and its descendents. Valid keys are:
* and its descendents.
*
* **General options**:
*
* - `updateOn`: string specifying which event should the input be bound to. You can set several
* events using an space delimited list. There is a special event called `default` that
* matches the default events belonging to the control. These are the events that are bound to
@@ -457,6 +513,10 @@ defaultModelOptions = new ModelOptions({
* not validate correctly instead of the default behavior of setting the model to undefined.
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
* `ngModel` as getters/setters.
*
*
* **Input-type specific options**:
*
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
* `<input type="date" />`, `<input type="time" />`, ... . It understands UTC/GMT and the
* continental US time zone abbreviations, but for general use, use a time zone offset, for
@@ -465,6 +525,24 @@ defaultModelOptions = new ModelOptions({
* Note that changing the timezone will have no effect on the current date, and is only applied after
* the next input / model change.
*
* - `timeSecondsFormat`: Defines if the `time` and `datetime-local` types should show seconds and
* milliseconds. The option follows the format string of {@link date date filter}.
* By default, the options is `undefined` which is equal to `'ss.sss'` (seconds and milliseconds).
* The other options are `'ss'` (strips milliseconds), and `''` (empty string), which strips both
* seconds and milliseconds.
* Note that browsers that support `time` and `datetime-local` require the hour and minutes
* part of the time string, and may show the value differently in the user interface.
* {@link ngModelOptions#formatting-the-value-of-time-and-datetime-local- See the example}.
*
* - `timeStripZeroSeconds`: Defines if the `time` and `datetime-local` types should strip the
* seconds and milliseconds from the formatted value if they are zero. This option is applied
* after `timeSecondsFormat`.
* This option can be used to make the formatting consistent over different browsers, as some
* browsers with support for `time` will natively hide the milliseconds and
* seconds if they are zero, but others won't, and browsers that don't implement these input
* types will always show the full string.
* {@link ngModelOptions#formatting-the-value-of-time-and-datetime-local- See the example}.
*
*/
var ngModelOptionsDirective = function() {
NgModelOptionsController.$inject = ['$attrs', '$scope'];
+296
View File
@@ -0,0 +1,296 @@
'use strict';
/**
* @ngdoc directive
* @name ngRef
* @restrict A
*
* @description
* The `ngRef` attribute tells AngularJS to assign the controller of a component (or a directive)
* to the given property in the current scope. It is also possible to add the jqlite-wrapped DOM
* element to the scope.
*
* If the element with `ngRef` is destroyed `null` is assigned to the property.
*
* Note that if you want to assign from a child into the parent scope, you must initialize the
* target property on the parent scope, otherwise `ngRef` will assign on the child scope.
* This commonly happens when assigning elements or components wrapped in {@link ngIf} or
* {@link ngRepeat}. See the second example below.
*
*
* @element ANY
* @param {string} ngRef property name - A valid AngularJS expression identifier to which the
* controller or jqlite-wrapped DOM element will be bound.
* @param {string=} ngRefRead read value - The name of a directive (or component) on this element,
* or the special string `$element`. If a name is provided, `ngRef` will
* assign the matching controller. If `$element` is provided, the element
* itself is assigned (even if a controller is available).
*
*
* @example
* ### Simple toggle
* This example shows how the controller of the component toggle
* is reused in the template through the scope to use its logic.
* <example name="ng-ref-component" module="myApp">
* <file name="index.html">
* <my-toggle ng-ref="myToggle"></my-toggle>
* <button ng-click="myToggle.toggle()">Toggle</button>
* <div ng-show="myToggle.isOpen()">
* You are using a component in the same template to show it.
* </div>
* </file>
* <file name="index.js">
* angular.module('myApp', [])
* .component('myToggle', {
* controller: function ToggleController() {
* var opened = false;
* this.isOpen = function() { return opened; };
* this.toggle = function() { opened = !opened; };
* }
* });
* </file>
* <file name="protractor.js" type="protractor">
* it('should publish the toggle into the scope', function() {
* var toggle = element(by.buttonText('Toggle'));
* expect(toggle.evaluate('myToggle.isOpen()')).toEqual(false);
* toggle.click();
* expect(toggle.evaluate('myToggle.isOpen()')).toEqual(true);
* });
* </file>
* </example>
*
* @example
* ### ngRef inside scopes
* This example shows how `ngRef` works with child scopes. The `ngRepeat`-ed `myWrapper` components
* are assigned to the scope of `myRoot`, because the `toggles` property has been initialized.
* The repeated `myToggle` components are published to the child scopes created by `ngRepeat`.
* `ngIf` behaves similarly - the assignment of `myToggle` happens in the `ngIf` child scope,
* because the target property has not been initialized on the `myRoot` component controller.
*
* <example name="ng-ref-scopes" module="myApp">
* <file name="index.html">
* <my-root></my-root>
* </file>
* <file name="index.js">
* angular.module('myApp', [])
* .component('myRoot', {
* templateUrl: 'root.html',
* controller: function() {
* this.wrappers = []; // initialize the array so that the wrappers are assigned into the parent scope
* }
* })
* .component('myToggle', {
* template: '<strong>myToggle</strong><button ng-click="$ctrl.toggle()" ng-transclude></button>',
* transclude: true,
* controller: function ToggleController() {
* var opened = false;
* this.isOpen = function() { return opened; };
* this.toggle = function() { opened = !opened; };
* }
* })
* .component('myWrapper', {
* transclude: true,
* template: '<strong>myWrapper</strong>' +
* '<div>ngRepeatToggle.isOpen(): {{$ctrl.ngRepeatToggle.isOpen() | json}}</div>' +
* '<my-toggle ng-ref="$ctrl.ngRepeatToggle"><ng-transclude></ng-transclude></my-toggle>'
* });
* </file>
* <file name="root.html">
* <strong>myRoot</strong>
* <my-toggle ng-ref="$ctrl.outerToggle">Outer Toggle</my-toggle>
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
* <div><em>wrappers assigned to root</em><br>
* <div ng-repeat="wrapper in $ctrl.wrappers">
* wrapper.ngRepeatToggle.isOpen(): {{wrapper.ngRepeatToggle.isOpen() | json}}
* </div>
*
* <ul>
* <li ng-repeat="(index, value) in [1,2,3]">
* <strong>ngRepeat</strong>
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
* <my-wrapper ng-ref="$ctrl.wrappers[index]">ngRepeat Toggle {{$index + 1}}</my-wrapper>
* </li>
* </ul>
*
* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen()}} // This is always undefined because it's
* assigned to the child scope created by ngIf.
* </div>
* <div ng-if="true">
<strong>ngIf</strong>
* <my-toggle ng-ref="ngIfToggle">ngIf Toggle</my-toggle>
* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen() | json}}</div>
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
* </div>
* </file>
* <file name="styles.css">
* ul {
* list-style: none;
* padding-left: 0;
* }
*
* li[ng-repeat] {
* background: lightgreen;
* padding: 8px;
* margin: 8px;
* }
*
* [ng-if] {
* background: lightgrey;
* padding: 8px;
* }
*
* my-root {
* background: lightgoldenrodyellow;
* padding: 8px;
* display: block;
* }
*
* my-wrapper {
* background: lightsalmon;
* padding: 8px;
* display: block;
* }
*
* my-toggle {
* background: lightblue;
* padding: 8px;
* display: block;
* }
* </file>
* <file name="protractor.js" type="protractor">
* var OuterToggle = function() {
* this.toggle = function() {
* element(by.buttonText('Outer Toggle')).click();
* };
* this.isOpen = function() {
* return element.all(by.binding('outerToggle.isOpen()')).first().getText();
* };
* };
* var NgRepeatToggle = function(i) {
* var parent = element.all(by.repeater('(index, value) in [1,2,3]')).get(i - 1);
* this.toggle = function() {
* element(by.buttonText('ngRepeat Toggle ' + i)).click();
* };
* this.isOpen = function() {
* return parent.element(by.binding('ngRepeatToggle.isOpen() | json')).getText();
* };
* this.isOuterOpen = function() {
* return parent.element(by.binding('outerToggle.isOpen() | json')).getText();
* };
* };
* var NgRepeatToggles = function() {
* var toggles = [1,2,3].map(function(i) { return new NgRepeatToggle(i); });
* this.forEach = function(fn) {
* toggles.forEach(fn);
* };
* this.isOuterOpen = function(i) {
* return toggles[i - 1].isOuterOpen();
* };
* };
* var NgIfToggle = function() {
* var parent = element(by.css('[ng-if]'));
* this.toggle = function() {
* element(by.buttonText('ngIf Toggle')).click();
* };
* this.isOpen = function() {
* return by.binding('ngIfToggle.isOpen() | json').getText();
* };
* this.isOuterOpen = function() {
* return parent.element(by.binding('outerToggle.isOpen() | json')).getText();
* };
* };
*
* it('should toggle the outer toggle', function() {
* var outerToggle = new OuterToggle();
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false');
* outerToggle.toggle();
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true');
* });
*
* it('should toggle all outer toggles', function() {
* var outerToggle = new OuterToggle();
* var repeatToggles = new NgRepeatToggles();
* var ifToggle = new NgIfToggle();
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false');
* expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): false');
* expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): false');
* expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): false');
* expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
* outerToggle.toggle();
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true');
* expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): true');
* expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): true');
* expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): true');
* expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): true');
* });
*
* it('should toggle each repeat iteration separately', function() {
* var repeatToggles = new NgRepeatToggles();
*
* repeatToggles.forEach(function(repeatToggle) {
* expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): false');
* expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
* repeatToggle.toggle();
* expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): true');
* expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
* });
* });
* </file>
* </example>
*
*/
var ngRefMinErr = minErr('ngRef');
var ngRefDirective = ['$parse', function($parse) {
return {
priority: -1, // Needed for compatibility with element transclusion on the same element
restrict: 'A',
compile: function(tElement, tAttrs) {
// Get the expected controller name, converts <data-some-thing> into "someThing"
var controllerName = directiveNormalize(nodeName_(tElement));
// Get the expression for value binding
var getter = $parse(tAttrs.ngRef);
var setter = getter.assign || function() {
throw ngRefMinErr('nonassign', 'Expression in ngRef="{0}" is non-assignable!', tAttrs.ngRef);
};
return function(scope, element, attrs) {
var refValue;
if (attrs.hasOwnProperty('ngRefRead')) {
if (attrs.ngRefRead === '$element') {
refValue = element;
} else {
refValue = element.data('$' + attrs.ngRefRead + 'Controller');
if (!refValue) {
throw ngRefMinErr(
'noctrl',
'The controller for ngRefRead="{0}" could not be found on ngRef="{1}"',
attrs.ngRefRead,
tAttrs.ngRef
);
}
}
} else {
refValue = element.data('$' + controllerName + 'Controller');
}
refValue = refValue || element;
setter(scope, refValue);
// when the element is removed, remove it (nullify it)
element.on('$destroy', function() {
// only remove it if value has not changed,
// because animations (and other procedures) may duplicate elements
if (getter(scope) === refValue) {
setter(scope, null);
}
});
};
}
};
}];
+1 -1
View File
@@ -74,7 +74,7 @@
* For example, if an item is added to the collection, `ngRepeat` will know that all other items
* already have DOM elements, and will not re-render them.
*
* All different types of tracking functions, their syntax, and and their support for duplicate
* All different types of tracking functions, their syntax, and their support for duplicate
* items in collections can be found in the
* {@link ngRepeat#ngRepeat-arguments ngRepeat expression description}.
*
+1 -13
View File
@@ -383,7 +383,7 @@ var SelectController =
if (optionAttrs.$attr.ngValue) {
// The value attribute is set by ngValue
var oldVal, hashedVal = NaN;
var oldVal, hashedVal;
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
var removal;
@@ -556,18 +556,6 @@ var SelectController =
* {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
*
*
* @knownIssue
*
* In Firefox, the select model is only updated when the select element is blurred. For example,
* when switching between options with the keyboard, the select model is only set to the
* currently selected option when the select is blurred, e.g via tab key or clicking the mouse
* outside the select.
*
* This is due to an ambiguity in the select element specification. See the
* [issue on the Firefox bug tracker](https://bugzilla.mozilla.org/show_bug.cgi?id=126379)
* for more information, and this
* [Github comment for a workaround](https://github.com/angular/angular.js/issues/9134#issuecomment-130800488)
*
* @example
* ### Simple `select` elements with static options
*
+2 -2
View File
@@ -1054,7 +1054,7 @@ function $HttpProvider() {
config.paramSerializer = isString(config.paramSerializer) ?
$injector.get(config.paramSerializer) : config.paramSerializer;
$browser.$$incOutstandingRequestCount();
$browser.$$incOutstandingRequestCount('$http');
var requestInterceptors = [];
var responseInterceptors = [];
@@ -1092,7 +1092,7 @@ function $HttpProvider() {
}
function completeOutstandingRequest() {
$browser.$$completeOutstandingRequest(noop);
$browser.$$completeOutstandingRequest(noop, '$http');
}
function executeHeaderFns(headers, config) {
+13 -48
View File
@@ -4,10 +4,18 @@ var $intervalMinErr = minErr('$interval');
/** @this */
function $IntervalProvider() {
this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser',
function($rootScope, $window, $q, $$q, $browser) {
this.$get = ['$$intervalFactory', '$window',
function($$intervalFactory, $window) {
var intervals = {};
var setIntervalFn = function(tick, delay, deferred) {
var id = $window.setInterval(tick, delay);
intervals[id] = deferred;
return id;
};
var clearIntervalFn = function(id) {
$window.clearInterval(id);
delete intervals[id];
};
/**
* @ngdoc service
@@ -135,49 +143,7 @@ function $IntervalProvider() {
* </file>
* </example>
*/
function interval(fn, delay, count, invokeApply) {
var hasParams = arguments.length > 4,
args = hasParams ? sliceArgs(arguments, 4) : [],
setInterval = $window.setInterval,
clearInterval = $window.clearInterval,
iteration = 0,
skipApply = (isDefined(invokeApply) && !invokeApply),
deferred = (skipApply ? $$q : $q).defer(),
promise = deferred.promise;
count = isDefined(count) ? count : 0;
promise.$$intervalId = setInterval(function tick() {
if (skipApply) {
$browser.defer(callback);
} else {
$rootScope.$evalAsync(callback);
}
deferred.notify(iteration++);
if (count > 0 && iteration >= count) {
deferred.resolve(iteration);
clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
}
if (!skipApply) $rootScope.$apply();
}, delay);
intervals[promise.$$intervalId] = deferred;
return promise;
function callback() {
if (!hasParams) {
fn(iteration);
} else {
fn.apply(null, args);
}
}
}
var interval = $$intervalFactory(setIntervalFn, clearIntervalFn);
/**
* @ngdoc method
@@ -205,8 +171,7 @@ function $IntervalProvider() {
// Interval cancels should not report an unhandled promise.
markQExceptionHandled(deferred.promise);
deferred.reject('canceled');
$window.clearInterval(id);
delete intervals[id];
clearIntervalFn(id);
return true;
};
+48
View File
@@ -0,0 +1,48 @@
'use strict';
/** @this */
function $$IntervalFactoryProvider() {
this.$get = ['$browser', '$q', '$$q', '$rootScope',
function($browser, $q, $$q, $rootScope) {
return function intervalFactory(setIntervalFn, clearIntervalFn) {
return function intervalFn(fn, delay, count, invokeApply) {
var hasParams = arguments.length > 4,
args = hasParams ? sliceArgs(arguments, 4) : [],
iteration = 0,
skipApply = isDefined(invokeApply) && !invokeApply,
deferred = (skipApply ? $$q : $q).defer(),
promise = deferred.promise;
count = isDefined(count) ? count : 0;
function callback() {
if (!hasParams) {
fn(iteration);
} else {
fn.apply(null, args);
}
}
function tick() {
if (skipApply) {
$browser.defer(callback);
} else {
$rootScope.$evalAsync(callback);
}
deferred.notify(iteration++);
if (count > 0 && iteration >= count) {
deferred.resolve(iteration);
clearIntervalFn(promise.$$intervalId);
}
if (!skipApply) $rootScope.$apply();
}
promise.$$intervalId = setIntervalFn(tick, delay, deferred, skipApply);
return promise;
};
};
}];
}
+36 -44
View File
@@ -1,4 +1,5 @@
'use strict';
/* global stripHash: true */
var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/,
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
@@ -38,6 +39,14 @@ function decodePath(path, html5Mode) {
return segments.join('/');
}
function normalizePath(pathValue, searchValue, hashValue) {
var search = toKeyValue(searchValue),
hash = hashValue ? '#' + encodeUriSegment(hashValue) : '',
path = encodePath(pathValue);
return path + (search ? '?' + search : '') + hash;
}
function parseAbsoluteUrl(absoluteUrl, locationObj) {
var parsedUrl = urlResolve(absoluteUrl);
@@ -86,17 +95,11 @@ function stripBaseUrl(base, url) {
}
}
function stripHash(url) {
var index = url.indexOf('#');
return index === -1 ? url : url.substr(0, index);
}
function trimEmptyHash(url) {
return url.replace(/(#.+)|#$/, '$1');
}
function stripFile(url) {
return url.substr(0, stripHash(url).lastIndexOf('/') + 1);
}
@@ -143,18 +146,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
this.$$compose();
};
/**
* Compose url and update `absUrl` property
* @private
*/
this.$$compose = function() {
var search = toKeyValue(this.$$search),
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/'
this.$$urlUpdatedByLocation = true;
this.$$normalizeUrl = function(url) {
return appBaseNoFile + url.substr(1); // first char is always '/'
};
this.$$parseLinkUrl = function(url, relHref) {
@@ -278,18 +271,8 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
}
};
/**
* Compose hashbang URL and update `absUrl` property
* @private
*/
this.$$compose = function() {
var search = toKeyValue(this.$$search),
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : '');
this.$$urlUpdatedByLocation = true;
this.$$normalizeUrl = function(url) {
return appBase + (url ? hashPrefix + url : '');
};
this.$$parseLinkUrl = function(url, relHref) {
@@ -340,17 +323,10 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
return !!rewrittenUrl;
};
this.$$compose = function() {
var search = toKeyValue(this.$$search),
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
this.$$normalizeUrl = function(url) {
// include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#'
this.$$absUrl = appBase + hashPrefix + this.$$url;
this.$$urlUpdatedByLocation = true;
return appBase + hashPrefix + url;
};
}
@@ -374,6 +350,16 @@ var locationPrototype = {
*/
$$replace: false,
/**
* Compose url and update `url` and `absUrl` property
* @private
*/
$$compose: function() {
this.$$url = normalizePath(this.$$path, this.$$search, this.$$hash);
this.$$absUrl = this.$$normalizeUrl(this.$$url);
this.$$urlUpdatedByLocation = true;
},
/**
* @ngdoc method
* @name $location#absUrl
@@ -879,6 +865,13 @@ function $LocationProvider() {
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
// Determine if two URLs are equal despite potentially having different encoding/normalizing
// such as $location.absUrl() vs $browser.url()
// See https://github.com/angular/angular.js/issues/16592
function urlsEqual(a, b) {
return a === b || urlResolve(a).href === urlResolve(b).href;
}
function setBrowserUrlWithFallback(url, replace, state) {
var oldUrl = $location.url();
var oldState = $location.$$state;
@@ -945,7 +938,7 @@ function $LocationProvider() {
// rewrite hashbang url <> html5 url
if (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) {
if ($location.absUrl() !== initialUrl) {
$browser.url($location.absUrl(), true);
}
@@ -964,7 +957,6 @@ function $LocationProvider() {
var oldUrl = $location.absUrl();
var oldState = $location.$$state;
var defaultPrevented;
newUrl = trimEmptyHash(newUrl);
$location.$$parse(newUrl);
$location.$$state = newState;
@@ -992,11 +984,11 @@ function $LocationProvider() {
if (initializing || $location.$$urlUpdatedByLocation) {
$location.$$urlUpdatedByLocation = false;
var oldUrl = trimEmptyHash($browser.url());
var newUrl = trimEmptyHash($location.absUrl());
var oldUrl = $browser.url();
var newUrl = $location.absUrl();
var oldState = $browser.state();
var currentReplace = $location.$$replace;
var urlOrStateChanged = oldUrl !== newUrl ||
var urlOrStateChanged = !urlsEqual(oldUrl, newUrl) ||
($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
if (initializing || urlOrStateChanged) {
+2 -2
View File
@@ -1122,7 +1122,7 @@ function $RootScopeProvider() {
if (asyncQueue.length) {
$rootScope.$digest();
}
});
}, null, '$evalAsync');
}
asyncQueue.push({scope: this, fn: $parse(expr), locals: locals});
@@ -1493,7 +1493,7 @@ function $RootScopeProvider() {
if (applyAsyncId === null) {
applyAsyncId = $browser.defer(function() {
$rootScope.$apply(flushApplyAsync);
});
}, null, '$applyAsync');
}
}
}];
+2 -2
View File
@@ -440,7 +440,7 @@ function $SceDelegateProvider() {
// If we get here, then we will either sanitize the value or throw an exception.
if (type === SCE_CONTEXTS.MEDIA_URL || type === SCE_CONTEXTS.URL) {
// we attempt to sanitize non-resource URLs
return $$sanitizeUri(maybeTrusted, type === SCE_CONTEXTS.MEDIA_URL);
return $$sanitizeUri(maybeTrusted.toString(), type === SCE_CONTEXTS.MEDIA_URL);
} else if (type === SCE_CONTEXTS.RESOURCE_URL) {
if (isResourceUrlAllowedByPolicy(maybeTrusted)) {
return maybeTrusted;
@@ -623,7 +623,7 @@ function $SceDelegateProvider() {
* | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. |
* | `$sce.MEDIA_URL` | For URLs that are safe to render as media. Is automatically converted from string by sanitizing when needed. |
* | `$sce.URL` | For URLs that are safe to follow as links. Is automatically converted from string by sanitizing when needed. Note that `$sce.URL` makes a stronger statement about the URL than `$sce.MEDIA_URL` does and therefore contexts requiring values trusted for `$sce.URL` can be used anywhere that values trusted for `$sce.MEDIA_URL` are required.|
* | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` or `$sce.MEDIA_URL` do and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` or `$sce.MEDIA_URL` are required. |
* | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` or `$sce.MEDIA_URL` do and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` or `$sce.MEDIA_URL` are required. <br><br> The {@link $sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider#resourceUrlWhitelist()} and {@link $sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider#resourceUrlBlacklist()} can be used to restrict trusted origins for `RESOURCE_URL` |
* | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. |
*
*
+122
View File
@@ -0,0 +1,122 @@
'use strict';
/**
* ! This is a private undocumented service !
*
* @name $$taskTrackerFactory
* @description
* A function to create `TaskTracker` instances.
*
* A `TaskTracker` can keep track of pending tasks (grouped by type) and can notify interested
* parties when all pending tasks (or tasks of a specific type) have been completed.
*
* @param {$log} log - A logger instance (such as `$log`). Used to log error during callback
* execution.
*
* @this
*/
function $$TaskTrackerFactoryProvider() {
this.$get = valueFn(function(log) { return new TaskTracker(log); });
}
function TaskTracker(log) {
var self = this;
var taskCounts = {};
var taskCallbacks = [];
var ALL_TASKS_TYPE = self.ALL_TASKS_TYPE = '$$all$$';
var DEFAULT_TASK_TYPE = self.DEFAULT_TASK_TYPE = '$$default$$';
/**
* Execute the specified function and decrement the appropriate `taskCounts` counter.
* If the counter reaches 0, all corresponding `taskCallbacks` are executed.
*
* @param {Function} fn - The function to execute.
* @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task that is being completed.
*/
self.completeTask = completeTask;
/**
* Increase the task count for the specified task type (or the default task type if non is
* specified).
*
* @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task whose count will be increased.
*/
self.incTaskCount = incTaskCount;
/**
* Execute the specified callback when all pending tasks have been completed.
*
* If there are no pending tasks, the callback is executed immediately. You can optionally limit
* the tasks that will be waited for to a specific type, by passing a `taskType`.
*
* @param {function} callback - The function to call when there are no pending tasks.
* @param {string=} [taskType=ALL_TASKS_TYPE] - The type of tasks that will be waited for.
*/
self.notifyWhenNoPendingTasks = notifyWhenNoPendingTasks;
function completeTask(fn, taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
try {
fn();
} finally {
decTaskCount(taskType);
var countForType = taskCounts[taskType];
var countForAll = taskCounts[ALL_TASKS_TYPE];
// If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks.
if (!countForAll || !countForType) {
var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType;
var nextCb;
while ((nextCb = getNextCallback(taskType))) {
try {
nextCb();
} catch (e) {
log.error(e);
}
}
}
}
}
function decTaskCount(taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
if (taskCounts[taskType]) {
taskCounts[taskType]--;
taskCounts[ALL_TASKS_TYPE]--;
}
}
function getLastCallback() {
var cbInfo = taskCallbacks.pop();
return cbInfo && cbInfo.cb;
}
function getLastCallbackForType(taskType) {
for (var i = taskCallbacks.length - 1; i >= 0; --i) {
var cbInfo = taskCallbacks[i];
if (cbInfo.type === taskType) {
taskCallbacks.splice(i, 1);
return cbInfo.cb;
}
}
}
function incTaskCount(taskType) {
taskType = taskType || DEFAULT_TASK_TYPE;
taskCounts[taskType] = (taskCounts[taskType] || 0) + 1;
taskCounts[ALL_TASKS_TYPE] = (taskCounts[ALL_TASKS_TYPE] || 0) + 1;
}
function notifyWhenNoPendingTasks(callback, taskType) {
taskType = taskType || ALL_TASKS_TYPE;
if (!taskCounts[taskType]) {
callback();
} else {
taskCallbacks.push({type: taskType, cb: callback});
}
}
}
+9 -1
View File
@@ -104,7 +104,15 @@ function $$TestabilityProvider() {
* @name $$testability#whenStable
*
* @description
* Calls the callback when $timeout and $http requests are completed.
* Calls the callback when all pending tasks are completed.
*
* Types of tasks waited for include:
* - Pending timeouts (via {@link $timeout}).
* - Pending HTTP requests (via {@link $http}).
* - In-progress route transitions (via {@link $route}).
* - Pending tasks scheduled via {@link $rootScope#$applyAsync}.
* - Pending tasks scheduled via {@link $rootScope#$evalAsync}.
* These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises).
*
* @param {function} callback
*/
+1 -1
View File
@@ -63,7 +63,7 @@ function $TimeoutProvider() {
}
if (!skipApply) $rootScope.$apply();
}, delay);
}, delay, '$timeout');
promise.$$timeoutId = timeoutId;
deferreds[timeoutId] = deferred;
+1
View File
@@ -73,6 +73,7 @@
/* ngAnimate directives/services */
"ngAnimateSwapDirective": true,
"$$rAFSchedulerFactory": true,
"$$AnimateCacheProvider": true,
"$$AnimateChildrenDirective": true,
"$$AnimateQueueProvider": true,
"$$AnimationProvider": true,
+57
View File
@@ -0,0 +1,57 @@
'use strict';
/** @this */
var $$AnimateCacheProvider = function() {
var KEY = '$$ngAnimateParentKey';
var parentCounter = 0;
var cache = Object.create(null);
this.$get = [function() {
return {
cacheKey: function(node, method, addClass, removeClass) {
var parentNode = node.parentNode;
var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
var parts = [parentID, method, node.getAttribute('class')];
if (addClass) {
parts.push(addClass);
}
if (removeClass) {
parts.push(removeClass);
}
return parts.join(' ');
},
containsCachedAnimationWithoutDuration: function(key) {
var entry = cache[key];
// nothing cached, so go ahead and animate
// otherwise it should be a valid animation
return (entry && !entry.isValid) || false;
},
flush: function() {
cache = Object.create(null);
},
count: function(key) {
var entry = cache[key];
return entry ? entry.total : 0;
},
get: function(key) {
var entry = cache[key];
return entry && entry.value;
},
put: function(key, value, isValid) {
if (!cache[key]) {
cache[key] = { total: 1, value: value, isValid: isValid };
} else {
cache[key].total++;
cache[key].value = value;
}
}
};
}];
};
+34 -58
View File
@@ -304,33 +304,6 @@ function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
return [style, value];
}
function createLocalCacheLookup() {
var cache = Object.create(null);
return {
flush: function() {
cache = Object.create(null);
},
count: function(key) {
var entry = cache[key];
return entry ? entry.total : 0;
},
get: function(key) {
var entry = cache[key];
return entry && entry.value;
},
put: function(key, value) {
if (!cache[key]) {
cache[key] = { total: 1, value: value };
} else {
cache[key].total++;
}
}
};
}
// we do not reassign an already present style value since
// if we detect the style property value again we may be
// detecting styles that were added via the `from` styles.
@@ -349,26 +322,16 @@ function registerRestorableStyles(backup, node, properties) {
}
var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animateProvider) {
var gcsLookup = createLocalCacheLookup();
var gcsStaggerLookup = createLocalCacheLookup();
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$animateCache',
'$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue',
function($window, $$jqLite, $$AnimateRunner, $timeout,
function($window, $$jqLite, $$AnimateRunner, $timeout, $$animateCache,
$$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) {
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
var parentCounter = 0;
function gcsHashFn(node, extraClasses) {
var KEY = '$$ngAnimateParentKey';
var parentNode = node.parentNode;
var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
return parentID + '-' + node.getAttribute('class') + '-' + extraClasses;
}
function computeCachedCssStyles(node, className, cacheKey, properties) {
var timings = gcsLookup.get(cacheKey);
function computeCachedCssStyles(node, className, cacheKey, allowNoDuration, properties) {
var timings = $$animateCache.get(cacheKey);
if (!timings) {
timings = computeCssStyles($window, node, properties);
@@ -377,20 +340,26 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
}
}
// if a css animation has no duration we
// should mark that so that repeated addClass/removeClass calls are skipped
var hasDuration = allowNoDuration || (timings.transitionDuration > 0 || timings.animationDuration > 0);
// we keep putting this in multiple times even though the value and the cacheKey are the same
// because we're keeping an internal tally of how many duplicate animations are detected.
gcsLookup.put(cacheKey, timings);
$$animateCache.put(cacheKey, timings, hasDuration);
return timings;
}
function computeCachedCssStaggerStyles(node, className, cacheKey, properties) {
var stagger;
var staggerCacheKey = 'stagger-' + cacheKey;
// if we have one or more existing matches of matching elements
// containing the same parent + CSS styles (which is how cacheKey works)
// then staggering is possible
if (gcsLookup.count(cacheKey) > 0) {
stagger = gcsStaggerLookup.get(cacheKey);
if ($$animateCache.count(cacheKey) > 0) {
stagger = $$animateCache.get(staggerCacheKey);
if (!stagger) {
var staggerClassName = pendClasses(className, '-stagger');
@@ -405,7 +374,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
$$jqLite.removeClass(node, staggerClassName);
gcsStaggerLookup.put(cacheKey, stagger);
$$animateCache.put(staggerCacheKey, stagger, true);
}
}
@@ -416,8 +385,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
function waitUntilQuiet(callback) {
rafWaitQueue.push(callback);
$$rAFScheduler.waitUntilQuiet(function() {
gcsLookup.flush();
gcsStaggerLookup.flush();
$$animateCache.flush();
// DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
// PLEASE EXAMINE THE `$$forceReflow` service to understand why.
@@ -432,8 +400,8 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
});
}
function computeTimings(node, className, cacheKey) {
var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES);
function computeTimings(node, className, cacheKey, allowNoDuration) {
var timings = computeCachedCssStyles(node, className, cacheKey, allowNoDuration, DETECT_CSS_PROPERTIES);
var aD = timings.animationDelay;
var tD = timings.transitionDelay;
timings.maxDelay = aD && tD
@@ -520,7 +488,6 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
var fullClassName = classes + ' ' + preparationClasses;
var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
var hasToStyles = styles.to && Object.keys(styles.to).length > 0;
var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0;
@@ -533,7 +500,12 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
return closeAndReturnNoopAnimator();
}
var cacheKey, stagger;
var stagger, cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass);
if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) {
preparationClasses = null;
return closeAndReturnNoopAnimator();
}
if (options.stagger > 0) {
var staggerVal = parseFloat(options.stagger);
stagger = {
@@ -543,7 +515,6 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
animationDuration: 0
};
} else {
cacheKey = gcsHashFn(node, fullClassName);
stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES);
}
@@ -577,7 +548,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
var itemIndex = stagger
? options.staggerIndex >= 0
? options.staggerIndex
: gcsLookup.count(cacheKey)
: $$animateCache.count(cacheKey)
: 0;
var isFirst = itemIndex === 0;
@@ -592,7 +563,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
}
var timings = computeTimings(node, fullClassName, cacheKey);
var timings = computeTimings(node, fullClassName, cacheKey, !isStructural);
var relativeDelay = timings.maxDelay;
maxDelay = Math.max(relativeDelay, 0);
maxDuration = timings.maxDuration;
@@ -630,6 +601,8 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
return closeAndReturnNoopAnimator();
}
var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
if (options.delay != null) {
var delayStyle;
if (typeof options.delay !== 'boolean') {
@@ -717,10 +690,13 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
animationClosed = true;
animationPaused = false;
if (!options.$$skipPreparationClasses) {
if (preparationClasses && !options.$$skipPreparationClasses) {
$$jqLite.removeClass(element, preparationClasses);
}
$$jqLite.removeClass(element, activeClasses);
if (activeClasses) {
$$jqLite.removeClass(element, activeClasses);
}
blockKeyframeAnimations(node, false);
blockTransitions(node, false);
@@ -904,9 +880,9 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
if (flags.recalculateTimingStyles) {
fullClassName = node.getAttribute('class') + ' ' + preparationClasses;
cacheKey = gcsHashFn(node, fullClassName);
cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass);
timings = computeTimings(node, fullClassName, cacheKey);
timings = computeTimings(node, fullClassName, cacheKey, false);
relativeDelay = timings.maxDelay;
maxDelay = Math.max(relativeDelay, 0);
maxDuration = timings.maxDuration;
+23 -5
View File
@@ -13,6 +13,15 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
join: []
};
function getEventData(options) {
return {
addClass: options.addClass,
removeClass: options.removeClass,
from: options.from,
to: options.to
};
}
function makeTruthyCssClassMap(classString) {
if (!classString) {
return null;
@@ -111,6 +120,10 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
var disabledElementsLookup = new $$Map();
var animationsEnabled = null;
function removeFromDisabledElementsLookup(evt) {
disabledElementsLookup.delete(evt.target);
}
function postDigestTaskFactory() {
var postDigestCalled = false;
return function(fn) {
@@ -294,6 +307,11 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
bool = !disabledElementsLookup.get(node);
} else {
// (element, bool) - Element setter
if (!disabledElementsLookup.has(node)) {
// The element is added to the map for the first time.
// Create a listener to remove it on `$destroy` (to avoid memory leak).
jqLite(element).on('$destroy', removeFromDisabledElementsLookup);
}
disabledElementsLookup.set(node, !bool);
}
}
@@ -379,9 +397,9 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
if (skipAnimations) {
// Callbacks should fire even if the document is hidden (regression fix for issue #14120)
if (documentHidden) notifyProgress(runner, event, 'start');
if (documentHidden) notifyProgress(runner, event, 'start', getEventData(options));
close();
if (documentHidden) notifyProgress(runner, event, 'close');
if (documentHidden) notifyProgress(runner, event, 'close', getEventData(options));
return runner;
}
@@ -438,7 +456,7 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
if (existingAnimation.state === RUNNING_STATE) {
normalizeAnimationDetails(element, newAnimation);
} else {
applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
applyGeneratedPreparationClasses($$jqLite, element, isStructural ? event : null, options);
event = newAnimation.event = existingAnimation.event;
options = mergeAnimationDetails(element, existingAnimation, newAnimation);
@@ -543,7 +561,7 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
// this will update the runner's flow-control events based on
// the `realRunner` object.
runner.setHost(realRunner);
notifyProgress(runner, event, 'start', {});
notifyProgress(runner, event, 'start', getEventData(options));
realRunner.done(function(status) {
close(!status);
@@ -551,7 +569,7 @@ var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animate
if (animationDetails && animationDetails.counter === counter) {
clearElementAnimationState(node);
}
notifyProgress(runner, event, 'close', {});
notifyProgress(runner, event, 'close', getEventData(options));
});
});
+56 -17
View File
@@ -8,6 +8,7 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
var drivers = this.drivers = [];
var RUNNER_STORAGE_KEY = '$$animationRunner';
var PREPARE_CLASSES_KEY = '$$animatePrepareClasses';
function setRunner(element, runner) {
element.data(RUNNER_STORAGE_KEY, runner);
@@ -21,8 +22,8 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
return element.data(RUNNER_STORAGE_KEY);
}
this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$Map', '$$rAFScheduler',
function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$Map, $$rAFScheduler) {
this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$Map', '$$rAFScheduler', '$$animateCache',
function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$Map, $$rAFScheduler, $$animateCache) {
var animationQueue = [];
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
@@ -37,6 +38,7 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
var animation = animations[i];
lookup.set(animation.domNode, animations[i] = {
domNode: animation.domNode,
element: animation.element,
fn: animation.fn,
children: []
});
@@ -93,7 +95,7 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
result.push(row);
row = [];
}
row.push(entry.fn);
row.push(entry);
entry.children.forEach(function(childEntry) {
nextLevelEntries++;
queue.push(childEntry);
@@ -128,8 +130,6 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
return runner;
}
setRunner(element, runner);
var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass));
var tempClasses = options.tempClasses;
if (tempClasses) {
@@ -137,12 +137,12 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
options.tempClasses = null;
}
var prepareClassName;
if (isStructural) {
prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX;
$$jqLite.addClass(element, prepareClassName);
element.data(PREPARE_CLASSES_KEY, 'ng-' + event + PREPARE_CLASS_SUFFIX);
}
setRunner(element, runner);
animationQueue.push({
// this data is used by the postDigest code and passed into
// the driver step function
@@ -182,16 +182,31 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
var toBeSortedAnimations = [];
forEach(groupedAnimations, function(animationEntry) {
var element = animationEntry.from ? animationEntry.from.element : animationEntry.element;
var extraClasses = options.addClass;
extraClasses = (extraClasses ? (extraClasses + ' ') : '') + NG_ANIMATE_CLASSNAME;
var cacheKey = $$animateCache.cacheKey(element[0], animationEntry.event, extraClasses, options.removeClass);
toBeSortedAnimations.push({
domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element),
element: element,
domNode: getDomNode(element),
fn: function triggerAnimationStart() {
var startAnimationFn, closeFn = animationEntry.close;
// in the event that we've cached the animation status for this element
// and it's in fact an invalid animation (something that has duration = 0)
// then we should skip all the heavy work from here on
if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) {
closeFn();
return;
}
// it's important that we apply the `ng-animate` CSS class and the
// temporary classes before we do any driver invoking since these
// CSS classes may be required for proper CSS detection.
animationEntry.beforeStart();
var startAnimationFn, closeFn = animationEntry.close;
// in the event that the element was removed before the digest runs or
// during the RAF sequencing then we should not trigger the animation.
var targetElement = animationEntry.anchors
@@ -221,7 +236,32 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
// we need to sort each of the animations in order of parent to child
// relationships. This ensures that the child classes are applied at the
// right time.
$$rAFScheduler(sortAnimations(toBeSortedAnimations));
var finalAnimations = sortAnimations(toBeSortedAnimations);
for (var i = 0; i < finalAnimations.length; i++) {
var innerArray = finalAnimations[i];
for (var j = 0; j < innerArray.length; j++) {
var entry = innerArray[j];
var element = entry.element;
// the RAFScheduler code only uses functions
finalAnimations[i][j] = entry.fn;
// the first row of elements shouldn't have a prepare-class added to them
// since the elements are at the top of the animation hierarchy and they
// will be applied without a RAF having to pass...
if (i === 0) {
element.removeData(PREPARE_CLASSES_KEY);
continue;
}
var prepareClassName = element.data(PREPARE_CLASSES_KEY);
if (prepareClassName) {
$$jqLite.addClass(element, prepareClassName);
}
}
}
$$rAFScheduler(finalAnimations);
});
return runner;
@@ -359,10 +399,10 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
}
function beforeStart() {
element.addClass(NG_ANIMATE_CLASSNAME);
if (tempClasses) {
$$jqLite.addClass(element, tempClasses);
}
tempClasses = (tempClasses ? (tempClasses + ' ') : '') + NG_ANIMATE_CLASSNAME;
$$jqLite.addClass(element, tempClasses);
var prepareClassName = element.data(PREPARE_CLASSES_KEY);
if (prepareClassName) {
$$jqLite.removeClass(element, prepareClassName);
prepareClassName = null;
@@ -402,7 +442,6 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
$$jqLite.removeClass(element, tempClasses);
}
element.removeClass(NG_ANIMATE_CLASSNAME);
runner.complete(!rejected);
}
};
+36 -14
View File
@@ -17,20 +17,28 @@
* ## Directive Support
* The following directives are "animation aware":
*
* | Directive | Supported Animations |
* |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
* | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
* | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
* | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
* | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
* | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
* | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) |
* | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) |
* | {@link ng.directive:form#animations form} & {@link ng.directive:ngModel#animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
* | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) |
* | {@link module:ngMessages#animations ngMessage} | enter and leave |
* | Directive | Supported Animations |
* |-------------------------------------------------------------------------------|---------------------------------------------------------------------------|
* | {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
* | {@link ngAnimate.directive:ngAnimateSwap#animations ngAnimateSwap} | enter and leave |
* | {@link ng.directive:ngClass#animations ngClass / {{class&#125;&#8203;&#125;} | add and remove |
* | {@link ng.directive:ngClassEven#animations ngClassEven} | add and remove |
* | {@link ng.directive:ngClassOdd#animations ngClassOdd} | add and remove |
* | {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) |
* | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
* | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
* | {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
* | {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) |
* | {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
* | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move |
* | {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
* | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
* | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
*
* (More information can be found by visiting each the documentation associated with each directive.)
* (More information can be found by visiting the documentation associated with each directive.)
*
* For a full breakdown of the steps involved during each animation event, refer to the
* {@link ng.$animate `$animate` API docs}.
*
* ## CSS-based Animations
*
@@ -267,9 +275,22 @@
* .message.ng-enter-prepare {
* opacity: 0;
* }
*
* ```
*
* ### Animating between value changes
*
* Sometimes you need to animate between different expression states, whose values
* don't necessary need to be known or referenced in CSS styles.
* Unless possible with another {@link ngAnimate#directive-support "animation aware" directive},
* that specific use case can always be covered with {@link ngAnimate.directive:ngAnimateSwap} as
* can be seen in {@link ngAnimate.directive:ngAnimateSwap#examples this example}.
*
* Note that {@link ngAnimate.directive:ngAnimateSwap} is a *structural directive*, which means it
* creates a new instance of the element (including any other/child directives it may have) and
* links it to a new scope every time *swap* happens. In some cases this might not be desirable
* (e.g. for performance reasons, or when you wish to retain internal state on the original
* element instance).
*
* ## JavaScript-based Animations
*
* ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared
@@ -765,6 +786,7 @@ angular.module('ngAnimate', [], function initAngularHelpers() {
.factory('$$rAFScheduler', $$rAFSchedulerFactory)
.provider('$$animateQueue', $$AnimateQueueProvider)
.provider('$$animateCache', $$AnimateCacheProvider)
.provider('$$animation', $$AnimationProvider)
.provider('$animateCss', $AnimateCssProvider)
+1 -1
View File
@@ -301,7 +301,7 @@ function getDomNode(element) {
return (element instanceof jqLite) ? element[0] : element;
}
function applyGeneratedPreparationClasses(element, event, options) {
function applyGeneratedPreparationClasses($$jqLite, element, event, options) {
var classes = '';
if (event) {
classes = pendClasses(event, EVENT_CLASS_PREFIX, true);
+28 -5
View File
@@ -14,8 +14,8 @@
*
* For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following
* directives are supported:
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`,
* `ngDblClick`, and `ngMessages`.
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`,
* `ngClick`, `ngDblClick`, and `ngMessages`.
*
* Below is a more detailed breakdown of the attributes handled by ngAria:
*
@@ -46,11 +46,17 @@
* <md-checkbox ng-disabled="disabled" aria-disabled="true">
* ```
*
* ## Disabling Attributes
* It's possible to disable individual attributes added by ngAria with the
* ## Disabling Specific Attributes
* It is possible to disable individual attributes added by ngAria with the
* {@link ngAria.$ariaProvider#config config} method. For more details, see the
* {@link guide/accessibility Developer Guide}.
*
* ## Disabling `ngAria` on Specific Elements
* It is possible to make `ngAria` ignore a specific element, by adding the `ng-aria-disable`
* attribute on it. Note that only the element itself (and not its child elements) will be ignored.
*/
var ARIA_DISABLE_ATTR = 'ngAriaDisable';
var ngAriaModule = angular.module('ngAria', ['ng']).
info({ angularVersion: '"NG_VERSION_FULL"' }).
provider('$aria', $AriaProvider);
@@ -132,6 +138,8 @@ function $AriaProvider() {
function watchExpr(attrName, ariaAttr, nodeBlackList, negate) {
return function(scope, elem, attr) {
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
var ariaCamelName = attr.$normalize(ariaAttr);
if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) {
scope.$watch(attr[attrName], function(boolVal) {
@@ -251,6 +259,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
require: 'ngModel',
priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value
compile: function(elem, attr) {
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
var shape = getShape(attr, elem);
return {
@@ -347,6 +357,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
restrict: 'A',
require: '?ngMessages',
link: function(scope, elem, attr, ngMessages) {
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
if (!elem.attr('aria-live')) {
elem.attr('aria-live', 'assertive');
}
@@ -357,6 +369,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
return {
restrict: 'A',
compile: function(elem, attr) {
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
var fn = $parse(attr.ngClick);
return function(scope, elem, attr) {
@@ -373,7 +387,14 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
if ($aria.config('bindKeydown') && !attr.ngKeydown && !attr.ngKeypress && !attr.ngKeyup) {
elem.on('keydown', function(event) {
var keyCode = event.which || event.keyCode;
if (keyCode === 32 || keyCode === 13) {
if (keyCode === 13 || keyCode === 32) {
// If the event is triggered on a non-interactive element ...
if (nodeBlackList.indexOf(event.target.nodeName) === -1) {
// ... prevent the default browser behavior (e.g. scrolling when pressing spacebar)
// See https://github.com/angular/angular.js/issues/16664
event.preventDefault();
}
scope.$apply(callback);
}
@@ -389,6 +410,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
}])
.directive('ngDblclick', ['$aria', function($aria) {
return function(scope, elem, attr) {
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) {
elem.attr('tabindex', 0);
}
+128 -46
View File
@@ -18,7 +18,7 @@ var jqLite;
* sequencing based on the order of how the messages are defined in the template.
*
* Currently, the ngMessages module only contains the code for the `ngMessages`, `ngMessagesInclude`
* `ngMessage` and `ngMessageExp` directives.
* `ngMessage`, `ngMessageExp` and `ngMessageDefault` directives.
*
* ## Usage
* The `ngMessages` directive allows keys in a key/value collection to be associated with a child element
@@ -257,7 +257,26 @@ var jqLite;
* .some-message.ng-leave.ng-leave-active {}
* ```
*
* {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate.
* {@link ngAnimate See the ngAnimate docs} to learn how to use JavaScript animations or to learn
* more about ngAnimate.
*
* ## Displaying a default message
* If the ngMessages renders no inner ngMessage directive (i.e. when none of the truthy
* keys are matched by a defined message), then it will render a default message
* using the {@link ngMessageDefault} directive.
* Note that matched messages will always take precedence over unmatched messages. That means
* the default message will not be displayed when another message is matched. This is also
* true for `ng-messages-multiple`.
*
* ```html
* <div ng-messages="myForm.myField.$error" role="alert">
* <div ng-message="required">This field is required</div>
* <div ng-message="minlength">This field is too short</div>
* <div ng-message-default>This field has an input error</div>
* </div>
* ```
*
*/
angular.module('ngMessages', [], function initAngularHelpers() {
// Access helpers from AngularJS core.
@@ -286,8 +305,11 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* at a time and this depends on the prioritization of the messages within the template. (This can
* be changed by using the `ng-messages-multiple` or `multiple` attribute on the directive container.)
*
* A remote template can also be used to promote message reusability and messages can also be
* overridden.
* A remote template can also be used (With {@link ngMessagesInclude}) to promote message
* reusability and messages can also be overridden.
*
* A default message can also be displayed when no `ngMessage` directive is inserted, using the
* {@link ngMessageDefault} directive.
*
* {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`.
*
@@ -298,6 +320,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <ANY ng-message="stringValue">...</ANY>
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
* <ANY ng-message-exp="expressionValue">...</ANY>
* <ANY ng-message-default>...</ANY>
* </ANY>
*
* <!-- or by using element directives -->
@@ -305,6 +328,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <ng-message when="stringValue">...</ng-message>
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
* <ng-message when-exp="expressionValue">...</ng-message>
* <ng-message-default>...</ng-message-default>
* </ng-messages>
* ```
*
@@ -333,6 +357,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <div ng-message="required">You did not enter a field</div>
* <div ng-message="minlength">Your field is too short</div>
* <div ng-message="maxlength">Your field is too long</div>
* <div ng-message-default>This field has an input error</div>
* </div>
* </form>
* </file>
@@ -370,6 +395,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
var unmatchedMessages = [];
var matchedKeys = {};
var truthyKeys = 0;
var messageItem = ctrl.head;
var messageFound = false;
var totalMessages = 0;
@@ -382,13 +408,17 @@ angular.module('ngMessages', [], function initAngularHelpers() {
var messageUsed = false;
if (!messageFound) {
forEach(collection, function(value, key) {
if (!messageUsed && truthy(value) && messageCtrl.test(key)) {
// this is to prevent the same error name from showing up twice
if (matchedKeys[key]) return;
matchedKeys[key] = true;
if (truthy(value) && !messageUsed) {
truthyKeys++;
messageUsed = true;
messageCtrl.attach();
if (messageCtrl.test(key)) {
// this is to prevent the same error name from showing up twice
if (matchedKeys[key]) return;
matchedKeys[key] = true;
messageUsed = true;
messageCtrl.attach();
}
}
});
}
@@ -408,7 +438,16 @@ angular.module('ngMessages', [], function initAngularHelpers() {
messageCtrl.detach();
});
if (unmatchedMessages.length !== totalMessages) {
var messageMatched = unmatchedMessages.length !== totalMessages;
var attachDefault = ctrl.default && !messageMatched && truthyKeys > 0;
if (attachDefault) {
ctrl.default.attach();
} else if (ctrl.default) {
ctrl.default.detach();
}
if (messageMatched || attachDefault) {
$animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS);
} else {
$animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS);
@@ -428,23 +467,31 @@ angular.module('ngMessages', [], function initAngularHelpers() {
}
};
this.register = function(comment, messageCtrl) {
var nextKey = latestKey.toString();
messages[nextKey] = {
message: messageCtrl
};
insertMessageNode($element[0], comment, nextKey);
comment.$$ngMessageNode = nextKey;
latestKey++;
this.register = function(comment, messageCtrl, isDefault) {
if (isDefault) {
ctrl.default = messageCtrl;
} else {
var nextKey = latestKey.toString();
messages[nextKey] = {
message: messageCtrl
};
insertMessageNode($element[0], comment, nextKey);
comment.$$ngMessageNode = nextKey;
latestKey++;
}
ctrl.reRender();
};
this.deregister = function(comment) {
var key = comment.$$ngMessageNode;
delete comment.$$ngMessageNode;
removeMessageNode($element[0], comment, key);
delete messages[key];
this.deregister = function(comment, isDefault) {
if (isDefault) {
delete ctrl.default;
} else {
var key = comment.$$ngMessageNode;
delete comment.$$ngMessageNode;
removeMessageNode($element[0], comment, key);
delete messages[key];
}
ctrl.reRender();
};
@@ -647,9 +694,41 @@ angular.module('ngMessages', [], function initAngularHelpers() {
*
* @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key.
*/
.directive('ngMessageExp', ngMessageDirectiveFactory());
.directive('ngMessageExp', ngMessageDirectiveFactory())
function ngMessageDirectiveFactory() {
/**
* @ngdoc directive
* @name ngMessageDefault
* @restrict AE
* @scope
*
* @description
* `ngMessageDefault` is a directive with the purpose to show and hide a default message for
* {@link directive:ngMessages}, when none of provided messages matches.
*
* More information about using `ngMessageDefault` can be found in the
* {@link module:ngMessages `ngMessages` module documentation}.
*
* @usage
* ```html
* <!-- using attribute directives -->
* <ANY ng-messages="expression" role="alert">
* <ANY ng-message="stringValue">...</ANY>
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
* <ANY ng-message-default>...</ANY>
* </ANY>
*
* <!-- or by using element directives -->
* <ng-messages for="expression" role="alert">
* <ng-message when="stringValue">...</ng-message>
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
* <ng-message-default>...</ng-message-default>
* </ng-messages>
*
*/
.directive('ngMessageDefault', ngMessageDirectiveFactory(true));
function ngMessageDirectiveFactory(isDefault) {
return ['$animate', function($animate) {
return {
restrict: 'AE',
@@ -658,25 +737,28 @@ function ngMessageDirectiveFactory() {
terminal: true,
require: '^^ngMessages',
link: function(scope, element, attrs, ngMessagesCtrl, $transclude) {
var commentNode = element[0];
var commentNode, records, staticExp, dynamicExp;
var records;
var staticExp = attrs.ngMessage || attrs.when;
var dynamicExp = attrs.ngMessageExp || attrs.whenExp;
var assignRecords = function(items) {
records = items
? (isArray(items)
? items
: items.split(/[\s,]+/))
: null;
ngMessagesCtrl.reRender();
};
if (!isDefault) {
commentNode = element[0];
staticExp = attrs.ngMessage || attrs.when;
dynamicExp = attrs.ngMessageExp || attrs.whenExp;
if (dynamicExp) {
assignRecords(scope.$eval(dynamicExp));
scope.$watchCollection(dynamicExp, assignRecords);
} else {
assignRecords(staticExp);
var assignRecords = function(items) {
records = items
? (isArray(items)
? items
: items.split(/[\s,]+/))
: null;
ngMessagesCtrl.reRender();
};
if (dynamicExp) {
assignRecords(scope.$eval(dynamicExp));
scope.$watchCollection(dynamicExp, assignRecords);
} else {
assignRecords(staticExp);
}
}
var currentElement, messageCtrl;
@@ -701,7 +783,7 @@ function ngMessageDirectiveFactory() {
// If the message element was removed via a call to `detach` then `currentElement` will be null
// So this handler only handles cases where something else removed the message element.
if (currentElement && currentElement.$$attachId === $$attachId) {
ngMessagesCtrl.deregister(commentNode);
ngMessagesCtrl.deregister(commentNode, isDefault);
messageCtrl.detach();
}
newScope.$destroy();
@@ -716,14 +798,14 @@ function ngMessageDirectiveFactory() {
$animate.leave(elm);
}
}
});
}, isDefault);
// We need to ensure that this directive deregisters itself when it no longer exists
// Normally this is done when the attached element is destroyed; but if this directive
// gets removed before we attach the message to the DOM there is nothing to watch
// in which case we must deregister when the containing scope is destroyed.
scope.$on('$destroy', function() {
ngMessagesCtrl.deregister(commentNode);
ngMessagesCtrl.deregister(commentNode, isDefault);
});
}
};
+477 -283
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -28,6 +28,9 @@
* - `charcode`: [charCode](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/charcode)
* for keyboard events (keydown, keypress, and keyup).
*
* - `data`: [data](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/data) for
* [CompositionEvents](https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent).
*
* - `elapsedTime`: the elapsedTime for
* [TransitionEvent](https://developer.mozilla.org/docs/Web/API/TransitionEvent)
* and [AnimationEvent](https://developer.mozilla.org/docs/Web/API/AnimationEvent).
+50 -53
View File
@@ -1,5 +1,6 @@
'use strict';
/* global routeToRegExp: false */
/* global shallowCopy: false */
// `isArray` and `isObject` are necessary for `shallowCopy()` (included via `src/shallowCopy.js`).
@@ -183,11 +184,22 @@ function $RouteProvider() {
* `redirectTo` takes precedence over `resolveRedirectTo`, so specifying both on the same
* route definition, will cause the latter to be ignored.
*
* - `[reloadOnUrl=true]` - `{boolean=}` - reload route when any part of the URL changes
* (including the path) even if the new URL maps to the same route.
*
* If the option is set to `false` and the URL in the browser changes, but the new URL maps
* to the same route, then a `$routeUpdate` event is broadcasted on the root scope (without
* reloading the route).
*
* - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()`
* or `$location.hash()` changes.
*
* If the option is set to `false` and url in the browser changes, then
* `$routeUpdate` event is broadcasted on the root scope.
* If the option is set to `false` and the URL in the browser changes, then a `$routeUpdate`
* event is broadcasted on the root scope (without reloading the route).
*
* <div class="alert alert-warning">
* **Note:** This option has no effect if `reloadOnUrl` is set to `false`.
* </div>
*
* - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive
*
@@ -202,6 +214,9 @@ function $RouteProvider() {
this.when = function(path, route) {
//copy original route object to preserve params inherited from proto chain
var routeCopy = shallowCopy(route);
if (angular.isUndefined(routeCopy.reloadOnUrl)) {
routeCopy.reloadOnUrl = true;
}
if (angular.isUndefined(routeCopy.reloadOnSearch)) {
routeCopy.reloadOnSearch = true;
}
@@ -210,7 +225,8 @@ function $RouteProvider() {
}
routes[path] = angular.extend(
routeCopy,
path && pathRegExp(path, routeCopy)
{originalPath: path},
path && routeToRegExp(path, routeCopy)
);
// create redirection for trailing slashes
@@ -220,8 +236,8 @@ function $RouteProvider() {
: path + '/';
routes[redirectPath] = angular.extend(
{redirectTo: path},
pathRegExp(redirectPath, routeCopy)
{originalPath: path, redirectTo: path},
routeToRegExp(redirectPath, routeCopy)
);
}
@@ -239,47 +255,6 @@ function $RouteProvider() {
*/
this.caseInsensitiveMatch = false;
/**
* @param path {string} path
* @param opts {Object} options
* @return {?Object}
*
* @description
* Normalizes the given path, returning a regular expression
* and the original path.
*
* Inspired by pathRexp in visionmedia/express/lib/utils.js.
*/
function pathRegExp(path, opts) {
var insensitive = opts.caseInsensitiveMatch,
ret = {
originalPath: path,
regexp: path
},
keys = ret.keys = [];
path = path
.replace(/([().])/g, '\\$1')
.replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) {
var optional = (option === '?' || option === '*?') ? '?' : null;
var star = (option === '*' || option === '*?') ? '*' : null;
keys.push({ name: key, optional: !!optional });
slash = slash || '';
return ''
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (star && '(.+?)' || '([^/]+)')
+ (optional || '')
+ ')'
+ (optional || '');
})
.replace(/([/$*])/g, '\\$1');
ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
return ret;
}
/**
* @ngdoc method
* @name $routeProvider#otherwise
@@ -544,8 +519,9 @@ function $RouteProvider() {
* @name $route#$routeUpdate
* @eventType broadcast on root scope
* @description
* The `reloadOnSearch` property has been set to false, and we are reusing the same
* instance of the Controller.
* Broadcasted if the same instance of a route (including template, controller instance,
* resolved dependencies, etc.) is being reused. This can happen if either `reloadOnSearch` or
* `reloadOnUrl` has been set to `false`.
*
* @param {Object} angularEvent Synthetic event object
* @param {Route} current Current/previous route information.
@@ -653,9 +629,7 @@ function $RouteProvider() {
var lastRoute = $route.current;
preparedRoute = parseRoute();
preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route
&& angular.equals(preparedRoute.pathParams, lastRoute.pathParams)
&& !preparedRoute.reloadOnSearch && !forceReload;
preparedRouteIsUpdateOnly = isNavigationUpdateOnly(preparedRoute, lastRoute);
if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) {
if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) {
@@ -680,7 +654,7 @@ function $RouteProvider() {
var nextRoutePromise = $q.resolve(nextRoute);
$browser.$$incOutstandingRequestCount();
$browser.$$incOutstandingRequestCount('$route');
nextRoutePromise.
then(getRedirectionData).
@@ -708,7 +682,7 @@ function $RouteProvider() {
// `outstandingRequestCount` to hit zero. This is important in case we are redirecting
// to a new route which also requires some asynchronous work.
$browser.$$completeOutstandingRequest(noop);
$browser.$$completeOutstandingRequest(noop, '$route');
});
}
}
@@ -835,6 +809,29 @@ function $RouteProvider() {
return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
}
/**
* @param {Object} newRoute - The new route configuration (as returned by `parseRoute()`).
* @param {Object} oldRoute - The previous route configuration (as returned by `parseRoute()`).
* @returns {boolean} Whether this is an "update-only" navigation, i.e. the URL maps to the same
* route and it can be reused (based on the config and the type of change).
*/
function isNavigationUpdateOnly(newRoute, oldRoute) {
// IF this is not a forced reload
return !forceReload
// AND both `newRoute`/`oldRoute` are defined
&& newRoute && oldRoute
// AND they map to the same Route Definition Object
&& (newRoute.$$route === oldRoute.$$route)
// AND `reloadOnUrl` is disabled
&& (!newRoute.reloadOnUrl
// OR `reloadOnSearch` is disabled
|| (!newRoute.reloadOnSearch
// AND both routes have the same path params
&& angular.equals(newRoute.pathParams, oldRoute.pathParams)
)
);
}
/**
* @returns {string} interpolation of the redirect path with the parameters
*/
+46
View File
@@ -0,0 +1,46 @@
'use strict';
/* global routeToRegExp: true */
/**
* @param {string} path - The path to parse. (It is assumed to have query and hash stripped off.)
* @param {Object} opts - Options.
* @return {Object} - An object containing an array of path parameter names (`keys`) and a regular
* expression (`regexp`) that can be used to identify a matching URL and extract the path
* parameter values.
*
* @description
* Parses the given path, extracting path parameter names and a regular expression to match URLs.
*
* Originally inspired by `pathRexp` in `visionmedia/express/lib/utils.js`.
*/
function routeToRegExp(path, opts) {
var keys = [];
var pattern = path
.replace(/([().])/g, '\\$1')
.replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) {
var optional = option === '?' || option === '*?';
var star = option === '*' || option === '*?';
keys.push({name: key, optional: optional});
slash = slash || '';
return (
(optional ? '(?:' + slash : slash + '(?:') +
(star ? '(.+?)' : '([^/]+)') +
(optional ? '?)?' : ')')
);
})
.replace(/([/$*])/g, '\\$1');
if (opts.ignoreTrailingSlashes) {
pattern = pattern.replace(/\/+$/, '') + '/*';
}
return {
keys: keys,
regexp: new RegExp(
'^' + pattern + '(?:[?#]|$)',
opts.caseInsensitiveMatch ? 'i' : ''
)
};
}
+28
View File
@@ -89,6 +89,34 @@ describe('api', function() {
expect(map.get(keys[2])).toBe(values[2]);
});
it('should return if a key exists or not', function() {
var map = new NgMapShim();
var keys = ['foo', {}];
expect(map.has(keys[0])).toBe(false);
expect(map.has(keys[1])).toBe(false);
map.set(keys[0], 'bar');
expect(map.has(keys[0])).toBe(true);
expect(map.has(keys[1])).toBe(false);
map.set(keys[1], 'baz');
expect(map.has(keys[0])).toBe(true);
expect(map.has(keys[1])).toBe(true);
map.delete(keys[0]);
expect(map.has(keys[0])).toBe(false);
expect(map.has(keys[1])).toBe(true);
map.delete(keys[1]);
expect(map.has(keys[0])).toBe(false);
expect(map.has(keys[1])).toBe(false);
map.set(keys[1], 'qux');
expect(map.has(keys[0])).toBe(false);
expect(map.has(keys[1])).toBe(true);
});
it('should be able to deal with `NaN` keys', function() {
var map = new NgMapShim();
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html ng-app="test" require-directive require-target-directive>
<body>
<div id="container"></div>
<script src="angular.js"></script>
<script src="script.js"></script>
</body>
</html>
@@ -0,0 +1,28 @@
'use strict';
angular.
module('test', []).
provider('$exceptionHandler', /** @this */ function() {
this.$get = [function() {
return function(error) {
window.document.querySelector('#container').textContent = error && error.message;
};
}];
}).
directive('requireDirective', function() {
return {
require: '^^requireTargetDirective',
link: function(scope, element, attrs, ctrl) {
window.document.querySelector('#container').textContent = ctrl.content;
}
};
}).
directive('requireTargetDirective', function() {
return {
controller: function() {
this.content = 'requiredContent';
}
};
});
@@ -0,0 +1,10 @@
'use strict';
describe('require parent controller on html element', function() {
it('should not use the html element as the parent element', function() {
loadFixture('directive-require-html');
expect(element(by.id('container')).getText()).toContain('Controller \'requireTargetDirective\', required by directive \'requireDirective\', can\'t be found!');
});
});
+1
View File
@@ -313,6 +313,7 @@ beforeEach(function() {
function generateCompare(isNot) {
return function(actual, namespace, code, content) {
var matcher = new MinErrMatcher(isNot, namespace, code, content, {
inputType: 'error',
expectedAction: 'equal',
+6
View File
@@ -501,6 +501,12 @@ describe('jqLite', function() {
expect(jqLite(c).data('prop')).toBeUndefined();
});
it('should not break on cleanData(), if element has no data', function() {
var selected = jqLite([a, b, c]);
spyOn(jqLite, '_data').and.returnValue(undefined);
expect(function() { jqLite.cleanData(selected); }).not.toThrow();
});
it('should add and remove data on SVGs', function() {
var svg = jqLite('<svg><rect></rect></svg>');
+49 -17
View File
@@ -2,32 +2,57 @@
describe('errors', function() {
var originalObjectMaxDepthInErrorMessage = minErrConfig.objectMaxDepth;
var originalUrlErrorParamsEnabled = minErrConfig.urlErrorParamsEnabled;
afterEach(function() {
minErrConfig.objectMaxDepth = originalObjectMaxDepthInErrorMessage;
minErrConfig.urlErrorParamsEnabled = originalUrlErrorParamsEnabled;
});
describe('errorHandlingConfig', function() {
it('should get default objectMaxDepth', function() {
expect(errorHandlingConfig().objectMaxDepth).toBe(5);
describe('objectMaxDepth',function() {
it('should get default objectMaxDepth', function() {
expect(errorHandlingConfig().objectMaxDepth).toBe(5);
});
it('should set objectMaxDepth', function() {
errorHandlingConfig({objectMaxDepth: 3});
expect(errorHandlingConfig().objectMaxDepth).toBe(3);
});
it('should not change objectMaxDepth when undefined is supplied', function() {
errorHandlingConfig({objectMaxDepth: undefined});
expect(errorHandlingConfig().objectMaxDepth).toBe(originalObjectMaxDepthInErrorMessage);
});
they('should set objectMaxDepth to NaN when $prop is supplied',
[NaN, null, true, false, -1, 0], function(maxDepth) {
errorHandlingConfig({objectMaxDepth: maxDepth});
expect(errorHandlingConfig().objectMaxDepth).toBeNaN();
}
);
});
it('should set objectMaxDepth', function() {
errorHandlingConfig({objectMaxDepth: 3});
expect(errorHandlingConfig().objectMaxDepth).toBe(3);
describe('urlErrorParamsEnabled',function() {
it('should get default urlErrorParamsEnabled', function() {
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(true);
});
it('should set urlErrorParamsEnabled', function() {
errorHandlingConfig({urlErrorParamsEnabled: false});
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(false);
errorHandlingConfig({urlErrorParamsEnabled: true});
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(true);
});
it('should not change its value when non-boolean is supplied', function() {
errorHandlingConfig({urlErrorParamsEnabled: 123});
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(originalUrlErrorParamsEnabled);
});
});
it('should not change objectMaxDepth when undefined is supplied', function() {
errorHandlingConfig({objectMaxDepth: undefined});
expect(errorHandlingConfig().objectMaxDepth).toBe(originalObjectMaxDepthInErrorMessage);
});
they('should set objectMaxDepth to NaN when $prop is supplied',
[NaN, null, true, false, -1, 0], function(maxDepth) {
errorHandlingConfig({objectMaxDepth: maxDepth});
expect(errorHandlingConfig().objectMaxDepth).toBeNaN();
}
);
});
describe('minErr', function() {
@@ -165,7 +190,6 @@ describe('errors', function() {
.toMatch(/^[\s\S]*\?p0=a&p1=b&p2=value%20with%20space$/);
});
it('should strip error reference urls from the error message parameters', function() {
var firstError = testError('firstcode', 'longer string and so on');
@@ -177,5 +201,13 @@ describe('errors', function() {
'%3A%2F%2Ferrors.angularjs.org%2F%22NG_VERSION_FULL%22%2Ftest%2Ffirstcode');
});
it('should not generate URL query parameters when urlErrorParamsEnabled is false', function() {
errorHandlingConfig({urlErrorParamsEnabled: false});
expect(testError('acode', 'aproblem', 'a', 'b', 'c').message).toBe('[test:acode] aproblem\n' +
'https://errors.angularjs.org/"NG_VERSION_FULL"/test/acode');
});
});
});
+172 -25
View File
@@ -34,9 +34,9 @@ function MockWindow(options) {
timeouts[id] = noop;
};
this.setTimeout.flush = function() {
var length = timeouts.length;
while (length-- > 0) timeouts.shift()();
this.setTimeout.flush = function(count) {
count = count || timeouts.length;
while (count-- > 0) timeouts.shift()();
};
this.addEventListener = function(name, listener) {
@@ -143,24 +143,26 @@ function MockDocument() {
}
describe('browser', function() {
/* global Browser: false */
var browser, fakeWindow, fakeDocument, fakeLog, logs, scripts, removedScripts;
/* global Browser: false, TaskTracker: false */
var browser, fakeWindow, fakeDocument, fakeLog, logs, taskTrackerFactory;
beforeEach(function() {
scripts = [];
removedScripts = [];
sniffer = {history: true};
fakeWindow = new MockWindow();
fakeDocument = new MockDocument();
taskTrackerFactory = function(log) { return new TaskTracker(log); };
logs = {log:[], warn:[], info:[], error:[]};
fakeLog = {log: function() { logs.log.push(slice.call(arguments)); },
warn: function() { logs.warn.push(slice.call(arguments)); },
info: function() { logs.info.push(slice.call(arguments)); },
error: function() { logs.error.push(slice.call(arguments)); }};
fakeLog = {
log: function() { logs.log.push(slice.call(arguments)); },
warn: function() { logs.warn.push(slice.call(arguments)); },
info: function() { logs.info.push(slice.call(arguments)); },
error: function() { logs.error.push(slice.call(arguments)); }
};
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
});
describe('MockBrowser', function() {
@@ -200,7 +202,7 @@ describe('browser', function() {
fakeWindow = new MockWindow({msie: msie});
fakeWindow.location.state = {prop: 'val'};
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
browser.url(fakeWindow.location.href, false, {prop: 'val'});
if (msie) {
@@ -214,12 +216,66 @@ describe('browser', function() {
}
});
describe('outstanding requests', function() {
it('should process callbacks immediately with no outstanding requests', function() {
describe('notifyWhenNoOutstandingRequests', function() {
it('should invoke callbacks immediately if there are no pending tasks', function() {
var callback = jasmine.createSpy('callback');
browser.notifyWhenNoOutstandingRequests(callback);
expect(callback).toHaveBeenCalled();
});
it('should invoke callbacks immediately if there are no pending tasks (for specific task-type)',
function() {
var callbackAll = jasmine.createSpy('callbackAll');
var callbackFoo = jasmine.createSpy('callbackFoo');
browser.$$incOutstandingRequestCount();
browser.notifyWhenNoOutstandingRequests(callbackAll);
browser.notifyWhenNoOutstandingRequests(callbackFoo, 'foo');
expect(callbackAll).not.toHaveBeenCalled();
expect(callbackFoo).toHaveBeenCalled();
}
);
it('should invoke callbacks as soon as there are no pending tasks', function() {
var callback = jasmine.createSpy('callback');
browser.$$incOutstandingRequestCount();
browser.notifyWhenNoOutstandingRequests(callback);
expect(callback).not.toHaveBeenCalled();
browser.$$completeOutstandingRequest(noop);
expect(callback).toHaveBeenCalled();
});
it('should invoke callbacks as soon as there are no pending tasks (for specific task-type)',
function() {
var callbackAll = jasmine.createSpy('callbackAll');
var callbackFoo = jasmine.createSpy('callbackFoo');
browser.$$incOutstandingRequestCount();
browser.$$incOutstandingRequestCount('foo');
browser.notifyWhenNoOutstandingRequests(callbackAll);
browser.notifyWhenNoOutstandingRequests(callbackFoo, 'foo');
expect(callbackAll).not.toHaveBeenCalled();
expect(callbackFoo).not.toHaveBeenCalled();
browser.$$completeOutstandingRequest(noop, 'foo');
expect(callbackAll).not.toHaveBeenCalled();
expect(callbackFoo).toHaveBeenCalledOnce();
browser.$$completeOutstandingRequest(noop);
expect(callbackAll).toHaveBeenCalledOnce();
expect(callbackFoo).toHaveBeenCalledOnce();
}
);
});
@@ -236,13 +292,36 @@ describe('browser', function() {
it('should update outstandingRequests counter', function() {
var callback = jasmine.createSpy('deferred');
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
browser.defer(callback);
expect(callback).not.toHaveBeenCalled();
browser.defer(noop);
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
expect(noPendingTasksSpy).not.toHaveBeenCalled();
fakeWindow.setTimeout.flush();
expect(callback).toHaveBeenCalledOnce();
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
});
it('should update outstandingRequests counter (for specific task-type)', function() {
var noPendingFooTasksSpy = jasmine.createSpy('noPendingFooTasks');
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
browser.defer(noop, 0, 'foo');
browser.defer(noop, 0, 'bar');
browser.notifyWhenNoOutstandingRequests(noPendingFooTasksSpy, 'foo');
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
expect(noPendingFooTasksSpy).not.toHaveBeenCalled();
expect(noPendingTasksSpy).not.toHaveBeenCalled();
fakeWindow.setTimeout.flush(1);
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
expect(noPendingTasksSpy).not.toHaveBeenCalled();
fakeWindow.setTimeout.flush(1);
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
});
@@ -270,6 +349,40 @@ describe('browser', function() {
expect(log).toEqual(['ok']);
expect(browser.defer.cancel(deferId2)).toBe(false);
});
it('should update outstandingRequests counter', function() {
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
var deferId = browser.defer(noop);
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
expect(noPendingTasksSpy).not.toHaveBeenCalled();
browser.defer.cancel(deferId);
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
});
it('should update outstandingRequests counter (for specific task-type)', function() {
var noPendingFooTasksSpy = jasmine.createSpy('noPendingFooTasks');
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
var deferId1 = browser.defer(noop, 0, 'foo');
var deferId2 = browser.defer(noop, 0, 'bar');
browser.notifyWhenNoOutstandingRequests(noPendingFooTasksSpy, 'foo');
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
expect(noPendingFooTasksSpy).not.toHaveBeenCalled();
expect(noPendingTasksSpy).not.toHaveBeenCalled();
browser.defer.cancel(deferId1);
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
expect(noPendingTasksSpy).not.toHaveBeenCalled();
browser.defer.cancel(deferId2);
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
});
});
});
@@ -291,6 +404,14 @@ describe('browser', function() {
expect(browser.url()).toEqual('https://another.com');
});
it('should strip an empty hash fragment', function() {
fakeWindow.location.href = 'http://test.com#';
expect(browser.url()).toEqual('http://test.com');
fakeWindow.location.href = 'https://another.com#foo';
expect(browser.url()).toEqual('https://another.com#foo');
});
it('should use history.pushState when available', function() {
sniffer.history = true;
browser.url('http://new.org');
@@ -462,7 +583,7 @@ describe('browser', function() {
// the initial URL contains a lengthy oauth token in the hash
var initialUrl = 'http://test.com/oauthcallback#state=xxx%3D&not-before-policy=0';
fakeWindow.location.href = initialUrl;
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
// somehow, $location gets a version of this url where the = is no longer escaped, and tells the browser:
var initialUrlFixedByLocation = initialUrl.replace('%3D', '=');
@@ -497,7 +618,7 @@ describe('browser', function() {
replaceState = spyOn(fakeWindow.history, 'replaceState').and.callThrough();
locationReplace = spyOn(fakeWindow.location, 'replace').and.callThrough();
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
browser.onUrlChange(function() {});
});
@@ -596,7 +717,7 @@ describe('browser', function() {
}
});
var browser = new Browser(mockWindow, fakeDocument, fakeLog, mockSniffer);
var browser = new Browser(mockWindow, fakeDocument, fakeLog, mockSniffer, taskTrackerFactory);
expect(historyStateAccessed).toBe(false);
});
@@ -609,7 +730,7 @@ describe('browser', function() {
return function() {
beforeEach(function() {
fakeWindow = new MockWindow({msie: options.msie});
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
});
it('should return history.state', function() {
@@ -712,7 +833,7 @@ describe('browser', function() {
return function() {
beforeEach(function() {
fakeWindow = new MockWindow({msie: options.msie});
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
});
it('should fire onUrlChange listeners only once if both popstate and hashchange triggered', function() {
@@ -781,7 +902,7 @@ describe('browser', function() {
function setup(options) {
fakeWindow = new MockWindow(options);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
module(function($provide, $locationProvider) {
@@ -934,6 +1055,32 @@ describe('browser', function() {
expect($location.absUrl()).toEqual('http://server/#otherHash');
});
});
// issue #16632
it('should not trigger `$locationChangeStart` more than once due to trailing `#`', function() {
setup({
history: true,
html5Mode: true
});
inject(function($flushPendingTasks, $location, $rootScope) {
$rootScope.$digest();
var spy = jasmine.createSpy('$locationChangeStart');
$rootScope.$on('$locationChangeStart', spy);
$rootScope.$evalAsync(function() {
fakeWindow.location.href += '#';
});
$rootScope.$digest();
expect(fakeWindow.location.href).toBe('http://server/#');
expect($location.absUrl()).toBe('http://server/');
expect(spy.calls.count()).toBe(0);
expect(spy).not.toHaveBeenCalled();
});
});
});
describe('integration test with $rootScope', function() {
+192 -8
View File
@@ -8843,6 +8843,50 @@ describe('$compile', function() {
});
});
it('should correctly handle multi-element directives', function() {
module(function() {
directive('foo', valueFn({
template: '[<div ng-transclude></div>]',
transclude: true
}));
directive('bar', valueFn({
template: '[<div ng-transclude="header"></div>|<div ng-transclude="footer"></div>]',
transclude: {
header: 'header',
footer: 'footer'
}
}));
});
inject(function($compile, $rootScope) {
var tmplWithFoo =
'<foo>' +
'<div ng-if-start="true">Hello, </div>' +
'<div ng-if-end>world!</div>' +
'</foo>';
var tmplWithBar =
'<bar>' +
'<header ng-if-start="true">This is a </header>' +
'<header ng-if-end>header!</header>' +
'<footer ng-if-start="true">This is a </footer>' +
'<footer ng-if-end>footer!</footer>' +
'</bar>';
var elem1 = $compile(tmplWithFoo)($rootScope);
var elem2 = $compile(tmplWithBar)($rootScope);
$rootScope.$digest();
expect(elem1.text()).toBe('[Hello, world!]');
expect(elem2.text()).toBe('[This is a header!|This is a footer!]');
dealoc(elem1);
dealoc(elem2);
});
});
//see issue https://github.com/angular/angular.js/issues/12936
it('should use the proper scope when it is on the root element of a replaced directive template', function() {
module(function() {
@@ -11436,7 +11480,7 @@ describe('$compile', function() {
expect(element.attr('srcset')).toEqual('http://example.com');
}));
it('does not work with trusted values', inject(function($rootScope, $compile, $sce) {
it('should NOT work with trusted values', inject(function($rootScope, $compile, $sce) {
// A limitation of the approach used for srcset is that you cannot use `trustAsUrl`.
// Use trustAsHtml and ng-bind-html to work around this.
element = $compile('<img srcset="{{testUrl}}"></img>')($rootScope);
@@ -11661,18 +11705,19 @@ describe('$compile', function() {
expect(function() {
$compile('<button onclick="{{onClickJs}}"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
expect(function() {
$compile('<button ONCLICK="{{onClickJs}}"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
expect(function() {
$compile('<button ng-attr-onclick="{{onClickJs}}"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
expect(function() {
$compile('<button ng-attr-ONCLICK="{{onClickJs}}"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
}));
it('should pass through arbitrary values on onXYZ event attributes that contain a hyphen', inject(function($compile, $rootScope) {
@@ -11789,7 +11834,7 @@ describe('$compile', function() {
}));
it('should pass through $sce.trustAs() values in action attribute', inject(function($compile, $rootScope, $sce) {
it('should pass through $sce.trustAsResourceUrl() values in action attribute', inject(function($compile, $rootScope, $sce) {
element = $compile('<form action="{{testUrl}}"></form>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
$rootScope.$apply();
@@ -11982,6 +12027,39 @@ describe('$compile', function() {
expect(element.attr('test3')).toBe('Misko');
}));
it('should use the non-prefixed name in $attr mappings', function() {
var attrs;
module(function() {
directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer ng-attr-title="12" ng-attr-super-title="34" ng-attr-my-camel_title="56">')($rootScope);
$rootScope.$apply();
expect(attrs.title).toBe('12');
expect(attrs.$attr.title).toBe('title');
expect(attrs.ngAttrTitle).toBeUndefined();
expect(attrs.$attr.ngAttrTitle).toBeUndefined();
expect(attrs.superTitle).toBe('34');
expect(attrs.$attr.superTitle).toBe('super-title');
expect(attrs.ngAttrSuperTitle).toBeUndefined();
expect(attrs.$attr.ngAttrSuperTitle).toBeUndefined();
// Note the casing is incorrect: https://github.com/angular/angular.js/issues/16624
expect(attrs.myCameltitle).toBe('56');
expect(attrs.$attr.myCameltitle).toBe('my-camelTitle');
expect(attrs.ngAttrMyCameltitle).toBeUndefined();
expect(attrs.ngAttrMyCamelTitle).toBeUndefined();
expect(attrs.$attr.ngAttrMyCameltitle).toBeUndefined();
expect(attrs.$attr.ngAttrMyCamelTitle).toBeUndefined();
});
});
it('should work with the "href" attribute', inject(function() {
$rootScope.value = 'test';
element = $compile('<a ng-attr-href="test/{{value}}"></a>')($rootScope);
@@ -12068,6 +12146,112 @@ describe('$compile', function() {
});
describe('addPropertySecurityContext', function() {
function testProvider(provider) {
module(provider);
inject(function($compile) { /* done! */ });
}
it('should allow adding new properties', function() {
testProvider(function($compileProvider) {
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
$compileProvider.addPropertySecurityContext('*', 'my-prop', 'resourceUrl');
});
});
it('should allow different sce types of a property on different element types', function() {
testProvider(function($compileProvider) {
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
$compileProvider.addPropertySecurityContext('span', 'title', 'css');
$compileProvider.addPropertySecurityContext('*', 'title', 'resourceUrl');
$compileProvider.addPropertySecurityContext('article', 'title', 'html');
});
});
it('should throw \'ctxoverride\' when changing an existing context', function() {
testProvider(function($compileProvider) {
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
expect(function() {
$compileProvider.addPropertySecurityContext('div', 'title', 'resourceUrl');
})
.toThrowMinErr('$compile', 'ctxoverride', 'Property context \'div.title\' already set to \'mediaUrl\', cannot override to \'resourceUrl\'.');
});
});
it('should allow setting the same property/element to the same value', function() {
testProvider(function($compileProvider) {
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
});
});
it('should enforce the specified sce type for properties added for specific elements', function() {
module(function($compileProvider) {
$compileProvider.addPropertySecurityContext('div', 'foo', 'mediaUrl');
});
inject(function($compile, $rootScope, $sce) {
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
$rootScope.bar = 'untrusted:test1';
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
$rootScope.$apply();
expect(element.prop('foo')).toBe('untrusted:test3');
});
});
it('should enforce the specified sce type for properties added for all elements (*)', function() {
module(function($compileProvider) {
$compileProvider.addPropertySecurityContext('*', 'foo', 'mediaUrl');
});
inject(function($compile, $rootScope, $sce) {
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
$rootScope.bar = 'untrusted:test1';
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
$rootScope.$apply();
expect(element.prop('foo')).toBe('untrusted:test3');
});
});
it('should enforce the specific sce type when both an element specific and generic exist', function() {
module(function($compileProvider) {
$compileProvider.addPropertySecurityContext('*', 'foo', 'css');
$compileProvider.addPropertySecurityContext('div', 'foo', 'mediaUrl');
});
inject(function($compile, $rootScope, $sce) {
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
$rootScope.bar = 'untrusted:test1';
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
$rootScope.$apply();
expect(element.prop('foo')).toBe('untrusted:test3');
});
});
});
describe('when an attribute has an underscore-separated name', function() {
it('should work with different prefixes', inject(function($compile, $rootScope) {
+46
View File
@@ -1200,6 +1200,52 @@ describe('form', function() {
});
});
describe('$getControls', function() {
it('should return an empty array if the controller has no controls', function() {
doc = $compile('<form name="testForm"></form>')(scope);
scope.$digest();
var formCtrl = scope.testForm;
expect(formCtrl.$getControls()).toEqual([]);
});
it('should return a shallow copy of the form controls', function() {
doc = $compile(
'<form name="testForm">' +
'<input ng-model="named" name="foo">' +
'<div ng-form>' +
'<input ng-model="named" name="foo">' +
'</div>' +
'</form>')(scope);
scope.$digest();
var form = doc,
formCtrl = scope.testForm,
formInput = form.children('input').eq(0),
formInputCtrl = formInput.controller('ngModel'),
nestedForm = form.find('div'),
nestedFormCtrl = nestedForm.controller('form'),
nestedInput = nestedForm.children('input').eq(0),
nestedInputCtrl = nestedInput.controller('ngModel');
var controls = formCtrl.$getControls();
expect(controls).not.toBe(formCtrl.$$controls);
controls.push('something');
expect(formCtrl.$$controls).not.toContain('something');
expect(controls[0]).toBe(formInputCtrl);
expect(controls[1]).toBe(nestedFormCtrl);
var nestedControls = controls[1].$getControls();
expect(nestedControls[0]).toBe(nestedInputCtrl);
});
});
it('should rename nested form controls when interpolated name changes', function() {
scope.idA = 'A';
+168 -4
View File
@@ -1384,6 +1384,88 @@ describe('input', function() {
expect($rootScope.form.alias.$error.datetimelocal).toBeTruthy();
});
it('should use the timeSecondsFormat specified in ngModelOptions', function() {
var inputElm = helper.compileInput(
'<input type="datetime-local" ng-model-options="{timeSecondsFormat: \'\'}" ng-model="time"/>'
);
var ctrl = inputElm.controller('ngModel');
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41');
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41');
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss'});
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 5, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:05');
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss.sss'});
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 50);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:50.050');
});
it('should strip empty milliseconds and seconds if specified in ngModelOptions', function() {
var inputElm = helper.compileInput(
'<input type="datetime-local" ng-model-options="{timeStripZeroSeconds: true}" ng-model="threeFortyOnePm"/>'
);
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:50.500');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:00.500');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 0);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:50');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 0);
});
expect(inputElm.val()).toBe('1970-01-01T15:41');
});
it('should apply timeStripZeroSeconds after timeSecondsFormat', function() {
var inputElm = helper.compileInput('<input type="datetime-local"' +
' ng-model-options="{timeSecondsFormat: \'ss\', timeStripZeroSeconds: true}"' +
' ng-model="threeFortyOnePm"/>');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41:50');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('1970-01-01T15:41');
});
describe('min', function() {
var inputElm;
beforeEach(function() {
@@ -1593,7 +1675,7 @@ describe('input', function() {
});
it('should set the view if the model if a valid Date object.', function() {
it('should set the view if the model is a valid Date object.', function() {
var inputElm = helper.compileInput('<input type="time" ng-model="threeFortyOnePm"/>');
$rootScope.$apply(function() {
@@ -1604,7 +1686,7 @@ describe('input', function() {
});
it('should set the model undefined if the view is invalid', function() {
it('should set the model to undefined if the view is invalid', function() {
var inputElm = helper.compileInput('<input type="time" ng-model="breakMe"/>');
$rootScope.$apply(function() {
@@ -1623,7 +1705,7 @@ describe('input', function() {
});
it('should render as blank if null', function() {
it('should set blank if null', function() {
var inputElm = helper.compileInput('<input type="time" ng-model="test" />');
$rootScope.$apply('test = null');
@@ -1633,7 +1715,7 @@ describe('input', function() {
});
it('should come up blank when no value specified', function() {
it('should set blank when no value specified', function() {
var inputElm = helper.compileInput('<input type="time" ng-model="test" />');
expect(inputElm.val()).toBe('');
@@ -1644,6 +1726,88 @@ describe('input', function() {
expect(inputElm.val()).toBe('');
});
it('should use the timeSecondsFormat specified in ngModelOptions', function() {
var inputElm = helper.compileInput(
'<input type="time" ng-model-options="{timeSecondsFormat: \'\'}" ng-model="time"/>'
);
var ctrl = inputElm.controller('ngModel');
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('15:41');
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('15:41');
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss'});
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 5, 500);
});
expect(inputElm.val()).toBe('15:41:05');
ctrl.$overrideModelOptions({timeSecondsFormat: 'ss.sss'});
$rootScope.$apply(function() {
$rootScope.time = new Date(1970, 0, 1, 15, 41, 50, 50);
});
expect(inputElm.val()).toBe('15:41:50.050');
});
it('should strip empty milliseconds and seconds if specified in ngModelOptions', function() {
var inputElm = helper.compileInput(
'<input type="time" ng-model-options="{timeStripZeroSeconds: true}" ng-model="threeFortyOnePm"/>'
);
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('15:41:50.500');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('15:41:00.500');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 0);
});
expect(inputElm.val()).toBe('15:41:50');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 0);
});
expect(inputElm.val()).toBe('15:41');
});
it('should apply timeStripZeroSeconds after timeSecondsFormat', function() {
var inputElm = helper.compileInput('<input type="time"' +
' ng-model-options="{timeSecondsFormat: \'ss\', timeStripZeroSeconds: true}"' +
' ng-model="threeFortyOnePm"/>');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 50, 500);
});
expect(inputElm.val()).toBe('15:41:50');
$rootScope.$apply(function() {
$rootScope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500);
});
expect(inputElm.val()).toBe('15:41');
});
it('should parse empty string to null', function() {
var inputElm = helper.compileInput('<input type="time" ng-model="test" />');
+6
View File
@@ -88,6 +88,12 @@ describe('ngClass', function() {
expect(element.hasClass('AnotB')).toBeFalsy();
}));
it('should not break when passed non-string/array/object, truthy values', inject(function($rootScope, $compile) {
element = $compile('<div ng-class="42"></div>')($rootScope);
$rootScope.$digest();
expect(element.hasClass('42')).toBeTruthy();
}));
it('should support adding multiple classes via an array mixed with conditionally via a map', inject(function($rootScope, $compile) {
element = $compile('<div class="existing" ng-class="[\'A\', {\'B\': condition}]"></div>')($rootScope);
$rootScope.$digest();
+127
View File
@@ -148,6 +148,133 @@ describe('event directives', function() {
expect($rootScope.blur).toHaveBeenCalledOnce();
expect(element.val()).toBe('newValue');
}));
});
it('should call the listener synchronously if the event is triggered inside of a digest',
inject(function($rootScope, $compile) {
var watchedVal;
element = $compile('<button type="button" ng-click="click()">Button</button>')($rootScope);
$rootScope.$watch('value', function(newValue) {
watchedVal = newValue;
});
$rootScope.click = jasmine.createSpy('click').and.callFake(function() {
$rootScope.value = 'newValue';
});
$rootScope.$apply(function() {
element.triggerHandler('click');
});
expect($rootScope.click).toHaveBeenCalledOnce();
expect(watchedVal).toEqual('newValue');
}));
it('should call the listener synchronously if the event is triggered outside of a digest',
inject(function($rootScope, $compile) {
var watchedVal;
element = $compile('<button type="button" ng-click="click()">Button</button>')($rootScope);
$rootScope.$watch('value', function(newValue) {
watchedVal = newValue;
});
$rootScope.click = jasmine.createSpy('click').and.callFake(function() {
$rootScope.value = 'newValue';
});
element.triggerHandler('click');
expect($rootScope.click).toHaveBeenCalledOnce();
expect(watchedVal).toEqual('newValue');
}));
describe('throwing errors in event handlers', function() {
it('should not stop execution if the event is triggered outside a digest', function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
});
inject(function($rootScope, $compile, $exceptionHandler, $log) {
element = $compile('<button ng-click="click()">Click</button>')($rootScope);
expect($log.assertEmpty());
$rootScope.click = function() {
throw new Error('listener error');
};
$rootScope.do = function() {
element.triggerHandler('click');
$log.log('done');
};
$rootScope.do();
expect($exceptionHandler.errors).toEqual([Error('listener error')]);
expect($log.log.logs).toEqual([['done']]);
$log.reset();
});
});
it('should not stop execution if the event is triggered inside a digest', function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
});
inject(function($rootScope, $compile, $exceptionHandler, $log) {
element = $compile('<button ng-click="click()">Click</button>')($rootScope);
expect($log.assertEmpty());
$rootScope.click = function() {
throw new Error('listener error');
};
$rootScope.do = function() {
element.triggerHandler('click');
$log.log('done');
};
$rootScope.$apply(function() {
$rootScope.do();
});
expect($exceptionHandler.errors).toEqual([Error('listener error')]);
expect($log.log.logs).toEqual([['done']]);
$log.reset();
});
});
it('should not stop execution if the event is triggered in a watch expression function', function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
});
inject(function($rootScope, $compile, $exceptionHandler, $log) {
element = $compile('<button ng-click="click()">Click</button>')($rootScope);
$rootScope.click = function() {
throw new Error('listener error');
};
$rootScope.$watch(function() {
element.triggerHandler('click');
$log.log('done');
});
$rootScope.$digest();
expect($exceptionHandler.errors).toEqual([Error('listener error'), Error('listener error')]);
expect($log.log.logs).toEqual([['done'], ['done']]);
$log.reset();
});
});
});
});
+36
View File
@@ -79,6 +79,42 @@ describe('ngHref', function() {
}));
}
it('should bind numbers', inject(function($rootScope, $compile) {
element = $compile('<a ng-href="{{1234}}"></a>')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('1234');
}));
it('should bind and sanitize the result of a (custom) toString() function', inject(function($rootScope, $compile) {
$rootScope.value = {};
element = $compile('<a ng-href="{{value}}"></a>')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('[object Object]');
function SafeClass() {}
SafeClass.prototype.toString = function() {
return 'custom value';
};
$rootScope.value = new SafeClass();
$rootScope.$digest();
expect(element.attr('href')).toEqual('custom value');
function UnsafeClass() {}
UnsafeClass.prototype.toString = function() {
return 'javascript:alert(1);';
};
$rootScope.value = new UnsafeClass();
$rootScope.$digest();
expect(element.attr('href')).toEqual('unsafe:javascript:alert(1);');
}));
if (isDefined(window.SVGElement)) {
describe('SVGAElement', function() {
it('should interpolate the expression and bind to xlink:href', inject(function($compile, $rootScope) {
+8 -2
View File
@@ -6,7 +6,7 @@ describe('ngModel', function() {
describe('NgModelController', function() {
/* global NgModelController: false */
var ctrl, scope, ngModelAccessor, element, parentFormCtrl;
var ctrl, scope, element, parentFormCtrl;
beforeEach(inject(function($rootScope, $controller) {
var attrs = {name: 'testAlias', ngModel: 'value'};
@@ -21,7 +21,6 @@ describe('ngModel', function() {
element = jqLite('<form><input></form>');
scope = $rootScope;
ngModelAccessor = jasmine.createSpy('ngModel accessor');
ctrl = $controller(NgModelController, {
$scope: scope,
$element: element.find('input'),
@@ -438,6 +437,13 @@ describe('ngModel', function() {
expect(ctrl.$modelValue).toBe('c');
expect(scope.value).toBe('c');
}));
it('should not throw an error if the scope has been destroyed', function() {
scope.$destroy();
ctrl.$setViewValue('some-val');
expect(ctrl.$viewValue).toBe('some-val');
});
});
+561
View File
@@ -0,0 +1,561 @@
'use strict';
describe('ngRef', function() {
beforeEach(function() {
jasmine.addMatchers({
toEqualJq: function(util) {
return {
compare: function(actual, expected) {
// Jquery <= 2.2 objects add a context property that is irrelevant for equality
if (actual && actual.hasOwnProperty('context')) {
delete actual.context;
}
if (expected && expected.hasOwnProperty('context')) {
delete expected.context;
}
return {
pass: util.equals(actual, expected)
};
}
};
}
});
});
describe('on a component', function() {
var myComponentController, attributeDirectiveController, $rootScope, $compile;
beforeEach(module(function($compileProvider) {
$compileProvider.component('myComponent', {
template: 'foo',
controller: function() {
myComponentController = this;
}
});
$compileProvider.directive('attributeDirective', function() {
return {
restrict: 'A',
controller: function() {
attributeDirectiveController = this;
}
};
});
}));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
}));
it('should bind in the current scope the controller of a component', function() {
$rootScope.$ctrl = 'undamaged';
$compile('<my-component ng-ref="myComponentRef"></my-component>')($rootScope);
expect($rootScope.$ctrl).toBe('undamaged');
expect($rootScope.myComponentRef).toBe(myComponentController);
});
it('should throw if the expression is not assignable', function() {
expect(function() {
$compile('<my-component ng-ref="\'hello\'"></my-component>')($rootScope);
}).toThrowMinErr('ngRef', 'nonassign', 'Expression in ngRef="\'hello\'" is non-assignable!');
});
it('should work with non:normalized entity name', function() {
$compile('<my:component ng-ref="myComponent1"></my:component>')($rootScope);
expect($rootScope.myComponent1).toBe(myComponentController);
});
it('should work with data-non-normalized entity name', function() {
$compile('<data-my-component ng-ref="myComponent2"></data-my-component>')($rootScope);
expect($rootScope.myComponent2).toBe(myComponentController);
});
it('should work with x-non-normalized entity name', function() {
$compile('<x-my-component ng-ref="myComponent3"></x-my-component>')($rootScope);
expect($rootScope.myComponent3).toBe(myComponentController);
});
it('should work with data-non-normalized attribute name', function() {
$compile('<my-component data-ng-ref="myComponent1"></my-component>')($rootScope);
expect($rootScope.myComponent1).toBe(myComponentController);
});
it('should work with x-non-normalized attribute name', function() {
$compile('<my-component x-ng-ref="myComponent2"></my-component>')($rootScope);
expect($rootScope.myComponent2).toBe(myComponentController);
});
it('should not bind the controller of an attribute directive', function() {
$compile('<my-component attribute-directive-1 ng-ref="myComponentRef"></my-component>')($rootScope);
expect($rootScope.myComponentRef).toBe(myComponentController);
});
it('should not leak to parent scopes', function() {
var template =
'<div ng-if="true">' +
'<my-component ng-ref="myComponent"></my-component>' +
'</div>';
$compile(template)($rootScope);
expect($rootScope.myComponent).toBe(undefined);
});
it('should nullify the variable once the component is destroyed', function() {
var template = '<div><my-component ng-ref="myComponent"></my-component></div>';
var element = $compile(template)($rootScope);
expect($rootScope.myComponent).toBe(myComponentController);
var componentElement = element.children();
var isolateScope = componentElement.isolateScope();
componentElement.remove();
isolateScope.$destroy();
expect($rootScope.myComponent).toBe(null);
});
it('should be compatible with entering/leaving components', inject(function($animate) {
var template = '<my-component ng-ref="myComponent"></my-component>';
$rootScope.$ctrl = {};
var parent = $compile('<div></div>')($rootScope);
var leaving = $compile(template)($rootScope);
var leavingController = myComponentController;
$animate.enter(leaving, parent);
expect($rootScope.myComponent).toBe(leavingController);
var entering = $compile(template)($rootScope);
var enteringController = myComponentController;
$animate.enter(entering, parent);
$animate.leave(leaving, parent);
expect($rootScope.myComponent).toBe(enteringController);
}));
it('should allow binding to a nested property', function() {
$rootScope.obj = {};
$compile('<my-component ng-ref="obj.myComponent"></my-component>')($rootScope);
expect($rootScope.obj.myComponent).toBe(myComponentController);
});
});
it('should bind the jqlite wrapped DOM element if there is no component', inject(function($compile, $rootScope) {
var el = $compile('<span ng-ref="mySpan">my text</span>')($rootScope);
expect($rootScope.mySpan).toEqualJq(el);
expect($rootScope.mySpan[0].textContent).toBe('my text');
}));
it('should nullify the expression value if the DOM element is destroyed', inject(function($compile, $rootScope) {
var element = $compile('<div><span ng-ref="mySpan">my text</span></div>')($rootScope);
element.children().remove();
expect($rootScope.mySpan).toBe(null);
}));
it('should bind the controller of an element directive', function() {
var myDirectiveController;
module(function($compileProvider) {
$compileProvider.directive('myDirective', function() {
return {
controller: function() {
myDirectiveController = this;
}
};
});
});
inject(function($compile, $rootScope) {
$compile('<my-directive ng-ref="myDirective"></my-directive>')($rootScope);
expect($rootScope.myDirective).toBe(myDirectiveController);
});
});
describe('ngRefRead', function() {
it('should bind the element instead of the controller of a component if ngRefRead="$element" is set', function() {
module(function($compileProvider) {
$compileProvider.component('myComponent', {
template: 'my text',
controller: function() {}
});
});
inject(function($compile, $rootScope) {
var el = $compile('<my-component ng-ref="myEl" ng-ref-read="$element"></my-component>')($rootScope);
expect($rootScope.myEl).toEqualJq(el);
expect($rootScope.myEl[0].textContent).toBe('my text');
});
});
it('should bind the element instead an element-directive controller if ngRefRead="$element" is set', function() {
module(function($compileProvider) {
$compileProvider.directive('myDirective', function() {
return {
restrict: 'E',
template: 'my text',
controller: function() {}
};
});
});
inject(function($compile, $rootScope) {
var el = $compile('<my-directive ng-ref="myEl" ng-ref-read="$element"></my-directive>')($rootScope);
expect($rootScope.myEl).toEqualJq(el);
expect($rootScope.myEl[0].textContent).toBe('my text');
});
});
it('should bind an attribute-directive controller if ngRefRead="controllerName" is set', function() {
var attrDirective1Controller;
module(function($compileProvider) {
$compileProvider.directive('elementDirective', function() {
return {
restrict: 'E',
template: 'my text',
controller: function() {}
};
});
$compileProvider.directive('attributeDirective1', function() {
return {
restrict: 'A',
controller: function() {
attrDirective1Controller = this;
}
};
});
$compileProvider.directive('attributeDirective2', function() {
return {
restrict: 'A',
controller: function() {}
};
});
});
inject(function($compile, $rootScope) {
var el = $compile('<element-directive' +
'attribute-directive-1' +
'attribute-directive-2' +
'ng-ref="myController"' +
'ng-ref-read="$element"></element-directive>')($rootScope);
expect($rootScope.myController).toBe(attrDirective1Controller);
});
});
it('should throw if no controller is found for the ngRefRead value', function() {
module(function($compileProvider) {
$compileProvider.directive('elementDirective', function() {
return {
restrict: 'E',
template: 'my text',
controller: function() {}
};
});
});
inject(function($compile, $rootScope) {
expect(function() {
$compile('<element-directive ' +
'ng-ref="myController"' +
'ng-ref-read="attribute"></element-directive>')($rootScope);
}).toThrowMinErr('ngRef', 'noctrl', 'The controller for ngRefRead="attribute" could not be found on ngRef="myController"');
});
});
});
it('should bind the jqlite element if the controller is on an attribute-directive', function() {
var myDirectiveController;
module(function($compileProvider) {
$compileProvider.directive('myDirective', function() {
return {
restrict: 'A',
template: 'my text',
controller: function() {
myDirectiveController = this;
}
};
});
});
inject(function($compile, $rootScope) {
var el = $compile('<div my-directive ng-ref="myEl"></div>')($rootScope);
expect(myDirectiveController).toBeDefined();
expect($rootScope.myEl).toEqualJq(el);
expect($rootScope.myEl[0].textContent).toBe('my text');
});
});
it('should bind the jqlite element if the controller is on an class-directive', function() {
var myDirectiveController;
module(function($compileProvider) {
$compileProvider.directive('myDirective', function() {
return {
restrict: 'C',
template: 'my text',
controller: function() {
myDirectiveController = this;
}
};
});
});
inject(function($compile, $rootScope) {
var el = $compile('<div class="my-directive" ng-ref="myEl"></div>')($rootScope);
expect(myDirectiveController).toBeDefined();
expect($rootScope.myEl).toEqualJq(el);
expect($rootScope.myEl[0].textContent).toBe('my text');
});
});
describe('transclusion', function() {
it('should work with simple transclusion', function() {
module(function($compileProvider) {
$compileProvider
.component('myComponent', {
transclude: true,
template: '<ng-transclude></ng-transclude>',
controller: function() {
this.text = 'SUCCESS';
}
});
});
inject(function($compile, $rootScope) {
var template = '<my-component ng-ref="myComponent">{{myComponent.text}}</my-component>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('SUCCESS');
dealoc(element);
});
});
it('should be compatible with element transclude components', function() {
module(function($compileProvider) {
$compileProvider
.component('myComponent', {
transclude: 'element',
controller: function($animate, $element, $transclude) {
this.text = 'SUCCESS';
this.$postLink = function() {
$transclude(function(clone, newScope) {
$animate.enter(clone, $element.parent(), $element);
});
};
}
});
});
inject(function($compile, $rootScope) {
var template =
'<div>' +
'<my-component ng-ref="myComponent">' +
'{{myComponent.text}}' +
'</my-component>' +
'</div>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('SUCCESS');
dealoc(element);
});
});
it('should be compatible with ngIf and transclusion on same element', function() {
module(function($compileProvider) {
$compileProvider.component('myComponent', {
template: '<ng-transclude></ng-transclude>',
transclude: true,
controller: function($scope) {
this.text = 'SUCCESS';
}
});
});
inject(function($compile, $rootScope) {
var template =
'<div>' +
'<my-component ng-if="present" ng-ref="myComponent" >' +
'{{myComponent.text}}' +
'</my-component>' +
'</div>';
var element = $compile(template)($rootScope);
$rootScope.$apply('present = false');
expect(element.text()).toBe('');
$rootScope.$apply('present = true');
expect(element.text()).toBe('SUCCESS');
$rootScope.$apply('present = false');
expect(element.text()).toBe('');
$rootScope.$apply('present = true');
expect(element.text()).toBe('SUCCESS');
dealoc(element);
});
});
it('should be compatible with element transclude & destroy components', function() {
var myComponentController;
module(function($compileProvider) {
$compileProvider
.component('myTranscludingComponent', {
transclude: 'element',
controller: function($animate, $element, $transclude) {
myComponentController = this;
var currentClone, currentScope;
this.transclude = function(text) {
this.text = text;
$transclude(function(clone, newScope) {
currentClone = clone;
currentScope = newScope;
$animate.enter(clone, $element.parent(), $element);
});
};
this.destroy = function() {
currentClone.remove();
currentScope.$destroy();
};
}
});
});
inject(function($compile, $rootScope) {
var template =
'<div>' +
'<my-transcluding-component ng-ref="myComponent">' +
'{{myComponent.text}}' +
'</my-transcluding-component>' +
'</div>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('');
myComponentController.transclude('transcludedOk');
$rootScope.$apply();
expect(element.text()).toBe('transcludedOk');
myComponentController.destroy();
$rootScope.$apply();
expect(element.text()).toBe('');
});
});
it('should be compatible with element transclude directives', function() {
module(function($compileProvider) {
$compileProvider
.directive('myDirective', function($animate) {
return {
transclude: 'element',
controller: function() {
this.text = 'SUCCESS';
},
link: function(scope, element, attrs, ctrl, $transclude) {
$transclude(function(clone, newScope) {
$animate.enter(clone, element.parent(), element);
});
}
};
});
});
inject(function($compile, $rootScope) {
var template =
'<div>' +
'<my-directive ng-ref="myDirective">' +
'{{myDirective.text}}' +
'</my-directive>' +
'</div>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('SUCCESS');
dealoc(element);
});
});
});
it('should work with components with templates via $http', function() {
module(function($compileProvider) {
$compileProvider.component('httpComponent', {
templateUrl: 'template.html',
controller: function() {
this.me = true;
}
});
});
inject(function($compile, $httpBackend, $rootScope) {
var template = '<div><http-component ng-ref="controller"></http-component></div>';
var element = $compile(template)($rootScope);
$httpBackend.expect('GET', 'template.html').respond('ok');
$rootScope.$apply();
expect($rootScope.controller).toBeUndefined();
$httpBackend.flush();
expect($rootScope.controller.me).toBe(true);
dealoc(element);
});
});
it('should work with ngRepeat-ed components', function() {
var controllers = [];
module(function($compileProvider) {
$compileProvider.component('myComponent', {
template: 'foo',
controller: function() {
controllers.push(this);
}
});
});
inject(function($compile, $rootScope) {
$rootScope.elements = [0,1,2,3,4];
$rootScope.controllers = []; // Initialize the array because ngRepeat creates a child scope
var template = '<div><my-component ng-repeat="(key, el) in elements" ng-ref="controllers[key]"></my-component></div>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect($rootScope.controllers).toEqual(controllers);
$rootScope.$apply('elements = []');
expect($rootScope.controllers).toEqual([null, null, null, null, null]);
});
});
});
+39 -16
View File
@@ -1530,11 +1530,14 @@ describe('select', function() {
['a'],
NaN
], function(prop) {
scope.option1 = prop;
scope.option2 = 'red';
scope.selected = 'NOMATCH';
compile('<select ng-model="selected">' +
'<option ng-value="option1">{{option1}}</option>' +
'<option ng-value="option2">{{option2}}</option>' +
'</select>');
scope.$digest();
@@ -1571,10 +1574,12 @@ describe('select', function() {
NaN
], function(prop) {
scope.option = prop;
scope.option2 = 'red';
scope.selected = 'NOMATCH';
compile('<select ng-model="selected">' +
'<option ng-value="option">{{option}}</option>' +
'<option ng-value="option2">{{option2}}</option>' +
'</select>');
var selectController = element.controller('select');
@@ -1604,7 +1609,7 @@ describe('select', function() {
expect(scope.selected).toBe(null);
expect(element[0].selectedIndex).toBe(0);
expect(element.find('option').length).toBe(2);
expect(element.find('option').length).toBe(3);
expect(element.find('option').eq(0).prop('selected')).toBe(true);
expect(element.find('option').eq(0).val()).toBe(unknownValue(prop));
expect(element.find('option').eq(1).prop('selected')).toBe(false);
@@ -1617,6 +1622,7 @@ describe('select', function() {
expect(element.find('option').eq(0).val()).toBe('string:UPDATEDVALUE');
});
it('should interact with custom attribute $observe and $set calls', function() {
var log = [], optionAttr;
@@ -1638,26 +1644,43 @@ describe('select', function() {
optionAttr.$set('value', 'update');
expect(log[1]).toBe('update');
expect(element.find('option').eq(1).val()).toBe('string:update');
});
it('should ignore the option text / value attribute if the ngValue attribute exists', function() {
scope.ngvalue = 'abc';
scope.value = 'def';
scope.textvalue = 'ghi';
compile('<select ng-model="x"><option ng-value="ngvalue" value="{{value}}">{{textvalue}}</option></select>');
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
});
it('should ignore the option text / value attribute if the ngValue attribute exists', function() {
scope.ngvalue = 'abc';
scope.value = 'def';
scope.textvalue = 'ghi';
it('should ignore option text with multiple interpolations if the ngValue attribute exists', function() {
scope.ngvalue = 'abc';
scope.textvalue = 'def';
scope.textvalue2 = 'ghi';
compile('<select ng-model="x"><option ng-value="ngvalue" value="{{value}}">{{textvalue}}</option></select>');
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
});
it('should ignore option text with multiple interpolations if the ngValue attribute exists', function() {
scope.ngvalue = 'abc';
scope.textvalue = 'def';
scope.textvalue2 = 'ghi';
compile('<select ng-model="x"><option ng-value="ngvalue">{{textvalue}} {{textvalue2}}</option></select>');
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
});
it('should select the first option if it is `undefined`', function() {
scope.selected = undefined;
scope.option1 = undefined;
scope.option2 = 'red';
compile('<select ng-model="selected">' +
'<option ng-value="option1">{{option1}}</option>' +
'<option ng-value="option2">{{option2}}</option>' +
'</select>');
expect(element).toEqualSelect(['undefined:undefined'], 'string:red');
});
compile('<select ng-model="x"><option ng-value="ngvalue">{{textvalue}} {{textvalue2}}</option></select>');
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
});
describe('and select[multiple]', function() {
+20 -20
View File
@@ -2002,7 +2002,7 @@ describe('$http', function() {
it('should immediately call `$browser.$$incOutstandingRequestCount()`', function() {
expect(incOutstandingRequestCountSpy).not.toHaveBeenCalled();
$http.get('');
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
});
@@ -2012,7 +2012,7 @@ describe('$http', function() {
$http.get('');
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
$httpBackend.flush();
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
});
@@ -2022,7 +2022,7 @@ describe('$http', function() {
$http.get('').catch(noop);
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
$httpBackend.flush();
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
});
@@ -2033,13 +2033,13 @@ describe('$http', function() {
$http.get('', {transformRequest: function() { throw new Error(); }}).catch(noop);
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
$rootScope.$digest();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
}
);
@@ -2052,13 +2052,13 @@ describe('$http', function() {
$httpBackend.when('GET').respond(200);
$http.get('', {transformResponse: function() { throw new Error(); }}).catch(noop);
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
$httpBackend.flush();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
}
);
});
@@ -2112,7 +2112,7 @@ describe('$http', function() {
expect(reqInterceptorFulfilled).toBe(false);
expect(resInterceptorFulfilled).toBe(false);
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
reqInterceptorDeferred.resolve();
@@ -2120,7 +2120,7 @@ describe('$http', function() {
expect(reqInterceptorFulfilled).toBe(true);
expect(resInterceptorFulfilled).toBe(false);
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
resInterceptorDeferred.resolve();
@@ -2128,8 +2128,8 @@ describe('$http', function() {
expect(reqInterceptorFulfilled).toBe(true);
expect(resInterceptorFulfilled).toBe(true);
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
}
);
@@ -2144,15 +2144,15 @@ describe('$http', function() {
$rootScope.$digest();
expect(reqInterceptorFulfilled).toBe(false);
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
reqInterceptorDeferred.reject();
$rootScope.$digest();
expect(reqInterceptorFulfilled).toBe(true);
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
}
);
@@ -2169,7 +2169,7 @@ describe('$http', function() {
expect(reqInterceptorFulfilled).toBe(false);
expect(resInterceptorFulfilled).toBe(false);
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
reqInterceptorDeferred.resolve();
@@ -2177,7 +2177,7 @@ describe('$http', function() {
expect(reqInterceptorFulfilled).toBe(true);
expect(resInterceptorFulfilled).toBe(false);
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).not.toHaveBeenCalled();
resInterceptorDeferred.reject();
@@ -2185,8 +2185,8 @@ describe('$http', function() {
expect(reqInterceptorFulfilled).toBe(true);
expect(resInterceptorFulfilled).toBe(true);
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnce();
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnce();
expect(incOutstandingRequestCountSpy).toHaveBeenCalledOnceWith('$http');
expect(completeOutstandingRequestSpy).toHaveBeenCalledOnceWith(noop, '$http');
}
);
});
+117 -9
View File
@@ -693,10 +693,10 @@ describe('$location', function() {
describe('location watch', function() {
it('should not update browser if only the empty hash fragment is cleared by updating the search', function() {
it('should not update browser if only the empty hash fragment is cleared', function() {
initService({supportHistory: true});
mockUpBrowser({initialUrl:'http://new.com/a/b#', baseHref:'/base/'});
inject(function($rootScope, $browser, $location) {
mockUpBrowser({initialUrl: 'http://new.com/a/b#', baseHref: '/base/'});
inject(function($browser, $rootScope) {
$browser.url('http://new.com/a/b');
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').and.callThrough();
$rootScope.$digest();
@@ -707,10 +707,11 @@ describe('$location', function() {
it('should not replace browser url if only the empty hash fragment is cleared', function() {
initService({html5Mode: true, supportHistory: true});
mockUpBrowser({initialUrl:'http://new.com/#', baseHref: '/'});
inject(function($browser, $location) {
expect($browser.url()).toBe('http://new.com/#');
mockUpBrowser({initialUrl: 'http://new.com/#', baseHref: '/'});
inject(function($browser, $location, $window) {
expect($browser.url()).toBe('http://new.com/');
expect($location.absUrl()).toBe('http://new.com/');
expect($window.location.href).toBe('http://new.com/#');
});
});
@@ -747,6 +748,58 @@ describe('$location', function() {
});
//https://github.com/angular/angular.js/issues/16592
it('should not infinitely digest when initial params contain a quote', function() {
initService({html5Mode:true,supportHistory:true});
mockUpBrowser({initialUrl:'http://localhost:9876/?q=\'', baseHref:'/'});
inject(function($location, $browser, $rootScope) {
expect(function() {
$rootScope.$digest();
}).not.toThrow();
});
});
//https://github.com/angular/angular.js/issues/16592
it('should not infinitely digest when initial params contain an escaped quote', function() {
initService({html5Mode:true,supportHistory:true});
mockUpBrowser({initialUrl:'http://localhost:9876/?q=%27', baseHref:'/'});
inject(function($location, $browser, $rootScope) {
expect(function() {
$rootScope.$digest();
}).not.toThrow();
});
});
//https://github.com/angular/angular.js/issues/16592
it('should not infinitely digest when updating params containing a quote (via $browser.url)', function() {
initService({html5Mode:true,supportHistory:true});
mockUpBrowser({initialUrl:'http://localhost:9876/', baseHref:'/'});
inject(function($location, $browser, $rootScope) {
$rootScope.$digest();
$browser.url('http://localhost:9876/?q=\'');
expect(function() {
$rootScope.$digest();
}).not.toThrow();
});
});
//https://github.com/angular/angular.js/issues/16592
it('should not infinitely digest when updating params containing a quote (via window.location + popstate)', function() {
initService({html5Mode:true,supportHistory:true});
mockUpBrowser({initialUrl:'http://localhost:9876/', baseHref:'/'});
inject(function($window, $location, $browser, $rootScope) {
$rootScope.$digest();
$window.location.href = 'http://localhost:9876/?q=\'';
expect(function() {
jqLite($window).triggerHandler('popstate');
}).not.toThrow();
});
});
describe('when changing the browser URL/history directly during a `$digest`', function() {
beforeEach(function() {
@@ -804,10 +857,13 @@ describe('$location', function() {
});
function updatePathOnLocationChangeSuccessTo(newPath) {
function updatePathOnLocationChangeSuccessTo(newPath, newParams) {
inject(function($rootScope, $location) {
$rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
$location.path(newPath);
if (newParams) {
$location.search(newParams);
}
});
});
}
@@ -950,6 +1006,24 @@ describe('$location', function() {
expect($browserUrl).not.toHaveBeenCalled();
});
});
//https://github.com/angular/angular.js/issues/16592
it('should not infinite $digest when going to base URL with trailing slash when $locationChangeSuccess watcher changes query params to contain quote', function() {
initService({html5Mode: true, supportHistory: true});
mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
inject(function($rootScope, $injector, $browser) {
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').and.callThrough();
var $location = $injector.get('$location');
updatePathOnLocationChangeSuccessTo('/', {q: '\''});
$rootScope.$digest();
expect($location.path()).toEqual('/');
expect($location.search()).toEqual({q: '\''});
expect($browserUrl).toHaveBeenCalledTimes(1);
});
});
});
});
@@ -1140,6 +1214,40 @@ describe('$location', function() {
});
});
//https://github.com/angular/angular.js/issues/16592
it('should not infinite $digest on pushState() with quote in param', function() {
initService({html5Mode: true, supportHistory: true});
mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
inject(function($rootScope, $injector, $window) {
var $location = $injector.get('$location');
$rootScope.$digest(); //allow $location initialization to finish
$window.history.pushState({}, null, 'http://server/app/Home?q=\'');
$rootScope.$digest();
expect($location.absUrl()).toEqual('http://server/app/Home?q=\'');
expect($location.path()).toEqual('/Home');
expect($location.search()).toEqual({q: '\''});
});
});
//https://github.com/angular/angular.js/issues/16592
it('should not infinite $digest on popstate event with quote in param', function() {
initService({html5Mode: true, supportHistory: true});
mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
inject(function($rootScope, $injector, $window) {
var $location = $injector.get('$location');
$rootScope.$digest(); //allow $location initialization to finish
$window.location.href = 'http://server/app/Home?q=\'';
jqLite($window).triggerHandler('popstate');
expect($location.absUrl()).toEqual('http://server/app/Home?q=\'');
expect($location.path()).toEqual('/Home');
expect($location.search()).toEqual({q: '\''});
});
});
it('should replace browser url & state when replace() was called at least once', function() {
initService({html5Mode:true, supportHistory: true});
mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
@@ -2766,9 +2874,9 @@ describe('$location', function() {
};
return win;
};
$browserProvider.$get = function($document, $window, $log, $sniffer) {
$browserProvider.$get = function($document, $window, $log, $sniffer, $$taskTrackerFactory) {
/* global Browser: false */
browser = new Browser($window, $document, $log, $sniffer);
browser = new Browser($window, $document, $log, $sniffer, $$taskTrackerFactory);
browser.baseHref = function() {
return options.baseHref;
};
+158
View File
@@ -0,0 +1,158 @@
'use strict';
describe('ngOn* event binding', function() {
it('should add event listener of specified name', inject(function($compile, $rootScope) {
$rootScope.name = 'Misko';
var element = $compile('<span ng-on-foo="name = name + 3"></span>')($rootScope);
element.triggerHandler('foo');
expect($rootScope.name).toBe('Misko3');
}));
it('should use angular.element(x).on() API to add listener', inject(function($compile, $rootScope) {
spyOn(angular.element.prototype, 'on');
var element = $compile('<span ng-on-foo="name = name + 3"></span>')($rootScope);
expect(angular.element.prototype.on).toHaveBeenCalledWith('foo', jasmine.any(Function));
}));
it('should allow access to the $event object', inject(function($rootScope, $compile) {
var element = $compile('<span ng-on-foo="e = $event"></span>')($rootScope);
element.triggerHandler('foo');
expect($rootScope.e.target).toBe(element[0]);
}));
it('should call the listener synchronously', inject(function($compile, $rootScope) {
var element = $compile('<span ng-on-foo="fooEvent()"></span>')($rootScope);
$rootScope.fooEvent = jasmine.createSpy('fooEvent');
element.triggerHandler('foo');
expect($rootScope.fooEvent).toHaveBeenCalledOnce();
}));
it('should support multiple events on a single element', inject(function($compile, $rootScope) {
var element = $compile('<span ng-on-foo="fooEvent()" ng-on-bar="barEvent()"></span>')($rootScope);
$rootScope.fooEvent = jasmine.createSpy('fooEvent');
$rootScope.barEvent = jasmine.createSpy('barEvent');
element.triggerHandler('foo');
expect($rootScope.fooEvent).toHaveBeenCalled();
expect($rootScope.barEvent).not.toHaveBeenCalled();
$rootScope.fooEvent.calls.reset();
$rootScope.barEvent.calls.reset();
element.triggerHandler('bar');
expect($rootScope.fooEvent).not.toHaveBeenCalled();
expect($rootScope.barEvent).toHaveBeenCalled();
}));
it('should work with different prefixes', inject(function($rootScope, $compile) {
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
var element = $compile('<span ng:on:test="cb(1)" ng-On-test2="cb(2)" ng_On_test3="cb(3)"></span>')($rootScope);
element.triggerHandler('test');
expect(cb).toHaveBeenCalledWith(1);
element.triggerHandler('test2');
expect(cb).toHaveBeenCalledWith(2);
element.triggerHandler('test3');
expect(cb).toHaveBeenCalledWith(3);
}));
it('should work if they are prefixed with x- or data- and different prefixes', inject(function($rootScope, $compile) {
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
var element = $compile('<span data-ng-on-test2="cb(2)" x-ng-on-test3="cb(3)" data-ng:on-test4="cb(4)" ' +
'x_ng-on-test5="cb(5)" data:ng-on-test6="cb(6)"></span>')($rootScope);
element.triggerHandler('test2');
expect(cb).toHaveBeenCalledWith(2);
element.triggerHandler('test3');
expect(cb).toHaveBeenCalledWith(3);
element.triggerHandler('test4');
expect(cb).toHaveBeenCalledWith(4);
element.triggerHandler('test5');
expect(cb).toHaveBeenCalledWith(5);
element.triggerHandler('test6');
expect(cb).toHaveBeenCalledWith(6);
}));
it('should work independently of attributes with the same name', inject(function($rootScope, $compile) {
var element = $compile('<span ng-on-asdf="cb()" asdf="foo" />')($rootScope);
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
$rootScope.$digest();
element.triggerHandler('asdf');
expect(cb).toHaveBeenCalled();
expect(element.attr('asdf')).toBe('foo');
}));
it('should work independently of (ng-)attributes with the same name', inject(function($rootScope, $compile) {
var element = $compile('<span ng-on-asdf="cb()" ng-attr-asdf="foo" />')($rootScope);
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
$rootScope.$digest();
element.triggerHandler('asdf');
expect(cb).toHaveBeenCalled();
expect(element.attr('asdf')).toBe('foo');
}));
it('should work independently of properties with the same name', inject(function($rootScope, $compile) {
var element = $compile('<span ng-on-asdf="cb()" ng-prop-asdf="123" />')($rootScope);
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
$rootScope.$digest();
element.triggerHandler('asdf');
expect(cb).toHaveBeenCalled();
expect(element.prop('asdf')).toBe(123);
}));
it('should use the full ng-on-* attribute name in $attr mappings', function() {
var attrs;
module(function($compileProvider) {
$compileProvider.directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer ng-on-title="cb(1)" ng-on-super-title="cb(2)" ng-on-my-camel_title="cb(3)">')($rootScope);
expect(attrs.title).toBeUndefined();
expect(attrs.$attr.title).toBeUndefined();
expect(attrs.ngOnTitle).toBe('cb(1)');
expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
expect(attrs.superTitle).toBeUndefined();
expect(attrs.$attr.superTitle).toBeUndefined();
expect(attrs.ngOnSuperTitle).toBe('cb(2)');
expect(attrs.$attr.ngOnSuperTitle).toBe('ng-on-super-title');
expect(attrs.myCamelTitle).toBeUndefined();
expect(attrs.$attr.myCamelTitle).toBeUndefined();
expect(attrs.ngOnMyCamelTitle).toBe('cb(3)');
expect(attrs.$attr.ngOnMyCamelTitle).toBe('ng-on-my-camel_title');
});
});
it('should not conflict with (ng-attr-)attribute mappings of the same name', function() {
var attrs;
module(function($compileProvider) {
$compileProvider.directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer ng-on-title="42" ng-attr-title="foo" title="bar">')($rootScope);
expect(attrs.title).toBe('foo');
expect(attrs.$attr.title).toBe('title');
expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
});
});
});
+836
View File
@@ -0,0 +1,836 @@
'use strict';
/* eslint-disable no-script-url */
describe('ngProp*', function() {
it('should bind boolean properties (input disabled)', inject(function($rootScope, $compile) {
var element = $compile('<button ng-prop-disabled="isDisabled">Button</button>')($rootScope);
$rootScope.$digest();
expect(element.prop('disabled')).toBe(false);
$rootScope.isDisabled = true;
$rootScope.$digest();
expect(element.prop('disabled')).toBe(true);
$rootScope.isDisabled = false;
$rootScope.$digest();
expect(element.prop('disabled')).toBe(false);
}));
it('should bind boolean properties (input checked)', inject(function($rootScope, $compile) {
var element = $compile('<input type="checkbox" ng-prop-checked="isChecked" />')($rootScope);
expect(element.prop('checked')).toBe(false);
$rootScope.isChecked = true;
$rootScope.$digest();
expect(element.prop('checked')).toBe(true);
$rootScope.isChecked = false;
$rootScope.$digest();
expect(element.prop('checked')).toBe(false);
}));
it('should bind string properties (title)', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-title="title" />')($rootScope);
$rootScope.title = 123;
$rootScope.$digest();
expect(element.prop('title')).toBe('123');
$rootScope.title = 'foobar';
$rootScope.$digest();
expect(element.prop('title')).toBe('foobar');
}));
it('should bind variable type properties', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-asdf="asdf" />')($rootScope);
$rootScope.asdf = 123;
$rootScope.$digest();
expect(element.prop('asdf')).toBe(123);
$rootScope.asdf = 'foobar';
$rootScope.$digest();
expect(element.prop('asdf')).toBe('foobar');
$rootScope.asdf = true;
$rootScope.$digest();
expect(element.prop('asdf')).toBe(true);
}));
it('should support mixed case using underscore-separated names', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-a_bcd_e="value" />')($rootScope);
$rootScope.value = 123;
$rootScope.$digest();
expect(element.prop('aBcdE')).toBe(123);
}));
it('should work with different prefixes', inject(function($rootScope, $compile) {
$rootScope.name = 'Misko';
var element = $compile('<span ng:prop:test="name" ng-Prop-test2="name" ng_Prop_test3="name"></span>')($rootScope);
expect(element.prop('test')).toBe('Misko');
expect(element.prop('test2')).toBe('Misko');
expect(element.prop('test3')).toBe('Misko');
}));
it('should work with the "href" property', inject(function($rootScope, $compile) {
$rootScope.value = 'test';
var element = $compile('<a ng-prop-href="\'test/\' + value"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toMatch(/\/test\/test$/);
}));
it('should work if they are prefixed with x- or data- and different prefixes', inject(function($rootScope, $compile) {
$rootScope.name = 'Misko';
var element = $compile('<span data-ng-prop-test2="name" x-ng-prop-test3="name" data-ng:prop-test4="name" ' +
'x_ng-prop-test5="name" data:ng-prop-test6="name"></span>')($rootScope);
expect(element.prop('test2')).toBe('Misko');
expect(element.prop('test3')).toBe('Misko');
expect(element.prop('test4')).toBe('Misko');
expect(element.prop('test5')).toBe('Misko');
expect(element.prop('test6')).toBe('Misko');
}));
it('should work independently of attributes with the same name', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-asdf="asdf" asdf="foo" />')($rootScope);
$rootScope.asdf = 123;
$rootScope.$digest();
expect(element.prop('asdf')).toBe(123);
expect(element.attr('asdf')).toBe('foo');
}));
it('should work independently of (ng-)attributes with the same name', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-asdf="asdf" ng-attr-asdf="foo" />')($rootScope);
$rootScope.asdf = 123;
$rootScope.$digest();
expect(element.prop('asdf')).toBe(123);
expect(element.attr('asdf')).toBe('foo');
}));
it('should use the full ng-prop-* attribute name in $attr mappings', function() {
var attrs;
module(function($compileProvider) {
$compileProvider.directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer ng-prop-title="12" ng-prop-super-title="34" ng-prop-my-camel_title="56">')($rootScope);
expect(attrs.title).toBeUndefined();
expect(attrs.$attr.title).toBeUndefined();
expect(attrs.ngPropTitle).toBe('12');
expect(attrs.$attr.ngPropTitle).toBe('ng-prop-title');
expect(attrs.superTitle).toBeUndefined();
expect(attrs.$attr.superTitle).toBeUndefined();
expect(attrs.ngPropSuperTitle).toBe('34');
expect(attrs.$attr.ngPropSuperTitle).toBe('ng-prop-super-title');
expect(attrs.myCamelTitle).toBeUndefined();
expect(attrs.$attr.myCamelTitle).toBeUndefined();
expect(attrs.ngPropMyCamelTitle).toBe('56');
expect(attrs.$attr.ngPropMyCamelTitle).toBe('ng-prop-my-camel_title');
});
});
it('should not conflict with (ng-attr-)attribute mappings of the same name', function() {
var attrs;
module(function($compileProvider) {
$compileProvider.directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer ng-prop-title="42" ng-attr-title="foo" title="bar">')($rootScope);
expect(attrs.title).toBe('foo');
expect(attrs.$attr.title).toBe('title');
expect(attrs.$attr.ngPropTitle).toBe('ng-prop-title');
});
});
it('should disallow property binding to onclick', inject(function($compile, $rootScope) {
// All event prop bindings are disallowed.
expect(function() {
$compile('<button ng-prop-onclick="onClickJs"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
expect(function() {
$compile('<button ng-prop-ONCLICK="onClickJs"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
}));
it('should process property bindings in pre-linking phase at priority 100', function() {
module(provideLog);
module(function($compileProvider) {
$compileProvider.directive('propLog', function(log, $rootScope) {
return {
compile: function($element, $attrs) {
log('compile=' + $element.prop('myName'));
return {
pre: function($scope, $element, $attrs) {
log('preLinkP0=' + $element.prop('myName'));
$rootScope.name = 'pre0';
},
post: function($scope, $element, $attrs) {
log('postLink=' + $element.prop('myName'));
$rootScope.name = 'post0';
}
};
}
};
});
});
module(function($compileProvider) {
$compileProvider.directive('propLogHighPriority', function(log, $rootScope) {
return {
priority: 101,
compile: function() {
return {
pre: function($scope, $element, $attrs) {
log('preLinkP101=' + $element.prop('myName'));
$rootScope.name = 'pre101';
}
};
}
};
});
});
inject(function($rootScope, $compile, log) {
var element = $compile('<div prop-log-high-priority prop-log ng-prop-my_name="name"></div>')($rootScope);
$rootScope.name = 'angular';
$rootScope.$apply();
log('digest=' + element.prop('myName'));
expect(log).toEqual('compile=undefined; preLinkP101=undefined; preLinkP0=pre101; postLink=pre101; digest=angular');
});
});
['img', 'audio', 'video'].forEach(function(tag) {
// Support: IE 9 only
// IE9 rejects the `video` / `audio` tags with "Error: Not implemented"
if (msie !== 9 || tag === 'img') {
describe(tag + '[src] context requirement', function() {
it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) {
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
$rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted
$rootScope.$digest();
expect(element.prop('src')).toEqual('http://example.com/image.mp4');
}));
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
// As a MEDIA_URL URL
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo()');
$rootScope.$digest();
expect(element.prop('src')).toEqual('untrusted:foo()');
// As a URL
element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('untrusted:foo()');
$rootScope.$digest();
expect(element.prop('src')).toEqual('untrusted:foo()');
// As a RESOURCE URL
element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('untrusted:foo()');
$rootScope.$digest();
expect(element.prop('src')).toEqual('untrusted:foo()');
}));
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile, $sce) {
// As a MEDIA_URL URL
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = 'untrusted:foo()';
$rootScope.$digest();
expect(element.prop('src')).toEqual('unsafe:untrusted:foo()');
}));
it('should sanitize wrongly typed values', inject(function($rootScope, $compile, $sce) {
// As a MEDIA_URL URL
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = $sce.trustAsCss('untrusted:foo()');
$rootScope.$digest();
expect(element.prop('src')).toEqual('unsafe:untrusted:foo()');
}));
});
}
});
// Support: IE 9 only
// IE 9 rejects the `source` / `track` tags with
// "Unable to get value of the property 'childNodes': object is null or undefined"
if (msie !== 9) {
['source', 'track'].forEach(function(tag) {
describe(tag + '[src]', function() {
it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) {
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('http://example.com/image.mp4');
}));
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
// As a MEDIA_URL URL
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = $sce.trustAsMediaUrl('javascript:foo()');
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
// As a URL
element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo()');
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
// As a RESOURCE URL
element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:foo()');
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
}));
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = 'untrusted:foo()';
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('unsafe:untrusted:foo()');
}));
it('should sanitize wrongly typed values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = $sce.trustAsCss('untrusted:foo()');
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('unsafe:untrusted:foo()');
}));
});
});
}
describe('img[src] sanitization', function() {
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = $sce.trustAsMediaUrl('someuntrustedthing:foo();');
$rootScope.$digest();
expect(element.prop('src')).toEqual('someuntrustedthing:foo();');
}));
it('should use $$sanitizeUri', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
$rootScope.testUrl = 'someUrl';
$rootScope.$apply();
expect(element.prop('src')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
});
});
it('should not use $$sanitizeUri with trusted values', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.throwError('Should not have been called');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope, $sce) {
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
// Assigning javascript:foo to src makes at least IE9-11 complain, so use another
// protocol name.
$rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo();');
$rootScope.$apply();
expect(element.prop('src')).toBe('untrusted:foo();');
});
});
});
['img', 'source'].forEach(function(srcsetElement) {
// Support: IE 9 only
// IE9 ignores source[srcset] property assignments
if (msie !== 9 || srcsetElement === 'img') {
describe(srcsetElement + '[srcset] sanitization', function() {
it('should not error if srcset is blank', inject(function($compile, $rootScope) {
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
// Set srcset to a value
$rootScope.testUrl = 'http://example.com/';
$rootScope.$digest();
expect(element.prop('srcset')).toBe('http://example.com/');
// Now set it to blank
$rootScope.testUrl = '';
$rootScope.$digest();
expect(element.prop('srcset')).toBe('');
}));
it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted
$rootScope.$digest();
expect(element.prop('srcset')).toEqual('http://example.com/image.png');
}));
it('should accept trusted values, if they are also whitelisted', inject(function($rootScope, $compile, $sce) {
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('http://example.com');
$rootScope.$digest();
expect(element.prop('srcset')).toEqual('http://example.com');
}));
it('should NOT work with trusted values', inject(function($rootScope, $compile, $sce) {
// A limitation of the approach used for srcset is that you cannot use `trustAsUrl`.
// Use trustAsHtml and ng-bind-html to work around this.
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:something');
$rootScope.$digest();
expect(element.prop('srcset')).toEqual('unsafe:javascript:something');
element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl + \',\' + testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:something');
$rootScope.$digest();
expect(element.prop('srcset')).toEqual(
'unsafe:javascript:something ,unsafe:javascript:something');
}));
it('should use $$sanitizeUri', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = 'someUrl';
$rootScope.$apply();
expect(element.prop('srcset')).toBe('someSanitizedUrl');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl + \',\' + testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = 'javascript:yay';
$rootScope.$apply();
expect(element.prop('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl');
element = $compile('<' + srcsetElement + ' ng-prop-srcset="\'java\' + testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = 'script:yay, javascript:nay';
$rootScope.$apply();
expect(element.prop('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl');
});
});
it('should sanitize all uris in srcset', inject(function($rootScope, $compile) {
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
var testSet = {
'http://example.com/image.png':'http://example.com/image.png',
' http://example.com/image.png':'http://example.com/image.png',
'http://example.com/image.png ':'http://example.com/image.png',
'http://example.com/image.png 128w':'http://example.com/image.png 128w',
'http://example.com/image.png 2x':'http://example.com/image.png 2x',
'http://example.com/image.png 1.5x':'http://example.com/image.png 1.5x',
'http://example.com/image1.png 1x,http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x ,http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x, http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x , http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
'http://example.com/image1.png 48w,http://example.com/image2.png 64w':'http://example.com/image1.png 48w,http://example.com/image2.png 64w',
//Test regex to make sure doesn't mistake parts of url for width descriptors
'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w':'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w',
'http://example.com/image1.png 1x,http://example.com/image2.png 64w':'http://example.com/image1.png 1x,http://example.com/image2.png 64w',
'http://example.com/image1.png,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png ,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png, http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png , http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png 1x, http://example.com/image2.png 2x, http://example.com/image3.png 3x':
'http://example.com/image1.png 1x,http://example.com/image2.png 2x,http://example.com/image3.png 3x',
'javascript:doEvilStuff() 2x': 'unsafe:javascript:doEvilStuff() 2x',
'http://example.com/image1.png 1x,javascript:doEvilStuff() 2x':'http://example.com/image1.png 1x,unsafe:javascript:doEvilStuff() 2x',
'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x',
//Test regex to make sure doesn't mistake parts of url for pixel density descriptors
'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x'
};
forEach(testSet, function(ref, url) {
$rootScope.testUrl = url;
$rootScope.$digest();
expect(element.prop('srcset')).toEqual(ref);
});
}));
});
}
});
describe('a[href] sanitization', function() {
it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile) {
$rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('http://example.com/image.png');
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('http://example.com/image.png');
}));
it('should accept trusted values for non-whitelisted values', inject(function($rootScope, $compile, $sce) {
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo()'); // `javascript` is not whitelisted
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('javascript:foo()');
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('javascript:foo()');
}));
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile) {
$rootScope.testUrl = 'javascript:foo()'; // `javascript` is not whitelisted
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('unsafe:javascript:foo()');
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('unsafe:javascript:foo()');
}));
it('should not sanitize href on elements other than anchor', inject(function($compile, $rootScope) {
var element = $compile('<div ng-prop-href="testUrl"></div>')($rootScope);
$rootScope.testUrl = 'javascript:doEvilStuff()';
$rootScope.$apply();
expect(element.prop('href')).toBe('javascript:doEvilStuff()');
}));
it('should not sanitize properties other then those configured', inject(function($compile, $rootScope) {
var element = $compile('<a ng-prop-title="testUrl"></a>')($rootScope);
$rootScope.testUrl = 'javascript:doEvilStuff()';
$rootScope.$apply();
expect(element.prop('title')).toBe('javascript:doEvilStuff()');
}));
it('should use $$sanitizeUri', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.testUrl = 'someUrl';
$rootScope.$apply();
expect(element.prop('href')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
$$sanitizeUri.calls.reset();
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$apply();
expect(element.prop('href')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
});
});
it('should not have endless digests when given arrays in concatenable context', inject(function($compile, $rootScope) {
var element = $compile('<foo ng-prop-href="testUrl"></foo><foo ng-prop-href="::testUrl"></foo>' +
'<foo ng-prop-href="\'http://example.com/\' + testUrl"></foo><foo ng-prop-href="::\'http://example.com/\' + testUrl"></foo>')($rootScope);
$rootScope.testUrl = [1];
$rootScope.$digest();
$rootScope.testUrl = [];
$rootScope.$digest();
$rootScope.testUrl = {a:'b'};
$rootScope.$digest();
$rootScope.testUrl = {};
$rootScope.$digest();
}));
});
describe('iframe[src]', function() {
it('should pass through src properties for the same domain', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
$rootScope.testUrl = 'different_page';
$rootScope.$apply();
expect(element.prop('src')).toMatch(/\/different_page$/);
}));
it('should clear out src properties for a different domain', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
$rootScope.testUrl = 'http://a.different.domain.example.com';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: http://a.different.domain.example.com');
}));
it('should clear out JS src properties', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
$rootScope.testUrl = 'javascript:alert(1);';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: javascript:alert(1);');
}));
it('should clear out non-resource_url src properties', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:doTrustedStuff()');
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: javascript:doTrustedStuff()');
}));
it('should pass through $sce.trustAs() values in src properties', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
$rootScope.$apply();
expect(element.prop('src')).toEqual('javascript:doTrustedStuff()');
}));
});
describe('base[href]', function() {
it('should be a RESOURCE_URL context', inject(function($compile, $rootScope, $sce) {
var element = $compile('<base ng-prop-href="testUrl"/>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('https://example.com/');
$rootScope.$apply();
expect(element.prop('href')).toContain('https://example.com/');
$rootScope.testUrl = 'https://not.example.com/';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: https://not.example.com/');
}));
});
describe('form[action]', function() {
it('should pass through action property for the same domain', inject(function($compile, $rootScope, $sce) {
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
$rootScope.testUrl = 'different_page';
$rootScope.$apply();
expect(element.prop('action')).toMatch(/\/different_page$/);
}));
it('should clear out action property for a different domain', inject(function($compile, $rootScope, $sce) {
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
$rootScope.testUrl = 'http://a.different.domain.example.com';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: http://a.different.domain.example.com');
}));
it('should clear out JS action property', inject(function($compile, $rootScope, $sce) {
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
$rootScope.testUrl = 'javascript:alert(1);';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: javascript:alert(1);');
}));
it('should clear out non-resource_url action property', inject(function($compile, $rootScope, $sce) {
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:doTrustedStuff()');
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: javascript:doTrustedStuff()');
}));
it('should pass through $sce.trustAsResourceUrl() values in action property', inject(function($compile, $rootScope, $sce) {
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
$rootScope.$apply();
expect(element.prop('action')).toEqual('javascript:doTrustedStuff()');
}));
});
describe('link[href]', function() {
it('should reject invalid RESOURCE_URLs', inject(function($compile, $rootScope) {
var element = $compile('<link ng-prop-href="testUrl" rel="stylesheet" />')($rootScope);
$rootScope.testUrl = 'https://evil.example.org/css.css';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: https://evil.example.org/css.css');
}));
it('should accept valid RESOURCE_URLs', inject(function($compile, $rootScope, $sce) {
var element = $compile('<link ng-prop-href="testUrl" rel="stylesheet" />')($rootScope);
$rootScope.testUrl = './css1.css';
$rootScope.$apply();
expect(element.prop('href')).toContain('css1.css');
$rootScope.testUrl = $sce.trustAsResourceUrl('https://elsewhere.example.org/css2.css');
$rootScope.$apply();
expect(element.prop('href')).toContain('https://elsewhere.example.org/css2.css');
}));
});
describe('*[innerHTML]', function() {
describe('SCE disabled', function() {
beforeEach(function() {
module(function($sceProvider) { $sceProvider.enabled(false); });
});
it('should set html', inject(function($rootScope, $compile) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = '<div onclick="">hello</div>';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
}));
it('should update html', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = 'hello';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('hello');
$rootScope.html = 'goodbye';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('goodbye');
}));
it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) {
var element = $compile('<div ng-prop-inner_h_t_m_l="::html"></div>')($rootScope);
$rootScope.html = '<div onclick="">hello</div>';
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.$digest();
expect(element.text()).toEqual('hello');
expect($rootScope.$$watchers.length).toEqual(0);
$rootScope.html = '<div onclick="">hello</div>';
$rootScope.$digest();
expect(element.text()).toEqual('hello');
}));
});
describe('SCE enabled', function() {
it('should NOT set html for untrusted values', inject(function($rootScope, $compile) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = '<div onclick="">hello</div>';
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
}));
it('should NOT set html for wrongly typed values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = $sce.trustAsCss('<div onclick="">hello</div>');
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
}));
it('should set html for trusted values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = $sce.trustAsHtml('<div onclick="">hello</div>');
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
}));
it('should update html', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = $sce.trustAsHtml('hello');
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('hello');
$rootScope.html = $sce.trustAsHtml('goodbye');
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('goodbye');
}));
it('should not cause infinite recursion for trustAsHtml object watches',
inject(function($rootScope, $compile, $sce) {
// Ref: https://github.com/angular/angular.js/issues/3932
// If the binding is a function that creates a new value on every call via trustAs, we'll
// trigger an infinite digest if we don't take care of it.
var element = $compile('<div ng-prop-inner_h_t_m_l="getHtml()"></div>')($rootScope);
$rootScope.getHtml = function() {
return $sce.trustAsHtml('<div onclick="">hello</div>');
};
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
}));
it('should handle custom $sce objects', function() {
function MySafeHtml(val) { this.val = val; }
module(function($provide) {
$provide.decorator('$sce', function($delegate) {
$delegate.trustAsHtml = function(html) { return new MySafeHtml(html); };
$delegate.getTrusted = function(type, mySafeHtml) { return mySafeHtml && mySafeHtml.val; };
$delegate.valueOf = function(v) { return v instanceof MySafeHtml ? v.val : v; };
return $delegate;
});
});
inject(function($rootScope, $compile, $sce) {
// Ref: https://github.com/angular/angular.js/issues/14526
// Previous code used toString for change detection, which fails for custom objects
// that don't override toString.
var element = $compile('<div ng-prop-inner_h_t_m_l="getHtml()"></div>')($rootScope);
var html = 'hello';
$rootScope.getHtml = function() { return $sce.trustAsHtml(html); };
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('hello');
html = 'goodbye';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('goodbye');
});
});
describe('when $sanitize is available', function() {
beforeEach(function() { module('ngSanitize'); });
it('should sanitize untrusted html', inject(function($rootScope, $compile) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = '<div onclick="">hello</div>';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('<div>hello</div>');
}));
});
});
});
describe('*[style]', function() {
// Support: IE9
// Some browsers throw when assignging to HTMLElement.style
function canAssignStyleProp() {
try {
window.document.createElement('div').style = 'margin-left: 10px';
return true;
} catch (e) {
return false;
}
}
it('should NOT set style for untrusted values', inject(function($rootScope, $compile) {
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
$rootScope.style = 'margin-left: 10px';
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
}));
it('should NOT set style for wrongly typed values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
$rootScope.style = $sce.trustAsHtml('margin-left: 10px');
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
}));
if (canAssignStyleProp()) {
it('should set style for trusted values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
$rootScope.style = $sce.trustAsCss('margin-left: 10px');
$rootScope.$digest();
// Support: IE
// IE allows assignments but does not register the styles
// Sometimes the value is '0px', sometimes ''
if (msie) {
expect(parseInt(element.css('margin-left'), 10) || 0).toBe(0);
} else {
expect(element.css('margin-left')).toEqual('10px');
}
}));
}
});
});
-1
View File
@@ -2387,7 +2387,6 @@ describe('Scope', function() {
it('should be cancelled if a $rootScope digest occurs before the next tick', inject(function($rootScope, $browser) {
var apply = spyOn($rootScope, '$apply').and.callThrough();
var cancel = spyOn($browser.defer, 'cancel').and.callThrough();
var expression = jasmine.createSpy('expr');
+9
View File
@@ -194,5 +194,14 @@ describe('$$testability', function() {
$$testability.whenStable(callback);
expect(callback).toHaveBeenCalled();
}));
it('should delegate to `$browser.notifyWhenNoOutstandingRequests()`',
inject(function($$testability, $browser) {
var spy = spyOn($browser, 'notifyWhenNoOutstandingRequests');
var callback = noop;
$$testability.whenStable(callback);
expect(spy).toHaveBeenCalledWith(callback);
}));
});
});
+1
View File
@@ -3,6 +3,7 @@
"new-cap": "off"
},
"globals": {
"getDomNode": false,
"mergeAnimationDetails": false,
"prepareAnimationOptions": false,
"applyAnimationStyles": false,
+99
View File
@@ -0,0 +1,99 @@
'use strict';
describe('ngAnimate $$animateCache', function() {
beforeEach(module('ngAnimate'));
it('should store the details in a lookup', inject(function($$animateCache) {
var data = { 'hello': 'there' };
$$animateCache.put('key', data, true);
expect($$animateCache.get('key')).toBe(data);
}));
it('should update existing stored details in a lookup', inject(function($$animateCache) {
var data = { 'hello': 'there' };
$$animateCache.put('key', data, true);
var otherData = { 'hi': 'you' };
$$animateCache.put('key', otherData, true);
expect($$animateCache.get('key')).toBe(otherData);
}));
it('should create a special cacheKey based on the element/parent and className relationship', inject(function($$animateCache) {
var cacheKey, elm = jqLite('<div></div>');
elm.addClass('one two');
var parent1 = jqLite('<div></div>');
parent1.append(elm);
cacheKey = $$animateCache.cacheKey(getDomNode(elm), 'event');
expect(cacheKey).toBe('1 event one two');
cacheKey = $$animateCache.cacheKey(getDomNode(elm), 'event', 'add');
expect(cacheKey).toBe('1 event one two add');
cacheKey = $$animateCache.cacheKey(getDomNode(elm), 'event', 'add', 'remove');
expect(cacheKey).toBe('1 event one two add remove');
var parent2 = jqLite('<div></div>');
parent2.append(elm);
cacheKey = $$animateCache.cacheKey(getDomNode(elm), 'event');
expect(cacheKey).toBe('2 event one two');
cacheKey = $$animateCache.cacheKey(getDomNode(elm), 'event', 'three', 'four');
expect(cacheKey).toBe('2 event one two three four');
}));
it('should keep a count of how many times a cache key has been updated', inject(function($$animateCache) {
var data = { 'hello': 'there' };
var key = 'key';
expect($$animateCache.count(key)).toBe(0);
$$animateCache.put(key, data, true);
expect($$animateCache.count(key)).toBe(1);
var otherData = { 'other': 'data' };
$$animateCache.put(key, otherData, true);
expect($$animateCache.count(key)).toBe(2);
}));
it('should flush the cache and the counters', inject(function($$animateCache) {
$$animateCache.put('key1', { data: 'value' }, true);
$$animateCache.put('key2', { data: 'value' }, true);
expect($$animateCache.count('key1')).toBe(1);
expect($$animateCache.count('key2')).toBe(1);
$$animateCache.flush();
expect($$animateCache.get('key1')).toBeFalsy();
expect($$animateCache.get('key2')).toBeFalsy();
expect($$animateCache.count('key1')).toBe(0);
expect($$animateCache.count('key2')).toBe(0);
}));
describe('containsCachedAnimationWithoutDuration', function() {
it('should return false if the validity of a key is false', inject(function($$animateCache) {
var validEntry = { someEssentialProperty: true };
var invalidEntry = { someEssentialProperty: false };
$$animateCache.put('key1', validEntry, true);
$$animateCache.put('key2', invalidEntry, false);
expect($$animateCache.containsCachedAnimationWithoutDuration('key1')).toBe(false);
expect($$animateCache.containsCachedAnimationWithoutDuration('key2')).toBe(true);
}));
it('should return false if the key does not exist in the cache', inject(function($$animateCache) {
expect($$animateCache.containsCachedAnimationWithoutDuration('key2')).toBe(false);
$$animateCache.put('key2', {}, false);
expect($$animateCache.containsCachedAnimationWithoutDuration('key2')).toBe(true);
$$animateCache.flush();
expect($$animateCache.containsCachedAnimationWithoutDuration('key2')).toBe(false);
}));
});
});
+273
View File
@@ -577,6 +577,41 @@ describe('animations', function() {
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
}));
it('should remove the element from the `disabledElementsLookup` map on `$destroy`',
inject(function($$Map, $animate, $rootScope) {
var setSpy = spyOn($$Map.prototype, 'set').and.callThrough();
var deleteSpy = spyOn($$Map.prototype, 'delete').and.callThrough();
parent.append(element);
$animate.enabled(element, false);
$animate.enabled(element, true);
$animate.enabled(element, false);
expect(setSpy).toHaveBeenCalledWith(element[0], jasmine.any(Boolean));
expect(deleteSpy).not.toHaveBeenCalledWith(element[0]);
expect($animate.enabled(element)).toBe(false);
// No clean-up on `detach` (no `$destroy` event).
element.detach();
expect(deleteSpy).not.toHaveBeenCalledWith(element[0]);
expect($animate.enabled(element)).toBe(false);
// Clean-up on `remove` (causes `$destroy` event).
element.remove();
expect(deleteSpy).toHaveBeenCalledOnceWith(element[0]);
expect($animate.enabled(element)).toBe(true);
deleteSpy.calls.reset();
element.triggerHandler('$destroy');
expect(deleteSpy).not.toHaveBeenCalledWith(element[0]);
$animate.enabled(element, true);
element.triggerHandler('$destroy');
expect(deleteSpy).toHaveBeenCalledOnceWith(element[0]);
}));
});
it('should strip all comment nodes from the animation and not issue an animation if not real elements are found',
@@ -2784,6 +2819,244 @@ describe('animations', function() {
});
describe('event data', function() {
it('should be included for enter',
inject(function($animate, $rootScope, $rootElement, $document) {
var eventData;
$animate.on('enter', jqLite($document[0].body), function(element, phase, data) {
eventData = data;
});
element = jqLite('<div></div>');
$animate.enter(element, $rootElement, null, {
addClass: 'red blue',
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
$rootScope.$digest();
$animate.flush();
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
}));
it('should be included for leave',
inject(function($animate, $rootScope, $rootElement, $document) {
var eventData;
$animate.on('leave', jqLite($document[0].body), function(element, phase, data) {
eventData = data;
});
var outerContainer = jqLite('<div></div>');
element = jqLite('<div></div>');
outerContainer.append(element);
$rootElement.append(outerContainer);
$animate.leave(element, {
addClass: 'red blue',
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
$animate.flush();
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
})
);
it('should be included for move',
inject(function($animate, $rootScope, $rootElement, $document) {
var eventData;
$animate.on('move', jqLite($document[0].body), function(element, phase, data) {
eventData = data;
});
var parent = jqLite('<div></div>');
var parent2 = jqLite('<div></div>');
element = jqLite('<div></div>');
parent.append(element);
$rootElement.append(parent);
$rootElement.append(parent2);
$animate.move(element, parent2, null, {
addClass: 'red blue',
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
$animate.flush();
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
})
);
it('should be included for addClass', inject(function($animate, $rootElement) {
var eventData;
element = jqLite('<div class="purple"></div>');
$animate.on('addClass', element, function(element, phase, data) {
eventData = data;
});
$rootElement.append(element);
$animate.addClass(element, 'red blue', {
from: {opacity: 0},
to: {opacity: 1}
});
$animate.flush();
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
}));
it('should be included for removeClass', inject(function($animate, $rootElement) {
var eventData;
element = jqLite('<div class="red blue purple"></div>');
$animate.on('removeClass', element, function(element, phase, data) {
eventData = data;
});
$rootElement.append(element);
$animate.removeClass(element, 'red blue', {
from: {opacity: 0},
to: {opacity: 1}
});
$animate.flush();
expect(eventData).toEqual({
removeClass: 'red blue',
addClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
}));
it('should be included for setClass', inject(function($animate, $rootElement) {
var eventData;
element = jqLite('<div class="yellow green purple"></div>');
$animate.on('setClass', element, function(element, phase, data) {
eventData = data;
});
$rootElement.append(element);
$animate.setClass(element, 'red blue', 'yellow green', {
from: {opacity: 0},
to: {opacity: 1}
});
$animate.flush();
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
}));
it('should be included for animate', inject(function($animate, $rootElement) {
// The event for animate changes to 'setClass' if both addClass and removeClass
// are definded, because the operations are merged. However, it is still 'animate'
// and not 'addClass' if only 'addClass' is defined.
// Ideally, we would make this consistent, but it's a BC
var eventData, eventName;
element = jqLite('<div class="yellow green purple"></div>');
$animate.on('setClass', element, function(element, phase, data) {
eventData = data;
eventName = 'setClass';
});
$animate.on('animate', element, function(element, phase, data) {
eventData = data;
eventName = 'animate';
});
$rootElement.append(element);
var runner = $animate.animate(element, {opacity: 0}, {opacity: 1}, null, {
addClass: 'red blue',
removeClass: 'yellow green'
});
$animate.flush();
runner.end();
expect(eventName).toBe('setClass');
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
eventData = eventName = null;
runner = $animate.animate(element, {opacity: 0}, {opacity: 1}, null, {
addClass: 'yellow green'
});
$animate.flush();
runner.end();
expect(eventName).toBe('animate');
expect(eventData).toEqual({
addClass: 'yellow green',
removeClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
eventData = eventName = null;
runner = $animate.animate(element, {opacity: 0}, {opacity: 1}, null, {
removeClass: 'yellow green'
});
$animate.flush();
runner.end();
expect(eventName).toBe('animate');
expect(eventData).toEqual({
addClass: null,
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
}));
});
they('should trigger a callback for a $prop animation if the listener is on the document',
['enter', 'leave'], function($event) {
module(function($provide) {
+44 -9
View File
@@ -36,6 +36,9 @@ describe('$$animation', function() {
});
inject(function($$animation, $animate, $rootScope) {
element = jqLite('<div></div>');
var parent = jqLite('<div></div>');
parent.append(element);
var done = false;
$$animation(element, 'someEvent').then(function() {
done = true;
@@ -197,7 +200,11 @@ describe('$$animation', function() {
});
inject(function($$animation, $rootScope, $animate) {
var status, element = jqLite('<div></div>');
var status;
var element = jqLite('<div></div>');
var parent = jqLite('<div></div>');
parent.append(element);
var runner = $$animation(element, 'enter');
runner.then(function() {
status = 'resolve';
@@ -519,11 +526,24 @@ describe('$$animation', function() {
}));
they('should add the preparation class before the $prop-animation is pushed to the queue',
they('should only apply the ng-$prop-prepare class if there are a child animations',
['enter', 'leave', 'move'], function(animationType) {
inject(function($$animation, $rootScope, $animate) {
var runner = $$animation(element, animationType);
expect(element).toHaveClass('ng-' + animationType + '-prepare');
var expectedClassName = 'ng-' + animationType + '-prepare';
$$animation(element, animationType);
$rootScope.$digest();
expect(element).not.toHaveClass(expectedClassName);
var child = jqLite('<div></div>');
element.append(child);
$$animation(element, animationType);
$$animation(child, animationType);
$rootScope.$digest();
expect(element).not.toHaveClass(expectedClassName);
expect(child).toHaveClass(expectedClassName);
});
});
@@ -531,9 +551,22 @@ describe('$$animation', function() {
they('should remove the preparation class before the $prop-animation starts',
['enter', 'leave', 'move'], function(animationType) {
inject(function($$animation, $rootScope, $$rAF) {
var runner = $$animation(element, animationType);
var expectedClassName = 'ng-' + animationType + '-prepare';
var child = jqLite('<div></div>');
element.append(child);
$$animation(element, animationType);
$$animation(child, animationType);
$rootScope.$digest();
expect(element).not.toHaveClass('ng-' + animationType + '-prepare');
expect(element).not.toHaveClass(expectedClassName);
expect(child).toHaveClass(expectedClassName);
$$rAF.flush();
expect(element).not.toHaveClass(expectedClassName);
expect(child).not.toHaveClass(expectedClassName);
});
});
});
@@ -982,11 +1015,12 @@ describe('$$animation', function() {
});
inject(function($$animation, $rootScope, $animate) {
element.addClass('four');
parent.append(element);
var completed = false;
$$animation(element, 'event', {
from: { background: 'red' },
to: { background: 'blue', 'font-size': '50px' }
from: { height: '100px' },
to: { height: '200px', 'font-size': '50px' }
}).then(function() {
completed = true;
});
@@ -997,7 +1031,7 @@ describe('$$animation', function() {
$rootScope.$digest(); //the runner promise
expect(completed).toBe(true);
expect(element.css('background')).toContain('blue');
expect(element.css('height')).toContain('200px');
expect(element.css('font-size')).toBe('50px');
});
});
@@ -1033,6 +1067,7 @@ describe('$$animation', function() {
});
});
inject(function($$animation, $rootScope, $animate) {
parent.append(element);
element.addClass('four');
var completed = false;
+192
View File
@@ -25,6 +25,7 @@ describe('ngAnimate integration tests', function() {
ss.destroy();
});
it('should cancel a running and started removeClass animation when a follow-up addClass animation adds the same class',
inject(function($animate, $rootScope, $$rAF, $document, $rootElement) {
@@ -316,6 +317,8 @@ describe('ngAnimate integration tests', function() {
it('should issue a RAF for each element animation on all DOM levels', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$rAF) {
ss.addRule('.ng-enter', 'transition:2s linear all;');
element = jqLite(
'<div ng-class="{parent:exp}">' +
'<div ng-class="{parent2:exp}">' +
@@ -360,6 +363,7 @@ describe('ngAnimate integration tests', function() {
});
});
it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
@@ -396,6 +400,155 @@ describe('ngAnimate integration tests', function() {
});
it('should avoid adding the ng-enter-prepare method to a parent structural animation that contains child animations', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$rAF) {
element = jqLite(
'<div ng-animate-children="true">' +
'<div ng-if="parent" class="parent">' +
'<div ng-if="child" class="child">' +
'<div ng-class="{something:true}"></div>' +
'</div>' +
'</div>' +
'</div>'
);
ss.addRule('.ng-enter', 'transition:2s linear all;');
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.parent = true;
$rootScope.child = true;
$rootScope.$digest();
var parent = jqLite(element[0].querySelector('.parent'));
var child = jqLite(element[0].querySelector('.child'));
expect(parent).not.toHaveClass('ng-enter-prepare');
expect(child).toHaveClass('ng-enter-prepare');
$$rAF.flush();
expect(parent).not.toHaveClass('ng-enter-prepare');
expect(child).not.toHaveClass('ng-enter-prepare');
});
});
it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
element = jqLite(
'<div ng-class="{parent:exp}">' +
'<div ng-if="exp">' +
'</div>' +
'</div>'
);
ss.addRule('.ng-enter', 'transition:2s linear all;');
ss.addRule('.parent-add', 'transition:5s linear all;');
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.exp = true;
$rootScope.$digest();
var parent = element;
var child = element.find('div');
expect(parent).not.toHaveClass('parent');
expect(parent).toHaveClass('parent-add');
expect(child).not.toHaveClass('ng-enter');
expect(child).toHaveClass('ng-enter-prepare');
$animate.flush();
expect(parent).toHaveClass('parent parent-add parent-add-active');
expect(child).toHaveClass('ng-enter ng-enter-active');
expect(child).not.toHaveClass('ng-enter-prepare');
});
});
it('should remove the prepare classes when different structural animations happen in the same digest', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$animateCache) {
element = jqLite(
// Class animation on parent element is neeeded so the child elements get the prepare class
'<div id="outer" ng-class="{blue: cond}" ng-switch="cond">' +
'<div id="default" ng-switch-default></div>' +
'<div id="truthy" ng-switch-when="true"></div>' +
'</div>'
);
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.cond = false;
$rootScope.$digest();
$rootScope.cond = true;
$rootScope.$digest();
var parent = element;
var truthySwitch = jqLite(parent[0].querySelector('#truthy'));
var defaultSwitch = jqLite(parent[0].querySelector('#default'));
expect(parent).not.toHaveClass('blue');
expect(parent).toHaveClass('blue-add');
expect(truthySwitch).toHaveClass('ng-enter-prepare');
expect(defaultSwitch).toHaveClass('ng-leave-prepare');
$animate.flush();
expect(parent).toHaveClass('blue');
expect(parent).not.toHaveClass('blue-add');
expect(truthySwitch).not.toHaveClass('ng-enter-prepare');
expect(defaultSwitch).not.toHaveClass('ng-leave-prepare');
});
});
it('should respect the element node for caching when animations with the same type happen in the same digest', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$animateCache) {
ss.addRule('.animate.ng-enter', 'transition:2s linear all;');
element = jqLite(
'<div>' +
'<div>' +
'<div id="noanimate" ng-if="cond"></div>' +
'</div>' +
'<div>' +
'<div id="animate" class="animate" ng-if="cond"></div>' +
'</div>' +
'</div>'
);
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.cond = true;
$rootScope.$digest();
var parent = element;
var noanimate = jqLite(parent[0].querySelector('#noanimate'));
var animate = jqLite(parent[0].querySelector('#animate'));
expect(noanimate).not.toHaveClass('ng-enter');
expect(animate).toHaveClass('ng-enter');
$animate.closeAndFlush();
expect(noanimate).not.toHaveClass('ng-enter');
expect(animate).not.toHaveClass('ng-enter');
});
});
it('should pack level elements into their own RAF flush', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
@@ -544,6 +697,45 @@ describe('ngAnimate integration tests', function() {
expect(child).not.toHaveClass('blue');
});
});
it('should not apply ngAnimate CSS preparation classes when a css animation definition has duration = 0', function() {
function fill(max) {
var arr = [];
for (var i = 0; i < max; i++) {
arr.push(i);
}
return arr;
}
inject(function($animate, $rootScope, $compile, $timeout, $$rAF, $$jqLite) {
ss.addRule('.animate-me', 'transition: all 0.5s;');
var classAddSpy = spyOn($$jqLite, 'addClass').and.callThrough();
var classRemoveSpy = spyOn($$jqLite, 'removeClass').and.callThrough();
element = jqLite(
'<div>' +
'<div ng-repeat="item in items"></div>' +
'</div> '
);
html(element);
$compile(element)($rootScope);
$rootScope.items = fill(100);
$rootScope.$digest();
expect(classAddSpy.calls.count()).toBe(2);
expect(classRemoveSpy.calls.count()).toBe(2);
expect(classAddSpy.calls.argsFor(0)[1]).toBe('ng-animate');
expect(classAddSpy.calls.argsFor(1)[1]).toBe('ng-enter');
expect(classRemoveSpy.calls.argsFor(0)[1]).toBe('ng-enter');
expect(classRemoveSpy.calls.argsFor(1)[1]).toBe('ng-animate');
expect(element.children().length).toBe(100);
});
});
});
describe('JS animations', function() {
+347 -102
View File
@@ -1,5 +1,7 @@
'use strict';
/* globals nodeBlackList false */
describe('$aria', function() {
var scope, $compile, element;
@@ -9,17 +11,236 @@ describe('$aria', function() {
dealoc(element);
});
function injectScopeAndCompiler() {
return inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
scope = _$rootScope_;
});
}
describe('with `ngAriaDisable`', function() {
beforeEach(injectScopeAndCompiler);
beforeEach(function() {
jasmine.addMatchers({
toHaveAttribute: function toHaveAttributeMatcher() {
return {
compare: function toHaveAttributeCompare(element, attr) {
var node = element[0];
var pass = node.hasAttribute(attr);
var message = 'Expected `' + node.outerHTML + '` ' + (pass ? 'not ' : '') +
'to have attribute `' + attr + '`.';
function compileElement(inputHtml) {
element = $compile(inputHtml)(scope);
scope.$digest();
}
return {
pass: pass,
message: message
};
}
};
}
});
});
// ariaChecked
it('should not attach aria-checked to custom checkbox', function() {
compileElement('<div role="checkbox" ng-model="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-checked');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-checked');
});
it('should not attach aria-checked to custom radio controls', function() {
compileElement(
'<div role="radio" ng-model="val" value="one" ng-aria-disable></div>' +
'<div role="radio" ng-model="val" value="two" ng-aria-disable></div>');
var radio1 = element.eq(0);
var radio2 = element.eq(1);
scope.$apply('val = "one"');
expect(radio1).not.toHaveAttribute('aria-checked');
expect(radio2).not.toHaveAttribute('aria-checked');
scope.$apply('val = "two"');
expect(radio1).not.toHaveAttribute('aria-checked');
expect(radio2).not.toHaveAttribute('aria-checked');
});
// ariaDisabled
it('should not attach aria-disabled to custom controls', function() {
compileElement('<div ng-disabled="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-disabled');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-disabled');
});
// ariaHidden
it('should not attach aria-hidden to `ngShow`', function() {
compileElement('<div ng-show="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-hidden');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-hidden');
});
it('should not attach aria-hidden to `ngHide`', function() {
compileElement('<div ng-hide="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-hidden');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-hidden');
});
// ariaInvalid
it('should not attach aria-invalid to input', function() {
compileElement('<input ng-model="val" ng-minlength="10" ng-aria-disable />');
scope.$apply('val = "lt 10"');
expect(element).not.toHaveAttribute('aria-invalid');
scope.$apply('val = "gt 10 characters"');
expect(element).not.toHaveAttribute('aria-invalid');
});
it('should not attach aria-invalid to custom controls', function() {
compileElement('<div role="textbox" ng-model="val" ng-minlength="10" ng-aria-disable></div>');
scope.$apply('val = "lt 10"');
expect(element).not.toHaveAttribute('aria-invalid');
scope.$apply('val = "gt 10 characters"');
expect(element).not.toHaveAttribute('aria-invalid');
});
// ariaLive
it('should not attach aria-live to `ngMessages`', function() {
compileElement('<div ng-messages="val" ng-aria-disable>');
expect(element).not.toHaveAttribute('aria-live');
});
// ariaReadonly
it('should not attach aria-readonly to custom controls', function() {
compileElement('<div ng-readonly="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-readonly');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-readonly');
});
// ariaRequired
it('should not attach aria-required to custom controls with `required`', function() {
compileElement('<div ng-model="val" required ng-aria-disable></div>');
expect(element).not.toHaveAttribute('aria-required');
});
it('should not attach aria-required to custom controls with `ngRequired`', function() {
compileElement('<div ng-model="val" ng-required="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-required');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-required');
});
// ariaValue
it('should not attach aria-value* to input[range]', function() {
compileElement('<input type="range" ng-model="val" min="0" max="100" ng-aria-disable />');
expect(element).not.toHaveAttribute('aria-valuemax');
expect(element).not.toHaveAttribute('aria-valuemin');
expect(element).not.toHaveAttribute('aria-valuenow');
scope.$apply('val = 50');
expect(element).not.toHaveAttribute('aria-valuemax');
expect(element).not.toHaveAttribute('aria-valuemin');
expect(element).not.toHaveAttribute('aria-valuenow');
scope.$apply('val = 150');
expect(element).not.toHaveAttribute('aria-valuemax');
expect(element).not.toHaveAttribute('aria-valuemin');
expect(element).not.toHaveAttribute('aria-valuenow');
});
it('should not attach aria-value* to custom controls', function() {
compileElement(
'<div role="progressbar" ng-model="val" min="0" max="100" ng-aria-disable></div>' +
'<div role="slider" ng-model="val" min="0" max="100" ng-aria-disable></div>');
var progressbar = element.eq(0);
var slider = element.eq(1);
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
expect(progressbar).not.toHaveAttribute(attr);
expect(slider).not.toHaveAttribute(attr);
});
scope.$apply('val = 50');
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
expect(progressbar).not.toHaveAttribute(attr);
expect(slider).not.toHaveAttribute(attr);
});
scope.$apply('val = 150');
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
expect(progressbar).not.toHaveAttribute(attr);
expect(slider).not.toHaveAttribute(attr);
});
});
// bindKeypress
it('should not bind keypress to `ngClick`', function() {
scope.onClick = jasmine.createSpy('onClick');
compileElement(
'<div ng-click="onClick()" tabindex="0" ng-aria-disable></div>' +
'<ul><li ng-click="onClick()" tabindex="0" ng-aria-disable></li></ul>');
var div = element.find('div');
var li = element.find('li');
div.triggerHandler({type: 'keypress', keyCode: 32});
li.triggerHandler({type: 'keypress', keyCode: 32});
expect(scope.onClick).not.toHaveBeenCalled();
});
// bindRoleForClick
it('should not attach role to custom controls', function() {
compileElement(
'<div ng-click="onClick()" ng-aria-disable></div>' +
'<div type="checkbox" ng-model="val" ng-aria-disable></div>' +
'<div type="radio" ng-model="val" ng-aria-disable></div>' +
'<div type="range" ng-model="val" ng-aria-disable></div>');
expect(element.eq(0)).not.toHaveAttribute('role');
expect(element.eq(1)).not.toHaveAttribute('role');
expect(element.eq(2)).not.toHaveAttribute('role');
expect(element.eq(3)).not.toHaveAttribute('role');
});
// tabindex
it('should not attach tabindex to custom controls', function() {
compileElement(
'<div role="checkbox" ng-model="val" ng-aria-disable></div>' +
'<div role="slider" ng-model="val" ng-aria-disable></div>');
expect(element.eq(0)).not.toHaveAttribute('tabindex');
expect(element.eq(1)).not.toHaveAttribute('tabindex');
});
it('should not attach tabindex to `ngClick` or `ngDblclick`', function() {
compileElement(
'<div ng-click="onClick()" ng-aria-disable></div>' +
'<div ng-dblclick="onDblclick()" ng-aria-disable></div>');
expect(element.eq(0)).not.toHaveAttribute('tabindex');
expect(element.eq(1)).not.toHaveAttribute('tabindex');
});
});
describe('aria-hidden', function() {
beforeEach(injectScopeAndCompiler);
@@ -703,115 +924,127 @@ describe('$aria', function() {
});
describe('accessible actions', function() {
var clickEvents;
beforeEach(injectScopeAndCompiler);
beforeEach(function() {
clickEvents = [];
scope.onClick = jasmine.createSpy('onClick').and.callFake(function(evt) {
var nodeName = evt ? evt.target.nodeName.toLowerCase() : '';
var prevented = !!(evt && evt.isDefaultPrevented());
clickEvents.push(nodeName + '(' + prevented + ')');
});
});
var clickFn;
it('should trigger a click from the keyboard (and prevent default action)', function() {
compileElement(
'<section>' +
'<div ng-click="onClick($event)"></div>' +
'<ul><li ng-click="onClick($event)"></li></ul>' +
'</section>');
it('should trigger a click from the keyboard', function() {
scope.someAction = function() {};
var elements = $compile('<section>' +
'<div class="div-click" ng-click="someAction(\'div\')" tabindex="0"></div>' +
'<ul><li ng-click="someAction( \'li\')" tabindex="0"></li></ul>' +
'</section>')(scope);
scope.$digest();
clickFn = spyOn(scope, 'someAction');
var divElement = elements.find('div');
var liElement = elements.find('li');
var divElement = element.find('div');
var liElement = element.find('li');
divElement.triggerHandler({type: 'keydown', keyCode: 13});
liElement.triggerHandler({type: 'keydown', keyCode: 13});
divElement.triggerHandler({type: 'keydown', keyCode: 32});
liElement.triggerHandler({type: 'keydown', keyCode: 32});
expect(clickFn).toHaveBeenCalledWith('div');
expect(clickFn).toHaveBeenCalledWith('li');
expect(clickEvents).toEqual(['div(true)', 'li(true)', 'div(true)', 'li(true)']);
});
it('should trigger a click in browsers that provide event.which instead of event.keyCode', function() {
scope.someAction = function() {};
they('should not prevent default keyboard action if an interactive $type element' +
'is nested inside ng-click', nodeBlackList, function(elementType) {
function createHTML(type) {
return '<' + type + '></' + type + '>';
}
var elements = $compile('<section>' +
'<div class="div-click" ng-click="someAction(\'div\')" tabindex="0"></div>' +
'<ul><li ng-click="someAction( \'li\')" tabindex="0"></li></ul>' +
'</section>')(scope);
compileElement(
'<section>' +
'<div ng-click="onClick($event)">' + createHTML(elementType) + '</div>' +
'</section>');
scope.$digest();
var divElement = element.find('div');
var interactiveElement = element.find(elementType);
clickFn = spyOn(scope, 'someAction');
// Use browserTrigger because it supports event bubbling
// 13 Enter
browserTrigger(interactiveElement, 'keydown', {cancelable: true, bubbles: true, keyCode: 13});
expect(clickEvents).toEqual([elementType.toLowerCase() + '(false)']);
var divElement = elements.find('div');
var liElement = elements.find('li');
clickEvents = [];
divElement.triggerHandler({type: 'keydown', which: 32});
liElement.triggerHandler({type: 'keydown', which: 32});
// 32 Space
browserTrigger(interactiveElement, 'keydown', {cancelable: true, bubbles: true, keyCode: 32});
expect(clickEvents).toEqual([elementType.toLowerCase() + '(false)']);
}
);
expect(clickFn).toHaveBeenCalledWith('div');
expect(clickFn).toHaveBeenCalledWith('li');
});
it('should trigger a click in browsers that provide `event.which` instead of `event.keyCode`',
function() {
compileElement(
'<section>' +
'<div ng-click="onClick($event)"></div>' +
'<ul><li ng-click="onClick($event)"></li></ul>' +
'</section>');
it('should not bind to key events if there is existing ng-keydown', function() {
scope.onClick = jasmine.createSpy('onClick');
scope.onKeydown = jasmine.createSpy('onKeydown');
var divElement = element.find('div');
var liElement = element.find('li');
var tmpl = '<div ng-click="onClick()" ng-keydown="onKeydown()" tabindex="0"></div>';
compileElement(tmpl);
divElement.triggerHandler({type: 'keydown', which: 13});
liElement.triggerHandler({type: 'keydown', which: 13});
divElement.triggerHandler({type: 'keydown', which: 32});
liElement.triggerHandler({type: 'keydown', which: 32});
element.triggerHandler({type: 'keydown', keyCode: 32});
expect(clickEvents).toEqual(['div(true)', 'li(true)', 'div(true)', 'li(true)']);
}
);
expect(scope.onKeydown).toHaveBeenCalled();
expect(scope.onClick).not.toHaveBeenCalled();
});
they('should not bind to key events if there is existing `ng-$prop`',
['keydown', 'keypress', 'keyup'], function(eventName) {
scope.onKeyEvent = jasmine.createSpy('onKeyEvent');
compileElement('<div ng-click="onClick()" ng-' + eventName + '="onKeyEvent()"></div>');
it('should not bind to key events if there is existing ng-keypress', function() {
scope.onClick = jasmine.createSpy('onClick');
scope.onKeypress = jasmine.createSpy('onKeypress');
element.triggerHandler({type: eventName, keyCode: 13});
element.triggerHandler({type: eventName, keyCode: 32});
var tmpl = '<div ng-click="onClick()" ng-keypress="onKeypress()" tabindex="0"></div>';
compileElement(tmpl);
element.triggerHandler({type: 'keypress', keyCode: 32});
expect(scope.onKeypress).toHaveBeenCalled();
expect(scope.onClick).not.toHaveBeenCalled();
});
it('should not bind to key events if there is existing ng-keyup', function() {
scope.onClick = jasmine.createSpy('onClick');
scope.onKeyup = jasmine.createSpy('onKeyup');
var tmpl = '<div ng-click="onClick()" ng-keyup="onKeyup()" tabindex="0"></div>';
compileElement(tmpl);
element.triggerHandler({type: 'keyup', keyCode: 32});
expect(scope.onKeyup).toHaveBeenCalled();
expect(scope.onClick).not.toHaveBeenCalled();
});
expect(scope.onClick).not.toHaveBeenCalled();
expect(scope.onKeyEvent).toHaveBeenCalledTimes(2);
}
);
it('should update bindings when keydown is handled', function() {
compileElement('<div ng-click="text = \'clicked!\'">{{text}}</div>');
expect(element.text()).toBe('');
spyOn(scope.$root, '$digest').and.callThrough();
element.triggerHandler({ type: 'keydown', keyCode: 13 });
expect(element.text()).toBe('clicked!');
expect(scope.$root.$digest).toHaveBeenCalledOnce();
scope.count = 0;
compileElement('<div ng-click="count = count + 1">Count: {{ count }}</div>');
expect(element.text()).toBe('Count: 0');
element.triggerHandler({type: 'keydown', keyCode: 13});
expect(element.text()).toBe('Count: 1');
element.triggerHandler({type: 'keydown', keyCode: 32});
expect(element.text()).toBe('Count: 2');
});
it('should pass $event to ng-click handler as local', function() {
compileElement('<div ng-click="event = $event">{{event.type}}' +
'{{event.keyCode}}</div>');
it('should pass `$event` to `ng-click` handler as local', function() {
compileElement('<div ng-click="event = $event">{{ event.type }}{{ event.keyCode }}</div>');
expect(element.text()).toBe('');
element.triggerHandler({ type: 'keydown', keyCode: 13 });
element.triggerHandler({type: 'keydown', keyCode: 13});
expect(element.text()).toBe('keydown13');
element.triggerHandler({type: 'keydown', keyCode: 32});
expect(element.text()).toBe('keydown32');
});
it('should not bind keydown to natively interactive elements', function() {
compileElement('<button ng-click="event = $event">{{event.type}}{{event.keyCode}}</button>');
expect(element.text()).toBe('');
element.triggerHandler({ type: 'keydown', keyCode: 13 });
expect(element.text()).toBe('');
compileElement('<button ng-click="onClick()">Click me</button>');
element.triggerHandler({type: 'keydown', keyCode: 13});
element.triggerHandler({type: 'keydown', keyCode: 32});
expect(scope.onClick).not.toHaveBeenCalled();
});
});
@@ -895,19 +1128,31 @@ describe('$aria', function() {
expect(element.attr('tabindex')).toBe('0');
});
});
});
function expectAriaAttrOnEachElement(elem, ariaAttr, expected) {
angular.forEach(elem, function(val) {
expect(angular.element(val).attr(ariaAttr)).toBe(expected);
});
}
// Helpers
function compileElement(inputHtml) {
element = $compile(inputHtml)(scope);
scope.$digest();
}
function configAriaProvider(config) {
return function() {
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider) {
$ariaProvider.config(config);
function configAriaProvider(config) {
return function() {
module(function($ariaProvider) {
$ariaProvider.config(config);
});
};
}
function expectAriaAttrOnEachElement(elem, ariaAttr, expected) {
angular.forEach(elem, function(val) {
expect(angular.element(val).attr(ariaAttr)).toBe(expected);
});
module('ariaTest');
};
}
}
function injectScopeAndCompiler() {
return inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
scope = _$rootScope_;
});
}
});
+94
View File
@@ -661,6 +661,100 @@ describe('ngMessages', function() {
);
describe('default message', function() {
it('should render a default message when no message matches', inject(function($rootScope, $compile) {
element = $compile('<div ng-messages="col">' +
' <div ng-message="val">Message is set</div>' +
' <div ng-message-default>Default message is set</div>' +
'</div>')($rootScope);
$rootScope.$apply(function() {
$rootScope.col = { unexpected: false };
});
$rootScope.$digest();
expect(element.text().trim()).toBe('');
expect(element).not.toHaveClass('ng-active');
$rootScope.$apply(function() {
$rootScope.col = { unexpected: true };
});
expect(element.text().trim()).toBe('Default message is set');
expect(element).toHaveClass('ng-active');
$rootScope.$apply(function() {
$rootScope.col = { unexpected: false };
});
expect(element.text().trim()).toBe('');
expect(element).not.toHaveClass('ng-active');
$rootScope.$apply(function() {
$rootScope.col = { val: true, unexpected: true };
});
expect(element.text().trim()).toBe('Message is set');
expect(element).toHaveClass('ng-active');
}));
it('should not render a default message with ng-messages-multiple if another error matches',
inject(function($rootScope, $compile) {
element = $compile('<div ng-messages="col" ng-messages-multiple>' +
' <div ng-message="val">Message is set</div>' +
' <div ng-message="other">Other message is set</div>' +
' <div ng-message-default>Default message is set</div>' +
'</div>')($rootScope);
expect(element.text().trim()).toBe('');
$rootScope.$apply(function() {
$rootScope.col = { val: true, other: false, unexpected: false };
});
expect(element.text().trim()).toBe('Message is set');
$rootScope.$apply(function() {
$rootScope.col = { val: true, other: true, unexpected: true };
});
expect(element.text().trim()).toBe('Message is set Other message is set');
$rootScope.$apply(function() {
$rootScope.col = { val: false, other: false, unexpected: true };
});
expect(element.text().trim()).toBe('Default message is set');
})
);
it('should handle a default message with ngIf', inject(function($rootScope, $compile) {
element = $compile('<div ng-messages="col">' +
' <div ng-message="val">Message is set</div>' +
' <div ng-if="default" ng-message-default>Default message is set</div>' +
'</div>')($rootScope);
$rootScope.default = true;
$rootScope.col = {unexpected: true};
$rootScope.$digest();
expect(element.text().trim()).toBe('Default message is set');
$rootScope.$apply('default = false');
expect(element.text().trim()).toBe('');
$rootScope.$apply('default = true');
expect(element.text().trim()).toBe('Default message is set');
$rootScope.$apply(function() {
$rootScope.col = { val: true };
});
expect(element.text().trim()).toBe('Message is set');
}));
});
describe('when including templates', function() {
they('should work with a dynamic collection model which is managed by ngRepeat',
{'<div ng-messages-include="...">': '<div ng-messages="item">' +
+404 -53
View File
@@ -323,16 +323,16 @@ describe('ngMock', function() {
it('should NOT call $apply if invokeApply is set to false',
inject(function($interval, $rootScope) {
var applySpy = spyOn($rootScope, '$apply').and.callThrough();
var digestSpy = spyOn($rootScope, '$digest').and.callThrough();
var counter = 0;
$interval(function increment() { counter++; }, 1000, 0, false);
expect(applySpy).not.toHaveBeenCalled();
expect(digestSpy).not.toHaveBeenCalled();
expect(counter).toBe(0);
$interval.flush(2000);
expect(applySpy).not.toHaveBeenCalled();
expect(digestSpy).not.toHaveBeenCalled();
expect(counter).toBe(2);
}));
@@ -601,7 +601,7 @@ describe('ngMock', function() {
});
describe('defer', function() {
describe('$browser', function() {
var browser, log;
beforeEach(inject(function($browser) {
browser = $browser;
@@ -614,47 +614,292 @@ describe('ngMock', function() {
};
}
it('should flush', function() {
browser.defer(logFn('A'));
expect(log).toEqual('');
browser.defer.flush();
expect(log).toEqual('A;');
describe('defer.flush', function() {
it('should flush', function() {
browser.defer(logFn('A'));
browser.defer(logFn('B'), null, 'taskType');
expect(log).toEqual('');
browser.defer.flush();
expect(log).toEqual('A;B;');
});
it('should flush delayed', function() {
browser.defer(logFn('A'));
browser.defer(logFn('B'), 0, 'taskTypeB');
browser.defer(logFn('C'), 10, 'taskTypeC');
browser.defer(logFn('D'), 20);
expect(log).toEqual('');
expect(browser.defer.now).toEqual(0);
browser.defer.flush(0);
expect(log).toEqual('A;B;');
browser.defer.flush();
expect(log).toEqual('A;B;C;D;');
});
it('should defer and flush over time', function() {
browser.defer(logFn('A'), 1);
browser.defer(logFn('B'), 2, 'taskType');
browser.defer(logFn('C'), 3);
browser.defer.flush(0);
expect(browser.defer.now).toEqual(0);
expect(log).toEqual('');
browser.defer.flush(1);
expect(browser.defer.now).toEqual(1);
expect(log).toEqual('A;');
browser.defer.flush(2);
expect(browser.defer.now).toEqual(3);
expect(log).toEqual('A;B;C;');
});
it('should throw an exception if there is nothing to be flushed', function() {
expect(function() {browser.defer.flush();}).toThrowError('No deferred tasks to be flushed');
});
it('should not throw an exception when passing a specific delay', function() {
expect(function() {browser.defer.flush(100);}).not.toThrow();
});
describe('tasks scheduled during flushing', function() {
it('should be flushed if they do not exceed the target delay (when no delay specified)',
function() {
browser.defer(function() {
logFn('1')();
browser.defer(function() {
logFn('3')();
browser.defer(logFn('4'), 1);
}, 2);
}, 1);
browser.defer(function() {
logFn('2')();
browser.defer(logFn('6'), 4);
}, 2);
browser.defer(logFn('5'), 5);
browser.defer.flush(0);
expect(browser.defer.now).toEqual(0);
expect(log).toEqual('');
browser.defer.flush();
expect(browser.defer.now).toEqual(5);
expect(log).toEqual('1;2;3;4;5;');
}
);
it('should be flushed if they do not exceed the specified delay',
function() {
browser.defer(function() {
logFn('1')();
browser.defer(function() {
logFn('3')();
browser.defer(logFn('4'), 1);
}, 2);
}, 1);
browser.defer(function() {
logFn('2')();
browser.defer(logFn('6'), 4);
}, 2);
browser.defer(logFn('5'), 5);
browser.defer.flush(0);
expect(browser.defer.now).toEqual(0);
expect(log).toEqual('');
browser.defer.flush(4);
expect(browser.defer.now).toEqual(4);
expect(log).toEqual('1;2;3;4;');
browser.defer.flush(6);
expect(browser.defer.now).toEqual(10);
expect(log).toEqual('1;2;3;4;5;6;');
}
);
});
});
it('should flush delayed', function() {
browser.defer(logFn('A'));
browser.defer(logFn('B'), 10);
browser.defer(logFn('C'), 20);
expect(log).toEqual('');
describe('defer.cancel', function() {
it('should cancel a pending task', function() {
var taskId1 = browser.defer(logFn('A'), 100, 'fooType');
var taskId2 = browser.defer(logFn('B'), 200);
expect(browser.defer.now).toEqual(0);
browser.defer.flush(0);
expect(log).toEqual('A;');
expect(log).toBe('');
expect(function() {browser.defer.verifyNoPendingTasks('fooType');}).toThrow();
expect(function() {browser.defer.verifyNoPendingTasks();}).toThrow();
browser.defer.flush();
expect(log).toEqual('A;B;C;');
browser.defer.cancel(taskId1);
expect(function() {browser.defer.verifyNoPendingTasks('fooType');}).not.toThrow();
expect(function() {browser.defer.verifyNoPendingTasks();}).toThrow();
browser.defer.cancel(taskId2);
expect(function() {browser.defer.verifyNoPendingTasks('fooType');}).not.toThrow();
expect(function() {browser.defer.verifyNoPendingTasks();}).not.toThrow();
browser.defer.flush(1000);
expect(log).toBe('');
});
});
it('should defer and flush over time', function() {
browser.defer(logFn('A'), 1);
browser.defer(logFn('B'), 2);
browser.defer(logFn('C'), 3);
describe('defer.verifyNoPendingTasks', function() {
it('should throw if there are pending tasks', function() {
expect(browser.defer.verifyNoPendingTasks).not.toThrow();
browser.defer.flush(0);
expect(browser.defer.now).toEqual(0);
expect(log).toEqual('');
browser.defer(noop);
expect(browser.defer.verifyNoPendingTasks).toThrow();
});
browser.defer.flush(1);
expect(browser.defer.now).toEqual(1);
expect(log).toEqual('A;');
it('should list the pending tasks (in order) in the error message', function() {
browser.defer(noop, 100);
browser.defer(noop, 300, 'fooType');
browser.defer(noop, 200, 'barType');
browser.defer.flush(2);
expect(browser.defer.now).toEqual(3);
expect(log).toEqual('A;B;C;');
var expectedError =
'Deferred tasks to flush (3):\n' +
' {id: 0, type: $$default$$, time: 100}\n' +
' {id: 2, type: barType, time: 200}\n' +
' {id: 1, type: fooType, time: 300}';
expect(browser.defer.verifyNoPendingTasks).toThrowError(expectedError);
});
describe('with specific task type', function() {
it('should throw if there are pending tasks', function() {
browser.defer(noop, 0, 'fooType');
expect(function() {browser.defer.verifyNoPendingTasks('barType');}).not.toThrow();
expect(function() {browser.defer.verifyNoPendingTasks('fooType');}).toThrow();
expect(function() {browser.defer.verifyNoPendingTasks();}).toThrow();
});
it('should list the pending tasks (in order) in the error message', function() {
browser.defer(noop, 100);
browser.defer(noop, 300, 'fooType');
browser.defer(noop, 200, 'barType');
browser.defer(noop, 400, 'fooType');
var expectedError =
'Deferred tasks to flush (2):\n' +
' {id: 1, type: fooType, time: 300}\n' +
' {id: 3, type: fooType, time: 400}';
expect(function() {browser.defer.verifyNoPendingTasks('fooType');}).
toThrowError(expectedError);
});
});
});
it('should throw an exception if there is nothing to be flushed', function() {
expect(function() {browser.defer.flush();}).toThrowError('No deferred tasks to be flushed');
describe('notifyWhenNoOutstandingRequests', function() {
var callback;
beforeEach(function() {
callback = jasmine.createSpy('callback');
});
it('should immediately run the callback if no pending tasks', function() {
browser.notifyWhenNoOutstandingRequests(callback);
expect(callback).toHaveBeenCalled();
});
it('should run the callback as soon as there are no pending tasks', function() {
browser.defer(noop, 100);
browser.defer(noop, 200);
browser.notifyWhenNoOutstandingRequests(callback);
expect(callback).not.toHaveBeenCalled();
browser.defer.flush(100);
expect(callback).not.toHaveBeenCalled();
browser.defer.flush(100);
expect(callback).toHaveBeenCalled();
});
it('should not run the callback more than once', function() {
browser.defer(noop, 100);
browser.notifyWhenNoOutstandingRequests(callback);
expect(callback).not.toHaveBeenCalled();
browser.defer.flush(100);
expect(callback).toHaveBeenCalledOnce();
browser.defer(noop, 200);
browser.defer.flush(100);
expect(callback).toHaveBeenCalledOnce();
});
describe('with specific task type', function() {
it('should immediately run the callback if no pending tasks', function() {
browser.notifyWhenNoOutstandingRequests(callback, 'fooType');
expect(callback).toHaveBeenCalled();
});
it('should run the callback as soon as there are no pending tasks', function() {
browser.defer(noop, 100, 'fooType');
browser.defer(noop, 200, 'barType');
browser.notifyWhenNoOutstandingRequests(callback, 'fooType');
expect(callback).not.toHaveBeenCalled();
browser.defer.flush(100);
expect(callback).toHaveBeenCalled();
});
it('should not run the callback more than once', function() {
browser.defer(noop, 100, 'fooType');
browser.defer(noop, 200);
browser.notifyWhenNoOutstandingRequests(callback, 'fooType');
expect(callback).not.toHaveBeenCalled();
browser.defer.flush(100);
expect(callback).toHaveBeenCalledOnce();
browser.defer.flush(100);
expect(callback).toHaveBeenCalledOnce();
browser.defer(noop, 100, 'fooType');
browser.defer(noop, 200);
browser.defer.flush();
expect(callback).toHaveBeenCalledOnce();
});
});
});
});
describe('$flushPendingTasks', function() {
var $flushPendingTasks;
var browserDeferFlushSpy;
beforeEach(inject(function($browser, _$flushPendingTasks_) {
$flushPendingTasks = _$flushPendingTasks_;
browserDeferFlushSpy = spyOn($browser.defer, 'flush').and.returnValue('flushed');
}));
it('should delegate to `$browser.defer.flush()`', function() {
var result = $flushPendingTasks(42);
expect(browserDeferFlushSpy).toHaveBeenCalledOnceWith(42);
expect(result).toBe('flushed');
});
});
describe('$verifyNoPendingTasks', function() {
var $verifyNoPendingTasks;
var browserDeferVerifySpy;
beforeEach(inject(function($browser, _$verifyNoPendingTasks_) {
$verifyNoPendingTasks = _$verifyNoPendingTasks_;
browserDeferVerifySpy = spyOn($browser.defer, 'verifyNoPendingTasks').and.returnValue('verified');
}));
it('should delegate to `$browser.defer.verifyNoPendingTasks()`', function() {
var result = $verifyNoPendingTasks('fortyTwo');
expect(browserDeferVerifySpy).toHaveBeenCalledOnceWith('fortyTwo');
expect(result).toBe('verified');
});
});
@@ -705,47 +950,74 @@ describe('ngMock', function() {
describe('$timeout', function() {
it('should expose flush method that will flush the pending queue of tasks', inject(
function($timeout) {
function($rootScope, $timeout) {
var logger = [],
logFn = function(msg) { return function() { logger.push(msg); }; };
$timeout(logFn('t1'));
$timeout(logFn('t2'), 200);
$rootScope.$evalAsync(logFn('rs')); // Non-timeout tasks are flushed as well.
$timeout(logFn('t3'));
expect(logger).toEqual([]);
$timeout.flush();
expect(logger).toEqual(['t1', 't3', 't2']);
expect(logger).toEqual(['t1', 'rs', 't3', 't2']);
}));
it('should throw an exception when not flushed', inject(function($timeout) {
$timeout(noop);
it('should throw an exception when not flushed', inject(function($rootScope, $timeout) {
$timeout(noop, 100);
$rootScope.$evalAsync(noop);
var expectedError = 'Deferred tasks to flush (1): {id: 0, time: 0}';
expect(function() {$timeout.verifyNoPendingTasks();}).toThrowError(expectedError);
var expectedError =
'Deferred tasks to flush (2):\n' +
' {id: 1, type: $evalAsync, time: 0}\n' +
' {id: 0, type: $timeout, time: 100}';
expect($timeout.verifyNoPendingTasks).toThrowError(expectedError);
}));
it('should do nothing when all tasks have been flushed', inject(function($timeout) {
$timeout(noop);
it('should recommend `$verifyNoPendingTasks()` when all pending tasks are not timeouts',
inject(function($rootScope, $timeout) {
var extraMessage = 'None of the pending tasks are timeouts. If you only want to verify ' +
'pending timeouts, use `$verifyNoPendingTasks(\'$timeout\')` instead.';
var errorMessage;
$timeout(noop, 100);
$rootScope.$evalAsync(noop);
try { $timeout.verifyNoPendingTasks(); } catch (err) { errorMessage = err.message; }
expect(errorMessage).not.toContain(extraMessage);
$timeout.flush(100);
$rootScope.$evalAsync(noop);
try { $timeout.verifyNoPendingTasks(); } catch (err) { errorMessage = err.message; }
expect(errorMessage).toContain(extraMessage);
})
);
it('should do nothing when all tasks have been flushed', inject(function($rootScope, $timeout) {
$timeout(noop, 100);
$rootScope.$evalAsync(noop);
$timeout.flush();
expect(function() {$timeout.verifyNoPendingTasks();}).not.toThrow();
expect($timeout.verifyNoPendingTasks).not.toThrow();
}));
it('should check against the delay if provided within timeout', inject(function($timeout) {
$timeout(noop, 100);
$timeout.flush(100);
expect(function() {$timeout.verifyNoPendingTasks();}).not.toThrow();
expect($timeout.verifyNoPendingTasks).not.toThrow();
$timeout(noop, 1000);
$timeout.flush(100);
expect(function() {$timeout.verifyNoPendingTasks();}).toThrow();
expect($timeout.verifyNoPendingTasks).toThrow();
$timeout.flush(900);
expect(function() {$timeout.verifyNoPendingTasks();}).not.toThrow();
expect($timeout.verifyNoPendingTasks).not.toThrow();
}));
@@ -763,6 +1035,7 @@ describe('ngMock', function() {
expect(count).toBe(2);
}));
it('should resolve timeout functions following the timeline', inject(function($timeout) {
var count1 = 0, count2 = 0;
var iterate1 = function() {
@@ -1056,7 +1329,7 @@ describe('ngMock', function() {
describe('$httpBackend', function() {
var hb, callback, realBackendSpy;
var hb, callback;
beforeEach(inject(function($httpBackend) {
callback = jasmine.createSpy('callback');
@@ -1233,6 +1506,42 @@ describe('ngMock', function() {
});
it('should throw error when expectation fails', function() {
expect(function() {
hb.expectPOST('/some', {foo: 1}).respond({});
hb('POST', '/some', {foo: 2}, callback);
hb.flush();
}).toThrowError(/^Expected POST \/some with different data/);
});
it('should throw error when expectation about headers fails', function() {
expect(function() {
hb.expectPOST('/some', {foo: 1}, {X: 'val1'}).respond({});
hb('POST', '/some', {foo: 1}, callback, {X: 'val2'});
hb.flush();
}).toThrowError(/^Expected POST \/some with different headers/);
});
it('should throw error about data when expectations about both data and headers fail', function() {
expect(function() {
hb.expectPOST('/some', {foo: 1}, {X: 'val1'}).respond({});
hb('POST', '/some', {foo: 2}, callback, {X: 'val2'});
hb.flush();
}).toThrowError(/^Expected POST \/some with different data/);
});
it('should throw error when response is not defined for a backend definition', function() {
expect(function() {
hb.whenGET('/some'); // no .respond(...) !
hb('GET', '/some', null, callback);
hb.flush();
}).toThrowError('No response defined !');
});
it('should match headers if specified', function() {
hb.when('GET', '/url', null, {'X': 'val1'}).respond(201, 'content1');
hb.when('GET', '/url', null, {'X': 'val2'}).respond(202, 'content2');
@@ -1941,12 +2250,36 @@ describe('ngMock', function() {
expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '', 'complete');
}
);
they('should ignore query param when matching in ' + routeShortcut + ' $prop method', methods,
function() {
hb[routeShortcut](this, '/route/:id').respond('path');
hb(this, '/route/123?q=str&foo=bar', undefined, callback);
hb.flush();
expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '', 'complete');
they('should ignore query params when matching in ' + routeShortcut + ' $prop method', methods,
function(method) {
angular.forEach([
{route: '/route1/:id', url: '/route1/Alpha', expectedParams: {id: 'Alpha'}},
{route: '/route2/:id', url: '/route2/Bravo/?', expectedParams: {id: 'Bravo'}},
{route: '/route3/:id', url: '/route3/Charlie?q=str&foo=bar', expectedParams: {id: 'Charlie', q: 'str', foo: 'bar'}},
{route: '/:x/route4', url: '/Delta/route4?q=str&foo=bar', expectedParams: {x: 'Delta', q: 'str', foo: 'bar'}},
{route: '/route5/:id*', url: '/route5/Echo/456?q=str&foo=bar', expectedParams: {id: 'Echo/456', q: 'str', foo: 'bar'}},
{route: '/route6/:id*', url: '/route6/Foxtrot/456/?q=str&foo=bar', expectedParams: {id: 'Foxtrot/456', q: 'str', foo: 'bar'}},
{route: '/route7/:id*', url: '/route7/Golf/456//?q=str&foo=bar', expectedParams: {id: 'Golf/456', q: 'str', foo: 'bar'}},
{route: '/:x*/route8', url: '/Hotel/123/456/route8/?q=str&foo=bar', expectedParams: {x: 'Hotel/123/456', q: 'str', foo: 'bar'}},
{route: '/:x*/route9/:id', url: '/India/456/route9/0?q=str&foo=bar', expectedParams: {x: 'India/456', id: '0', q: 'str', foo: 'bar'}},
{route: '/route10', url: '/route10?q=Juliet&foo=bar', expectedParams: {q: 'Juliet', foo: 'bar'}},
{route: '/route11', url: '/route11///?q=Kilo', expectedParams: {q: 'Kilo'}},
{route: '/route12', url: '/route12///', expectedParams: {}}
], function(testDataEntry) {
callback.calls.reset();
var paramsSpy = jasmine.createSpy('params');
hb[routeShortcut](method, testDataEntry.route).respond(
function(method, url, data, headers, params) {
paramsSpy(params);
// status, response, headers, statusText, xhrStatus
return [200, 'path', { 'x-header': 'foo' }, 'OK', 'complete'];
}
);
hb(method, testDataEntry.url, undefined, callback);
hb.flush();
expect(callback).toHaveBeenCalledOnceWith(200, 'path', 'x-header: foo', 'OK', 'complete');
expect(paramsSpy).toHaveBeenCalledOnceWith(testDataEntry.expectedParams);
});
}
);
});
@@ -2536,6 +2869,24 @@ describe('ngMockE2E', function() {
}).toThrowError('Unexpected request: GET /some\nNo more request expected');
});
it('should throw error when expectation fails - without error callback', function() {
expect(function() {
hb.expectPOST('/some', { foo: 1 }).respond({});
$http.post('/some', { foo: 2 }).then(noop);
hb.flush();
}).toThrowError(/^Expected POST \/some with different data/);
});
it('should throw error when unexpected request - with error callback', function() {
expect(function() {
hb.expectPOST('/some', { foo: 1 }).respond({});
$http.post('/some', { foo: 2 }).then(noop, noop);
hb.flush();
}).toThrowError(/^Expected POST \/some with different data/);
});
describe('passThrough()', function() {
it('should delegate requests to the real backend when passThrough is invoked', function() {
+40
View File
@@ -77,5 +77,45 @@ describe('$routeParams', function() {
});
});
it('should correctly extract path params containing hashes and/or question marks', function() {
module(function($routeProvider) {
$routeProvider.when('/foo/:bar', {});
$routeProvider.when('/zoo/:bar/:baz/:qux', {});
});
inject(function($location, $rootScope, $routeParams) {
$location.path('/foo/bar?baz');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar?baz'});
$location.path('/foo/bar?baz=val');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar?baz=val'});
$location.path('/foo/bar#baz');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar#baz'});
$location.path('/foo/bar?baz#qux');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar?baz#qux'});
$location.path('/foo/bar?baz=val#qux');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar?baz=val#qux'});
$location.path('/foo/bar#baz?qux');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar#baz?qux'});
$location.path('/zoo/bar?p1=v1#h1/baz?p2=v2#h2/qux?p3=v3#h3');
$rootScope.$digest();
expect($routeParams).toEqual({
bar: 'bar?p1=v1#h1',
baz: 'baz?p2=v2#h2',
qux: 'qux?p3=v3#h3'
});
});
});
});
+403 -92
View File
@@ -65,8 +65,8 @@ describe('$route', function() {
$httpBackend.when('GET', 'Chapter.html').respond('chapter');
$httpBackend.when('GET', 'test.html').respond('test');
$httpBackend.when('GET', 'foo.html').respond('foo');
$httpBackend.when('GET', 'baz.html').respond('baz');
$httpBackend.when('GET', 'bar.html').respond('bar');
$httpBackend.when('GET', 'baz.html').respond('baz');
$httpBackend.when('GET', 'http://example.com/trusted-template.html').respond('cross domain trusted template');
$httpBackend.when('GET', '404.html').respond('not found');
};
@@ -76,6 +76,7 @@ describe('$route', function() {
dealoc(element);
});
it('should allow cancellation via $locationChangeStart via $routeChangeStart', function() {
module(function($routeProvider) {
$routeProvider.when('/Edit', {
@@ -1677,95 +1678,413 @@ describe('$route', function() {
});
describe('reloadOnUrl', function() {
it('should reload when `reloadOnUrl` is true and `.url()` changes', function() {
var routeChange = jasmine.createSpy('routeChange');
module(function($routeProvider) {
$routeProvider.when('/path/:param', {});
});
inject(function($location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', routeChange);
// Initial load
$location.path('/path/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledOnce();
expect($routeParams).toEqual({param: 'foo'});
routeChange.calls.reset();
// Reload on `path` change
$location.path('/path/bar');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledOnce();
expect($routeParams).toEqual({param: 'bar'});
routeChange.calls.reset();
// Reload on `search` change
$location.search('foo', 'bar');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledOnce();
expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
routeChange.calls.reset();
// Reload on `hash` change
$location.hash('baz');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledOnce();
expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
});
});
it('should reload when `reloadOnUrl` is false and URL maps to different route',
function() {
var routeChange = jasmine.createSpy('routeChange');
var routeUpdate = jasmine.createSpy('routeUpdate');
module(function($routeProvider) {
$routeProvider.
when('/path/:param', {reloadOnUrl: false}).
otherwise({});
});
inject(function($location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
$rootScope.$on('$routeUpdate', routeUpdate);
expect(routeChange).not.toHaveBeenCalled();
// Initial load
$location.path('/path/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
expect(routeUpdate).not.toHaveBeenCalled();
expect($routeParams).toEqual({param: 'foo'});
routeChange.calls.reset();
// Route change
$location.path('/other/path/bar');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
expect(routeUpdate).not.toHaveBeenCalled();
expect($routeParams).toEqual({});
});
}
);
it('should not reload when `reloadOnUrl` is false and URL maps to the same route',
function() {
var routeChange = jasmine.createSpy('routeChange');
var routeUpdate = jasmine.createSpy('routeUpdate');
module(function($routeProvider) {
$routeProvider.when('/path/:param', {reloadOnUrl: false});
});
inject(function($location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
$rootScope.$on('$routeUpdate', routeUpdate);
expect(routeChange).not.toHaveBeenCalled();
// Initial load
$location.path('/path/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
expect(routeUpdate).not.toHaveBeenCalled();
expect($routeParams).toEqual({param: 'foo'});
routeChange.calls.reset();
// Route update (no reload)
$location.path('/path/bar').search('foo', 'bar').hash('baz');
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
expect(routeUpdate).toHaveBeenCalledOnce();
expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
});
}
);
it('should update `$routeParams` even when not reloading a route', function() {
var routeChange = jasmine.createSpy('routeChange');
module(function($routeProvider) {
$routeProvider.when('/path/:param', {reloadOnUrl: false});
});
inject(function($location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
expect(routeChange).not.toHaveBeenCalled();
// Initial load
$location.path('/path/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
expect($routeParams).toEqual({param: 'foo'});
routeChange.calls.reset();
// Route update (no reload)
$location.path('/path/bar');
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
expect($routeParams).toEqual({param: 'bar'});
});
});
describe('with `$route.reload()`', function() {
var $location;
var $log;
var $rootScope;
var $route;
var routeChangeStart;
var routeChangeSuccess;
beforeEach(module(function($routeProvider) {
$routeProvider.when('/path/:param', {
template: '',
reloadOnUrl: false,
controller: function Controller($log) {
$log.debug('initialized');
}
});
}));
beforeEach(inject(function($compile, _$location_, _$log_, _$rootScope_, _$route_) {
$location = _$location_;
$log = _$log_;
$rootScope = _$rootScope_;
$route = _$route_;
routeChangeStart = jasmine.createSpy('routeChangeStart');
routeChangeSuccess = jasmine.createSpy('routeChangeSuccess');
$rootScope.$on('$routeChangeStart', routeChangeStart);
$rootScope.$on('$routeChangeSuccess', routeChangeSuccess);
element = $compile('<div><ng-view></ng-view></div>')($rootScope);
}));
it('should reload the current route', function() {
$location.path('/path/foo');
$rootScope.$digest();
expect($location.path()).toBe('/path/foo');
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).toHaveBeenCalledOnce();
expect($log.debug.logs).toEqual([['initialized']]);
routeChangeStart.calls.reset();
routeChangeSuccess.calls.reset();
$log.reset();
$route.reload();
$rootScope.$digest();
expect($location.path()).toBe('/path/foo');
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).toHaveBeenCalledOnce();
expect($log.debug.logs).toEqual([['initialized']]);
$log.reset();
});
it('should support preventing a route reload', function() {
$location.path('/path/foo');
$rootScope.$digest();
expect($location.path()).toBe('/path/foo');
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).toHaveBeenCalledOnce();
expect($log.debug.logs).toEqual([['initialized']]);
routeChangeStart.calls.reset();
routeChangeSuccess.calls.reset();
$log.reset();
routeChangeStart.and.callFake(function(evt) { evt.preventDefault(); });
$route.reload();
$rootScope.$digest();
expect($location.path()).toBe('/path/foo');
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).not.toHaveBeenCalled();
expect($log.debug.logs).toEqual([]);
});
it('should reload the current route even if `reloadOnUrl` is disabled',
inject(function($routeParams) {
$location.path('/path/foo');
$rootScope.$digest();
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).toHaveBeenCalledOnce();
expect($log.debug.logs).toEqual([['initialized']]);
expect($routeParams).toEqual({param: 'foo'});
routeChangeStart.calls.reset();
routeChangeSuccess.calls.reset();
$log.reset();
$location.path('/path/bar');
$rootScope.$digest();
expect(routeChangeStart).not.toHaveBeenCalled();
expect(routeChangeSuccess).not.toHaveBeenCalled();
expect($log.debug.logs).toEqual([]);
expect($routeParams).toEqual({param: 'bar'});
$route.reload();
$rootScope.$digest();
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).toHaveBeenCalledOnce();
expect($log.debug.logs).toEqual([['initialized']]);
expect($routeParams).toEqual({param: 'bar'});
$log.reset();
})
);
});
});
describe('reloadOnSearch', function() {
it('should reload a route when reloadOnSearch is enabled and .search() changes', function() {
it('should not have any effect if `reloadOnUrl` is false', function() {
var reloaded = jasmine.createSpy('route reload');
module(function($routeProvider) {
$routeProvider.when('/foo', {controller: angular.noop});
$routeProvider.when('/foo', {
reloadOnUrl: false,
reloadOnSearch: true
});
});
inject(function($route, $location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', reloaded);
$location.path('/foo');
$rootScope.$digest();
expect(reloaded).toHaveBeenCalled();
expect(reloaded).toHaveBeenCalledOnce();
expect($routeParams).toEqual({});
reloaded.calls.reset();
// trigger reload
// trigger reload (via .search())
$location.search({foo: 'bar'});
$rootScope.$digest();
expect(reloaded).toHaveBeenCalled();
expect($routeParams).toEqual({foo:'bar'});
expect(reloaded).not.toHaveBeenCalled();
expect($routeParams).toEqual({foo: 'bar'});
// trigger reload (via .hash())
$location.hash('baz');
$rootScope.$digest();
expect(reloaded).not.toHaveBeenCalled();
expect($routeParams).toEqual({foo: 'bar'});
});
});
it('should not reload a route when reloadOnSearch is disabled and only .search() changes', function() {
var routeChange = jasmine.createSpy('route change'),
routeUpdate = jasmine.createSpy('route update');
it('should reload when `reloadOnSearch` is true and `.search()`/`.hash()` changes',
function() {
var reloaded = jasmine.createSpy('route reload');
module(function($routeProvider) {
$routeProvider.when('/foo', {controller: angular.noop, reloadOnSearch: false});
});
module(function($routeProvider) {
$routeProvider.when('/foo', {controller: angular.noop});
});
inject(function($route, $location, $rootScope) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
$rootScope.$on('$routeUpdate', routeUpdate);
inject(function($route, $location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', reloaded);
expect(routeChange).not.toHaveBeenCalled();
$location.path('/foo');
$rootScope.$digest();
expect(reloaded).toHaveBeenCalledOnce();
expect($routeParams).toEqual({});
$location.path('/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalled();
expect(routeChange).toHaveBeenCalledTimes(2);
expect(routeUpdate).not.toHaveBeenCalled();
routeChange.calls.reset();
reloaded.calls.reset();
// don't trigger reload
$location.search({foo: 'bar'});
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
expect(routeUpdate).toHaveBeenCalled();
});
});
// trigger reload (via .search())
$location.search({foo: 'bar'});
$rootScope.$digest();
expect(reloaded).toHaveBeenCalledOnce();
expect($routeParams).toEqual({foo: 'bar'});
reloaded.calls.reset();
// trigger reload (via .hash())
$location.hash('baz');
$rootScope.$digest();
expect(reloaded).toHaveBeenCalledOnce();
expect($routeParams).toEqual({foo: 'bar'});
});
}
);
it('should reload reloadOnSearch route when url differs only in route path param', function() {
var routeChange = jasmine.createSpy('route change');
it('should not reload when `reloadOnSearch` is false and `.search()`/`.hash()` changes',
function() {
var routeChange = jasmine.createSpy('route change'),
routeUpdate = jasmine.createSpy('route update');
module(function($routeProvider) {
$routeProvider.when('/foo/:fooId', {controller: angular.noop, reloadOnSearch: false});
});
module(function($routeProvider) {
$routeProvider.when('/foo', {controller: angular.noop, reloadOnSearch: false});
});
inject(function($route, $location, $rootScope) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
inject(function($route, $location, $rootScope) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
$rootScope.$on('$routeUpdate', routeUpdate);
expect(routeChange).not.toHaveBeenCalled();
expect(routeChange).not.toHaveBeenCalled();
$location.path('/foo/aaa');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalled();
expect(routeChange).toHaveBeenCalledTimes(2);
routeChange.calls.reset();
$location.path('/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
expect(routeUpdate).not.toHaveBeenCalled();
$location.path('/foo/bbb');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalled();
expect(routeChange).toHaveBeenCalledTimes(2);
routeChange.calls.reset();
routeChange.calls.reset();
$location.search({foo: 'bar'});
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
});
});
// don't trigger reload (via .search())
$location.search({foo: 'bar'});
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
expect(routeUpdate).toHaveBeenCalledOnce();
routeUpdate.calls.reset();
// don't trigger reload (via .hash())
$location.hash('baz');
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
expect(routeUpdate).toHaveBeenCalled();
});
}
);
it('should update params when reloadOnSearch is disabled and .search() changes', function() {
it('should reload when `reloadOnSearch` is false and url differs only in route path param',
function() {
var routeChange = jasmine.createSpy('route change');
module(function($routeProvider) {
$routeProvider.when('/foo/:fooId', {controller: angular.noop, reloadOnSearch: false});
});
inject(function($route, $location, $rootScope) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
expect(routeChange).not.toHaveBeenCalled();
$location.path('/foo/aaa');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
routeChange.calls.reset();
$location.path('/foo/bbb');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
routeChange.calls.reset();
$location.search({foo: 'bar'}).hash('baz');
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
});
}
);
it('should update params when `reloadOnSearch` is false and `.search()` changes', function() {
var routeParamsWatcher = jasmine.createSpy('routeParamsWatcher');
module(function($routeProvider) {
@@ -1852,7 +2171,8 @@ describe('$route', function() {
});
});
describe('reload', function() {
describe('with `$route.reload()`', function() {
var $location;
var $log;
var $rootScope;
@@ -1886,6 +2206,7 @@ describe('$route', function() {
element = $compile('<div><div ng-view></div></div>')($rootScope);
}));
it('should reload the current route', function() {
$location.path('/bar/123');
$rootScope.$digest();
@@ -1908,6 +2229,7 @@ describe('$route', function() {
$log.reset();
});
it('should support preventing a route reload', function() {
$location.path('/bar/123');
$rootScope.$digest();
@@ -1930,6 +2252,7 @@ describe('$route', function() {
expect($log.debug.logs).toEqual([]);
});
it('should reload even if reloadOnSearch is false', inject(function($routeParams) {
$location.path('/bar/123');
$rootScope.$digest();
@@ -1946,6 +2269,15 @@ describe('$route', function() {
expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
expect($log.debug.logs).toEqual([]);
routeChangeSuccessSpy.calls.reset();
$log.reset();
$location.hash('c');
$rootScope.$digest();
expect($routeParams).toEqual({barId: '123', a: 'b'});
expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
expect($log.debug.logs).toEqual([]);
$route.reload();
$rootScope.$digest();
expect($routeParams).toEqual({barId: '123', a: 'b'});
@@ -2087,9 +2419,8 @@ describe('$route', function() {
it('should wait for $resolve promises before calling callbacks', function() {
var deferred;
module(function($provide, $routeProvider) {
module(function($routeProvider) {
$routeProvider.when('/path', {
template: '',
resolve: {
a: function($q) {
deferred = $q.defer();
@@ -2099,7 +2430,7 @@ describe('$route', function() {
});
});
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
inject(function($browser, $location, $rootScope, $$testability) {
$location.path('/path');
$rootScope.$digest();
@@ -2108,7 +2439,7 @@ describe('$route', function() {
expect(callback).not.toHaveBeenCalled();
deferred.resolve();
$rootScope.$digest();
$browser.defer.flush();
expect(callback).toHaveBeenCalled();
});
});
@@ -2116,9 +2447,8 @@ describe('$route', function() {
it('should call callback after $resolve promises are rejected', function() {
var deferred;
module(function($provide, $routeProvider) {
module(function($routeProvider) {
$routeProvider.when('/path', {
template: '',
resolve: {
a: function($q) {
deferred = $q.defer();
@@ -2128,7 +2458,7 @@ describe('$route', function() {
});
});
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
inject(function($browser, $location, $rootScope, $$testability) {
$location.path('/path');
$rootScope.$digest();
@@ -2137,7 +2467,7 @@ describe('$route', function() {
expect(callback).not.toHaveBeenCalled();
deferred.reject();
$rootScope.$digest();
$browser.defer.flush();
expect(callback).toHaveBeenCalled();
});
});
@@ -2145,7 +2475,7 @@ describe('$route', function() {
it('should wait for resolveRedirectTo promises before calling callbacks', function() {
var deferred;
module(function($provide, $routeProvider) {
module(function($routeProvider) {
$routeProvider.when('/path', {
resolveRedirectTo: function($q) {
deferred = $q.defer();
@@ -2154,7 +2484,7 @@ describe('$route', function() {
});
});
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
inject(function($browser, $location, $rootScope, $$testability) {
$location.path('/path');
$rootScope.$digest();
@@ -2163,7 +2493,7 @@ describe('$route', function() {
expect(callback).not.toHaveBeenCalled();
deferred.resolve();
$rootScope.$digest();
$browser.defer.flush();
expect(callback).toHaveBeenCalled();
});
});
@@ -2171,7 +2501,7 @@ describe('$route', function() {
it('should call callback after resolveRedirectTo promises are rejected', function() {
var deferred;
module(function($provide, $routeProvider) {
module(function($routeProvider) {
$routeProvider.when('/path', {
resolveRedirectTo: function($q) {
deferred = $q.defer();
@@ -2180,7 +2510,7 @@ describe('$route', function() {
});
});
inject(function($location, $route, $rootScope, $httpBackend, $$testability) {
inject(function($browser, $location, $rootScope, $$testability) {
$location.path('/path');
$rootScope.$digest();
@@ -2189,7 +2519,7 @@ describe('$route', function() {
expect(callback).not.toHaveBeenCalled();
deferred.reject();
$rootScope.$digest();
$browser.defer.flush();
expect(callback).toHaveBeenCalled();
});
});
@@ -2197,30 +2527,11 @@ describe('$route', function() {
it('should wait for all route promises before calling callbacks', function() {
var deferreds = {};
module(function($provide, $routeProvider) {
// While normally `$browser.defer()` modifies the `outstandingRequestCount`, the mocked
// version (provided by `ngMock`) does not. This doesn't matter in most tests, but in this
// case we need the `outstandingRequestCount` logic to ensure that we don't call the
// `$$testability.whenStable()` callbacks part way through a `$rootScope.$evalAsync` block.
// See ngRoute's commitRoute()'s finally() block for details.
$provide.decorator('$browser', function($delegate) {
var oldDefer = $delegate.defer;
var newDefer = function(fn, delay) {
var requestCountAwareFn = function() { $delegate.$$completeOutstandingRequest(fn); };
$delegate.$$incOutstandingRequestCount();
return oldDefer.call($delegate, requestCountAwareFn, delay);
};
$delegate.defer = angular.extend(newDefer, oldDefer);
return $delegate;
});
module(function($routeProvider) {
addRouteWithAsyncRedirect('/foo', '/bar');
addRouteWithAsyncRedirect('/bar', '/baz');
addRouteWithAsyncRedirect('/baz', '/qux');
$routeProvider.when('/qux', {
template: '',
resolve: {
a: function($q) {
var deferred = deferreds['/qux'] = $q.defer();
@@ -2240,7 +2551,7 @@ describe('$route', function() {
}
});
inject(function($browser, $location, $rootScope, $route, $$testability) {
inject(function($browser, $location, $rootScope, $$testability) {
$location.path('/foo');
$rootScope.$digest();

Some files were not shown because too many files have changed in this diff Show More