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:
parent
4c8ea12903
commit
b2a0be87e8
|
@ -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);
|
||||||
|
|
|
@ -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); }
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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");
|
||||||
|
|
Loading…
Reference in New Issue