Compare commits

..

43 Commits

Author SHA1 Message Date
4e047302f2 chore(release): cut the 2.3.0-beta.0 realse and add change log 2016-11-17 11:59:03 -08:00
419a812f04 chore(release): cut angular 2.2.1 2016-11-17 11:51:25 -08:00
f340e1a414 fix(tools): fix error when running test.sh (#12927) 2016-11-16 13:35:31 -08:00
481c9b3258 refactor(compiler): allows synchronous retrieving of metadata (#12908)
Allows non-normalized metadata to be retrieved synchronously.

Related to #7482
2016-11-16 10:22:11 -08:00
8b2dfb2eca fix(core): support ngTemplateOutlet in production mode (#12921)
Fixes #12911
2016-11-16 10:00:18 -08:00
824ea8406c docs(upgrade/static): improve API docs with examples
Closes #12717
2016-11-16 09:18:17 -08:00
1f96a93f59 chore(public_api): remove Angular 1 types from upgrade/static API 2016-11-16 09:18:10 -08:00
009d545787 chore(examples): add upgrade/static example 2016-11-16 09:18:10 -08:00
53c25210a6 chore(examples): support upgrade/static examples 2016-11-16 09:18:10 -08:00
927aa69726 fix(router): add a banner file for the router (#12919) 2016-11-16 09:17:19 -08:00
ce89039036 fix(platform_browser): fix disableDebugTools() (#12918) 2016-11-16 09:16:40 -08:00
42198cd7d5 fix(ngUpgrade): make AoT ngUpgrade work with the testability API and resumeBootstrap() (#12910) 2016-11-16 01:04:56 -08:00
d6ba092a27 build(build.sh): echo before building examples 2016-11-15 20:59:37 -08:00
773b31de8f fix(router): should not create a route state if navigation is canceled (#12868)
Closes #12776
2016-11-15 19:00:20 -08:00
f79b320fc4 refactor(forms): remove facade (#12558) 2016-11-15 18:48:34 -08:00
6a212fd561 fix(router): removes a peer dependency from router to upgrade 2016-11-15 18:37:08 -08:00
be010a292a fix(animations): only pass in same typed players as previous players into web-animations (#12907)
Closes #12907
2016-11-15 17:47:21 -08:00
7c36e7f956 chore(router): remove @angular/upgrade peer dep (#12896) 2016-11-15 14:00:11 -08:00
13ba2f90b9 refactor(http): remove all facade methods from http module (#12870) 2016-11-15 09:19:14 -08:00
75277cd94b fix(tsickle): support ctorParams in function closure (#12876)
See https://github.com/angular/tsickle/issues/261 for context.
2016-11-15 09:19:00 -08:00
46d150266b feat(router_link): add skipLocationChange and replaceUrl inputs (#12850) 2016-11-14 18:30:13 -08:00
1b5384ee54 feat(core): expose ViewRef as ChangeDetectorRef
closes #12722

This is helpful when manually dirty checking embedded views.
2016-11-14 17:01:41 -08:00
9f7d32a326 feat(core): add attachView / detachView to ApplicationRef
This feature is useful to allow components / embedded views
to be dirty checked if they are not placed in any `ViewContainer`.

Closes #9293
2016-11-14 17:01:35 -08:00
9de76ebfa5 fix(animations): retain styling when transition destinations are changed (#12208)
Closes #9661
Closes #12208
2016-11-14 16:59:06 -08:00
46023e4792 fix(select): allow for null values in HTML select options bound with ngValue
closes #12829
2016-11-14 16:47:14 -08:00
b55aaf094f fix: allow for null values in HTML select options bound with ngValue
This corrects the case of <option [ngValue]="null"> binding a string like "{0: null}" to the model instead of an actual null object.

Closes #10349
2016-11-14 16:47:09 -08:00
d90b622fa4 fix: allow for null values in HTML select options bound with ngValue
This corrects the case of <option [ngValue]="null"> binding a string like "{0: null}" to the model instead of an actual null object.

Closes #10349
2016-11-14 16:47:09 -08:00
79e2bb9291 refactor(core): remove dead code (#12871) 2016-11-14 16:44:25 -08:00
efbbefd353 fix(platform-browser): enable AOT
closes #12783
2016-11-14 12:57:11 -08:00
c2fae72bc6 feat(router): register router with ngprobe 2016-11-14 12:57:05 -08:00
7908679c4b fix(compiler): assert xliff messages have translations
fixes #12815
closes #12604
2016-11-14 12:55:56 -08:00
9ed9ff40b3 test(compiler): improve xliff tests 2016-11-14 12:55:48 -08:00
2f14415836 fix(compiler): updates hash algo for xmb/xtb files 2016-11-14 12:55:48 -08:00
76e4911e8b fix(core): fix placeholders handling in i18n.
Prior to this commit, translations were built in the serializers. This
could not work as a single translation can be used for different source
messages having different placeholder content.

Serializers do not try to replace the placeholders any more.
Placeholders are replaced by the translation bundle and the source
message is given as parameter so that the content of the placeholders is
taken into account.

Also XMB ids are now independent of the expression which is replaced by
a placeholder in the extracted file.
fixes #12512
2016-11-14 12:55:48 -08:00
ed5e98d0df fix(core): misc i18n fixes 2016-11-14 12:55:48 -08:00
146af1fed9 refactor(core): simplify i18n serializers code 2016-11-14 12:55:48 -08:00
c60ba7a72f refactor(core): remove ListWrapper from i18n 2016-11-14 12:55:48 -08:00
05beffe0d0 test(core): fix a typo in the i18n integration spec 2016-11-14 12:55:48 -08:00
08c038ebd9 fix(core): xmb serializer uses decimal messaged IDs
fixes #12511
2016-11-14 12:55:48 -08:00
582550a90d feat(core): implements a decimal fingerprint for i18n 2016-11-14 12:55:48 -08:00
1d53a870dd fix(http): return request url if it cannot be retrieved from response
closes #12837
2016-11-14 12:54:43 -08:00
a0c58a6b5c fix(http): correctly handle response body for 204 status code
closes #12830
fixes #12393
2016-11-14 12:36:22 -08:00
d3eff6c483 refactor(xhr_backend): remove facade 2016-11-14 12:36:16 -08:00
116 changed files with 3043 additions and 1226 deletions

View File

@ -1,3 +1,46 @@
<a name="2.3.0-beta.0"></a>
# [2.3.0-beta.0](https://github.com/angular/angular/compare/2.2.0...2.3.0-beta.0) (2016-11-17)
### Bug Fixes
* **compiler:** assert xliff messages have translations ([7908679](https://github.com/angular/angular/commit/7908679)), closes [#12815](https://github.com/angular/angular/issues/12815) [#12604](https://github.com/angular/angular/issues/12604)
* **compiler:** updates hash algo for xmb/xtb files ([2f14415](https://github.com/angular/angular/commit/2f14415))
* **core:** fix placeholders handling in i18n. ([76e4911](https://github.com/angular/angular/commit/76e4911)), closes [#12512](https://github.com/angular/angular/issues/12512)
* **core:** misc i18n fixes ([ed5e98d](https://github.com/angular/angular/commit/ed5e98d))
* **core:** xmb serializer uses decimal messaged IDs ([08c038e](https://github.com/angular/angular/commit/08c038e)), closes [#12511](https://github.com/angular/angular/issues/12511)
* **platform-browser:** enable AOT ([efbbefd](https://github.com/angular/angular/commit/efbbefd)), closes [#12783](https://github.com/angular/angular/issues/12783)
### Features
* **core:** add `attachView` / `detachView` to ApplicationRef ([9f7d32a](https://github.com/angular/angular/commit/9f7d32a)), closes [#9293](https://github.com/angular/angular/issues/9293)
* **core:** expose `ViewRef` as `ChangeDetectorRef` ([1b5384e](https://github.com/angular/angular/commit/1b5384e)), closes [#12722](https://github.com/angular/angular/issues/12722)
* **core:** implements a decimal fingerprint for i18n ([582550a](https://github.com/angular/angular/commit/582550a))
* **router:** register router with ngprobe ([c2fae72](https://github.com/angular/angular/commit/c2fae72))
* **router_link:** add skipLocationChange and replaceUrl inputs ([#12850](https://github.com/angular/angular/issues/12850)) ([46d1502](https://github.com/angular/angular/commit/46d1502))
Note: The 2.3.0-beta.1 release also contains all the changes present in the 2.2.1 release.
<a name="2.2.1"></a>
## [2.2.1](https://github.com/angular/angular/compare/2.2.0...2.2.1) (2016-11-17)
### Bug Fixes
* **animations:** only pass in same typed players as previous players into web-animations ([#12907](https://github.com/angular/angular/issues/12907)) ([583d283](https://github.com/angular/angular/commit/583d283))
* **animations:** retain styling when transition destinations are changed ([#12208](https://github.com/angular/angular/issues/12208)) ([5c46c49](https://github.com/angular/angular/commit/5c46c49)), closes [#9661](https://github.com/angular/angular/issues/9661)
* **core:** support `ngTemplateOutlet` in production mode ([#12921](https://github.com/angular/angular/issues/12921)) ([4628798](https://github.com/angular/angular/commit/4628798)), closes [#12911](https://github.com/angular/angular/issues/12911)
* **http:** correctly handle response body for 204 status code ([21a4de9](https://github.com/angular/angular/commit/21a4de9)), closes [#12830](https://github.com/angular/angular/issues/12830) [#12393](https://github.com/angular/angular/issues/12393)
* **http:** return request url if it cannot be retrieved from response ([845ea23](https://github.com/angular/angular/commit/845ea23)), closes [#12837](https://github.com/angular/angular/issues/12837)
* **upgrade:** make AoT ngUpgrade work with the testability API and resumeBootstrap() ([#12910](https://github.com/angular/angular/issues/12910)) ([dc1662a](https://github.com/angular/angular/commit/dc1662a))
* **platform-browser:** fix disableDebugTools() ([#12918](https://github.com/angular/angular/issues/12918)) ([7b67bad](https://github.com/angular/angular/commit/7b67bad))
* **router:** add a banner file for the router ([#12919](https://github.com/angular/angular/issues/12919)) ([364642d](https://github.com/angular/angular/commit/364642d))
* **router:** removes a peer dependency from router to upgrade ([1dcf1f4](https://github.com/angular/angular/commit/1dcf1f4))
* **forms** allow for null values in HTML select options bound with ngValue ([e0ce545](https://github.com/angular/angular/commit/e0ce545)), closes [#10349](https://github.com/angular/angular/issues/10349)
* **router:** should not create a route state if navigation is canceled ([#12868](https://github.com/angular/angular/issues/12868)) ([dabaf85](https://github.com/angular/angular/commit/dabaf85)), closes [#12776](https://github.com/angular/angular/issues/12776)
* **common:** select should allow for null values in HTML select options bound with ngValue ([e02c180](https://github.com/angular/angular/commit/e02c180)), closes [#12829](https://github.com/angular/angular/issues/12829)
* **compiler-cli:** support ctorParams in function closure ([#12876](https://github.com/angular/angular/issues/12876)) ([6cdc3b5](https://github.com/angular/angular/commit/6cdc3b5))
<a name="2.2.0"></a> <a name="2.2.0"></a>
# [2.2.0 upgrade-firebooster](https://github.com/angular/angular/compare/2.2.0-rc.0...2.2.0) (2016-11-14) # [2.2.0 upgrade-firebooster](https://github.com/angular/angular/compare/2.2.0-rc.0...2.2.0) (2016-11-14)

View File

@ -106,7 +106,13 @@ do
UMD_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}.umd.min.js UMD_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}.umd.min.js
UMD_STATIC_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}-static.umd.min.js UMD_STATIC_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}-static.umd.min.js
UMD_UPGRADE_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}-upgrade.umd.min.js UMD_UPGRADE_ES5_MIN_PATH=${DESTDIR}/bundles/${PACKAGE}-upgrade.umd.min.js
LICENSE_BANNER=${PWD}/modules/@angular/license-banner.txt
if [[ ${PACKAGE} != router ]]; then
LICENSE_BANNER=${PWD}/modules/@angular/license-banner.txt
fi
if [[ ${PACKAGE} == router ]]; then
LICENSE_BANNER=${PWD}/modules/@angular/router-license-banner.txt
fi
rm -rf ${DESTDIR} rm -rf ${DESTDIR}
@ -195,4 +201,5 @@ do
fi fi
done done
echo "====== Building examples: ./modules/@angular/examples/build.sh ====="
./modules/@angular/examples/build.sh ./modules/@angular/examples/build.sh

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE translationbundle [<!ELEMENT translationbundle (translation)*>
<!ATTLIST translationbundle lang CDATA #REQUIRED>
<!ELEMENT translation (#PCDATA|ph)*>
<!ATTLIST translation id CDATA #REQUIRED>
<!ELEMENT ph EMPTY>
<!ATTLIST ph name CDATA #REQUIRED>
]>
<translationbundle>
<translation id="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4">käännä teksti</translation>
<translation id="65cc4ab3b4c438e07c89be2b677d08369fb62da2">tervetuloa</translation>
<translation id="63a85808f03b8181e36a952e0fa38202c2304862">other-3rdP-component</translation>
</translationbundle>

View File

@ -34,9 +34,9 @@ const EXPECTED_XMB = `<?xml version="1.0" encoding="UTF-8" ?>
<!ELEMENT ex (#PCDATA)> <!ELEMENT ex (#PCDATA)>
]> ]>
<messagebundle> <messagebundle>
<msg id="63a85808f03b8181e36a952e0fa38202c2304862">other-3rdP-component</msg> <msg id="3772663375917578720">other-3rdP-component</msg>
<msg id="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4" desc="desc" meaning="meaning">translate me</msg> <msg id="8136548302122759730" desc="desc" meaning="meaning">translate me</msg>
<msg id="65cc4ab3b4c438e07c89be2b677d08369fb62da2">Welcome</msg> <msg id="3492007542396725315">Welcome</msg>
</messagebundle> </messagebundle>
`; `;
@ -79,5 +79,4 @@ describe('template i18n extraction output', () => {
const xlf = fs.readFileSync(xlfOutput, {encoding: 'utf-8'}); const xlf = fs.readFileSync(xlfOutput, {encoding: 'utf-8'});
expect(xlf).toEqual(EXPECTED_XLIFF); expect(xlf).toEqual(EXPECTED_XLIFF);
}); });
}); });

View File

@ -51,9 +51,8 @@ function extract(
case 'xliff': case 'xliff':
case 'xlf': case 'xlf':
default: default:
const htmlParser = new compiler.I18NHtmlParser(new compiler.HtmlParser());
ext = 'xlf'; ext = 'xlf';
serializer = new compiler.Xliff(htmlParser, compiler.DEFAULT_INTERPOLATION_CONFIG); serializer = new compiler.Xliff();
break; break;
} }

View File

@ -34,34 +34,34 @@ export class Extractor {
const programSymbols: StaticSymbol[] = const programSymbols: StaticSymbol[] =
extractProgramSymbols(this.program, this.staticReflector, this.reflectorHost, this.options); extractProgramSymbols(this.program, this.staticReflector, this.reflectorHost, this.options);
return compiler const {ngModules, files} = compiler.analyzeAndValidateNgModules(
.analyzeNgModules(programSymbols, {transitiveModules: true}, this.metadataResolver) programSymbols, {transitiveModules: true}, this.metadataResolver);
.then(({files}) => { return compiler.loadNgModuleDirectives(ngModules).then(() => {
const errors: compiler.ParseError[] = []; const errors: compiler.ParseError[] = [];
files.forEach(file => { files.forEach(file => {
const compMetas: compiler.CompileDirectiveMetadata[] = []; const compMetas: compiler.CompileDirectiveMetadata[] = [];
file.directives.forEach(directiveType => { file.directives.forEach(directiveType => {
const dirMeta = this.metadataResolver.getDirectiveMetadata(directiveType); const dirMeta = this.metadataResolver.getDirectiveMetadata(directiveType);
if (dirMeta && dirMeta.isComponent) { if (dirMeta && dirMeta.isComponent) {
compMetas.push(dirMeta); compMetas.push(dirMeta);
}
});
compMetas.forEach(compMeta => {
const html = compMeta.template.template;
const interpolationConfig =
compiler.InterpolationConfig.fromArray(compMeta.template.interpolation);
errors.push(
...this.messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig));
});
});
if (errors.length) {
throw new Error(errors.map(e => e.toString()).join('\n'));
} }
return this.messageBundle;
}); });
compMetas.forEach(compMeta => {
const html = compMeta.template.template;
const interpolationConfig =
compiler.InterpolationConfig.fromArray(compMeta.template.interpolation);
errors.push(
...this.messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig));
});
});
if (errors.length) {
throw new Error(errors.map(e => e.toString()).join('\n'));
}
return this.messageBundle;
});
} }
static create( static create(

View File

@ -132,7 +132,6 @@ export class StaticReflector implements ReflectorReader {
const ctor = (<any[]>ctorData).find(a => a['__symbolic'] == 'constructor'); const ctor = (<any[]>ctorData).find(a => a['__symbolic'] == 'constructor');
const parameterTypes = <any[]>this.simplify(type, ctor['parameters'] || []); const parameterTypes = <any[]>this.simplify(type, ctor['parameters'] || []);
const parameterDecorators = <any[]>this.simplify(type, ctor['parameterDecorators'] || []); const parameterDecorators = <any[]>this.simplify(type, ctor['parameterDecorators'] || []);
parameters = []; parameters = [];
parameterTypes.forEach((paramType, index) => { parameterTypes.forEach((paramType, index) => {
const nestedResult: any[] = []; const nestedResult: any[] = [];

View File

@ -7,7 +7,7 @@
*/ */
import {StaticReflector, StaticReflectorHost, StaticSymbol} from '@angular/compiler-cli/src/static_reflector'; import {StaticReflector, StaticReflectorHost, StaticSymbol} from '@angular/compiler-cli/src/static_reflector';
import {HostListener, animate, group, keyframes, sequence, state, style, transition, trigger} from '@angular/core'; import {HostListener, Inject, animate, group, keyframes, sequence, state, style, transition, trigger} from '@angular/core';
import {MetadataCollector} from '@angular/tsc-wrapped'; import {MetadataCollector} from '@angular/tsc-wrapped';
import * as ts from 'typescript'; import * as ts from 'typescript';
@ -410,6 +410,13 @@ describe('StaticReflector', () => {
expect(props).toEqual({foo: []}); expect(props).toEqual({foo: []});
}); });
it('should read ctor parameters with forwardRef', () => {
const src = '/tmp/src/forward-ref.ts';
const dep = host.getStaticSymbol(src, 'Dep');
const props = reflector.parameters(host.getStaticSymbol(src, 'Forward'));
expect(props).toEqual([[dep, new Inject(dep)]]);
});
it('should report an error for invalid function calls', () => { it('should report an error for invalid function calls', () => {
expect( expect(
() => () =>
@ -1068,6 +1075,18 @@ class MockReflectorHost implements StaticReflectorHost {
providers: [ { provider: 'a', useValue: (() => 1)() }] providers: [ { provider: 'a', useValue: (() => 1)() }]
}) })
export class InvalidMetadata {} export class InvalidMetadata {}
`,
'/tmp/src/forward-ref.ts': `
import {forwardRef} from 'angular2/core';
import {Component} from 'angular2/src/core/metadata';
import {Inject} from 'angular2/src/core/di/metadata';
@Component({})
export class Forward {
constructor(@Inject(forwardRef(() => Dep)) d: Dep) {}
}
export class Dep {
@Input f: Forward;
}
` `
}; };

View File

@ -41,7 +41,9 @@ const _ANIMATION_TIME_VAR = o.variable('totalTime');
const _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles'); const _ANIMATION_START_STATE_STYLES_VAR = o.variable('startStateStyles');
const _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles'); const _ANIMATION_END_STATE_STYLES_VAR = o.variable('endStateStyles');
const _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles'); const _ANIMATION_COLLECTED_STYLES = o.variable('collectedStyles');
const EMPTY_MAP = o.literalMap([]); const _PREVIOUS_ANIMATION_PLAYERS = o.variable('previousPlayers');
const _EMPTY_MAP = o.literalMap([]);
const _EMPTY_ARRAY = o.literalArr([]);
class _AnimationBuilder implements AnimationAstVisitor { class _AnimationBuilder implements AnimationAstVisitor {
private _fnVarName: string; private _fnVarName: string;
@ -110,10 +112,15 @@ class _AnimationBuilder implements AnimationAstVisitor {
_callAnimateMethod( _callAnimateMethod(
ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any, ast: AnimationStepAst, startingStylesExpr: any, keyframesExpr: any,
context: _AnimationBuilderContext) { context: _AnimationBuilderContext) {
let previousStylesValue: o.Expression = _EMPTY_ARRAY;
if (context.isExpectingFirstAnimateStep) {
previousStylesValue = _PREVIOUS_ANIMATION_PLAYERS;
context.isExpectingFirstAnimateStep = false;
}
context.totalTransitionTime += ast.duration + ast.delay; context.totalTransitionTime += ast.duration + ast.delay;
return _ANIMATION_FACTORY_RENDERER_VAR.callMethod('animate', [ return _ANIMATION_FACTORY_RENDERER_VAR.callMethod('animate', [
_ANIMATION_FACTORY_ELEMENT_VAR, startingStylesExpr, keyframesExpr, o.literal(ast.duration), _ANIMATION_FACTORY_ELEMENT_VAR, startingStylesExpr, keyframesExpr, o.literal(ast.duration),
o.literal(ast.delay), o.literal(ast.easing) o.literal(ast.delay), o.literal(ast.easing), previousStylesValue
]); ]);
} }
@ -150,6 +157,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
context.totalTransitionTime = 0; context.totalTransitionTime = 0;
context.isExpectingFirstStyleStep = true; context.isExpectingFirstStyleStep = true;
context.isExpectingFirstAnimateStep = true;
const stateChangePreconditions: o.Expression[] = []; const stateChangePreconditions: o.Expression[] = [];
@ -187,17 +195,16 @@ class _AnimationBuilder implements AnimationAstVisitor {
context.stateMap.registerState(DEFAULT_STATE, {}); context.stateMap.registerState(DEFAULT_STATE, {});
const statements: o.Statement[] = []; const statements: o.Statement[] = [];
statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT statements.push(_PREVIOUS_ANIMATION_PLAYERS
.callMethod( .set(_ANIMATION_FACTORY_VIEW_CONTEXT.callMethod(
'cancelActiveAnimation', 'getAnimationPlayers',
[ [
_ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName), _ANIMATION_FACTORY_ELEMENT_VAR, o.literal(this.animationName),
_ANIMATION_NEXT_STATE_VAR.equals(o.literal(EMPTY_STATE)) _ANIMATION_NEXT_STATE_VAR.equals(o.literal(EMPTY_STATE))
]) ]))
.toStmt()); .toDeclStmt());
statements.push(_ANIMATION_COLLECTED_STYLES.set(_EMPTY_MAP).toDeclStmt());
statements.push(_ANIMATION_COLLECTED_STYLES.set(EMPTY_MAP).toDeclStmt());
statements.push(_ANIMATION_PLAYER_VAR.set(o.NULL_EXPR).toDeclStmt()); statements.push(_ANIMATION_PLAYER_VAR.set(o.NULL_EXPR).toDeclStmt());
statements.push(_ANIMATION_TIME_VAR.set(o.literal(0)).toDeclStmt()); statements.push(_ANIMATION_TIME_VAR.set(o.literal(0)).toDeclStmt());
@ -223,17 +230,6 @@ class _AnimationBuilder implements AnimationAstVisitor {
const RENDER_STYLES_FN = o.importExpr(resolveIdentifier(Identifiers.renderStyles)); const RENDER_STYLES_FN = o.importExpr(resolveIdentifier(Identifiers.renderStyles));
// before we start any animation we want to clear out the starting
// styles from the element's style property (since they were placed
// there at the end of the last animation
statements.push(RENDER_STYLES_FN
.callFn([
_ANIMATION_FACTORY_ELEMENT_VAR, _ANIMATION_FACTORY_RENDERER_VAR,
o.importExpr(resolveIdentifier(Identifiers.clearStyles))
.callFn([_ANIMATION_START_STATE_STYLES_VAR])
])
.toStmt());
ast.stateTransitions.forEach(transAst => statements.push(transAst.visit(this, context))); ast.stateTransitions.forEach(transAst => statements.push(transAst.visit(this, context)));
// this check ensures that the animation factory always returns a player // this check ensures that the animation factory always returns a player
@ -269,6 +265,22 @@ class _AnimationBuilder implements AnimationAstVisitor {
])]) ])])
.toStmt()); .toStmt());
statements.push(o.importExpr(resolveIdentifier(Identifiers.AnimationSequencePlayer))
.instantiate([_PREVIOUS_ANIMATION_PLAYERS])
.callMethod('destroy', [])
.toStmt());
// before we start any animation we want to clear out the starting
// styles from the element's style property (since they were placed
// there at the end of the last animation
statements.push(RENDER_STYLES_FN
.callFn([
_ANIMATION_FACTORY_ELEMENT_VAR, _ANIMATION_FACTORY_RENDERER_VAR,
o.importExpr(resolveIdentifier(Identifiers.clearStyles))
.callFn([_ANIMATION_START_STATE_STYLES_VAR])
])
.toStmt());
statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT statements.push(_ANIMATION_FACTORY_VIEW_CONTEXT
.callMethod( .callMethod(
'queueAnimation', 'queueAnimation',
@ -304,7 +316,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
const lookupMap: any[] = []; const lookupMap: any[] = [];
Object.keys(context.stateMap.states).forEach(stateName => { Object.keys(context.stateMap.states).forEach(stateName => {
const value = context.stateMap.states[stateName]; const value = context.stateMap.states[stateName];
let variableValue = EMPTY_MAP; let variableValue = _EMPTY_MAP;
if (isPresent(value)) { if (isPresent(value)) {
const styleMap: any[] = []; const styleMap: any[] = [];
Object.keys(value).forEach(key => { styleMap.push([key, o.literal(value[key])]); }); Object.keys(value).forEach(key => { styleMap.push([key, o.literal(value[key])]); });
@ -324,6 +336,7 @@ class _AnimationBuilderContext {
stateMap = new _AnimationBuilderStateMap(); stateMap = new _AnimationBuilderStateMap();
endStateAnimateStep: AnimationStepAst = null; endStateAnimateStep: AnimationStepAst = null;
isExpectingFirstStyleStep = false; isExpectingFirstStyleStep = false;
isExpectingFirstAnimateStep = false;
totalTransitionTime = 0; totalTransitionTime = 0;
} }

View File

@ -589,7 +589,7 @@ export interface CompileNgModuleDirectiveSummary extends CompileSummary {
exportedDirectives: CompileIdentifierMetadata[]; exportedDirectives: CompileIdentifierMetadata[];
exportedPipes: CompileIdentifierMetadata[]; exportedPipes: CompileIdentifierMetadata[];
exportedModules: CompileNgModuleDirectiveSummary[]; exportedModules: CompileNgModuleDirectiveSummary[];
loadingPromises: Promise<any>[]; directiveLoaders: (() => Promise<void>)[];
} }
export type CompileNgModuleSummary = export type CompileNgModuleSummary =
@ -661,7 +661,7 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
exportedModules: this.exportedModules, exportedModules: this.exportedModules,
exportedDirectives: this.exportedDirectives, exportedDirectives: this.exportedDirectives,
exportedPipes: this.exportedPipes, exportedPipes: this.exportedPipes,
loadingPromises: this.transitiveModule.loadingPromises directiveLoaders: this.transitiveModule.directiveLoaders
}; };
} }
@ -682,7 +682,7 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
exportedDirectives: this.exportedDirectives, exportedDirectives: this.exportedDirectives,
exportedPipes: this.exportedPipes, exportedPipes: this.exportedPipes,
exportedModules: this.exportedModules, exportedModules: this.exportedModules,
loadingPromises: this.transitiveModule.loadingPromises directiveLoaders: this.transitiveModule.directiveLoaders
}; };
} }
} }
@ -695,7 +695,7 @@ export class TransitiveCompileNgModuleMetadata {
public modules: CompileNgModuleInjectorSummary[], public providers: CompileProviderMetadata[], public modules: CompileNgModuleInjectorSummary[], public providers: CompileProviderMetadata[],
public entryComponents: CompileIdentifierMetadata[], public entryComponents: CompileIdentifierMetadata[],
public directives: CompileIdentifierMetadata[], public pipes: CompileIdentifierMetadata[], public directives: CompileIdentifierMetadata[], public pipes: CompileIdentifierMetadata[],
public loadingPromises: Promise<any>[]) { public directiveLoaders: (() => Promise<void>)[]) {
directives.forEach(dir => this.directivesSet.add(dir.reference)); directives.forEach(dir => this.directivesSet.add(dir.reference));
pipes.forEach(pipe => this.pipesSet.add(pipe.reference)); pipes.forEach(pipe => this.pipesSet.add(pipe.reference));
} }

View File

@ -8,10 +8,16 @@
import * as i18n from './i18n_ast'; import * as i18n from './i18n_ast';
export function digestMessage(message: i18n.Message): string { export function digest(message: i18n.Message): string {
return sha1(serializeNodes(message.nodes).join('') + `[${message.meaning}]`); return sha1(serializeNodes(message.nodes).join('') + `[${message.meaning}]`);
} }
export function decimalDigest(message: i18n.Message): string {
const visitor = new _SerializerIgnoreIcuExpVisitor();
const parts = message.nodes.map(a => a.visit(visitor, null));
return computeMsgId(parts.join(''), message.meaning);
}
/** /**
* Serialize the i18n ast to something xml-like in order to generate an UID. * Serialize the i18n ast to something xml-like in order to generate an UID.
* *
@ -39,7 +45,7 @@ class _SerializerVisitor implements i18n.Visitor {
} }
visitPlaceholder(ph: i18n.Placeholder, context: any): any { visitPlaceholder(ph: i18n.Placeholder, context: any): any {
return `<ph name="${ph.name}">${ph.value}</ph>`; return ph.value ? `<ph name="${ph.name}">${ph.value}</ph>` : `<ph name="${ph.name}"/>`;
} }
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
@ -53,6 +59,21 @@ export function serializeNodes(nodes: i18n.Node[]): string[] {
return nodes.map(a => a.visit(serializerVisitor, null)); return nodes.map(a => a.visit(serializerVisitor, null));
} }
/**
* Serialize the i18n ast to something xml-like in order to generate an UID.
*
* Ignore the ICU expressions so that message IDs stays identical if only the expression changes.
*
* @internal
*/
class _SerializerIgnoreIcuExpVisitor extends _SerializerVisitor {
visitIcu(icu: i18n.Icu, context: any): any {
let strCases = Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
// Do not take the expression into account
return `{${icu.type}, ${strCases.join(', ')}}`;
}
}
/** /**
* Compute the SHA1 of the given string * Compute the SHA1 of the given string
* *
@ -63,7 +84,7 @@ export function serializeNodes(nodes: i18n.Node[]): string[] {
*/ */
export function sha1(str: string): string { export function sha1(str: string): string {
const utf8 = utf8Encode(str); const utf8 = utf8Encode(str);
const words32 = stringToWords32(utf8); const words32 = stringToWords32(utf8, Endian.Big);
const len = utf8.length * 8; const len = utf8.length * 8;
const w = new Array(80); const w = new Array(80);
@ -90,15 +111,99 @@ export function sha1(str: string): string {
[a, b, c, d, e] = [add32(a, h0), add32(b, h1), add32(c, h2), add32(d, h3), add32(e, h4)]; [a, b, c, d, e] = [add32(a, h0), add32(b, h1), add32(c, h2), add32(d, h3), add32(e, h4)];
} }
const sha1 = words32ToString([a, b, c, d, e]); return byteStringToHexString(words32ToByteString([a, b, c, d, e]));
}
let hex: string = ''; function fk(index: number, b: number, c: number, d: number): [number, number] {
for (let i = 0; i < sha1.length; i++) { if (index < 20) {
const b = sha1.charCodeAt(i); return [(b & c) | (~b & d), 0x5a827999];
hex += (b >>> 4 & 0x0f).toString(16) + (b & 0x0f).toString(16);
} }
return hex.toLowerCase(); if (index < 40) {
return [b ^ c ^ d, 0x6ed9eba1];
}
if (index < 60) {
return [(b & c) | (b & d) | (c & d), 0x8f1bbcdc];
}
return [b ^ c ^ d, 0xca62c1d6];
}
/**
* Compute the fingerprint of the given string
*
* The output is 64 bit number encoded as a decimal string
*
* based on:
* https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/GoogleJsMessageIdGenerator.java
*/
export function fingerprint(str: string): [number, number] {
const utf8 = utf8Encode(str);
let [hi, lo] = [hash32(utf8, 0), hash32(utf8, 102072)];
if (hi == 0 && (lo == 0 || lo == 1)) {
hi = hi ^ 0x130f9bef;
lo = lo ^ -0x6b5f56d8;
}
return [hi, lo];
}
export function computeMsgId(msg: string, meaning: string): string {
let [hi, lo] = fingerprint(msg);
if (meaning) {
const [him, lom] = fingerprint(meaning);
[hi, lo] = add64(rol64([hi, lo], 1), [him, lom]);
}
return byteStringToDecString(words32ToByteString([hi & 0x7fffffff, lo]));
}
function hash32(str: string, c: number): number {
let [a, b] = [0x9e3779b9, 0x9e3779b9];
let i: number;
const len = str.length;
for (i = 0; i + 12 <= len; i += 12) {
a = add32(a, wordAt(str, i, Endian.Little));
b = add32(b, wordAt(str, i + 4, Endian.Little));
c = add32(c, wordAt(str, i + 8, Endian.Little));
[a, b, c] = mix([a, b, c]);
}
a = add32(a, wordAt(str, i, Endian.Little));
b = add32(b, wordAt(str, i + 4, Endian.Little));
// the first byte of c is reserved for the length
c = add32(c, len);
c = add32(c, wordAt(str, i + 8, Endian.Little) << 8);
return mix([a, b, c])[2];
}
// clang-format off
function mix([a, b, c]: [number, number, number]): [number, number, number] {
a = sub32(a, b); a = sub32(a, c); a ^= c >>> 13;
b = sub32(b, c); b = sub32(b, a); b ^= a << 8;
c = sub32(c, a); c = sub32(c, b); c ^= b >>> 13;
a = sub32(a, b); a = sub32(a, c); a ^= c >>> 12;
b = sub32(b, c); b = sub32(b, a); b ^= a << 16;
c = sub32(c, a); c = sub32(c, b); c ^= b >>> 5;
a = sub32(a, b); a = sub32(a, c); a ^= c >>> 3;
b = sub32(b, c); b = sub32(b, a); b ^= a << 10;
c = sub32(c, a); c = sub32(c, b); c ^= b >>> 15;
return [a, b, c];
}
// clang-format on
// Utils
enum Endian {
Little,
Big,
} }
function utf8Encode(str: string): string { function utf8Encode(str: string): string {
@ -131,10 +236,9 @@ function decodeSurrogatePairs(str: string, index: number): number {
} }
const high = str.charCodeAt(index); const high = str.charCodeAt(index);
let low: number;
if (high >= 0xd800 && high <= 0xdfff && str.length > index + 1) { if (high >= 0xd800 && high <= 0xdfff && str.length > index + 1) {
low = str.charCodeAt(index + 1); const low = byteAt(str, index + 1);
if (low >= 0xdc00 && low <= 0xdfff) { if (low >= 0xdc00 && low <= 0xdfff) {
return (high - 0xd800) * 0x400 + low - 0xdc00 + 0x10000; return (high - 0xd800) * 0x400 + low - 0xdc00 + 0x10000;
} }
@ -143,50 +247,126 @@ function decodeSurrogatePairs(str: string, index: number): number {
return high; return high;
} }
function stringToWords32(str: string): number[] { function add32(a: number, b: number): number {
const words32 = Array(str.length >>> 2); return add32to64(a, b)[1];
}
function add32to64(a: number, b: number): [number, number] {
const low = (a & 0xffff) + (b & 0xffff);
const high = (a >>> 16) + (b >>> 16) + (low >>> 16);
return [high >>> 16, (high << 16) | (low & 0xffff)];
}
function add64([ah, al]: [number, number], [bh, bl]: [number, number]): [number, number] {
const [carry, l] = add32to64(al, bl);
const h = add32(add32(ah, bh), carry);
return [h, l];
}
function sub32(a: number, b: number): number {
const low = (a & 0xffff) - (b & 0xffff);
const high = (a >> 16) - (b >> 16) + (low >> 16);
return (high << 16) | (low & 0xffff);
}
// Rotate a 32b number left `count` position
function rol32(a: number, count: number): number {
return (a << count) | (a >>> (32 - count));
}
// Rotate a 64b number left `count` position
function rol64([hi, lo]: [number, number], count: number): [number, number] {
const h = (hi << count) | (lo >>> (32 - count));
const l = (lo << count) | (hi >>> (32 - count));
return [h, l];
}
function stringToWords32(str: string, endian: Endian): number[] {
const words32 = Array((str.length + 3) >>> 2);
for (let i = 0; i < words32.length; i++) { for (let i = 0; i < words32.length; i++) {
words32[i] = 0; words32[i] = wordAt(str, i * 4, endian);
}
for (let i = 0; i < str.length; i++) {
words32[i >>> 2] |= (str.charCodeAt(i) & 0xff) << 8 * (3 - i & 0x3);
} }
return words32; return words32;
} }
function words32ToString(words32: number[]): string { function byteAt(str: string, index: number): number {
return index >= str.length ? 0 : str.charCodeAt(index) & 0xff;
}
function wordAt(str: string, index: number, endian: Endian): number {
let word = 0;
if (endian === Endian.Big) {
for (let i = 0; i < 4; i++) {
word += byteAt(str, index + i) << (24 - 8 * i);
}
} else {
for (let i = 0; i < 4; i++) {
word += byteAt(str, index + i) << 8 * i;
}
}
return word;
}
function words32ToByteString(words32: number[]): string {
return words32.reduce((str, word) => str + word32ToByteString(word), '');
}
function word32ToByteString(word: number): string {
let str = ''; let str = '';
for (let i = 0; i < words32.length * 4; i++) { for (let i = 0; i < 4; i++) {
str += String.fromCharCode((words32[i >>> 2] >>> 8 * (3 - i & 0x3)) & 0xff); str += String.fromCharCode((word >>> 8 * (3 - i)) & 0xff);
} }
return str; return str;
} }
function fk(index: number, b: number, c: number, d: number): [number, number] { function byteStringToHexString(str: string): string {
if (index < 20) { let hex: string = '';
return [(b & c) | (~b & d), 0x5a827999]; for (let i = 0; i < str.length; i++) {
const b = byteAt(str, i);
hex += (b >>> 4).toString(16) + (b & 0x0f).toString(16);
} }
return hex.toLowerCase();
if (index < 40) {
return [b ^ c ^ d, 0x6ed9eba1];
}
if (index < 60) {
return [(b & c) | (b & d) | (c & d), 0x8f1bbcdc];
}
return [b ^ c ^ d, 0xca62c1d6];
} }
function add32(a: number, b: number): number { // based on http://www.danvk.org/hex2dec.html (JS can not handle more than 56b)
const low = (a & 0xffff) + (b & 0xffff); function byteStringToDecString(str: string): string {
const high = (a >> 16) + (b >> 16) + (low >> 16); let decimal = '';
return (high << 16) | (low & 0xffff); let toThePower = '1';
for (let i = str.length - 1; i >= 0; i--) {
decimal = addBigInt(decimal, numberTimesBigInt(byteAt(str, i), toThePower));
toThePower = numberTimesBigInt(256, toThePower);
}
return decimal.split('').reverse().join('');
} }
function rol32(a: number, count: number): number { // x and y decimal, lowest significant digit first
return (a << count) | (a >>> (32 - count)); function addBigInt(x: string, y: string): string {
let sum = '';
const len = Math.max(x.length, y.length);
for (let i = 0, carry = 0; i < len || carry; i++) {
const tmpSum = carry + +(x[i] || 0) + +(y[i] || 0);
if (tmpSum >= 10) {
carry = 1;
sum += tmpSum - 10;
} else {
carry = 0;
sum += tmpSum;
}
}
return sum;
}
function numberTimesBigInt(num: number, b: string): string {
let product = '';
let bToThePower = b;
for (; num !== 0; num = num >>> 1) {
if (num & 1) product = addBigInt(product, bToThePower);
bToThePower = addBigInt(bToThePower, bToThePower);
}
return product;
} }

View File

@ -10,7 +10,6 @@ import * as html from '../ml_parser/ast';
import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseTreeResult} from '../ml_parser/parser'; import {ParseTreeResult} from '../ml_parser/parser';
import {digestMessage} from './digest';
import * as i18n from './i18n_ast'; import * as i18n from './i18n_ast';
import {createI18nMessageFactory} from './i18n_parser'; import {createI18nMessageFactory} from './i18n_parser';
import {I18nError} from './parse_util'; import {I18nError} from './parse_util';
@ -214,8 +213,8 @@ class _Visitor implements html.Visitor {
// Extract only top level nodes with the (implicit) "i18n" attribute if not in a block or an ICU // Extract only top level nodes with the (implicit) "i18n" attribute if not in a block or an ICU
// message // message
const i18nAttr = _getI18nAttr(el); const i18nAttr = _getI18nAttr(el);
const isImplicit = this._implicitTags.some((tag: string): boolean => el.name === tag) && const isImplicit = this._implicitTags.some(tag => el.name === tag) && !this._inIcu &&
!this._inIcu && !this._isInTranslatableSection; !this._isInTranslatableSection;
const isTopLevelImplicit = !wasInImplicitNode && isImplicit; const isTopLevelImplicit = !wasInImplicitNode && isImplicit;
this._inImplicitNode = this._inImplicitNode || isImplicit; this._inImplicitNode = this._inImplicitNode || isImplicit;
@ -348,14 +347,14 @@ class _Visitor implements html.Visitor {
// no-op when called in extraction mode (returns []) // no-op when called in extraction mode (returns [])
private _translateMessage(el: html.Node, message: i18n.Message): html.Node[] { private _translateMessage(el: html.Node, message: i18n.Message): html.Node[] {
if (message && this._mode === _VisitorMode.Merge) { if (message && this._mode === _VisitorMode.Merge) {
const id = digestMessage(message); const nodes = this._translations.get(message);
const nodes = this._translations.get(id);
if (nodes) { if (nodes) {
return nodes; return nodes;
} }
this._reportError(el, `Translation unavailable for message id="${id}"`); this._reportError(
el, `Translation unavailable for message id="${this._translations.digest(message)}"`);
} }
return []; return [];
@ -384,19 +383,20 @@ class _Visitor implements html.Visitor {
if (attr.value && attr.value != '' && i18nAttributeMeanings.hasOwnProperty(attr.name)) { if (attr.value && attr.value != '' && i18nAttributeMeanings.hasOwnProperty(attr.name)) {
const meaning = i18nAttributeMeanings[attr.name]; const meaning = i18nAttributeMeanings[attr.name];
const message: i18n.Message = this._createI18nMessage([attr], meaning, ''); const message: i18n.Message = this._createI18nMessage([attr], meaning, '');
const id = digestMessage(message); const nodes = this._translations.get(message);
const nodes = this._translations.get(id);
if (nodes) { if (nodes) {
if (nodes[0] instanceof html.Text) { if (nodes[0] instanceof html.Text) {
const value = (nodes[0] as html.Text).value; const value = (nodes[0] as html.Text).value;
translatedAttributes.push(new html.Attribute(attr.name, value, attr.sourceSpan)); translatedAttributes.push(new html.Attribute(attr.name, value, attr.sourceSpan));
} else { } else {
this._reportError( this._reportError(
el, `Unexpected translation for attribute "${attr.name}" (id="${id}")`); el,
`Unexpected translation for attribute "${attr.name}" (id="${this._translations.digest(message)}")`);
} }
} else { } else {
this._reportError( this._reportError(
el, `Translation unavailable for attribute "${attr.name}" (id="${id}")`); el,
`Translation unavailable for attribute "${attr.name}" (id="${this._translations.digest(message)}")`);
} }
} else { } else {
translatedAttributes.push(attr); translatedAttributes.push(attr);

View File

@ -12,18 +12,20 @@ export class Message {
/** /**
* @param nodes message AST * @param nodes message AST
* @param placeholders maps placeholder names to static content * @param placeholders maps placeholder names to static content
* @param placeholderToMsgIds maps placeholder names to translatable message IDs (used for ICU * @param placeholderToMessage maps placeholder names to messages (used for nested ICU messages)
* messages)
* @param meaning * @param meaning
* @param description * @param description
*/ */
constructor( constructor(
public nodes: Node[], public placeholders: {[name: string]: string}, public nodes: Node[], public placeholders: {[phName: string]: string},
public placeholderToMsgIds: {[name: string]: string}, public meaning: string, public placeholderToMessage: {[phName: string]: Message}, public meaning: string,
public description: string) {} public description: string) {}
} }
export interface Node { visit(visitor: Visitor, context?: any): any; } export interface Node {
sourceSpan: ParseSourceSpan;
visit(visitor: Visitor, context?: any): any;
}
export class Text implements Node { export class Text implements Node {
constructor(public value: string, public sourceSpan: ParseSourceSpan) {} constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
@ -31,6 +33,7 @@ export class Text implements Node {
visit(visitor: Visitor, context?: any): any { return visitor.visitText(this, context); } visit(visitor: Visitor, context?: any): any { return visitor.visitText(this, context); }
} }
// TODO(vicb): do we really need this node (vs an array) ?
export class Container implements Node { export class Container implements Node {
constructor(public children: Node[], public sourceSpan: ParseSourceSpan) {} constructor(public children: Node[], public sourceSpan: ParseSourceSpan) {}
@ -38,6 +41,7 @@ export class Container implements Node {
} }
export class Icu implements Node { export class Icu implements Node {
public expressionPlaceholder: string;
constructor( constructor(
public expression: string, public type: string, public cases: {[k: string]: Node}, public expression: string, public type: string, public cases: {[k: string]: Node},
public sourceSpan: ParseSourceSpan) {} public sourceSpan: ParseSourceSpan) {}
@ -55,13 +59,13 @@ export class TagPlaceholder implements Node {
} }
export class Placeholder implements Node { export class Placeholder implements Node {
constructor(public value: string, public name: string = '', public sourceSpan: ParseSourceSpan) {} constructor(public value: string, public name: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitPlaceholder(this, context); } visit(visitor: Visitor, context?: any): any { return visitor.visitPlaceholder(this, context); }
} }
export class IcuPlaceholder implements Node { export class IcuPlaceholder implements Node {
constructor(public value: Icu, public name: string = '', public sourceSpan: ParseSourceSpan) {} constructor(public value: Icu, public name: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitIcuPlaceholder(this, context); } visit(visitor: Visitor, context?: any): any { return visitor.visitIcuPlaceholder(this, context); }
} }

View File

@ -11,7 +11,6 @@ import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/in
import {ParseTreeResult} from '../ml_parser/parser'; import {ParseTreeResult} from '../ml_parser/parser';
import {mergeTranslations} from './extractor_merger'; import {mergeTranslations} from './extractor_merger';
import {MessageBundle} from './message_bundle';
import {Serializer} from './serializers/serializer'; import {Serializer} from './serializers/serializer';
import {Xliff} from './serializers/xliff'; import {Xliff} from './serializers/xliff';
import {Xmb} from './serializers/xmb'; import {Xmb} from './serializers/xmb';
@ -41,32 +40,29 @@ export class I18NHtmlParser implements HtmlParser {
} }
// TODO(vicb): add support for implicit tags / attributes // TODO(vicb): add support for implicit tags / attributes
const messageBundle = new MessageBundle(this._htmlParser, [], {});
const errors = messageBundle.updateFromTemplate(source, url, interpolationConfig);
if (errors && errors.length) { if (parseResult.errors.length) {
return new ParseTreeResult(parseResult.rootNodes, parseResult.errors.concat(errors)); return new ParseTreeResult(parseResult.rootNodes, parseResult.errors);
} }
const serializer = this._createSerializer(interpolationConfig); const serializer = this._createSerializer();
const translationBundle = const translationBundle = TranslationBundle.load(this._translations, url, serializer);
TranslationBundle.load(this._translations, url, messageBundle, serializer);
return mergeTranslations(parseResult.rootNodes, translationBundle, interpolationConfig, [], {}); return mergeTranslations(parseResult.rootNodes, translationBundle, interpolationConfig, [], {});
} }
private _createSerializer(interpolationConfig: InterpolationConfig): Serializer { private _createSerializer(): Serializer {
const format = (this._translationsFormat || 'xlf').toLowerCase(); const format = (this._translationsFormat || 'xlf').toLowerCase();
switch (format) { switch (format) {
case 'xmb': case 'xmb':
return new Xmb(); return new Xmb();
case 'xtb': case 'xtb':
return new Xtb(this._htmlParser, interpolationConfig); return new Xtb();
case 'xliff': case 'xliff':
case 'xlf': case 'xlf':
default: default:
return new Xliff(this._htmlParser, interpolationConfig); return new Xliff();
} }
} }
} }

View File

@ -12,7 +12,6 @@ import * as html from '../ml_parser/ast';
import {getHtmlTagDefinition} from '../ml_parser/html_tags'; import {getHtmlTagDefinition} from '../ml_parser/html_tags';
import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseSourceSpan} from '../parse_util'; import {ParseSourceSpan} from '../parse_util';
import {digestMessage} from './digest';
import * as i18n from './i18n_ast'; import * as i18n from './i18n_ast';
import {PlaceholderRegistry} from './serializers/placeholder'; import {PlaceholderRegistry} from './serializers/placeholder';
@ -34,8 +33,8 @@ class _I18nVisitor implements html.Visitor {
private _isIcu: boolean; private _isIcu: boolean;
private _icuDepth: number; private _icuDepth: number;
private _placeholderRegistry: PlaceholderRegistry; private _placeholderRegistry: PlaceholderRegistry;
private _placeholderToContent: {[name: string]: string}; private _placeholderToContent: {[phName: string]: string};
private _placeholderToIds: {[name: string]: string}; private _placeholderToMessage: {[phName: string]: i18n.Message};
constructor( constructor(
private _expressionParser: ExpressionParser, private _expressionParser: ExpressionParser,
@ -46,12 +45,12 @@ class _I18nVisitor implements html.Visitor {
this._icuDepth = 0; this._icuDepth = 0;
this._placeholderRegistry = new PlaceholderRegistry(); this._placeholderRegistry = new PlaceholderRegistry();
this._placeholderToContent = {}; this._placeholderToContent = {};
this._placeholderToIds = {}; this._placeholderToMessage = {};
const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {}); const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {});
return new i18n.Message( return new i18n.Message(
i18nodes, this._placeholderToContent, this._placeholderToIds, meaning, description); i18nodes, this._placeholderToContent, this._placeholderToMessage, meaning, description);
} }
visitElement(el: html.Element, context: any): i18n.Node { visitElement(el: html.Element, context: any): i18n.Node {
@ -99,7 +98,13 @@ class _I18nVisitor implements html.Visitor {
this._icuDepth--; this._icuDepth--;
if (this._isIcu || this._icuDepth > 0) { if (this._isIcu || this._icuDepth > 0) {
// If the message (vs a part of the message) is an ICU message returns it // Returns an ICU node when:
// - the message (vs a part of the message) is an ICU message, or
// - the ICU message is nested.
const expPh = this._placeholderRegistry.getUniquePlaceholder(`VAR_${icu.type}`);
i18nIcu.expressionPlaceholder = expPh;
this._placeholderToContent[expPh] = icu.switchValue;
return i18nIcu; return i18nIcu;
} }
@ -110,7 +115,7 @@ class _I18nVisitor implements html.Visitor {
// TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg // TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg
const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString()); const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
const visitor = new _I18nVisitor(this._expressionParser, this._interpolationConfig); const visitor = new _I18nVisitor(this._expressionParser, this._interpolationConfig);
this._placeholderToIds[phName] = digestMessage(visitor.toI18nMessage([icu], '', '')); this._placeholderToMessage[phName] = visitor.toI18nMessage([icu], '', '');
return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan); return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
} }

View File

@ -10,7 +10,6 @@ import {HtmlParser} from '../ml_parser/html_parser';
import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseError} from '../parse_util'; import {ParseError} from '../parse_util';
import {digestMessage} from './digest';
import {extractMessages} from './extractor_merger'; import {extractMessages} from './extractor_merger';
import {Message} from './i18n_ast'; import {Message} from './i18n_ast';
import {Serializer} from './serializers/serializer'; import {Serializer} from './serializers/serializer';
@ -19,7 +18,7 @@ import {Serializer} from './serializers/serializer';
* A container for message extracted from the templates. * A container for message extracted from the templates.
*/ */
export class MessageBundle { export class MessageBundle {
private _messageMap: {[id: string]: Message} = {}; private _messages: Message[] = [];
constructor( constructor(
private _htmlParser: HtmlParser, private _implicitTags: string[], private _htmlParser: HtmlParser, private _implicitTags: string[],
@ -40,11 +39,10 @@ export class MessageBundle {
return i18nParserResult.errors; return i18nParserResult.errors;
} }
i18nParserResult.messages.forEach( this._messages.push(...i18nParserResult.messages);
(message) => { this._messageMap[digestMessage(message)] = message; });
} }
getMessageMap(): {[id: string]: Message} { return this._messageMap; } getMessages(): Message[] { return this._messages; }
write(serializer: Serializer): string { return serializer.write(this._messageMap); } write(serializer: Serializer): string { return serializer.write(this._messages); }
} }

View File

@ -40,7 +40,9 @@ const TAG_TO_PLACEHOLDER_NAMES: {[k: string]: string} = {
}; };
/** /**
* Creates unique names for placeholder with different content * Creates unique names for placeholder with different content.
*
* Returns the same placeholder name when the content is identical.
* *
* @internal * @internal
*/ */
@ -93,6 +95,10 @@ export class PlaceholderRegistry {
return uniqueName; return uniqueName;
} }
getUniquePlaceholder(name: string): string {
return this._generateUniqueName(name.toUpperCase());
}
// Generate a hash for a tag - does not take attribute order into account // Generate a hash for a tag - does not take attribute order into account
private _hashTag(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string { private _hashTag(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string {
const start = `<${tag}`; const start = `<${tag}`;
@ -105,18 +111,8 @@ export class PlaceholderRegistry {
private _hashClosingTag(tag: string): string { return this._hashTag(`/${tag}`, {}, false); } private _hashClosingTag(tag: string): string { return this._hashTag(`/${tag}`, {}, false); }
private _generateUniqueName(base: string): string { private _generateUniqueName(base: string): string {
let name = base; const next = this._placeHolderNameCounts[base];
let next = this._placeHolderNameCounts[name]; this._placeHolderNameCounts[base] = next ? next + 1 : 1;
return next ? `${base}_${next}` : base;
if (!next) {
next = 1;
} else {
name += `_${next}`;
next++;
}
this._placeHolderNameCounts[base] = next;
return name;
} }
} }

View File

@ -6,36 +6,12 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as html from '../../ml_parser/ast';
import * as i18n from '../i18n_ast'; import * as i18n from '../i18n_ast';
import {MessageBundle} from '../message_bundle';
export interface Serializer { export interface Serializer {
write(messageMap: {[id: string]: i18n.Message}): string; write(messages: i18n.Message[]): string;
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: html.Node[]}; load(content: string, url: string): {[msgId: string]: i18n.Node[]};
}
digest(message: i18n.Message): string;
// Generate a map of placeholder to content indexed by message ids
export function extractPlaceholders(messageBundle: MessageBundle) {
const messageMap = messageBundle.getMessageMap();
const placeholders: {[id: string]: {[name: string]: string}} = {};
Object.keys(messageMap).forEach(msgId => {
placeholders[msgId] = messageMap[msgId].placeholders;
});
return placeholders;
}
// Generate a map of placeholder to message ids indexed by message ids
export function extractPlaceholderToIds(messageBundle: MessageBundle) {
const messageMap = messageBundle.getMessageMap();
const placeholderToIds: {[id: string]: {[name: string]: string}} = {};
Object.keys(messageMap).forEach(msgId => {
placeholderToIds[msgId] = messageMap[msgId].placeholderToMsgIds;
});
return placeholderToIds;
} }

View File

@ -6,17 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ListWrapper} from '../../facade/collection';
import * as ml from '../../ml_parser/ast'; import * as ml from '../../ml_parser/ast';
import {HtmlParser} from '../../ml_parser/html_parser';
import {InterpolationConfig} from '../../ml_parser/interpolation_config';
import {XmlParser} from '../../ml_parser/xml_parser'; import {XmlParser} from '../../ml_parser/xml_parser';
import {ParseError} from '../../parse_util'; import {digest} from '../digest';
import * as i18n from '../i18n_ast'; import * as i18n from '../i18n_ast';
import {MessageBundle} from '../message_bundle';
import {I18nError} from '../parse_util'; import {I18nError} from '../parse_util';
import {Serializer, extractPlaceholderToIds, extractPlaceholders} from './serializer'; import {Serializer} from './serializer';
import * as xml from './xml_helper'; import * as xml from './xml_helper';
const _VERSION = '1.2'; const _VERSION = '1.2';
@ -24,6 +20,7 @@ const _XMLNS = 'urn:oasis:names:tc:xliff:document:1.2';
// TODO(vicb): make this a param (s/_/-/) // TODO(vicb): make this a param (s/_/-/)
const _SOURCE_LANG = 'en'; const _SOURCE_LANG = 'en';
const _PLACEHOLDER_TAG = 'x'; const _PLACEHOLDER_TAG = 'x';
const _SOURCE_TAG = 'source'; const _SOURCE_TAG = 'source';
const _TARGET_TAG = 'target'; const _TARGET_TAG = 'target';
const _UNIT_TAG = 'trans-unit'; const _UNIT_TAG = 'trans-unit';
@ -31,17 +28,19 @@ const _UNIT_TAG = 'trans-unit';
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html // http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html
// http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html // http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
export class Xliff implements Serializer { export class Xliff implements Serializer {
constructor(private _htmlParser: HtmlParser, private _interpolationConfig: InterpolationConfig) {} write(messages: i18n.Message[]): string {
write(messageMap: {[id: string]: i18n.Message}): string {
const visitor = new _WriteVisitor(); const visitor = new _WriteVisitor();
const visited: {[id: string]: boolean} = {};
const transUnits: xml.Node[] = []; const transUnits: xml.Node[] = [];
Object.keys(messageMap).forEach((id) => { messages.forEach(message => {
const message = messageMap[id]; const id = this.digest(message);
const transUnit = new xml.Tag(_UNIT_TAG, {id: id, datatype: 'html'}); // deduplicate messages
if (visited[id]) return;
visited[id] = true;
const transUnit = new xml.Tag(_UNIT_TAG, {id, datatype: 'html'});
transUnit.children.push( transUnit.children.push(
new xml.CR(8), new xml.Tag(_SOURCE_TAG, {}, visitor.serialize(message.nodes)), new xml.CR(8), new xml.Tag(_SOURCE_TAG, {}, visitor.serialize(message.nodes)),
new xml.CR(8), new xml.Tag(_TARGET_TAG)); new xml.CR(8), new xml.Tag(_TARGET_TAG));
@ -76,38 +75,28 @@ export class Xliff implements Serializer {
]); ]);
} }
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: ml.Node[]} { load(content: string, url: string): {[msgId: string]: i18n.Node[]} {
// Parse the xtb file into xml nodes // xliff to xml nodes
const result = new XmlParser().parse(content, url); const xliffParser = new XliffParser();
const {mlNodesByMsgId, errors} = xliffParser.parse(content, url);
if (result.errors.length) { // xml nodes to i18n nodes
throw new Error(`xtb parse errors:\n${result.errors.join('\n')}`); const i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {};
} const converter = new XmlToI18n();
Object.keys(mlNodesByMsgId).forEach(msgId => {
// Replace the placeholders, messages are now string const {i18nNodes, errors: e} = converter.convert(mlNodesByMsgId[msgId]);
const {messages, errors} = new _LoadVisitor().parse(result.rootNodes, messageBundle); errors.push(...e);
i18nNodesByMsgId[msgId] = i18nNodes;
if (errors.length) {
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
}
// Convert the string messages to html ast
// TODO(vicb): map error message back to the original message in xtb
const messageMap: {[id: string]: ml.Node[]} = {};
const parseErrors: ParseError[] = [];
Object.keys(messages).forEach((id) => {
const res = this._htmlParser.parse(messages[id], url, true, this._interpolationConfig);
parseErrors.push(...res.errors);
messageMap[id] = res.rootNodes;
}); });
if (parseErrors.length) { if (errors.length) {
throw new Error(`xtb parse errors:\n${parseErrors.join('\n')}`); throw new Error(`xliff parse errors:\n${errors.join('\n')}`);
} }
return messageMap; return i18nNodesByMsgId;
} }
digest(message: i18n.Message): string { return digest(message); }
} }
class _WriteVisitor implements i18n.Visitor { class _WriteVisitor implements i18n.Visitor {
@ -162,80 +151,51 @@ class _WriteVisitor implements i18n.Visitor {
serialize(nodes: i18n.Node[]): xml.Node[] { serialize(nodes: i18n.Node[]): xml.Node[] {
this._isInIcu = false; this._isInIcu = false;
return ListWrapper.flatten(nodes.map(node => node.visit(this))); return [].concat(...nodes.map(node => node.visit(this)));
} }
} }
// TODO(vicb): add error management (structure) // TODO(vicb): add error management (structure)
// TODO(vicb): factorize (xtb) ? // Extract messages as xml nodes from the xliff file
class _LoadVisitor implements ml.Visitor { class XliffParser implements ml.Visitor {
private _messageNodes: [string, ml.Node[]][]; private _unitMlNodes: ml.Node[];
private _translatedMessages: {[id: string]: string};
private _msgId: string;
private _target: ml.Node[];
private _errors: I18nError[]; private _errors: I18nError[];
private _placeholders: {[name: string]: string}; private _mlNodesByMsgId: {[msgId: string]: ml.Node[]};
private _placeholderToIds: {[name: string]: string};
parse(nodes: ml.Node[], messageBundle: MessageBundle): parse(xliff: string, url: string) {
{messages: {[k: string]: string}, errors: I18nError[]} { this._unitMlNodes = [];
this._messageNodes = []; this._mlNodesByMsgId = {};
this._translatedMessages = {};
this._msgId = '';
this._target = [];
this._errors = [];
// Find all messages const xml = new XmlParser().parse(xliff, url, false);
ml.visitAll(this, nodes, null);
const messageMap = messageBundle.getMessageMap(); this._errors = xml.errors;
const placeholders = extractPlaceholders(messageBundle); ml.visitAll(this, xml.rootNodes, null);
const placeholderToIds = extractPlaceholderToIds(messageBundle);
this._messageNodes return {
.filter(message => { mlNodesByMsgId: this._mlNodesByMsgId,
// Remove any messages that is not present in the source message bundle. errors: this._errors,
return messageMap.hasOwnProperty(message[0]); };
})
.sort((a, b) => {
// Because there could be no ICU placeholders inside an ICU message,
// we do not need to take into account the `placeholderToMsgIds` of the referenced
// messages, those would always be empty
// TODO(vicb): overkill - create 2 buckets and [...woDeps, ...wDeps].process()
if (Object.keys(messageMap[a[0]].placeholderToMsgIds).length == 0) {
return -1;
}
if (Object.keys(messageMap[b[0]].placeholderToMsgIds).length == 0) {
return 1;
}
return 0;
})
.forEach(message => {
const id = message[0];
this._placeholders = placeholders[id] || {};
this._placeholderToIds = placeholderToIds[id] || {};
// TODO(vicb): make sure there is no `_TRANSLATIONS_TAG` nor `_TRANSLATION_TAG`
this._translatedMessages[id] = ml.visitAll(this, message[1]).join('');
});
return {messages: this._translatedMessages, errors: this._errors};
} }
visitElement(element: ml.Element, context: any): any { visitElement(element: ml.Element, context: any): any {
switch (element.name) { switch (element.name) {
case _UNIT_TAG: case _UNIT_TAG:
this._target = null; this._unitMlNodes = null;
const msgId = element.attrs.find((attr) => attr.name === 'id'); const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!msgId) { if (!idAttr) {
this._addError(element, `<${_UNIT_TAG}> misses the "id" attribute`); this._addError(element, `<${_UNIT_TAG}> misses the "id" attribute`);
} else { } else {
this._msgId = msgId.value; const id = idAttr.value;
} if (this._mlNodesByMsgId.hasOwnProperty(id)) {
ml.visitAll(this, element.children, null); this._addError(element, `Duplicated translations for msg ${id}`);
if (this._msgId !== null) { } else {
this._messageNodes.push([this._msgId, this._target]); ml.visitAll(this, element.children, null);
if (this._unitMlNodes) {
this._mlNodesByMsgId[id] = this._unitMlNodes;
} else {
this._addError(element, `Message ${id} misses a translation`);
}
}
} }
break; break;
@ -244,48 +204,65 @@ class _LoadVisitor implements ml.Visitor {
break; break;
case _TARGET_TAG: case _TARGET_TAG:
this._target = element.children; this._unitMlNodes = element.children;
break;
case _PLACEHOLDER_TAG:
const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) {
this._addError(element, `<${_PLACEHOLDER_TAG}> misses the "id" attribute`);
} else {
const id = idAttr.value;
if (this._placeholders.hasOwnProperty(id)) {
return this._placeholders[id];
}
if (this._placeholderToIds.hasOwnProperty(id) &&
this._translatedMessages.hasOwnProperty(this._placeholderToIds[id])) {
return this._translatedMessages[this._placeholderToIds[id]];
}
// TODO(vicb): better error message for when
// !this._translatedMessages.hasOwnProperty(this._placeholderToIds[id])
this._addError(element, `The placeholder "${id}" does not exists in the source message`);
}
break; break;
default: default:
// TODO(vicb): assert file structure, xliff version
// For now only recurse on unhandled nodes
ml.visitAll(this, element.children, null); ml.visitAll(this, element.children, null);
} }
} }
visitAttribute(attribute: ml.Attribute, context: any): any { visitAttribute(attribute: ml.Attribute, context: any): any {}
throw new Error('unreachable code');
visitText(text: ml.Text, context: any): any {}
visitComment(comment: ml.Comment, context: any): any {}
visitExpansion(expansion: ml.Expansion, context: any): any {}
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message));
}
}
// Convert ml nodes (xliff syntax) to i18n nodes
class XmlToI18n implements ml.Visitor {
private _errors: I18nError[];
convert(nodes: ml.Node[]) {
this._errors = [];
return {
i18nNodes: ml.visitAll(this, nodes),
errors: this._errors,
};
} }
visitText(text: ml.Text, context: any): any { return text.value; } visitText(text: ml.Text, context: any) { return new i18n.Text(text.value, text.sourceSpan); }
visitComment(comment: ml.Comment, context: any): any { return ''; } visitElement(el: ml.Element, context: any): i18n.Placeholder {
if (el.name === _PLACEHOLDER_TAG) {
const nameAttr = el.attrs.find((attr) => attr.name === 'id');
if (nameAttr) {
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
}
visitExpansion(expansion: ml.Expansion, context: any): any { this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "id" attribute`);
throw new Error('unreachable code'); } else {
this._addError(el, `Unexpected tag`);
}
} }
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any { visitExpansion(icu: ml.Expansion, context: any) {}
throw new Error('unreachable code');
} visitExpansionCase(icuCase: ml.ExpansionCase, context: any): any {}
visitComment(comment: ml.Comment, context: any) {}
visitAttribute(attribute: ml.Attribute, context: any) {}
private _addError(node: ml.Node, message: string): void { private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message)); this._errors.push(new I18nError(node.sourceSpan, message));

View File

@ -6,10 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ListWrapper} from '../../facade/collection'; import {decimalDigest} from '../digest';
import * as html from '../../ml_parser/ast';
import * as i18n from '../i18n_ast'; import * as i18n from '../i18n_ast';
import {MessageBundle} from '../message_bundle';
import {Serializer} from './serializer'; import {Serializer} from './serializer';
import * as xml from './xml_helper'; import * as xml from './xml_helper';
@ -40,12 +38,18 @@ const _DOCTYPE = `<!ELEMENT messagebundle (msg)*>
<!ELEMENT ex (#PCDATA)>`; <!ELEMENT ex (#PCDATA)>`;
export class Xmb implements Serializer { export class Xmb implements Serializer {
write(messageMap: {[k: string]: i18n.Message}): string { write(messages: i18n.Message[]): string {
const visitor = new _Visitor(); const visitor = new _Visitor();
const rootNode = new xml.Tag(_MESSAGES_TAG); const visited: {[id: string]: boolean} = {};
let rootNode = new xml.Tag(_MESSAGES_TAG);
messages.forEach(message => {
const id = this.digest(message);
// deduplicate messages
if (visited[id]) return;
visited[id] = true;
Object.keys(messageMap).forEach((id) => {
const message = messageMap[id];
const attrs: {[k: string]: string} = {id}; const attrs: {[k: string]: string} = {id};
if (message.description) { if (message.description) {
@ -72,9 +76,11 @@ export class Xmb implements Serializer {
]); ]);
} }
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: html.Node[]} { load(content: string, url: string): {[msgId: string]: i18n.Node[]} {
throw new Error('Unsupported'); throw new Error('Unsupported');
} }
digest(message: i18n.Message): string { return digest(message); }
} }
class _Visitor implements i18n.Visitor { class _Visitor implements i18n.Visitor {
@ -87,7 +93,7 @@ class _Visitor implements i18n.Visitor {
} }
visitIcu(icu: i18n.Icu, context?: any): xml.Node[] { visitIcu(icu: i18n.Icu, context?: any): xml.Node[] {
const nodes = [new xml.Text(`{${icu.expression}, ${icu.type}, `)]; const nodes = [new xml.Text(`{${icu.expressionPlaceholder}, ${icu.type}, `)];
Object.keys(icu.cases).forEach((c: string) => { Object.keys(icu.cases).forEach((c: string) => {
nodes.push(new xml.Text(`${c} {`), ...icu.cases[c].visit(this), new xml.Text(`} `)); nodes.push(new xml.Text(`${c} {`), ...icu.cases[c].visit(this), new xml.Text(`} `));
@ -121,6 +127,10 @@ class _Visitor implements i18n.Visitor {
} }
serialize(nodes: i18n.Node[]): xml.Node[] { serialize(nodes: i18n.Node[]): xml.Node[] {
return ListWrapper.flatten(nodes.map(node => node.visit(this))); return [].concat(...nodes.map(node => node.visit(this)));
} }
} }
export function digest(message: i18n.Message): string {
return decimalDigest(message);
}

View File

@ -7,112 +7,63 @@
*/ */
import * as ml from '../../ml_parser/ast'; import * as ml from '../../ml_parser/ast';
import {HtmlParser} from '../../ml_parser/html_parser';
import {InterpolationConfig} from '../../ml_parser/interpolation_config';
import {XmlParser} from '../../ml_parser/xml_parser'; import {XmlParser} from '../../ml_parser/xml_parser';
import {ParseError} from '../../parse_util';
import * as i18n from '../i18n_ast'; import * as i18n from '../i18n_ast';
import {MessageBundle} from '../message_bundle';
import {I18nError} from '../parse_util'; import {I18nError} from '../parse_util';
import {Serializer, extractPlaceholderToIds, extractPlaceholders} from './serializer'; import {Serializer} from './serializer';
import {digest} from './xmb';
const _TRANSLATIONS_TAG = 'translationbundle'; const _TRANSLATIONS_TAG = 'translationbundle';
const _TRANSLATION_TAG = 'translation'; const _TRANSLATION_TAG = 'translation';
const _PLACEHOLDER_TAG = 'ph'; const _PLACEHOLDER_TAG = 'ph';
export class Xtb implements Serializer { export class Xtb implements Serializer {
constructor(private _htmlParser: HtmlParser, private _interpolationConfig: InterpolationConfig) {} write(messages: i18n.Message[]): string { throw new Error('Unsupported'); }
write(messageMap: {[id: string]: i18n.Message}): string { throw new Error('Unsupported'); } load(content: string, url: string): {[msgId: string]: i18n.Node[]} {
// xtb to xml nodes
const xtbParser = new XtbParser();
const {mlNodesByMsgId, errors} = xtbParser.parse(content, url);
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: ml.Node[]} { // xml nodes to i18n nodes
// Parse the xtb file into xml nodes const i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {};
const result = new XmlParser().parse(content, url); const converter = new XmlToI18n();
Object.keys(mlNodesByMsgId).forEach(msgId => {
if (result.errors.length) { const {i18nNodes, errors: e} = converter.convert(mlNodesByMsgId[msgId]);
throw new Error(`xtb parse errors:\n${result.errors.join('\n')}`); errors.push(...e);
} i18nNodesByMsgId[msgId] = i18nNodes;
});
// Replace the placeholders, messages are now string
const {messages, errors} = new _Visitor().parse(result.rootNodes, messageBundle);
if (errors.length) { if (errors.length) {
throw new Error(`xtb parse errors:\n${errors.join('\n')}`); throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
} }
// Convert the string messages to html ast return i18nNodesByMsgId;
// TODO(vicb): map error message back to the original message in xtb
const messageMap: {[id: string]: ml.Node[]} = {};
const parseErrors: ParseError[] = [];
Object.keys(messages).forEach((id) => {
const res = this._htmlParser.parse(messages[id], url, true, this._interpolationConfig);
parseErrors.push(...res.errors);
messageMap[id] = res.rootNodes;
});
if (parseErrors.length) {
throw new Error(`xtb parse errors:\n${parseErrors.join('\n')}`);
}
return messageMap;
} }
digest(message: i18n.Message): string { return digest(message); }
} }
class _Visitor implements ml.Visitor { // Extract messages as xml nodes from the xtb file
private _messageNodes: [string, ml.Node[]][]; class XtbParser implements ml.Visitor {
private _translatedMessages: {[id: string]: string};
private _bundleDepth: number; private _bundleDepth: number;
private _translationDepth: number;
private _errors: I18nError[]; private _errors: I18nError[];
private _placeholders: {[name: string]: string}; private _mlNodesByMsgId: {[msgId: string]: ml.Node[]};
private _placeholderToIds: {[name: string]: string};
parse(nodes: ml.Node[], messageBundle: MessageBundle): parse(xtb: string, url: string) {
{messages: {[k: string]: string}, errors: I18nError[]} {
this._messageNodes = [];
this._translatedMessages = {};
this._bundleDepth = 0; this._bundleDepth = 0;
this._translationDepth = 0; this._mlNodesByMsgId = {};
this._errors = [];
// Find all messages const xml = new XmlParser().parse(xtb, url, true);
ml.visitAll(this, nodes, null);
const messageMap = messageBundle.getMessageMap(); this._errors = xml.errors;
const placeholders = extractPlaceholders(messageBundle); ml.visitAll(this, xml.rootNodes);
const placeholderToIds = extractPlaceholderToIds(messageBundle);
this._messageNodes return {
.filter(message => { mlNodesByMsgId: this._mlNodesByMsgId,
// Remove any messages that is not present in the source message bundle. errors: this._errors,
return messageMap.hasOwnProperty(message[0]); };
})
.sort((a, b) => {
// Because there could be no ICU placeholders inside an ICU message,
// we do not need to take into account the `placeholderToMsgIds` of the referenced
// messages, those would always be empty
// TODO(vicb): overkill - create 2 buckets and [...woDeps, ...wDeps].process()
if (Object.keys(messageMap[a[0]].placeholderToMsgIds).length == 0) {
return -1;
}
if (Object.keys(messageMap[b[0]].placeholderToMsgIds).length == 0) {
return 1;
}
return 0;
})
.forEach(message => {
const id = message[0];
this._placeholders = placeholders[id] || {};
this._placeholderToIds = placeholderToIds[id] || {};
// TODO(vicb): make sure there is no `_TRANSLATIONS_TAG` nor `_TRANSLATION_TAG`
this._translatedMessages[id] = ml.visitAll(this, message[1]).join('');
});
return {messages: this._translatedMessages, errors: this._errors};
} }
visitElement(element: ml.Element, context: any): any { visitElement(element: ml.Element, context: any): any {
@ -127,40 +78,16 @@ class _Visitor implements ml.Visitor {
break; break;
case _TRANSLATION_TAG: case _TRANSLATION_TAG:
this._translationDepth++;
if (this._translationDepth > 1) {
this._addError(element, `<${_TRANSLATION_TAG}> elements can not be nested`);
}
const idAttr = element.attrs.find((attr) => attr.name === 'id'); const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) { if (!idAttr) {
this._addError(element, `<${_TRANSLATION_TAG}> misses the "id" attribute`); this._addError(element, `<${_TRANSLATION_TAG}> misses the "id" attribute`);
} else { } else {
// ICU placeholders are reference to other messages. const id = idAttr.value;
// The referenced message might not have been decoded yet. if (this._mlNodesByMsgId.hasOwnProperty(id)) {
// We need to have all messages available to make sure deps are decoded first. this._addError(element, `Duplicated translations for msg ${id}`);
// TODO(vicb): report an error on duplicate id } else {
this._messageNodes.push([idAttr.value, element.children]); this._mlNodesByMsgId[id] = element.children;
}
this._translationDepth--;
break;
case _PLACEHOLDER_TAG:
const nameAttr = element.attrs.find((attr) => attr.name === 'name');
if (!nameAttr) {
this._addError(element, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
} else {
const name = nameAttr.value;
if (this._placeholders.hasOwnProperty(name)) {
return this._placeholders[name];
} }
if (this._placeholderToIds.hasOwnProperty(name) &&
this._translatedMessages.hasOwnProperty(this._placeholderToIds[name])) {
return this._translatedMessages[this._placeholderToIds[name]];
}
// TODO(vicb): better error message for when
// !this._translatedMessages.hasOwnProperty(this._placeholderToIds[name])
this._addError(
element, `The placeholder "${name}" does not exists in the source message`);
} }
break; break;
@ -169,23 +96,68 @@ class _Visitor implements ml.Visitor {
} }
} }
visitAttribute(attribute: ml.Attribute, context: any): any { visitAttribute(attribute: ml.Attribute, context: any): any {}
throw new Error('unreachable code');
}
visitText(text: ml.Text, context: any): any { return text.value; } visitText(text: ml.Text, context: any): any {}
visitComment(comment: ml.Comment, context: any): any { return ''; } visitComment(comment: ml.Comment, context: any): any {}
visitExpansion(expansion: ml.Expansion, context: any): any { visitExpansion(expansion: ml.Expansion, context: any): any {}
const strCases = expansion.cases.map(c => c.visit(this, null));
return `{${expansion.switchValue}, ${expansion.type}, strCases.join(' ')}`; visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
}
private _addError(node: ml.Node, message: string): void {
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any { this._errors.push(new I18nError(node.sourceSpan, message));
return `${expansionCase.value} {${ml.visitAll(this, expansionCase.expression, null)}}`; }
} }
// Convert ml nodes (xtb syntax) to i18n nodes
class XmlToI18n implements ml.Visitor {
private _errors: I18nError[];
convert(nodes: ml.Node[]) {
this._errors = [];
return {
i18nNodes: ml.visitAll(this, nodes),
errors: this._errors,
};
}
visitText(text: ml.Text, context: any) { return new i18n.Text(text.value, text.sourceSpan); }
visitExpansion(icu: ml.Expansion, context: any) {
const caseMap: {[value: string]: i18n.Node} = {};
ml.visitAll(this, icu.cases).forEach(c => {
caseMap[c.value] = new i18n.Container(c.nodes, icu.sourceSpan);
});
return new i18n.Icu(icu.switchValue, icu.type, caseMap, icu.sourceSpan);
}
visitExpansionCase(icuCase: ml.ExpansionCase, context: any): any {
return {
value: icuCase.value,
nodes: ml.visitAll(this, icuCase.expression),
};
}
visitElement(el: ml.Element, context: any): i18n.Placeholder {
if (el.name === _PLACEHOLDER_TAG) {
const nameAttr = el.attrs.find((attr) => attr.name === 'name');
if (nameAttr) {
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
}
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
} else {
this._addError(el, `Unexpected tag`);
}
}
visitComment(comment: ml.Comment, context: any) {}
visitAttribute(attribute: ml.Attribute, context: any) {}
private _addError(node: ml.Node, message: string): void { private _addError(node: ml.Node, message: string): void {
this._errors.push(new I18nError(node.sourceSpan, message)); this._errors.push(new I18nError(node.sourceSpan, message));

View File

@ -7,22 +7,120 @@
*/ */
import * as html from '../ml_parser/ast'; import * as html from '../ml_parser/ast';
import {HtmlParser} from '../ml_parser/html_parser';
import {MessageBundle} from './message_bundle'; import * as i18n from './i18n_ast';
import {I18nError} from './parse_util';
import {Serializer} from './serializers/serializer'; import {Serializer} from './serializers/serializer';
/** /**
* A container for translated messages * A container for translated messages
*/ */
export class TranslationBundle { export class TranslationBundle {
constructor(private _messageMap: {[id: string]: html.Node[]} = {}) {} private _i18nToHtml: I18nToHtmlVisitor;
static load(content: string, url: string, messageBundle: MessageBundle, serializer: Serializer): constructor(
TranslationBundle { private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {},
return new TranslationBundle(serializer.load(content, url, messageBundle)); public digest: (m: i18n.Message) => string) {
this._i18nToHtml = new I18nToHtmlVisitor(_i18nNodesByMsgId, digest);
} }
get(id: string): html.Node[] { return this._messageMap[id]; } static load(content: string, url: string, serializer: Serializer): TranslationBundle {
const i18nNodesByMsgId = serializer.load(content, url);
const digestFn = (m: i18n.Message) => serializer.digest(m);
return new TranslationBundle(i18nNodesByMsgId, digestFn);
}
has(id: string): boolean { return id in this._messageMap; } get(srcMsg: i18n.Message): html.Node[] {
const html = this._i18nToHtml.convert(srcMsg);
if (html.errors.length) {
throw new Error(html.errors.join('\n'));
}
return html.nodes;
}
has(srcMsg: i18n.Message): boolean { return this.digest(srcMsg) in this._i18nNodesByMsgId; }
}
class I18nToHtmlVisitor implements i18n.Visitor {
private _srcMsg: i18n.Message;
private _srcMsgStack: i18n.Message[] = [];
private _errors: I18nError[] = [];
constructor(
private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {},
private _digest: (m: i18n.Message) => string) {}
convert(srcMsg: i18n.Message): {nodes: html.Node[], errors: I18nError[]} {
this._srcMsgStack.length = 0;
this._errors.length = 0;
// i18n to text
const text = this._convertToText(srcMsg);
// text to html
const url = srcMsg.nodes[0].sourceSpan.start.file.url;
const html = new HtmlParser().parse(text, url, true);
return {
nodes: html.rootNodes,
errors: [...this._errors, ...html.errors],
};
}
visitText(text: i18n.Text, context?: any): string { return text.value; }
visitContainer(container: i18n.Container, context?: any): any {
return container.children.map(n => n.visit(this)).join('');
}
visitIcu(icu: i18n.Icu, context?: any): any {
const cases = Object.keys(icu.cases).map(k => `${k} {${icu.cases[k].visit(this)}}`);
// TODO(vicb): Once all format switch to using expression placeholders
// we should throw when the placeholder is not in the source message
const exp = this._srcMsg.placeholders.hasOwnProperty(icu.expression) ?
this._srcMsg.placeholders[icu.expression] :
icu.expression;
return `{${exp}, ${icu.type}, ${cases.join(' ')}}`;
}
visitPlaceholder(ph: i18n.Placeholder, context?: any): string {
const phName = ph.name;
if (this._srcMsg.placeholders.hasOwnProperty(phName)) {
return this._srcMsg.placeholders[phName];
}
if (this._srcMsg.placeholderToMessage.hasOwnProperty(phName)) {
return this._convertToText(this._srcMsg.placeholderToMessage[phName]);
}
this._addError(ph, `Unknown placeholder`);
return '';
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): any { throw 'unreachable code'; }
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { throw 'unreachable code'; }
private _convertToText(srcMsg: i18n.Message): string {
const digest = this._digest(srcMsg);
if (this._i18nNodesByMsgId.hasOwnProperty(digest)) {
this._srcMsgStack.push(this._srcMsg);
this._srcMsg = srcMsg;
const nodes = this._i18nNodesByMsgId[digest];
const text = nodes.map(node => node.visit(this)).join('');
this._srcMsg = this._srcMsgStack.pop();
return text;
}
this._addError(srcMsg.nodes[0], `Missing translation for message ${digest}`);
return '';
}
private _addError(el: i18n.Node, msg: string) {
this._errors.push(new I18nError(el.sourceSpan, msg));
}
} }

View File

@ -146,97 +146,43 @@ export class CompileMetadataResolver {
return; return;
} }
directiveType = resolveForwardRef(directiveType); directiveType = resolveForwardRef(directiveType);
const dirMeta = this._directiveResolver.resolve(directiveType); const nonNormalizedMetadata = this.getNonNormalizedDirectiveMetadata(directiveType);
if (!dirMeta) {
return null;
}
let moduleUrl = staticTypeModuleUrl(directiveType);
const createDirectiveMetadata = (templateMeta: cpl.CompileTemplateMetadata) => { const createDirectiveMetadata = (templateMetadata: cpl.CompileTemplateMetadata) => {
let changeDetectionStrategy: ChangeDetectionStrategy = null; const normalizedDirMeta = new cpl.CompileDirectiveMetadata({
let viewProviders: Array<cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]> = []; type: nonNormalizedMetadata.type,
let entryComponentMetadata: cpl.CompileIdentifierMetadata[] = []; isComponent: nonNormalizedMetadata.isComponent,
let selector = dirMeta.selector; selector: nonNormalizedMetadata.selector,
exportAs: nonNormalizedMetadata.exportAs,
if (dirMeta instanceof Component) { changeDetection: nonNormalizedMetadata.changeDetection,
// Component inputs: nonNormalizedMetadata.inputs,
changeDetectionStrategy = dirMeta.changeDetection; outputs: nonNormalizedMetadata.outputs,
if (dirMeta.viewProviders) { hostListeners: nonNormalizedMetadata.hostListeners,
viewProviders = this._getProvidersMetadata( hostProperties: nonNormalizedMetadata.hostProperties,
dirMeta.viewProviders, entryComponentMetadata, hostAttributes: nonNormalizedMetadata.hostAttributes,
`viewProviders for "${stringify(directiveType)}"`); providers: nonNormalizedMetadata.providers,
} viewProviders: nonNormalizedMetadata.viewProviders,
if (dirMeta.entryComponents) { queries: nonNormalizedMetadata.queries,
entryComponentMetadata = viewQueries: nonNormalizedMetadata.viewQueries,
flattenAndDedupeArray(dirMeta.entryComponents) entryComponents: nonNormalizedMetadata.entryComponents,
.map((type) => this._getIdentifierMetadata(type, staticTypeModuleUrl(type))) template: templateMetadata
.concat(entryComponentMetadata);
}
if (!selector) {
selector = this._schemaRegistry.getDefaultComponentElementName();
}
} else {
// Directive
if (!selector) {
throw new Error(`Directive ${stringify(directiveType)} has no selector, please add it!`);
}
}
let providers: Array<cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]> = [];
if (isPresent(dirMeta.providers)) {
providers = this._getProvidersMetadata(
dirMeta.providers, entryComponentMetadata,
`providers for "${stringify(directiveType)}"`);
}
let queries: cpl.CompileQueryMetadata[] = [];
let viewQueries: cpl.CompileQueryMetadata[] = [];
if (isPresent(dirMeta.queries)) {
queries = this._getQueriesMetadata(dirMeta.queries, false, directiveType);
viewQueries = this._getQueriesMetadata(dirMeta.queries, true, directiveType);
}
const meta = cpl.CompileDirectiveMetadata.create({
selector: selector,
exportAs: dirMeta.exportAs,
isComponent: !!templateMeta,
type: this._getTypeMetadata(directiveType, moduleUrl),
template: templateMeta,
changeDetection: changeDetectionStrategy,
inputs: dirMeta.inputs,
outputs: dirMeta.outputs,
host: dirMeta.host,
providers: providers,
viewProviders: viewProviders,
queries: queries,
viewQueries: viewQueries,
entryComponents: entryComponentMetadata
}); });
this._directiveCache.set(directiveType, meta); this._directiveCache.set(directiveType, normalizedDirMeta);
this._directiveSummaryCache.set(directiveType, meta.toSummary()); this._directiveSummaryCache.set(directiveType, normalizedDirMeta.toSummary());
return meta; return normalizedDirMeta;
}; };
if (dirMeta instanceof Component) { if (nonNormalizedMetadata.isComponent) {
// component
moduleUrl = componentModuleUrl(this._reflector, directiveType, dirMeta);
assertArrayOfStrings('styles', dirMeta.styles);
assertArrayOfStrings('styleUrls', dirMeta.styleUrls);
assertInterpolationSymbols('interpolation', dirMeta.interpolation);
const animations = dirMeta.animations ?
dirMeta.animations.map(e => this.getAnimationEntryMetadata(e)) :
null;
const templateMeta = this._directiveNormalizer.normalizeTemplate({ const templateMeta = this._directiveNormalizer.normalizeTemplate({
componentType: directiveType, componentType: directiveType,
moduleUrl: moduleUrl, moduleUrl: nonNormalizedMetadata.type.moduleUrl,
encapsulation: dirMeta.encapsulation, encapsulation: nonNormalizedMetadata.template.encapsulation,
template: dirMeta.template, template: nonNormalizedMetadata.template.template,
templateUrl: dirMeta.templateUrl, templateUrl: nonNormalizedMetadata.template.templateUrl,
styles: dirMeta.styles, styles: nonNormalizedMetadata.template.styles,
styleUrls: dirMeta.styleUrls, styleUrls: nonNormalizedMetadata.template.styleUrls,
animations: animations, animations: nonNormalizedMetadata.template.animations,
interpolation: dirMeta.interpolation interpolation: nonNormalizedMetadata.template.interpolation
}); });
if (templateMeta.syncResult) { if (templateMeta.syncResult) {
createDirectiveMetadata(templateMeta.syncResult); createDirectiveMetadata(templateMeta.syncResult);
@ -254,6 +200,96 @@ export class CompileMetadataResolver {
} }
} }
getNonNormalizedDirectiveMetadata(directiveType: any): cpl.CompileDirectiveMetadata {
directiveType = resolveForwardRef(directiveType);
const dirMeta = this._directiveResolver.resolve(directiveType);
if (!dirMeta) {
return null;
}
let moduleUrl = staticTypeModuleUrl(directiveType);
let nonNormalizedTemplateMetadata: cpl.CompileTemplateMetadata;
if (dirMeta instanceof Component) {
// component
moduleUrl = componentModuleUrl(this._reflector, directiveType, dirMeta);
assertArrayOfStrings('styles', dirMeta.styles);
assertArrayOfStrings('styleUrls', dirMeta.styleUrls);
assertInterpolationSymbols('interpolation', dirMeta.interpolation);
const animations = dirMeta.animations ?
dirMeta.animations.map(e => this.getAnimationEntryMetadata(e)) :
null;
nonNormalizedTemplateMetadata = new cpl.CompileTemplateMetadata({
encapsulation: dirMeta.encapsulation,
template: dirMeta.template,
templateUrl: dirMeta.templateUrl,
styles: dirMeta.styles,
styleUrls: dirMeta.styleUrls,
animations: animations,
interpolation: dirMeta.interpolation
});
}
let changeDetectionStrategy: ChangeDetectionStrategy = null;
let viewProviders: Array<cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]> = [];
let entryComponentMetadata: cpl.CompileIdentifierMetadata[] = [];
let selector = dirMeta.selector;
if (dirMeta instanceof Component) {
// Component
changeDetectionStrategy = dirMeta.changeDetection;
if (dirMeta.viewProviders) {
viewProviders = this._getProvidersMetadata(
dirMeta.viewProviders, entryComponentMetadata,
`viewProviders for "${stringify(directiveType)}"`);
}
if (dirMeta.entryComponents) {
entryComponentMetadata =
flattenAndDedupeArray(dirMeta.entryComponents)
.map((type) => this._getIdentifierMetadata(type, staticTypeModuleUrl(type)))
.concat(entryComponentMetadata);
}
if (!selector) {
selector = this._schemaRegistry.getDefaultComponentElementName();
}
} else {
// Directive
if (!selector) {
throw new Error(`Directive ${stringify(directiveType)} has no selector, please add it!`);
}
}
let providers: Array<cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]> = [];
if (isPresent(dirMeta.providers)) {
providers = this._getProvidersMetadata(
dirMeta.providers, entryComponentMetadata, `providers for "${stringify(directiveType)}"`);
}
let queries: cpl.CompileQueryMetadata[] = [];
let viewQueries: cpl.CompileQueryMetadata[] = [];
if (isPresent(dirMeta.queries)) {
queries = this._getQueriesMetadata(dirMeta.queries, false, directiveType);
viewQueries = this._getQueriesMetadata(dirMeta.queries, true, directiveType);
}
return cpl.CompileDirectiveMetadata.create({
selector: selector,
exportAs: dirMeta.exportAs,
isComponent: !!nonNormalizedTemplateMetadata,
type: this._getTypeMetadata(directiveType, moduleUrl),
template: nonNormalizedTemplateMetadata,
changeDetection: changeDetectionStrategy,
inputs: dirMeta.inputs,
outputs: dirMeta.outputs,
host: dirMeta.host,
providers: providers,
viewProviders: viewProviders,
queries: queries,
viewQueries: viewQueries,
entryComponents: entryComponentMetadata
});
}
/** /**
* Gets the metadata for the given directive. * Gets the metadata for the given directive.
* This assumes `loadNgModuleMetadata` has been called first. * This assumes `loadNgModuleMetadata` has been called first.
@ -309,11 +345,20 @@ export class CompileMetadataResolver {
loadNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true): loadNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true):
{ngModule: cpl.CompileNgModuleMetadata, loading: Promise<any>} { {ngModule: cpl.CompileNgModuleMetadata, loading: Promise<any>} {
const ngModule = this._loadNgModuleMetadata(moduleType, isSync, throwIfNotFound); const ngModule = this._loadNgModuleMetadata(moduleType, isSync, throwIfNotFound);
const loading = const loading = ngModule ?
ngModule ? Promise.all(ngModule.transitiveModule.loadingPromises) : Promise.resolve(null); Promise.all(ngModule.transitiveModule.directiveLoaders.map(loader => loader())) :
Promise.resolve(null);
return {ngModule, loading}; return {ngModule, loading};
} }
/**
* Get the NgModule metadata without loading the directives.
*/
getUnloadedNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true):
cpl.CompileNgModuleMetadata {
return this._loadNgModuleMetadata(moduleType, isSync, throwIfNotFound);
}
private _loadNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true): private _loadNgModuleMetadata(moduleType: any, isSync: boolean, throwIfNotFound = true):
cpl.CompileNgModuleMetadata { cpl.CompileNgModuleMetadata {
moduleType = resolveForwardRef(moduleType); moduleType = resolveForwardRef(moduleType);
@ -396,10 +441,8 @@ export class CompileMetadataResolver {
transitiveModule.directives.push(declaredIdentifier); transitiveModule.directives.push(declaredIdentifier);
declaredDirectives.push(declaredIdentifier); declaredDirectives.push(declaredIdentifier);
this._addTypeToModule(declaredType, moduleType); this._addTypeToModule(declaredType, moduleType);
const loadingPromise = this._loadDirectiveMetadata(declaredType, isSync); transitiveModule.directiveLoaders.push(
if (loadingPromise) { () => this._loadDirectiveMetadata(declaredType, isSync));
transitiveModule.loadingPromises.push(loadingPromise);
}
} else if (this._pipeResolver.isPipe(declaredType)) { } else if (this._pipeResolver.isPipe(declaredType)) {
transitiveModule.pipesSet.add(declaredType); transitiveModule.pipesSet.add(declaredType);
transitiveModule.pipes.push(declaredIdentifier); transitiveModule.pipes.push(declaredIdentifier);
@ -525,10 +568,10 @@ export class CompileMetadataResolver {
const directives = const directives =
flattenArray(transitiveExportedModules.map((ngModule) => ngModule.exportedDirectives)); flattenArray(transitiveExportedModules.map((ngModule) => ngModule.exportedDirectives));
const pipes = flattenArray(transitiveExportedModules.map((ngModule) => ngModule.exportedPipes)); const pipes = flattenArray(transitiveExportedModules.map((ngModule) => ngModule.exportedPipes));
const loadingPromises = const directiveLoaders =
ListWrapper.flatten(transitiveExportedModules.map(ngModule => ngModule.loadingPromises)); ListWrapper.flatten(transitiveExportedModules.map(ngModule => ngModule.directiveLoaders));
return new cpl.TransitiveCompileNgModuleMetadata( return new cpl.TransitiveCompileNgModuleMetadata(
transitiveModules, providers, entryComponents, directives, pipes, loadingPromises); transitiveModules, providers, entryComponents, directives, pipes, directiveLoaders);
} }
private _getIdentifierMetadata(type: Type<any>, moduleUrl: string): private _getIdentifierMetadata(type: Type<any>, moduleUrl: string):
@ -584,20 +627,26 @@ export class CompileMetadataResolver {
return pipeSummary; return pipeSummary;
} }
private _loadPipeMetadata(pipeType: Type<any>): void { getOrLoadPipeMetadata(pipeType: any): cpl.CompilePipeMetadata {
pipeType = resolveForwardRef(pipeType); let pipeMeta = this._pipeCache.get(pipeType);
const pipeMeta = this._pipeResolver.resolve(pipeType);
if (!pipeMeta) { if (!pipeMeta) {
return null; pipeMeta = this._loadPipeMetadata(pipeType);
} }
return pipeMeta;
}
const meta = new cpl.CompilePipeMetadata({ private _loadPipeMetadata(pipeType: any): cpl.CompilePipeMetadata {
pipeType = resolveForwardRef(pipeType);
const pipeAnnotation = this._pipeResolver.resolve(pipeType);
const pipeMeta = new cpl.CompilePipeMetadata({
type: this._getTypeMetadata(pipeType, staticTypeModuleUrl(pipeType)), type: this._getTypeMetadata(pipeType, staticTypeModuleUrl(pipeType)),
name: pipeMeta.name, name: pipeAnnotation.name,
pure: pipeMeta.pure pure: pipeAnnotation.pure
}); });
this._pipeCache.set(pipeType, meta); this._pipeCache.set(pipeType, pipeMeta);
this._pipeSummaryCache.set(pipeType, meta.toSummary()); this._pipeSummaryCache.set(pipeType, pipeMeta.toSummary());
return pipeMeta;
} }
private _getDependenciesMetadata(typeOrFunc: Type<any>|Function, dependencies: any[]): private _getDependenciesMetadata(typeOrFunc: Type<any>|Function, dependencies: any[]):

View File

@ -27,17 +27,46 @@ export class SourceModule {
constructor(public fileUrl: string, public moduleUrl: string, public source: string) {} constructor(public fileUrl: string, public moduleUrl: string, public source: string) {}
} }
export interface NgAnalyzedModules {
ngModules: CompileNgModuleMetadata[];
ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>;
files: Array<{srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}>;
symbolsMissingModule?: StaticSymbol[];
}
// Returns all the source files and a mapping from modules to directives // Returns all the source files and a mapping from modules to directives
export function analyzeNgModules( export function analyzeNgModules(
programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean}, programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean},
metadataResolver: CompileMetadataResolver): Promise<{ metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>, const {ngModules, symbolsMissingModule} =
files: Array<{srcUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]}> _createNgModules(programStaticSymbols, options, metadataResolver);
}> { return _analyzeNgModules(ngModules, symbolsMissingModule);
return _loadNgModules(programStaticSymbols, options, metadataResolver).then(_analyzeNgModules);
} }
function _analyzeNgModules(ngModuleMetas: CompileNgModuleMetadata[]) {
export function analyzeAndValidateNgModules(
programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean},
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
const result = analyzeNgModules(programStaticSymbols, options, metadataResolver);
if (result.symbolsMissingModule && result.symbolsMissingModule.length) {
const messages = result.symbolsMissingModule.map(
s => `Cannot determine the module for class ${s.name} in ${s.filePath}!`);
throw new Error(messages.join('\n'));
}
return result;
}
// Wait for the directives in the given modules have been loaded
export function loadNgModuleDirectives(ngModules: CompileNgModuleMetadata[]) {
return Promise
.all(ListWrapper.flatten(ngModules.map(
(ngModule) => ngModule.transitiveModule.directiveLoaders.map(loader => loader()))))
.then(() => {});
}
function _analyzeNgModules(
ngModuleMetas: CompileNgModuleMetadata[],
symbolsMissingModule: StaticSymbol[]): NgAnalyzedModules {
const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>(); const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>();
ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule)); ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule));
const ngModuleByPipeOrDirective = new Map<StaticSymbol, CompileNgModuleMetadata>(); const ngModuleByPipeOrDirective = new Map<StaticSymbol, CompileNgModuleMetadata>();
@ -78,10 +107,11 @@ function _analyzeNgModules(ngModuleMetas: CompileNgModuleMetadata[]) {
}); });
return { return {
// map directive/pipe to module // map directive/pipe to module
ngModuleByPipeOrDirective, ngModuleByPipeOrDirective,
// list modules and directives for every source file // list modules and directives for every source file
files, files,
ngModules: ngModuleMetas, symbolsMissingModule
}; };
} }
@ -100,13 +130,14 @@ export class OfflineCompiler {
compileModules(staticSymbols: StaticSymbol[], options: {transitiveModules: boolean}): compileModules(staticSymbols: StaticSymbol[], options: {transitiveModules: boolean}):
Promise<SourceModule[]> { Promise<SourceModule[]> {
return analyzeNgModules(staticSymbols, options, this._metadataResolver) const {ngModuleByPipeOrDirective, files, ngModules} =
.then(({ngModuleByPipeOrDirective, files}) => { analyzeAndValidateNgModules(staticSymbols, options, this._metadataResolver);
const sourceModules = files.map( return loadNgModuleDirectives(ngModules).then(() => {
file => this._compileSrcFile( const sourceModules = files.map(
file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.ngModules)); file => this._compileSrcFile(
return ListWrapper.flatten(sourceModules); file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.ngModules));
}); return ListWrapper.flatten(sourceModules);
});
} }
private _compileSrcFile( private _compileSrcFile(
@ -328,22 +359,21 @@ function _splitTypescriptSuffix(path: string): string[] {
// Load the NgModules and check // Load the NgModules and check
// that all directives / pipes that are present in the program // that all directives / pipes that are present in the program
// are also declared by a module. // are also declared by a module.
function _loadNgModules( function _createNgModules(
programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean}, programStaticSymbols: StaticSymbol[], options: {transitiveModules: boolean},
metadataResolver: CompileMetadataResolver): Promise<CompileNgModuleMetadata[]> { metadataResolver: CompileMetadataResolver):
{ngModules: CompileNgModuleMetadata[], symbolsMissingModule: StaticSymbol[]} {
const ngModules = new Map<any, CompileNgModuleMetadata>(); const ngModules = new Map<any, CompileNgModuleMetadata>();
const programPipesAndDirectives: StaticSymbol[] = []; const programPipesAndDirectives: StaticSymbol[] = [];
const ngModulePipesAndDirective = new Set<StaticSymbol>(); const ngModulePipesAndDirective = new Set<StaticSymbol>();
const loadingPromises: Promise<any>[] = [];
const addNgModule = (staticSymbol: any) => { const addNgModule = (staticSymbol: any) => {
if (ngModules.has(staticSymbol)) { if (ngModules.has(staticSymbol)) {
return false; return false;
} }
const {ngModule, loading} = metadataResolver.loadNgModuleMetadata(staticSymbol, false, false); const ngModule = metadataResolver.getUnloadedNgModuleMetadata(staticSymbol, false, false);
if (ngModule) { if (ngModule) {
ngModules.set(ngModule.type.reference, ngModule); ngModules.set(ngModule.type.reference, ngModule);
loadingPromises.push(loading);
ngModule.declaredDirectives.forEach((dir) => ngModulePipesAndDirective.add(dir.reference)); ngModule.declaredDirectives.forEach((dir) => ngModulePipesAndDirective.add(dir.reference));
ngModule.declaredPipes.forEach((pipe) => ngModulePipesAndDirective.add(pipe.reference)); ngModule.declaredPipes.forEach((pipe) => ngModulePipesAndDirective.add(pipe.reference));
if (options.transitiveModules) { if (options.transitiveModules) {
@ -364,11 +394,5 @@ function _loadNgModules(
const symbolsMissingModule = const symbolsMissingModule =
programPipesAndDirectives.filter(s => !ngModulePipesAndDirective.has(s)); programPipesAndDirectives.filter(s => !ngModulePipesAndDirective.has(s));
if (symbolsMissingModule.length) { return {ngModules: Array.from(ngModules.values()), symbolsMissingModule};
const messages = symbolsMissingModule.map(
s => `Cannot determine the module for class ${s.name} in ${s.filePath}!`);
throw new Error(messages.join('\n'));
}
return Promise.all(loadingPromises).then(() => Array.from(ngModules.values()));
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {isBlank, isPrimitive, isStrictStringMap} from './facade/lang'; import {isPrimitive, isStrictStringMap} from './facade/lang';
export const MODULE_SUFFIX = ''; export const MODULE_SUFFIX = '';
@ -48,7 +48,7 @@ export function visitValue(value: any, visitor: ValueVisitor, context: any): any
return visitor.visitStringMap(<{[key: string]: any}>value, context); return visitor.visitStringMap(<{[key: string]: any}>value, context);
} }
if (isBlank(value) || isPrimitive(value)) { if (value == null || isPrimitive(value)) {
return visitor.visitPrimitive(value, context); return visitor.visitPrimitive(value, context);
} }

View File

@ -6,53 +6,101 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {describe, expect, it} from '@angular/core/testing/testing_internal'; import {computeMsgId, sha1} from '../../src/i18n/digest';
import {sha1} from '../../src/i18n/digest';
export function main(): void { export function main(): void {
describe('sha1', () => { describe('digest', () => {
it('should work on emnpty strings', describe('sha1', () => {
() => { expect(sha1('')).toEqual('da39a3ee5e6b4b0d3255bfef95601890afd80709'); }); it('should work on empty strings',
() => { expect(sha1('')).toEqual('da39a3ee5e6b4b0d3255bfef95601890afd80709'); });
it('should returns the sha1 of "hello world"', it('should returns the sha1 of "hello world"',
() => { expect(sha1('abc')).toEqual('a9993e364706816aba3e25717850c26c9cd0d89d'); }); () => { expect(sha1('abc')).toEqual('a9993e364706816aba3e25717850c26c9cd0d89d'); });
it('should returns the sha1 of unicode strings', it('should returns the sha1 of unicode strings',
() => { expect(sha1('你好,世界')).toEqual('3becb03b015ed48050611c8d7afe4b88f70d5a20'); }); () => { expect(sha1('你好,世界')).toEqual('3becb03b015ed48050611c8d7afe4b88f70d5a20'); });
it('should support arbitrary string size', () => { it('should support arbitrary string size', () => {
// node.js reference code: // node.js reference code:
// //
// var crypto = require('crypto'); // var crypto = require('crypto');
// //
// function sha1(string) { // function sha1(string) {
// var shasum = crypto.createHash('sha1'); // var shasum = crypto.createHash('sha1');
// shasum.update(string, 'utf8'); // shasum.update(string, 'utf8');
// return shasum.digest('hex', 'utf8'); // return shasum.digest('hex', 'utf8');
// } // }
// //
// var prefix = `你好,世界`; // var prefix = `你好,世界`;
// var result = sha1(prefix); // var result = sha1(prefix);
// for (var size = prefix.length; size < 5000; size += 101) { // for (var size = prefix.length; size < 5000; size += 101) {
// result = prefix + sha1(result); // result = prefix + sha1(result);
// while (result.length < size) { // while (result.length < size) {
// result += result; // result += result;
// } // }
// result = result.slice(-size); // result = result.slice(-size);
// } // }
// //
// console.log(sha1(result)); // console.log(sha1(result));
const prefix = `你好,世界`; const prefix = `你好,世界`;
let result = sha1(prefix); let result = sha1(prefix);
for (let size = prefix.length; size < 5000; size += 101) { for (let size = prefix.length; size < 5000; size += 101) {
result = prefix + sha1(result); result = prefix + sha1(result);
while (result.length < size) { while (result.length < size) {
result += result; result += result;
}
result = result.slice(-size);
} }
result = result.slice(-size); expect(sha1(result)).toEqual('24c2dae5c1ac6f604dbe670a60290d7ce6320b45');
} });
expect(sha1(result)).toEqual('24c2dae5c1ac6f604dbe670a60290d7ce6320b45'); });
describe('decimal fingerprint', () => {
it('should work on well known inputs w/o meaning', () => {
const fixtures: {[msg: string]: string} = {
' Spaced Out ': '3976450302996657536',
'Last Name': '4407559560004943843',
'First Name': '6028371114637047813',
'View': '2509141182388535183',
'START_BOLDNUMEND_BOLD of START_BOLDmillionsEND_BOLD': '29997634073898638',
'The customer\'s credit card was authorized for AMOUNT and passed all risk checks.':
'6836487644149622036',
'Hello world!': '3022994926184248873',
'Jalape\u00f1o': '8054366208386598941',
'The set of SET_NAME is {XXX, ...}.': '135956960462609535',
'NAME took a trip to DESTINATION.': '768490705511913603',
'by AUTHOR (YEAR)': '7036633296476174078',
'': '4416290763660062288',
};
Object.keys(fixtures).forEach(
msg => { expect(computeMsgId(msg, '')).toEqual(fixtures[msg]); });
});
it('should work on well known inputs with meaning', () => {
const fixtures: {[msg: string]: [string, string]} = {
'7790835225175622807': ['Last Name', 'Gmail UI'],
'1809086297585054940': ['First Name', 'Gmail UI'],
'3993998469942805487': ['View', 'Gmail UI'],
};
Object.keys(fixtures).forEach(
id => { expect(computeMsgId(fixtures[id][0], fixtures[id][1])).toEqual(id); });
});
it('should support arbitrary string size', () => {
const prefix = `你好,世界`;
let result = computeMsgId(prefix, '');
for (let size = prefix.length; size < 5000; size += 101) {
result = prefix + computeMsgId(result, '');
while (result.length < size) {
result += result;
}
result = result.slice(-size);
}
expect(computeMsgId(result, '')).toEqual('2122606631351252558');
});
}); });
}); });
} }

View File

@ -6,15 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {describe, expect, it} from '@angular/core/testing/testing_internal'; import {DEFAULT_INTERPOLATION_CONFIG, HtmlParser} from '@angular/compiler';
import {digestMessage, serializeNodes as serializeI18nNodes} from '../../src/i18n/digest'; import {digest, serializeNodes as serializeI18nNodes} from '../../src/i18n/digest';
import {extractMessages, mergeTranslations} from '../../src/i18n/extractor_merger'; import {extractMessages, mergeTranslations} from '../../src/i18n/extractor_merger';
import * as i18n from '../../src/i18n/i18n_ast'; import * as i18n from '../../src/i18n/i18n_ast';
import {TranslationBundle} from '../../src/i18n/translation_bundle'; import {TranslationBundle} from '../../src/i18n/translation_bundle';
import * as html from '../../src/ml_parser/ast'; import * as html from '../../src/ml_parser/ast';
import {HtmlParser} from '../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
import {serializeNodes as serializeHtmlNodes} from '../ml_parser/ast_serializer_spec'; import {serializeNodes as serializeHtmlNodes} from '../ml_parser/ast_serializer_spec';
export function main() { export function main() {
@ -94,9 +92,10 @@ export function main() {
], ],
[ [
[ [
'text', 'text', '<ph tag name="START_PARAGRAPH">html, <ph tag' +
'<ph tag name="START_PARAGRAPH">html, <ph tag name="START_BOLD_TEXT">nested</ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">', ' name="START_BOLD_TEXT">nested</ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">',
'<ph icu name="ICU">{count, plural, =0 {[<ph tag name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}</ph>', '<ph icu name="ICU">{count, plural, =0 {[<ph tag' +
' name="START_TAG_SPAN">html</ph name="CLOSE_TAG_SPAN">]}}</ph>',
'[<ph name="INTERPOLATION">interp</ph>]' '[<ph name="INTERPOLATION">interp</ph>]'
], ],
'', '' '', ''
@ -190,9 +189,8 @@ export function main() {
it('should extract from attributes in translatable elements', () => { it('should extract from attributes in translatable elements', () => {
expect(extract('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>')).toEqual([ expect(extract('<div i18n><p><b i18n-title="m|d" title="msg"></b></p></div>')).toEqual([
[ [
[ ['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' +
'<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">' ' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'],
],
'', '' '', ''
], ],
[['msg'], 'm', 'd'], [['msg'], 'm', 'd'],
@ -204,9 +202,8 @@ export function main() {
.toEqual([ .toEqual([
[['msg'], 'm', 'd'], [['msg'], 'm', 'd'],
[ [
[ ['<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph' +
'<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">' ' name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">'],
],
'', '' '', ''
], ],
]); ]);
@ -220,7 +217,8 @@ export function main() {
[['msg'], 'm', 'd'], [['msg'], 'm', 'd'],
[ [
[ [
'{count, plural, =0 {[<ph tag name="START_PARAGRAPH"><ph tag name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">]}}' '{count, plural, =0 {[<ph tag name="START_PARAGRAPH"><ph tag' +
' name="START_BOLD_TEXT"></ph name="CLOSE_BOLD_TEXT"></ph name="CLOSE_PARAGRAPH">]}}'
], ],
'', '' '', ''
], ],
@ -351,7 +349,9 @@ export function main() {
const HTML = `before<!-- i18n --><p>foo</p><span><i>bar</i></span><!-- /i18n -->after`; const HTML = `before<!-- i18n --><p>foo</p><span><i>bar</i></span><!-- /i18n -->after`;
expect(fakeTranslate(HTML)) expect(fakeTranslate(HTML))
.toEqual( .toEqual(
'before**<ph tag name="START_PARAGRAPH">foo</ph name="CLOSE_PARAGRAPH"><ph tag name="START_TAG_SPAN"><ph tag name="START_ITALIC_TEXT">bar</ph name="CLOSE_ITALIC_TEXT"></ph name="CLOSE_TAG_SPAN">**after'); 'before**[ph tag name="START_PARAGRAPH">foo[/ph name="CLOSE_PARAGRAPH">[ph tag' +
' name="START_TAG_SPAN">[ph tag name="START_ITALIC_TEXT">bar[/ph' +
' name="CLOSE_ITALIC_TEXT">[/ph name="CLOSE_TAG_SPAN">**after');
}); });
it('should merge nested blocks', () => { it('should merge nested blocks', () => {
@ -359,7 +359,9 @@ export function main() {
`<div>before<!-- i18n --><p>foo</p><span><i>bar</i></span><!-- /i18n -->after</div>`; `<div>before<!-- i18n --><p>foo</p><span><i>bar</i></span><!-- /i18n -->after</div>`;
expect(fakeTranslate(HTML)) expect(fakeTranslate(HTML))
.toEqual( .toEqual(
'<div>before**<ph tag name="START_PARAGRAPH">foo</ph name="CLOSE_PARAGRAPH"><ph tag name="START_TAG_SPAN"><ph tag name="START_ITALIC_TEXT">bar</ph name="CLOSE_ITALIC_TEXT"></ph name="CLOSE_TAG_SPAN">**after</div>'); '<div>before**[ph tag name="START_PARAGRAPH">foo[/ph name="CLOSE_PARAGRAPH">[ph' +
' tag name="START_TAG_SPAN">[ph tag name="START_ITALIC_TEXT">bar[/ph' +
' name="CLOSE_ITALIC_TEXT">[/ph name="CLOSE_TAG_SPAN">**after</div>');
}); });
}); });
@ -400,15 +402,15 @@ function fakeTranslate(
extractMessages(htmlNodes, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs) extractMessages(htmlNodes, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs)
.messages; .messages;
const i18nMsgMap: {[id: string]: html.Node[]} = {}; const i18nMsgMap: {[id: string]: i18n.Node[]} = {};
messages.forEach(message => { messages.forEach(message => {
const id = digestMessage(message); const id = digest(message);
const text = serializeI18nNodes(message.nodes).join(''); const text = serializeI18nNodes(message.nodes).join('').replace(/</g, '[');
i18nMsgMap[id] = [new html.Text(`**${text}**`, null)]; i18nMsgMap[id] = [new i18n.Text(`**${text}**`, null)];
}); });
const translations = new TranslationBundle(i18nMsgMap); const translations = new TranslationBundle(i18nMsgMap, digest);
const translatedNodes = const translatedNodes =
mergeTranslations( mergeTranslations(

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {digest} from '@angular/compiler/src/i18n/digest';
import {extractMessages} from '@angular/compiler/src/i18n/extractor_merger'; import {extractMessages} from '@angular/compiler/src/i18n/extractor_merger';
import {Message} from '@angular/compiler/src/i18n/i18n_ast'; import {Message} from '@angular/compiler/src/i18n/i18n_ast';
import {describe, expect, it} from '@angular/core/testing/testing_internal';
import {serializeNodes} from '../../src/i18n/digest'; import {serializeNodes} from '../../src/i18n/digest';
import {HtmlParser} from '../../src/ml_parser/html_parser'; import {HtmlParser} from '../../src/ml_parser/html_parser';
@ -272,11 +272,14 @@ export function main() {
[['{count, plural, =1 {[1]}}'], '', ''], [['{count, plural, =1 {[1]}}'], '', ''],
]); ]);
// ICU message placeholders are reference to translations. expect(_humanizePlaceholders(html)).toEqual([
// As such they have no static content but refs to message ids. '',
expect(_humanizePlaceholders(html)).toEqual(['', '', '', '']); 'VAR_PLURAL=count',
'VAR_PLURAL=count',
'VAR_PLURAL=count',
]);
expect(_humanizePlaceholdersToIds(html)).toEqual([ expect(_humanizePlaceholdersToMessage(html)).toEqual([
'ICU=f0f76923009914f1b05f41042a5c7231b9496504, ICU_1=73693d1f78d0fc882f0bcbce4cb31a0aa1995cfe', 'ICU=f0f76923009914f1b05f41042a5c7231b9496504, ICU_1=73693d1f78d0fc882f0bcbce4cb31a0aa1995cfe',
'', '',
'', '',
@ -308,13 +311,13 @@ function _humanizePlaceholders(
// clang-format on // clang-format on
} }
function _humanizePlaceholdersToIds( function _humanizePlaceholdersToMessage(
html: string, implicitTags: string[] = [], html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): string[] { implicitAttrs: {[k: string]: string[]} = {}): string[] {
// clang-format off // clang-format off
// https://github.com/angular/clang-format/issues/35 // https://github.com/angular/clang-format/issues/35
return _extractMessages(html, implicitTags, implicitAttrs).map( return _extractMessages(html, implicitTags, implicitAttrs).map(
msg => Object.keys(msg.placeholderToMsgIds).map(k => `${k}=${msg.placeholderToMsgIds[k]}`).join(', ')); msg => Object.keys(msg.placeholderToMessage).map(k => `${k}=${digest(msg.placeholderToMessage[k])}`).join(', '));
// clang-format on // clang-format on
} }

View File

@ -43,6 +43,9 @@ export function main() {
expectHtml(el, '#i18n-2').toBe('<div id="i18n-2"><p>imbriqué</p></div>'); expectHtml(el, '#i18n-2').toBe('<div id="i18n-2"><p>imbriqué</p></div>');
expectHtml(el, '#i18n-3') expectHtml(el, '#i18n-3')
.toBe('<div id="i18n-3"><p><i>avec des espaces réservés</i></p></div>'); .toBe('<div id="i18n-3"><p><i>avec des espaces réservés</i></p></div>');
expectHtml(el, '#i18n-3b')
.toBe(
'<div id="i18n-3b"><p><i class="preserved-on-placeholders">avec des espaces réservés</i></p></div>');
expectHtml(el, '#i18n-4') expectHtml(el, '#i18n-4')
.toBe('<p id="i18n-4" title="sur des balises non traductibles"></p>'); .toBe('<p id="i18n-4" title="sur des balises non traductibles"></p>');
expectHtml(el, '#i18n-5').toBe('<p id="i18n-5" title="sur des balises traductibles"></p>'); expectHtml(el, '#i18n-5').toBe('<p id="i18n-5" title="sur des balises traductibles"></p>');
@ -66,8 +69,10 @@ export function main() {
expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('beaucoup'); expect(el.query(By.css('#i18n-14')).nativeElement).toHaveText('beaucoup');
cmp.sex = 'm'; cmp.sex = 'm';
cmp.sexB = 'f';
tb.detectChanges(); tb.detectChanges();
expect(el.query(By.css('#i18n-8')).nativeElement).toHaveText('homme'); expect(el.query(By.css('#i18n-8')).nativeElement).toHaveText('homme');
expect(el.query(By.css('#i18n-8b')).nativeElement).toHaveText('femme');
cmp.sex = 'f'; cmp.sex = 'f';
tb.detectChanges(); tb.detectChanges();
expect(el.query(By.css('#i18n-8')).nativeElement).toHaveText('femme'); expect(el.query(By.css('#i18n-8')).nativeElement).toHaveText('femme');
@ -106,6 +111,7 @@ function expectHtml(el: DebugElement, cssSelector: string): any {
<div id="i18n-2"><p i18n="different meaning|">nested</p></div> <div id="i18n-2"><p i18n="different meaning|">nested</p></div>
<div id="i18n-3"><p i18n><i>with placeholders</i></p></div> <div id="i18n-3"><p i18n><i>with placeholders</i></p></div>
<div id="i18n-3b"><p i18n><i class="preserved-on-placeholders">with placeholders</i></p></div>
<div> <div>
<p id="i18n-4" i18n-title title="on not translatable node"></p> <p id="i18n-4" i18n-title title="on not translatable node"></p>
@ -117,7 +123,10 @@ function expectHtml(el: DebugElement, cssSelector: string): any {
<div i18n id="i18n-7">{count, plural, =0 {zero} =1 {one} =2 {two} other {<b>many</b>}}</div> <div i18n id="i18n-7">{count, plural, =0 {zero} =1 {one} =2 {two} other {<b>many</b>}}</div>
<div i18n id="i18n-8"> <div i18n id="i18n-8">
{sex, sex, m {male} f {female}} {sex, select, m {male} f {female}}
</div>
<div i18n id="i18n-8b">
{sexB, select, m {male} f {female}}
</div> </div>
<div i18n id="i18n-9">{{ "count = " + count }}</div> <div i18n id="i18n-9">{{ "count = " + count }}</div>
@ -135,8 +144,9 @@ function expectHtml(el: DebugElement, cssSelector: string): any {
` `
}) })
class I18nComponent { class I18nComponent {
count: number = 0; count: number;
sex: string = 'm'; sex: string;
sexB: string;
} }
class FrLocalization extends NgLocalization { class FrLocalization extends NgLocalization {
@ -153,51 +163,52 @@ class FrLocalization extends NgLocalization {
const XTB = ` const XTB = `
<translationbundle> <translationbundle>
<translation id="3cb04208df1c2f62553ed48e75939cf7107f9dad">attributs i18n sur les balises</translation> <translation id="615790887472569365">attributs i18n sur les balises</translation>
<translation id="52895b1221effb3f3585b689f049d2784d714952">imbriqué</translation> <translation id="3707494640264351337">imbriqué</translation>
<translation id="88d5f22050a9df477ee5646153558b3a4862d47e">imbriqué</translation> <translation id="5539162898278769904">imbriqué</translation>
<translation id="34fec9cc62e28e8aa6ffb306fa8569ef0a8087fe"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation> <translation id="3780349238193953556"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation>
<translation id="1fe4616cce80a57c7707bac1c97054aa8e244a67">sur des balises non traductibles</translation> <translation id="5525133077318024839">sur des balises non traductibles</translation>
<translation id="67162b5af5f15fd0eb6480c88688dafdf952b93a">sur des balises traductibles</translation> <translation id="8670732454866344690">sur des balises traductibles</translation>
<translation id="dc5536bb9e0e07291c185a0d306601a2ecd4813f">{count, plural, =0 {zero} =1 {un} =2 {deux} other {<ph name="START_BOLD_TEXT"/>beaucoup<ph name="CLOSE_BOLD_TEXT"/>}}</translation> <translation id="4593805537723189714">{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {<ph name="START_BOLD_TEXT"/>beaucoup<ph name="CLOSE_BOLD_TEXT"/>}}</translation>
<translation id="018efa03821ca41e27611e4a584736810d56ed8a"><ph name="ICU"/></translation> <translation id="1746565782635215"><ph name="ICU"/></translation>
<translation id="fd3186ad2a9aa801fe072ddb16ca34cd98ae93da">{sex, sex, m {homme} f {femme}}</translation> <translation id="5868084092545682515">{VAR_SELECT, select, m {homme} f {femme}}</translation>
<translation id="d9879678f727b244bc7c7e20f22b63d98cb14890"><ph name="INTERPOLATION"/></translation> <translation id="4851788426695310455"><ph name="INTERPOLATION"/></translation>
<translation id="50dac33dc6fc0578884baac79d875785ed77c928">sexe = <ph name="INTERPOLATION"/></translation> <translation id="9013357158046221374">sexe = <ph name="INTERPOLATION"/></translation>
<translation id="a46f833b1fe6ca49e8b97c18f4b7ea0b930c9383"><ph name="CUSTOM_NAME"/></translation> <translation id="8324617391167353662"><ph name="CUSTOM_NAME"/></translation>
<translation id="2ec983b4893bcd5b24af33bebe3ecba63868453c">dans une section traductible</translation> <translation id="7685649297917455806">dans une section traductible</translation>
<translation id="eee74a5be8a75881a4785905bd8302a71f7d9f75"> <translation id="2387287228265107305">
<ph name="START_HEADING_LEVEL1"/>Balises dans les commentaires html<ph name="CLOSE_HEADING_LEVEL1"/> <ph name="START_HEADING_LEVEL1"/>Balises dans les commentaires html<ph name="CLOSE_HEADING_LEVEL1"/>
<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/> <ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/>
<ph name="START_TAG_DIV_1"/><ph name="ICU"/><ph name="CLOSE_TAG_DIV"></ph> <ph name="START_TAG_DIV_1"/><ph name="ICU"/><ph name="CLOSE_TAG_DIV"></ph>
</translation> </translation>
<translation id="93a30c67d4e6c9b37aecfe2ac0f2b5d366d7b520">ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation> <translation id="1491627405349178954">ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation>
</translationbundle>`; </translationbundle>`;
// unused, for reference only // unused, for reference only
// can be generated from xmb_spec as follow: // can be generated from xmb_spec as follow:
// `iit('extract xmb', () => { console.log(toXmb(HTML)); });` // `fit('extract xmb', () => { console.log(toXmb(HTML)); });`
const XMB = ` const XMB = `
<messagebundle> <messagebundle>
<msg id="3cb04208df1c2f62553ed48e75939cf7107f9dad">i18n attribute on tags</msg> <msg id="615790887472569365">i18n attribute on tags</msg>
<msg id="52895b1221effb3f3585b689f049d2784d714952">nested</msg> <msg id="3707494640264351337">nested</msg>
<msg id="88d5f22050a9df477ee5646153558b3a4862d47e" meaning="different meaning">nested</msg> <msg id="5539162898278769904" meaning="different meaning">nested</msg>
<msg id="34fec9cc62e28e8aa6ffb306fa8569ef0a8087fe"><ph name="START_ITALIC_TEXT"><ex>&lt;i&gt;</ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex>&lt;/i&gt;</ex></ph></msg> <msg id="3780349238193953556"><ph name="START_ITALIC_TEXT"><ex>&lt;i&gt;</ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex>&lt;/i&gt;</ex></ph></msg>
<msg id="1fe4616cce80a57c7707bac1c97054aa8e244a67">on not translatable node</msg> <msg id="5525133077318024839">on not translatable node</msg>
<msg id="67162b5af5f15fd0eb6480c88688dafdf952b93a">on translatable node</msg> <msg id="8670732454866344690">on translatable node</msg>
<msg id="dc5536bb9e0e07291c185a0d306601a2ecd4813f">{count, plural, =0 {zero}=1 {one}=2 {two}other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>}}</msg> <msg id="4593805537723189714">{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph>} }</msg>
<msg id="018efa03821ca41e27611e4a584736810d56ed8a"> <msg id="1746565782635215">
<ph name="ICU"/> <ph name="ICU"/>
</msg> </msg>
<msg id="fd3186ad2a9aa801fe072ddb16ca34cd98ae93da">{sex, sex, m {male}f {female}}</msg> <msg id="5868084092545682515">{VAR_SELECT, select, m {male} f {female} }</msg>
<msg id="d9879678f727b244bc7c7e20f22b63d98cb14890"><ph name="INTERPOLATION"/></msg> <msg id="4851788426695310455"><ph name="INTERPOLATION"/></msg>
<msg id="50dac33dc6fc0578884baac79d875785ed77c928">sex = <ph name="INTERPOLATION"/></msg> <msg id="9013357158046221374">sex = <ph name="INTERPOLATION"/></msg>
<msg id="a46f833b1fe6ca49e8b97c18f4b7ea0b930c9383"><ph name="CUSTOM_NAME"/></msg> <msg id="8324617391167353662"><ph name="CUSTOM_NAME"/></msg>
<msg id="2ec983b4893bcd5b24af33bebe3ecba63868453c">in a translatable section</msg> <msg id="7685649297917455806">in a translatable section</msg>
<msg id="eee74a5be8a75881a4785905bd8302a71f7d9f75"> <msg id="2387287228265107305">
<ph name="START_HEADING_LEVEL1"><ex>&lt;h1&gt;</ex></ph>Markers in html comments<ph name="CLOSE_HEADING_LEVEL1"><ex>&lt;/h1&gt;</ex></ph> <ph name="START_HEADING_LEVEL1"><ex>&lt;h1&gt;</ex></ph>Markers in html comments<ph name="CLOSE_HEADING_LEVEL1"><ex>&lt;/h1&gt;</ex></ph>
<ph name="START_TAG_DIV"><ex>&lt;div&gt;</ex></ph><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph> <ph name="START_TAG_DIV"><ex>&lt;div&gt;</ex></ph><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph>
<ph name="START_TAG_DIV_1"><ex>&lt;div&gt;</ex></ph><ph name="ICU"/><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph> <ph name="START_TAG_DIV_1"><ex>&lt;div&gt;</ex></ph><ph name="ICU"/><ph name="CLOSE_TAG_DIV"><ex>&lt;/div&gt;</ex></ph>
</msg> </msg>
<msg id="93a30c67d4e6c9b37aecfe2ac0f2b5d366d7b520">it <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> work</msg> <msg id="1491627405349178954">it <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> work</msg>
</messagebundle>`; </messagebundle>
`;

View File

@ -6,12 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as i18n from '@angular/compiler/src/i18n/i18n_ast';
import {Serializer} from '@angular/compiler/src/i18n/serializers/serializer';
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {serializeNodes} from '../../src/i18n/digest'; import {serializeNodes} from '../../src/i18n/digest';
import * as i18n from '../../src/i18n/i18n_ast';
import {MessageBundle} from '../../src/i18n/message_bundle'; import {MessageBundle} from '../../src/i18n/message_bundle';
import {Serializer} from '../../src/i18n/serializers/serializer';
import {HtmlParser} from '../../src/ml_parser/html_parser'; import {HtmlParser} from '../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
@ -26,17 +24,18 @@ export function main(): void {
messages.updateFromTemplate( messages.updateFromTemplate(
'<p i18n="m|d">Translate Me</p>', 'url', DEFAULT_INTERPOLATION_CONFIG); '<p i18n="m|d">Translate Me</p>', 'url', DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeMessages(messages)).toEqual([ expect(humanizeMessages(messages)).toEqual([
'2e791a68a3324ecdd29e252198638dafacec46e9=Translate Me', 'Translate Me (m|d)',
]); ]);
}); });
it('should extract the same message with different meaning in different entries', () => { it('should extract the all messages and duplicates', () => {
messages.updateFromTemplate( messages.updateFromTemplate(
'<p i18n="m|d">Translate Me</p><p i18n>Translate Me</p>', 'url', '<p i18n="m|d">Translate Me</p><p i18n>Translate Me</p><p i18n>Translate Me</p>', 'url',
DEFAULT_INTERPOLATION_CONFIG); DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeMessages(messages)).toEqual([ expect(humanizeMessages(messages)).toEqual([
'2e791a68a3324ecdd29e252198638dafacec46e9=Translate Me', 'Translate Me (m|d)',
'8ca133f957845af1b1868da1b339180d1f519644=Translate Me', 'Translate Me (|)',
'Translate Me (|)',
]); ]);
}); });
}); });
@ -44,13 +43,14 @@ export function main(): void {
} }
class _TestSerializer implements Serializer { class _TestSerializer implements Serializer {
write(messageMap: {[id: string]: i18n.Message}): string { write(messages: i18n.Message[]): string {
return Object.keys(messageMap) return messages.map(msg => `${serializeNodes(msg.nodes)} (${msg.meaning}|${msg.description})`)
.map(id => `${id}=${serializeNodes(messageMap[id].nodes)}`)
.join('//'); .join('//');
} }
load(content: string, url: string, placeholders: {}): {} { return null; } load(content: string, url: string): {} { return null; }
digest(msg: i18n.Message): string { return 'unused'; }
} }
function humanizeMessages(catalog: MessageBundle): string[] { function humanizeMessages(catalog: MessageBundle): string[] {

View File

@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {PlaceholderRegistry} from '../../../src/i18n/serializers/placeholder'; import {PlaceholderRegistry} from '../../../src/i18n/serializers/placeholder';
export function main(): void { export function main(): void {

View File

@ -6,12 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Xliff} from '@angular/compiler/src/i18n/serializers/xliff'; import {escapeRegExp} from '@angular/core/src/facade/lang';
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
import {serializeNodes} from '../../../src/i18n/digest';
import {MessageBundle} from '../../../src/i18n/message_bundle'; import {MessageBundle} from '../../../src/i18n/message_bundle';
import {Xliff} from '../../../src/i18n/serializers/xliff';
import {HtmlParser} from '../../../src/ml_parser/html_parser'; import {HtmlParser} from '../../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
import {serializeNodes} from '../../ml_parser/ast_serializer_spec';
const HTML = ` const HTML = `
<p i18n-title title="translatable attribute">not translatable</p> <p i18n-title title="translatable attribute">not translatable</p>
@ -77,8 +78,7 @@ const LOAD_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
`; `;
export function main(): void { export function main(): void {
let serializer: Xliff; const serializer = new Xliff();
let htmlParser: HtmlParser;
function toXliff(html: string): string { function toXliff(html: string): string {
const catalog = new MessageBundle(new HtmlParser, [], {}); const catalog = new MessageBundle(new HtmlParser, [], {});
@ -86,39 +86,130 @@ export function main(): void {
return catalog.write(serializer); return catalog.write(serializer);
} }
function loadAsText(template: string, xliff: string): {[id: string]: string} { function loadAsMap(xliff: string): {[id: string]: string} {
const messageBundle = new MessageBundle(htmlParser, [], {}); const i18nNodesByMsgId = serializer.load(xliff, 'url');
messageBundle.updateFromTemplate(template, 'url', DEFAULT_INTERPOLATION_CONFIG); const msgMap: {[id: string]: string} = {};
Object.keys(i18nNodesByMsgId)
.forEach(id => msgMap[id] = serializeNodes(i18nNodesByMsgId[id]).join(''));
const asAst = serializer.load(xliff, 'url', messageBundle); return msgMap;
const asText: {[id: string]: string} = {};
Object.keys(asAst).forEach(id => { asText[id] = serializeNodes(asAst[id]).join(''); });
return asText;
} }
describe('XLIFF serializer', () => { describe('XLIFF serializer', () => {
beforeEach(() => {
htmlParser = new HtmlParser();
serializer = new Xliff(htmlParser, DEFAULT_INTERPOLATION_CONFIG);
});
describe('write', () => { describe('write', () => {
it('should write a valid xliff file', () => { expect(toXliff(HTML)).toEqual(WRITE_XLIFF); }); it('should write a valid xliff file', () => { expect(toXliff(HTML)).toEqual(WRITE_XLIFF); });
}); });
describe('load', () => { describe('load', () => {
it('should load XLIFF files', () => { it('should load XLIFF files', () => {
expect(loadAsText(HTML, LOAD_XLIFF)).toEqual({ expect(loadAsMap(LOAD_XLIFF)).toEqual({
'983775b9a51ce14b036be72d4cfd65d68d64e231': 'etubirtta elbatalsnart', '983775b9a51ce14b036be72d4cfd65d68d64e231': 'etubirtta elbatalsnart',
'ec1d033f2436133c14ab038286c4f5df4697484a': 'ec1d033f2436133c14ab038286c4f5df4697484a':
'{{ interpolation}} footnemele elbatalsnart <b>sredlohecalp htiw</b>', '<ph name="INTERPOLATION"/> footnemele elbatalsnart <ph name="START_BOLD_TEXT"/>sredlohecalp htiw<ph name="CLOSE_BOLD_TEXT"/>',
'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': 'oof', 'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': 'oof',
'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d': '<div></div><img/><br/>', 'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d':
'<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/><ph name="TAG_IMG"/><ph name="LINE_BREAK"/>',
}); });
}); });
describe('structure errors', () => {
it('should throw when a trans-unit has no translation', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="missingtarget">
<source/>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(/Message missingtarget misses a translation/);
});
it('should throw when a trans-unit has no id attribute', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit datatype="html">
<source/>
<target/>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(/<trans-unit> misses the "id" attribute/);
});
it('should throw on duplicate trans-unit id', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="deadbeef">
<source/>
<target/>
</trans-unit>
<trans-unit id="deadbeef">
<source/>
<target/>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(/Duplicated translations for msg deadbeef/);
});
});
describe('message errors', () => {
it('should throw on unknown message tags', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="deadbeef" datatype="html">
<source/>
<target><b>msg should contain only ph tags</b></target>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => { loadAsMap(XLIFF); })
.toThrowError(
new RegExp(escapeRegExp(`[ERROR ->]<b>msg should contain only ph tags</b>`)));
});
it('should throw when a placeholder misses an id attribute', () => {
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="deadbeef" datatype="html">
<source/>
<target><x/></target>
</trans-unit>
</body>
</file>
</xliff>`;
expect(() => {
loadAsMap(XLIFF);
}).toThrowError(new RegExp(escapeRegExp(`<x> misses the "id" attribute`)));
});
});
}); });
}); });
} }

View File

@ -43,10 +43,10 @@ export function main(): void {
<!ELEMENT ex (#PCDATA)> <!ELEMENT ex (#PCDATA)>
]> ]>
<messagebundle> <messagebundle>
<msg id="ec1d033f2436133c14ab038286c4f5df4697484a">translatable element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></msg> <msg id="7056919470098446707">translatable element <ph name="START_BOLD_TEXT"><ex>&lt;b&gt;</ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex>&lt;/b&gt;</ex></ph> <ph name="INTERPOLATION"/></msg>
<msg id="e2ccf3d131b15f54aa1fcf1314b1ca77c14bfcc2">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} }</msg> <msg id="2981514368455622387">{VAR_PLURAL, plural, =0 {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} }</msg>
<msg id="db3e0a6a5a96481f60aec61d98c3eecddef5ac23" desc="d" meaning="m">foo</msg> <msg id="7999024498831672133" desc="d" meaning="m">foo</msg>
<msg id="0e16a673a5a7a135c9f7b957ec2c5c6f6ee6e2c4">{ count, plural, =0 {{ sex, select, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} } } }</msg> <msg id="2015957479576096115">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex>&lt;p&gt;</ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex>&lt;/p&gt;</ex></ph>} } } }</msg>
</messagebundle> </messagebundle>
`; `;
@ -55,7 +55,7 @@ export function main(): void {
it('should throw when trying to load an xmb file', () => { it('should throw when trying to load an xmb file', () => {
expect(() => { expect(() => {
const serializer = new Xmb(); const serializer = new Xmb();
serializer.load(XMB, 'url', null); serializer.load(XMB, 'url');
}).toThrowError(/Unsupported/); }).toThrowError(/Unsupported/);
}); });
}); });

View File

@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {describe, expect, it} from '@angular/core/testing/testing_internal';
import * as xml from '../../../src/i18n/serializers/xml_helper'; import * as xml from '../../../src/i18n/serializers/xml_helper';
export function main(): void { export function main(): void {

View File

@ -8,37 +8,24 @@
import {escapeRegExp} from '@angular/core/src/facade/lang'; import {escapeRegExp} from '@angular/core/src/facade/lang';
import {MessageBundle} from '../../../src/i18n/message_bundle'; import {serializeNodes} from '../../../src/i18n/digest';
import {Xtb} from '../../../src/i18n/serializers/xtb'; import {Xtb} from '../../../src/i18n/serializers/xtb';
import {HtmlParser} from '../../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
import {serializeNodes} from '../../ml_parser/ast_serializer_spec';
export function main(): void { export function main(): void {
describe('XTB serializer', () => { describe('XTB serializer', () => {
let serializer: Xtb; const serializer = new Xtb();
let htmlParser: HtmlParser;
function loadAsText(template: string, xtb: string): {[id: string]: string} { function loadAsMap(xtb: string): {[id: string]: string} {
const messageBundle = new MessageBundle(htmlParser, [], {}); const i18nNodesByMsgId = serializer.load(xtb, 'url');
messageBundle.updateFromTemplate(template, 'url', DEFAULT_INTERPOLATION_CONFIG); const msgMap: {[id: string]: string} = {};
Object.keys(i18nNodesByMsgId).forEach(id => {
const asAst = serializer.load(xtb, 'url', messageBundle); msgMap[id] = serializeNodes(i18nNodesByMsgId[id]).join('');
const asText: {[id: string]: string} = {}; });
Object.keys(asAst).forEach(id => { asText[id] = serializeNodes(asAst[id]).join(''); }); return msgMap;
return asText;
} }
beforeEach(() => {
htmlParser = new HtmlParser();
serializer = new Xtb(htmlParser, DEFAULT_INTERPOLATION_CONFIG);
});
describe('load', () => { describe('load', () => {
it('should load XTB files with a doctype', () => { it('should load XTB files with a doctype', () => {
const HTML = `<div i18n>bar</div>`;
const XTB = `<?xml version="1.0" encoding="UTF-8"?> const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE translationbundle [<!ELEMENT translationbundle (translation)*> <!DOCTYPE translationbundle [<!ELEMENT translationbundle (translation)*>
<!ATTLIST translationbundle lang CDATA #REQUIRED> <!ATTLIST translationbundle lang CDATA #REQUIRED>
@ -50,75 +37,66 @@ export function main(): void {
<!ATTLIST ph name CDATA #REQUIRED> <!ATTLIST ph name CDATA #REQUIRED>
]> ]>
<translationbundle> <translationbundle>
<translation id="28a86c8a00ae573b2bac698d6609316dc7b4a226">rab</translation> <translation id="8841459487341224498">rab</translation>
</translationbundle>`; </translationbundle>`;
expect(loadAsText(HTML, XTB)).toEqual({'28a86c8a00ae573b2bac698d6609316dc7b4a226': 'rab'}); expect(loadAsMap(XTB)).toEqual({'8841459487341224498': 'rab'});
}); });
it('should load XTB files without placeholders', () => { it('should load XTB files without placeholders', () => {
const HTML = `<div i18n>bar</div>`;
const XTB = `<?xml version="1.0" encoding="UTF-8"?> const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<translationbundle> <translationbundle>
<translation id="28a86c8a00ae573b2bac698d6609316dc7b4a226">rab</translation> <translation id="8841459487341224498">rab</translation>
</translationbundle>`; </translationbundle>`;
expect(loadAsText(HTML, XTB)).toEqual({'28a86c8a00ae573b2bac698d6609316dc7b4a226': 'rab'}); expect(loadAsMap(XTB)).toEqual({'8841459487341224498': 'rab'});
}); });
it('should load XTB files with placeholders', () => {
const HTML = `<div i18n><p>bar</p></div>`;
it('should load XTB files with placeholders', () => {
const XTB = `<?xml version="1.0" encoding="UTF-8"?> const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<translationbundle> <translationbundle>
<translation id="7de4d8ff1e42b7b31da6204074818236a9a5317f"><ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/></translation> <translation id="8877975308926375834"><ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/></translation>
</translationbundle>`; </translationbundle>`;
expect(loadAsText(HTML, XTB)).toEqual({ expect(loadAsMap(XTB)).toEqual({
'7de4d8ff1e42b7b31da6204074818236a9a5317f': '<p>rab</p>' '8877975308926375834': '<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>'
}); });
}); });
it('should replace ICU placeholders with their translations', () => { it('should replace ICU placeholders with their translations', () => {
const HTML = `<div i18n>-{ count, plural, =0 {<p>bar</p>}}-</div>`;
const XTB = `<?xml version="1.0" encoding="UTF-8" ?> const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
<translationbundle> <translationbundle>
<translation id="eb404e202fed4846e25e7d9ac1fcb719fe4da257">*<ph name="ICU"/>*</translation> <translation id="7717087045075616176">*<ph name="ICU"/>*</translation>
<translation id="fc92b9b781194a02ab773129c8c5a7fc0735efd7">{ count, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation> <translation id="5115002811911870583">{VAR_PLURAL, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
</translationbundle>`; </translationbundle>`;
expect(loadAsText(HTML, XTB)).toEqual({ expect(loadAsMap(XTB)).toEqual({
'eb404e202fed4846e25e7d9ac1fcb719fe4da257': `*{ count, plural, =1 {<p>rab</p>}}*`, '7717087045075616176': `*<ph name="ICU"/>*`,
'fc92b9b781194a02ab773129c8c5a7fc0735efd7': `{ count, plural, =1 {<p>rab</p>}}`, '5115002811911870583':
`{VAR_PLURAL, plural, =1 {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}`,
}); });
}); });
it('should load complex XTB files', () => { it('should load complex XTB files', () => {
const HTML = `
<div i18n>foo <b>bar</b> {{ a + b }}</div>
<div i18n>{ count, plural, =0 {<p>bar</p>}}</div>
<div i18n="m|d">foo</div>
<div i18n>{ count, plural, =0 {{ sex, select, other {<p>bar</p>}} }}</div>`;
const XTB = `<?xml version="1.0" encoding="UTF-8" ?> const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
<translationbundle> <translationbundle>
<translation id="7103b4b13b616270a0044efade97d8b4f96f2ca6"><ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof</translation> <translation id="8281795707202401639"><ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof</translation>
<translation id="fc92b9b781194a02ab773129c8c5a7fc0735efd7">{ count, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation> <translation id="5115002811911870583">{VAR_PLURAL, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
<translation id="db3e0a6a5a96481f60aec61d98c3eecddef5ac23">oof</translation> <translation id="130772889486467622">oof</translation>
<translation id="8fb569d3dd83e92eff2551b24f5290d3035ce61b">{ count, plural, =1 {{ sex, select, other {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}} }}</translation> <translation id="4739316421648347533">{VAR_PLURAL, plural, =1 {{VAR_GENDER, gender, male {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}} }}</translation>
</translationbundle>`; </translationbundle>`;
expect(loadAsText(HTML, XTB)).toEqual({ expect(loadAsMap(XTB)).toEqual({
'7103b4b13b616270a0044efade97d8b4f96f2ca6': `{{ a + b }}<b>rab</b> oof`, '8281795707202401639':
'fc92b9b781194a02ab773129c8c5a7fc0735efd7': `{ count, plural, =1 {<p>rab</p>}}`, `<ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof`,
'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': `oof`, '5115002811911870583':
'8fb569d3dd83e92eff2551b24f5290d3035ce61b': `{VAR_PLURAL, plural, =1 {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}`,
`{ count, plural, =1 {{ sex, select, other {<p>rab</p>}} }}`, '130772889486467622': `oof`,
'4739316421648347533':
`{VAR_PLURAL, plural, =1 {[{VAR_GENDER, gender, male {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}, ]}}`,
}); });
}); });
}); });
describe('errors', () => { describe('errors', () => {
@ -127,7 +105,7 @@ export function main(): void {
'<translationbundle><translationbundle></translationbundle></translationbundle>'; '<translationbundle><translationbundle></translationbundle></translationbundle>';
expect(() => { expect(() => {
loadAsText('', XTB); loadAsMap(XTB);
}).toThrowError(/<translationbundle> elements can not be nested/); }).toThrowError(/<translationbundle> elements can not be nested/);
}); });
@ -136,58 +114,49 @@ export function main(): void {
<translation></translation> <translation></translation>
</translationbundle>`; </translationbundle>`;
expect(() => { expect(() => { loadAsMap(XTB); }).toThrowError(/<translation> misses the "id" attribute/);
loadAsText('', XTB);
}).toThrowError(/<translation> misses the "id" attribute/);
}); });
it('should throw when a placeholder has no name attribute', () => { it('should throw when a placeholder has no name attribute', () => {
const HTML = '<div i18n>give me a message</div>';
const XTB = `<translationbundle> const XTB = `<translationbundle>
<translation id="8de97c6a35252d9409dcaca0b8171c952740b28c"><ph /></translation> <translation id="1186013544048295927"><ph /></translation>
</translationbundle>`; </translationbundle>`;
expect(() => { loadAsText(HTML, XTB); }).toThrowError(/<ph> misses the "name" attribute/); expect(() => { loadAsMap(XTB); }).toThrowError(/<ph> misses the "name" attribute/);
}); });
it('should throw when a placeholder is not present in the source message', () => { it('should throw on unknown xtb tags', () => {
const HTML = `<div i18n>bar</div>`; const XTB = `<what></what>`;
const XTB = `<?xml version="1.0" encoding="UTF-8"?> expect(() => {
<translationbundle> loadAsMap(XTB);
<translation id="28a86c8a00ae573b2bac698d6609316dc7b4a226"><ph name="UNKNOWN"/></translation> }).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]<what></what>")`)));
});
it('should throw on unknown message tags', () => {
const XTB = `<translationbundle>
<translation id="1186013544048295927"><b>msg should contain only ph tags</b></translation>
</translationbundle>`;
expect(() => { loadAsMap(XTB); })
.toThrowError(
new RegExp(escapeRegExp(`[ERROR ->]<b>msg should contain only ph tags</b>`)));
});
it('should throw on duplicate message id', () => {
const XTB = `<translationbundle>
<translation id="1186013544048295927">msg1</translation>
<translation id="1186013544048295927">msg2</translation>
</translationbundle>`; </translationbundle>`;
expect(() => { expect(() => {
loadAsText(HTML, XTB); loadAsMap(XTB);
}).toThrowError(/The placeholder "UNKNOWN" does not exists in the source message/); }).toThrowError(/Duplicated translations for msg 1186013544048295927/);
}); });
});
it('should throw when the translation results in invalid html', () => { it('should throw when trying to save an xtb file',
const HTML = `<div i18n><p>bar</p></div>`; () => { expect(() => { serializer.write([]); }).toThrowError(/Unsupported/); });
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
<translationbundle>
<translation id="7de4d8ff1e42b7b31da6204074818236a9a5317f">rab<ph name="CLOSE_PARAGRAPH"/></translation>
</translationbundle>`;
expect(() => {
loadAsText(HTML, XTB);
}).toThrowError(/xtb parse errors:\nUnexpected closing tag "p"/);
}); });
it('should throw on unknown tags', () => {
const XTB = `<what></what>`;
expect(() => {
loadAsText('', XTB);
}).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]<what></what>")`)));
});
it('should throw when trying to save an xtb file',
() => { expect(() => { serializer.write({}); }).toThrowError(/Unsupported/); });
}); });
} }

View File

@ -0,0 +1,110 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as i18n from '../../src/i18n/i18n_ast';
import {TranslationBundle} from '../../src/i18n/translation_bundle';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
import {serializeNodes} from '../ml_parser/ast_serializer_spec';
export function main(): void {
describe('TranslationBundle', () => {
const file = new ParseSourceFile('content', 'url');
const location = new ParseLocation(file, 0, 0, 0);
const span = new ParseSourceSpan(location, null);
const srcNode = new i18n.Text('src', span);
it('should translate a plain message', () => {
const msgMap = {foo: [new i18n.Text('bar', null)]};
const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
expect(serializeNodes(tb.get(msg))).toEqual(['bar']);
});
it('should translate a message with placeholder', () => {
const msgMap = {
foo: [
new i18n.Text('bar', null),
new i18n.Placeholder('', 'ph1', null),
]
};
const phMap = {
ph1: '*phContent*',
};
const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd');
expect(serializeNodes(tb.get(msg))).toEqual(['bar*phContent*']);
});
it('should translate a message with placeholder referencing messages', () => {
const msgMap = {
foo: [
new i18n.Text('--', null),
new i18n.Placeholder('', 'ph1', null),
new i18n.Text('++', null),
],
ref: [
new i18n.Text('*refMsg*', null),
],
};
const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd');
let count = 0;
const digest = (_: any) => count++ ? 'ref' : 'foo';
const tb = new TranslationBundle(msgMap, digest);
expect(serializeNodes(tb.get(msg))).toEqual(['--*refMsg*++']);
});
describe('errors', () => {
it('should report unknown placeholders', () => {
const msgMap = {
foo: [
new i18n.Text('bar', null),
new i18n.Placeholder('', 'ph1', span),
]
};
const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
expect(() => tb.get(msg)).toThrowError(/Unknown placeholder/);
});
it('should report missing translation', () => {
const tb = new TranslationBundle({}, (_) => 'foo');
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
expect(() => tb.get(msg)).toThrowError(/Missing translation for message foo/);
});
it('should report missing referenced message', () => {
const msgMap = {
foo: [new i18n.Placeholder('', 'ph1', span)],
};
const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd');
let count = 0;
const digest = (_: any) => count++ ? 'ref' : 'foo';
const tb = new TranslationBundle(msgMap, digest);
expect(() => tb.get(msg)).toThrowError(/Missing translation for message ref/);
});
it('should report invalid translated html', () => {
const msgMap = {
foo: [
new i18n.Text('text', null),
new i18n.Placeholder('', 'ph1', null),
]
};
const phMap = {
ph1: '</b>',
};
const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd');
expect(() => tb.get(msg)).toThrowError(/Unexpected closing tag "b"/);
});
});
});
}

View File

@ -88,7 +88,7 @@ export class AnimationGroupPlayer implements AnimationPlayer {
this._started = false; this._started = false;
} }
setPosition(p: any /** TODO #9100 */): void { setPosition(p: number): void {
this._players.forEach(player => { player.setPosition(p); }); this._players.forEach(player => { player.setPosition(p); });
} }
@ -100,4 +100,6 @@ export class AnimationGroupPlayer implements AnimationPlayer {
}); });
return min; return min;
} }
get players(): AnimationPlayer[] { return this._players; }
} }

View File

@ -56,6 +56,6 @@ export class NoOpAnimationPlayer implements AnimationPlayer {
finish(): void { this._onFinish(); } finish(): void { this._onFinish(); }
destroy(): void {} destroy(): void {}
reset(): void {} reset(): void {}
setPosition(p: any /** TODO #9100 */): void {} setPosition(p: number): void {}
getPosition(): number { return 0; } getPosition(): number { return 0; }
} }

View File

@ -104,7 +104,9 @@ export class AnimationSequencePlayer implements AnimationPlayer {
} }
} }
setPosition(p: any /** TODO #9100 */): void { this._players[0].setPosition(p); } setPosition(p: number): void { this._players[0].setPosition(p); }
getPosition(): number { return this._players[0].getPosition(); } getPosition(): number { return this._players[0].getPosition(); }
get players(): AnimationPlayer[] { return this._players; }
} }

View File

@ -84,6 +84,8 @@ export function balanceAnimationKeyframes(
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles); firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
} }
collectAndResolveStyles(collectedStyles, [finalStateStyles]);
return keyframes; return keyframes;
} }

View File

@ -21,6 +21,8 @@ import {CompilerFactory, CompilerOptions} from './linker/compiler';
import {ComponentFactory, ComponentRef} from './linker/component_factory'; import {ComponentFactory, ComponentRef} from './linker/component_factory';
import {ComponentFactoryResolver} from './linker/component_factory_resolver'; import {ComponentFactoryResolver} from './linker/component_factory_resolver';
import {NgModuleFactory, NgModuleInjector, NgModuleRef} from './linker/ng_module_factory'; import {NgModuleFactory, NgModuleInjector, NgModuleRef} from './linker/ng_module_factory';
import {AppView} from './linker/view';
import {ViewRef, ViewRef_} from './linker/view_ref';
import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile'; import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile';
import {Testability, TestabilityRegistry} from './testability/testability'; import {Testability, TestabilityRegistry} from './testability/testability';
import {Type} from './type'; import {Type} from './type';
@ -60,6 +62,15 @@ export function isDevMode(): boolean {
return _devMode; return _devMode;
} }
/**
* A token for third-party components that can register themselves with NgProbe.
*
* @experimental
*/
export class NgProbeToken {
constructor(public name: string, public token: any) {}
}
/** /**
* Creates a platform. * Creates a platform.
* Platforms have to be eagerly created via this function. * Platforms have to be eagerly created via this function.
@ -378,6 +389,23 @@ export abstract class ApplicationRef {
* Get a list of components registered to this application. * Get a list of components registered to this application.
*/ */
get components(): ComponentRef<any>[] { return <ComponentRef<any>[]>unimplemented(); }; get components(): ComponentRef<any>[] { return <ComponentRef<any>[]>unimplemented(); };
/**
* Attaches a view so that it will be dirty checked.
* The view will be automatically detached when it is destroyed.
* This will throw if the view is already attached to a ViewContainer.
*/
attachView(view: ViewRef): void { unimplemented(); }
/**
* Detaches a view from dirty checking again.
*/
detachView(view: ViewRef): void { unimplemented(); }
/**
* Returns the number of attached views.
*/
get viewCount() { return unimplemented(); }
} }
@Injectable() @Injectable()
@ -388,7 +416,7 @@ export class ApplicationRef_ extends ApplicationRef {
private _bootstrapListeners: Function[] = []; private _bootstrapListeners: Function[] = [];
private _rootComponents: ComponentRef<any>[] = []; private _rootComponents: ComponentRef<any>[] = [];
private _rootComponentTypes: Type<any>[] = []; private _rootComponentTypes: Type<any>[] = [];
private _changeDetectorRefs: ChangeDetectorRef[] = []; private _views: AppView<any>[] = [];
private _runningTick: boolean = false; private _runningTick: boolean = false;
private _enforceNoNewChanges: boolean = false; private _enforceNoNewChanges: boolean = false;
@ -406,12 +434,16 @@ export class ApplicationRef_ extends ApplicationRef {
{next: () => { this._zone.run(() => { this.tick(); }); }}); {next: () => { this._zone.run(() => { this.tick(); }); }});
} }
registerChangeDetector(changeDetector: ChangeDetectorRef): void { attachView(viewRef: ViewRef): void {
this._changeDetectorRefs.push(changeDetector); const view = (viewRef as ViewRef_<any>).internalView;
this._views.push(view);
view.attachToAppRef(this);
} }
unregisterChangeDetector(changeDetector: ChangeDetectorRef): void { detachView(viewRef: ViewRef): void {
ListWrapper.remove(this._changeDetectorRefs, changeDetector); const view = (viewRef as ViewRef_<any>).internalView;
ListWrapper.remove(this._views, view);
view.detach();
} }
bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>): ComponentRef<C> { bootstrap<C>(componentOrFactory: ComponentFactory<C>|Type<C>): ComponentRef<C> {
@ -442,9 +474,8 @@ export class ApplicationRef_ extends ApplicationRef {
return compRef; return compRef;
} }
/** @internal */ private _loadComponent(componentRef: ComponentRef<any>): void {
_loadComponent(componentRef: ComponentRef<any>): void { this.attachView(componentRef.hostView);
this._changeDetectorRefs.push(componentRef.changeDetectorRef);
this.tick(); this.tick();
this._rootComponents.push(componentRef); this._rootComponents.push(componentRef);
// Get the listeners lazily to prevent DI cycles. // Get the listeners lazily to prevent DI cycles.
@ -454,12 +485,8 @@ export class ApplicationRef_ extends ApplicationRef {
listeners.forEach((listener) => listener(componentRef)); listeners.forEach((listener) => listener(componentRef));
} }
/** @internal */ private _unloadComponent(componentRef: ComponentRef<any>): void {
_unloadComponent(componentRef: ComponentRef<any>): void { this.detachView(componentRef.hostView);
if (this._rootComponents.indexOf(componentRef) == -1) {
return;
}
this.unregisterChangeDetector(componentRef.changeDetectorRef);
ListWrapper.remove(this._rootComponents, componentRef); ListWrapper.remove(this._rootComponents, componentRef);
} }
@ -471,9 +498,9 @@ export class ApplicationRef_ extends ApplicationRef {
const scope = ApplicationRef_._tickScope(); const scope = ApplicationRef_._tickScope();
try { try {
this._runningTick = true; this._runningTick = true;
this._changeDetectorRefs.forEach((detector) => detector.detectChanges()); this._views.forEach((view) => view.ref.detectChanges());
if (this._enforceNoNewChanges) { if (this._enforceNoNewChanges) {
this._changeDetectorRefs.forEach((detector) => detector.checkNoChanges()); this._views.forEach((view) => view.ref.checkNoChanges());
} }
} finally { } finally {
this._runningTick = false; this._runningTick = false;
@ -483,9 +510,11 @@ export class ApplicationRef_ extends ApplicationRef {
ngOnDestroy() { ngOnDestroy() {
// TODO(alxhub): Dispose of the NgZone. // TODO(alxhub): Dispose of the NgZone.
this._rootComponents.slice().forEach((component) => component.destroy()); this._views.slice().forEach((view) => view.destroy());
} }
get viewCount() { return this._views.length; }
get componentTypes(): Type<any>[] { return this._rootComponentTypes; } get componentTypes(): Type<any>[] { return this._rootComponentTypes; }
get components(): ComponentRef<any>[] { return this._rootComponents; } get components(): ComponentRef<any>[] { return this._rootComponents; }

View File

@ -14,7 +14,7 @@
export * from './metadata'; export * from './metadata';
export * from './util'; export * from './util';
export * from './di'; export * from './di';
export {createPlatform, assertPlatform, destroyPlatform, getPlatform, PlatformRef, ApplicationRef, enableProdMode, isDevMode, createPlatformFactory} from './application_ref'; export {createPlatform, assertPlatform, destroyPlatform, getPlatform, PlatformRef, ApplicationRef, enableProdMode, isDevMode, createPlatformFactory, NgProbeToken} from './application_ref';
export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, APP_BOOTSTRAP_LISTENER} from './application_tokens'; export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, APP_BOOTSTRAP_LISTENER} from './application_tokens';
export {APP_INITIALIZER, ApplicationInitStatus} from './application_init'; export {APP_INITIALIZER, ApplicationInitStatus} from './application_init';
export * from './zone'; export * from './zone';

View File

@ -22,7 +22,7 @@ export class DebugDomRootRenderer implements RootRenderer {
} }
} }
export class DebugDomRenderer implements Renderer { export class DebugDomRenderer {
constructor(private _delegate: Renderer) {} constructor(private _delegate: Renderer) {}
selectRootElement(selectorOrNode: string|any, debugInfo?: RenderDebugInfo): any { selectRootElement(selectorOrNode: string|any, debugInfo?: RenderDebugInfo): any {
@ -150,7 +150,9 @@ export class DebugDomRenderer implements Renderer {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
return this._delegate.animate(element, startingStyles, keyframes, duration, delay, easing); previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return this._delegate.animate(
element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} }
} }

View File

@ -8,8 +8,9 @@
import {AnimationGroupPlayer} from '../animation/animation_group_player'; import {AnimationGroupPlayer} from '../animation/animation_group_player';
import {AnimationPlayer} from '../animation/animation_player'; import {AnimationPlayer} from '../animation/animation_player';
import {queueAnimation as queueAnimationGlobally} from '../animation/animation_queue'; import {queueAnimation as queueAnimationGlobally} from '../animation/animation_queue';
import {AnimationTransitionEvent} from '../animation/animation_transition_event'; import {AnimationSequencePlayer} from '../animation/animation_sequence_player';
import {ViewAnimationMap} from '../animation/view_animation_map'; import {ViewAnimationMap} from '../animation/view_animation_map';
import {ListWrapper} from '../facade/collection';
export class AnimationViewContext { export class AnimationViewContext {
private _players = new ViewAnimationMap(); private _players = new ViewAnimationMap();
@ -30,15 +31,26 @@ export class AnimationViewContext {
this._players.set(element, animationName, player); this._players.set(element, animationName, player);
} }
cancelActiveAnimation(element: any, animationName: string, removeAllAnimations: boolean = false): getAnimationPlayers(element: any, animationName: string, removeAllAnimations: boolean = false):
void { AnimationPlayer[] {
const players: AnimationPlayer[] = [];
if (removeAllAnimations) { if (removeAllAnimations) {
this._players.findAllPlayersByElement(element).forEach(player => player.destroy()); this._players.findAllPlayersByElement(element).forEach(
player => { _recursePlayers(player, players); });
} else { } else {
const player = this._players.find(element, animationName); const currentPlayer = this._players.find(element, animationName);
if (player) { if (currentPlayer) {
player.destroy(); _recursePlayers(currentPlayer, players);
} }
} }
return players;
}
}
function _recursePlayers(player: AnimationPlayer, collectedPlayers: AnimationPlayer[]) {
if ((player instanceof AnimationGroupPlayer) || (player instanceof AnimationSequencePlayer)) {
player.players.forEach(player => _recursePlayers(player, collectedPlayers));
} else {
collectedPlayers.push(player);
} }
} }

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ApplicationRef} from '../application_ref';
import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection'; import {ChangeDetectorRef, ChangeDetectorStatus} from '../change_detection/change_detection';
import {Injector, THROW_IF_NOT_FOUND} from '../di/injector'; import {Injector, THROW_IF_NOT_FOUND} from '../di/injector';
import {ListWrapper} from '../facade/collection'; import {ListWrapper} from '../facade/collection';
@ -41,7 +42,10 @@ export abstract class AppView<T> {
lastRootNode: any; lastRootNode: any;
allNodes: any[]; allNodes: any[];
disposables: Function[]; disposables: Function[];
viewContainer: ViewContainer = null; viewContainer: ViewContainer;
// This will be set if a view is directly attached to an ApplicationRef
// and not to a view container.
appRef: ApplicationRef;
numberOfChecks: number = 0; numberOfChecks: number = 0;
@ -138,10 +142,12 @@ export abstract class AppView<T> {
injector(nodeIndex: number): Injector { return new ElementInjector(this, nodeIndex); } injector(nodeIndex: number): Injector { return new ElementInjector(this, nodeIndex); }
detachAndDestroy() { detachAndDestroy() {
if (this._hasExternalHostElement) { if (this.viewContainer) {
this.detach();
} else if (isPresent(this.viewContainer)) {
this.viewContainer.detachView(this.viewContainer.nestedViews.indexOf(this)); this.viewContainer.detachView(this.viewContainer.nestedViews.indexOf(this));
} else if (this.appRef) {
this.appRef.detachView(this.ref);
} else if (this._hasExternalHostElement) {
this.detach();
} }
this.destroy(); this.destroy();
} }
@ -196,6 +202,7 @@ export abstract class AppView<T> {
projectedViews.splice(index, 1); projectedViews.splice(index, 1);
} }
} }
this.appRef = null;
this.viewContainer = null; this.viewContainer = null;
this.dirtyParentQueriesInternal(); this.dirtyParentQueriesInternal();
} }
@ -208,7 +215,18 @@ export abstract class AppView<T> {
} }
} }
attachToAppRef(appRef: ApplicationRef) {
if (this.viewContainer) {
throw new Error('This view is already attached to a ViewContainer!');
}
this.appRef = appRef;
this.dirtyParentQueriesInternal();
}
attachAfter(viewContainer: ViewContainer, prevView: AppView<any>) { attachAfter(viewContainer: ViewContainer, prevView: AppView<any>) {
if (this.appRef) {
throw new Error('This view is already attached directly to the ApplicationRef!');
}
this._renderAttach(viewContainer, prevView); this._renderAttach(viewContainer, prevView);
this.viewContainer = viewContainer; this.viewContainer = viewContainer;
if (this.declaredViewContainer && this.declaredViewContainer !== viewContainer) { if (this.declaredViewContainer && this.declaredViewContainer !== viewContainer) {
@ -232,8 +250,10 @@ export abstract class AppView<T> {
if (nextSibling) { if (nextSibling) {
this.visitRootNodesInternal(this._directRenderer.insertBefore, nextSibling); this.visitRootNodesInternal(this._directRenderer.insertBefore, nextSibling);
} else { } else {
this.visitRootNodesInternal( const parentElement = this._directRenderer.parentElement(prevNode);
this._directRenderer.appendChild, this._directRenderer.parentElement(prevNode)); if (parentElement) {
this.visitRootNodesInternal(this._directRenderer.appendChild, parentElement);
}
} }
} else { } else {
this.renderer.attachViewAfter(prevNode, this.flatRootNodes); this.renderer.attachViewAfter(prevNode, this.flatRootNodes);

View File

@ -17,7 +17,7 @@ import {AppView} from './view';
/** /**
* @stable * @stable
*/ */
export abstract class ViewRef { export abstract class ViewRef extends ChangeDetectorRef {
get destroyed(): boolean { return <boolean>unimplemented(); } get destroyed(): boolean { return <boolean>unimplemented(); }
abstract onDestroy(callback: Function): any /** TODO #9100 */; abstract onDestroy(callback: Function): any /** TODO #9100 */;

View File

@ -56,8 +56,12 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities {
} }
// API of tsickle for lowering decorators to properties on the class. // API of tsickle for lowering decorators to properties on the class.
if ((<any>type).ctorParameters) { const tsickleCtorParams = (<any>type).ctorParameters;
const ctorParameters = (<any>type).ctorParameters; if (tsickleCtorParams) {
// Newer tsickle uses a function closure
// Retain the non-function case for compatibility with older tsickle
const ctorParameters =
typeof tsickleCtorParams === 'function' ? tsickleCtorParams() : tsickleCtorParams;
const paramTypes = ctorParameters.map((ctorParam: any) => ctorParam && ctorParam.type); const paramTypes = ctorParameters.map((ctorParam: any) => ctorParam && ctorParam.type);
const paramAnnotations = ctorParameters.map( const paramAnnotations = ctorParameters.map(
(ctorParam: any) => (ctorParam: any) =>

View File

@ -88,7 +88,8 @@ export abstract class Renderer {
abstract animate( abstract animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer; duration: number, delay: number, easing: string,
previousPlayers?: AnimationPlayer[]): AnimationPlayer;
} }
/** /**

View File

@ -1854,6 +1854,8 @@ function declareTests({useJit}: {useJit: boolean}) {
let animation = driver.log.pop(); let animation = driver.log.pop();
let kf = animation['keyframeLookup']; let kf = animation['keyframeLookup'];
expect(kf[1]).toEqual([1, {'background': 'green'}]); expect(kf[1]).toEqual([1, {'background': 'green'}]);
let player = animation['player'];
player.finish();
cmp.exp = 'blue'; cmp.exp = 'blue';
fixture.detectChanges(); fixture.detectChanges();
@ -1863,6 +1865,8 @@ function declareTests({useJit}: {useJit: boolean}) {
kf = animation['keyframeLookup']; kf = animation['keyframeLookup'];
expect(kf[0]).toEqual([0, {'background': 'green'}]); expect(kf[0]).toEqual([0, {'background': 'green'}]);
expect(kf[1]).toEqual([1, {'background': 'grey'}]); expect(kf[1]).toEqual([1, {'background': 'grey'}]);
player = animation['player'];
player.finish();
cmp.exp = 'red'; cmp.exp = 'red';
fixture.detectChanges(); fixture.detectChanges();
@ -1872,6 +1876,8 @@ function declareTests({useJit}: {useJit: boolean}) {
kf = animation['keyframeLookup']; kf = animation['keyframeLookup'];
expect(kf[0]).toEqual([0, {'background': 'grey'}]); expect(kf[0]).toEqual([0, {'background': 'grey'}]);
expect(kf[1]).toEqual([1, {'background': 'red'}]); expect(kf[1]).toEqual([1, {'background': 'red'}]);
player = animation['player'];
player.finish();
cmp.exp = 'orange'; cmp.exp = 'orange';
fixture.detectChanges(); fixture.detectChanges();
@ -1881,6 +1887,8 @@ function declareTests({useJit}: {useJit: boolean}) {
kf = animation['keyframeLookup']; kf = animation['keyframeLookup'];
expect(kf[0]).toEqual([0, {'background': 'red'}]); expect(kf[0]).toEqual([0, {'background': 'red'}]);
expect(kf[1]).toEqual([1, {'background': 'grey'}]); expect(kf[1]).toEqual([1, {'background': 'grey'}]);
player = animation['player'];
player.finish();
})); }));
it('should seed in the origin animation state styles into the first animation step', it('should seed in the origin animation state styles into the first animation step',
@ -1911,6 +1919,44 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(animation['startingStyles']).toEqual({'height': '100px'}); expect(animation['startingStyles']).toEqual({'height': '100px'});
})); }));
it('should seed in the previous animation styles into the transition if the previous transition was interupted midway',
fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, {
set: {
template: `
<div class="target" [@status]="exp"></div>
`,
animations: [trigger(
'status',
[
state('*', style({ opacity: 0 })),
state('a', style({height: '100px', width: '200px'})),
state('b', style({height: '1000px' })),
transition('* => *', [
animate(1000, style({ fontSize: '20px' })),
animate(1000)
])
])]
}
});
const driver = TestBed.get(AnimationDriver) as MockAnimationDriver;
const fixture = TestBed.createComponent(DummyIfCmp);
const cmp = fixture.componentInstance;
cmp.exp = 'a';
fixture.detectChanges();
flushMicrotasks();
driver.log = [];
cmp.exp = 'b';
fixture.detectChanges();
flushMicrotasks();
const animation = driver.log[0];
expect(animation['previousStyles']).toEqual({opacity: '0', fontSize: '*'});
}));
it('should perform a state change even if there is no transition that is found', it('should perform a state change even if there is no transition that is found',
fakeAsync(() => { fakeAsync(() => {
TestBed.overrideComponent(DummyIfCmp, { TestBed.overrideComponent(DummyIfCmp, {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, PlatformRef, Type} from '@angular/core'; import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, PlatformRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref'; import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref';
import {ErrorHandler} from '@angular/core/src/error_handler'; import {ErrorHandler} from '@angular/core/src/error_handler';
import {ComponentRef} from '@angular/core/src/linker/component_factory'; import {ComponentRef} from '@angular/core/src/linker/component_factory';
@ -16,9 +16,7 @@ import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
import {expect} from '@angular/platform-browser/testing/matchers'; import {expect} from '@angular/platform-browser/testing/matchers';
import {ServerModule} from '@angular/platform-server'; import {ServerModule} from '@angular/platform-server';
import {TestBed, async, inject, withModule} from '../testing'; import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
import {SpyChangeDetectorRef} from './spies';
@Component({selector: 'comp', template: 'hello'}) @Component({selector: 'comp', template: 'hello'})
class SomeComponent { class SomeComponent {
@ -74,13 +72,16 @@ export function main() {
beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); }); beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); });
it('should throw when reentering tick', inject([ApplicationRef], (ref: ApplicationRef_) => { it('should throw when reentering tick', inject([ApplicationRef], (ref: ApplicationRef_) => {
const cdRef = <any>new SpyChangeDetectorRef(); const view = jasmine.createSpyObj('view', ['detach', 'attachToAppRef']);
const viewRef = jasmine.createSpyObj('viewRef', ['detectChanges']);
viewRef.internalView = view;
view.ref = viewRef;
try { try {
ref.registerChangeDetector(cdRef); ref.attachView(viewRef);
cdRef.spy('detectChanges').and.callFake(() => ref.tick()); viewRef.detectChanges.and.callFake(() => ref.tick());
expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively'); expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively');
} finally { } finally {
ref.unregisterChangeDetector(cdRef); ref.detachView(viewRef);
} }
})); }));
@ -261,6 +262,84 @@ export function main() {
}); });
})); }));
}); });
describe('attachView / detachView', () => {
@Component({template: '{{name}}'})
class MyComp {
name = 'Initial';
}
@Component({template: '<ng-container #vc></ng-container>'})
class ContainerComp {
@ViewChild('vc', {read: ViewContainerRef})
vc: ViewContainerRef;
}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComp, ContainerComp],
providers: [{provide: ComponentFixtureNoNgZone, useValue: true}]
});
});
it('should dirty check attached views', () => {
const comp = TestBed.createComponent(MyComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
expect(appRef.viewCount).toBe(0);
appRef.tick();
expect(comp.nativeElement).toHaveText('');
appRef.attachView(comp.componentRef.hostView);
appRef.tick();
expect(appRef.viewCount).toBe(1);
expect(comp.nativeElement).toHaveText('Initial');
});
it('should not dirty check detached views', () => {
const comp = TestBed.createComponent(MyComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
appRef.attachView(comp.componentRef.hostView);
appRef.tick();
expect(comp.nativeElement).toHaveText('Initial');
appRef.detachView(comp.componentRef.hostView);
comp.componentInstance.name = 'New';
appRef.tick();
expect(appRef.viewCount).toBe(0);
expect(comp.nativeElement).toHaveText('Initial');
});
it('should detach attached views if they are destroyed', () => {
const comp = TestBed.createComponent(MyComp);
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
appRef.attachView(comp.componentRef.hostView);
comp.destroy();
expect(appRef.viewCount).toBe(0);
});
it('should not allow to attach a view to both, a view container and the ApplicationRef',
() => {
const comp = TestBed.createComponent(MyComp);
const hostView = comp.componentRef.hostView;
const containerComp = TestBed.createComponent(ContainerComp);
containerComp.detectChanges();
const vc = containerComp.componentInstance.vc;
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
vc.insert(hostView);
expect(() => appRef.attachView(hostView))
.toThrowError('This view is already attached to a ViewContainer!');
vc.detach(0);
appRef.attachView(hostView);
expect(() => vc.insert(hostView))
.toThrowError('This view is already attached directly to the ApplicationRef!');
});
});
}); });
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, Injectable, RenderComponentType, Renderer, RootRenderer} from '@angular/core'; import {Component, ContentChild, Injectable, Input, RenderComponentType, Renderer, RootRenderer, TemplateRef} from '@angular/core';
import {DebugDomRenderer} from '@angular/core/src/debug/debug_renderer'; import {DebugDomRenderer} from '@angular/core/src/debug/debug_renderer';
import {DirectRenderer} from '@angular/core/src/render/api'; import {DirectRenderer} from '@angular/core/src/render/api';
import {TestBed, inject} from '@angular/core/testing'; import {TestBed, inject} from '@angular/core/testing';
@ -125,6 +125,46 @@ export function main() {
const projectedNode = childHostEl.childNodes[1]; const projectedNode = childHostEl.childNodes[1];
expect(directRenderer.appendChild).toHaveBeenCalledWith(projectedNode, childHostEl); expect(directRenderer.appendChild).toHaveBeenCalledWith(projectedNode, childHostEl);
}); });
it('should support using structural directives with ngTemplateOutlet', () => {
@Component({
template:
'<child [templateCtx]="templateCtx"><template let-shown="shown" #tpl><span *ngIf="shown">hello</span></template></child>'
})
class Parent {
templateCtx = {shown: false};
}
@Component({
selector: 'child',
template:
'(<template [ngTemplateOutlet]="templateRef" [ngOutletContext]="templateCtx"></template>)'
})
class Child {
@Input()
templateCtx: any;
@ContentChild('tpl')
templateRef: TemplateRef<any>;
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
let fixture = TestBed.createComponent(Parent);
fixture.componentInstance.templateCtx.shown = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('()');
fixture.destroy();
fixture = TestBed.createComponent(Parent);
fixture.componentInstance.templateCtx.shown = true;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('(hello)');
fixture.componentInstance.templateCtx.shown = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('()');
});
}); });
} }

View File

@ -89,6 +89,25 @@ export function main() {
const p = reflector.parameters(ClassWithoutDecorators); const p = reflector.parameters(ClassWithoutDecorators);
expect(p.length).toEqual(2); expect(p.length).toEqual(2);
}); });
// See https://github.com/angular/tsickle/issues/261
it('should read forwardRef down-leveled type', () => {
class Dep {}
class ForwardLegacy {
constructor(d: Dep) {}
// Older tsickle had a bug: wrote a forward reference
static ctorParameters = [{type: Dep}];
}
expect(reflector.parameters(ForwardLegacy)).toEqual([[Dep]]);
class Forward {
constructor(d: Dep) {}
// Newer tsickle generates a functionClosure
static ctorParameters = () => [{type: ForwardDep}];
}
class ForwardDep {}
expect(reflector.parameters(Forward)).toEqual([[ForwardDep]]);
});
}); });
describe('propMetadata', () => { describe('propMetadata', () => {

View File

@ -5,8 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AUTO_STYLE, AnimationPlayer} from '@angular/core';
import {AnimationPlayer} from '@angular/core';
export class MockAnimationPlayer implements AnimationPlayer { export class MockAnimationPlayer implements AnimationPlayer {
private _onDoneFns: Function[] = []; private _onDoneFns: Function[] = [];
@ -16,8 +15,21 @@ export class MockAnimationPlayer implements AnimationPlayer {
private _started = false; private _started = false;
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
public previousStyles: {[styleName: string]: string | number} = {};
public log: any[] /** TODO #9100 */ = []; public log: any[] = [];
constructor(
public startingStyles: {[key: string]: string | number} = {},
public keyframes: Array<[number, {[style: string]: string | number}]> = [],
previousPlayers: AnimationPlayer[] = []) {
previousPlayers.forEach(player => {
if (player instanceof MockAnimationPlayer) {
const styles = player._captureStyles();
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
}
});
}
private _onFinish(): void { private _onFinish(): void {
if (!this._finished) { if (!this._finished) {
@ -67,6 +79,32 @@ export class MockAnimationPlayer implements AnimationPlayer {
} }
} }
setPosition(p: any /** TODO #9100 */): void {} setPosition(p: number): void {}
getPosition(): number { return 0; } getPosition(): number { return 0; }
private _captureStyles(): {[styleName: string]: string | number} {
const captures: {[prop: string]: string | number} = {};
if (this.hasStarted()) {
// when assembling the captured styles, it's important that
// we build the keyframe styles in the following order:
// {startingStyles, ... other styles within keyframes, ... previousStyles }
Object.keys(this.startingStyles).forEach(prop => {
captures[prop] = this.startingStyles[prop];
});
this.keyframes.forEach(kf => {
const [offset, styles] = kf;
const newStyles: {[prop: string]: string | number} = {};
Object.keys(styles).forEach(
prop => { captures[prop] = this._finished ? styles[prop] : AUTO_STYLE; });
});
}
Object.keys(this.previousStyles).forEach(prop => {
captures[prop] = this.previousStyles[prop];
});
return captures;
}
} }

View File

@ -10,6 +10,9 @@
writeScriptTag('/vendor/system.js'); writeScriptTag('/vendor/system.js');
writeScriptTag('/vendor/Reflect.js'); writeScriptTag('/vendor/Reflect.js');
writeScriptTag('/_common/system-config.js'); writeScriptTag('/_common/system-config.js');
if (location.pathname.indexOf('/upgrade/') != -1) {
writeScriptTag('/vendor/angular.js');
}
function writeScriptTag(scriptUrl: string, onload: string = '') { function writeScriptTag(scriptUrl: string, onload: string = '') {
document.write('<script src="' + scriptUrl + '" onload="' + onload + '"></script>'); document.write('<script src="' + scriptUrl + '" onload="' + onload + '"></script>');

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './module'; import * as module from './module';
platformBrowserDynamic().bootstrapModule(AppModule); if (module.AppModule) {
platformBrowserDynamic().bootstrapModule(module.AppModule);
}

View File

@ -19,6 +19,7 @@ System.config({
'/vendor/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', '/vendor/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/router': '/vendor/@angular/router/bundles/router.umd.js', '@angular/router': '/vendor/@angular/router/bundles/router.umd.js',
'@angular/upgrade': '/vendor/@angular/upgrade/bundles/upgrade.umd.js', '@angular/upgrade': '/vendor/@angular/upgrade/bundles/upgrade.umd.js',
'@angular/upgrade/static': '/vendor/@angular/upgrade/bundles/upgrade-static.umd.js',
'rxjs': '/vendor/rxjs', 'rxjs': '/vendor/rxjs',
}, },
packages: { packages: {

View File

@ -20,6 +20,7 @@ mkdir $DIST/vendor/
ln -s ../../../dist/packages-dist/ $DIST/vendor/@angular ln -s ../../../dist/packages-dist/ $DIST/vendor/@angular
for FILE in \ for FILE in \
../../../node_modules/angular/angular.js \
../../../node_modules/zone.js/dist/zone.js \ ../../../node_modules/zone.js/dist/zone.js \
../../../node_modules/systemjs/dist/system.js \ ../../../node_modules/systemjs/dist/system.js \
../../../node_modules/reflect-metadata/Reflect.js \ ../../../node_modules/reflect-metadata/Reflect.js \
@ -35,4 +36,6 @@ for MODULE in `find . -name module.ts`; do
cp _common/*.html $FINAL_DIR_PATH cp _common/*.html $FINAL_DIR_PATH
cp $DIST/_common/*.js $FINAL_DIR_PATH cp $DIST/_common/*.js $FINAL_DIR_PATH
cp $DIST/_common/*.js.map $FINAL_DIR_PATH cp $DIST/_common/*.js.map $FINAL_DIR_PATH
find `dirname $MODULE` -name \*.css -exec cp {} $FINAL_DIR_PATH \;
done done

View File

@ -18,7 +18,7 @@
"target": "es5", "target": "es5",
"lib": ["es2015", "dom"], "lib": ["es2015", "dom"],
"skipLibCheck": true, "skipLibCheck": true,
"types": ["jasmine", "node"] "types": ["jasmine", "node", "angularjs"]
}, },
"include": [ "include": [
"./_common/*.ts", "./_common/*.ts",

View File

@ -0,0 +1,54 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {browser, by, element} from 'protractor';
import {verifyNoBrowserErrors} from '../../../../_common/e2e_util';
function loadPage(url: string) {
browser.ng12Hybrid = true;
browser.rootEl = 'example-app';
browser.get(url);
}
describe('upgrade(static)', () => {
beforeEach(() => { loadPage('/upgrade/static/ts/'); });
afterEach(verifyNoBrowserErrors);
it('should render the `ng2-heroes` component', () => {
expect(element(by.css('h1')).getText()).toEqual('Heroes');
expect(element.all(by.css('p')).get(0).getText()).toEqual('There are 3 heroes.');
});
it('should render 3 ng1-hero components', () => {
const heroComponents = element.all(by.css('ng1-hero'));
expect(heroComponents.count()).toEqual(3);
});
it('should add a new hero when the "Add Hero" button is pressed', () => {
const addHeroButton = element.all(by.css('button')).last();
expect(addHeroButton.getText()).toEqual('Add Hero');
addHeroButton.click();
const heroComponents = element.all(by.css('ng1-hero'));
expect(heroComponents.last().element(by.css('h2')).getText()).toEqual('Kamala Khan');
});
it('should remove a hero when the "Remove" button is pressed', () => {
let firstHero = element.all(by.css('ng1-hero')).get(0);
expect(firstHero.element(by.css('h2')).getText()).toEqual('Superman');
const removeHeroButton = firstHero.element(by.css('button'));
expect(removeHeroButton.getText()).toEqual('Remove');
removeHeroButton.click();
const heroComponents = element.all(by.css('ng1-hero'));
expect(heroComponents.count()).toEqual(2);
firstHero = element.all(by.css('ng1-hero')).get(0);
expect(firstHero.element(by.css('h2')).getText()).toEqual('Wonder Woman');
});
});

View File

@ -0,0 +1,184 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Directive, DoCheck, ElementRef, EventEmitter, Inject, Injectable, Injector, Input, NgModule, OnChanges, OnDestroy, OnInit, Output, SimpleChanges} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {UpgradeComponent, UpgradeModule, downgradeComponent, downgradeInjectable} from '@angular/upgrade/static';
interface Hero {
name: string;
description: string;
}
// #docregion Angular 2 Stuff
// #docregion ng2-heroes
// This Angular 2 component will be "downgraded" to be used in Angular 1
@Component({
selector: 'ng2-heroes',
// This template uses the upgraded `ng1-hero` component
// Note that because its element is compiled by Angular 2+ we must use camelCased attribute names
template: `<h1>Heroes</h1>
<p><ng-content></ng-content></p>
<div *ngFor="let hero of heroes">
<ng1-hero [hero]="hero" (onRemove)="removeHero.emit(hero)"><strong>Super Hero</strong></ng1-hero>
</div>
<button (click)="addHero.emit()">Add Hero</button>`,
})
class Ng2HeroesComponent {
@Input() heroes: Hero[];
@Output() addHero = new EventEmitter();
@Output() removeHero = new EventEmitter();
}
// #enddocregion
// #docregion ng2-heroes-service
// This Angular 2 service will be "downgraded" to be used in Angular 1
@Injectable()
class HeroesService {
heroes: Hero[] = [
{name: 'superman', description: 'The man of steel'},
{name: 'wonder woman', description: 'Princess of the Amazons'},
{name: 'thor', description: 'The hammer-wielding god'}
];
// #docregion use-ng1-upgraded-service
constructor(@Inject('titleCase') titleCase: (v: string) => string) {
// Change all the hero names to title case, using the "upgraded" Angular 1 service
this.heroes.forEach((hero: Hero) => hero.name = titleCase(hero.name));
}
// #enddocregion
addHero() {
this.heroes =
this.heroes.concat([{name: 'Kamala Khan', description: 'Epic shape-shifting healer'}]);
}
removeHero(hero: Hero) { this.heroes = this.heroes.filter((item: Hero) => item !== hero); }
}
// #enddocregion
// #docregion ng1-hero-wrapper
// This Angular 2 directive will act as an interface to the "upgraded" Angular 1 component
@Directive({selector: 'ng1-hero'})
class Ng1HeroComponentWrapper extends UpgradeComponent implements OnInit, OnChanges, DoCheck,
OnDestroy {
// The names of the input and output properties here must match the names of the
// `<` and `&` bindings in the Angular 1 component that is being wrapped
@Input() hero: Hero;
@Output() onRemove: EventEmitter<void>;
constructor(@Inject(ElementRef) elementRef: ElementRef, @Inject(Injector) injector: Injector) {
// We must pass the name of the directive as used by Angular 1 to the super
super('ng1Hero', elementRef, injector);
}
// For this class to work when compiled with AoT, we must implement these lifecycle hooks
// because the AoT compiler will not realise that the super class implements them
ngOnInit() { super.ngOnInit(); }
ngOnChanges(changes: SimpleChanges) { super.ngOnChanges(changes); }
ngDoCheck() { super.ngDoCheck(); }
ngOnDestroy() { super.ngOnDestroy(); }
}
// #enddocregion
// #docregion ng2-module
// This NgModule represents the Angular 2 pieces of the application
@NgModule({
declarations: [Ng2HeroesComponent, Ng1HeroComponentWrapper],
providers: [
HeroesService,
// #docregion upgrade-ng1-service
// Register an Angular 2+ provider whose value is the "upgraded" Angular 1 service
{provide: 'titleCase', useFactory: (i: any) => i.get('titleCase'), deps: ['$injector']}
// #enddocregion
],
// All components that are to be "downgraded" must be declared as `entryComponents`
entryComponents: [Ng2HeroesComponent],
// We must import `UpgradeModule` to get access to the Angular 1 core services
imports: [BrowserModule, UpgradeModule]
})
class Ng2AppModule {
ngDoBootstrap() { /* this is a placeholder to stop the boostrapper from complaining */
}
}
// #enddocregion
// #enddocregion
// #docregion Angular 1 Stuff
// #docregion ng1-module
// This Angular 1 module represents the Angular 1 pieces of the application
const ng1AppModule = angular.module('ng1AppModule', []);
// #enddocregion
// #docregion ng1-hero
// This Angular 1 component will be "upgraded" to be used in Angular 2+
ng1AppModule.component('ng1Hero', {
bindings: {hero: '<', onRemove: '&'},
transclude: true,
template: `<div class="title" ng-transclude></div>
<h2>{{ $ctrl.hero.name }}</h2>
<p>{{ $ctrl.hero.description }}</p>
<button ng-click="$ctrl.onRemove()">Remove</button>`
});
// #enddocregion
// #docregion ng1-title-case-service
// This Angular 1 service will be "upgraded" to be used in Angular 2+
ng1AppModule.factory(
'titleCase',
() => (value: string) => value.replace(/((^|\s)[a-z])/g, (_, c) => c.toUpperCase()));
// #enddocregion
// #docregion downgrade-ng2-heroes-service
// Register an Angular 1 service, whose value is the "downgraded" Angular 2+ injectable.
ng1AppModule.factory('heroesService', downgradeInjectable(HeroesService));
// #enddocregion
// #docregion ng2-heroes-wrapper
// This is directive will act as the interface to the "downgraded" Angular 2+ component
ng1AppModule.directive(
'ng2Heroes',
downgradeComponent(
// The inputs and outputs here must match the relevant names of the properties on the
// "downgraded" component
{component: Ng2HeroesComponent, inputs: ['heroes'], outputs: ['addHero', 'removeHero']}));
// #enddocregion
// #docregion example-app
// This is our top level application component
ng1AppModule.component('exampleApp', {
// We inject the "downgraded" HeroesService into this Angular 1 component
// (We don't need the `HeroesService` type for Angular 1 DI - it just helps with TypeScript
// compilation)
controller: [
'heroesService', function(heroesService: HeroesService) { this.heroesService = heroesService; }
],
// This template make use of the downgraded `ng2-heroes` component
// Note that because its element is compiled by Angular 1 we must use kebab-case attributes for
// inputs and outputs
template: `<link rel="stylesheet" href="./styles.css">
<ng2-heroes [heroes]="$ctrl.heroesService.heroes" (add-hero)="$ctrl.heroesService.addHero()" (remove-hero)="$ctrl.heroesService.removeHero($event)">
There are {{ $ctrl.heroesService.heroes.length }} heroes.
</ng2-heroes>`
});
// #enddocregion
// #enddocregion
// #docregion bootstrap
// First we bootstrap the Angular 2 HybridModule
// (We are using the dynamic browser platform as this example has not been compiled AoT)
platformBrowserDynamic().bootstrapModule(Ng2AppModule).then(ref => {
// Once Angular 2 bootstrap is complete then we bootstrap the Angular 1 module
const upgrade = ref.injector.get(UpgradeModule) as UpgradeModule;
upgrade.bootstrap(document.body, [ng1AppModule.name]);
});
// #enddocregion

View File

@ -0,0 +1,17 @@
ng2-heroes {
border: solid black 2px;
display: block;
padding: 5px;
}
ng1-hero {
border: solid green 2px;
margin-top: 5px;
padding: 5px;
display: block;
}
.title {
background-color: blue;
color: white;
}

View File

@ -56,22 +56,12 @@ export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] = [NgModel, NgModelGroup, N
export const REACTIVE_DRIVEN_DIRECTIVES: Type<any>[] = export const REACTIVE_DRIVEN_DIRECTIVES: Type<any>[] =
[FormControlDirective, FormGroupDirective, FormControlName, FormGroupName, FormArrayName]; [FormControlDirective, FormGroupDirective, FormControlName, FormGroupName, FormArrayName];
/**
* A list of all the form directives.
*
* @stable
*/
export const FORM_DIRECTIVES: Type<any>[][] = [TEMPLATE_DRIVEN_DIRECTIVES, SHARED_FORM_DIRECTIVES];
/**
* @stable
*/
export const REACTIVE_FORM_DIRECTIVES: Type<any>[][] =
[REACTIVE_DRIVEN_DIRECTIVES, SHARED_FORM_DIRECTIVES];
/** /**
* Internal module used for sharing directives between FormsModule and ReactiveFormsModule * Internal module used for sharing directives between FormsModule and ReactiveFormsModule
*/ */
@NgModule({declarations: SHARED_FORM_DIRECTIVES, exports: SHARED_FORM_DIRECTIVES}) @NgModule({
declarations: SHARED_FORM_DIRECTIVES,
exports: SHARED_FORM_DIRECTIVES,
})
export class InternalFormsSharedModule { export class InternalFormsSharedModule {
} }

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Renderer, forwardRef} from '@angular/core'; import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Provider, Renderer, forwardRef} from '@angular/core';
import {isPrimitive, looseIdentical} from '../facade/lang'; import {isPrimitive, looseIdentical} from '../facade/lang';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const SELECT_VALUE_ACCESSOR: any = { export const SELECT_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectControlValueAccessor), useExisting: forwardRef(() => SelectControlValueAccessor),
multi: true multi: true
@ -115,8 +115,8 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
/** @internal */ /** @internal */
_getOptionValue(valueString: string): any { _getOptionValue(valueString: string): any {
const value = this._optionMap.get(_extractId(valueString)); const id: string = _extractId(valueString);
return value != null ? value : valueString; return this._optionMap.has(id) ? this._optionMap.get(id) : valueString;
} }
} }
@ -158,7 +158,7 @@ export class NgSelectOption implements OnDestroy {
this._renderer.setElementProperty(this._element.nativeElement, 'value', value); this._renderer.setElementProperty(this._element.nativeElement, 'value', value);
} }
ngOnDestroy() { ngOnDestroy(): void {
if (this._select) { if (this._select) {
this._select._optionMap.delete(this.id); this._select._optionMap.delete(this.id);
this._select.writeValue(this._select.value); this._select.writeValue(this._select.value);

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Directive, ElementRef, Host, Input, OnDestroy, OpaqueToken, Optional, Renderer, Type, forwardRef} from '@angular/core'; import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Provider, Renderer, forwardRef} from '@angular/core';
import {isPrimitive, looseIdentical} from '../facade/lang'; import {isPrimitive, looseIdentical} from '../facade/lang';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
export const SELECT_MULTIPLE_VALUE_ACCESSOR = { export const SELECT_MULTIPLE_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectMultipleControlValueAccessor), useExisting: forwardRef(() => SelectMultipleControlValueAccessor),
multi: true multi: true
@ -121,8 +121,8 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
/** @internal */ /** @internal */
_getOptionValue(valueString: string): any { _getOptionValue(valueString: string): any {
const opt = this._optionMap.get(_extractId(valueString)); const id: string = _extractId(valueString);
return opt ? opt._value : valueString; return this._optionMap.has(id) ? this._optionMap.get(id)._value : valueString;
} }
} }
@ -180,12 +180,10 @@ export class NgSelectMultipleOption implements OnDestroy {
this._renderer.setElementProperty(this._element.nativeElement, 'selected', selected); this._renderer.setElementProperty(this._element.nativeElement, 'selected', selected);
} }
ngOnDestroy() { ngOnDestroy(): void {
if (this._select) { if (this._select) {
this._select._optionMap.delete(this.id); this._select._optionMap.delete(this.id);
this._select.writeValue(this._select.value); this._select.writeValue(this._select.value);
} }
} }
} }
export const SELECT_DIRECTIVES = [SelectMultipleControlValueAccessor, NgSelectMultipleOption];

View File

@ -5,10 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Directive, Input, OnChanges, SimpleChanges, forwardRef} from '@angular/core'; import {Directive, Input, OnChanges, SimpleChanges, forwardRef} from '@angular/core';
import {isPresent} from '../facade/lang';
import {AbstractControl} from '../model'; import {AbstractControl} from '../model';
import {NG_VALIDATORS, Validators} from '../validators'; import {NG_VALIDATORS, Validators} from '../validators';
@ -57,17 +54,17 @@ export const REQUIRED_VALIDATOR: any = {
@Directive({ @Directive({
selector: '[required][formControlName],[required][formControl],[required][ngModel]', selector: '[required][formControlName],[required][formControl],[required][ngModel]',
providers: [REQUIRED_VALIDATOR], providers: [REQUIRED_VALIDATOR],
host: {'[attr.required]': 'required? "" : null'} host: {'[attr.required]': 'required ? "" : null'}
}) })
export class RequiredValidator implements Validator { export class RequiredValidator implements Validator {
private _required: boolean; private _required: boolean;
private _onChange: () => void; private _onChange: () => void;
@Input() @Input()
get required(): boolean { return this._required; } get required(): boolean /*| string*/ { return this._required; }
set required(value: boolean) { set required(value: boolean) {
this._required = isPresent(value) && `${value}` !== 'false'; this._required = value != null && value !== false && `${value}` !== 'false';
if (this._onChange) this._onChange(); if (this._onChange) this._onChange();
} }
@ -75,7 +72,7 @@ export class RequiredValidator implements Validator {
return this.required ? Validators.required(c) : null; return this.required ? Validators.required(c) : null;
} }
registerOnValidatorChange(fn: () => void) { this._onChange = fn; } registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
} }
/** /**
@ -112,7 +109,7 @@ export const MIN_LENGTH_VALIDATOR: any = {
@Directive({ @Directive({
selector: '[minlength][formControlName],[minlength][formControl],[minlength][ngModel]', selector: '[minlength][formControlName],[minlength][formControl],[minlength][ngModel]',
providers: [MIN_LENGTH_VALIDATOR], providers: [MIN_LENGTH_VALIDATOR],
host: {'[attr.minlength]': 'minlength? minlength : null'} host: {'[attr.minlength]': 'minlength ? minlength : null'}
}) })
export class MinLengthValidator implements Validator, export class MinLengthValidator implements Validator,
OnChanges { OnChanges {
@ -121,12 +118,8 @@ export class MinLengthValidator implements Validator,
@Input() minlength: string; @Input() minlength: string;
private _createValidator() { ngOnChanges(changes: SimpleChanges): void {
this._validator = Validators.minLength(parseInt(this.minlength, 10)); if ('minlength' in changes) {
}
ngOnChanges(changes: SimpleChanges) {
if (changes['minlength']) {
this._createValidator(); this._createValidator();
if (this._onChange) this._onChange(); if (this._onChange) this._onChange();
} }
@ -136,7 +129,11 @@ export class MinLengthValidator implements Validator,
return this.minlength == null ? null : this._validator(c); return this.minlength == null ? null : this._validator(c);
} }
registerOnValidatorChange(fn: () => void) { this._onChange = fn; } registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
private _createValidator(): void {
this._validator = Validators.minLength(parseInt(this.minlength, 10));
}
} }
/** /**
@ -162,7 +159,7 @@ export const MAX_LENGTH_VALIDATOR: any = {
@Directive({ @Directive({
selector: '[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]', selector: '[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]',
providers: [MAX_LENGTH_VALIDATOR], providers: [MAX_LENGTH_VALIDATOR],
host: {'[attr.maxlength]': 'maxlength? maxlength : null'} host: {'[attr.maxlength]': 'maxlength ? maxlength : null'}
}) })
export class MaxLengthValidator implements Validator, export class MaxLengthValidator implements Validator,
OnChanges { OnChanges {
@ -171,22 +168,22 @@ export class MaxLengthValidator implements Validator,
@Input() maxlength: string; @Input() maxlength: string;
private _createValidator() { ngOnChanges(changes: SimpleChanges): void {
this._validator = Validators.maxLength(parseInt(this.maxlength, 10)); if ('maxlength' in changes) {
}
ngOnChanges(changes: SimpleChanges) {
if (changes['maxlength']) {
this._createValidator(); this._createValidator();
if (this._onChange) this._onChange(); if (this._onChange) this._onChange();
} }
} }
validate(c: AbstractControl): {[key: string]: any} { validate(c: AbstractControl): {[key: string]: any} {
return isPresent(this.maxlength) ? this._validator(c) : null; return this.maxlength != null ? this._validator(c) : null;
} }
registerOnValidatorChange(fn: () => void) { this._onChange = fn; } registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
private _createValidator(): void {
this._validator = Validators.maxLength(parseInt(this.maxlength, 10));
}
} }
@ -220,20 +217,18 @@ export class PatternValidator implements Validator,
private _validator: ValidatorFn; private _validator: ValidatorFn;
private _onChange: () => void; private _onChange: () => void;
@Input() pattern: string; @Input() pattern: string /*|RegExp*/;
private _createValidator() { this._validator = Validators.pattern(this.pattern); } ngOnChanges(changes: SimpleChanges): void {
if ('pattern' in changes) {
ngOnChanges(changes: SimpleChanges) {
if (changes['pattern']) {
this._createValidator(); this._createValidator();
if (this._onChange) this._onChange(); if (this._onChange) this._onChange();
} }
} }
validate(c: AbstractControl): {[key: string]: any} { validate(c: AbstractControl): {[key: string]: any} { return this._validator(c); }
return this.pattern ? this._validator(c) : null;
}
registerOnValidatorChange(fn: () => void) { this._onChange = fn; } registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
private _createValidator(): void { this._validator = Validators.pattern(this.pattern); }
} }

View File

@ -23,7 +23,7 @@ export function main() {
NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName, NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper, NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper,
NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator, NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator,
NgModelAsyncValidation NgModelAsyncValidation, NgModelSelectWithNullForm
], ],
imports: [FormsModule] imports: [FormsModule]
}); });
@ -699,6 +699,28 @@ export function main() {
expect(select.nativeElement.value).toEqual('2: Object'); expect(select.nativeElement.value).toEqual('2: Object');
expect(secondNYC.nativeElement.selected).toBe(true); expect(secondNYC.nativeElement.selected).toBe(true);
})); }));
it('should work with null option', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelSelectWithNullForm);
const comp = fixture.componentInstance;
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}];
comp.selectedCity = null;
fixture.detectChanges();
const select = fixture.debugElement.query(By.css('select'));
select.nativeElement.value = '2: Object';
dispatchEvent(select.nativeElement, 'change');
fixture.detectChanges();
tick();
expect(comp.selectedCity['name']).toEqual('NYC');
select.nativeElement.value = '0: null';
dispatchEvent(select.nativeElement, 'change');
fixture.detectChanges();
tick();
expect(comp.selectedCity).toEqual(null);
}));
}); });
describe('custom value accessors', () => { describe('custom value accessors', () => {
@ -771,7 +793,7 @@ export function main() {
expect(form.valid).toEqual(true); expect(form.valid).toEqual(true);
})); }));
it('should support optional fields with pattern validator', fakeAsync(() => { it('should support optional fields with string pattern validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators); const fixture = TestBed.createComponent(NgModelMultipleValidators);
fixture.componentInstance.required = false; fixture.componentInstance.required = false;
fixture.componentInstance.pattern = '[a-z]+'; fixture.componentInstance.pattern = '[a-z]+';
@ -793,6 +815,28 @@ export function main() {
expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy(); expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy();
})); }));
it('should support optional fields with RegExp pattern validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators);
fixture.componentInstance.required = false;
fixture.componentInstance.pattern = /^[a-z]+$/;
fixture.detectChanges();
tick();
const form = fixture.debugElement.children[0].injector.get(NgForm);
const input = fixture.debugElement.query(By.css('input'));
input.nativeElement.value = '';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeTruthy();
input.nativeElement.value = '1';
dispatchEvent(input.nativeElement, 'input');
fixture.detectChanges();
expect(form.valid).toBeFalsy();
expect(form.control.hasError('pattern', ['tovalidate'])).toBeTruthy();
}));
it('should support optional fields with minlength validator', fakeAsync(() => { it('should support optional fields with minlength validator', fakeAsync(() => {
const fixture = TestBed.createComponent(NgModelMultipleValidators); const fixture = TestBed.createComponent(NgModelMultipleValidators);
fixture.componentInstance.required = false; fixture.componentInstance.required = false;
@ -1078,6 +1122,20 @@ class NgModelSelectForm {
cities: any[] = []; cities: any[] = [];
} }
@Component({
selector: 'ng-model-select-null-form',
template: `
<select [(ngModel)]="selectedCity">
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
<option [ngValue]="null">Unspecified</option>
</select>
`
})
class NgModelSelectWithNullForm {
selectedCity: {[k: string]: string} = {};
cities: any[] = [];
}
@Component({ @Component({
selector: 'ng-model-custom-comp', selector: 'ng-model-custom-comp',
template: ` template: `
@ -1141,7 +1199,7 @@ class NgModelValidationBindings {
class NgModelMultipleValidators { class NgModelMultipleValidators {
required: boolean; required: boolean;
minLen: number; minLen: number;
pattern: string; pattern: string|RegExp;
} }
@Directive({ @Directive({

View File

@ -7,15 +7,15 @@
*/ */
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {global} from '../facade/lang';
let _nextRequestId = 0; let _nextRequestId = 0;
export const JSONP_HOME = '__ng_jsonp__'; export const JSONP_HOME = '__ng_jsonp__';
let _jsonpConnections: {[key: string]: any} = null; let _jsonpConnections: {[key: string]: any} = null;
function _getJsonpConnections(): {[key: string]: any} { function _getJsonpConnections(): {[key: string]: any} {
const w: {[key: string]: any} = typeof window == 'object' ? window : {};
if (_jsonpConnections === null) { if (_jsonpConnections === null) {
_jsonpConnections = (<{[key: string]: any}>global)[JSONP_HOME] = {}; _jsonpConnections = w[JSONP_HOME] = {};
} }
return _jsonpConnections; return _jsonpConnections;
} }

View File

@ -12,7 +12,6 @@ import {Observer} from 'rxjs/Observer';
import {ResponseOptions} from '../base_response_options'; import {ResponseOptions} from '../base_response_options';
import {ReadyState, RequestMethod, ResponseType} from '../enums'; import {ReadyState, RequestMethod, ResponseType} from '../enums';
import {isPresent} from '../facade/lang';
import {Connection, ConnectionBackend} from '../interfaces'; import {Connection, ConnectionBackend} from '../interfaces';
import {Request} from '../static_request'; import {Request} from '../static_request';
import {Response} from '../static_response'; import {Response} from '../static_response';
@ -89,7 +88,7 @@ export class JSONPConnection_ extends JSONPConnection {
if (!this._finished) { if (!this._finished) {
let responseOptions = let responseOptions =
new ResponseOptions({body: JSONP_ERR_NO_CALLBACK, type: ResponseType.Error, url}); new ResponseOptions({body: JSONP_ERR_NO_CALLBACK, type: ResponseType.Error, url});
if (isPresent(baseResponseOptions)) { if (baseResponseOptions) {
responseOptions = baseResponseOptions.merge(responseOptions); responseOptions = baseResponseOptions.merge(responseOptions);
} }
responseObserver.error(new Response(responseOptions)); responseObserver.error(new Response(responseOptions));
@ -97,7 +96,7 @@ export class JSONPConnection_ extends JSONPConnection {
} }
let responseOptions = new ResponseOptions({body: this._responseData, url}); let responseOptions = new ResponseOptions({body: this._responseData, url});
if (isPresent(this.baseResponseOptions)) { if (this.baseResponseOptions) {
responseOptions = this.baseResponseOptions.merge(responseOptions); responseOptions = this.baseResponseOptions.merge(responseOptions);
} }
@ -110,7 +109,7 @@ export class JSONPConnection_ extends JSONPConnection {
this.readyState = ReadyState.Done; this.readyState = ReadyState.Done;
_dom.cleanup(script); _dom.cleanup(script);
let responseOptions = new ResponseOptions({body: error.message, type: ResponseType.Error}); let responseOptions = new ResponseOptions({body: error.message, type: ResponseType.Error});
if (isPresent(baseResponseOptions)) { if (baseResponseOptions) {
responseOptions = baseResponseOptions.merge(responseOptions); responseOptions = baseResponseOptions.merge(responseOptions);
} }
responseObserver.error(new Response(responseOptions)); responseObserver.error(new Response(responseOptions));
@ -125,10 +124,7 @@ export class JSONPConnection_ extends JSONPConnection {
this.readyState = ReadyState.Cancelled; this.readyState = ReadyState.Cancelled;
script.removeEventListener('load', onLoad); script.removeEventListener('load', onLoad);
script.removeEventListener('error', onError); script.removeEventListener('error', onError);
if (isPresent(script)) { this._dom.cleanup(script);
this._dom.cleanup(script);
}
}; };
}); });
} }

View File

@ -10,16 +10,13 @@ import {Injectable} from '@angular/core';
import {__platform_browser_private__} from '@angular/platform-browser'; import {__platform_browser_private__} from '@angular/platform-browser';
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer'; import {Observer} from 'rxjs/Observer';
import {ResponseOptions} from '../base_response_options'; import {ResponseOptions} from '../base_response_options';
import {ContentType, ReadyState, RequestMethod, ResponseContentType, ResponseType} from '../enums'; import {ContentType, ReadyState, RequestMethod, ResponseContentType, ResponseType} from '../enums';
import {isPresent} from '../facade/lang';
import {Headers} from '../headers'; import {Headers} from '../headers';
import {getResponseURL, isSuccess} from '../http_utils'; import {getResponseURL, isSuccess} from '../http_utils';
import {Connection, ConnectionBackend, XSRFStrategy} from '../interfaces'; import {Connection, ConnectionBackend, XSRFStrategy} from '../interfaces';
import {Request} from '../static_request'; import {Request} from '../static_request';
import {Response} from '../static_response'; import {Response} from '../static_response';
import {BrowserXhr} from './browser_xhr'; import {BrowserXhr} from './browser_xhr';
const XSSI_PREFIX = /^\)\]\}',?\n/; const XSSI_PREFIX = /^\)\]\}',?\n/;
@ -47,24 +44,29 @@ export class XHRConnection implements Connection {
this.response = new Observable<Response>((responseObserver: Observer<Response>) => { this.response = new Observable<Response>((responseObserver: Observer<Response>) => {
const _xhr: XMLHttpRequest = browserXHR.build(); const _xhr: XMLHttpRequest = browserXHR.build();
_xhr.open(RequestMethod[req.method].toUpperCase(), req.url); _xhr.open(RequestMethod[req.method].toUpperCase(), req.url);
if (isPresent(req.withCredentials)) { if (req.withCredentials != null) {
_xhr.withCredentials = req.withCredentials; _xhr.withCredentials = req.withCredentials;
} }
// load event handler // load event handler
const onLoad = () => { const onLoad = () => {
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
// response/responseType properties were introduced in ResourceLoader Level2 spec (supported
// by IE10)
let body = _xhr.response === undefined ? _xhr.responseText : _xhr.response;
// Implicitly strip a potential XSSI prefix.
if (typeof body === 'string') body = body.replace(XSSI_PREFIX, '');
const headers = Headers.fromResponseHeaderString(_xhr.getAllResponseHeaders());
const url = getResponseURL(_xhr);
// normalize IE9 bug (http://bugs.jquery.com/ticket/1450) // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
let status: number = _xhr.status === 1223 ? 204 : _xhr.status; let status: number = _xhr.status === 1223 ? 204 : _xhr.status;
let body: any = null;
// HTTP 204 means no content
if (status !== 204) {
// responseText is the old-school way of retrieving response (supported by IE8 & 9)
// response/responseType properties were introduced in ResourceLoader Level2 spec
// (supported by IE10)
body = _xhr.response == null ? _xhr.responseText : _xhr.response;
// Implicitly strip a potential XSSI prefix.
if (typeof body === 'string') {
body = body.replace(XSSI_PREFIX, '');
}
}
// fix status code when it is 0 (0 status is undocumented). // fix status code when it is 0 (0 status is undocumented).
// Occurs when accessing file resources or on Android 4.1 stock browser // Occurs when accessing file resources or on Android 4.1 stock browser
// while retrieving files from application cache. // while retrieving files from application cache.
@ -72,10 +74,13 @@ export class XHRConnection implements Connection {
status = body ? 200 : 0; status = body ? 200 : 0;
} }
const statusText = _xhr.statusText || 'OK'; const headers: Headers = Headers.fromResponseHeaderString(_xhr.getAllResponseHeaders());
// IE 9 does not provide the way to get URL of response
const url = getResponseURL(_xhr) || req.url;
const statusText: string = _xhr.statusText || 'OK';
let responseOptions = new ResponseOptions({body, status, headers, statusText, url}); let responseOptions = new ResponseOptions({body, status, headers, statusText, url});
if (isPresent(baseResponseOptions)) { if (baseResponseOptions != null) {
responseOptions = baseResponseOptions.merge(responseOptions); responseOptions = baseResponseOptions.merge(responseOptions);
} }
const response = new Response(responseOptions); const response = new Response(responseOptions);
@ -89,14 +94,14 @@ export class XHRConnection implements Connection {
responseObserver.error(response); responseObserver.error(response);
}; };
// error event handler // error event handler
const onError = (err: any) => { const onError = (err: ErrorEvent) => {
let responseOptions = new ResponseOptions({ let responseOptions = new ResponseOptions({
body: err, body: err,
type: ResponseType.Error, type: ResponseType.Error,
status: _xhr.status, status: _xhr.status,
statusText: _xhr.statusText, statusText: _xhr.statusText,
}); });
if (isPresent(baseResponseOptions)) { if (baseResponseOptions != null) {
responseOptions = baseResponseOptions.merge(responseOptions); responseOptions = baseResponseOptions.merge(responseOptions);
} }
responseObserver.error(new Response(responseOptions)); responseObserver.error(new Response(responseOptions));
@ -104,12 +109,12 @@ export class XHRConnection implements Connection {
this.setDetectedContentType(req, _xhr); this.setDetectedContentType(req, _xhr);
if (isPresent(req.headers)) { if (req.headers != null) {
req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(','))); req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(',')));
} }
// Select the correct buffer type to store the response // Select the correct buffer type to store the response
if (isPresent(req.responseType) && isPresent(_xhr.responseType)) { if (req.responseType != null && _xhr.responseType != null) {
switch (req.responseType) { switch (req.responseType) {
case ResponseContentType.ArrayBuffer: case ResponseContentType.ArrayBuffer:
_xhr.responseType = 'arraybuffer'; _xhr.responseType = 'arraybuffer';
@ -141,9 +146,9 @@ export class XHRConnection implements Connection {
}); });
} }
setDetectedContentType(req: any /** TODO #9100 */, _xhr: any /** TODO #9100 */) { setDetectedContentType(req: any /** TODO Request */, _xhr: any /** XMLHttpRequest */) {
// Skip if a custom Content-Type header is provided // Skip if a custom Content-Type header is provided
if (isPresent(req.headers) && isPresent(req.headers.get('Content-Type'))) { if (req.headers != null && req.headers.get('Content-Type') != null) {
return; return;
} }
@ -161,7 +166,7 @@ export class XHRConnection implements Connection {
_xhr.setRequestHeader('content-type', 'text/plain'); _xhr.setRequestHeader('content-type', 'text/plain');
break; break;
case ContentType.BLOB: case ContentType.BLOB:
let blob = req.blob(); const blob = req.blob();
if (blob.type) { if (blob.type) {
_xhr.setRequestHeader('content-type', blob.type); _xhr.setRequestHeader('content-type', blob.type);
} }
@ -185,7 +190,7 @@ export class CookieXSRFStrategy implements XSRFStrategy {
constructor( constructor(
private _cookieName: string = 'XSRF-TOKEN', private _headerName: string = 'X-XSRF-TOKEN') {} private _cookieName: string = 'XSRF-TOKEN', private _headerName: string = 'X-XSRF-TOKEN') {}
configureRequest(req: Request) { configureRequest(req: Request): void {
const xsrfToken = __platform_browser_private__.getDOM().getCookie(this._cookieName); const xsrfToken = __platform_browser_private__.getDOM().getCookie(this._cookieName);
if (xsrfToken) { if (xsrfToken) {
req.headers.set(this._headerName, xsrfToken); req.headers.set(this._headerName, xsrfToken);

View File

@ -8,8 +8,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {isPresent} from '../src/facade/lang';
import {RequestMethod, ResponseContentType} from './enums'; import {RequestMethod, ResponseContentType} from './enums';
import {Headers} from './headers'; import {Headers} from './headers';
import {normalizeMethodName} from './http_utils'; import {normalizeMethodName} from './http_utils';
@ -77,16 +75,14 @@ export class RequestOptions {
constructor( constructor(
{method, headers, body, url, search, withCredentials, {method, headers, body, url, search, withCredentials,
responseType}: RequestOptionsArgs = {}) { responseType}: RequestOptionsArgs = {}) {
this.method = isPresent(method) ? normalizeMethodName(method) : null; this.method = method != null ? normalizeMethodName(method) : null;
this.headers = isPresent(headers) ? headers : null; this.headers = headers != null ? headers : null;
this.body = isPresent(body) ? body : null; this.body = body != null ? body : null;
this.url = isPresent(url) ? url : null; this.url = url != null ? url : null;
this.search = isPresent(search) ? this.search =
(typeof search === 'string' ? new URLSearchParams(<string>(search)) : search != null ? (typeof search === 'string' ? new URLSearchParams(search) : search) : null;
<URLSearchParams>(search)) : this.withCredentials = withCredentials != null ? withCredentials : null;
null; this.responseType = responseType != null ? responseType : null;
this.withCredentials = isPresent(withCredentials) ? withCredentials : null;
this.responseType = isPresent(responseType) ? responseType : null;
} }
/** /**
@ -116,18 +112,18 @@ export class RequestOptions {
*/ */
merge(options?: RequestOptionsArgs): RequestOptions { merge(options?: RequestOptionsArgs): RequestOptions {
return new RequestOptions({ return new RequestOptions({
method: options && isPresent(options.method) ? options.method : this.method, method: options && options.method != null ? options.method : this.method,
headers: options && isPresent(options.headers) ? options.headers : this.headers, headers: options && options.headers != null ? options.headers : this.headers,
body: options && isPresent(options.body) ? options.body : this.body, body: options && options.body != null ? options.body : this.body,
url: options && isPresent(options.url) ? options.url : this.url, url: options && options.url != null ? options.url : this.url,
search: options && isPresent(options.search) ? search: options && options.search != null ?
(typeof options.search === 'string' ? new URLSearchParams(options.search) : (typeof options.search === 'string' ? new URLSearchParams(options.search) :
(<URLSearchParams>(options.search)).clone()) : options.search.clone()) :
this.search, this.search,
withCredentials: options && isPresent(options.withCredentials) ? options.withCredentials : withCredentials: options && options.withCredentials != null ? options.withCredentials :
this.withCredentials, this.withCredentials,
responseType: options && isPresent(options.responseType) ? options.responseType : responseType: options && options.responseType != null ? options.responseType :
this.responseType this.responseType
}); });
} }
} }

View File

@ -8,8 +8,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {isPresent} from '../src/facade/lang';
import {ResponseType} from './enums'; import {ResponseType} from './enums';
import {Headers} from './headers'; import {Headers} from './headers';
import {ResponseOptionsArgs} from './interfaces'; import {ResponseOptionsArgs} from './interfaces';
@ -68,12 +66,12 @@ export class ResponseOptions {
type: ResponseType; type: ResponseType;
url: string; url: string;
constructor({body, status, headers, statusText, type, url}: ResponseOptionsArgs = {}) { constructor({body, status, headers, statusText, type, url}: ResponseOptionsArgs = {}) {
this.body = isPresent(body) ? body : null; this.body = body != null ? body : null;
this.status = isPresent(status) ? status : null; this.status = status != null ? status : null;
this.headers = isPresent(headers) ? headers : null; this.headers = headers != null ? headers : null;
this.statusText = isPresent(statusText) ? statusText : null; this.statusText = statusText != null ? statusText : null;
this.type = isPresent(type) ? type : null; this.type = type != null ? type : null;
this.url = isPresent(url) ? url : null; this.url = url != null ? url : null;
} }
/** /**
@ -103,13 +101,12 @@ export class ResponseOptions {
*/ */
merge(options?: ResponseOptionsArgs): ResponseOptions { merge(options?: ResponseOptionsArgs): ResponseOptions {
return new ResponseOptions({ return new ResponseOptions({
body: isPresent(options) && isPresent(options.body) ? options.body : this.body, body: options && options.body != null ? options.body : this.body,
status: isPresent(options) && isPresent(options.status) ? options.status : this.status, status: options && options.status != null ? options.status : this.status,
headers: isPresent(options) && isPresent(options.headers) ? options.headers : this.headers, headers: options && options.headers != null ? options.headers : this.headers,
statusText: isPresent(options) && isPresent(options.statusText) ? options.statusText : statusText: options && options.statusText != null ? options.statusText : this.statusText,
this.statusText, type: options && options.type != null ? options.type : this.type,
type: isPresent(options) && isPresent(options.type) ? options.type : this.type, url: options && options.url != null ? options.url : this.url,
url: isPresent(options) && isPresent(options.url) ? options.url : this.url,
}); });
} }
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {isJsObject, stringToArrayBuffer} from './http_utils'; import {stringToArrayBuffer} from './http_utils';
import {URLSearchParams} from './url_search_params'; import {URLSearchParams} from './url_search_params';
@ -51,7 +51,7 @@ export abstract class Body {
return ''; return '';
} }
if (isJsObject(this._body)) { if (typeof this._body === 'object') {
return JSON.stringify(this._body, null, 2); return JSON.stringify(this._body, null, 2);
} }

View File

@ -1 +0,0 @@
../../facade/src

View File

@ -8,7 +8,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Observable} from 'rxjs/Observable'; import {Observable} from 'rxjs/Observable';
import {isPresent} from '../src/facade/lang';
import {BaseRequestOptions, RequestOptions} from './base_request_options'; import {BaseRequestOptions, RequestOptions} from './base_request_options';
import {RequestMethod} from './enums'; import {RequestMethod} from './enums';
import {ConnectionBackend, RequestOptionsArgs} from './interfaces'; import {ConnectionBackend, RequestOptionsArgs} from './interfaces';
@ -23,7 +22,7 @@ function mergeOptions(
defaultOpts: BaseRequestOptions, providedOpts: RequestOptionsArgs, method: RequestMethod, defaultOpts: BaseRequestOptions, providedOpts: RequestOptionsArgs, method: RequestMethod,
url: string): RequestOptions { url: string): RequestOptions {
const newOptions = defaultOpts; const newOptions = defaultOpts;
if (isPresent(providedOpts)) { if (providedOpts) {
// Hack so Dart can used named parameters // Hack so Dart can used named parameters
return newOptions.merge(new RequestOptions({ return newOptions.merge(new RequestOptions({
method: providedOpts.method || method, method: providedOpts.method || method,
@ -35,11 +34,8 @@ function mergeOptions(
responseType: providedOpts.responseType responseType: providedOpts.responseType
})); }));
} }
if (isPresent(method)) {
return newOptions.merge(new RequestOptions({method: method, url: url})); return newOptions.merge(new RequestOptions({method, url}));
} else {
return newOptions.merge(new RequestOptions({url: url}));
}
} }
/** /**

View File

@ -49,5 +49,3 @@ export function stringToArrayBuffer(input: String): ArrayBuffer {
} }
return view.buffer; return view.buffer;
} }
export {isJsObject} from '../src/facade/lang';

View File

@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {isPresent} from '../src/facade/lang';
import {Body} from './body'; import {Body} from './body';
import {ContentType, RequestMethod, ResponseContentType} from './enums'; import {ContentType, RequestMethod, ResponseContentType} from './enums';
import {Headers} from './headers'; import {Headers} from './headers';
@ -78,7 +76,7 @@ export class Request extends Body {
// TODO: assert that url is present // TODO: assert that url is present
const url = requestOptions.url; const url = requestOptions.url;
this.url = requestOptions.url; this.url = requestOptions.url;
if (isPresent(requestOptions.search)) { if (requestOptions.search) {
const search = requestOptions.search.toString(); const search = requestOptions.search.toString();
if (search.length > 0) { if (search.length > 0) {
let prefix = '?'; let prefix = '?';
@ -93,7 +91,6 @@ export class Request extends Body {
this.method = normalizeMethodName(requestOptions.method); this.method = normalizeMethodName(requestOptions.method);
// TODO(jeffbcross): implement behavior // TODO(jeffbcross): implement behavior
// Defaults to 'omit', consistent with browser // Defaults to 'omit', consistent with browser
// TODO(jeffbcross): implement behavior
this.headers = new Headers(requestOptions.headers); this.headers = new Headers(requestOptions.headers);
this.contentType = this.detectContentType(); this.contentType = this.detectContentType();
this.withCredentials = requestOptions.withCredentials; this.withCredentials = requestOptions.withCredentials;

View File

@ -14,7 +14,6 @@ import {JSONPBackend, JSONPBackend_, JSONPConnection, JSONPConnection_} from '..
import {BaseRequestOptions, RequestOptions} from '../../src/base_request_options'; import {BaseRequestOptions, RequestOptions} from '../../src/base_request_options';
import {BaseResponseOptions, ResponseOptions} from '../../src/base_response_options'; import {BaseResponseOptions, ResponseOptions} from '../../src/base_response_options';
import {ReadyState, RequestMethod, ResponseType} from '../../src/enums'; import {ReadyState, RequestMethod, ResponseType} from '../../src/enums';
import {isPresent} from '../../src/facade/lang';
import {Request} from '../../src/static_request'; import {Request} from '../../src/static_request';
let existingScripts: MockBrowserJsonp[] = []; let existingScripts: MockBrowserJsonp[] = [];
@ -22,18 +21,14 @@ let existingScripts: MockBrowserJsonp[] = [];
class MockBrowserJsonp extends BrowserJsonp { class MockBrowserJsonp extends BrowserJsonp {
src: string; src: string;
callbacks = new Map<string, (data: any) => any>(); callbacks = new Map<string, (data: any) => any>();
constructor() { super(); }
addEventListener(type: string, cb: (data: any) => any) { this.callbacks.set(type, cb); } addEventListener(type: string, cb: (data: any) => any) { this.callbacks.set(type, cb); }
removeEventListener(type: string, cb: Function) { this.callbacks.delete(type); } removeEventListener(type: string, cb: Function) { this.callbacks.delete(type); }
dispatchEvent(type: string, argument?: any) { dispatchEvent(type: string, argument: any = {}) {
if (!isPresent(argument)) {
argument = {};
}
const cb = this.callbacks.get(type); const cb = this.callbacks.get(type);
if (isPresent(cb)) { if (cb) {
cb(argument); cb(argument);
} }
} }

View File

@ -9,7 +9,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {AsyncTestCompleter, SpyObject, afterEach, beforeEach, beforeEachProviders, describe, expect, inject, it} from '@angular/core/testing/testing_internal'; import {AsyncTestCompleter, SpyObject, afterEach, beforeEach, beforeEachProviders, describe, expect, inject, it} from '@angular/core/testing/testing_internal';
import {__platform_browser_private__} from '@angular/platform-browser'; import {__platform_browser_private__} from '@angular/platform-browser';
import {BrowserXhr} from '../../src/backends/browser_xhr'; import {BrowserXhr} from '../../src/backends/browser_xhr';
import {CookieXSRFStrategy, XHRBackend, XHRConnection} from '../../src/backends/xhr_backend'; import {CookieXSRFStrategy, XHRBackend, XHRConnection} from '../../src/backends/xhr_backend';
import {BaseRequestOptions, RequestOptions} from '../../src/base_request_options'; import {BaseRequestOptions, RequestOptions} from '../../src/base_request_options';
@ -486,6 +485,7 @@ export function main() {
existingXHRs[0].setStatusCode(statusCode); existingXHRs[0].setStatusCode(statusCode);
existingXHRs[0].dispatchEvent('load'); existingXHRs[0].dispatchEvent('load');
})); }));
it('should normalize IE\'s 1223 status code into 204', it('should normalize IE\'s 1223 status code into 204',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const statusCode = 1223; const statusCode = 1223;
@ -502,6 +502,22 @@ export function main() {
existingXHRs[0].dispatchEvent('load'); existingXHRs[0].dispatchEvent('load');
})); }));
it('should ignore response body for 204 status code',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const statusCode = 204;
const connection = new XHRConnection(
sampleRequest, new MockBrowserXHR(), new ResponseOptions({status: statusCode}));
connection.response.subscribe((res: Response) => {
expect(res.text()).toBe('');
async.done();
});
existingXHRs[0].setStatusCode(statusCode);
existingXHRs[0].setResponseText('Doge');
existingXHRs[0].dispatchEvent('load');
}));
it('should normalize responseText and response', it('should normalize responseText and response',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const responseBody = 'Doge'; const responseBody = 'Doge';
@ -623,6 +639,21 @@ Connection: keep-alive`;
existingXHRs[0].dispatchEvent('load'); existingXHRs[0].dispatchEvent('load');
})); }));
it('should return request url if it cannot be retrieved from response',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const statusCode = 200;
const connection = new XHRConnection(
sampleRequest, new MockBrowserXHR(), new ResponseOptions({status: statusCode}));
connection.response.subscribe((res: Response) => {
expect(res.url).toEqual('https://google.com');
async.done();
});
existingXHRs[0].setStatusCode(statusCode);
existingXHRs[0].dispatchEvent('load');
}));
it('should set the status text property from the XMLHttpRequest instance if present', it('should set the status text property from the XMLHttpRequest instance if present',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const statusText = 'test'; const statusText = 'test';

View File

@ -303,6 +303,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
importIntoDoc(node: Node): any { return document.importNode(this.templateAwareRoot(node), true); } importIntoDoc(node: Node): any { return document.importNode(this.templateAwareRoot(node), true); }
adoptNode(node: Node): any { return document.adoptNode(node); } adoptNode(node: Node): any { return document.adoptNode(node); }
getHref(el: Element): string { return (<any>el).href; } getHref(el: Element): string { return (<any>el).href; }
getEventKey(event: any): string { getEventKey(event: any): string {
let key = event.key; let key = event.key;
if (isBlank(key)) { if (isBlank(key)) {

View File

@ -37,5 +37,7 @@ export function enableDebugTools<T>(ref: ComponentRef<T>): ComponentRef<T> {
* @experimental All debugging apis are currently experimental. * @experimental All debugging apis are currently experimental.
*/ */
export function disableDebugTools(): void { export function disableDebugTools(): void {
delete context.ng.profiler; if (context.ng) {
delete context.ng.profiler;
}
} }

View File

@ -13,7 +13,8 @@ import {AnimationKeyframe, AnimationStyles, NoOpAnimationPlayer} from '../privat
class _NoOpAnimationDriver implements AnimationDriver { class _NoOpAnimationDriver implements AnimationDriver {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return new NoOpAnimationPlayer(); return new NoOpAnimationPlayer();
} }
} }
@ -25,5 +26,6 @@ export abstract class AnimationDriver {
static NOOP: AnimationDriver = new _NoOpAnimationDriver(); static NOOP: AnimationDriver = new _NoOpAnimationDriver();
abstract animate( abstract animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer; duration: number, delay: number, easing: string,
previousPlayers?: AnimationPlayer[]): AnimationPlayer;
} }

View File

@ -6,17 +6,16 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ApplicationRef, DebugNode, NgZone, Optional, Provider, RootRenderer, getDebugNode, isDevMode} from '@angular/core'; import * as core from '@angular/core';
import {StringMapWrapper} from '../../facade/collection'; import {StringMapWrapper} from '../../facade/collection';
import {DebugDomRootRenderer} from '../../private_import_core'; import {DebugDomRootRenderer} from '../../private_import_core';
import {getDOM} from '../dom_adapter'; import {getDOM} from '../dom_adapter';
import {DomRootRenderer} from '../dom_renderer'; import {DomRootRenderer} from '../dom_renderer';
const CORE_TOKENS = { const CORE_TOKENS = {
'ApplicationRef': ApplicationRef, 'ApplicationRef': core.ApplicationRef,
'NgZone': NgZone 'NgZone': core.NgZone,
}; };
const INSPECT_GLOBAL_NAME = 'ng.probe'; const INSPECT_GLOBAL_NAME = 'ng.probe';
@ -27,26 +26,27 @@ const CORE_TOKENS_GLOBAL_NAME = 'ng.coreTokens';
* null if the given native element does not have an Angular view associated * null if the given native element does not have an Angular view associated
* with it. * with it.
*/ */
export function inspectNativeElement(element: any /** TODO #9100 */): DebugNode { export function inspectNativeElement(element: any): core.DebugNode {
return getDebugNode(element); return core.getDebugNode(element);
} }
/** /**
* @experimental * Deprecated. Use the one from '@angular/core'.
* @deprecated
*/ */
export class NgProbeToken { export class NgProbeToken {
constructor(private name: string, private token: any) {} constructor(public name: string, public token: any) {}
} }
export function _createConditionalRootRenderer( export function _createConditionalRootRenderer(
rootRenderer: any /** TODO #9100 */, extraTokens: NgProbeToken[]) { rootRenderer: any, extraTokens: NgProbeToken[], coreTokens: core.NgProbeToken[]) {
if (isDevMode()) { return core.isDevMode() ?
return _createRootRenderer(rootRenderer, extraTokens); _createRootRenderer(rootRenderer, (extraTokens || []).concat(coreTokens || [])) :
} rootRenderer;
return rootRenderer;
} }
function _createRootRenderer(rootRenderer: any /** TODO #9100 */, extraTokens: NgProbeToken[]) { function _createRootRenderer(rootRenderer: any, extraTokens: NgProbeToken[]) {
getDOM().setGlobalVar(INSPECT_GLOBAL_NAME, inspectNativeElement); getDOM().setGlobalVar(INSPECT_GLOBAL_NAME, inspectNativeElement);
getDOM().setGlobalVar( getDOM().setGlobalVar(
CORE_TOKENS_GLOBAL_NAME, CORE_TOKENS_GLOBAL_NAME,
@ -61,14 +61,11 @@ function _ngProbeTokensToMap(tokens: NgProbeToken[]): {[name: string]: any} {
/** /**
* Providers which support debugging Angular applications (e.g. via `ng.probe`). * Providers which support debugging Angular applications (e.g. via `ng.probe`).
*/ */
export const ELEMENT_PROBE_PROVIDERS: Provider[] = [{ export const ELEMENT_PROBE_PROVIDERS: core.Provider[] = [{
provide: RootRenderer, provide: core.RootRenderer,
useFactory: _createConditionalRootRenderer, useFactory: _createConditionalRootRenderer,
deps: [DomRootRenderer, [NgProbeToken, new Optional()]] deps: [
}]; DomRootRenderer, [NgProbeToken, new core.Optional()],
[core.NgProbeToken, new core.Optional()]
export const ELEMENT_PROBE_PROVIDERS_PROD_MODE: any[] = [{ ]
provide: RootRenderer,
useFactory: _createRootRenderer,
deps: [DomRootRenderer, [NgProbeToken, new Optional()]]
}]; }];

View File

@ -260,9 +260,10 @@ export class DomRenderer implements Renderer {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return this._animationDriver.animate( return this._animationDriver.animate(
element, startingStyles, keyframes, duration, delay, easing); element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} }
} }

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AnimationPlayer} from '@angular/core';
import {isPresent} from '../facade/lang'; import {isPresent} from '../facade/lang';
import {AnimationKeyframe, AnimationStyles} from '../private_import_core'; import {AnimationKeyframe, AnimationStyles} from '../private_import_core';
@ -15,17 +16,18 @@ import {WebAnimationsPlayer} from './web_animations_player';
export class WebAnimationsDriver implements AnimationDriver { export class WebAnimationsDriver implements AnimationDriver {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): WebAnimationsPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): WebAnimationsPlayer {
let formattedSteps: {[key: string]: string | number}[] = []; let formattedSteps: {[key: string]: string | number}[] = [];
let startingStyleLookup: {[key: string]: string | number} = {}; let startingStyleLookup: {[key: string]: string | number} = {};
if (isPresent(startingStyles) && startingStyles.styles.length > 0) { if (isPresent(startingStyles) && startingStyles.styles.length > 0) {
startingStyleLookup = _populateStyles(element, startingStyles, {}); startingStyleLookup = _populateStyles(startingStyles, {});
startingStyleLookup['offset'] = 0; startingStyleLookup['offset'] = 0;
formattedSteps.push(startingStyleLookup); formattedSteps.push(startingStyleLookup);
} }
keyframes.forEach((keyframe: AnimationKeyframe) => { keyframes.forEach((keyframe: AnimationKeyframe) => {
const data = _populateStyles(element, keyframe.styles, startingStyleLookup); const data = _populateStyles(keyframe.styles, startingStyleLookup);
data['offset'] = keyframe.offset; data['offset'] = keyframe.offset;
formattedSteps.push(data); formattedSteps.push(data);
}); });
@ -52,13 +54,16 @@ export class WebAnimationsDriver implements AnimationDriver {
playerOptions['easing'] = easing; playerOptions['easing'] = easing;
} }
return new WebAnimationsPlayer(element, formattedSteps, playerOptions); // there may be a chance a NoOp player is returned depending
// on when the previous animation was cancelled
previousPlayers = previousPlayers.filter(filterWebAnimationPlayerFn);
return new WebAnimationsPlayer(
element, formattedSteps, playerOptions, <WebAnimationsPlayer[]>previousPlayers);
} }
} }
function _populateStyles( function _populateStyles(styles: AnimationStyles, defaultStyles: {[key: string]: string | number}):
element: any, styles: AnimationStyles, {[key: string]: string | number} {
defaultStyles: {[key: string]: string | number}): {[key: string]: string | number} {
const data: {[key: string]: string | number} = {}; const data: {[key: string]: string | number} = {};
styles.styles.forEach( styles.styles.forEach(
(entry) => { Object.keys(entry).forEach(prop => { data[prop] = entry[prop]; }); }); (entry) => { Object.keys(entry).forEach(prop => { data[prop] = entry[prop]; }); });
@ -69,3 +74,7 @@ function _populateStyles(
}); });
return data; return data;
} }
function filterWebAnimationPlayerFn(player: AnimationPlayer) {
return player instanceof WebAnimationsPlayer;
}

View File

@ -7,6 +7,8 @@
*/ */
import {AUTO_STYLE} from '@angular/core'; import {AUTO_STYLE} from '@angular/core';
import {isPresent} from '../facade/lang';
import {AnimationPlayer} from '../private_import_core'; import {AnimationPlayer} from '../private_import_core';
import {getDOM} from './dom_adapter'; import {getDOM} from './dom_adapter';
@ -21,13 +23,22 @@ export class WebAnimationsPlayer implements AnimationPlayer {
private _finished = false; private _finished = false;
private _started = false; private _started = false;
private _destroyed = false; private _destroyed = false;
private _finalKeyframe: {[key: string]: string | number};
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
public previousStyles: {[styleName: string]: string | number};
constructor( constructor(
public element: any, public keyframes: {[key: string]: string | number}[], public element: any, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number}) { public options: {[key: string]: string | number},
previousPlayers: WebAnimationsPlayer[] = []) {
this._duration = <number>options['duration']; this._duration = <number>options['duration'];
this.previousStyles = {};
previousPlayers.forEach(player => {
let styles = player._captureStyles();
Object.keys(styles).forEach(prop => this.previousStyles[prop] = styles[prop]);
});
} }
private _onFinish() { private _onFinish() {
@ -44,14 +55,30 @@ export class WebAnimationsPlayer implements AnimationPlayer {
const keyframes = this.keyframes.map(styles => { const keyframes = this.keyframes.map(styles => {
const formattedKeyframe: {[key: string]: string | number} = {}; const formattedKeyframe: {[key: string]: string | number} = {};
Object.keys(styles).forEach(prop => { Object.keys(styles).forEach((prop, index) => {
const value = styles[prop]; let value = styles[prop];
formattedKeyframe[prop] = value == AUTO_STYLE ? _computeStyle(this.element, prop) : value; if (value == AUTO_STYLE) {
value = _computeStyle(this.element, prop);
}
if (value != undefined) {
formattedKeyframe[prop] = value;
}
}); });
return formattedKeyframe; return formattedKeyframe;
}); });
const previousStyleProps = Object.keys(this.previousStyles);
if (previousStyleProps.length) {
let startingKeyframe = findStartingKeyframe(keyframes);
previousStyleProps.forEach(prop => {
if (isPresent(startingKeyframe[prop])) {
startingKeyframe[prop] = this.previousStyles[prop];
}
});
}
this._player = this._triggerWebAnimation(this.element, keyframes, this.options); this._player = this._triggerWebAnimation(this.element, keyframes, this.options);
this._finalKeyframe = _copyKeyframeStyles(keyframes[keyframes.length - 1]);
// this is required so that the player doesn't start to animate right away // this is required so that the player doesn't start to animate right away
this._resetDomPlayerState(); this._resetDomPlayerState();
@ -119,8 +146,47 @@ export class WebAnimationsPlayer implements AnimationPlayer {
setPosition(p: number): void { this._player.currentTime = p * this.totalTime; } setPosition(p: number): void { this._player.currentTime = p * this.totalTime; }
getPosition(): number { return this._player.currentTime / this.totalTime; } getPosition(): number { return this._player.currentTime / this.totalTime; }
private _captureStyles(): {[prop: string]: string | number} {
const styles: {[key: string]: string | number} = {};
if (this.hasStarted()) {
Object.keys(this._finalKeyframe).forEach(prop => {
if (prop != 'offset') {
styles[prop] =
this._finished ? this._finalKeyframe[prop] : _computeStyle(this.element, prop);
}
});
}
return styles;
}
} }
function _computeStyle(element: any, prop: string): string { function _computeStyle(element: any, prop: string): string {
return getDOM().getComputedStyle(element)[prop]; return getDOM().getComputedStyle(element)[prop];
} }
function _copyKeyframeStyles(styles: {[style: string]: string | number}):
{[style: string]: string | number} {
const newStyles: {[style: string]: string | number} = {};
Object.keys(styles).forEach(prop => {
if (prop != 'offset') {
newStyles[prop] = styles[prop];
}
});
return newStyles;
}
function findStartingKeyframe(keyframes: {[prop: string]: string | number}[]):
{[prop: string]: string | number} {
let startingKeyframe = keyframes[0];
// it's important that we find the LAST keyframe
// to ensure that style overidding is final.
for (let i = 1; i < keyframes.length; i++) {
const kf = keyframes[i];
const offset = kf['offset'];
if (offset !== 0) break;
startingKeyframe = kf;
}
return startingKeyframe;
}

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal'; import {AnimationPlayer} from '@angular/core';
import {el} from '@angular/platform-browser/testing/browser_util'; import {el} from '@angular/platform-browser/testing/browser_util';
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player'; import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
import {WebAnimationsDriver} from '../../src/dom/web_animations_driver'; import {WebAnimationsDriver} from '../../src/dom/web_animations_driver';
import {WebAnimationsPlayer} from '../../src/dom/web_animations_player'; import {WebAnimationsPlayer} from '../../src/dom/web_animations_player';
import {AnimationKeyframe, AnimationStyles} from '../../src/private_import_core'; import {AnimationKeyframe, AnimationStyles, NoOpAnimationPlayer} from '../../src/private_import_core';
import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player'; import {MockDomAnimatePlayer} from '../../testing/mock_dom_animate_player';
class ExtendedWebAnimationsDriver extends WebAnimationsDriver { class ExtendedWebAnimationsDriver extends WebAnimationsDriver {
@ -48,8 +48,7 @@ export function main() {
it('should use a fill mode of `both`', () => { it('should use a fill mode of `both`', () => {
const startingStyles = _makeStyles({}); const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear', []);
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'linear');
const details = _formatOptions(player); const details = _formatOptions(player);
const options = details['options']; const options = details['options'];
expect(options['fill']).toEqual('both'); expect(options['fill']).toEqual('both');
@ -58,8 +57,7 @@ export function main() {
it('should apply the provided easing', () => { it('should apply the provided easing', () => {
const startingStyles = _makeStyles({}); const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out', []);
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, 'ease-out');
const details = _formatOptions(player); const details = _formatOptions(player);
const options = details['options']; const options = details['options'];
expect(options['easing']).toEqual('ease-out'); expect(options['easing']).toEqual('ease-out');
@ -68,16 +66,32 @@ export function main() {
it('should only apply the provided easing if present', () => { it('should only apply the provided easing if present', () => {
const startingStyles = _makeStyles({}); const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})]; const styles = [_makeKeyframe(0, {'color': 'green'}), _makeKeyframe(1, {'color': 'red'})];
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, null, []);
const player = driver.animate(elm, startingStyles, styles, 1000, 1000, null);
const details = _formatOptions(player); const details = _formatOptions(player);
const options = details['options']; const options = details['options'];
const keys = Object.keys(options); const keys = Object.keys(options);
expect(keys.indexOf('easing')).toEqual(-1); expect(keys.indexOf('easing')).toEqual(-1);
}); });
it('should only apply the provided easing if present', () => {
const previousPlayers = [
new NoOpAnimationPlayerWithStyles(),
new NoOpAnimationPlayerWithStyles(),
new NoOpAnimationPlayerWithStyles(),
];
const startingStyles = _makeStyles({});
const styles = [_makeKeyframe(0, {}), _makeKeyframe(1, {})];
const player = driver.animate(
elm, startingStyles, styles, 1000, 1000, null, <AnimationPlayer[]>previousPlayers);
expect(player.previousStyles).toEqual({});
});
}); });
} }
class NoOpAnimationPlayerWithStyles extends NoOpAnimationPlayer {
private _captureStyles() { return {color: 'red'}; }
}
function _formatOptions(player: WebAnimationsPlayer): {[key: string]: any} { function _formatOptions(player: WebAnimationsPlayer): {[key: string]: any} {
return {'element': player.element, 'keyframes': player.keyframes, 'options': player.options}; return {'element': player.element, 'keyframes': player.keyframes, 'options': player.options};
} }

View File

@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {MockAnimationPlayer, beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal'; import {AUTO_STYLE, AnimationPlayer} from '@angular/core';
import {MockAnimationPlayer} from '@angular/core/testing/testing_internal';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {el} from '@angular/platform-browser/testing/browser_util'; import {el} from '@angular/platform-browser/testing/browser_util';
import {DomAnimatePlayer} from '../../src/dom/dom_animate_player'; import {DomAnimatePlayer} from '../../src/dom/dom_animate_player';
@ -18,14 +20,16 @@ class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer {
constructor( constructor(
public element: HTMLElement, public keyframes: {[key: string]: string | number}[], public element: HTMLElement, public keyframes: {[key: string]: string | number}[],
public options: {[key: string]: string | number}) { public options: {[key: string]: string | number},
super(element, keyframes, options); public previousPlayers: WebAnimationsPlayer[] = []) {
super(element, keyframes, options, previousPlayers);
} }
get domPlayer() { return this._overriddenDomPlayer; } get domPlayer() { return this._overriddenDomPlayer; }
/** @internal */ /** @internal */
_triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer { _triggerWebAnimation(elm: any, keyframes: any[], options: any): DomAnimatePlayer {
this._overriddenDomPlayer._capture('trigger', {elm, keyframes, options});
return this._overriddenDomPlayer; return this._overriddenDomPlayer;
} }
} }
@ -33,7 +37,7 @@ class ExtendedWebAnimationsPlayer extends WebAnimationsPlayer {
export function main() { export function main() {
function makePlayer(): {[key: string]: any} { function makePlayer(): {[key: string]: any} {
const someElm = el('<div></div>'); const someElm = el('<div></div>');
const player = new ExtendedWebAnimationsPlayer(someElm, [], {}); const player = new ExtendedWebAnimationsPlayer(someElm, [{}, {}], {}, []);
player.init(); player.init();
return {'captures': player.domPlayer.captures, 'player': player}; return {'captures': player.domPlayer.captures, 'player': player};
} }
@ -156,5 +160,72 @@ export function main() {
player.destroy(); player.destroy();
expect(captures['cancel'].length).toBe(1); expect(captures['cancel'].length).toBe(1);
}); });
it('should resolve auto styles based on what is computed from the provided element', () => {
const elm = el('<div></div>');
document.body.appendChild(elm); // required for getComputedStyle() to work
elm.style.opacity = '0.5';
const player = new ExtendedWebAnimationsPlayer(
elm, [{opacity: AUTO_STYLE}, {opacity: '1'}], {duration: 1000}, []);
player.init();
const data = player.domPlayer.captures['trigger'][0];
expect(data['keyframes']).toEqual([{opacity: '0.5'}, {opacity: '1'}]);
});
describe('previousStyle', () => {
it('should merge keyframe styles based on the previous styles passed in when the player has finished its operation',
() => {
const elm = el('<div></div>');
const previousStyles = {width: '100px', height: '666px'};
const previousPlayer =
new ExtendedWebAnimationsPlayer(elm, [previousStyles, previousStyles], {}, []);
previousPlayer.play();
previousPlayer.finish();
const player = new ExtendedWebAnimationsPlayer(
elm,
[
{width: '0px', height: '0px', opacity: 0, offset: 0},
{width: '0px', height: '0px', opacity: 1, offset: 1}
],
{duration: 1000}, [previousPlayer]);
player.init();
const data = player.domPlayer.captures['trigger'][0];
expect(data['keyframes']).toEqual([
{width: '100px', height: '666px', opacity: 0, offset: 0},
{width: '0px', height: '0px', opacity: 1, offset: 1}
]);
});
it('should properly calculate the previous styles for the player even when its currently playing',
() => {
if (!getDOM().supportsWebAnimation()) return;
const elm = el('<div></div>');
document.body.appendChild(elm);
const fromStyles = {width: '100px', height: '666px'};
const toStyles = {width: '50px', height: '333px'};
const previousPlayer =
new WebAnimationsPlayer(elm, [fromStyles, toStyles], {duration: 1000}, []);
previousPlayer.play();
previousPlayer.setPosition(0.5);
previousPlayer.pause();
const newStyles = {width: '0px', height: '0px'};
const player = new WebAnimationsPlayer(
elm, [newStyles, newStyles], {duration: 1000}, [previousPlayer]);
player.init();
const data = player.previousStyles;
expect(player.previousStyles).toEqual({width: '75px', height: '499.5px'});
});
});
}); });
} }

View File

@ -9,19 +9,29 @@
import {AnimationPlayer} from '@angular/core'; import {AnimationPlayer} from '@angular/core';
import {MockAnimationPlayer} from '@angular/core/testing/testing_internal'; import {MockAnimationPlayer} from '@angular/core/testing/testing_internal';
import {AnimationDriver} from '@angular/platform-browser'; import {AnimationDriver} from '@angular/platform-browser';
import {ListWrapper} from './facade/collection';
import {AnimationKeyframe, AnimationStyles} from './private_import_core'; import {AnimationKeyframe, AnimationStyles} from './private_import_core';
export class MockAnimationDriver extends AnimationDriver { export class MockAnimationDriver extends AnimationDriver {
public log: {[key: string]: any}[] = []; public log: {[key: string]: any}[] = [];
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
const player = new MockAnimationPlayer(); previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
const mockPlayers = <MockAnimationPlayer[]>previousPlayers.filter(
player => player instanceof MockAnimationPlayer);
const normalizedStartingStyles = _serializeStyles(startingStyles);
const normalizedKeyframes = _serializeKeyframes(keyframes);
const player =
new MockAnimationPlayer(normalizedStartingStyles, normalizedKeyframes, previousPlayers);
this.log.push({ this.log.push({
'element': element, 'element': element,
'startingStyles': _serializeStyles(startingStyles), 'startingStyles': normalizedStartingStyles,
'previousStyles': player.previousStyles,
'keyframes': keyframes, 'keyframes': keyframes,
'keyframeLookup': _serializeKeyframes(keyframes), 'keyframeLookup': normalizedKeyframes,
'duration': duration, 'duration': duration,
'delay': delay, 'delay': delay,
'easing': easing, 'easing': easing,

View File

@ -206,9 +206,10 @@ export class ServerRenderer implements Renderer {
animate( animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
return this._animationDriver.animate( return this._animationDriver.animate(
element, startingStyles, keyframes, duration, delay, easing); element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} }
} }

View File

@ -89,7 +89,7 @@ export class MessageBasedRenderer {
'animate', 'animate',
[ [
RenderStoreObject, RenderStoreObject, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE, RenderStoreObject, RenderStoreObject, PRIMITIVE, PRIMITIVE, PRIMITIVE, PRIMITIVE,
PRIMITIVE, PRIMITIVE PRIMITIVE, PRIMITIVE, PRIMITIVE
], ],
this._animate.bind(this)); this._animate.bind(this));
@ -248,8 +248,14 @@ export class MessageBasedRenderer {
private _animate( private _animate(
renderer: Renderer, element: any, startingStyles: any, keyframes: any[], duration: number, renderer: Renderer, element: any, startingStyles: any, keyframes: any[], duration: number,
delay: number, easing: string, playerId: any) { delay: number, easing: string, previousPlayers: number[], playerId: any) {
const player = renderer.animate(element, startingStyles, keyframes, duration, delay, easing); let normalizedPreviousPlayers: AnimationPlayer[];
if (previousPlayers && previousPlayers.length) {
normalizedPreviousPlayers =
previousPlayers.map(playerId => this._renderStore.deserialize(playerId));
}
const player = renderer.animate(
element, startingStyles, keyframes, duration, delay, easing, normalizedPreviousPlayers);
this._renderStore.store(player, playerId); this._renderStore.store(player, playerId);
} }

View File

@ -16,6 +16,7 @@ import {MessageBus} from '../shared/message_bus';
import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api'; import {EVENT_CHANNEL, RENDERER_CHANNEL} from '../shared/messaging_api';
import {RenderStore} from '../shared/render_store'; import {RenderStore} from '../shared/render_store';
import {ANIMATION_WORKER_PLAYER_PREFIX, RenderStoreObject, Serializer} from '../shared/serializer'; import {ANIMATION_WORKER_PLAYER_PREFIX, RenderStoreObject, Serializer} from '../shared/serializer';
import {deserializeGenericEvent} from './event_deserializer'; import {deserializeGenericEvent} from './event_deserializer';
@Injectable() @Injectable()
@ -239,13 +240,16 @@ export class WebWorkerRenderer implements Renderer, RenderStoreObject {
animate( animate(
renderElement: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], renderElement: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): AnimationPlayer { duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
const playerId = this._rootRenderer.allocateId(); const playerId = this._rootRenderer.allocateId();
const previousPlayerIds: number[] =
previousPlayers.map(player => this._rootRenderer.renderStore.serialize(player));
this._runOnService('animate', [ this._runOnService('animate', [
new FnArg(renderElement, RenderStoreObject), new FnArg(startingStyles, null), new FnArg(renderElement, RenderStoreObject), new FnArg(startingStyles, null),
new FnArg(keyframes, null), new FnArg(duration, null), new FnArg(delay, null), new FnArg(keyframes, null), new FnArg(duration, null), new FnArg(delay, null),
new FnArg(easing, null), new FnArg(playerId, null) new FnArg(easing, null), new FnArg(previousPlayerIds, null), new FnArg(playerId, null)
]); ]);
const player = new _AnimationWorkerRendererPlayer(this._rootRenderer, renderElement); const player = new _AnimationWorkerRendererPlayer(this._rootRenderer, renderElement);
@ -325,7 +329,7 @@ export class WebWorkerRenderNode {
animationPlayerEvents = new AnimationPlayerEmitter(); animationPlayerEvents = new AnimationPlayerEmitter();
} }
class _AnimationWorkerRendererPlayer implements AnimationPlayer, RenderStoreObject { class _AnimationWorkerRendererPlayer implements RenderStoreObject {
public parentPlayer: AnimationPlayer = null; public parentPlayer: AnimationPlayer = null;
private _destroyed: boolean = false; private _destroyed: boolean = false;

View File

@ -289,6 +289,30 @@ export function main() {
expect(player.log.indexOf('destroy') >= 0).toBe(true); expect(player.log.indexOf('destroy') >= 0).toBe(true);
})); }));
it('should properly transition to the next animation if the current one is cancelled',
fakeAsync(() => {
const fixture = TestBed.createComponent(AnimationCmp);
const cmp = fixture.componentInstance;
cmp.state = 'on';
fixture.detectChanges();
flushMicrotasks();
let player = <MockAnimationPlayer>uiDriver.log.shift()['player'];
player.finish();
player = <MockAnimationPlayer>uiDriver.log.shift()['player'];
player.setPosition(0.5);
uiDriver.log = [];
cmp.state = 'off';
fixture.detectChanges();
flushMicrotasks();
const step = uiDriver.log.shift();
expect(step['previousStyles']).toEqual({opacity: AUTO_STYLE, fontSize: AUTO_STYLE});
}));
}); });
} }

View File

@ -0,0 +1,5 @@
/**
* @license Angular v0.0.0-ROUTERPLACEHOLDER
* (c) 2010-2016 Google, Inc. https://angular.io/
* License: MIT
*/

View File

@ -1,6 +1,6 @@
{ {
"name": "@angular/router", "name": "@angular/router",
"version": "3.0.0-rc.1", "version": "0.0.0-ROUTERPLACEHOLDER",
"description": "Angular - the routing library", "description": "Angular - the routing library",
"main": "bundles/router.umd.js", "main": "bundles/router.umd.js",
"module": "index.js", "module": "index.js",
@ -24,7 +24,6 @@
"@angular/core": "0.0.0-PLACEHOLDER", "@angular/core": "0.0.0-PLACEHOLDER",
"@angular/common": "0.0.0-PLACEHOLDER", "@angular/common": "0.0.0-PLACEHOLDER",
"@angular/platform-browser": "0.0.0-PLACEHOLDER", "@angular/platform-browser": "0.0.0-PLACEHOLDER",
"@angular/upgrade": "0.0.0-PLACEHOLDER",
"rxjs": "5.0.0-beta.12" "rxjs": "5.0.0-beta.12"
} }
} }

View File

@ -154,6 +154,9 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree';
* When navigating to `/team/11/user/jim`, the router will instantiate the wrapper component with * When navigating to `/team/11/user/jim`, the router will instantiate the wrapper component with
* the user component in it. * the user component in it.
* *
* An empty path route inherits its parent's params and data. This is because it cannot have its
* own params, and, as a result, it often uses its parent's params and data as its own.
*
* ### Matching Strategy * ### Matching Strategy
* *
* By default the router will look at what is left in the url, and check if it starts with * By default the router will look at what is left in the url, and check if it starts with
@ -219,7 +222,8 @@ import {UrlSegment, UrlSegmentGroup} from './url_tree';
* has to have the primary and aux outlets defined. * has to have the primary and aux outlets defined.
* *
* The router will also merge the `params`, `data`, and `resolve` of the componentless parent into * The router will also merge the `params`, `data`, and `resolve` of the componentless parent into
* the `params`, `data`, and `resolve` of the children. * the `params`, `data`, and `resolve` of the children. This is done because there is no component
* that can inject the activated route of the componentless parent.
* *
* This is especially useful when child components are defined as follows: * This is especially useful when child components are defined as follows:
* *

View File

@ -89,11 +89,13 @@ import {UrlTree} from '../url_tree';
*/ */
@Directive({selector: ':not(a)[routerLink]'}) @Directive({selector: ':not(a)[routerLink]'})
export class RouterLink { export class RouterLink {
private commands: any[] = [];
@Input() queryParams: {[k: string]: any}; @Input() queryParams: {[k: string]: any};
@Input() fragment: string; @Input() fragment: string;
@Input() preserveQueryParams: boolean; @Input() preserveQueryParams: boolean;
@Input() preserveFragment: boolean; @Input() preserveFragment: boolean;
@Input() skipLocationChange: boolean;
@Input() replaceUrl: boolean;
private commands: any[] = [];
constructor( constructor(
private router: Router, private route: ActivatedRoute, private router: Router, private route: ActivatedRoute,
@ -120,7 +122,9 @@ export class RouterLink {
queryParams: this.queryParams, queryParams: this.queryParams,
fragment: this.fragment, fragment: this.fragment,
preserveQueryParams: toBool(this.preserveQueryParams), preserveQueryParams: toBool(this.preserveQueryParams),
preserveFragment: toBool(this.preserveFragment) preserveFragment: toBool(this.preserveFragment),
skipLocationChange: toBool(this.skipLocationChange),
replaceUrl: toBool(this.replaceUrl),
}); });
} }
} }
@ -138,12 +142,14 @@ export class RouterLink {
@Directive({selector: 'a[routerLink]'}) @Directive({selector: 'a[routerLink]'})
export class RouterLinkWithHref implements OnChanges, OnDestroy { export class RouterLinkWithHref implements OnChanges, OnDestroy {
@Input() target: string; @Input() target: string;
private commands: any[] = [];
@Input() queryParams: {[k: string]: any}; @Input() queryParams: {[k: string]: any};
@Input() fragment: string; @Input() fragment: string;
@Input() routerLinkOptions: {preserveQueryParams: boolean, preserveFragment: boolean}; @Input() routerLinkOptions: {preserveQueryParams: boolean, preserveFragment: boolean};
@Input() preserveQueryParams: boolean; @Input() preserveQueryParams: boolean;
@Input() preserveFragment: boolean; @Input() preserveFragment: boolean;
@Input() skipLocationChange: boolean;
@Input() replaceUrl: boolean;
private commands: any[] = [];
private subscription: Subscription; private subscription: Subscription;
// the url displayed on the anchor element. // the url displayed on the anchor element.
@ -195,7 +201,9 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
queryParams: this.queryParams, queryParams: this.queryParams,
fragment: this.fragment, fragment: this.fragment,
preserveQueryParams: toBool(this.preserveQueryParams), preserveQueryParams: toBool(this.preserveQueryParams),
preserveFragment: toBool(this.preserveFragment) preserveFragment: toBool(this.preserveFragment),
skipLocationChange: toBool(this.skipLocationChange),
replaceUrl: toBool(this.replaceUrl),
}); });
} }
} }

