refactor(ivy): Switch styling to new reconcile algorithm (#34616)

NOTE: This change must be reverted with previous deletes so that it code remains in build-able state.

This change deletes old styling code and replaces it with a simplified styling algorithm.

The mental model for the new algorithm is:
- Create a linked list of styling bindings in the order of priority. All styling bindings ere executed in compiled order and than a linked list of bindings is created in priority order.
- Flush the style bindings at the end of `advance()` instruction. This implies that there are two flush events. One at the end of template `advance` instruction in the template. Second one at the end of `hostBindings` `advance` instruction when processing host bindings (if any).
- Each binding instructions effectively updates the string to represent the string at that location. Because most of the bindings are additive, this is a cheap strategy in most cases. In rare cases the strategy requires removing tokens from the styling up to this point. (We expect that to be rare case)S Because, the bindings are presorted in the order of priority, it is safe to resume the processing of the concatenated string from the last change binding.

PR Close #34616
This commit is contained in:
Miško Hevery 2019-12-17 15:40:37 -08:00
parent b4a711ea9f
commit 5aabe93abe
60 changed files with 2439 additions and 1413 deletions

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2987,
"main-es2015": 462235,
"main-es2015": 456890,
"polyfills-es2015": 52503
}
}
@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 3097,
"main-es2015": 438671,
"main-es2015": 425216,
"polyfills-es2015": 52503
}
}

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 18214,
"main-es2015": 16787,
"polyfills-es2015": 36808
}
}
@ -30,7 +30,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 139487,
"main-es2015": 137226,
"polyfills-es2015": 37494
}
}
@ -39,7 +39,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 268796,
"main-es2015": 254857,
"polyfills-es2015": 36808,
"5-es2015": 751
}
@ -49,7 +49,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 228770,
"main-es2015": 226519,
"polyfills-es2015": 36808,
"5-es2015": 779
}
@ -60,7 +60,7 @@
"uncompressed": {
"bundle": "TODO(i): temporarily increase the payload size limit from 105779 - this is due to a closure issue related to ESM reexports that still needs to be investigated",
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
"bundle": 176433
"bundle": 175498
}
}
}

View File

@ -18,7 +18,7 @@ import {BrowserModule} from '@angular/platform-browser';
<ng-template #t3><button [class.bar]="exp ==='bar'"></button></ng-template>
<ng-template #t4><button class="foo" [class.bar]="exp ==='bar'"></button></ng-template>
<ng-template #t5><button class="foo" [ngClass]="{bar: exp ==='bar'}"></button></ng-template>
<ng-template #t6><button class="foo" [ngStyle]="staticStyle" [style.background-color]="exp"></button></ng-template>
<ng-template #t6><button class="foo" [ngStyle]="staticStyle" [style.background-color]="exp == 'bar' ? 'yellow': 'red'"></button></ng-template>
<ng-template #t7><button style="color: red"></button></ng-template>
<ng-template #t8><button [style.width.px]="exp ==='bar' ? 10 : 20" [style.color]="exp"></button></ng-template>
<ng-template #t9><button style="width: 10px" [style.color]="exp"></button></ng-template>

View File

@ -25,7 +25,7 @@ export class TreeFunction {
type: TreeFunction,
selectors: [['tree']],
decls: 5,
vars: 1,
vars: 2,
template: function(rf: ɵRenderFlags, ctx: TreeFunction) {
// bit of a hack
TreeTpl(rf, ctx.data);
@ -34,6 +34,7 @@ export class TreeFunction {
});
}
const TreeFunctionCmpDef = TreeFunction.ɵcmp as{decls: number, vars: number};
export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
if (rf & ɵRenderFlags.Create) {
ɵɵelementStart(0, 'tree');
@ -54,7 +55,7 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
ɵɵcontainerRefreshStart(3);
{
if (ctx.left != null) {
let rf0 = ɵɵembeddedViewStart(0, 5, 1);
let rf0 = ɵɵembeddedViewStart(0, 5, 2);
{ TreeTpl(rf0, ctx.left); }
ɵɵembeddedViewEnd();
}
@ -63,7 +64,7 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
ɵɵcontainerRefreshStart(4);
{
if (ctx.right != null) {
let rf0 = ɵɵembeddedViewStart(0, 5, 1);
let rf0 = ɵɵembeddedViewStart(0, TreeFunctionCmpDef.decls, TreeFunctionCmpDef.vars);
{ TreeTpl(rf0, ctx.right); }
ɵɵembeddedViewEnd();
}

View File

@ -157,4 +157,11 @@ export class NgClass implements DoCheck {
});
}
}
// TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs
// We need to export this to make angular/flex-layout happy
// https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9
setClass(value: string) { this.klass = value; }
setNgClass(value: any) { this.ngClass = value; }
applyChanges() { this.ngDoCheck(); }
}

View File

@ -85,4 +85,10 @@ export class NgStyle implements DoCheck {
changes.forEachAddedItem((record) => this._setStyle(record.key, record.currentValue));
changes.forEachChangedItem((record) => this._setStyle(record.key, record.currentValue));
}
// TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs
// We need to export this to make angular/flex-layout happy
// https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9
setNgStyle(value: any) { this.ngStyle = value; }
applyChanges() { this.ngDoCheck(); }
}

View File

@ -6,5 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
// TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs
// We need to export this to make angular/flex-layout happy
// https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9
export {NgClass as ɵNgClassImpl, NgClass as ɵNgClassR2Impl} from './directives/ng_class';
export {NgStyle as ɵNgStyleR2Impl} from './directives/ng_style';
export {DomAdapter as ɵDomAdapter, getDOM as ɵgetDOM, setRootDomAdapter as ɵsetRootDomAdapter} from './dom_adapter';
export {BrowserPlatformLocation as ɵBrowserPlatformLocation} from './location/platform_location';

View File

