Compare commits
43 Commits
2.2.0
...
2.3.0-beta
Author | SHA1 | Date | |
---|---|---|---|
4e047302f2 | |||
419a812f04 | |||
f340e1a414 | |||
481c9b3258 | |||
8b2dfb2eca | |||
824ea8406c | |||
1f96a93f59 | |||
009d545787 | |||
53c25210a6 | |||
927aa69726 | |||
ce89039036 | |||
42198cd7d5 | |||
d6ba092a27 | |||
773b31de8f | |||
f79b320fc4 | |||
6a212fd561 | |||
be010a292a | |||
7c36e7f956 | |||
13ba2f90b9 | |||
75277cd94b | |||
46d150266b | |||
1b5384ee54 | |||
9f7d32a326 | |||
9de76ebfa5 | |||
46023e4792 | |||
b55aaf094f | |||
d90b622fa4 | |||
79e2bb9291 | |||
efbbefd353 | |||
c2fae72bc6 | |||
7908679c4b | |||
9ed9ff40b3 | |||
2f14415836 | |||
76e4911e8b | |||
ed5e98d0df | |||
146af1fed9 | |||
c60ba7a72f | |||
05beffe0d0 | |||
08c038ebd9 | |||
582550a90d | |||
1d53a870dd | |||
a0c58a6b5c | |||
d3eff6c483 |
43
CHANGELOG.md
43
CHANGELOG.md
@ -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)
|
||||||
|
|
||||||
|
9
build.sh
9
build.sh
@ -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
|
||||||
|
@ -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>
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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[] = [];
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
@ -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);
|
||||||
|
@ -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); }
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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); }
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
@ -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));
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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));
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
@ -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[]):
|
||||||
|
@ -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()));
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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><i></ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex></i></ex></ph></msg>
|
<msg id="3780349238193953556"><ph name="START_ITALIC_TEXT"><ex><i></ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex></i></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><b></ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph>}}</msg>
|
<msg id="4593805537723189714">{VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex><b></ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex></b></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><h1></ex></ph>Markers in html comments<ph name="CLOSE_HEADING_LEVEL1"><ex></h1></ex></ph>
|
<ph name="START_HEADING_LEVEL1"><ex><h1></ex></ph>Markers in html comments<ph name="CLOSE_HEADING_LEVEL1"><ex></h1></ex></ph>
|
||||||
<ph name="START_TAG_DIV"><ex><div></ex></ph><ph name="CLOSE_TAG_DIV"><ex></div></ex></ph>
|
<ph name="START_TAG_DIV"><ex><div></ex></ph><ph name="CLOSE_TAG_DIV"><ex></div></ex></ph>
|
||||||
<ph name="START_TAG_DIV_1"><ex><div></ex></ph><ph name="ICU"/><ph name="CLOSE_TAG_DIV"><ex></div></ex></ph>
|
<ph name="START_TAG_DIV_1"><ex><div></ex></ph><ph name="ICU"/><ph name="CLOSE_TAG_DIV"><ex></div></ex></ph>
|
||||||
</msg>
|
</msg>
|
||||||
<msg id="93a30c67d4e6c9b37aecfe2ac0f2b5d366d7b520">it <ph name="START_BOLD_TEXT"><ex><b></ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> work</msg>
|
<msg id="1491627405349178954">it <ph name="START_BOLD_TEXT"><ex><b></ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> work</msg>
|
||||||
</messagebundle>`;
|
</messagebundle>
|
||||||
|
`;
|
||||||
|
@ -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[] {
|
||||||
|
@ -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 {
|
||||||
|
@ -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`)));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -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><b></ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"/></msg>
|
<msg id="7056919470098446707">translatable element <ph name="START_BOLD_TEXT"><ex><b></ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"/></msg>
|
||||||
<msg id="e2ccf3d131b15f54aa1fcf1314b1ca77c14bfcc2">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} }</msg>
|
<msg id="2981514368455622387">{VAR_PLURAL, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex></p></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><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} } } }</msg>
|
<msg id="2015957479576096115">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></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/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 {
|
||||||
|
@ -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/); });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
110
modules/@angular/compiler/test/i18n/translation_bundle_spec.ts
Normal file
110
modules/@angular/compiler/test/i18n/translation_bundle_spec.ts
Normal 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"/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,8 @@ export function balanceAnimationKeyframes(
|
|||||||
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
|
firstKeyframe.styles.styles.push(extraFirstKeyframeStyles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
collectAndResolveStyles(collectedStyles, [finalStateStyles]);
|
||||||
|
|
||||||
return keyframes;
|
return keyframes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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; }
|
||||||
|
@ -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';
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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 */;
|
||||||
|
@ -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) =>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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, {
|
||||||
|
@ -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!');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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('()');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>');
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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: {
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
184
modules/@angular/examples/upgrade/static/ts/module.ts
Normal file
184
modules/@angular/examples/upgrade/static/ts/module.ts
Normal 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
|
17
modules/@angular/examples/upgrade/static/ts/styles.css
Normal file
17
modules/@angular/examples/upgrade/static/ts/styles.css
Normal 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;
|
||||||
|
}
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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];
|
|
||||||
|
@ -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); }
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
../../facade/src
|
|
@ -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}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,5 +49,3 @@ export function stringToArrayBuffer(input: String): ArrayBuffer {
|
|||||||
}
|
}
|
||||||
return view.buffer;
|
return view.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {isJsObject} from '../src/facade/lang';
|
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
@ -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)) {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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()]]
|
|
||||||
}];
|
}];
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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};
|
||||||
}
|
}
|
||||||
|
@ -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'});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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});
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
modules/@angular/router-license-banner.txt
Normal file
5
modules/@angular/router-license-banner.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* @license Angular v0.0.0-ROUTERPLACEHOLDER
|
||||||
|
* (c) 2010-2016 Google, Inc. https://angular.io/
|
||||||
|
* License: MIT
|
||||||
|
*/
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
*
|
*
|
||||||
|
@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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
Reference in New Issue
Block a user