View File

@ -623,7 +623,8 @@ export class Router {
Promise.resolve() Promise.resolve()
.then( .then(
(_) => this.runNavigate( (_) => this.runNavigate(
url, rawUrl, false, false, id, createEmptyState(url, this.rootComponentType))) url, rawUrl, false, false, id,
createEmptyState(url, this.rootComponentType).snapshot))
.then(resolve, reject); .then(resolve, reject);
} else { } else {
@ -634,7 +635,7 @@ export class Router {
private runNavigate( private runNavigate(
url: UrlTree, rawUrl: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean, url: UrlTree, rawUrl: UrlTree, shouldPreventPushState: boolean, shouldReplaceUrl: boolean,
id: number, precreatedState: RouterState): Promise<boolean> { id: number, precreatedState: RouterStateSnapshot): Promise<boolean> {
if (id !== this.navigationId) { if (id !== this.navigationId) {
this.location.go(this.urlSerializer.serialize(this.currentUrlTree)); this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
this.routerEvents.next(new NavigationCancel( this.routerEvents.next(new NavigationCancel(
@ -644,68 +645,80 @@ export class Router {
} }
return new Promise((resolvePromise, rejectPromise) => { return new Promise((resolvePromise, rejectPromise) => {
let state: RouterState; // create an observable of the url and route state snapshot
let navigationIsSuccessful: boolean; // this operation do not result in any side effects
let preActivation: PreActivation; let urlAndSnapshot$: Observable<{appliedUrl: UrlTree, snapshot: RouterStateSnapshot}>;
let appliedUrl: UrlTree;
const storedState = this.currentRouterState;
const storedUrl = this.currentUrlTree;
let routerState$: any;
if (!precreatedState) { if (!precreatedState) {
const redirectsApplied$ = const redirectsApplied$ =
applyRedirects(this.injector, this.configLoader, url, this.config); applyRedirects(this.injector, this.configLoader, url, this.config);
const snapshot$ = mergeMap.call(redirectsApplied$, (u: UrlTree) => { urlAndSnapshot$ = mergeMap.call(redirectsApplied$, (appliedUrl: UrlTree) => {
appliedUrl = u; return map.call(
return recognize( recognize(
this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)); this.rootComponentType, this.config, appliedUrl, this.serializeUrl(appliedUrl)),
}); (snapshot: any) => {
const emitRecognzied$ = this.routerEvents.next(new RoutesRecognized(
map.call(snapshot$, (newRouterStateSnapshot: RouterStateSnapshot) => { id, this.serializeUrl(url), this.serializeUrl(appliedUrl), snapshot));
this.routerEvents.next(new RoutesRecognized(
id, this.serializeUrl(url), this.serializeUrl(appliedUrl),
newRouterStateSnapshot));
return newRouterStateSnapshot;
});
routerState$ = map.call(emitRecognzied$, (routerStateSnapshot: RouterStateSnapshot) => { return {appliedUrl, snapshot};
return createRouterState(routerStateSnapshot, this.currentRouterState); });
}); });
} else { } else {
appliedUrl = url; urlAndSnapshot$ = of ({appliedUrl: url, snapshot: precreatedState});
routerState$ = of (precreatedState);
} }
const preactivation$ = map.call(routerState$, (newState: RouterState) => {
state = newState; // run preactivation: guards and data resolvers
let preActivation: PreActivation;
const preactivationTraverse$ = map.call(urlAndSnapshot$, ({appliedUrl, snapshot}: any) => {
preActivation = preActivation =
new PreActivation(state.snapshot, this.currentRouterState.snapshot, this.injector); new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
preActivation.traverse(this.outletMap); preActivation.traverse(this.outletMap);
return {appliedUrl, snapshot};
}); });
const preactivation2$ = mergeMap.call(preactivation$, () => { const preactivationCheckGuards =
mergeMap.call(preactivationTraverse$, ({appliedUrl, snapshot}: any) => {
if (this.navigationId !== id) return of (false);
return map.call(preActivation.checkGuards(), (shouldActivate: boolean) => {
return {appliedUrl: appliedUrl, snapshot: snapshot, shouldActivate: shouldActivate};
});
});
const preactivationResolveData$ = mergeMap.call(preactivationCheckGuards, (p: any) => {
if (this.navigationId !== id) return of (false); if (this.navigationId !== id) return of (false);
return preActivation.checkGuards(); if (p.shouldActivate) {
}); return map.call(preActivation.resolveData(), () => p);
const resolveData$ = mergeMap.call(preactivation2$, (shouldActivate: boolean) => {
if (this.navigationId !== id) return of (false);
if (shouldActivate) {
return map.call(preActivation.resolveData(), () => shouldActivate);
} else { } else {
return of (shouldActivate); return of (p);
} }
}); });
resolveData$
.forEach((shouldActivate: boolean) => { // create router state
// this operation has side effects => route state is being affected
const routerState$ =
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
if (shouldActivate) {
const state = createRouterState(snapshot, this.currentRouterState);
return {appliedUrl, state, shouldActivate};
} else {
return {appliedUrl, state: null, shouldActivate};
}
});
// applied the new router state
// this operation has side effects
let navigationIsSuccessful: boolean;
const storedState = this.currentRouterState;
const storedUrl = this.currentUrlTree;
routerState$
.forEach(({appliedUrl, state, shouldActivate}: any) => {
if (!shouldActivate || id !== this.navigationId) { if (!shouldActivate || id !== this.navigationId) {
navigationIsSuccessful = false; navigationIsSuccessful = false;
return; return;
@ -733,8 +746,8 @@ export class Router {
() => { () => {
this.navigated = true; this.navigated = true;
if (navigationIsSuccessful) { if (navigationIsSuccessful) {
this.routerEvents.next( this.routerEvents.next(new NavigationEnd(
new NavigationEnd(id, this.serializeUrl(url), this.serializeUrl(appliedUrl))); id, this.serializeUrl(url), this.serializeUrl(this.currentUrlTree)));
resolvePromise(true); resolvePromise(true);
} else { } else {
this.resetUrlToCurrentUrlTree(); this.resetUrlToCurrentUrlTree();

View File

@ -7,7 +7,7 @@
*/ */
import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common'; import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, ApplicationRef, Compiler, ComponentRef, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, OpaqueToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core'; import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, ApplicationRef, Compiler, ComponentRef, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, OpaqueToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
import {Route, Routes} from './config'; import {Route, Routes} from './config';
import {RouterLink, RouterLinkWithHref} from './directives/router_link'; import {RouterLink, RouterLinkWithHref} from './directives/router_link';
import {RouterLinkActive} from './directives/router_link_active'; import {RouterLinkActive} from './directives/router_link_active';
@ -40,17 +40,10 @@ export const ROUTER_CONFIGURATION = new OpaqueToken('ROUTER_CONFIGURATION');
*/ */
export const ROUTER_FORROOT_GUARD = new OpaqueToken('ROUTER_FORROOT_GUARD'); export const ROUTER_FORROOT_GUARD = new OpaqueToken('ROUTER_FORROOT_GUARD');
const pathLocationStrategy = {
provide: LocationStrategy,
useClass: PathLocationStrategy
};
const hashLocationStrategy = {
provide: LocationStrategy,
useClass: HashLocationStrategy
};
export const ROUTER_PROVIDERS: Provider[] = [ export const ROUTER_PROVIDERS: Provider[] = [
Location, {provide: UrlSerializer, useClass: DefaultUrlSerializer}, { Location,
{provide: UrlSerializer, useClass: DefaultUrlSerializer},
{
provide: Router, provide: Router,
useFactory: setupRouter, useFactory: setupRouter,
deps: [ deps: [
@ -58,11 +51,19 @@ export const ROUTER_PROVIDERS: Provider[] = [
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()] Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()]
] ]
}, },
RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]}, RouterOutletMap,
{provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader}, RouterPreloader, NoPreloading, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
PreloadAllModules, {provide: ROUTER_CONFIGURATION, useValue: {enableTracing: false}} {provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader},
RouterPreloader,
NoPreloading,
PreloadAllModules,
{provide: ROUTER_CONFIGURATION, useValue: {enableTracing: false}},
]; ];
export function routerNgProbeToken() {
return new NgProbeToken('Router', Router);
}
/** /**
* @whatItDoes Adds router directives and providers. * @whatItDoes Adds router directives and providers.
* *
@ -76,10 +77,9 @@ export const ROUTER_PROVIDERS: Provider[] = [
* `RouterModule.forChild`. * `RouterModule.forChild`.
* *
* * `forRoot` creates a module that contains all the directives, the given routes, and the router * * `forRoot` creates a module that contains all the directives, the given routes, and the router
* service itself. * service itself.
* * `forChild` creates a module that contains all the directives and the given routes, but does not * * `forChild` creates a module that contains all the directives and the given routes, but does not
* include * include the router service.
* the router service.
* *
* When registered at the root, the module should be used as follows * When registered at the root, the module should be used as follows
* *
@ -134,12 +134,15 @@ export class RouterModule {
return { return {
ngModule: RouterModule, ngModule: RouterModule,
providers: [ providers: [
ROUTER_PROVIDERS, provideRoutes(routes), { ROUTER_PROVIDERS,
provideRoutes(routes),
{
provide: ROUTER_FORROOT_GUARD, provide: ROUTER_FORROOT_GUARD,
useFactory: provideForRootGuard, useFactory: provideForRootGuard,
deps: [[Router, new Optional(), new SkipSelf()]] deps: [[Router, new Optional(), new SkipSelf()]]
}, },
{provide: ROUTER_CONFIGURATION, useValue: config ? config : {}}, { {provide: ROUTER_CONFIGURATION, useValue: config ? config : {}},
{
provide: LocationStrategy, provide: LocationStrategy,
useFactory: provideLocationStrategy, useFactory: provideLocationStrategy,
deps: [ deps: [
@ -151,8 +154,9 @@ export class RouterModule {
useExisting: config && config.preloadingStrategy ? config.preloadingStrategy : useExisting: config && config.preloadingStrategy ? config.preloadingStrategy :
NoPreloading NoPreloading
}, },
provideRouterInitializer() {provide: NgProbeToken, multi: true, useFactory: routerNgProbeToken},
] provideRouterInitializer(),
],
}; };
} }
@ -196,7 +200,7 @@ export function provideForRootGuard(router: Router): any {
export function provideRoutes(routes: Routes): any { export function provideRoutes(routes: Routes): any {
return [ return [
{provide: ANALYZE_FOR_ENTRY_COMPONENTS, multi: true, useValue: routes}, {provide: ANALYZE_FOR_ENTRY_COMPONENTS, multi: true, useValue: routes},
{provide: ROUTES, multi: true, useValue: routes} {provide: ROUTES, multi: true, useValue: routes},
]; ];
} }
@ -297,6 +301,6 @@ export function provideRouterInitializer() {
useFactory: initialRouterNavigation, useFactory: initialRouterNavigation,
deps: [Router, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION] deps: [Router, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION]
}, },
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER} {provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER},
]; ];
} }

View File

@ -1156,8 +1156,6 @@ describe('Integration', () => {
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/initial'); expect(location.path()).toEqual('/initial');
}))); })));
// should not break the back button when trigger by initial navigation
}); });
describe('guards', () => { describe('guards', () => {
@ -1380,6 +1378,11 @@ describe('Integration', () => {
return true; return true;
} }
}, },
{
provide: 'alwaysFalse',
useValue:
(c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return false; }
},
] ]
}); });
}); });
@ -1504,6 +1507,31 @@ describe('Integration', () => {
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/team/33/user/fedor'); expect(location.path()).toEqual('/team/33/user/fedor');
}))); })));
it('should not create a route state if navigation is canceled',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);
router.resetConfig([{
path: 'main',
component: TeamCmp,
children: [
{path: 'component1', component: SimpleCmp, canDeactivate: ['alwaysFalse']},
{path: 'component2', component: SimpleCmp}
]
}]);
router.navigateByUrl('/main/component1');
advance(fixture);
router.navigateByUrl('/main/component2');
advance(fixture);
const teamCmp = fixture.debugElement.children[1].componentInstance;
expect(teamCmp.route.firstChild.url.value[0].path).toEqual('component1');
expect(location.path()).toEqual('/main/component1');
})));
}); });
describe('should work when given a class', () => { describe('should work when given a class', () => {

View File

@ -14,6 +14,12 @@ export interface ComponentInfo {
outputs?: string[]; outputs?: string[];
} }
/**
* A `PropertyBinding` represents a mapping between a property name
* and an attribute name. It is parsed from a string of the form
* `"prop: attr"`; or simply `"propAndAttr" where the property
* and attribute have the same identifier.
*/
export class PropertyBinding { export class PropertyBinding {
prop: string; prop: string;
attr: string; attr: string;

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