fix(ivy): hack implementation of host styles (#27180)

PR Close #27180
This commit is contained in:
Miško Hevery 2018-11-19 14:55:57 -08:00 committed by Misko Hevery
parent 975c269752
commit ca40565f9a
7 changed files with 284 additions and 182 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import './ng_dev_mode';
import {resolveForwardRef} from '../di/forward_ref'; import {resolveForwardRef} from '../di/forward_ref';
import {InjectionToken} from '../di/injection_token'; import {InjectionToken} from '../di/injection_token';
import {Injector} from '../di/injector'; import {Injector} from '../di/injector';
@ -16,6 +16,7 @@ import {Sanitizer} from '../sanitization/security';
import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {Type} from '../type'; import {Type} from '../type';
import {noop} from '../util/noop'; import {noop} from '../util/noop';
import {assertDefined, assertEqual, assertLessThan, assertNotEqual} from './assert'; import {assertDefined, assertEqual, assertLessThan, assertNotEqual} from './assert';
import {attachPatchData, getComponentViewByInstance} from './context_discovery'; import {attachPatchData, getComponentViewByInstance} from './context_discovery';
import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreateNodeInjectorForNode, injectAttributeImpl} from './di'; import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreateNodeInjectorForNode, injectAttributeImpl} from './di';
@ -40,7 +41,7 @@ import {createStylingContextTemplate, renderStyleAndClassBindings, updateClassPr
import {BoundPlayerFactory} from './styling/player_factory'; import {BoundPlayerFactory} from './styling/player_factory';
import {getStylingContext} from './styling/util'; import {getStylingContext} from './styling/util';
import {NO_CHANGE} from './tokens'; import {NO_CHANGE} from './tokens';
import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isDifferent, loadInternal, readPatchedLViewData, stringify} from './util'; import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isDifferent, loadInternal, readElementValue, readPatchedLViewData, stringify} from './util';
/** /**
* A permanent marker promise which signifies that the current CD tree is * A permanent marker promise which signifies that the current CD tree is
@ -125,6 +126,12 @@ export function setHostBindings(tView: TView, viewData: LViewData): void {
viewData[BINDING_INDEX] = bindingRootIndex; viewData[BINDING_INDEX] = bindingRootIndex;
// We must subtract the header offset because the load() instruction // We must subtract the header offset because the load() instruction
// expects a raw, unadjusted index. // expects a raw, unadjusted index.
// <HACK(misko)>: set the `previousOrParentTNode` so that hostBindings functions can
// correctly retrieve it. This should be removed once we call the hostBindings function
// inline as part of the `RenderFlags.Create` because in that case the value will already be
// correctly set.
setPreviousOrParentTNode(getTView().data[currentElementIndex + HEADER_OFFSET] as TNode);
// </HACK>
instruction(currentDirectiveIndex - HEADER_OFFSET, currentElementIndex); instruction(currentDirectiveIndex - HEADER_OFFSET, currentElementIndex);
currentDirectiveIndex++; currentDirectiveIndex++;
} }
@ -1069,17 +1076,21 @@ function generatePropertyAliases(
* This instruction is meant to handle the [class.foo]="exp" case * This instruction is meant to handle the [class.foo]="exp" case
* *
* @param index The index of the element to update in the data array * @param index The index of the element to update in the data array
* @param className Name of class to toggle. Because it is going to DOM, this is not subject to * @param classIndex Index of class to toggle. Because it is going to DOM, this is not subject to
* renaming as part of minification. * renaming as part of minification.
* @param value A value indicating if a given class should be added or removed. * @param value A value indicating if a given class should be added or removed.
* @param directiveIndex the index for the directive that is attempting to change styling. * @param directiveIndex the index for the directive that is attempting to change styling.
*/ */
export function elementClassProp( export function elementClassProp(
index: number, stylingIndex: number, value: boolean | PlayerFactory, index: number, classIndex: number, value: boolean | PlayerFactory,
directiveIndex?: number): void { directiveIndex?: number): void {
if (directiveIndex != undefined) {
return hackImplementationOfElementClassProp(
index, classIndex, value, directiveIndex); // proper supported in next PR
}
const val = const val =
(value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory<boolean>) : (!!value); (value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory<boolean>) : (!!value);
updateElementClassProp(getStylingContext(index, getViewData()), stylingIndex, val); updateElementClassProp(getStylingContext(index, getViewData()), classIndex, val);
} }
/** /**
@ -1115,7 +1126,13 @@ export function elementStyling(
classDeclarations?: (string | boolean | InitialStylingFlags)[] | null, classDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
styleSanitizer?: StyleSanitizeFn | null, directiveIndex?: number): void { styleSanitizer?: StyleSanitizeFn | null, directiveIndex?: number): void {
if (directiveIndex) return; // supported in next PR if (directiveIndex !== undefined) {
getCreationMode() &&
hackImplementationOfElementStyling(
classDeclarations || null, styleDeclarations || null, styleSanitizer || null,
directiveIndex); // supported in next PR
return;
}
const tNode = getPreviousOrParentTNode(); const tNode = getPreviousOrParentTNode();
const inputData = initializeTNodeInputs(tNode); const inputData = initializeTNodeInputs(tNode);
@ -1159,7 +1176,9 @@ export function elementStyling(
* @param directiveIndex the index for the directive that is attempting to change styling. * @param directiveIndex the index for the directive that is attempting to change styling.
*/ */
export function elementStylingApply(index: number, directiveIndex?: number): void { export function elementStylingApply(index: number, directiveIndex?: number): void {
if (directiveIndex) return; // supported in next PR if (directiveIndex != undefined) {
return hackImplementationOfElementStylingApply(index, directiveIndex); // supported in next PR
}
const viewData = getViewData(); const viewData = getViewData();
const isFirstRender = (viewData[FLAGS] & LViewFlags.CreationMode) !== 0; const isFirstRender = (viewData[FLAGS] & LViewFlags.CreationMode) !== 0;
const totalPlayersQueued = renderStyleAndClassBindings( const totalPlayersQueued = renderStyleAndClassBindings(
@ -1194,7 +1213,9 @@ export function elementStylingApply(index: number, directiveIndex?: number): voi
export function elementStyleProp( export function elementStyleProp(
index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, index: number, styleIndex: number, value: string | number | String | PlayerFactory | null,
suffix?: string, directiveIndex?: number): void { suffix?: string, directiveIndex?: number): void {
if (directiveIndex) return; // supported in next PR if (directiveIndex != undefined)
return hackImplementationOfElementStyleProp(
index, styleIndex, value, suffix, directiveIndex); // supported in next PR
let valueToAdd: string|null = null; let valueToAdd: string|null = null;
if (value) { if (value) {
if (suffix) { if (suffix) {
@ -1237,7 +1258,9 @@ export function elementStyleProp(
export function elementStylingMap<T>( export function elementStylingMap<T>(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
styles?: {[styleName: string]: any} | NO_CHANGE | null, directiveIndex?: number): void { styles?: {[styleName: string]: any} | NO_CHANGE | null, directiveIndex?: number): void {
if (directiveIndex) return; // supported in next PR if (directiveIndex != undefined)
return hackImplementationOfElementStylingMap(
index, classes, styles, directiveIndex); // supported in next PR
const viewData = getViewData(); const viewData = getViewData();
const tNode = getTNode(index, viewData); const tNode = getTNode(index, viewData);
const stylingContext = getStylingContext(index, viewData); const stylingContext = getStylingContext(index, viewData);
@ -1250,6 +1273,74 @@ export function elementStylingMap<T>(
updateStylingMap(stylingContext, classes, styles); updateStylingMap(stylingContext, classes, styles);
} }
/* START OF HACK BLOCK */
/*
* HACK
* The code below is a quick and dirty implementation of the host style binding so that we can make
* progress on TestBed. Once the correct implementation is created this code should be removed.
*/
interface HostStylingHack {
classDeclarations: string[];
styleDeclarations: string[];
styleSanitizer: StyleSanitizeFn|null;
}
interface HostStylingHackMap {
[directiveIndex: number]: HostStylingHack;
}
function hackImplementationOfElementStyling(
classDeclarations: (string | boolean | InitialStylingFlags)[] | null,
styleDeclarations: (string | boolean | InitialStylingFlags)[] | null,
styleSanitizer: StyleSanitizeFn | null, directiveIndex: number): void {
const node = getNativeByTNode(getPreviousOrParentTNode(), getViewData());
ngDevMode && assertDefined(node, 'expecting parent DOM node');
const hostStylingHackMap: HostStylingHackMap =
((node as any).hostStylingHack || ((node as any).hostStylingHack = {}));
hostStylingHackMap[directiveIndex] = {
classDeclarations: hackSquashDeclaration(classDeclarations),
styleDeclarations: hackSquashDeclaration(styleDeclarations), styleSanitizer
};
}
function hackSquashDeclaration(declarations: (string | boolean | InitialStylingFlags)[] | null):
string[] {
// assume the array is correct. This should be fine for View Engine compatibility.
return declarations || [] as any;
}
function hackImplementationOfElementClassProp(
index: number, classIndex: number, value: boolean | PlayerFactory,
directiveIndex: number): void {
const node = getNativeByIndex(index, getViewData());
ngDevMode && assertDefined(node, 'could not locate node');
const hostStylingHack: HostStylingHack = (node as any).hostStylingHack[directiveIndex];
const className = hostStylingHack.classDeclarations[classIndex];
const renderer = getRenderer();
if (isProceduralRenderer(renderer)) {
value ? renderer.addClass(node, className) : renderer.removeClass(node, className);
} else {
const classList = (node as HTMLElement).classList;
value ? classList.add(className) : classList.remove(className);
}
}
function hackImplementationOfElementStylingApply(index: number, directiveIndex?: number): void {
// Do nothing because the hack implementation is eager.
}
function hackImplementationOfElementStyleProp(
index: number, styleIndex: number, value: string | number | String | PlayerFactory | null,
suffix?: string, directiveIndex?: number): void {
throw new Error('unimplemented. Should not be needed by ViewEngine compatibility');
}
function hackImplementationOfElementStylingMap<T>(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
styles?: {[styleName: string]: any} | NO_CHANGE | null, directiveIndex?: number): void {
throw new Error('unimplemented. Should not be needed by ViewEngine compatibility');
}
/* END OF HACK BLOCK */
////////////////////////// //////////////////////////
//// Text //// Text
////////////////////////// //////////////////////////

View File

@ -785,6 +785,18 @@
{ {
"name": "getViewData" "name": "getViewData"
}, },
{
"name": "hackImplementationOfElementStyling"
},
{
"name": "hackImplementationOfElementStylingApply"
},
{
"name": "hackImplementationOfElementStylingMap"
},
{
"name": "hackSquashDeclaration"
},
{ {
"name": "hasParentInjector" "name": "hasParentInjector"
}, },

View File

@ -818,6 +818,18 @@
{ {
"name": "getViewData" "name": "getViewData"
}, },
{
"name": "hackImplementationOfElementClassProp"
},
{
"name": "hackImplementationOfElementStyling"
},
{
"name": "hackImplementationOfElementStylingApply"
},
{
"name": "hackSquashDeclaration"
},
{ {
"name": "hasParentInjector" "name": "hasParentInjector"
}, },

View File

@ -1943,6 +1943,18 @@
{ {
"name": "globalListener" "name": "globalListener"
}, },
{
"name": "hackImplementationOfElementClassProp"
},
{
"name": "hackImplementationOfElementStyling"
},
{
"name": "hackImplementationOfElementStylingApply"
},
{
"name": "hackSquashDeclaration"
},
{ {
"name": "hasBalancedQuotes" "name": "hasBalancedQuotes"
}, },

View File

@ -12,7 +12,6 @@ import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MO
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
import {fixmeIvy} from '@angular/private/testing';
import {merge, timer} from 'rxjs'; import {merge, timer} from 'rxjs';
import {tap} from 'rxjs/operators'; import {tap} from 'rxjs/operators';
@ -713,7 +712,6 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec';
}); });
fixmeIvy('Host bindings to styles do not yet work') &&
describe('setting status classes', () => { describe('setting status classes', () => {
it('should work with single fields', () => { it('should work with single fields', () => {
const fixture = initTest(FormControlComp); const fixture = initTest(FormControlComp);
@ -743,9 +741,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec';
fixture.detectChanges(); fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement; const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(sortedClassList(input)).toEqual([ expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-untouched']);
'ng-pending', 'ng-pristine', 'ng-untouched'
]);
dispatchEvent(input, 'blur'); dispatchEvent(input, 'blur');
fixture.detectChanges(); fixture.detectChanges();
@ -759,8 +755,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec';
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
})); }));
it('should work with single fields that combines async and sync validators', it('should work with single fields that combines async and sync validators', fakeAsync(() => {
fakeAsync(() => {
const fixture = initTest(FormControlComp); const fixture = initTest(FormControlComp);
const control = const control =
new FormControl('', Validators.required, uniqLoginAsyncValidator('good')); new FormControl('', Validators.required, uniqLoginAsyncValidator('good'));
@ -768,9 +763,7 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec';
fixture.detectChanges(); fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement; const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(sortedClassList(input)).toEqual([ expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
'ng-invalid', 'ng-pristine', 'ng-untouched'
]);
dispatchEvent(input, 'blur'); dispatchEvent(input, 'blur');
fixture.detectChanges(); fixture.detectChanges();

View File

@ -12,7 +12,6 @@ import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, FormControl, F
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
import {fixmeIvy} from '@angular/private/testing';
import {merge} from 'rxjs'; import {merge} from 'rxjs';
import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integration_spec'; import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integration_spec';
@ -149,7 +148,6 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
expect(form.value).toEqual({}); expect(form.value).toEqual({});
})); }));
fixmeIvy('host bindings do not yet work with classes or styles') &&
it('should set status classes with ngModel', async(() => { it('should set status classes with ngModel', async(() => {
const fixture = initTest(NgModelForm); const fixture = initTest(NgModelForm);
fixture.componentInstance.name = 'aa'; fixture.componentInstance.name = 'aa';
@ -158,16 +156,12 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
fixture.detectChanges(); fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement; const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(sortedClassList(input)).toEqual([ expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
'ng-invalid', 'ng-pristine', 'ng-untouched'
]);
dispatchEvent(input, 'blur'); dispatchEvent(input, 'blur');
fixture.detectChanges(); fixture.detectChanges();
expect(sortedClassList(input)).toEqual([ expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']);
'ng-invalid', 'ng-pristine', 'ng-touched'
]);
input.value = 'updatedValue'; input.value = 'updatedValue';
dispatchEvent(input, 'input'); dispatchEvent(input, 'input');
@ -176,7 +170,6 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
}); });
})); }));
fixmeIvy('host bindings do not yet work with classes or styles') &&
it('should set status classes with ngModel and async validators', fakeAsync(() => { it('should set status classes with ngModel and async validators', fakeAsync(() => {
const fixture = initTest(NgModelAsyncValidation, NgAsyncValidator); const fixture = initTest(NgModelAsyncValidation, NgAsyncValidator);
@ -184,16 +177,12 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
fixture.detectChanges(); fixture.detectChanges();
const input = fixture.debugElement.query(By.css('input')).nativeElement; const input = fixture.debugElement.query(By.css('input')).nativeElement;
expect(sortedClassList(input)).toEqual([ expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-untouched']);
'ng-pending', 'ng-pristine', 'ng-untouched'
]);
dispatchEvent(input, 'blur'); dispatchEvent(input, 'blur');
fixture.detectChanges(); fixture.detectChanges();
expect(sortedClassList(input)).toEqual([ expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-touched']);
'ng-pending', 'ng-pristine', 'ng-touched'
]);
input.value = 'updatedValue'; input.value = 'updatedValue';
dispatchEvent(input, 'input'); dispatchEvent(input, 'input');
@ -204,15 +193,13 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
}); });
})); }));
fixmeIvy('host bindings do not yet work with classes or styles') &&
it('should set status classes with ngModelGroup and ngForm', async(() => { it('should set status classes with ngModelGroup and ngForm', async(() => {
const fixture = initTest(NgModelGroupForm); const fixture = initTest(NgModelGroupForm);
fixture.componentInstance.first = ''; fixture.componentInstance.first = '';
fixture.detectChanges(); fixture.detectChanges();
const form = fixture.debugElement.query(By.css('form')).nativeElement; const form = fixture.debugElement.query(By.css('form')).nativeElement;
const modelGroup = const modelGroup = fixture.debugElement.query(By.css('[ngModelGroup]')).nativeElement;
fixture.debugElement.query(By.css('[ngModelGroup]')).nativeElement;
const input = fixture.debugElement.query(By.css('input')).nativeElement; const input = fixture.debugElement.query(By.css('input')).nativeElement;
// ngModelGroup creates its control asynchronously // ngModelGroup creates its control asynchronously
@ -222,9 +209,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
'ng-invalid', 'ng-pristine', 'ng-untouched' 'ng-invalid', 'ng-pristine', 'ng-untouched'
]); ]);
expect(sortedClassList(form)).toEqual([ expect(sortedClassList(form)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
'ng-invalid', 'ng-pristine', 'ng-untouched'
]);
dispatchEvent(input, 'blur'); dispatchEvent(input, 'blur');
fixture.detectChanges(); fixture.detectChanges();
@ -238,9 +223,7 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
dispatchEvent(input, 'input'); dispatchEvent(input, 'input');
fixture.detectChanges(); fixture.detectChanges();
expect(sortedClassList(modelGroup)).toEqual([ expect(sortedClassList(modelGroup)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
'ng-dirty', 'ng-touched', 'ng-valid'
]);
expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']); expect(sortedClassList(form)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
}); });
})); }));

View File

@ -11,7 +11,6 @@ import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/t
import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgControl, NgForm, NgModel, ReactiveFormsModule, Validators} from '@angular/forms'; import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, FormsModule, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgControl, NgForm, NgModel, ReactiveFormsModule, Validators} from '@angular/forms';
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
import {fixmeIvy} from '@angular/private/testing';
{ {
describe('value accessors', () => { describe('value accessors', () => {