fix(change_detect): Sort `DirectiveMetadata` properties during processing

The Angular 2 render compiler can get out of sync between its transformer
execution and its runtime execution, leading to incorrect change detectors with
out-of-order property values. Stable sorting solves this problem (temporarily).
This commit is contained in:
Tim Blasi 2015-07-20 13:37:50 -07:00
parent 4c8ea12903
commit b2a0be87e8
6 changed files with 33 additions and 9 deletions

View File

@ -42,6 +42,7 @@ class MapWrapper {
static forEach(Map m, fn(v, k)) { static forEach(Map m, fn(v, k)) {
m.forEach((k, v) => fn(v, k)); m.forEach((k, v) => fn(v, k));
} }
static get(Map map, key) => map[key];
static int size(Map m) => m.length; static int size(Map m) => m.length;
static void delete(Map m, k) { static void delete(Map m, k) {
m.remove(k); m.remove(k);

View File

@ -64,6 +64,7 @@ export class MapWrapper {
} }
static createFromPairs(pairs: List<any>): Map<any, any> { return createMapFromPairs(pairs); } static createFromPairs(pairs: List<any>): Map<any, any> { return createMapFromPairs(pairs); }
static forEach<K, V>(m: Map<K, V>, fn: /*(V, K) => void*/ Function) { m.forEach(<any>fn); } static forEach<K, V>(m: Map<K, V>, fn: /*(V, K) => void*/ Function) { m.forEach(<any>fn); }
static get<K, V>(map: Map<K, V>, key: K): V { return map.get(key); }
static size(m: Map<any, any>): number { return m.size; } static size(m: Map<any, any>): number { return m.size; }
static delete<K>(m: Map<K, any>, k: K) { m.delete(k); } static delete<K>(m: Map<K, any>, k: K) { m.delete(k); }
static clearValues(m: Map<any, any>) { _clearValues(m); } static clearValues(m: Map<any, any>) { _clearValues(m); }

View File

@ -97,6 +97,8 @@ class StringWrapper {
static bool contains(String s, String substr) { static bool contains(String s, String substr) {
return s.contains(substr); return s.contains(substr);
} }
static int compare(String a, String b) => a.compareTo(b);
} }
class StringJoiner { class StringJoiner {

View File

@ -158,6 +158,16 @@ export class StringWrapper {
} }
static contains(s: string, substr: string): boolean { return s.indexOf(substr) != -1; } static contains(s: string, substr: string): boolean { return s.indexOf(substr) != -1; }
static compare(a: string, b: string): int {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
}
} }
export class StringJoiner { export class StringJoiner {

View File

@ -76,17 +76,17 @@ export class DirectiveParser implements CompileStep {
}); });
} }
if (isPresent(dirMetadata.hostListeners)) { if (isPresent(dirMetadata.hostListeners)) {
MapWrapper.forEach(dirMetadata.hostListeners, (action, eventName) => { this._sortedKeysForEach(dirMetadata.hostListeners, (action, eventName) => {
this._bindDirectiveEvent(eventName, action, current, directiveBinderBuilder); this._bindDirectiveEvent(eventName, action, current, directiveBinderBuilder);
}); });
} }
if (isPresent(dirMetadata.hostProperties)) { if (isPresent(dirMetadata.hostProperties)) {
MapWrapper.forEach(dirMetadata.hostProperties, (expression, hostPropertyName) => { this._sortedKeysForEach(dirMetadata.hostProperties, (expression, hostPropertyName) => {
this._bindHostProperty(hostPropertyName, expression, current, directiveBinderBuilder); this._bindHostProperty(hostPropertyName, expression, current, directiveBinderBuilder);
}); });
} }
if (isPresent(dirMetadata.hostAttributes)) { if (isPresent(dirMetadata.hostAttributes)) {
MapWrapper.forEach(dirMetadata.hostAttributes, (hostAttrValue, hostAttrName) => { this._sortedKeysForEach(dirMetadata.hostAttributes, (hostAttrValue, hostAttrName) => {
this._addHostAttribute(hostAttrName, hostAttrValue, current); this._addHostAttribute(hostAttrName, hostAttrValue, current);
}); });
} }
@ -97,6 +97,16 @@ export class DirectiveParser implements CompileStep {
}); });
} }
_sortedKeysForEach(map: Map<string, string>, fn: (value: string, key: string) => void): void {
var keys = MapWrapper.keys(map);
ListWrapper.sort(keys, (a, b) => {
// Ensure a stable sort.
var compareVal = StringWrapper.compare(a, b);
return compareVal == 0 ? -1 : compareVal;
});
ListWrapper.forEach(keys, (key) => { fn(MapWrapper.get(map, key), key); });
}
_ensureHasOnlyOneComponent(elementBinder: ElementBinderBuilder, elDescription: string): void { _ensureHasOnlyOneComponent(elementBinder: ElementBinderBuilder, elDescription: string): void {
if (isPresent(elementBinder.componentId)) { if (isPresent(elementBinder.componentId)) {
throw new BaseException( throw new BaseException(

View File

@ -645,13 +645,13 @@ export function main() {
var input = rootTC.query(By.css("input")).nativeElement; var input = rootTC.query(By.css("input")).nativeElement;
expect(DOM.classList(input)) expect(DOM.classList(input))
.toEqual(['ng-binding', 'ng-untouched', 'ng-pristine', 'ng-invalid']); .toEqual(['ng-binding', 'ng-invalid', 'ng-pristine', 'ng-untouched']);
dispatchEvent(input, "blur"); dispatchEvent(input, "blur");
rootTC.detectChanges(); rootTC.detectChanges();
expect(DOM.classList(input)) expect(DOM.classList(input))
.toEqual(["ng-binding", "ng-pristine", "ng-invalid", "ng-touched"]); .toEqual(["ng-binding", "ng-invalid", "ng-pristine", "ng-touched"]);
input.value = "updatedValue"; input.value = "updatedValue";
dispatchEvent(input, "change"); dispatchEvent(input, "change");
@ -675,13 +675,13 @@ export function main() {
var input = rootTC.query(By.css("input")).nativeElement; var input = rootTC.query(By.css("input")).nativeElement;
expect(DOM.classList(input)) expect(DOM.classList(input))
.toEqual(["ng-binding", "ng-untouched", "ng-pristine", "ng-invalid"]); .toEqual(["ng-binding", "ng-invalid", "ng-pristine", "ng-untouched"]);
dispatchEvent(input, "blur"); dispatchEvent(input, "blur");
rootTC.detectChanges(); rootTC.detectChanges();
expect(DOM.classList(input)) expect(DOM.classList(input))
.toEqual(["ng-binding", "ng-pristine", "ng-invalid", "ng-touched"]); .toEqual(["ng-binding", "ng-invalid", "ng-pristine", "ng-touched"]);
input.value = "updatedValue"; input.value = "updatedValue";
dispatchEvent(input, "change"); dispatchEvent(input, "change");
@ -703,13 +703,13 @@ export function main() {
var input = rootTC.query(By.css("input")).nativeElement; var input = rootTC.query(By.css("input")).nativeElement;
expect(DOM.classList(input)) expect(DOM.classList(input))
.toEqual(["ng-binding", "ng-untouched", "ng-pristine", "ng-invalid"]); .toEqual(["ng-binding", "ng-invalid", "ng-pristine", "ng-untouched"]);
dispatchEvent(input, "blur"); dispatchEvent(input, "blur");
rootTC.detectChanges(); rootTC.detectChanges();
expect(DOM.classList(input)) expect(DOM.classList(input))
.toEqual(["ng-binding", "ng-pristine", "ng-invalid", "ng-touched"]); .toEqual(["ng-binding", "ng-invalid", "ng-pristine", "ng-touched"]);
input.value = "updatedValue"; input.value = "updatedValue";
dispatchEvent(input, "change"); dispatchEvent(input, "change");