@ -196,7 +196,7 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing';
fixture = createTestComponent(`<div [ngClass]="['foo', {}]"></div>`);
expect(() => fixture !.detectChanges())
.toThrowError(
/NgClass can only toggle CSS classes expressed as strings, got: \[object Object\]/);
/NgClass can only toggle CSS classes expressed as strings, got \[object Object\]/);
});
});
@ -372,6 +372,27 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing';
detectChangesAndExpectClassName('color-red');
});
it('should allow classes with trailing and leading spaces in [ngClass]', () => {
@Component({
template: `
<div leading-space [ngClass]="{' foo': applyClasses}"></div>
<div trailing-space [ngClass]="{'foo ': applyClasses}"></div>
`
})
class Cmp {
applyClasses = true;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const leading = fixture.nativeElement.querySelector('[leading-space]');
const trailing = fixture.nativeElement.querySelector('[trailing-space]');
expect(leading.className).toBe('foo');
expect(trailing.className).toBe('foo');
});
});
});
}
@ -379,8 +400,7 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing';
@Component({selector: 'test-cmp', template: ''})
class TestComponent {
condition: boolean = true;
// TODO(issue/24571): remove '!'.
items !: any[];
items: any[]|undefined;
arrExpr: string[] = ['foo'];
setExpr: Set<string> = new Set<string>();
objExpr: {[klass: string]: any}|null = {'foo': true, 'bar': false};

View File

@ -856,7 +856,7 @@ describe('compiler compliance: bindings', () => {
type: HostAttributeDir,
selectors: [["", "hostAttributeDir", ""]],
hostAttrs: ["title", "hello there from directive", ${AttributeMarker.Classes}, "one", "two", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"],
hostVars: 2,
hostVars: 4,
hostBindings: function HostAttributeDir_HostBindings(rf, ctx, elIndex) {
}

View File

@ -1006,15 +1006,8 @@ describe('compiler compliance: styling', () => {
const template = `
hostAttrs: [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"],
hostVars: 6,
hostVars: 8,
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
<<<<<<< HEAD
=======
if (rf & 1) {
$r3$.ɵɵallocHostVars(8);
$r3$.ɵɵelementHostAttrs($e0_attrs$);
}
>>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction
if (rf & 2) {
$r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵclassMap(ctx.myClass);
@ -1068,14 +1061,8 @@ describe('compiler compliance: styling', () => {
};
const template = `
hostVars: 8,
hostVars: 12,
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
<<<<<<< HEAD
=======
if (rf & 1) {
$r3$.ɵɵallocHostVars(12);
}
>>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction
if (rf & 2) {
$r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵclassMap(ctx.myClasses);
@ -1143,14 +1130,8 @@ describe('compiler compliance: styling', () => {
`;
const hostBindings = `
hostVars: 6,
hostVars: 8,
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
<<<<<<< HEAD
=======
if (rf & 1) {
$r3$.ɵɵallocHostVars(8);
}
>>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction
if (rf & 2) {
$r3$.ɵɵstyleMap(ctx.myStyleExp, $r3$.ɵɵdefaultStyleSanitizer);
$r3$.ɵɵclassMap(ctx.myClassExp);
@ -1212,6 +1193,7 @@ describe('compiler compliance: styling', () => {
// NOTE: IF YOU ARE CHANGING THIS COMPILER SPEC, YOU MAY NEED TO CHANGE THE DIRECTIVE
// DEF THAT'S HARD-CODED IN `ng_class.ts`.
const template = `
hostVars: 2,
hostBindings: function ClassDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
@ -1219,7 +1201,7 @@ describe('compiler compliance: styling', () => {
}
}
hostVars: 2,
hostVars: 4,
hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
$r3$.ɵɵstyleProp("width", ctx.myWidth);
@ -1227,7 +1209,7 @@ describe('compiler compliance: styling', () => {
}
}
hostVars: 2,
hostVars: 4,
hostBindings: function HeightDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
$r3$.ɵɵstyleProp("height", ctx.myHeight);
@ -1917,7 +1899,7 @@ describe('compiler compliance: styling', () => {
};
const template = `
hostVars: 4,
hostVars: 6,
hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
$r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title);
@ -2079,14 +2061,8 @@ describe('compiler compliance: styling', () => {
};
const template = `
hostVars: 9,
hostVars: 10,
hostBindings: function MyDir_HostBindings(rf, ctx, elIndex) {
<<<<<<< HEAD
=======
$r3$.ɵɵallocHostVars(10);
>>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction
if (rf & 2) {
$r3$.ɵɵhostProperty("title", ctx.title);
$r3$.ɵɵupdateSyntheticHostBinding("@anim",

View File

@ -2340,7 +2340,7 @@ runInEachFileSystem(os => {
env.driveMain();
const jsContents = env.getContents('test.js');
const hostBindingsFn = `
hostVars: 3,
hostVars: 4,
hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); })("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody)("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); });

View File

@ -524,9 +524,10 @@ function isStyleSanitizable(prop: string): boolean {
// Note that browsers support both the dash case and
// camel case property names when setting through JS.
return prop === 'background-image' || prop === 'backgroundImage' || prop === 'background' ||
prop === 'border-image' || prop === 'borderImage' || prop === 'filter' ||
prop === 'list-style' || prop === 'listStyle' || prop === 'list-style-image' ||
prop === 'listStyleImage' || prop === 'clip-path' || prop === 'clipPath';
prop === 'border-image' || prop === 'borderImage' || prop === 'border-image-source' ||
prop === 'borderImageSource' || prop === 'filter' || prop === 'list-style' ||
prop === 'listStyle' || prop === 'list-style-image' || prop === 'listStyleImage' ||
prop === 'clip-path' || prop === 'clipPath';
}
/**

View File

@ -21,6 +21,11 @@ describe('style parsing', () => {
expect(result).toEqual(['width', '100px', 'height', '200px', 'opacity', '0']);
});
it('should allow empty values', () => {
const result = parseStyle('width:;height: ;');
expect(result).toEqual(['width', '', 'height', '']);
});
it('should trim values and properties', () => {
const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;');
expect(result).toEqual(['width', '333px', 'height', '666px', 'opacity', '0.5']);

View File

@ -73,6 +73,11 @@ export function assertFirstCreatePass(tView: TView, errMessage?: string) {
tView.firstCreatePass, true, errMessage || 'Should only be called in first create pass.');
}
export function assertFirstUpdatePass(tView: TView, errMessage?: string) {
assertEqual(
tView.firstUpdatePass, true, errMessage || 'Should only be called in first update pass.');
}
/**
* This is a basic sanity check that an object is probably a directive def. DirectiveDef is
* an interface, so we can't do a direct instanceof check.

View File

@ -30,7 +30,19 @@ export function getBinding(lView: LView, bindingIndex: number): any {
return lView[bindingIndex];
}
/** Updates binding if changed, then returns whether it was updated. */
/**
* Updates binding if changed, then returns whether it was updated.
*
* This function also checks the `CheckNoChangesMode` and throws if changes are made.
* Some changes (Objects/iterables) during `CheckNoChangesMode` are exempt to comply with VE
* behavior.
*
* @param lView current `LView`
* @param bindingIndex The binding in the `LView` to check
* @param value New value to check against `lView[bindingIndex]`
* @returns `true` if the bindings has changed. (Throws if binding has changed during
* `CheckNoChangesMode`)
*/
export function bindingUpdated(lView: LView, bindingIndex: number, value: any): boolean {
ngDevMode && assertNotSame(value, NO_CHANGE, 'Incoming value should never be NO_CHANGE.');
ngDevMode &&
@ -50,6 +62,11 @@ export function bindingUpdated(lView: LView, bindingIndex: number, value: any):
throwErrorIfNoChangesMode(
oldValue === NO_CHANGE, details.oldValue, details.newValue, details.propName);
}
// There was a change, but the `devModeEqual` decided that the change is exempt from an error.
// For this reason we exit as if no change. The early exit is needed to prevent the changed
// value to be written into `LView` (If we would write the new value that we would not see it
// as change on next CD.)
return false;
}
lView[bindingIndex] = value;
return true;

View File

@ -20,9 +20,10 @@ import {CLEAN_PROMISE, addHostBindingsToExpandoInstructions, addToViewTree, crea
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
import {TElementNode, TNode, TNodeType} from './interfaces/node';
import {PlayerHandler} from './interfaces/player';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, LView, LViewFlags, RootContext, RootContextFlags, TVIEW, TViewType} from './interfaces/view';
import {enterView, getPreviousOrParentTNode, incrementActiveDirectiveId, leaveView, setActiveHostElement} from './state';
import {enterView, getPreviousOrParentTNode, leaveView, setActiveHostElement} from './state';
import {writeDirectClass, writeDirectStyle} from './styling/reconcile';
import {computeStaticStyling} from './styling/static_styling';
import {setUpAttributes} from './util/attrs_utils';
import {publishDefaultGlobalUtils} from './util/global_utils';
@ -119,8 +120,9 @@ export function renderComponent<T>(
// The first index of the first selector is the tag name.
const componentTag = componentDef.selectors ![0] ![0] as string;
const hostRenderer = rendererFactory.createRenderer(null, null);
const hostRNode =
locateHostElement(rendererFactory, opts.host || componentTag, componentDef.encapsulation);
locateHostElement(hostRenderer, opts.host || componentTag, componentDef.encapsulation);
const rootFlags = componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot :
LViewFlags.CheckAlways | LViewFlags.IsRoot;
const rootContext = createRootContext(opts.scheduler, opts.playerHandler);
@ -137,7 +139,7 @@ export function renderComponent<T>(
try {
if (rendererFactory.begin) rendererFactory.begin();
const componentView = createRootComponentView(
hostRNode, componentDef, rootView, rendererFactory, renderer, sanitizer);
hostRNode, componentDef, rootView, rendererFactory, renderer, null, sanitizer);
component = createRootComponent(
componentView, componentDef, rootView, rootContext, opts.hostFeatures || null);
@ -160,14 +162,15 @@ export function renderComponent<T>(
* @param rNode Render host element.
* @param def ComponentDef
* @param rootView The parent view where the host node is stored
* @param renderer The current renderer
* @param hostRenderer The current renderer
* @param sanitizer The sanitizer, if provided
*
* @returns Component view created
*/
export function createRootComponentView(
rNode: RElement | null, def: ComponentDef<any>, rootView: LView,
rendererFactory: RendererFactory3, renderer: Renderer3, sanitizer?: Sanitizer | null): LView {
rendererFactory: RendererFactory3, hostRenderer: Renderer3, addVersion: string | null,
sanitizer: Sanitizer | null): LView {
const tView = rootView[TVIEW];
ngDevMode && assertDataInRange(rootView, 0 + HEADER_OFFSET);
rootView[0 + HEADER_OFFSET] = rNode;
@ -176,13 +179,27 @@ export function createRootComponentView(
if (mergedAttrs !== null) {
computeStaticStyling(tNode, mergedAttrs);
if (rNode !== null) {
setUpAttributes(renderer, rNode, mergedAttrs);
setUpAttributes(hostRenderer, rNode, mergedAttrs);
if (tNode.classes !== null) {
writeDirectClass(hostRenderer, rNode, tNode.classes);
}
if (tNode.styles !== null) {
writeDirectStyle(hostRenderer, rNode, tNode.styles);
}
}
}
const viewRenderer = rendererFactory.createRenderer(rNode, def);
if (rNode !== null && addVersion) {
ngDevMode && ngDevMode.rendererSetAttribute++;
isProceduralRenderer(hostRenderer) ?
hostRenderer.setAttribute(rNode, 'ng-version', addVersion) :
rNode.setAttribute('ng-version', addVersion);
}
const componentView = createLView(
rootView, getOrCreateTComponentView(def), null,
def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, rootView[HEADER_OFFSET], tNode,
rendererFactory, renderer, sanitizer);
rendererFactory, viewRenderer, sanitizer);
if (tView.firstCreatePass) {
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), tView, def.type);
@ -219,26 +236,17 @@ export function createRootComponent<T>(
}
const rootTNode = getPreviousOrParentTNode();
// TODO(misko-next): This is a temporary work around for the fact that we moved the information
// from instruction to declaration. The workaround is to just call the instruction as if it was
// part of the `hostAttrs`.
// The check for componentDef.hostBindings is wrong since now some directives may not
// have componentDef.hostBindings but they still need to process hostVars and hostAttrs
if (tView.firstCreatePass &&
(componentDef.hostBindings !== null || componentDef.hostAttrs !== null)) {
const elementIndex = rootTNode.index - HEADER_OFFSET;
setActiveHostElement(elementIndex);
incrementActiveDirectiveId();
const rootTView = rootLView[TVIEW];
addHostBindingsToExpandoInstructions(rootTView, componentDef);
growHostVarsSpace(rootTView, rootLView, componentDef.hostVars);
invokeHostBindingsInCreationMode(componentDef, component, rootTNode);
setActiveHostElement(null);
}
return component;
}

View File

@ -19,7 +19,6 @@ import {RendererFactory2} from '../render/api';
import {Sanitizer} from '../sanitization/sanitizer';
import {VERSION} from '../version';
import {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '../view/provider';
import {assertComponentType} from './assert';
import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component';
import {getComponentDef} from './definition';
@ -27,7 +26,7 @@ import {NodeInjector} from './di';
import {assignTViewNodeToLView, createLView, createTView, elementCreate, locateHostElement, renderView} from './instructions/shared';
import {ComponentDef} from './interfaces/definition';
import {TContainerNode, TElementContainerNode, TElementNode} from './interfaces/node';
import {RNode, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {RNode, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
import {LView, LViewFlags, TVIEW, TViewType} from './interfaces/view';
import {stringifyCSSSelectorList} from './node_selector_matcher';
import {enterView, leaveView} from './state';
@ -132,8 +131,9 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
rootViewInjector.get(RendererFactory2, domRendererFactory3) as RendererFactory3;
const sanitizer = rootViewInjector.get(Sanitizer, null);
const hostRenderer = rendererFactory.createRenderer(null, this.componentDef);
const hostRNode = rootSelectorOrNode ?
locateHostElement(rendererFactory, rootSelectorOrNode, this.componentDef.encapsulation) :
locateHostElement(hostRenderer, rootSelectorOrNode, this.componentDef.encapsulation) :
// Determine a tag name used for creating host elements when this component is created
// dynamically. Default to 'div' if this component did not specify any tag name in its
// selector.
@ -152,20 +152,13 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
/^#root-ng-internal-isolated-\d+/.test(rootSelectorOrNode);
const rootContext = createRootContext();
const renderer = rendererFactory.createRenderer(hostRNode, this.componentDef);
if (rootSelectorOrNode && hostRNode) {
ngDevMode && ngDevMode.rendererSetAttribute++;
isProceduralRenderer(renderer) ?
renderer.setAttribute(hostRNode, 'ng-version', VERSION.full) :
hostRNode.setAttribute('ng-version', VERSION.full);
}
// Create the root view. Uses empty TView and ContentTemplate.
const rootTView = createTView(TViewType.Root, -1, null, 1, 0, null, null, null, null, null);
const rootLView = createLView(
null, rootTView, rootContext, rootFlags, null, null, rendererFactory, renderer, sanitizer,
rootViewInjector);
null, rootTView, rootContext, rootFlags, null, null, rendererFactory, hostRenderer,
sanitizer, rootViewInjector);
const addVersion = rootSelectorOrNode && hostRNode ? VERSION.full : null;
// rootView is the parent when bootstrapping
// TODO(misko): it looks like we are entering view here but we don't really need to as
@ -179,7 +172,7 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
try {
const componentView = createRootComponentView(
hostRNode, this.componentDef, rootLView, rendererFactory, renderer);
hostRNode, this.componentDef, rootLView, rendererFactory, hostRenderer, addVersion, null);
tElementNode = getTNode(0, rootLView) as TElementNode;

View File

@ -29,7 +29,6 @@ import {enterDI, leaveDI} from './state';
import {isNameOnlyAttributeMarker} from './util/attrs_utils';
import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from './util/injector_utils';
import {stringifyForError} from './util/misc_utils';
import {getInitialStylingValue} from './util/styling_utils';
@ -269,10 +268,10 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
ngDevMode && assertDefined(tNode, 'expecting tNode');
if (attrNameToInject === 'class') {
return getInitialStylingValue(tNode.classes);
return tNode.classes;
}
if (attrNameToInject === 'style') {
return getInitialStylingValue(tNode.styles);
return tNode.styles;
}
const attrs = tNode.attrs;

View File

@ -8,7 +8,7 @@
import {assertDataInRange, assertGreaterThan} from '../../util/assert';
import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks';
import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TVIEW} from '../interfaces/view';
import {ActiveElementFlags, executeElementExitFn, getCheckNoChangesMode, getLView, getSelectedIndex, hasActiveElementFlag, setSelectedIndex} from '../state';
import {executeElementExitFn, getCheckNoChangesMode, getLView, getSelectedIndex, setSelectedIndex} from '../state';
@ -46,6 +46,7 @@ export function ɵɵadvance(delta: number): void {
* @codeGenApi
*/
export function ɵɵselect(index: number): void {
// TODO(misko): Remove this function as it is no longer being used.
selectIndexInternal(getLView(), index, getCheckNoChangesMode());
}
@ -53,9 +54,7 @@ export function selectIndexInternal(lView: LView, index: number, checkNoChangesM
ngDevMode && assertGreaterThan(index, -1, 'Invalid index');
ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET);
if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) {
executeElementExitFn();
}
executeElementExitFn();
// Flush the initial hooks for elements in the view that have been added up to this point.
// PERF WARNING: do NOT extract this to a separate function without running benchmarks

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {getLView, getSelectedIndex} from '../state';
import {getLView} from '../state';
import {CLASS_MAP_STYLING_KEY} from '../styling/style_binding_list';
import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation';
import {classMapInternal} from './styling';
import {checkStylingMap} from './styling';
@ -37,7 +37,7 @@ import {classMapInternal} from './styling';
export function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void {
const lView = getLView();
const interpolatedValue = interpolation1(lView, prefix, v0, suffix);
classMapInternal(getSelectedIndex(), interpolatedValue);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
}
/**
@ -67,7 +67,7 @@ export function ɵɵclassMapInterpolate2(
prefix: string, v0: any, i0: string, v1: any, suffix: string): void {
const lView = getLView();
const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix);
classMapInternal(getSelectedIndex(), interpolatedValue);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
}
/**
@ -100,7 +100,7 @@ export function ɵɵclassMapInterpolate3(
prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): void {
const lView = getLView();
const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix);
classMapInternal(getSelectedIndex(), interpolatedValue);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
}
/**
@ -136,7 +136,7 @@ export function ɵɵclassMapInterpolate4(
suffix: string): void {
const lView = getLView();
const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix);
classMapInternal(getSelectedIndex(), interpolatedValue);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
}
/**
@ -175,7 +175,7 @@ export function ɵɵclassMapInterpolate5(
const lView = getLView();
const interpolatedValue =
interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix);
classMapInternal(getSelectedIndex(), interpolatedValue);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
}
/**
@ -216,7 +216,7 @@ export function ɵɵclassMapInterpolate6(
const lView = getLView();
const interpolatedValue =
interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix);
classMapInternal(getSelectedIndex(), interpolatedValue);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
}
/**
@ -259,7 +259,7 @@ export function ɵɵclassMapInterpolate7(
const lView = getLView();
const interpolatedValue =
interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix);
classMapInternal(getSelectedIndex(), interpolatedValue);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
}
/**
@ -305,7 +305,7 @@ export function ɵɵclassMapInterpolate8(
const lView = getLView();
const interpolatedValue = interpolation8(
lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix);
classMapInternal(getSelectedIndex(), interpolatedValue);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
}
/**
@ -334,5 +334,5 @@ export function ɵɵclassMapInterpolate8(
export function ɵɵclassMapInterpolateV(values: any[]): void {
const lView = getLView();
const interpolatedValue = interpolationV(lView, values);
classMapInternal(getSelectedIndex(), interpolatedValue);
checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true);
}

View File

@ -10,20 +10,20 @@ import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert';
import {assertFirstCreatePass, assertHasParent} from '../assert';
import {attachPatchData} from '../context_discovery';
import {registerPostOrderHooks} from '../hooks';
import {TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {TAttributes, TElementNode, TNode, TNodeType, hasClassInput, hasStyleInput} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {StylingMapArray, TStylingContext} from '../interfaces/styling';
import {isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks';
import {HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from '../interfaces/view';
import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation';
import {decreaseElementDepthCount, getBindingIndex, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state';
import {decreaseElementDepthCount, getBindingIndex, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state';
import {writeDirectClass, writeDirectStyle} from '../styling/reconcile';
import {computeStaticStyling} from '../styling/static_styling';
import {setUpAttributes} from '../util/attrs_utils';
import {getInitialStylingValue, hasClassInput, hasStyleInput, selectClassBasedInputName} from '../util/styling_utils';
import {getConstant, getNativeByTNode, getTNode} from '../util/view_utils';
import {getConstant} from '../util/view_utils';
import {setDirectiveInputsWhichShadowsStyling} from './property';
import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared';
import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, matchingSchemas, renderInitialStyling, resolveDirectives, saveResolvedLocalsInData, setInputsForProperty} from './shared';
import {registerInitialStylingOnTNode} from './styling';
function elementStartFirstCreatePass(
index: number, tView: TView, lView: LView, native: RElement, name: string,
@ -40,7 +40,7 @@ function elementStartFirstCreatePass(
ngDevMode && warnAboutUnknownElement(lView, native, tNode, hasDirectives);
if (tNode.mergedAttrs !== null) {
registerInitialStylingOnTNode(tNode, tNode.mergedAttrs, 0);
computeStaticStyling(tNode, tNode.mergedAttrs);
}
if (tView.queries !== null) {
@ -88,8 +88,13 @@ export function ɵɵelementStart(
if (mergedAttrs !== null) {
setUpAttributes(renderer, native, mergedAttrs);
}
if ((tNode.flags & TNodeFlags.hasInitialStyling) === TNodeFlags.hasInitialStyling) {
renderInitialStyling(renderer, native, tNode, false);
const classes = tNode.classes;
if (classes !== null) {
writeDirectClass(renderer, native, classes);
}
const styles = tNode.styles;
if (styles !== null) {
writeDirectStyle(renderer, native, styles);
}
appendChild(native, tNode, lView);
@ -143,17 +148,15 @@ export function ɵɵelementEnd(): void {
}
}
if (hasClassInput(tNode)) {
const inputName: string = selectClassBasedInputName(tNode.inputs !);
setDirectiveStylingInput(tNode.classes, lView, tNode.inputs ![inputName], inputName);
if (tNode.classes !== null && hasClassInput(tNode)) {
setDirectiveInputsWhichShadowsStyling(tNode, lView, tNode.classes, true);
}
if (hasStyleInput(tNode)) {
setDirectiveStylingInput(tNode.styles, lView, tNode.inputs !['style'], 'style');
if (tNode.styles !== null && hasStyleInput(tNode)) {
setDirectiveInputsWhichShadowsStyling(tNode, lView, tNode.styles, false);
}
}
/**
* Creates an empty element using {@link elementStart} and {@link elementEnd}
*
@ -170,20 +173,6 @@ export function ɵɵelement(
ɵɵelementEnd();
}
function setDirectiveStylingInput(
context: TStylingContext | StylingMapArray | string | null, lView: LView,
stylingInputs: (string | number)[], propName: string) {
// older versions of Angular treat the input as `null` in the
// event that the value does not exist at all. For this reason
// we can't have a styling value be an empty string.
const value = (context && getInitialStylingValue(context)) || null;
// Ivy does an extra `[class]` write with a falsy value since the value
// is applied during creation mode. This is a deviation from VE and should
// be (Jira Issue = FW-1467).
setInputsForProperty(lView, stylingInputs, propName, value);
}
function warnAboutUnknownElement(
hostView: LView, element: RElement, tNode: TNode, hasDirectives: boolean): void {
const schemas = hostView[TVIEW].schemas;

View File

@ -15,10 +15,10 @@ import {HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from '../interface
import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation';
import {getBindingIndex, getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from '../state';
import {computeStaticStyling} from '../styling/static_styling';
import {getConstant} from '../util/view_utils';
import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, resolveDirectives, saveResolvedLocalsInData} from './shared';
import {registerInitialStylingOnTNode} from './styling';
function elementContainerStartFirstCreatePass(
index: number, tView: TView, lView: LView, attrsIndex?: number | null,
@ -33,7 +33,7 @@ function elementContainerStartFirstCreatePass(
// While ng-container doesn't necessarily support styling, we use the style context to identify
// and execute directives on the ng-container.
if (attrs !== null) {
registerInitialStylingOnTNode(tNode, attrs, 0);
computeStaticStyling(tNode, attrs);
}
const localRefs = getConstant<string[]>(tViewConsts, localRefsIndex);

View File

@ -14,15 +14,13 @@ import {initNgDevMode} from '../../util/ng_dev_mode';
import {ACTIVE_INDEX, ActiveIndexFlag, CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container';
import {DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition';
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from '../interfaces/i18n';
import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node';
import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node';
import {SelectorFlags} from '../interfaces/projection';
import {TQueries} from '../interfaces/query';
import {RComment, RElement, RNode} from '../interfaces/renderer';
import {TStylingContext, TStylingRange} from '../interfaces/styling';
import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate} from '../interfaces/styling';
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, TView, TViewType, T_HOST} from '../interfaces/view';
import {DebugNodeStyling, NodeStylingDebug} from '../styling/styling_debug';
import {attachDebugObject} from '../util/debug_utils';
import {isStylingContext} from '../util/styling_utils';
import {getLContainerActiveIndex, getTNode, unwrapRNode} from '../util/view_utils';
const NG_DEV_MODE = ((typeof ngDevMode === 'undefined' || !!ngDevMode) && initNgDevMode());
@ -140,7 +138,7 @@ export const TViewConstructor = class TView implements ITView {
public components: number[]|null, //
public directiveRegistry: DirectiveDefList|null, //
public pipeRegistry: PipeDefList|null, //
public firstChild: TNode|null, //
public firstChild: ITNode|null, //
public schemas: SchemaMetadata[]|null, //
public consts: TConstants|null, //
) {}
@ -152,7 +150,7 @@ export const TViewConstructor = class TView implements ITView {
}
};
export const TNodeConstructor = class TNode implements ITNode {
class TNode implements ITNode {
constructor(
public tView_: TView, //
public type: TNodeType, //
@ -176,8 +174,8 @@ export const TNodeConstructor = class TNode implements ITNode {
public child: ITNode|null, //
public parent: TElementNode|TContainerNode|null, //
public projection: number|(ITNode|RNode[])[]|null, //
public styles: TStylingContext|null, //
public classes: TStylingContext|null, //
public styles: string|null, //
public classes: string|null, //
public classBindings: TStylingRange, //
public styleBindings: TStylingRange, //
) {}
@ -206,7 +204,6 @@ export const TNodeConstructor = class TNode implements ITNode {
if (this.flags & TNodeFlags.hasClassInput) flags.push('TNodeFlags.hasClassInput');
if (this.flags & TNodeFlags.hasContentQuery) flags.push('TNodeFlags.hasContentQuery');
if (this.flags & TNodeFlags.hasStyleInput) flags.push('TNodeFlags.hasStyleInput');
if (this.flags & TNodeFlags.hasInitialStyling) flags.push('TNodeFlags.hasInitialStyling');
if (this.flags & TNodeFlags.hasHostBindings) flags.push('TNodeFlags.hasHostBindings');
if (this.flags & TNodeFlags.isComponentHost) flags.push('TNodeFlags.isComponentHost');
if (this.flags & TNodeFlags.isDirectiveHost) flags.push('TNodeFlags.isDirectiveHost');
@ -233,9 +230,54 @@ export const TNodeConstructor = class TNode implements ITNode {
buf.push('</', this.tagName || this.type_, '>');
return buf.join('');
}
};
function processTNodeChildren(tNode: TNode | null, buf: string[]) {
get styleBindings_(): DebugStyleBindings { return toDebugStyleBinding(this, false); }
get classBindings_(): DebugStyleBindings { return toDebugStyleBinding(this, true); }
}
export const TNodeDebug = TNode;
export type TNodeDebug = TNode;
export interface DebugStyleBindings extends Array<DebugStyleBinding|string|null> {
[0]: string|null;
}
export interface DebugStyleBinding {
key: TStylingKey;
index: number;
isTemplate: boolean;
prevDuplicate: boolean;
nextDuplicate: boolean;
prevIndex: number;
nextIndex: number;
}
function toDebugStyleBinding(tNode: TNode, isClassBased: boolean): DebugStyleBindings {
const tData = tNode.tView_.data;
const bindings: DebugStyleBindings = [] as any;
const range = isClassBased ? tNode.classBindings : tNode.styleBindings;
const prev = getTStylingRangePrev(range);
const next = getTStylingRangeNext(range);
let isTemplate = next !== 0;
let cursor = isTemplate ? next : prev;
while (cursor !== 0) {
const itemKey = tData[cursor] as TStylingKey;
const itemRange = tData[cursor + 1] as TStylingRange;
bindings.unshift({
key: itemKey,
index: cursor,
isTemplate: isTemplate,
prevDuplicate: getTStylingRangePrevDuplicate(itemRange),
nextDuplicate: getTStylingRangeNextDuplicate(itemRange),
nextIndex: getTStylingRangeNext(itemRange),
prevIndex: getTStylingRangePrev(itemRange),
});
if (cursor === prev) isTemplate = false;
cursor = getTStylingRangePrev(itemRange);
}
bindings.unshift(isClassBased ? tNode.classes : tNode.styles);
return bindings;
}
function processTNodeChildren(tNode: ITNode | null, buf: string[]) {
while (tNode) {
buf.push((tNode as any as{template_: string}).template_);
tNode = tNode.next;
@ -389,8 +431,6 @@ export class LViewDebug {
export interface DebugNode {
html: string|null;
native: Node;
styles: DebugNodeStyling|null;
classes: DebugNodeStyling|null;
nodes: DebugNode[]|null;
component: LViewDebug|null;
}
@ -401,10 +441,10 @@ export interface DebugNode {
* @param tNode
* @param lView
*/
export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|null {
export function toDebugNodes(tNode: ITNode | null, lView: LView): DebugNode[]|null {
if (tNode) {
const debugNodes: DebugNode[] = [];
let tNodeCursor: TNode|null = tNode;
let tNodeCursor: ITNode|null = tNode;
while (tNodeCursor) {
debugNodes.push(buildDebugNode(tNodeCursor, lView, tNodeCursor.index));
tNodeCursor = tNodeCursor.next;
@ -415,19 +455,13 @@ export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|nul
}
}
export function buildDebugNode(tNode: TNode, lView: LView, nodeIndex: number): DebugNode {
export function buildDebugNode(tNode: ITNode, lView: LView, nodeIndex: number): DebugNode {
const rawValue = lView[nodeIndex];
const native = unwrapRNode(rawValue);
const componentLViewDebug = toDebug(readLViewValue(rawValue));
const styles = isStylingContext(tNode.styles) ?
new NodeStylingDebug(tNode.styles as any as TStylingContext, tNode, lView, false) :
null;
const classes = isStylingContext(tNode.classes) ?
new NodeStylingDebug(tNode.classes as any as TStylingContext, tNode, lView, true) :
null;
return {
html: toHtml(native),
native: native as any, styles, classes,
native: native as any,
nodes: toDebugNodes(tNode.child, lView),
component: componentLViewDebug,
};

View File

@ -6,11 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {bindingUpdated} from '../bindings';
import {TNode} from '../interfaces/node';
import {SanitizerFn} from '../interfaces/sanitization';
import {TVIEW} from '../interfaces/view';
import {LView, TVIEW} from '../interfaces/view';
import {getLView, getSelectedIndex, nextBindingIndex} from '../state';
import {elementPropertyInternal, storePropertyBindingMetadata} from './shared';
import {elementPropertyInternal, setInputsForProperty, storePropertyBindingMetadata} from './shared';
/**
@ -42,3 +42,16 @@ export function ɵɵproperty<T>(
}
return ɵɵproperty;
}
/**
* Given `<div style="..." my-dir>` and `MyDir` with `@Input('style')` we need to write to
* directive input.
*/
export function setDirectiveInputsWhichShadowsStyling(
tNode: TNode, lView: LView, value: any, isClassBased: boolean) {
const inputs = tNode.inputs !;
const property = isClassBased ? 'class' : 'style';
// We support both 'class' and `className` hence the fallback.
const stylingInputs = inputs[property] || (isClassBased && inputs['className']);
setInputsForProperty(lView, stylingInputs, property, value);
}

View File

@ -31,16 +31,14 @@ import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRoo
import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TVIEW, TView, TViewType, T_HOST} from '../interfaces/view';
import {assertNodeOfPossibleTypes} from '../node_assert';
import {isNodeMatchingSelectorList} from '../node_selector_matcher';
import {ActiveElementFlags, enterView, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, hasActiveElementFlag, incrementActiveDirectiveId, leaveView, leaveViewProcessExit, setActiveHostElement, setBindingIndex, setBindingRoot, setCheckNoChangesMode, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state';
import {renderStylingMap, writeStylingValueDirectly} from '../styling/bindings';
import {clearActiveHostElement, enterView, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, leaveView, leaveViewProcessExit, setActiveHostElement, setBindingIndex, setBindingRoot, setCheckNoChangesMode, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils';
import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils';
import {getInitialStylingValue} from '../util/styling_utils';
import {getLViewParent} from '../util/view_traversal_utils';
import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils';
import {selectIndexInternal} from './advance';
import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeConstructor, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug';
import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeDebug, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug';
@ -102,15 +100,6 @@ export function setHostBindingsByExecutingExpandoInstructions(tView: TView, lVie
} else {
// If it's not a number, it's a host binding function that needs to be executed.
if (instruction !== null) {
// Each directive gets a uniqueId value that is the same for both
// create and update calls when the hostBindings function is called. The
// directive uniqueId is not set anywhere--it is just incremented between
// each hostBindings call and is useful for helping instruction code
// uniquely determine which directive is currently active when executed.
// It is important that this be called first before the actual instructions
// are run because this way the first directive ID value is not zero.
incrementActiveDirectiveId();
setBindingIndex(bindingRootIndex);
const hostCtx = lView[currentDirectiveIndex];
instruction(RenderFlags.Update, hostCtx, currentElementIndex);
@ -123,10 +112,11 @@ export function setHostBindingsByExecutingExpandoInstructions(tView: TView, lVie
// iterate over those directives which actually have `hostBindings`.
currentDirectiveIndex++;
}
setBindingRoot(bindingRootIndex);
}
}
} finally {
setActiveHostElement(selectedIndex);
clearActiveHostElement();
}
}
@ -493,9 +483,13 @@ export function refreshView<T>(
incrementInitPhaseFlags(lView, InitPhaseState.AfterViewInitHooksToBeRun);
}
}
} finally {
if (tView.firstUpdatePass === true) {
// We need to make sure that we only flip the flag on successful `refreshView` only
// Don't do this in `finally` block.
// If we did this in `finally` block then an exception could block the execution of styling
// instructions which in turn would be unable to insert themselves into the styling linked
// list. The result of this would be that if the exception would not be throw on subsequent CD
// the styling would be unable to process it data and reflect to the DOM.
tView.firstUpdatePass = false;
}
@ -508,7 +502,7 @@ export function refreshView<T>(
if (!checkNoChangesMode) {
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
}
} finally {
leaveViewProcessExit();
}
}
@ -538,7 +532,7 @@ function executeTemplate<T>(
lView: LView, templateFn: ComponentTemplate<T>, rf: RenderFlags, context: T) {
const prevSelectedIndex = getSelectedIndex();
try {
setActiveHostElement(null);
clearActiveHostElement();
if (rf & RenderFlags.Update && lView.length > HEADER_OFFSET) {
// When we're updating, inherently select 0 so we don't
// have to generate that instruction for most update blocks.
@ -546,9 +540,7 @@ function executeTemplate<T>(
}
templateFn(rf, context);
} finally {
if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) {
executeElementExitFn();
}
executeElementExitFn();
setSelectedIndex(prevSelectedIndex);
}
}
@ -742,10 +734,8 @@ function assertHostNodeExists(rElement: RElement, elementOrSelector: RElement |
* @param encapsulation View Encapsulation defined for component that requests host element.
*/
export function locateHostElement(
rendererFactory: RendererFactory3, elementOrSelector: RElement | string,
renderer: Renderer3, elementOrSelector: RElement | string,
encapsulation: ViewEncapsulation): RElement {
const renderer = rendererFactory.createRenderer(null, null);
if (isProceduralRenderer(renderer)) {
// When using native Shadow DOM, do not clear host element to allow native slot projection
const preserveContent = encapsulation === ViewEncapsulation.ShadowDom;
@ -814,7 +804,7 @@ export function createTNode(
adjustedIndex: number, tagName: string | null, attrs: TAttributes | null): TNode {
ngDevMode && ngDevMode.tNode++;
let injectorIndex = tParent ? tParent.injectorIndex : -1;
return ngDevMode ? new TNodeConstructor(
return ngDevMode ? new TNodeDebug(
tView, // tView_: TView
type, // type: TNodeType
adjustedIndex, // index: number
@ -837,8 +827,8 @@ export function createTNode(
null, // child: ITNode|null
tParent, // parent: TElementNode|TContainerNode|null
null, // projection: number|(ITNode|RNode[])[]|null
null, // styles: TStylingContext|null
null, // classes: TStylingContext|null
null, // styles: string|null
null, // classes: string|null
0 as any, // classBindings: TStylingRange;
0 as any, // styleBindings: TStylingRange;
) :
@ -1264,7 +1254,7 @@ function instantiateAllDirectives(
}
}
function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNode) {
function invokeDirectivesHostBindings(tView: TView, lView: LView, tNode: TNode) {
const start = tNode.directiveStart;
const end = tNode.directiveEnd;
const expando = tView.expandoInstructions !;
@ -1272,25 +1262,27 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
const elementIndex = tNode.index - HEADER_OFFSET;
try {
setActiveHostElement(elementIndex);
for (let i = start; i < end; i++) {
const def = tView.data[i] as DirectiveDef<any>;
const directive = viewData[i];
const directive = lView[i];
if (def.hostBindings !== null || def.hostVars !== 0 || def.hostAttrs !== null) {
// It is important that this be called first before the actual instructions
// are run because this way the first directive ID value is not zero.
incrementActiveDirectiveId();
invokeHostBindingsInCreationMode(def, directive, tNode);
} else if (firstCreatePass) {
expando.push(null);
}
}
} finally {
setActiveHostElement(null);
clearActiveHostElement();
}
}
// TODO(COMMIT): jsdoc
/**
* Invoke the host bindings in creation mode.
*
* @param def `DirectiveDef` which may contain the `hostBindings` function.
* @param directive Instance of directive.
* @param tNode Associated `TNode`.
*/
export function invokeHostBindingsInCreationMode(
def: DirectiveDef<any>, directive: any, tNode: TNode) {
if (def.hostBindings !== null) {
@ -1300,11 +1292,11 @@ export function invokeHostBindingsInCreationMode(
}
/**
* Generates a new block in TView.expandoInstructions for this node.
*
* Each expando block starts with the element index (turned negative so we can distinguish
* it from the hostVar count) and the directive count. See more in VIEW_DATA.md.
*/
* Generates a new block in TView.expandoInstructions for this node.
*
* Each expando block starts with the element index (turned negative so we can distinguish
* it from the hostVar count) and the directive count. See more in VIEW_DATA.md.
*/
export function generateExpandoInstructionBlock(
tView: TView, tNode: TNode, directiveCount: number): void {
ngDevMode && assertEqual(
@ -1993,4 +1985,3 @@ export function textBindingInternal(lView: LView, index: number, value: string):
const renderer = lView[RENDERER];
isProceduralRenderer(renderer) ? renderer.setValue(element, value) : element.textContent = value;
}

View File

@ -5,11 +5,10 @@
* 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 {getLView, getSelectedIndex} from '../state';
import {getLView,} from '../state';
import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation';
import {stylePropInternal} from './styling';
import {checkStylingProperty} from './styling';
/**
@ -43,7 +42,7 @@ export function ɵɵstylePropInterpolate1(
valueSuffix?: string | null): typeof ɵɵstylePropInterpolate1 {
const lView = getLView();
const interpolatedValue = interpolation1(lView, prefix, v0, suffix);
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
checkStylingProperty(prop, interpolatedValue, valueSuffix, false);
return ɵɵstylePropInterpolate1;
}
@ -80,7 +79,7 @@ export function ɵɵstylePropInterpolate2(
valueSuffix?: string | null): typeof ɵɵstylePropInterpolate2 {
const lView = getLView();
const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix);
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
checkStylingProperty(prop, interpolatedValue, valueSuffix, false);
return ɵɵstylePropInterpolate2;
}
@ -119,7 +118,7 @@ export function ɵɵstylePropInterpolate3(
valueSuffix?: string | null): typeof ɵɵstylePropInterpolate3 {
const lView = getLView();
const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix);
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
checkStylingProperty(prop, interpolatedValue, valueSuffix, false);
return ɵɵstylePropInterpolate3;
}
@ -160,7 +159,7 @@ export function ɵɵstylePropInterpolate4(
v3: any, suffix: string, valueSuffix?: string | null): typeof ɵɵstylePropInterpolate4 {
const lView = getLView();
const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix);
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
checkStylingProperty(prop, interpolatedValue, valueSuffix, false);
return ɵɵstylePropInterpolate4;
}
@ -205,7 +204,7 @@ export function ɵɵstylePropInterpolate5(
const lView = getLView();
const interpolatedValue =
interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix);
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
checkStylingProperty(prop, interpolatedValue, valueSuffix, false);
return ɵɵstylePropInterpolate5;
}
@ -252,7 +251,7 @@ export function ɵɵstylePropInterpolate6(
const lView = getLView();
const interpolatedValue =
interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix);
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
checkStylingProperty(prop, interpolatedValue, valueSuffix, false);
return ɵɵstylePropInterpolate6;
}
@ -302,7 +301,7 @@ export function ɵɵstylePropInterpolate7(
const lView = getLView();
const interpolatedValue =
interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix);
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
checkStylingProperty(prop, interpolatedValue, valueSuffix, false);
return ɵɵstylePropInterpolate7;
}
@ -354,7 +353,7 @@ export function ɵɵstylePropInterpolate8(
const lView = getLView();
const interpolatedValue = interpolation8(
lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix);
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
checkStylingProperty(prop, interpolatedValue, valueSuffix, false);
return ɵɵstylePropInterpolate8;
}
@ -392,6 +391,6 @@ export function ɵɵstylePropInterpolateV(
prop: string, values: any[], valueSuffix?: string | null): typeof ɵɵstylePropInterpolateV {
const lView = getLView();
const interpolatedValue = interpolationV(lView, values);
stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix);
checkStylingProperty(prop, interpolatedValue, valueSuffix, false);
return ɵɵstylePropInterpolateV;
}

View File

@ -7,22 +7,22 @@
*/
import {SafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {throwErrorIfNoChangesMode} from '../errors';
import {setInputsForProperty} from '../instructions/shared';
import {AttributeMarker, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {assertEqual, assertGreaterThan, assertLessThan} from '../../util/assert';
import {concatStringsWithSpace} from '../../util/stringify';
import {assertFirstUpdatePass} from '../assert';
import {bindingUpdated} from '../bindings';
import {TNode, TNodeFlags, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from '../interfaces/styling';
import {isDirectiveHost} from '../interfaces/type_checks';
import {LView, RENDERER, TVIEW} from '../interfaces/view';
import {getActiveDirectiveId, getCheckNoChangesMode, getCurrentStyleSanitizer, getLView, getSelectedIndex, incrementBindingIndex, nextBindingIndex, resetCurrentStyleSanitizer, setCurrentStyleSanitizer, setElementExitFn} from '../state';
import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from '../styling/bindings';
import {activateStylingMapFeature} from '../styling/map_based_bindings';
import {attachStylingDebugObject} from '../styling/styling_debug';
import {SanitizerFn} from '../interfaces/sanitization';
import {TStylingKey, TStylingMapKey, TStylingSanitizationKey, TStylingSuffixKey, getTStylingRangeTail} from '../interfaces/styling';
import {HEADER_OFFSET, RENDERER, TVIEW, TView} from '../interfaces/view';
import {getCheckNoChangesMode, getClassBindingChanged, getCurrentStyleSanitizer, getLView, getSelectedIndex, getStyleBindingChanged, incrementBindingIndex, isActiveHostElement, markStylingBindingDirty, setCurrentStyleSanitizer, setElementExitFn} from '../state';
import {writeAndReconcileClass, writeAndReconcileStyle} from '../styling/reconcile';
import {CLASS_MAP_STYLING_KEY, IGNORE_DUE_TO_INPUT_SHADOW, STYLE_MAP_STYLING_KEY, flushStyleBinding, insertTStylingBinding} from '../styling/style_binding_list';
import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils';
import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, getValue, hasClassInput, hasStyleInput, hasValueChanged, hasValueChangedUnwrapSafeValue, isHostStylingActive, isStylingContext, isStylingMapArray, isStylingValueDefined, normalizeIntoStylingMap, patchConfig, selectClassBasedInputName, setValue, stylingMapToString} from '../util/styling_utils';
import {getNativeByTNode, getTNode} from '../util/view_utils';
import {unwrapRNode} from '../util/view_utils';
import {setDirectiveInputsWhichShadowsStyling} from './property';
@ -68,9 +68,9 @@ export function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void {
* @codeGenApi
*/
export function ɵɵstyleProp(
prop: string, value: string | number | SafeValue | null,
prop: string, value: string | number | SafeValue | null | undefined,
suffix?: string | null): typeof ɵɵstyleProp {
stylePropInternal(getSelectedIndex(), prop, value, suffix);
checkStylingProperty(prop, value, suffix, false);
return ɵɵstyleProp;
}
@ -89,32 +89,9 @@ export function ɵɵstyleProp(
*
* @codeGenApi
*/
export function ɵɵclassProp(className: string, value: boolean | null): typeof ɵɵclassProp {
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = nextBindingIndex();
const lView = getLView();
const elementIndex = getSelectedIndex();
const tNode = getTNode(elementIndex, lView);
const firstUpdatePass = lView[TVIEW].firstUpdatePass;
// we check for this in the instruction code so that the context can be notified
// about prop or map bindings so that the direct apply check can decide earlier
// if it allows for context resolution to be bypassed.
if (firstUpdatePass) {
patchConfig(tNode, TNodeFlags.hasClassPropBindings);
patchHostStylingFlag(tNode, isHostStyling(), true);
}
const updated = stylingProp(tNode, firstUpdatePass, lView, bindingIndex, className, value, true);
if (ngDevMode) {
ngDevMode.classProp++;
if (updated) {
ngDevMode.classPropCacheMiss++;
}
}
export function ɵɵclassProp(
className: string, value: boolean | null | undefined): typeof ɵɵclassProp {
checkStylingProperty(className, value, null, true);
return ɵɵclassProp;
}
@ -138,39 +115,10 @@ export function ɵɵclassProp(className: string, value: boolean | null): typeof
*
* @codeGenApi
*/
export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | null): void {
const index = getSelectedIndex();
const lView = getLView();
const tNode = getTNode(index, lView);
const firstUpdatePass = lView[TVIEW].firstUpdatePass;
const context = getStylesContext(tNode);
const hasDirectiveInput = hasStyleInput(tNode);
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = incrementBindingIndex(2);
const hostBindingsMode = isHostStyling();
// inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function)
if (!hostBindingsMode && hasDirectiveInput && styles !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false, firstUpdatePass);
styles = NO_CHANGE;
}
// we check for this in the instruction code so that the context can be notified
// about prop or map bindings so that the direct apply check can decide earlier
// if it allows for context resolution to be bypassed.
if (firstUpdatePass) {
patchConfig(tNode, TNodeFlags.hasStyleMapBindings);
patchHostStylingFlag(tNode, isHostStyling(), false);
}
stylingMap(
context, tNode, firstUpdatePass, lView, bindingIndex, styles, false, hasDirectiveInput);
export function ɵɵstyleMap(
styles: {[styleName: string]: any} | Map<string, string|number|null|undefined>| string | null |
undefined): void {
checkStylingMap(STYLE_MAP_STYLING_KEY, styles, false);
}
/**
@ -191,7 +139,224 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu
*
* @codeGenApi
*/
export function ɵɵclassMap(classes: {[className: string]: any} | NO_CHANGE | string | null): void {
classMapInternal(getSelectedIndex(), classes);
export function ɵɵclassMap(
classes: {[className: string]: boolean | null | undefined} |
Map<string, boolean|undefined|null>| Set<string>| string[] | string | null | undefined): void {
checkStylingMap(CLASS_MAP_STYLING_KEY, classes, true);
}
/**
* Common code between `ɵɵclassProp` and `ɵɵstyleProp`.
*
* @param prop property name.
* @param value binding value.
* @param suffixOrSanitizer suffix or sanitization function
* @param isClassBased `true` if `class` change (`false` if `style`)
*/
export function checkStylingProperty(
prop: string, value: any | NO_CHANGE,
suffixOrSanitizer: SanitizerFn | string | null | undefined, isClassBased: boolean): void {
const lView = getLView();
const tView = lView[TVIEW];
// Styling instructions use 2 slots per binding.
// 1. one for the value / TStylingKey
// 2. one for the intermittent-value / TStylingRange
const bindingIndex = incrementBindingIndex(2);
if (tView.firstUpdatePass) {
// This is a work around. Once PR#34480 lands the sanitizer is passed explicitly and this line
// can be removed.
let styleSanitizer: StyleSanitizeFn|null;
if (suffixOrSanitizer == null) {
if (styleSanitizer = getCurrentStyleSanitizer()) {
suffixOrSanitizer = styleSanitizer as any;
}
}
stylingPropertyFirstUpdatePass(tView, prop, suffixOrSanitizer, bindingIndex, isClassBased);
}
if (value !== NO_CHANGE && bindingUpdated(lView, bindingIndex, value)) {
markStylingBindingDirty(bindingIndex, isClassBased);
setElementExitFn(flushStylingOnElementExit);
}
}
/**
* Common code between `ɵɵclassMap` and `ɵɵstyleMap`.
*
* @param tStylingMapKey See `STYLE_MAP_STYLING_KEY` and `CLASS_MAP_STYLING_KEY`.
* @param value binding value.
* @param isClassBased `true` if `class` change (`false` if `style`)
*/
export function checkStylingMap(
tStylingMapKey: TStylingMapKey, value: any | NO_CHANGE, isClassBased: boolean): void {
const lView = getLView();
const tView = lView[TVIEW];
const bindingIndex = incrementBindingIndex(2);
if (tView.firstUpdatePass) {
stylingPropertyFirstUpdatePass(tView, tStylingMapKey, null, bindingIndex, isClassBased);
}
if (value !== NO_CHANGE && bindingUpdated(lView, bindingIndex, value)) {
// `getSelectedIndex()` should be here (rather than in instruction) so that it is guarded by the
// if so as not to read unnecessarily.
const tNode = tView.data[getSelectedIndex() + HEADER_OFFSET] as TNode;
if (hasStylingInputShadow(tNode, isClassBased) && !isInHostBindings(tView, bindingIndex)) {
// VE concatenates the static portion with the dynamic portion.
// We are doing the same.
let staticPrefix = isClassBased ? tNode.classes : tNode.styles;
ngDevMode && isClassBased === false && staticPrefix !== null &&
assertEqual(
staticPrefix.endsWith(';'), true, 'Expecting static portion to end with \';\'');
if (typeof value === 'string') {
value = concatStringsWithSpace(staticPrefix, value as string);
}
// Given `<div [style] my-dir>` such that `my-dir` has `@Input('style')`.
// This takes over the `[style]` binding. (Same for `[class]`)
setDirectiveInputsWhichShadowsStyling(tNode, lView, value, isClassBased);
} else {
markStylingBindingDirty(bindingIndex, isClassBased);
setElementExitFn(flushStylingOnElementExit);
}
}
}
/**
* Determines when the binding is in `hostBindings` section
*
* @param tView Current `TView`
* @param bindingIndex index of binding which we would like if it is in `hostBindings`
*/
function isInHostBindings(tView: TView, bindingIndex: number): boolean {
// All host bindings are placed after the expando section.
return bindingIndex >= tView.expandoStartIndex;
}
/**
* Collects the necessary information to insert the binding into a linked list of style bindings
* using `insertTStylingBinding`.
*
* @param tView `TView` where the binding linked list will be stored.
* @param prop Property/key of the binding.
* @param suffix Optional suffix or Sanitization function.
* @param bindingIndex Index of binding associated with the `prop`
* @param isClassBased `true` if `class` change (`false` if `style`)
*/
function stylingPropertyFirstUpdatePass(
tView: TView, prop: TStylingMapKey, suffix: null, bindingIndex: number,
isClassBased: boolean): void;
function stylingPropertyFirstUpdatePass(
tView: TView, prop: string, suffix: SanitizerFn | string | null | undefined,
bindingIndex: number, isClassBased: boolean): void;
function stylingPropertyFirstUpdatePass(
tView: TView, prop: string | TStylingMapKey,
suffixOrSanitization: SanitizerFn | string | null | undefined, bindingIndex: number,
isClassBased: boolean): void {
ngDevMode && assertFirstUpdatePass(tView);
const tData = tView.data;
if (tData[bindingIndex + 1] === null) {
// The above check is necessary because we don't clear first update pass until first successful
// (no exception) template execution. This prevents the styling instruction from double adding
// itself to the list.
// `getSelectedIndex()` should be here (rather than in instruction) so that it is guarded by the
// if so as not to read unnecessarily.
const tNode = tData[getSelectedIndex() + HEADER_OFFSET] as TNode;
if (hasStylingInputShadow(tNode, isClassBased) && typeof prop === 'object' &&
!isInHostBindings(tView, bindingIndex)) {
// typeof prop === 'object' implies that we are either `STYLE_MAP_STYLING_KEY` or
// `CLASS_MAP_STYLING_KEY` which means that we are either `[style]` or `[class]` binding.
// If there is a directive which uses `@Input('style')` or `@Input('class')` than
// we need to neutralize this binding since that directive is shadowing it.
// We turn this into a noop using `IGNORE_DUE_TO_INPUT_SHADOW`
prop = IGNORE_DUE_TO_INPUT_SHADOW;
}
const tStylingKey: TStylingKey = suffixOrSanitization == null ? prop : ({
key: prop as string, extra: suffixOrSanitization
} as TStylingSuffixKey | TStylingSanitizationKey);
insertTStylingBinding(
tData, tNode, tStylingKey, bindingIndex, isActiveHostElement(), isClassBased);
}
}
/**
* Tests if the `TNode` has input shadow.
*
* An input shadow is when a directive steals (shadows) the input by using `@Input('style')` or
* `@Input('class')` as input.
*
* @param tNode `TNode` which we would like to see if it has shadow.
* @param isClassBased `true` if `class` (`false` if `style`)
*/
export function hasStylingInputShadow(tNode: TNode, isClassBased: boolean) {
return (tNode.flags & (isClassBased ? TNodeFlags.hasClassInput : TNodeFlags.hasStyleInput)) !== 0;
}
/**
* Flushes styling into DOM element from the bindings.
*
* The function starts at `LFrame.stylingBindingChanged` and computes new styling information from
* the bindings progressing towards the tail of the list. At the end the resulting style is written
* into the DOM Element.
*
* This function is invoked from:
* 1. Template `advance` instruction.
* 2. HostBinding instruction.
*/
function flushStylingOnElementExit() {
ngDevMode && assertEqual(
getStyleBindingChanged() > 0 || getClassBindingChanged() > 0, true,
'Only expected to be here if binding has changed.');
ngDevMode &&
assertEqual(
getCheckNoChangesMode(), false, 'Should never get here during check no changes mode');
const lView = getLView();
const tView = lView[TVIEW];
const tData = tView.data;
const elementIndex = getSelectedIndex() + HEADER_OFFSET;
const tNode = tData[elementIndex] as TNode;
const renderer = lView[RENDERER];
const element = unwrapRNode(lView[elementIndex]) as RElement;
const classBindingIndex = getClassBindingChanged();
if (classBindingIndex > 0) {
const classLastWrittenValueIndex = getTStylingRangeTail(tNode.classBindings) + 1;
ngDevMode &&
assertGreaterThan(
classLastWrittenValueIndex, 1,
'Ignoring `class` binding because there is no `class` metadata associated with the element. ' +
'(Was exception thrown during `firstUpdatePass` which prevented the metadata creation?)');
ngDevMode &&
assertLessThan(classLastWrittenValueIndex, lView.length, 'Reading past end of LView');
const lastValue: string|NO_CHANGE = lView[classLastWrittenValueIndex];
const newValue = flushStyleBinding(tData, tNode, lView, classBindingIndex, true);
if (lastValue !== newValue) {
if (tNode.type === TNodeType.Element) {
writeAndReconcileClass(
renderer, element, lastValue === NO_CHANGE ? tNode.classes || '' : lastValue as string,
newValue);
}
lView[classLastWrittenValueIndex] = newValue;
}
}
const styleBindingIndex = getStyleBindingChanged();
if (styleBindingIndex > 0) {
const styleLastWrittenValueIndex = getTStylingRangeTail(tNode.styleBindings) + 1;
ngDevMode &&
assertGreaterThan(
styleLastWrittenValueIndex, 1,
'Ignoring `style` binding because there is no `style` metadata associated with the element. ' +
'(Was exception thrown during `firstUpdatePass` which prevented the metadata creation?)');
ngDevMode &&
assertLessThan(styleLastWrittenValueIndex, lView.length, 'Reading past end of LView');
const lastValue: string|NO_CHANGE = lView[styleLastWrittenValueIndex];
const newValue = flushStyleBinding(tData, tNode, lView, styleBindingIndex, false);
if (lastValue !== newValue) {
if (tNode.type === TNodeType.Element) {
writeAndReconcileStyle(
renderer, element, lastValue === NO_CHANGE ? tNode.styles || '' : lastValue as string,
newValue);
}
lView[styleLastWrittenValueIndex] = newValue;
}
}
ngDevMode && ngDevMode.flushStyling++;
}

View File

@ -77,111 +77,6 @@ export const enum TNodeFlags {
* that actually have directives with host bindings.
*/
hasHostBindings = 0x80,
/** Bit #9 - This bit is set if the node has initial styling */
hasInitialStyling = 0x100,
/**
* Bit #10 - Whether or not there are class-based map bindings present.
*
* Examples include:
* 1. `<div [class]="x">`
* 2. `@HostBinding('class') x`
*/
hasClassMapBindings = 0x200,
/**
* Bit #11 - Whether or not there are any class-based prop bindings present.
*
* Examples include:
* 1. `<div [class.name]="x">`
* 2. `@HostBinding('class.name') x`
*/
hasClassPropBindings = 0x400,
/**
* Bit #12 - whether or not there are any active [class] and [class.name] bindings
*/
hasClassPropAndMapBindings = hasClassMapBindings | hasClassPropBindings,
/**
* Bit #13 - Whether or not the context contains one or more class-based template bindings.
*
* Examples include:
* 1. `<div [class]="x">`
* 2. `<div [class.name]="x">`
*/
hasTemplateClassBindings = 0x800,
/**
* Bit #14 - Whether or not the context contains one or more class-based host bindings.
*
* Examples include:
* 1. `@HostBinding('class') x`
* 2. `@HostBinding('class.name') x`
*/
hasHostClassBindings = 0x1000,
/**
* Bit #15 - Whether or not there are two or more sources for a class property in the context.
*
* Examples include:
* 1. prop + prop: `<div [class.active]="x" dir-that-sets-active-class>`
* 2. map + prop: `<div [class]="x" [class.foo]>`
* 3. map + map: `<div [class]="x" dir-that-sets-class>`
*/
hasDuplicateClassBindings = 0x2000,
/**
* Bit #16 - Whether or not there are style-based map bindings present.
*
* Examples include:
* 1. `<div [style]="x">`
* 2. `@HostBinding('style') x`
*/
hasStyleMapBindings = 0x4000,
/**
* Bit #17 - Whether or not there are any style-based prop bindings present.
*
* Examples include:
* 1. `<div [style.prop]="x">`
* 2. `@HostBinding('style.prop') x`
*/
hasStylePropBindings = 0x8000,
/**
* Bit #18 - whether or not there are any active [style] and [style.prop] bindings
*/
hasStylePropAndMapBindings = hasStyleMapBindings | hasStylePropBindings,
/**
* Bit #19 - Whether or not the context contains one or more style-based template bindings.
*
* Examples include:
* 1. `<div [style]="x">`
* 2. `<div [style.prop]="x">`
*/
hasTemplateStyleBindings = 0x10000,
/**
* Bit #20 - Whether or not the context contains one or more style-based host bindings.
*
* Examples include:
* 1. `@HostBinding('style') x`
* 2. `@HostBinding('style.prop') x`
*/
hasHostStyleBindings = 0x20000,
/**
* Bit #21 - Whether or not there are two or more sources for a style property in the context.
*
* Examples include:
* 1. prop + prop: `<div [style.width]="x" dir-that-sets-width>`
* 2. map + prop: `<div [style]="x" [style.prop]>`
* 3. map + map: `<div [style]="x" dir-that-sets-style>`
*/
hasDuplicateStyleBindings = 0x40000,
}
/**
@ -588,20 +483,8 @@ export interface TNode {
* This field will be populated if and when:
*
* - There are one or more initial styles on an element (e.g. `<div style="width:200px">`)
* - There are one or more style bindings on an element (e.g. `<div [style.width]="w">`)
*
* If and when there are only initial styles (no bindings) then an instance of `StylingMapArray`
* will be used here. Otherwise an instance of `TStylingContext` will be created when there
* are one or more style bindings on an element.
*
* During element creation this value is likely to be populated with an instance of
* `StylingMapArray` and only when the bindings are evaluated (which happens during
* update mode) then it will be converted to a `TStylingContext` if any style bindings
* are encountered. If and when this happens then the existing `StylingMapArray` value
* will be placed into the initial styling slot in the newly created `TStylingContext`.
*/
// TODO(misko): `Remove StylingMapArray|TStylingContext|null` in follow up PR.
styles: StylingMapArray|TStylingContext|string|null;
styles: string|null;
/**
* A collection of all class bindings and/or static class values for an element.
@ -609,20 +492,8 @@ export interface TNode {
* This field will be populated if and when:
*
* - There are one or more initial classes on an element (e.g. `<div class="one two three">`)
* - There are one or more class bindings on an element (e.g. `<div [class.foo]="f">`)
*
* If and when there are only initial classes (no bindings) then an instance of `StylingMapArray`
* will be used here. Otherwise an instance of `TStylingContext` will be created when there
* are one or more class bindings on an element.
*
* During element creation this value is likely to be populated with an instance of
* `StylingMapArray` and only when the bindings are evaluated (which happens during
* update mode) then it will be converted to a `TStylingContext` if any class bindings
* are encountered. If and when this happens then the existing `StylingMapArray` value
* will be placed into the initial styling slot in the newly created `TStylingContext`.
*/
// TODO(misko): `Remove StylingMapArray|TStylingContext|null` in follow up PR.
classes: StylingMapArray|TStylingContext|string|null;
classes: string|null;
/**
* Stores the head/tail index of the class bindings.
@ -842,3 +713,53 @@ export type TNodeWithLocalRefs = TContainerNode | TElementNode | TElementContain
* - `<ng-template #tplRef>` - `tplRef` should point to the `TemplateRef` instance;
*/
export type LocalRefExtractor = (tNode: TNodeWithLocalRefs, currentView: LView) => any;
/**
* Returns `true` if the `TNode` has a directive which has `@Input()` for `class` binding.
*
* ```
* <div my-dir [class]="exp"></div>
* ```
* and
* ```
* @Directive({
* })
* class MyDirective {
* @Input()
* class: string;
* }
* ```
*
* In the above case it is necessary to write the reconciled styling information into the
* directive's input.
*
* @param tNode
*/
export function hasClassInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasClassInput) !== 0;
}
/**
* Returns `true` if the `TNode` has a directive which has `@Input()` for `style` binding.
*
* ```
* <div my-dir [style]="exp"></div>
* ```
* and
* ```
* @Directive({
* })
* class MyDirective {
* @Input()
* class: string;
* }
* ```
*
* In the above case it is necessary to write the reconciled styling information into the
* directive's input.
*
* @param tNode
*/
export function hasStyleInput(tNode: TNode) {
return (tNode.flags & TNodeFlags.hasStyleInput) !== 0;
}

View File

@ -642,3 +642,8 @@ export function getTStylingRangeNextDuplicate(tStylingRange: TStylingRange): boo
export function setTStylingRangeNextDuplicate(tStylingRange: TStylingRange): TStylingRange {
return ((tStylingRange as any as number) | StylingRange.NEXT_DUPLICATE) as any;
}
export function getTStylingRangeTail(tStylingRange: TStylingRange): number {
const next = getTStylingRangeNext(tStylingRange);
return next === 0 ? getTStylingRangePrev(tStylingRange) : next;
}

View File

@ -471,7 +471,17 @@ export interface TView {
/** Whether or not this template has been processed in creation mode. */
firstCreatePass: boolean;
/** Whether or not the first update for this template has been processed. */
/**
* Whether or not this template has been processed in update mode (e.g. change detected)
*
* `firstUpdatePass` is used by styling to set up `TData` to contain metadata about the styling
* instructions. (Mainly to build up a linked list of styling priority order.)
*
* Typically this function gets cleared after first execution. If exception is thrown then this
* flag can remain turned un until there is first successful (no exception) pass. This means that
* individual styling instructions keep track of if they have already been added to the linked
* list to prevent double adding.
*/
firstUpdatePass: boolean;
/** Static data equivalent of LView.data[]. Contains TNodes, PipeDefInternal or TI18n. */

View File

@ -7,8 +7,7 @@
*/
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {assertDefined, assertEqual} from '../util/assert';
import {assertDefined, assertEqual, assertGreaterThan} from '../util/assert';
import {assertLViewOrUndefined} from './assert';
import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {TNode} from './interfaces/node';
@ -44,7 +43,7 @@ interface LFrame {
/**
* Used to set the parent property when nodes are created and track query results.
*
* This is used in conjection with `isParent`.
* This is used in conjunction with `isParent`.
*/
previousOrParentTNode: TNode;
@ -94,11 +93,6 @@ interface LFrame {
currentSanitizer: StyleSanitizeFn|null;
/**
* Used when processing host bindings.
*/
currentDirectiveDef: DirectiveDef<any>|ComponentDef<any>|null;
/**
* The root index from which pure function instructions should calculate their binding
* indices. In component views, this is TView.bindingStartIndex. In a host binding
@ -111,6 +105,18 @@ interface LFrame {
* We iterate over the list of Queries and increment current query index at every step.
*/
currentQueryIndex: number;
/**
* Stores the index of the style binding which changed first.
*
* A change in styling binding implies that all bindings starting with this index need to be
* recomputed. See: `flushStylingOnElementExit` and `markStylingBindingDirty` functions for
* details.
*
* If this value is set then `flushStylingOnElementExit` needs to execute during the `advance`
* instruction to update the styling.
*/
stylingBindingChanged: number;
}
/**
@ -259,17 +265,15 @@ export function getLView(): LView {
* function or when all host bindings are processed for an element).
*/
export const enum ActiveElementFlags {
Initial = 0b00,
RunExitFn = 0b01,
HostMode = 0b1,
RunExitFn = 0b1,
Size = 1,
}
/**
* Sets a flag is for the active element.
*/
function setActiveElementFlag(flag: ActiveElementFlags) {
instructionState.lFrame.selectedIndex |= flag;
export function isActiveHostElement(): boolean {
return (instructionState.lFrame.selectedIndex & ActiveElementFlags.HostMode) ===
ActiveElementFlags.HostMode;
}
/**
@ -279,17 +283,23 @@ function setActiveElementFlag(flag: ActiveElementFlags) {
* @param elementIndex the element index value for the host element where
* the directive/component instance lives
*/
export function setActiveHostElement(elementIndex: number | null) {
if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) {
executeElementExitFn();
}
setSelectedIndex(elementIndex === null ? -1 : elementIndex);
instructionState.lFrame.activeDirectiveId = 0;
export function setActiveHostElement(elementIndex: number) {
executeElementExitFn();
setSelectedIndex(elementIndex);
instructionState.lFrame.selectedIndex |= ActiveElementFlags.HostMode;
}
export function clearActiveHostElement() {
executeElementExitFn();
setSelectedIndex(-1);
}
export function executeElementExitFn() {
instructionState.elementExitFn !();
instructionState.lFrame.selectedIndex &= ~ActiveElementFlags.RunExitFn;
const lFrame = instructionState.lFrame;
if (lFrame.stylingBindingChanged !== 0) {
instructionState.elementExitFn !();
lFrame.stylingBindingChanged = 0;
}
}
/**
@ -306,7 +316,6 @@ export function executeElementExitFn() {
* @param fn
*/
export function setElementExitFn(fn: () => void): void {
setActiveElementFlag(ActiveElementFlags.RunExitFn);
if (instructionState.elementExitFn === null) {
instructionState.elementExitFn = fn;
}
@ -354,6 +363,7 @@ export function getContextLView(): LView {
}
export function getCheckNoChangesMode(): boolean {
// TODO(misko): remove this from the LView since it is ngDevMode=true mode only.
return instructionState.checkNoChangesMode;
}
@ -430,8 +440,6 @@ export function enterDI(newView: LView, tNode: TNode) {
newLFrame.elementDepthCount = DEV_MODE_VALUE;
newLFrame.currentNamespace = DEV_MODE_VALUE;
newLFrame.currentSanitizer = DEV_MODE_VALUE;
newLFrame.currentDirectiveDef = DEV_MODE_VALUE;
newLFrame.activeDirectiveId = DEV_MODE_VALUE;
newLFrame.bindingRootIndex = DEV_MODE_VALUE;
newLFrame.currentQueryIndex = DEV_MODE_VALUE;
}
@ -471,11 +479,10 @@ export function enterView(newView: LView, tNode: TNode | null): void {
newLFrame.elementDepthCount = 0;
newLFrame.currentNamespace = null;
newLFrame.currentSanitizer = null;
newLFrame.currentDirectiveDef = null;
newLFrame.activeDirectiveId = 0;
newLFrame.bindingRootIndex = -1;
newLFrame.bindingIndex = newView === null ? -1 : newView[TVIEW].bindingStartIndex;
newLFrame.currentQueryIndex = 0;
newLFrame.stylingBindingChanged = 0;
}
/**
@ -498,22 +505,19 @@ function createLFrame(parent: LFrame | null): LFrame {
elementDepthCount: 0, //
currentNamespace: null, //
currentSanitizer: null, //
currentDirectiveDef: null, //
activeDirectiveId: 0, //
bindingRootIndex: -1, //
bindingIndex: -1, //
currentQueryIndex: 0, //
parent: parent !, //
child: null, //
stylingBindingChanged: 0, //
};
parent !== null && (parent.child = lFrame); // link the new LFrame for reuse.
return lFrame;
}
export function leaveViewProcessExit() {
if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) {
executeElementExitFn();
}
executeElementExitFn();
leaveView();
}
@ -539,7 +543,7 @@ function walkUpViews(nestingLevel: number, currentView: LView): LView {
}
/**
* Gets the most recent index passed to {@link select}
* Gets the currently selected element index.
*
* Used with {@link property} instruction (and more in the future) to identify the index in the
* current `LView` to act on.
@ -616,3 +620,48 @@ export function getCurrentStyleSanitizer() {
const lFrame = instructionState.lFrame;
return lFrame === null ? null : lFrame.currentSanitizer;
}
/**
* Used for encoding both Class and Style index into `LFrame.stylingBindingChanged`.
*/
const enum BindingChanged {
CLASS_SHIFT = 16,
STYLE_MASK = 0xFFFF,
}
/**
* Store the first binding location from where the style flushing should start.
*
* This function stores the first binding location. Any subsequent binding changes are ignored as
* they are downstream from this change and will be picked up once the flushing starts traversing
* forward.
*
* Because flushing for template and flushing for host elements are separate, we don't need to worry
* about the fact that they will be out of order.
*
* @param bindingIndex Index of binding location. This will be a binding location from which the
* flushing of styling should start.
* @param isClassBased `true` if `class` change (`false` if `style`)
*/
export function markStylingBindingDirty(bindingIndex: number, isClassBased: boolean) {
ngDevMode && assertGreaterThan(bindingIndex, 0, 'expected valid binding index changed');
ngDevMode &&
assertEqual(
getCheckNoChangesMode(), false, 'Should never get here during check no changes mode');
const lFrame = instructionState.lFrame;
const stylingBindingChanged = lFrame.stylingBindingChanged;
const stylingBindingChangedExtracted = isClassBased ?
stylingBindingChanged >> BindingChanged.CLASS_SHIFT :
stylingBindingChanged & BindingChanged.STYLE_MASK;
if (stylingBindingChangedExtracted === 0) {
lFrame.stylingBindingChanged = stylingBindingChanged |
(isClassBased ? bindingIndex << BindingChanged.CLASS_SHIFT : bindingIndex);
}
}
export function getClassBindingChanged() {
return instructionState.lFrame.stylingBindingChanged >> BindingChanged.CLASS_SHIFT;
}
export function getStyleBindingChanged() {
return instructionState.lFrame.stylingBindingChanged & BindingChanged.STYLE_MASK;
}

View File

@ -6,11 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertNotEqual} from '../../util/assert';
import {CharCode} from '../../util/char_code';
import {concatStringsWithSpace} from '../../util/stringify';
import {consumeWhitespace, getLastParsedKey, parseClassName, parseClassNameNext} from './styling_parser';
/**
* Computes the diff between two class-list strings.
*
@ -92,17 +94,6 @@ export function processClassToken(
}
}
/**
* Removes a class from a `className` string.
*
* @param className A string containing classes (whitespace separated)
* @param classToRemove A class name to remove from the `className`
* @returns a new class-list which does not have `classToRemove`
*/
export function removeClass(className: string, classToRemove: string): string {
return toggleClass(className, classToRemove, false);
}
/**
* Toggles a class in `className` string.
*
@ -121,17 +112,23 @@ export function toggleClass(className: string, classToToggle: string, toggle: bo
start = classIndexOf(className, classToToggle, start);
if (start === -1) {
if (toggle === true) {
className = className === '' ? classToToggle : className + ' ' + classToToggle;
className = concatStringsWithSpace(className, classToToggle);
}
break;
}
const removeLength = classToToggle.length;
if (toggle === true) {
// we found it and we should have it so just return
return className;
} else {
const length = classToToggle.length;
// Cut out the class which should be removed.
const endWhitespace = consumeWhitespace(className, start + removeLength, end);
const endWhitespace = consumeWhitespace(className, start + length, end);
if (endWhitespace === end) {
// If we are the last token then we need back search trailing whitespace.
while (start > 0 && className.charCodeAt(start - 1) <= CharCode.SPACE) {
start--;
}
}
className = className.substring(0, start) + className.substring(endWhitespace, end);
end = className.length;
}
@ -151,15 +148,16 @@ export function toggleClass(className: string, classToToggle: string, toggle: bo
*/
export function classIndexOf(
className: string, classToSearch: string, startingIndex: number): number {
ngDevMode && assertNotEqual(classToSearch, '', 'can not look for "" string.');
let end = className.length;
while (true) {
const foundIndex = className.indexOf(classToSearch, startingIndex);
if (foundIndex === -1) return foundIndex;
if (foundIndex === 0 || className.charCodeAt(foundIndex - 1) <= CharCode.SPACE) {
// Ensure that it has leading whitespace
const removeLength = classToSearch.length;
if (foundIndex + removeLength === end ||
className.charCodeAt(foundIndex + removeLength) <= CharCode.SPACE) {
const length = classToSearch.length;
if (foundIndex + length === end ||
className.charCodeAt(foundIndex + length) <= CharCode.SPACE) {
// Ensure that it has trailing whitespace
return foundIndex;
}

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertDefined, assertString} from '../../util/assert';
import {ProceduralRenderer3, RElement, Renderer3, isProceduralRenderer} from '../interfaces/renderer';
import {computeClassChanges} from './class_differ';
import {computeStyleChanges} from './style_differ';
@ -43,14 +44,11 @@ import {computeStyleChanges} from './style_differ';
*/
export function writeAndReconcileClass(
renderer: Renderer3, element: RElement, expectedValue: string, newValue: string): void {
ngDevMode && assertDefined(element, 'Expecting DOM element');
ngDevMode && assertString(expectedValue, '\'oldValue\' should be a string');
ngDevMode && assertString(newValue, '\'newValue\' should be a string');
if (element.className === expectedValue) {
// This is the simple/fast case where no one has written into element without our knowledge.
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(element, 'class', newValue);
} else {
element.className = newValue;
}
ngDevMode && ngDevMode.rendererSetClassName++;
writeDirectClass(renderer, element, newValue);
} else {
// The expected value is not the same as last value. Something changed the DOM element without
// our knowledge so we need to do reconciliation instead.
@ -58,6 +56,32 @@ export function writeAndReconcileClass(
}
}
/**
* Write `className` to `RElement`.
*
* This function does direct write without any reconciliation. Used for writing initial values, so
* that static styling values do not pull in the style parser.
*
* @param renderer Renderer to use
* @param element The element which needs to be updated.
* @param newValue The new class list to write.
*/
export function writeDirectClass(renderer: Renderer3, element: RElement, newValue: string) {
ngDevMode && assertString(newValue, '\'newValue\' should be a string');
if (isProceduralRenderer(renderer)) {
if (newValue === '') {
// There are tests in `google3` which expect `element.getAttribute('class')` to be `null`.
// TODO(commit): add test case
renderer.removeAttribute(element, 'class');
} else {
renderer.setAttribute(element, 'class', newValue);
}
} else {
element.className = newValue;
}
ngDevMode && ngDevMode.rendererSetClassName++;
}
/**
* Writes new `cssText` value in the DOM node.
*
@ -88,15 +112,12 @@ export function writeAndReconcileClass(
*/
export function writeAndReconcileStyle(
renderer: Renderer3, element: RElement, expectedValue: string, newValue: string): void {
const style = (element as HTMLElement).style;
if (style != null && style.cssText === expectedValue) {
// This is the simple/fast case where no one has written into element without our knowledge.
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(element, 'style', newValue);
} else {
style.cssText = newValue;
}
ngDevMode && ngDevMode.rendererCssText++;
ngDevMode && assertDefined(element, 'Expecting DOM element');
ngDevMode && assertString(expectedValue, '\'expectedValue\' should be a string');
ngDevMode && assertString(newValue, '\'newValue\' should be a string');
const style = expectedValue === null ? null : (element as HTMLElement).style;
if (expectedValue === null || style != null && (style !.cssText === expectedValue)) {
writeDirectStyle(renderer, element, newValue);
} else {
// The expected value is not the same as last value. Something changed the DOM element without
// our knowledge so we need to do reconciliation instead.
@ -104,6 +125,26 @@ export function writeAndReconcileStyle(
}
}
/**
* Write `cssText` to `RElement`.
*
* This function does direct write without any reconciliation. Used for writing initial values, so
* that static styling values do not pull in the style parser.
*
* @param renderer Renderer to use
* @param element The element which needs to be updated.
* @param newValue The new class list to write.
*/
export function writeDirectStyle(renderer: Renderer3, element: RElement, newValue: string) {
ngDevMode && assertString(newValue, '\'newValue\' should be a string');
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(element, 'style', newValue);
} else {
(element as HTMLElement).style.cssText = newValue;
}
ngDevMode && ngDevMode.rendererSetStyle++;
}
/**
* Writes to `classNames` by computing the difference between `oldValue` and `newValue` and using
* `classList.add` and `classList.remove`.

View File

@ -23,8 +23,8 @@ import {getLView} from '../state';
export function computeStaticStyling(tNode: TNode, attrs: TAttributes): void {
ngDevMode && assertFirstCreatePass(
getLView()[TVIEW], 'Expecting to be called in first template pass only');
let styles: string|null = tNode.styles as string | null;
let classes: string|null = tNode.classes as string | null;
let styles: string|null = tNode.styles;
let classes: string|null = tNode.classes;
let mode: AttributeMarker|0 = 0;
for (let i = 0; i < attrs.length; i++) {
const value = attrs[i];

View File

@ -6,13 +6,18 @@
* found in the LICENSE file at https://angular.io/license
*/
import {unwrapSafeValue} from '../../sanitization/bypass';
import {stylePropNeedsSanitization, ɵɵsanitizeStyle} from '../../sanitization/sanitization';
import {assertEqual, throwError} from '../../util/assert';
import {assertEqual, assertString, throwError} from '../../util/assert';
import {CharCode} from '../../util/char_code';
import {concatStringsWithSpace} from '../../util/stringify';
import {assertFirstUpdatePass} from '../assert';
import {TNode} from '../interfaces/node';
import {SanitizerFn} from '../interfaces/sanitization';
import {TStylingKey, TStylingMapKey, TStylingRange, getTStylingRangeNext, getTStylingRangePrev, getTStylingRangePrevDuplicate, setTStylingRangeNext, setTStylingRangeNextDuplicate, setTStylingRangePrev, setTStylingRangePrevDuplicate, toTStylingRange} from '../interfaces/styling';
import {LView, TData, TVIEW} from '../interfaces/view';
import {getLView} from '../state';
import {NO_CHANGE} from '../tokens';
import {splitClassList, toggleClass} from './class_differ';
import {StyleChangesMap, parseKeyValue, removeStyle} from './style_differ';
import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseStyleNext} from './styling_parser';
@ -172,7 +177,7 @@ import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseS
* NOTE: See `should support example in 'tnode_linked_list.ts' documentation` in
* `tnode_linked_list_spec.ts` for working example.
*/
let __unused_const_as_closure_does_not_like_standalone_comment_blocks__: undefined;
/**
* Insert new `tStyleValue` at `TData` and link existing style bindings such that we maintain linked
@ -197,9 +202,7 @@ import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseS
export function insertTStylingBinding(
tData: TData, tNode: TNode, tStylingKey: TStylingKey, index: number, isHostBinding: boolean,
isClassBinding: boolean): void {
ngDevMode && assertEqual(
getLView()[TVIEW].firstUpdatePass, true,
'Should be called during \'firstUpdatePass` only.');
ngDevMode && assertFirstUpdatePass(getLView()[TVIEW]);
let tBindings = isClassBinding ? tNode.classBindings : tNode.styleBindings;
let tmplHead = getTStylingRangePrev(tBindings);
let tmplTail = getTStylingRangeNext(tBindings);
@ -255,7 +258,7 @@ export function insertTStylingBinding(
// Now we need to update / compute the duplicates.
// Starting with our location search towards head (least priority)
markDuplicates(
tData, tStylingKey, index, (isClassBinding ? tNode.classes : tNode.styles) as string, true,
tData, tStylingKey, index, (isClassBinding ? tNode.classes : tNode.styles) || '', true,
isClassBinding);
markDuplicates(tData, tStylingKey, index, '', false, isClassBinding);
@ -347,6 +350,7 @@ function markDuplicates(
cursor = isPrevDir ? getTStylingRangePrev(tStyleRangeAtCursor) :
getTStylingRangeNext(tStyleRangeAtCursor);
}
// We also need to process the static values.
if (staticValues !== '' && // If we have static values to search
!foundDuplicate // If we have duplicate don't bother since we are already marked as
// duplicate
@ -355,6 +359,8 @@ function markDuplicates(
// if we are a Map (and we have statics) we must assume duplicate
foundDuplicate = true;
} else if (staticValues != null) {
// If we found non-map then we iterate over its keys to determine if any of them match ours
// If we find a match than we mark it as duplicate.
for (let i = isClassBinding ? parseClassName(staticValues) : parseStyle(staticValues); //
i >= 0; //
i = isClassBinding ? parseClassNameNext(staticValues, i) :
@ -367,6 +373,7 @@ function markDuplicates(
}
}
if (foundDuplicate) {
// if we found a duplicate, than mark ourselves.
tData[index + 1] = isPrevDir ? setTStylingRangePrevDuplicate(tStylingAtIndex) :
setTStylingRangeNextDuplicate(tStylingAtIndex);
}
@ -392,18 +399,21 @@ export function flushStyleBinding(
// value and look up the previous concatenation as a starting point going forward.
const lastUnchangedValueIndex = getTStylingRangePrev(tStylingRangeAtIndex);
let text = lastUnchangedValueIndex === 0 ?
((isClassBinding ? tNode.classes : tNode.styles) as string) :
lView[lastUnchangedValueIndex + 1] as string;
(isClassBinding ? tNode.classes : tNode.styles) :
lView[lastUnchangedValueIndex + 1] as string | NO_CHANGE;
if (text === null || text === NO_CHANGE) text = '';
ngDevMode && assertString(text, 'Last unchanged value should be a string');
let cursor = index;
while (cursor !== 0) {
const value = lView[cursor];
const key = tData[cursor] as TStylingKey;
const stylingRange = tData[cursor + 1] as TStylingRange;
lView[cursor + 1] = text = appendStyling(
text, key, value, null, getTStylingRangePrevDuplicate(stylingRange), isClassBinding);
text as string, key, value, null, getTStylingRangePrevDuplicate(stylingRange),
isClassBinding);
cursor = getTStylingRangeNext(stylingRange);
}
return text;
return text as string;
}
@ -452,17 +462,27 @@ export function appendStyling(
if (hasPreviousDuplicate) {
text = toggleClass(text, stylingKey as string, !!value);
} else if (value) {
text = text === '' ? stylingKey as string : text + ' ' + stylingKey;
text = concatStringsWithSpace(text, stylingKey as string);
}
} else {
if (value === undefined) {
// If undefined than treat it as if we have no value. This means that we will fallback to the
// previous value (if any).
// `<div style="width: 10px" [style.width]="{width: undefined}">` => `width: 10px`.
return text;
}
if (hasPreviousDuplicate) {
text = removeStyle(text, key);
}
const keyValue =
key + ': ' + (typeof suffixOrSanitizer === 'function' ?
suffixOrSanitizer(value) :
(suffixOrSanitizer == null ? value : value + suffixOrSanitizer));
text = text === '' ? keyValue : text + '; ' + keyValue;
if (value !== false && value !== null) {
// `<div style="width: 10px" [style.width]="{width: null}">` => ``. (remove it)
// `<div style="width: 10px" [style.width]="{width: false}">` => ``. (remove it)
value = typeof suffixOrSanitizer === 'function' ? suffixOrSanitizer(value) :
unwrapSafeValue(value);
const keyValue = key + ': ' +
(typeof suffixOrSanitizer === 'string' ? value + suffixOrSanitizer : value) + ';';
text = concatStringsWithSpace(text, keyValue);
}
}
return text;
}
@ -489,7 +509,10 @@ export const CLASS_MAP_STYLING_KEY: TStylingMapKey = {
} else if (typeof value === 'object') {
// We support maps
for (let key in value) {
text = appendStyling(text, key, value[key], null, hasPreviousDuplicate, true);
if (key !== '') {
// We have to guard for `""` empty string as key since it will break search and replace.
text = appendStyling(text, key, value[key], null, hasPreviousDuplicate, true);
}
}
} else if (typeof value === 'string') {
// We support strings
@ -500,7 +523,7 @@ export const CLASS_MAP_STYLING_KEY: TStylingMapKey = {
changes.forEach((_, key) => text = appendStyling(text, key, true, null, true, true));
} else {
// No duplicates, just append it.
text = text === '' ? value : text + ' ' + value;
text = concatStringsWithSpace(text, value);
}
} else {
// All other cases are not supported.
@ -531,9 +554,12 @@ export const STYLE_MAP_STYLING_KEY: TStylingMapKey = {
} else if (typeof value === 'object') {
// We support maps
for (let key in value) {
text = appendStyling(
text, key, value[key], stylePropNeedsSanitization(key) ? ɵɵsanitizeStyle : null,
hasPreviousDuplicate, false);
if (key !== '') {
// We have to guard for `""` empty string as key since it will break search and replace.
text = appendStyling(
text, key, value[key], stylePropNeedsSanitization(key) ? ɵɵsanitizeStyle : null,
hasPreviousDuplicate, false);
}
}
} else if (typeof value === 'string') {
// We support strings
@ -548,7 +574,10 @@ export const STYLE_MAP_STYLING_KEY: TStylingMapKey = {
true, false));
} else {
// No duplicates, just append it.
text = text === '' ? value : text + '; ' + value;
if (value.charCodeAt(value.length - 1) !== CharCode.SEMI_COLON) {
value += ';';
}
text = concatStringsWithSpace(text, value);
}
} else {
// All other cases are not supported.
@ -557,3 +586,14 @@ export const STYLE_MAP_STYLING_KEY: TStylingMapKey = {
return text;
}
};
/**
* If we have `<div [class] my-dir>` such that `my-dir` has `@Input('class')`, the `my-dir` captures
* the `[class]` binding, so that it no longer participates in the style bindings. For this case
* we use `IGNORE_DUE_TO_INPUT_SHADOW` so that `flushStyleBinding` ignores it.
*/
export const IGNORE_DUE_TO_INPUT_SHADOW: TStylingMapKey = {
key: null,
extra: (text: string, value: any, hasPreviousDuplicate: boolean): string => { return text;}
};

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {getLastParsedKey, getLastParsedValue, getLastParsedValueEnd, parseStyle, parseStyleNext, resetParserState} from './styling_parser';
import {concatStringsWithSpace} from '../../util/stringify';
import {consumeWhitespace, getLastParsedKey, getLastParsedValue, parseStyle, parseStyleNext, resetParserState} from './styling_parser';
/**
* Stores changes to Style values.
@ -111,18 +112,20 @@ export function removeStyle(cssText: string, styleToRemove: string): string {
for (let i = parseStyle(cssText); i >= 0; i = parseStyleNext(cssText, i)) {
const key = getLastParsedKey(cssText);
if (key === styleToRemove) {
// Consume any remaining whitespace.
i = consumeWhitespace(cssText, i, cssText.length);
if (lastValueEnd === 0) {
cssText = cssText.substring(i);
i = 0;
} else if (i === cssText.length) {
return cssText.substring(0, lastValueEnd);
} else {
cssText = cssText.substring(0, lastValueEnd) + '; ' + cssText.substring(i);
i = lastValueEnd + 2; // 2 is for '; '.length(so that we skip the separator)
cssText = concatStringsWithSpace(cssText.substring(0, lastValueEnd), cssText.substring(i));
i = lastValueEnd + 1; // 1 is for ';'.length(so that we skip the separator)
}
resetParserState(cssText);
}
lastValueEnd = getLastParsedValueEnd();
lastValueEnd = i;
}
return cssText;
}

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {assertEqual, throwError} from '../../util/assert';
import {CharCode} from '../../util/char_code';
/**
@ -142,17 +143,16 @@ export function parseStyle(text: string): number {
*/
export function parseStyleNext(text: string, startIndex: number): number {
const end = parserState.textEnd;
if (end === startIndex) {
let index = parserState.key = consumeWhitespace(text, startIndex, end);
if (end === index) {
// we reached an end so just quit
return -1;
}
let index = parserState.keyEnd = consumeStyleKey(text, parserState.key = startIndex, end);
index = parserState.value = consumeSeparatorWithWhitespace(text, index, end, CharCode.COLON);
index = parserState.keyEnd = consumeStyleKey(text, index, end);
index = consumeSeparator(text, index, end, CharCode.COLON);
index = parserState.value = consumeWhitespace(text, index, end);
index = parserState.valueEnd = consumeStyleValue(text, index, end);
if (ngDevMode && parserState.value === parserState.valueEnd) {
throw malformedStyleError(text, index);
}
return consumeSeparatorWithWhitespace(text, index, end, CharCode.SEMI_COLON);
return consumeSeparator(text, index, end, CharCode.SEMI_COLON);
}
/**
@ -167,15 +167,6 @@ export function resetParserState(text: string): void {
parserState.textEnd = text.length;
}
/**
* Retrieves tha `valueEnd` from the parser global state.
*
* See: `ParserState`.
*/
export function getLastParsedValueEnd(): number {
return parserState.valueEnd;
}
/**
* Returns index of next non-whitespace character.
*
@ -233,16 +224,15 @@ export function consumeStyleKey(text: string, startIndex: number, endIndex: numb
* @param endIndex Ending index of character where the scan should end.
* @returns Index after separator and surrounding whitespace.
*/
export function consumeSeparatorWithWhitespace(
export function consumeSeparator(
text: string, startIndex: number, endIndex: number, separator: number): number {
startIndex = consumeWhitespace(text, startIndex, endIndex);
if (startIndex < endIndex) {
if (ngDevMode && text.charCodeAt(startIndex) !== separator) {
throw expectingError(text, String.fromCharCode(separator), startIndex);
malformedStyleError(text, String.fromCharCode(separator), startIndex);
}
startIndex++;
}
startIndex = consumeWhitespace(text, startIndex, endIndex);
return startIndex;
}
@ -310,18 +300,14 @@ export function consumeQuotedText(
ch1 = ch;
}
}
throw ngDevMode ? expectingError(text, String.fromCharCode(quoteCharCode), endIndex) :
throw ngDevMode ? malformedStyleError(text, String.fromCharCode(quoteCharCode), endIndex) :
new Error();
}
function expectingError(text: string, expecting: string, index: number) {
return new Error(
`Expecting '${expecting}' at location ${index} in string '` + text.substring(0, index) +
'[>>' + text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + '\'.');
}
function malformedStyleError(text: string, index: number) {
return new Error(
function malformedStyleError(text: string, expecting: string, index: number): never {
ngDevMode && assertEqual(typeof text === 'string', true, 'String expected here');
throw throwError(
`Malformed style at location ${index} in string '` + text.substring(0, index) + '[>>' +
text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + '\'.');
text.substring(index, index + 1) + '<<]' + text.substr(index + 1) +
`'. Expecting '${expecting}'.`);
}

View File

@ -8,8 +8,9 @@
export interface NO_CHANGE {
// This is a brand that ensures that this type can never match anything else
brand: 'NO_CHANGE';
__brand__: 'NO_CHANGE';
}
/** A special value which designates that a value has not changed. */
export const NO_CHANGE = {} as NO_CHANGE;
export const NO_CHANGE: NO_CHANGE =
(typeof ngDevMode === 'undefined' || ngDevMode) ? {__brand__: 'NO_CHANGE'} : ({} as NO_CHANGE);

View File

@ -187,6 +187,12 @@ export function ɵɵsanitizeUrlOrResourceUrl(unsafeUrl: any, tag: string, prop:
*/
export const ɵɵdefaultStyleSanitizer =
(function(prop: string, value: string|null, mode?: StyleSanitizeMode): string | boolean | null {
if (value === undefined && mode === undefined) {
// This is a workaround for the fact that `StyleSanitizeFn` should not exist once PR#34480
// lands. For now the `StyleSanitizeFn` and should act like `(value: any) => string` as a
// work around.
return ɵɵsanitizeStyle(prop);
}
mode = mode || StyleSanitizeMode.ValidateAndSanitize;
let doSanitizeValue = true;
if (mode & StyleSanitizeMode.ValidateProperty) {
@ -201,9 +207,11 @@ export const ɵɵdefaultStyleSanitizer =
} as StyleSanitizeFn);
export function stylePropNeedsSanitization(prop: string): boolean {
return prop === 'background-image' || prop === 'background' || prop === 'border-image' ||
prop === 'filter' || prop === 'list-style' || prop === 'list-style-image' ||
prop === 'clip-path';
return prop === 'background-image' || prop === 'backgroundImage' || prop === 'background' ||
prop === 'border-image' || prop === 'borderImage' || prop === 'border-image-source' ||
prop === 'borderImageSource' || prop === 'filter' || prop === 'list-style' ||
prop === 'listStyle' || prop === 'list-style-image' || prop === 'listStyleImage' ||
prop === 'clip-path' || prop === 'clipPath';
}
export function validateAgainstEventProperties(name: string) {

View File

@ -37,7 +37,6 @@ export function stringify(token: any): string {
return newLineIndex === -1 ? res : res.substring(0, newLineIndex);
}
/**
* Concatenates two strings with separator, allocating new strings only when necessary.
*
@ -50,4 +49,4 @@ export function concatStringsWithSpace(before: string | null, after: string | nu
return (before == null || before === '') ?
(after === null ? '' : after) :
((after == null || after === '') ? before : before + ' ' + after);
}
}

View File

@ -1536,16 +1536,15 @@ describe('change detection', () => {
});
it('should include style prop name in case of style binding', () => {
const message = ivyEnabled ?
`Previous value for 'style.color': 'red'. Current value: 'green'` :
`Previous value: 'color: red'. Current value: 'color: green'`;
const message = ivyEnabled ? `Previous value for 'color': 'red'. Current value: 'green'` :
`Previous value: 'color: red'. Current value: 'color: green'`;
expect(() => initWithTemplate('<div [style.color]="unstableColorExpression"></div>'))
.toThrowError(new RegExp(message));
});
it('should include class name in case of class binding', () => {
const message = ivyEnabled ?
`Previous value for 'class.someClass': 'true'. Current value: 'false'` :
`Previous value for 'someClass': 'true'. Current value: 'false'` :
`Previous value: 'someClass: true'. Current value: 'someClass: false'`;
expect(() => initWithTemplate('<div [class.someClass]="unstableBooleanExpression"></div>'))
.toThrowError(new RegExp(message));
@ -1574,16 +1573,15 @@ describe('change detection', () => {
});
it('should include style prop name in case of host style bindings', () => {
const message = ivyEnabled ?
`Previous value for 'style.color': 'red'. Current value: 'green'` :
`Previous value: 'color: red'. Current value: 'color: green'`;
const message = ivyEnabled ? `Previous value for 'color': 'red'. Current value: 'green'` :
`Previous value: 'color: red'. Current value: 'color: green'`;
expect(() => initWithHostBindings({'[style.color]': 'unstableColorExpression'}))
.toThrowError(new RegExp(message));
});
it('should include class name in case of host class bindings', () => {
const message = ivyEnabled ?
`Previous value for 'class.someClass': 'true'. Current value: 'false'` :
`Previous value for 'someClass': 'true'. Current value: 'false'` :
`Previous value: 'someClass: true'. Current value: 'someClass: false'`;
expect(() => initWithHostBindings({'[class.someClass]': 'unstableBooleanExpression'}))
.toThrowError(new RegExp(message));

View File

@ -10,6 +10,8 @@ import {Component, Directive, HostBinding, InjectionToken, ViewChild} from '@ang
import {isLView} from '@angular/core/src/render3/interfaces/type_checks';
import {CONTEXT} from '@angular/core/src/render3/interfaces/view';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {getElementStyles} from '@angular/core/testing/src/styling';
import {expect} from '@angular/core/testing/src/testing_internal';
import {onlyInIvy} from '@angular/private/testing';
import {getHostElement, markDirty} from '../../src/render3/index';
@ -473,11 +475,10 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils deprecated', () =>
const childDebug = getDebugNode(child) !;
expect(childDebug.native).toBe(child);
expect(childDebug.styles).toBeTruthy();
const styles = childDebug.styles !.values;
expect(styles['width']).toEqual('200px');
expect(styles['height']).toEqual('400px');
expect(getElementStyles(child)).toEqual({
width: '200px',
height: '400px',
});
});
});
});

View File

@ -254,7 +254,7 @@ describe('host bindings', () => {
}
TestBed.configureTestingModule(
{declarations: [MyApp, ParentDir, ChildDir, SiblingDir]});
{declarations: [MyApp, ParentDir, SiblingDir, ChildDir]});
const fixture = TestBed.createComponent(MyApp);
const element = fixture.nativeElement;
fixture.detectChanges();
@ -262,10 +262,9 @@ describe('host bindings', () => {
const childElement = element.querySelector('div');
// width/height values were set in all directives, but the sub-class directive
// (ChildDir)
// had priority over the parent directive (ParentDir) which is why its value won. It
// also
// won over Dir because the SiblingDir directive was evaluated later on.
// (ChildDir) had priority over the parent directive (ParentDir) which is why its
// value won. It also won over Dir because the SiblingDir directive was declared
// later in `declarations`.
expect(childElement.style.width).toEqual('200px');
expect(childElement.style.height).toEqual('200px');

View File

@ -116,10 +116,10 @@ describe('inheritance', () => {
'Base.backgroundColor', 'Super.color', 'Sub2.width', //
]);
if (ivyEnabled) {
expect(getDirectiveDef(BaseDirective) !.hostVars).toEqual(1);
expect(getDirectiveDef(SuperDirective) !.hostVars).toEqual(2);
expect(getDirectiveDef(Sub1Directive) !.hostVars).toEqual(3);
expect(getDirectiveDef(Sub2Directive) !.hostVars).toEqual(3);
expect(getDirectiveDef(BaseDirective) !.hostVars).toEqual(2);
expect(getDirectiveDef(SuperDirective) !.hostVars).toEqual(4);
expect(getDirectiveDef(Sub1Directive) !.hostVars).toEqual(6);
expect(getDirectiveDef(Sub2Directive) !.hostVars).toEqual(6);
}
});
});

View File

@ -1032,7 +1032,7 @@ describe('acceptance integration tests', () => {
fixture.componentInstance.value = false;
fixture.detectChanges();
expect(structuralCompEl.getAttribute('class')).toEqual('');
expect(structuralCompEl.getAttribute('class')).toBeFalsy();
});
@Directive({selector: '[DirWithClass]'})
@ -1071,7 +1071,7 @@ describe('acceptance integration tests', () => {
it('should delegate initial styles to a [style] input binding if present on a directive on the same element',
() => {
@Component({template: '<div style="width:100px;height:200px" DirWithStyle></div>'})
@Component({template: '<div style="width: 100px; height: 200px" DirWithStyle></div>'})
class App {
@ViewChild(DirWithStyleDirective)
mockStyleDirective !: DirWithStyleDirective;
@ -1084,8 +1084,8 @@ describe('acceptance integration tests', () => {
const styles = fixture.componentInstance.mockStyleDirective.stylesVal;
// Use `toContain` since Ivy and ViewEngine have some slight differences in formatting.
expect(styles).toContain('width:100px');
expect(styles).toContain('height:200px');
expect(styles).toContain('width: 100px');
expect(styles).toContain('height: 200px');
});
it('should update `[class]` and bindings in the provided directive if the input is matched',
@ -1122,7 +1122,7 @@ describe('acceptance integration tests', () => {
fixture.detectChanges();
expect(fixture.componentInstance.mockStyleDirective.stylesVal)
.toEqual({'width': '200px', 'height': '500px'});
.toEqual({width: '200px', height: '500px'});
});
onlyInIvy('Style binding merging works differently in Ivy')
@ -1177,8 +1177,8 @@ describe('acceptance integration tests', () => {
}
})
class DirWithSingleStylingBindings {
width: null|string = null;
height: null|string = null;
width: string|null|undefined = undefined;
height: string|null|undefined = undefined;
activateXYZClass: boolean = false;
}
@ -1214,8 +1214,8 @@ describe('acceptance integration tests', () => {
expect(target.classList.contains('def')).toBeTruthy();
expect(target.classList.contains('xyz')).toBeTruthy();
dirInstance.width = null;
dirInstance.height = null;
dirInstance.width = undefined;
dirInstance.height = undefined;
fixture.detectChanges();
expect(target.style.getPropertyValue('width')).toEqual('100px');
@ -1230,7 +1230,7 @@ describe('acceptance integration tests', () => {
() => {
@Directive({selector: '[Dir1WithStyle]', host: {'[style.width]': 'width'}})
class Dir1WithStyle {
width: null|string = null;
width: null|string|undefined = undefined;
}
@Directive({
@ -1238,7 +1238,7 @@ describe('acceptance integration tests', () => {
host: {'style': 'width: 111px', '[style.width]': 'width'}
})
class Dir2WithStyle {
width: null|string = null;
width: null|string|undefined = undefined;
}
@Component(
@ -1246,10 +1246,10 @@ describe('acceptance integration tests', () => {
class App {
@ViewChild(Dir1WithStyle) dir1Instance !: Dir1WithStyle;
@ViewChild(Dir2WithStyle) dir2Instance !: Dir2WithStyle;
width: string|null = null;
width: string|null|undefined = undefined;
}
TestBed.configureTestingModule({declarations: [App, Dir1WithStyle, Dir2WithStyle]});
TestBed.configureTestingModule({declarations: [App, Dir2WithStyle, Dir1WithStyle]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const {dir1Instance, dir2Instance} = fixture.componentInstance;
@ -1263,15 +1263,15 @@ describe('acceptance integration tests', () => {
fixture.detectChanges();
expect(target.style.getPropertyValue('width')).toEqual('999px');
fixture.componentInstance.width = null;
fixture.componentInstance.width = undefined;
fixture.detectChanges();
expect(target.style.getPropertyValue('width')).toEqual('222px');
dir1Instance.width = null;
dir1Instance.width = undefined;
fixture.detectChanges();
expect(target.style.getPropertyValue('width')).toEqual('333px');
dir2Instance.width = null;
dir2Instance.width = undefined;
fixture.detectChanges();
expect(target.style.getPropertyValue('width')).toEqual('111px');
@ -1316,7 +1316,7 @@ describe('acceptance integration tests', () => {
}
TestBed.configureTestingModule(
{declarations: [App, Dir1WithStyling, Dir2WithStyling]});
{declarations: [App, Dir2WithStyling, Dir1WithStyling]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const {dir1Instance, dir2Instance} = fixture.componentInstance;
@ -1325,7 +1325,7 @@ describe('acceptance integration tests', () => {
expect(target.style.getPropertyValue('width')).toEqual('111px');
const compInstance = fixture.componentInstance;
compInstance.stylesExp = {width: '999px', height: null};
compInstance.stylesExp = {width: '999px', height: undefined};
compInstance.classesExp = {one: true, two: false};
dir1Instance.stylesExp = {width: '222px'};
dir1Instance.classesExp = {two: true, three: false};

View File

@ -73,7 +73,6 @@ describe('renderer factory lifecycle', () => {
fixture.detectChanges();
expect(logs).toEqual(
['create', 'create', 'begin', 'some_component create', 'some_component update', 'end']);
logs = [];
fixture.detectChanges();
expect(logs).toEqual(['begin', 'some_component update', 'end']);

File diff suppressed because it is too large Load Diff

View File

@ -113,9 +113,6 @@
{
"name": "RENDERER_FACTORY"
},
{
"name": "RendererStyleFlags3"
},
{
"name": "SANITIZER"
},
@ -158,18 +155,12 @@
{
"name": "addHostBindingsToExpandoInstructions"
},
{
"name": "addItemToStylingMap"
},
{
"name": "addToViewTree"
},
{
"name": "allocLFrame"
},
{
"name": "allocStylingMapArray"
},
{
"name": "appendChild"
},
@ -195,7 +186,13 @@
"name": "classIndexOf"
},
{
"name": "concatString"
"name": "clearActiveHostElement"
},
{
"name": "computeStaticStyling"
},
{
"name": "concatStringsWithSpace"
},
{
"name": "createDirectivesInstances"
@ -284,9 +281,6 @@
{
"name": "findDirectiveDefMatches"
},
{
"name": "forceStylesAsString"
},
{
"name": "generateExpandoInstructionBlock"
},
@ -335,9 +329,6 @@
{
"name": "getFirstNativeNode"
},
{
"name": "getInitialStylingValue"
},
{
"name": "getInjectorIndex"
},
@ -353,12 +344,6 @@
{
"name": "getLViewParent"
},
{
"name": "getMapProp"
},
{
"name": "getMapValue"
},
{
"name": "getNameOnlyMarkerIndex"
},
@ -407,15 +392,9 @@
{
"name": "getSelectedIndex"
},
{
"name": "getStylingMapArray"
},
{
"name": "growHostVarsSpace"
},
{
"name": "hasActiveElementFlag"
},
{
"name": "hasClassInput"
},
@ -428,18 +407,12 @@
{
"name": "hasTagAndTypeMatch"
},
{
"name": "hyphenate"
},
{
"name": "includeViewProviders"
},
{
"name": "increaseElementDepthCount"
},
{
"name": "incrementActiveDirectiveId"
},
{
"name": "incrementInitPhaseFlags"
},
@ -509,12 +482,6 @@
{
"name": "isProceduralRenderer"
},
{
"name": "isStylingContext"
},
{
"name": "isStylingValueDefined"
},
{
"name": "leaveDI"
},
@ -557,12 +524,6 @@
{
"name": "noSideEffects"
},
{
"name": "objectToClassName"
},
{
"name": "setHostBindingsByExecutingExpandoInstructions"
},
{
"name": "refreshChildComponents"
},
@ -581,9 +542,6 @@
{
"name": "refreshView"
},
{
"name": "registerInitialStylingOnTNode"
},
{
"name": "registerPostOrderHooks"
},
@ -599,15 +557,9 @@
{
"name": "renderComponent"
},
{
"name": "renderInitialStyling"
},
{
"name": "renderStringify"
},
{
"name": "renderStylingMap"
},
{
"name": "renderView"
},
@ -623,9 +575,6 @@
{
"name": "saveResolvedLocalsInData"
},
{
"name": "selectClassBasedInputName"
},
{
"name": "selectIndexInternal"
},
@ -638,17 +587,14 @@
{
"name": "setBindingRoot"
},
{
"name": "setClass"
},
{
"name": "setClassName"
},
{
"name": "setCurrentQueryIndex"
},
{
"name": "setDirectiveStylingInput"
"name": "setDirectiveInputsWhichShadowsStyling"
},
{
"name": "setHostBindingsByExecutingExpandoInstructions"
},
{
"name": "setIncludeViewProviders"
@ -665,30 +611,18 @@
{
"name": "setIsNotParent"
},
{
"name": "setMapValue"
},
{
"name": "setPreviousOrParentTNode"
},
{
"name": "setSelectedIndex"
},
{
"name": "setStyle"
},
{
"name": "setStyleAttr"
},
{
"name": "setUpAttributes"
},
{
"name": "stringifyForError"
},
{
"name": "stylingMapToString"
},
{
"name": "syncViewWithBlueprint"
},
@ -698,14 +632,14 @@
{
"name": "unwrapRNode"
},
{
"name": "updateRawValueOnContext"
},
{
"name": "viewAttachedToChangeDetector"
},
{
"name": "writeStylingValueDirectly"
"name": "writeDirectClass"
},
{
"name": "writeDirectStyle"
},
{
"name": "ɵɵdefineComponent"

View File

@ -104,9 +104,6 @@
{
"name": "RENDERER_FACTORY"
},
{
"name": "RendererStyleFlags3"
},
{
"name": "SANITIZER"
},
@ -143,18 +140,12 @@
{
"name": "addHostBindingsToExpandoInstructions"
},
{
"name": "addItemToStylingMap"
},
{
"name": "addToViewTree"
},
{
"name": "allocLFrame"
},
{
"name": "allocStylingMapArray"
},
{
"name": "appendChild"
},
@ -174,7 +165,13 @@
"name": "callHooks"
},
{
"name": "concatString"
"name": "clearActiveHostElement"
},
{
"name": "computeStaticStyling"
},
{
"name": "concatStringsWithSpace"
},
{
"name": "createLFrame"
@ -242,9 +239,6 @@
{
"name": "extractPipeDef"
},
{
"name": "forceStylesAsString"
},
{
"name": "generateExpandoInstructionBlock"
},
@ -278,9 +272,6 @@
{
"name": "getFirstNativeNode"
},
{
"name": "getInitialStylingValue"
},
{
"name": "getInjectorIndex"
},
@ -296,12 +287,6 @@
{
"name": "getLViewParent"
},
{
"name": "getMapProp"
},
{
"name": "getMapValue"
},
{
"name": "getNativeAnchorNode"
},
@ -344,27 +329,15 @@
{
"name": "getSelectedIndex"
},
{
"name": "getStylingMapArray"
},
{
"name": "growHostVarsSpace"
},
{
"name": "hasActiveElementFlag"
},
{
"name": "hasParentInjector"
},
{
"name": "hyphenate"
},
{
"name": "includeViewProviders"
},
{
"name": "incrementActiveDirectiveId"
},
{
"name": "incrementInitPhaseFlags"
},
@ -404,12 +377,6 @@
{
"name": "isProceduralRenderer"
},
{
"name": "isStylingContext"
},
{
"name": "isStylingValueDefined"
},
{
"name": "leaveDI"
},
@ -443,12 +410,6 @@
{
"name": "noSideEffects"
},
{
"name": "objectToClassName"
},
{
"name": "setHostBindingsByExecutingExpandoInstructions"
},
{
"name": "refreshChildComponents"
},
@ -467,9 +428,6 @@
{
"name": "refreshView"
},
{
"name": "registerInitialStylingOnTNode"
},
{
"name": "registerPreOrderHooks"
},
@ -482,15 +440,9 @@
{
"name": "renderComponent"
},
{
"name": "renderInitialStyling"
},
{
"name": "renderStringify"
},
{
"name": "renderStylingMap"
},
{
"name": "renderView"
},
@ -509,59 +461,44 @@
{
"name": "setBindingRoot"
},
{
"name": "setClass"
},
{
"name": "setClassName"
},
{
"name": "setCurrentQueryIndex"
},
{
"name": "setHostBindingsByExecutingExpandoInstructions"
},
{
"name": "setIncludeViewProviders"
},
{
"name": "setInjectImplementation"
},
{
"name": "setMapValue"
},
{
"name": "setPreviousOrParentTNode"
},
{
"name": "setSelectedIndex"
},
{
"name": "setStyle"
},
{
"name": "setStyleAttr"
},
{
"name": "setUpAttributes"
},
{
"name": "stringifyForError"
},
{
"name": "stylingMapToString"
},
{
"name": "syncViewWithBlueprint"
},
{
"name": "unwrapRNode"
},
{
"name": "updateRawValueOnContext"
},
{
"name": "viewAttachedToChangeDetector"
},
{
"name": "writeStylingValueDirectly"
"name": "writeDirectClass"
},
{
"name": "writeDirectStyle"
},
{
"name": "ɵɵdefineComponent"

View File

@ -2,9 +2,6 @@
{
"name": "ACTIVE_INDEX"
},
{
"name": "BIT_MASK_START_VALUE"
},
{
"name": "BLOOM_MASK"
},
@ -38,18 +35,6 @@
{
"name": "DECLARATION_VIEW"
},
{
"name": "DEFAULT_BINDING_INDEX"
},
{
"name": "DEFAULT_BINDING_VALUE"
},
{
"name": "DEFAULT_GUARD_MASK_VALUE"
},
{
"name": "DEFAULT_TOTAL_SOURCES"
},
{
"name": "DOCUMENT"
},
@ -90,7 +75,7 @@
"name": "HOST"
},
{
"name": "INDEX_START_VALUE"
"name": "IGNORE_DUE_TO_INPUT_SHADOW"
},
{
"name": "INJECTOR"
@ -107,12 +92,6 @@
{
"name": "IterableDiffers"
},
{
"name": "MAP_BASED_ENTRY_PROP_NAME"
},
{
"name": "MAP_DIRTY_VALUE"
},
{
"name": "MONKEY_PATCH_KEY_NAME"
},
@ -206,15 +185,9 @@
{
"name": "RecordViewTuple"
},
{
"name": "RendererStyleFlags3"
},
{
"name": "SANITIZER"
},
{
"name": "STYLING_INDEX_FOR_MAP_BINDING"
},
{
"name": "SWITCH_ELEMENT_REF_FACTORY"
},
@ -230,9 +203,6 @@
{
"name": "SkipSelf"
},
{
"name": "TEMPLATE_DIRECTIVE_INDEX"
},
{
"name": "TNODE"
},
@ -317,9 +287,6 @@
{
"name": "__window"
},
{
"name": "_activeStylingMapApplyFn"
},
{
"name": "_currentInjector"
},
@ -332,27 +299,15 @@
{
"name": "_renderCompCount"
},
{
"name": "_state"
},
{
"name": "_symbolIterator"
},
{
"name": "addBindingIntoContext"
},
{
"name": "addComponentLogic"
},
{
"name": "addHostBindingsToExpandoInstructions"
},
{
"name": "addItemToStylingMap"
},
{
"name": "addNewSourceColumn"
},
{
"name": "addRemoveViewFromContainer"
},
@ -365,21 +320,12 @@
{
"name": "allocLFrame"
},
{
"name": "allocStylingMapArray"
},
{
"name": "allocTStylingContext"
},
{
"name": "allocateNewContextEntry"
},
{
"name": "allowDirectStyling"
},
{
"name": "appendChild"
},
{
"name": "appendStyling"
},
{
"name": "applyContainer"
},
@ -389,15 +335,6 @@
{
"name": "applyProjectionRecursive"
},
{
"name": "applyStylingValue"
},
{
"name": "applyStylingValueDirectly"
},
{
"name": "applyStylingViaContext"
},
{
"name": "applyToElementOrContainer"
},
@ -437,17 +374,50 @@
{
"name": "checkNoChangesInternal"
},
{
"name": "checkStylingProperty"
},
{
"name": "classIndexOf"
},
{
"name": "cleanUpView"
},
{
"name": "clearActiveHostElement"
},
{
"name": "collectNativeNodes"
},
{
"name": "concatString"
"name": "computeClassChanges"
},
{
"name": "computeStaticStyling"
},
{
"name": "computeStyleChanges"
},
{
"name": "concatStringsWithSpace"
},
{
"name": "consumeClassToken"
},
{
"name": "consumeQuotedText"
},
{
"name": "consumeSeparator"
},
{
"name": "consumeStyleKey"
},
{
"name": "consumeStyleValue"
},
{
"name": "consumeWhitespace"
},
{
"name": "createContainerRef"
@ -575,9 +545,6 @@
{
"name": "extractPipeDef"
},
{
"name": "findAndApplyMapValue"
},
{
"name": "findAttrIndexInNode"
},
@ -591,10 +558,10 @@
"name": "findViaComponent"
},
{
"name": "flushStyling"
"name": "flushStyleBinding"
},
{
"name": "forceStylesAsString"
"name": "flushStylingOnElementExit"
},
{
"name": "forwardRef"
@ -608,15 +575,9 @@
{
"name": "generatePropertyAliases"
},
{
"name": "getActiveDirectiveId"
},
{
"name": "getBeforeNodeForView"
},
{
"name": "getBindingValue"
},
{
"name": "getBindingsEnabled"
},
@ -624,7 +585,7 @@
"name": "getCheckNoChangesMode"
},
{
"name": "getClassesContext"
"name": "getClassBindingChanged"
},
{
"name": "getCleanup"
@ -647,9 +608,6 @@
{
"name": "getContainerRenderParent"
},
{
"name": "getContext"
},
{
"name": "getContextLView"
},
@ -659,9 +617,6 @@
{
"name": "getDebugContext"
},
{
"name": "getDefaultValue"
},
{
"name": "getDirectiveDef"
},
@ -680,12 +635,6 @@
{
"name": "getFirstNativeNode"
},
{
"name": "getGuardMask"
},
{
"name": "getInitialStylingValue"
},
{
"name": "getInjectableDef"
},
@ -705,10 +654,10 @@
"name": "getLViewParent"
},
{
"name": "getMapProp"
"name": "getLastParsedKey"
},
{
"name": "getMapValue"
"name": "getLastParsedValue"
},
{
"name": "getNameOnlyMarkerIndex"
@ -773,35 +722,14 @@
{
"name": "getPreviousOrParentTNode"
},
{
"name": "getProp"
},
{
"name": "getPropConfig"
},
{
"name": "getPropValuesStartPosition"
},
{
"name": "getRenderParent"
},
{
"name": "getRenderer"
},
{
"name": "getSelectedIndex"
},
{
"name": "getStylesContext"
},
{
"name": "getStylingMapArray"
},
{
"name": "getStylingMapsSyncFn"
},
{
"name": "getStylingState"
"name": "getStyleBindingChanged"
},
{
"name": "getSymbolIterator"
@ -810,10 +738,19 @@
"name": "getTNode"
},
{
"name": "getTViewCleanup"
"name": "getTStylingRangeNext"
},
{
"name": "getTotalSources"
"name": "getTStylingRangePrev"
},
{
"name": "getTStylingRangePrevDuplicate"
},
{
"name": "getTStylingRangeTail"
},
{
"name": "getTViewCleanup"
},
{
"name": "getTypeName"
@ -821,42 +758,27 @@
{
"name": "getTypeNameForDebugging"
},
{
"name": "getValue"
},
{
"name": "getValuesCount"
},
{
"name": "growHostVarsSpace"
},
{
"name": "handleError"
},
{
"name": "hasActiveElementFlag"
},
{
"name": "hasClassInput"
},
{
"name": "hasConfig"
},
{
"name": "hasParentInjector"
},
{
"name": "hasStyleInput"
},
{
"name": "hasStylingInputShadow"
},
{
"name": "hasTagAndTypeMatch"
},
{
"name": "hasValueChanged"
},
{
"name": "hyphenate"
},
{
"name": "includeViewProviders"
},
@ -864,7 +786,7 @@
"name": "increaseElementDepthCount"
},
{
"name": "incrementActiveDirectiveId"
"name": "incrementBindingIndex"
},
{
"name": "incrementInitPhaseFlags"
@ -893,6 +815,9 @@
{
"name": "insertBloom"
},
{
"name": "insertTStylingBinding"
},
{
"name": "insertView"
},
@ -917,6 +842,9 @@
{
"name": "invokeHostBindingsInCreationMode"
},
{
"name": "isActiveHostElement"
},
{
"name": "isAnimationProp"
},
@ -948,10 +876,7 @@
"name": "isForwardRef"
},
{
"name": "isHostStyling"
},
{
"name": "isHostStylingActive"
"name": "isInHostBindings"
},
{
"name": "isJsObject"
@ -983,15 +908,6 @@
{
"name": "isRootView"
},
{
"name": "isSanitizationRequired"
},
{
"name": "isStylingContext"
},
{
"name": "isStylingValueDefined"
},
{
"name": "iterateListLike"
},
@ -1037,6 +953,12 @@
{
"name": "markDirtyIfOnPush"
},
{
"name": "markDuplicates"
},
{
"name": "markStylingBindingDirty"
},
{
"name": "markViewDirty"
},
@ -1083,19 +1005,28 @@
"name": "noSideEffects"
},
{
"name": "normalizeBitMaskValue"
"name": "parseClassName"
},
{
"name": "objectToClassName"
"name": "parseClassNameNext"
},
{
"name": "patchConfig"
"name": "parseKeyValue"
},
{
"name": "patchHostStylingFlag"
"name": "parseStyle"
},
{
"name": "setHostBindingsByExecutingExpandoInstructions"
"name": "parseStyleNext"
},
{
"name": "parserState"
},
{
"name": "processClassToken"
},
{
"name": "processStyleKeyValue"
},
{
"name": "readPatchedData"
@ -1103,6 +1034,12 @@
{
"name": "readPatchedLView"
},
{
"name": "reconcileClassNames"
},
{
"name": "reconcileStyleNames"
},
{
"name": "refreshChildComponents"
},
@ -1121,12 +1058,6 @@
{
"name": "refreshView"
},
{
"name": "registerBinding"
},
{
"name": "registerInitialStylingOnTNode"
},
{
"name": "registerPostOrderHooks"
},
@ -1139,6 +1070,9 @@
{
"name": "removeListeners"
},
{
"name": "removeStyle"
},
{
"name": "removeView"
},
@ -1157,30 +1091,18 @@
{
"name": "renderDetachView"
},
{
"name": "renderHostBindingsAsStale"
},
{
"name": "renderInitialStyling"
},
{
"name": "renderStringify"
},
{
"name": "renderStylingMap"
},
{
"name": "renderView"
},
{
"name": "resetCurrentStyleSanitizer"
"name": "resetParserState"
},
{
"name": "resetPreOrderHookFlags"
},
{
"name": "resetStylingState"
},
{
"name": "resolveDirectives"
},
@ -1199,15 +1121,9 @@
{
"name": "searchTokensOnInjector"
},
{
"name": "selectClassBasedInputName"
},
{
"name": "selectIndexInternal"
},
{
"name": "setActiveElementFlag"
},
{
"name": "setActiveHostElement"
},
@ -1220,29 +1136,17 @@
{
"name": "setCheckNoChangesMode"
},
{
"name": "setClass"
},
{
"name": "setClassName"
},
{
"name": "setCurrentQueryIndex"
},
{
"name": "setCurrentStyleSanitizer"
},
{
"name": "setDefaultValue"
},
{
"name": "setDirectiveStylingInput"
"name": "setDirectiveInputsWhichShadowsStyling"
},
{
"name": "setElementExitFn"
},
{
"name": "setGuardMask"
"name": "setHostBindingsByExecutingExpandoInstructions"
},
{
"name": "setIncludeViewProviders"
@ -1262,12 +1166,6 @@
{
"name": "setLContainerActiveIndex"
},
{
"name": "setMapAsDirty"
},
{
"name": "setMapValue"
},
{
"name": "setPreviousOrParentTNode"
},
@ -1275,19 +1173,25 @@
"name": "setSelectedIndex"
},
{
"name": "setStyle"
"name": "setTStylingRangeNext"
},
{
"name": "setStyleAttr"
"name": "setTStylingRangeNextDuplicate"
},
{
"name": "setTStylingRangePrev"
},
{
"name": "setTStylingRangePrevDuplicate"
},
{
"name": "setUpAttributes"
},
{
"name": "setValue"
"name": "shouldSearchParent"
},
{
"name": "shouldSearchParent"
"name": "splitClassList"
},
{
"name": "storeCleanupFn"
@ -1299,16 +1203,10 @@
"name": "stringifyForError"
},
{
"name": "stylingApply"
"name": "styleKeyValue"
},
{
"name": "stylingMapToString"
},
{
"name": "stylingProp"
},
{
"name": "syncContextInitialStyling"
"name": "stylingPropertyFirstUpdatePass"
},
{
"name": "syncViewWithBlueprint"
@ -1325,6 +1223,12 @@
{
"name": "tickRootContext"
},
{
"name": "toTStylingRange"
},
{
"name": "toggleClass"
},
{
"name": "trackByIdentity"
},
@ -1337,21 +1241,6 @@
{
"name": "unwrapSafeValue"
},
{
"name": "updateBindingData"
},
{
"name": "updateClassViaContext"
},
{
"name": "updateInitialStylingOnContext"
},
{
"name": "updateRawValueOnContext"
},
{
"name": "updateStyleViaContext"
},
{
"name": "viewAttachedToChangeDetector"
},
@ -1365,7 +1254,16 @@
"name": "wrapListener"
},
{
"name": "writeStylingValueDirectly"
"name": "writeAndReconcileClass"
},
{
"name": "writeAndReconcileStyle"
},
{
"name": "writeDirectClass"
},
{
"name": "writeDirectStyle"
},
{
"name": "ɵɵadvance"

View File

@ -8,10 +8,10 @@
import {Injector, NgModuleRef, ViewEncapsulation} from '../../src/core';
import {ComponentFactory} from '../../src/linker/component_factory';
import {RendererFactory2} from '../../src/render/api';
import {RendererFactory2, RendererType2} from '../../src/render/api';
import {injectComponentFactoryResolver} from '../../src/render3/component_ref';
import {ɵɵdefineComponent} from '../../src/render3/index';
import {domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {AttributeMarker, ɵɵdefineComponent} from '../../src/render3/index';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {Sanitizer} from '../../src/sanitization/sanitizer';
describe('ComponentFactory', () => {
@ -97,6 +97,7 @@ describe('ComponentFactory', () => {
decls: 0,
vars: 0,
template: () => undefined,
hostAttrs: [AttributeMarker.Classes, 'HOST_COMPONENT']
});
}
@ -291,5 +292,24 @@ describe('ComponentFactory', () => {
expect(mSanitizerFactorySpy).toHaveBeenCalled();
});
});
it('should ensure that rendererFactory is called after initial styling is set', () => {
const myRendererFactory: RendererFactory3 = {
createRenderer: function(hostElement: RElement|null, rendererType: RendererType2|null):
Renderer3 {
if (hostElement) {
hostElement.classList.add('HOST_RENDERER');
}
return document;
}
};
const injector = Injector.create([
{provide: RendererFactory2, useValue: myRendererFactory},
]);
const hostNode = document.createElement('div');
const componentRef = cf.create(injector, undefined, hostNode);
expect(hostNode.className).toEqual('HOST_COMPONENT HOST_RENDERER');
});
});
});

View File

@ -0,0 +1,147 @@
/**
* @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 {TNodeDebug} from '@angular/core/src/render3/instructions/lview_debug';
import {createTNode, createTView} from '@angular/core/src/render3/instructions/shared';
import {TNodeType} from '@angular/core/src/render3/interfaces/node';
import {LView, TView, TViewType} from '@angular/core/src/render3/interfaces/view';
import {enterView, leaveView} from '@angular/core/src/render3/state';
import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list';
describe('lView_debug', () => {
const mockFirstUpdatePassLView: LView = [null, {firstUpdatePass: true}] as any;
beforeEach(() => enterView(mockFirstUpdatePassLView, null));
afterEach(() => leaveView());
describe('TNode', () => {
let tNode !: TNodeDebug;
let tView !: TView;
beforeEach(() => {
tView = createTView(TViewType.Component, 0, null, 0, 0, null, null, null, null, null);
tNode = createTNode(tView, null !, TNodeType.Element, 0, '', null) as TNodeDebug;
});
afterEach(() => tNode = tView = null !);
describe('styling', () => {
it('should decode no styling', () => {
expect(tNode.styleBindings_).toEqual([null]);
expect(tNode.classBindings_).toEqual([null]);
});
it('should decode static styling', () => {
tNode.styles = 'color: blue';
tNode.classes = 'STATIC';
expect(tNode.styleBindings_).toEqual(['color: blue']);
expect(tNode.classBindings_).toEqual(['STATIC']);
});
it('should decode no-template property binding', () => {
tNode.classes = 'STATIC';
insertTStylingBinding(tView.data, tNode, 'CLASS', 2, true, true);
insertTStylingBinding(tView.data, tNode, 'color', 4, true, false);
expect(tNode.styleBindings_).toEqual([
null, {
index: 4,
key: 'color',
isTemplate: false,
prevDuplicate: false,
nextDuplicate: false,
prevIndex: 0,
nextIndex: 0,
}
]);
expect(tNode.classBindings_).toEqual([
'STATIC', {
index: 2,
key: 'CLASS',
isTemplate: false,
prevDuplicate: false,
nextDuplicate: false,
prevIndex: 0,
nextIndex: 0,
}
]);
});
it('should decode template and directive property binding', () => {
tNode.classes = 'STATIC';
insertTStylingBinding(tView.data, tNode, 'CLASS', 2, false, true);
insertTStylingBinding(tView.data, tNode, 'color', 4, false, false);
expect(tNode.styleBindings_).toEqual([
null, {
index: 4,
key: 'color',
isTemplate: true,
prevDuplicate: false,
nextDuplicate: false,
prevIndex: 0,
nextIndex: 0,
}
]);
expect(tNode.classBindings_).toEqual([
'STATIC', {
index: 2,
key: 'CLASS',
isTemplate: true,
prevDuplicate: false,
nextDuplicate: false,
prevIndex: 0,
nextIndex: 0,
}
]);
insertTStylingBinding(tView.data, tNode, STYLE_MAP_STYLING_KEY, 6, true, true);
insertTStylingBinding(tView.data, tNode, CLASS_MAP_STYLING_KEY, 8, true, false);
expect(tNode.styleBindings_).toEqual([
null, {
index: 8,
key: CLASS_MAP_STYLING_KEY,
isTemplate: false,
prevDuplicate: false,
nextDuplicate: true,
prevIndex: 0,
nextIndex: 4,
},
{
index: 4,
key: 'color',
isTemplate: true,
prevDuplicate: true,
nextDuplicate: false,
prevIndex: 8,
nextIndex: 0,
}
]);
expect(tNode.classBindings_).toEqual([
'STATIC', {
index: 6,
key: STYLE_MAP_STYLING_KEY,
isTemplate: false,
prevDuplicate: true,
nextDuplicate: true,
prevIndex: 0,
nextIndex: 2,
},
{
index: 2,
key: 'CLASS',
isTemplate: true,
prevDuplicate: true,
nextDuplicate: false,
prevIndex: 6,
nextIndex: 0,
}
]);
});
});
});
});

View File

@ -7,11 +7,12 @@
*/
import {NgForOfContext} from '@angular/common';
import {getSortedClassName} from '@angular/core/testing/src/styling';
import {ɵɵdefineComponent} from '../../src/render3/definition';
import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
import {AttributeMarker} from '../../src/render3/interfaces/node';
import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, unwrapSafeValue} from '../../src/sanitization/bypass';
import {SafeValue, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, unwrapSafeValue} from '../../src/sanitization/bypass';
import {ɵɵdefaultStyleSanitizer, ɵɵsanitizeHtml, ɵɵsanitizeResourceUrl, ɵɵsanitizeScript, ɵɵsanitizeStyle, ɵɵsanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer} from '../../src/sanitization/sanitizer';
import {SecurityContext} from '../../src/sanitization/security';
@ -137,18 +138,20 @@ describe('instructions', () => {
describe('styleProp', () => {
it('should automatically sanitize unless a bypass operation is applied', () => {
const t = new TemplateFixture(() => { return createDiv(); }, () => {}, 1);
t.update(() => {
ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer);
ɵɵstyleProp('background-image', 'url("http://server")');
});
let backgroundImage: string|SafeValue = 'url("http://server")';
const t = new TemplateFixture(
() => { return createDiv(); },
() => {
ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer);
ɵɵstyleProp('background-image', backgroundImage);
},
2, 2);
// nothing is set because sanitizer suppresses it.
expect(t.html).toEqual('<div></div>');
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('');
t.update(() => {
ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer);
ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('url("http://server2")'));
});
backgroundImage = bypassSanitizationTrustStyle('url("http://server2")');
t.update();
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('url("http://server2")');
});
@ -160,9 +163,10 @@ describe('instructions', () => {
function createDivWithStyle() { ɵɵelement(0, 'div', 0); }
it('should add style', () => {
const fixture = new TemplateFixture(
createDivWithStyle, () => {}, 1, 0, null, null, null, undefined, attrs);
fixture.update(() => { ɵɵstyleMap({'background-color': 'red'}); });
const fixture = new TemplateFixture(createDivWithStyle, () => {
ɵɵstyleMap({'background-color': 'red'});
}, 1, 2, null, null, null, undefined, attrs);
fixture.update();
expect(fixture.html).toEqual('<div style="background-color: red; height: 10px;"></div>');
});
@ -184,7 +188,7 @@ describe('instructions', () => {
'width': 'width'
});
},
1, 0, null, null, sanitizerInterceptor);
1, 2, null, null, sanitizerInterceptor);
const props = detectedValues.sort();
expect(props).toEqual([
@ -197,9 +201,10 @@ describe('instructions', () => {
function createDivWithStyling() { ɵɵelement(0, 'div'); }
it('should add class', () => {
const fixture =
new TemplateFixture(createDivWithStyling, () => { ɵɵclassMap('multiple classes'); }, 1);
expect(fixture.html).toEqual('<div class="classes multiple"></div>');
const fixture = new TemplateFixture(
createDivWithStyling, () => { ɵɵclassMap('multiple classes'); }, 1, 2);
const div = fixture.containerElement.querySelector('div.multiple') !;
expect(getSortedClassName(div)).toEqual('classes multiple');
});
});

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {classIndexOf, computeClassChanges, removeClass, splitClassList, toggleClass} from '../../../src/render3/styling/class_differ';
import {classIndexOf, computeClassChanges, splitClassList, toggleClass} from '../../../src/render3/styling/class_differ';
describe('class differ', () => {
describe('computeClassChanges', () => {
@ -81,25 +81,25 @@ describe('class differ', () => {
});
});
describe('removeClass', () => {
describe('toggleClass', () => {
it('should remove class name from a class-list string', () => {
expect(removeClass('', '')).toEqual('');
expect(removeClass('A', 'A')).toEqual('');
expect(removeClass('AB', 'AB')).toEqual('');
expect(removeClass('A B', 'A')).toEqual('B');
expect(removeClass('A B', 'A')).toEqual('B');
expect(toggleClass('', '', false)).toEqual('');
expect(toggleClass('A', 'A', false)).toEqual('');
expect(toggleClass('AB', 'AB', false)).toEqual('');
expect(toggleClass('A B', 'A', false)).toEqual('B');
expect(toggleClass('A B', 'A', false)).toEqual('B');
expect(toggleClass('A B', 'B', false)).toEqual('A');
expect(toggleClass(' B ', 'B', false)).toEqual('');
});
it('should not remove a sub-string', () => {
expect(removeClass('ABC', 'A')).toEqual('ABC');
expect(removeClass('ABC', 'B')).toEqual('ABC');
expect(removeClass('ABC', 'C')).toEqual('ABC');
expect(removeClass('ABC', 'AB')).toEqual('ABC');
expect(removeClass('ABC', 'BC')).toEqual('ABC');
expect(toggleClass('ABC', 'A', false)).toEqual('ABC');
expect(toggleClass('ABC', 'B', false)).toEqual('ABC');
expect(toggleClass('ABC', 'C', false)).toEqual('ABC');
expect(toggleClass('ABC', 'AB', false)).toEqual('ABC');
expect(toggleClass('ABC', 'BC', false)).toEqual('ABC');
});
});
describe('removeClass', () => {
it('should toggle a class', () => {
expect(toggleClass('', 'B', false)).toEqual('');
expect(toggleClass('', 'B', true)).toEqual('B');

View File

@ -8,6 +8,7 @@
import {Renderer3, domRendererFactory3} from '@angular/core/src/render3/interfaces/renderer';
import {writeAndReconcileClass, writeAndReconcileStyle} from '@angular/core/src/render3/styling/reconcile';
import {getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling';
describe('styling reconcile', () => {
[document, domRendererFactory3.createRenderer(null, null)].forEach((renderer: Renderer3) => {
@ -84,41 +85,3 @@ describe('styling reconcile', () => {
});
});
});
function getSortedClassName(element: HTMLElement): string {
const names: string[] = [];
const classList = element.classList || [];
for (let i = 0; i < classList.length; i++) {
const name = classList[i];
if (names.indexOf(name) === -1) {
names.push(name);
}
}
names.sort();
return names.join(' ');
}
function getSortedStyle(element: HTMLElement): string {
const names: string[] = [];
const style = element.style;
// reading `style.color` is a work around for a bug in Domino. The issue is that Domino has stale
// value for `style.length`. It seems that reading a property from the element causes the stale
// value to be updated. (As of Domino v 2.1.3)
style.color;
for (let i = 0; i < style.length; i++) {
const name = style.item(i);
if (names.indexOf(name) === -1) {
names.push(name);
}
}
names.sort();
let sorted = '';
names.forEach(key => {
const value = style.getPropertyValue(key);
if (value != null && value !== '') {
if (sorted !== '') sorted += ' ';
sorted += key + ': ' + value + ';';
}
});
return sorted;
}

View File

@ -12,7 +12,6 @@ import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDu
import {LView, TData} from '@angular/core/src/render3/interfaces/view';
import {enterView, leaveView} from '@angular/core/src/render3/state';
import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, appendStyling, flushStyleBinding, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list';
import {getStylingBindingHead} from '@angular/core/src/render3/styling/styling_debug';
import {newArray} from '@angular/core/src/util/array_utils';
describe('TNode styling linked list', () => {
@ -438,34 +437,47 @@ describe('TNode styling linked list', () => {
it('should write basic value', () => {
const fixture = new StylingFixture([['color']], false);
fixture.setBinding(0, 'red');
expect(fixture.flush(0)).toEqual('color: red');
expect(fixture.flush(0)).toEqual('color: red;');
});
it('should chain values and allow update mid list', () => {
const fixture = new StylingFixture([['color', {key: 'width', extra: 'px'}]], false);
fixture.setBinding(0, 'red');
fixture.setBinding(1, '100');
expect(fixture.flush(0)).toEqual('color: red; width: 100px');
expect(fixture.flush(0)).toEqual('color: red; width: 100px;');
fixture.setBinding(0, 'blue');
fixture.setBinding(1, '200');
expect(fixture.flush(1)).toEqual('color: red; width: 200px');
expect(fixture.flush(0)).toEqual('color: blue; width: 200px');
expect(fixture.flush(1)).toEqual('color: red; width: 200px;');
expect(fixture.flush(0)).toEqual('color: blue; width: 200px;');
});
it('should remove duplicates', () => {
const fixture = new StylingFixture([['color', 'color']], false);
fixture.setBinding(0, 'red');
fixture.setBinding(1, 'blue');
expect(fixture.flush(0)).toEqual('color: blue');
expect(fixture.flush(0)).toEqual('color: blue;');
});
it('should treat undefined values as previous value', () => {
const fixture = new StylingFixture([['color', 'color']], false);
fixture.setBinding(0, 'red');
fixture.setBinding(1, undefined);
expect(fixture.flush(0)).toEqual('color: red;');
});
it('should treat null value as removal', () => {
const fixture = new StylingFixture([['color']], false);
fixture.setBinding(0, null);
expect(fixture.flush(0)).toEqual('');
});
});
describe('appendStyling', () => {
it('should append simple style', () => {
expect(appendStyling('', 'color', 'red', null, false, false)).toEqual('color: red');
expect(appendStyling('', 'color', 'red', null, true, false)).toEqual('color: red');
expect(appendStyling('', 'color', 'red', null, false, false)).toEqual('color: red;');
expect(appendStyling('', 'color', 'red', null, true, false)).toEqual('color: red;');
expect(appendStyling('', 'color', 'red', null, false, true)).toEqual('color');
expect(appendStyling('', 'color', 'red', null, true, true)).toEqual('color');
expect(appendStyling('', 'color', true, null, true, true)).toEqual('color');
@ -476,25 +488,25 @@ describe('TNode styling linked list', () => {
it('should append simple style with suffix', () => {
expect(appendStyling('', {key: 'width', extra: 'px'}, 100, null, false, false))
.toEqual('width: 100px');
.toEqual('width: 100px;');
});
it('should append simple style with sanitizer', () => {
expect(
appendStyling('', {key: 'width', extra: (v: any) => `-${v}-`}, 100, null, false, false))
.toEqual('width: -100-');
.toEqual('width: -100-;');
});
it('should append class/style', () => {
expect(appendStyling('color: white', 'color', 'red', null, false, false))
.toEqual('color: white; color: red');
expect(appendStyling('color: white;', 'color', 'red', null, false, false))
.toEqual('color: white; color: red;');
expect(appendStyling('MY-CLASS', 'color', true, null, false, true)).toEqual('MY-CLASS color');
expect(appendStyling('MY-CLASS', 'color', false, null, true, true)).toEqual('MY-CLASS');
});
it('should remove existing', () => {
expect(appendStyling('color: white', 'color', 'blue', null, true, false))
.toEqual('color: blue');
expect(appendStyling('color: white;', 'color', 'blue', null, true, false))
.toEqual('color: blue;');
expect(appendStyling('A YES B', 'YES', false, null, true, true)).toEqual('A B');
});
@ -510,10 +522,10 @@ describe('TNode styling linked list', () => {
it('should support maps for styles', () => {
expect(appendStyling('', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false))
.toEqual('A: a; B: b');
.toEqual('A: a; B: b;');
expect(appendStyling(
'A:_; B:_; C:_', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false))
.toEqual('C:_; A: a; B: b');
'A:_; B:_; C:_;', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false))
.toEqual('C:_; A: a; B: b;');
});
it('should support strings for classes', () => {
@ -525,11 +537,11 @@ describe('TNode styling linked list', () => {
});
it('should support strings for styles', () => {
expect(appendStyling('A:a;B:b', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, false, false))
.toEqual('A:a;B:b; A : a ; B : b');
expect(
appendStyling('A:_; B:_; C:_', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, true, false))
.toEqual('C:_; A: a; B: b');
expect(appendStyling('A:a;B:b;', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, false, false))
.toEqual('A:a;B:b; A : a ; B : b;');
expect(appendStyling(
'A:_; B:_; C:_;', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, true, false))
.toEqual('C:_; A: a; B: b;');
});
it('should throw no arrays for styles', () => {
@ -560,7 +572,7 @@ describe('TNode styling linked list', () => {
'list-style: unsafe; ' +
'list-style-image: unsafe; ' +
'clip-path: unsafe; ' +
'width: url(javascript:evil())');
'width: url(javascript:evil());');
// verify string
expect(appendStyling(
'', STYLE_MAP_STYLING_KEY,
@ -571,7 +583,7 @@ describe('TNode styling linked list', () => {
'list-style: url(javascript:evil());' +
'list-style-image: url(javascript:evil());' +
'clip-path: url(javascript:evil());' +
'width: url(javascript:evil())' // should not sanitize
'width: url(javascript:evil());' // should not sanitize
,
null, true, false))
.toEqual(
@ -582,7 +594,7 @@ describe('TNode styling linked list', () => {
'list-style: unsafe; ' +
'list-style-image: unsafe; ' +
'clip-path: unsafe; ' +
'width: url(javascript:evil())');
'width: url(javascript:evil());');
});
});
});
@ -632,6 +644,24 @@ function expectPriorityOrder(tData: TData, tNode: TNode, isClassBinding: boolean
return expect(indexes);
}
/**
* Find the head of the styling binding linked list.
*/
export function getStylingBindingHead(tData: TData, tNode: TNode, isClassBinding: boolean): number {
let index = getTStylingRangePrev(isClassBinding ? tNode.classBindings : tNode.styleBindings);
while (true) {
const tStylingRange = tData[index + 1] as TStylingRange;
const prev = getTStylingRangePrev(tStylingRange);
if (prev === 0) {
// found head exit.
return index;
} else {
index = prev;
}
}
}
class StylingFixture {
tData: TData = [null, null];
lView: LView = [null, null !] as any;

View File

@ -7,8 +7,7 @@
*/
import {StyleChangesMap, parseKeyValue, removeStyle} from '@angular/core/src/render3/styling/style_differ';
import {consumeSeparatorWithWhitespace, consumeStyleValue} from '@angular/core/src/render3/styling/styling_parser';
import {CharCode} from '@angular/core/src/util/char_code';
import {getLastParsedValue, parseStyle} from '@angular/core/src/render3/styling/styling_parser';
import {sortedForEach} from './class_differ_spec';
describe('style differ', () => {
@ -31,6 +30,13 @@ describe('style differ', () => {
expectParseValue(': text1 text2 ;🛑').toBe('text1 text2');
});
it('should parse empty vale', () => {
expectParseValue(':').toBe('');
expectParseValue(': ').toBe('');
expectParseValue(': ;🛑').toBe('');
expectParseValue(':;🛑').toBe('');
});
it('should parse quoted values', () => {
expectParseValue(':""').toBe('""');
expectParseValue(':"\\\\"').toBe('"\\\\"');
@ -54,11 +60,16 @@ describe('style differ', () => {
});
describe('parseKeyValue', () => {
it('should parse empty value', () => {
it('should parse empty string', () => {
expectParseKeyValue('').toEqual([]);
expectParseKeyValue(' \n\t\r ').toEqual([]);
});
it('should parse empty value', () => {
expectParseKeyValue('key:').toEqual(['key', '', null]);
expectParseKeyValue('key: \n\t\r; ').toEqual(['key', '', null]);
});
it('should prase single style', () => {
expectParseKeyValue('width: 100px').toEqual(['width', '100px', null]);
expectParseKeyValue(' width : 100px ;').toEqual(['width', '100px', null]);
@ -79,27 +90,27 @@ describe('style differ', () => {
describe('removeStyle', () => {
it('should remove no style', () => {
expect(removeStyle('', 'foo')).toEqual('');
expect(removeStyle('abc: bar', 'a')).toEqual('abc: bar');
expect(removeStyle('abc: bar', 'b')).toEqual('abc: bar');
expect(removeStyle('abc: bar', 'c')).toEqual('abc: bar');
expect(removeStyle('abc: bar', 'bar')).toEqual('abc: bar');
expect(removeStyle('abc: bar;', 'a')).toEqual('abc: bar;');
expect(removeStyle('abc: bar;', 'b')).toEqual('abc: bar;');
expect(removeStyle('abc: bar;', 'c')).toEqual('abc: bar;');
expect(removeStyle('abc: bar;', 'bar')).toEqual('abc: bar;');
});
it('should remove all style', () => {
expect(removeStyle('foo: bar', 'foo')).toEqual('');
expect(removeStyle('foo: bar;', 'foo')).toEqual('');
expect(removeStyle('foo: bar; foo: bar;', 'foo')).toEqual('');
});
it('should remove some of the style', () => {
expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b');
expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b');
expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c', 'foo'))
.toEqual('a: a; b: b; c: c');
expect(removeStyle('a: a; foo: bar; b: b;', 'foo')).toEqual('a: a; b: b;');
expect(removeStyle('a: a; foo: bar; b: b;', 'foo')).toEqual('a: a; b: b;');
expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c;', 'foo'))
.toEqual('a: a; b: b; c: c;');
});
it('should remove trailing ;', () => {
expect(removeStyle('a: a; foo: bar', 'foo')).toEqual('a: a');
expect(removeStyle('a: a ; foo: bar ; ', 'foo')).toEqual('a: a');
expect(removeStyle('a: a; foo: bar;', 'foo')).toEqual('a: a;');
expect(removeStyle('a: a ; foo: bar ; ', 'foo')).toEqual('a: a ;');
});
});
});
@ -114,11 +125,9 @@ function expectParseValue(
text: string) {
let stopIndex = text.indexOf('🛑');
if (stopIndex < 0) stopIndex = text.length;
const valueStart = consumeSeparatorWithWhitespace(text, 0, text.length, CharCode.COLON);
const valueEnd = consumeStyleValue(text, valueStart, text.length);
const valueSep = consumeSeparatorWithWhitespace(text, valueEnd, text.length, CharCode.SEMI_COLON);
expect(valueSep).toBe(stopIndex);
return expect(text.substring(valueStart, valueEnd));
let i = parseStyle(text);
expect(i).toBe(stopIndex);
return expect(getLastParsedValue(text));
}
function expectParseKeyValue(text: string) {

View File

@ -0,0 +1,84 @@
/**
* @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
*/
/**
* Returns element classes in form of a stable (sorted) string.
*
* @param element HTML Element.
* @returns Returns element classes in form of a stable (sorted) string.
*/
export function getSortedClassName(element: Element): string {
const names: string[] = Object.keys(getElementClasses(element));
names.sort();
return names.join(' ');
}
/**
* Returns element classes in form of a map.
*
* @param element HTML Element.
* @returns Map of class values.
*/
export function getElementClasses(element: Element): {[key: string]: true} {
const classes: {[key: string]: true} = {};
if (element.nodeType === Node.ELEMENT_NODE) {
const classList = element.classList;
for (let i = 0; i < classList.length; i++) {
const key = classList[i];
classes[key] = true;
}
}
return classes;
}
/**
* Returns element styles in form of a stable (sorted) string.
*
* @param element HTML Element.
* @returns Returns element styles in form of a stable (sorted) string.
*/
export function getSortedStyle(element: Element): string {
const styles = getElementStyles(element);
const names: string[] = Object.keys(styles);
names.sort();
let sorted = '';
names.forEach(key => {
const value = styles[key];
if (value != null && value !== '') {
if (sorted !== '') sorted += ' ';
sorted += key + ': ' + value + ';';
}
});
return sorted;
}
/**
* Returns element styles in form of a map.
*
* @param element HTML Element.
* @returns Map of style values.
*/
export function getElementStyles(element: Element): {[key: string]: string} {
const styles: {[key: string]: string} = {};
if (element.nodeType === Node.ELEMENT_NODE) {
const style = (element as HTMLElement).style;
// reading `style.color` is a work around for a bug in Domino. The issue is that Domino has
// stale value for `style.length`. It seems that reading a property from the element causes the
// stale value to be updated. (As of Domino v 2.1.3)
style.color;
for (let i = 0; i < style.length; i++) {
const key = style.item(i);
const value = style.getPropertyValue(key);
if (value !== '') {
// Workaround for IE not clearing properties, instead it just sets them to blank value.
styles[key] = value;
}
}
}
return styles;
}

View File

@ -195,7 +195,10 @@ export declare class NgClass implements DoCheck {
[klass: string]: any;
});
constructor(_iterableDiffers: IterableDiffers, _keyValueDiffers: KeyValueDiffers, _ngEl: ElementRef, _renderer: Renderer2);
applyChanges(): void;
ngDoCheck(): void;
setClass(value: string): void;
setNgClass(value: any): void;
}
export declare class NgComponentOutlet implements OnChanges, OnDestroy {
@ -270,7 +273,9 @@ export declare class NgStyle implements DoCheck {
[klass: string]: any;
} | null);
constructor(_ngEl: ElementRef, _differs: KeyValueDiffers, _renderer: Renderer2);
applyChanges(): void;
ngDoCheck(): void;
setNgStyle(value: any): void;
}
export declare class NgSwitch {

View File

@ -703,8 +703,8 @@ export declare function ɵɵattributeInterpolate8(attrName: string, prefix: stri
export declare function ɵɵattributeInterpolateV(attrName: string, values: any[], sanitizer?: SanitizerFn, namespace?: string): typeof ɵɵattributeInterpolateV;
export declare function ɵɵclassMap(classes: {
[className: string]: any;
} | NO_CHANGE | string | null): void;
[className: string]: boolean | null | undefined;
} | Map<string, boolean | undefined | null> | Set<string> | string[] | string | null | undefined): void;
export declare function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void;
@ -724,7 +724,7 @@ export declare function ɵɵclassMapInterpolate8(prefix: string, v0: any, i0: st
export declare function ɵɵclassMapInterpolateV(values: any[]): void;
export declare function ɵɵclassProp(className: string, value: boolean | null): typeof ɵɵclassProp;
export declare function ɵɵclassProp(className: string, value: boolean | null | undefined): typeof ɵɵclassProp;
export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportAs extends string[], InputMap extends {
[key: string]: string;
@ -1035,9 +1035,9 @@ export declare function ɵɵstaticViewQuery<T>(predicate: Type<any> | string[],
export declare function ɵɵstyleMap(styles: {
[styleName: string]: any;
} | NO_CHANGE | null): void;
} | Map<string, string | number | null | undefined> | string | null | undefined): void;
export declare function ɵɵstyleProp(prop: string, value: string | number | SafeValue | null, suffix?: string | null): typeof ɵɵstyleProp;
export declare function ɵɵstyleProp(prop: string, value: string | number | SafeValue | null | undefined, suffix?: string | null): typeof ɵɵstyleProp;
export declare function ɵɵstylePropInterpolate1(prop: string, prefix: string, v0: any, suffix: string, valueSuffix?: string | null): typeof ɵɵstylePropInterpolate1;