diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 7c2ebdf8a1..4fc2b657cb 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -108,6 +108,9 @@ export function extractDirectiveMetadata( member => member.isStatic && member.kind === ClassMemberKind.Method && member.name === 'ngOnChanges') !== undefined; + // Detect if the component inherits from another class + const usesInheritance = clazz.heritageClauses !== undefined && + clazz.heritageClauses.some(hc => hc.token === ts.SyntaxKind.ExtendsKeyword); return { name: clazz.name !.text, deps: getConstructorDependencies(clazz, reflector, isCore), @@ -123,7 +126,7 @@ export function extractDirectiveMetadata( outputs: {...outputsFromMeta, ...outputsFromFields}, queries: [], selector, type: new WrappedNodeExpr(clazz.name !), - typeSourceSpan: null !, + typeSourceSpan: null !, usesInheritance, }; } diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 2d5475c6eb..78dd3ca0a7 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -138,6 +138,9 @@ export class Identifiers { static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE}; + static InheritDefinitionFeature: + o.ExternalReference = {name: 'ɵInheritDefinitionFeature', moduleName: CORE}; + static listener: o.ExternalReference = {name: 'ɵL', moduleName: CORE}; // Reserve slots for pure functions diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 495ccf8976..8ebcc44fb8 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -86,6 +86,11 @@ export interface R3DirectiveMetadata { * A mapping of output field names to the property names. */ outputs: {[field: string]: string}; + + /** + * Whether or not the component or directive inherits from another class + */ + usesInheritance: boolean; } /** diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 63a2462408..375c9e134b 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -64,10 +64,14 @@ function baseDirectiveFields( // e.g 'outputs: {a: 'a'}` definitionMap.set('outputs', conditionallyCreateMapObjectLiteral(meta.outputs)); - // e.g. `features: [NgOnChangesFeature(MyComponent)]` + // e.g. `features: [NgOnChangesFeature]` const features: o.Expression[] = []; + + if (meta.usesInheritance) { + features.push(o.importExpr(R3.InheritDefinitionFeature)); + } if (meta.lifecycle.usesOnChanges) { - features.push(o.importExpr(R3.NgOnChangesFeature, null, null).callFn([meta.type])); + features.push(o.importExpr(R3.NgOnChangesFeature)); } if (features.length) { definitionMap.set('features', o.literalArr(features)); @@ -255,6 +259,7 @@ function directiveMetadataFromGlobalMetadata( }, inputs: directive.inputs, outputs: directive.outputs, + usesInheritance: false, }; } diff --git a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts index ee6b53d123..a7d9befbe6 100644 --- a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts +++ b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts @@ -1212,7 +1212,7 @@ describe('compiler compliance', () => { selectors: [['lifecycle-comp']], factory: function LifecycleComp_Factory() { return new LifecycleComp(); }, inputs: {nameMin: 'name'}, - features: [$r3$.ɵNgOnChangesFeature(LifecycleComp)], + features: [$r3$.ɵNgOnChangesFeature], template: function LifecycleComp_Template(rf: IDENT, ctx: IDENT) {} });`; @@ -1328,7 +1328,7 @@ describe('compiler compliance', () => { factory: function ForOfDirective_Factory() { return new ForOfDirective($r3$.ɵinjectViewContainerRef(), $r3$.ɵinjectTemplateRef()); }, - features: [$r3$.ɵNgOnChangesFeature(NgForOf)], + features: [$r3$.ɵNgOnChangesFeature], inputs: {forOf: 'forOf'} }); `; @@ -1401,7 +1401,7 @@ describe('compiler compliance', () => { factory: function ForOfDirective_Factory() { return new ForOfDirective($r3$.ɵinjectViewContainerRef(), $r3$.ɵinjectTemplateRef()); }, - features: [$r3$.ɵNgOnChangesFeature(NgForOf)], + features: [$r3$.ɵNgOnChangesFeature], inputs: {forOf: 'forOf'} }); `; diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index f8ce00f91c..e671928716 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -61,14 +61,47 @@ export function defineComponent(componentDefinition: { /** * A map of input names. * - * The format is in: `{[actualPropertyName: string]:string}`. + * The format is in: `{[actualPropertyName: string]:(string|[string, string])}`. * - * Which the minifier may translate to: `{[minifiedPropertyName: string]:string}`. + * Given: + * ``` + * class MyComponent { + * @Input() + * publicInput1: string; * - * This allows the render to re-construct the minified and non-minified names + * @Input('publicInput2') + * declaredInput2: string; + * } + * ``` + * + * is described as: + * ``` + * { + * publicInput1: 'publicInput1', + * declaredInput2: ['declaredInput2', 'publicInput2'], + * } + * ``` + * + * Which the minifier may translate to: + * ``` + * { + * minifiedPublicInput1: 'publicInput1', + * minifiedDeclaredInput2: [ 'publicInput2', 'declaredInput2'], + * } + * ``` + * + * This allows the render to re-construct the minified, public, and declared names * of properties. + * + * NOTE: + * - Because declared and public name are usually same we only generate the array + * `['declared', 'public']` format when they differ. + * - The reason why this API and `outputs` API is not the same is that `NgOnChanges` has + * inconsistent behavior in that it uses declared names rather than minified or public. For + * this reason `NgOnChanges` will be deprecated and removed in future version and this + * API will be simplified to be consistent with `output`. */ - inputs?: {[P in keyof T]?: string}; + inputs?: {[P in keyof T]?: string | [string, string]}; /** * A map of output names. @@ -176,6 +209,7 @@ export function defineComponent(componentDefinition: { const type = componentDefinition.type; const pipeTypes = componentDefinition.pipes !; const directiveTypes = componentDefinition.directives !; + const declaredInputs: {[P in keyof T]: P} = {} as any; const def: ComponentDefInternal = { type: type, diPublic: null, @@ -183,7 +217,8 @@ export function defineComponent(componentDefinition: { template: componentDefinition.template || null !, hostBindings: componentDefinition.hostBindings || null, attributes: componentDefinition.attributes || null, - inputs: invertObject(componentDefinition.inputs), + inputs: invertObject(componentDefinition.inputs, declaredInputs), + declaredInputs: declaredInputs, outputs: invertObject(componentDefinition.outputs), rendererType: resolveRendererType2(componentDefinition.rendererType) || null, exportAs: componentDefinition.exportAs || null, @@ -204,6 +239,7 @@ export function defineComponent(componentDefinition: { null, selectors: componentDefinition.selectors, viewQuery: componentDefinition.viewQuery || null, + features: componentDefinition.features || null, }; const feature = componentDefinition.features; feature && feature.forEach((fn) => fn(def)); @@ -239,115 +275,72 @@ export function defineNgModule(def: {type: T} & Partial): void { - const inputs = definition.inputs; - const proto = definition.type.prototype; - for (let pubKey in inputs) { - const minKey = inputs[pubKey]; - const propertyName = inputPropertyNames && inputPropertyNames[minKey] || pubKey; - const privateMinKey = PRIVATE_PREFIX + minKey; - const originalProperty = Object.getOwnPropertyDescriptor(proto, minKey); - const getter = originalProperty && originalProperty.get; - const setter = originalProperty && originalProperty.set; - // create a getter and setter for property - Object.defineProperty(proto, minKey, { - get: getter || - (setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }), - set: function(this: OnChangesExpando, value: any) { - let simpleChanges = this[PRIVATE_PREFIX]; - if (!simpleChanges) { - // Place where we will store SimpleChanges if there is a change - Object.defineProperty( - this, PRIVATE_PREFIX, {value: simpleChanges = {}, writable: true}); - } - const isFirstChange = !this.hasOwnProperty(privateMinKey); - const currentChange: SimpleChange|undefined = simpleChanges[propertyName]; - if (currentChange) { - currentChange.currentValue = value; - } else { - simpleChanges[propertyName] = - new SimpleChange(this[privateMinKey], value, isFirstChange); - } - if (isFirstChange) { - // Create a place where the actual value will be stored and make it non-enumerable - Object.defineProperty(this, privateMinKey, {value, writable: true}); - } else { - this[privateMinKey] = value; - } - setter && setter.call(this, value); - } - }); - } - - // If an onInit hook is defined, it will need to wrap the ngOnChanges call - // so the call order is changes-init-check in creation mode. In subsequent - // change detection runs, only the check wrapper will be called. - if (definition.onInit != null) { - definition.onInit = onChangesWrapper(definition.onInit); - } - - definition.doCheck = onChangesWrapper(definition.doCheck); - }; - - function onChangesWrapper(delegateHook: (() => void) | null) { - return function(this: OnChangesExpando) { - let simpleChanges = this[PRIVATE_PREFIX]; - if (simpleChanges != null) { - this.ngOnChanges(simpleChanges); - this[PRIVATE_PREFIX] = null; - } - delegateHook && delegateHook.apply(this); - }; - } -} - - -export function PublicFeature(definition: DirectiveDefInternal) { - definition.diPublic = diPublic; -} - const EMPTY = {}; -/** Swaps the keys and values of an object. */ -function invertObject(obj: any): any { +/** + * Inverts an inputs or outputs lookup such that the keys, which were the + * minified keys, are part of the values, and the values are parsed so that + * the publicName of the property is the new key + * + * e.g. for + * + * ``` + * class Comp { + * @Input() + * propName1: string; + * + * @Input('publicName') + * propName2: number; + * } + * ``` + * + * will be serialized as + * + * ``` + * { + * a0: 'propName1', + * b1: ['publicName', 'propName2'], + * } + * ``` + * + * becomes + * + * ``` + * { + * 'propName1': 'a0', + * 'publicName': 'b1' + * } + * ``` + * + * Optionally the function can take `secondary` which will result in: + * + * ``` + * { + * 'propName1': 'a0', + * 'propName2': 'b1' + * } + * ``` + * + + */ +function invertObject(obj: any, secondary?: any): any { if (obj == null) return EMPTY; - const newObj: any = {}; - for (let minifiedKey in obj) { - newObj[obj[minifiedKey]] = minifiedKey; + const newLookup: any = {}; + for (const minifiedKey in obj) { + if (obj.hasOwnProperty(minifiedKey)) { + let publicName = obj[minifiedKey]; + let declaredName = publicName; + if (Array.isArray(publicName)) { + declaredName = publicName[1]; + publicName = publicName[0]; + } + newLookup[publicName] = minifiedKey; + if (secondary) { + (secondary[declaredName] = minifiedKey); + } + } } - return newObj; + return newLookup; } /** @@ -389,14 +382,47 @@ export const defineDirective = defineComponent as any as(directiveDefinition: /** * A map of input names. * - * The format is in: `{[actualPropertyName: string]:string}`. + * The format is in: `{[actualPropertyName: string]:(string|[string, string])}`. * - * Which the minifier may translate to: `{[minifiedPropertyName: string]:string}`. + * Given: + * ``` + * class MyComponent { + * @Input() + * publicInput1: string; * - * This allows the render to re-construct the minified and non-minified names + * @Input('publicInput2') + * declaredInput2: string; + * } + * ``` + * + * is described as: + * ``` + * { + * publicInput1: 'publicInput1', + * declaredInput2: ['declaredInput2', 'publicInput2'], + * } + * ``` + * + * Which the minifier may translate to: + * ``` + * { + * minifiedPublicInput1: 'publicInput1', + * minifiedDeclaredInput2: [ 'publicInput2', 'declaredInput2'], + * } + * ``` + * + * This allows the render to re-construct the minified, public, and declared names * of properties. + * + * NOTE: + * - Because declared and public name are usually same we only generate the array + * `['declared', 'public']` format when they differ. + * - The reason why this API and `outputs` API is not the same is that `NgOnChanges` has + * inconsistent behavior in that it uses declared names rather than minified or public. For + * this reason `NgOnChanges` will be deprecated and removed in future version and this + * API will be simplified to be consistent with `output`. */ - inputs?: {[P in keyof T]?: string}; + inputs?: {[P in keyof T]?: string | [string, string]}; /** * A map of output names. @@ -413,7 +439,7 @@ export const defineDirective = defineComponent as any as(directiveDefinition: /** * A list of optional features to apply. * - * See: {@link NgOnChangesFeature}, {@link PublicFeature} + * See: {@link NgOnChangesFeature}, {@link PublicFeature}, {@link InheritDefinitionFeature} */ features?: DirectiveDefFeature[]; diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts new file mode 100644 index 0000000000..8d4588459a --- /dev/null +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -0,0 +1,120 @@ +/** + * @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 {Type} from '../../type'; +import {ComponentDefInternal, ComponentType, DirectiveDefFeature, DirectiveDefInternal} from '../interfaces/definition'; + + +/** + * Sets properties on a target object from a source object, but only if + * the property doesn't already exist on the target object. + * @param target The target to set properties on + * @param source The source of the property keys and values to set + */ +function fillProperties(target: {[key: string]: string}, source: {[key: string]: string}) { + for (const key in source) { + if (source.hasOwnProperty(key) && !target.hasOwnProperty(key)) { + target[key] = source[key]; + } + } +} +/** + * Determines if a definition is a {@link ComponentDefInternal} or a {@link DirectiveDefInternal} + * @param definition The definition to examine + */ +function isComponentDef(definition: ComponentDefInternal| DirectiveDefInternal): + definition is ComponentDefInternal { + const def = definition as ComponentDefInternal; + return typeof def.template === 'function'; +} + +function getSuperType(type: Type): Type& + {ngComponentDef?: ComponentDefInternal, ngDirectiveDef?: DirectiveDefInternal} { + return Object.getPrototypeOf(type.prototype).constructor; +} + +/** + * Merges the definition from a super class to a sub class. + * @param definition The definition that is a SubClass of another directive of component + */ +export function InheritDefinitionFeature( + definition: DirectiveDefInternal| ComponentDefInternal): void { + let superType = getSuperType(definition.type); + let superDef: DirectiveDefInternal|ComponentDefInternal|undefined = undefined; + + while (superType && !superDef) { + if (isComponentDef(definition)) { + superDef = superType.ngComponentDef || superType.ngDirectiveDef; + } else { + if (superType.ngComponentDef) { + throw new Error('Directives cannot inherit Components'); + } + superDef = superType.ngDirectiveDef; + } + + if (superDef) { + // Merge inputs and outputs + fillProperties(definition.inputs, superDef.inputs); + fillProperties(definition.declaredInputs, superDef.declaredInputs); + fillProperties(definition.outputs, superDef.outputs); + + // Merge hostBindings + const prevHostBindings = definition.hostBindings; + const superHostBindings = superDef.hostBindings; + if (superHostBindings) { + if (prevHostBindings) { + definition.hostBindings = (directiveIndex: number, elementIndex: number) => { + superHostBindings(directiveIndex, elementIndex); + prevHostBindings(directiveIndex, elementIndex); + }; + } else { + definition.hostBindings = superHostBindings; + } + } + + // Inherit hooks + // Assume super class inheritance feature has already run. + definition.afterContentChecked = + definition.afterContentChecked || superDef.afterContentChecked; + definition.afterContentInit = definition.afterContentInit || superDef.afterContentInit; + definition.afterViewChecked = definition.afterViewChecked || superDef.afterViewChecked; + definition.afterViewInit = definition.afterViewInit || superDef.afterViewInit; + definition.doCheck = definition.doCheck || superDef.doCheck; + definition.onDestroy = definition.onDestroy || superDef.onDestroy; + definition.onInit = definition.onInit || superDef.onInit; + + // Run parent features + const features = superDef.features; + if (features) { + for (const feature of features) { + if (feature && feature !== InheritDefinitionFeature) { + (feature as DirectiveDefFeature)(definition); + } + } + } + } else { + // Even if we don't have a definition, check the type for the hooks and use those if need be + const superPrototype = superType.prototype; + + if (superPrototype) { + definition.afterContentChecked = + definition.afterContentChecked || superPrototype.afterContentChecked; + definition.afterContentInit = + definition.afterContentInit || superPrototype.afterContentInit; + definition.afterViewChecked = + definition.afterViewChecked || superPrototype.afterViewChecked; + definition.afterViewInit = definition.afterViewInit || superPrototype.afterViewInit; + definition.doCheck = definition.doCheck || superPrototype.doCheck; + definition.onDestroy = definition.onDestroy || superPrototype.onDestroy; + definition.onInit = definition.onInit || superPrototype.onInit; + } + } + + superType = Object.getPrototypeOf(superType); + } +} diff --git a/packages/core/src/render3/features/ng_onchanges_feature.ts b/packages/core/src/render3/features/ng_onchanges_feature.ts new file mode 100644 index 0000000000..09259a9eb5 --- /dev/null +++ b/packages/core/src/render3/features/ng_onchanges_feature.ts @@ -0,0 +1,116 @@ +/** + * @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 {SimpleChange} from '../../change_detection/change_detection_util'; +import {OnChanges, SimpleChanges} from '../../metadata/lifecycle_hooks'; +import {DirectiveDefInternal} from '../interfaces/definition'; + +const PRIVATE_PREFIX = '__ngOnChanges_'; + +type OnChangesExpando = OnChanges & { + __ngOnChanges_: SimpleChanges|null|undefined; + // tslint:disable-next-line:no-any Can hold any value + [key: string]: any; +}; + +/** + * The NgOnChangesFeature decorates a component with support for the ngOnChanges + * lifecycle hook, so it should be included in any component that implements + * that hook. + * + * If the component or directive uses inheritance, the NgOnChangesFeature MUST + * be included as a feature AFTER {@link InheritDefinitionFeature}, otherwise + * inherited properties will not be propagated to the ngOnChanges lifecycle + * hook. + * + * Example usage: + * + * ``` + * static ngComponentDef = defineComponent({ + * ... + * inputs: {name: 'publicName'}, + * features: [NgOnChangesFeature] + * }); + * ``` + */ +export function NgOnChangesFeature(definition: DirectiveDefInternal): void { + const declaredToMinifiedInputs = definition.declaredInputs; + const proto = definition.type.prototype; + for (const declaredName in declaredToMinifiedInputs) { + if (declaredToMinifiedInputs.hasOwnProperty(declaredName)) { + const minifiedKey = declaredToMinifiedInputs[declaredName]; + const privateMinKey = PRIVATE_PREFIX + minifiedKey; + + // Walk the prototype chain to see if we find a property descriptor + // That way we can honor setters and getters that were inherited. + let originalProperty: PropertyDescriptor|undefined = undefined; + let checkProto = proto; + while (!originalProperty && checkProto && + Object.getPrototypeOf(checkProto) !== Object.getPrototypeOf(Object.prototype)) { + originalProperty = Object.getOwnPropertyDescriptor(checkProto, minifiedKey); + checkProto = Object.getPrototypeOf(checkProto); + } + + const getter = originalProperty && originalProperty.get; + const setter = originalProperty && originalProperty.set; + + // create a getter and setter for property + Object.defineProperty(proto, minifiedKey, { + get: getter || + (setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }), + set(this: OnChangesExpando, value: T) { + let simpleChanges = this[PRIVATE_PREFIX]; + if (!simpleChanges) { + simpleChanges = {}; + // Place where we will store SimpleChanges if there is a change + Object.defineProperty(this, PRIVATE_PREFIX, {value: simpleChanges, writable: true}); + } + + const isFirstChange = !this.hasOwnProperty(privateMinKey); + const currentChange = simpleChanges[declaredName]; + + if (currentChange) { + currentChange.currentValue = value; + } else { + simpleChanges[declaredName] = + new SimpleChange(this[privateMinKey], value, isFirstChange); + } + + if (isFirstChange) { + // Create a place where the actual value will be stored and make it non-enumerable + Object.defineProperty(this, privateMinKey, {value, writable: true}); + } else { + this[privateMinKey] = value; + } + + if (setter) setter.call(this, value); + } + }); + } + } + + // If an onInit hook is defined, it will need to wrap the ngOnChanges call + // so the call order is changes-init-check in creation mode. In subsequent + // change detection runs, only the check wrapper will be called. + if (definition.onInit != null) { + definition.onInit = onChangesWrapper(definition.onInit); + } + + definition.doCheck = onChangesWrapper(definition.doCheck); +} + +function onChangesWrapper(delegateHook: (() => void) | null) { + return function(this: OnChangesExpando) { + const simpleChanges = this[PRIVATE_PREFIX]; + if (simpleChanges != null) { + this.ngOnChanges(simpleChanges); + this[PRIVATE_PREFIX] = null; + } + if (delegateHook) delegateHook.apply(this); + }; +} diff --git a/packages/core/src/render3/features/public_feature.ts b/packages/core/src/render3/features/public_feature.ts new file mode 100644 index 0000000000..6ed10af191 --- /dev/null +++ b/packages/core/src/render3/features/public_feature.ts @@ -0,0 +1,19 @@ +/** + * @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 {diPublic} from '../di'; +import {DirectiveDefInternal} from '../interfaces/definition'; + +/** + * This feature publishes the directive (or component) into the DI system, making it visible to + * others for injection. + * + * @param definition + */ +export function PublicFeature(definition: DirectiveDefInternal) { + definition.diPublic = diPublic; +} diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index b9a7ee98fc..963a4cd424 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -7,9 +7,13 @@ */ import {LifecycleHooksFeature, getHostElement, getRenderedText, renderComponent, whenRendered} from './component'; -import {NgOnChangesFeature, PublicFeature, defineComponent, defineDirective, defineNgModule, definePipe} from './definition'; +import {defineComponent, defineDirective, defineNgModule, definePipe} from './definition'; +import {InheritDefinitionFeature} from './features/inherit_definition_feature'; +import {NgOnChangesFeature} from './features/ng_onchanges_feature'; +import {PublicFeature} from './features/public_feature'; import {I18nExpInstruction, I18nInstruction, i18nExpMapping, i18nInterpolation, i18nInterpolationV} from './i18n'; import {ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDefFlags, DirectiveDefInternal, DirectiveType, PipeDef} from './interfaces/definition'; + export {ComponentFactory, ComponentFactoryResolver, ComponentRef} from './component_ref'; export {QUERY_READ_CONTAINER_REF, QUERY_READ_ELEMENT_REF, QUERY_READ_FROM_NODE, QUERY_READ_TEMPLATE_REF, directiveInject, injectAttribute, injectChangeDetectorRef, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di'; export {RenderFlags} from './interfaces/definition'; @@ -133,6 +137,7 @@ export { DirectiveDefFlags, DirectiveType, NgOnChangesFeature, + InheritDefinitionFeature, PublicFeature, PipeDef, LifecycleHooksFeature, diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 69d685919b..cfcb79e308 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -96,6 +96,12 @@ export interface DirectiveDef { */ readonly inputs: {[P in keyof T]: P}; + /** + * @deprecated This is only here because `NgOnChanges` incorrectly uses declared name instead of + * public or minified name. + */ + readonly declaredInputs: {[P in keyof T]: P}; + /** * A dictionary mapping the outputs' minified property names to their public API names, which * are their aliases if any, or their original unminified property names @@ -135,6 +141,11 @@ export interface DirectiveDef { afterViewInit: (() => void)|null; afterViewChecked: (() => void)|null; onDestroy: (() => void)|null; + + /** + * The features applied to this directive + */ + features: DirectiveDefFeature[]|null; } /** diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index e982be0a1c..70ba78624d 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -121,6 +121,10 @@ export function compileDirective(type: Type, directive: Directive): void { } +export function extendsDirectlyFromObject(type: Type): boolean { + return Object.getPrototypeOf(type.prototype) === Object.prototype; +} + /** * Extract the `R3DirectiveMetadata` for a particular directive (either a `Directive` or a * `Component`). @@ -136,14 +140,16 @@ function directiveMetadata(type: Type, metadata: Directive): R3DirectiveMet const inputsFromType: StringMap = {}; const outputsFromType: StringMap = {}; - for (let field in propMetadata) { - propMetadata[field].forEach(ann => { - if (isInput(ann)) { - inputsFromType[field] = ann.bindingPropertyName || field; - } else if (isOutput(ann)) { - outputsFromType[field] = ann.bindingPropertyName || field; - } - }); + for (const field in propMetadata) { + if (propMetadata.hasOwnProperty(field)) { + propMetadata[field].forEach(ann => { + if (isInput(ann)) { + inputsFromType[field] = ann.bindingPropertyName || field; + } else if (isOutput(ann)) { + outputsFromType[field] = ann.bindingPropertyName || field; + } + }); + } } return { @@ -158,6 +164,7 @@ function directiveMetadata(type: Type, metadata: Directive): R3DirectiveMet usesOnChanges: type.prototype.ngOnChanges !== undefined, }, typeSourceSpan: null !, + usesInheritance: !extendsDirectlyFromObject(type), }; } @@ -174,14 +181,16 @@ function extractHostBindings(metadata: Directive, propMetadata: {[key: string]: } // Next, loop over the properties of the object, looking for @HostBinding and @HostListener. - for (let field in propMetadata) { - propMetadata[field].forEach(ann => { - if (isHostBinding(ann)) { - properties[ann.hostPropertyName || field] = field; - } else if (isHostListener(ann)) { - listeners[ann.eventName || field] = `${field}(${(ann.args || []).join(',')})`; - } - }); + for (const field in propMetadata) { + if (propMetadata.hasOwnProperty(field)) { + propMetadata[field].forEach(ann => { + if (isHostBinding(ann)) { + properties[ann.hostPropertyName || field] = field; + } else if (isHostListener(ann)) { + listeners[ann.eventName || field] = `${field}(${(ann.args || []).join(',')})`; + } + }); + } } return {attributes, listeners, properties}; diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index b82abaa389..d1abad4ff3 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -31,6 +31,7 @@ export const angularCoreEnv: {[name: string]: Function} = { 'ɵinjectTemplateRef': r3.injectTemplateRef, 'ɵinjectViewContainerRef': r3.injectViewContainerRef, 'ɵNgOnChangesFeature': r3.NgOnChangesFeature, + 'ɵInheritDefinitionFeature': r3.InheritDefinitionFeature, 'ɵa': r3.a, 'ɵb': r3.b, 'ɵC': r3.C, diff --git a/packages/core/test/bundling/todo/index.ts b/packages/core/test/bundling/todo/index.ts index 60d65f0242..7584c645df 100644 --- a/packages/core/test/bundling/todo/index.ts +++ b/packages/core/test/bundling/todo/index.ts @@ -8,8 +8,8 @@ import '@angular/core/test/bundling/util/src/reflect_metadata'; -import {CommonModule, NgForOf, NgIf} from '@angular/common'; -import {Component, Injectable, IterableDiffers, NgModule, defineInjector, ɵNgOnChangesFeature as NgOnChangesFeature, ɵdefineDirective as defineDirective, ɵdirectiveInject as directiveInject, ɵinjectTemplateRef as injectTemplateRef, ɵinjectViewContainerRef as injectViewContainerRef, ɵrenderComponent as renderComponent} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, Injectable, NgModule, ɵrenderComponent as renderComponent} from '@angular/core'; class Todo { editing: boolean; @@ -63,32 +63,32 @@ class TodoStore {

todos

-
-
    -
  • -
    -
@@ -98,8 +98,8 @@ class TodoStore { {{todoStore.getRemaining().length}} {{todoStore.getRemaining().length == 1 ? 'item' : 'items'}} left - diff --git a/packages/core/test/render3/Inherit_definition_feature_spec.ts b/packages/core/test/render3/Inherit_definition_feature_spec.ts new file mode 100644 index 0000000000..f4babe51b4 --- /dev/null +++ b/packages/core/test/render3/Inherit_definition_feature_spec.ts @@ -0,0 +1,238 @@ +/** + * @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 {DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges} from '../../src/core'; +import {InheritDefinitionFeature} from '../../src/render3/features/inherit_definition_feature'; +import {DirectiveDefInternal, NgOnChangesFeature, defineComponent, defineDirective} from '../../src/render3/index'; + +describe('InheritDefinitionFeature', () => { + it('should inherit lifecycle hooks', () => { + class SuperDirective { + ngOnInit() {} + ngOnDestroy() {} + ngAfterContentInit() {} + ngAfterContentChecked() {} + ngAfterViewInit() {} + ngAfterViewChecked() {} + ngDoCheck() {} + } + + class SubDirective extends SuperDirective { + ngAfterViewInit() {} + ngAfterViewChecked() {} + ngDoCheck() {} + + static ngDirectiveDef = defineDirective({ + type: SubDirective, + selectors: [['', 'subDir', '']], + factory: () => new SubDirective(), + features: [InheritDefinitionFeature] + }); + } + + const finalDef = SubDirective.ngDirectiveDef as DirectiveDefInternal; + + + expect(finalDef.onInit).toBe(SuperDirective.prototype.ngOnInit); + expect(finalDef.onDestroy).toBe(SuperDirective.prototype.ngOnDestroy); + expect(finalDef.afterContentChecked).toBe(SuperDirective.prototype.ngAfterContentChecked); + expect(finalDef.afterContentInit).toBe(SuperDirective.prototype.ngAfterContentInit); + expect(finalDef.afterViewChecked).toBe(SubDirective.prototype.ngAfterViewChecked); + expect(finalDef.afterViewInit).toBe(SubDirective.prototype.ngAfterViewInit); + expect(finalDef.doCheck).toBe(SubDirective.prototype.ngDoCheck); + }); + + it('should inherit inputs', () => { + // tslint:disable-next-line:class-as-namespace + class SuperDirective { + static ngDirectiveDef = defineDirective({ + inputs: { + superFoo: ['foo', 'declaredFoo'], + superBar: 'bar', + superBaz: 'baz', + }, + type: SuperDirective, + selectors: [['', 'superDir', '']], + factory: () => new SuperDirective(), + }); + } + + // tslint:disable-next-line:class-as-namespace + class SubDirective extends SuperDirective { + static ngDirectiveDef = defineDirective({ + type: SubDirective, + inputs: { + subBaz: 'baz', + subQux: 'qux', + }, + selectors: [['', 'subDir', '']], + factory: () => new SubDirective(), + features: [InheritDefinitionFeature] + }); + } + + const subDef = SubDirective.ngDirectiveDef as DirectiveDefInternal; + + expect(subDef.inputs).toEqual({ + foo: 'superFoo', + bar: 'superBar', + baz: 'subBaz', + qux: 'subQux', + }); + expect(subDef.declaredInputs).toEqual({ + declaredFoo: 'superFoo', + bar: 'superBar', + baz: 'subBaz', + qux: 'subQux', + }); + }); + + it('should inherit outputs', () => { + // tslint:disable-next-line:class-as-namespace + class SuperDirective { + static ngDirectiveDef = defineDirective({ + outputs: { + superFoo: 'foo', + superBar: 'bar', + superBaz: 'baz', + }, + type: SuperDirective, + selectors: [['', 'superDir', '']], + factory: () => new SuperDirective(), + }); + } + + // tslint:disable-next-line:class-as-namespace + class SubDirective extends SuperDirective { + static ngDirectiveDef = defineDirective({ + type: SubDirective, + outputs: { + subBaz: 'baz', + subQux: 'qux', + }, + selectors: [['', 'subDir', '']], + factory: () => new SubDirective(), + features: [InheritDefinitionFeature] + }); + } + + const subDef = SubDirective.ngDirectiveDef as DirectiveDefInternal; + + expect(subDef.outputs).toEqual({ + foo: 'superFoo', + bar: 'superBar', + baz: 'subBaz', + qux: 'subQux', + }); + }); + + it('should compose hostBindings', () => { + const log: Array<[string, number, number]> = []; + + // tslint:disable-next-line:class-as-namespace + class SuperDirective { + static ngDirectiveDef = defineDirective({ + type: SuperDirective, + selectors: [['', 'superDir', '']], + hostBindings: (directiveIndex: number, elementIndex: number) => { + log.push(['super', directiveIndex, elementIndex]); + }, + factory: () => new SuperDirective(), + }); + } + + // tslint:disable-next-line:class-as-namespace + class SubDirective extends SuperDirective { + static ngDirectiveDef = defineDirective({ + type: SubDirective, + selectors: [['', 'subDir', '']], + hostBindings: (directiveIndex: number, elementIndex: number) => { + log.push(['sub', directiveIndex, elementIndex]); + }, + factory: () => new SubDirective(), + features: [InheritDefinitionFeature] + }); + } + + const subDef = SubDirective.ngDirectiveDef as DirectiveDefInternal; + + subDef.hostBindings !(1, 2); + + expect(log).toEqual([['super', 1, 2], ['sub', 1, 2]]); + }); + + it('should throw if inheriting a component from a directive', () => { + // tslint:disable-next-line:class-as-namespace + class SuperComponent { + static ngComponentDef = defineComponent({ + type: SuperComponent, + template: () => {}, + selectors: [['', 'superDir', '']], + factory: () => new SuperComponent() + }); + } + + expect(() => { + // tslint:disable-next-line:class-as-namespace + class SubDirective extends SuperComponent{static ngDirectiveDef = defineDirective({ + type: SubDirective, + selectors: [['', 'subDir', '']], + factory: () => new SubDirective(), + features: [InheritDefinitionFeature] + });} + }).toThrowError('Directives cannot inherit Components'); + }); + + it('should run inherited features', () => { + const log: any[] = []; + + // tslint:disable-next-line:class-as-namespace + class SuperDirective { + static ngDirectiveDef = defineDirective({ + type: SuperDirective, + selectors: [['', 'superDir', '']], + factory: () => new SuperDirective(), + features: [ + (arg: any) => { log.push('super1', arg); }, + (arg: any) => { log.push('super2', arg); }, + ] + }); + } + + class SubDirective extends SuperDirective { + @Output() + baz = new EventEmitter(); + + @Output() + qux = new EventEmitter(); + + static ngDirectiveDef = defineDirective({ + type: SubDirective, + selectors: [['', 'subDir', '']], + factory: () => new SubDirective(), + features: [InheritDefinitionFeature, (arg: any) => { log.push('sub1', arg); }] + }); + } + + const superDef = SuperDirective.ngDirectiveDef as DirectiveDefInternal; + const subDef = SubDirective.ngDirectiveDef as DirectiveDefInternal; + + expect(log).toEqual([ + 'super1', + superDef, + 'super2', + superDef, + 'super1', + subDef, + 'super2', + subDef, + 'sub1', + subDef, + ]); + }); +}); diff --git a/packages/core/test/render3/common_with_def.ts b/packages/core/test/render3/common_with_def.ts index 39d1077f0b..b030cd9ddf 100644 --- a/packages/core/test/render3/common_with_def.ts +++ b/packages/core/test/render3/common_with_def.ts @@ -38,8 +38,7 @@ NgForOf.ngDirectiveDef = defineDirective({ type: NgTemplateOutletDef, selectors: [['', 'ngTemplateOutlet', '']], factory: () => new NgTemplateOutletDef(injectViewContainerRef()), - features: [NgOnChangesFeature( - {ngTemplateOutlet: 'ngTemplateOutlet', ngTemplateOutletContext: 'ngTemplateOutletContext'})], + features: [NgOnChangesFeature], inputs: {ngTemplateOutlet: 'ngTemplateOutlet', ngTemplateOutletContext: 'ngTemplateOutletContext'} }); diff --git a/packages/core/test/render3/compiler_canonical/life_cycle_spec.ts b/packages/core/test/render3/compiler_canonical/life_cycle_spec.ts index f8cedef8ed..c3bda43937 100644 --- a/packages/core/test/render3/compiler_canonical/life_cycle_spec.ts +++ b/packages/core/test/render3/compiler_canonical/life_cycle_spec.ts @@ -47,8 +47,8 @@ describe('lifecycle hooks', () => { selectors: [['lifecycle-comp']], factory: function LifecycleComp_Factory() { return new LifecycleComp(); }, template: function LifecycleComp_Template(rf: $RenderFlags$, ctx: $LifecycleComp$) {}, - inputs: {nameMin: 'name'}, - features: [$r3$.ɵNgOnChangesFeature({nameMin: 'nameMin'})] + inputs: {nameMin: ['name', 'nameMin']}, + features: [$r3$.ɵNgOnChangesFeature] }); // /NORMATIVE } diff --git a/packages/core/test/render3/compiler_canonical/template_variables_spec.ts b/packages/core/test/render3/compiler_canonical/template_variables_spec.ts index 0633142d6b..5b0bc8fcfe 100644 --- a/packages/core/test/render3/compiler_canonical/template_variables_spec.ts +++ b/packages/core/test/render3/compiler_canonical/template_variables_spec.ts @@ -72,7 +72,7 @@ describe('template variables', () => { return new ForOfDirective($r3$.ɵinjectViewContainerRef(), $r3$.ɵinjectTemplateRef()); }, // TODO(chuckj): Enable when ngForOf enabling lands. - // features: [NgOnChangesFeature(NgForOf)], + // features: [NgOnChangesFeature], inputs: {forOf: 'forOf'} }); // /NORMATIVE diff --git a/packages/core/test/render3/define_spec.ts b/packages/core/test/render3/define_spec.ts deleted file mode 100644 index bd4d2bbac3..0000000000 --- a/packages/core/test/render3/define_spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * @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 {DoCheck, OnChanges, SimpleChange, SimpleChanges} from '../../src/core'; -import {DirectiveDefInternal, NgOnChangesFeature, defineDirective} from '../../src/render3/index'; - -describe('define', () => { - describe('component', () => { - describe('NgOnChangesFeature', () => { - it('should patch class', () => { - class MyDirective implements OnChanges, DoCheck { - public log: Array = []; - public valA: string = 'initValue'; - public set valB(value: string) { this.log.push(value); } - - public get valB() { return 'works'; } - - ngDoCheck(): void { this.log.push('ngDoCheck'); } - ngOnChanges(changes: SimpleChanges): void { - this.log.push('ngOnChanges'); - this.log.push('valA', changes['valA']); - this.log.push('valB', changes['valB']); - } - - static ngDirectiveDef = defineDirective({ - type: MyDirective, - selectors: [['', 'myDir', '']], - factory: () => new MyDirective(), - features: [NgOnChangesFeature()], - inputs: {valA: 'valA', valB: 'valB'} - }); - } - - const myDir = (MyDirective.ngDirectiveDef as DirectiveDefInternal) - .factory() as MyDirective; - myDir.valA = 'first'; - expect(myDir.valA).toEqual('first'); - myDir.valB = 'second'; - expect(myDir.log).toEqual(['second']); - expect(myDir.valB).toEqual('works'); - myDir.log.length = 0; - (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); - const changeA = new SimpleChange(undefined, 'first', true); - const changeB = new SimpleChange(undefined, 'second', true); - expect(myDir.log).toEqual(['ngOnChanges', 'valA', changeA, 'valB', changeB, 'ngDoCheck']); - }); - - it('correctly computes firstChange', () => { - class MyDirective implements OnChanges { - public log: Array = []; - public valA: string = 'initValue'; - // TODO(issue/24571): remove '!'. - public valB !: string; - - ngOnChanges(changes: SimpleChanges): void { - this.log.push('valA', changes['valA']); - this.log.push('valB', changes['valB']); - } - - static ngDirectiveDef = defineDirective({ - type: MyDirective, - selectors: [['', 'myDir', '']], - factory: () => new MyDirective(), - features: [NgOnChangesFeature()], - inputs: {valA: 'valA', valB: 'valB'} - }); - } - - const myDir = (MyDirective.ngDirectiveDef as DirectiveDefInternal) - .factory() as MyDirective; - myDir.valA = 'first'; - myDir.valB = 'second'; - (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); - const changeA1 = new SimpleChange(undefined, 'first', true); - const changeB1 = new SimpleChange(undefined, 'second', true); - expect(myDir.log).toEqual(['valA', changeA1, 'valB', changeB1]); - - myDir.log.length = 0; - myDir.valA = 'third'; - (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); - const changeA2 = new SimpleChange('first', 'third', false); - expect(myDir.log).toEqual(['valA', changeA2, 'valB', undefined]); - }); - - it('should not create a getter when only a setter is originally defined', () => { - class MyDirective implements OnChanges { - public log: Array = []; - - public set onlySetter(value: string) { this.log.push(value); } - - ngOnChanges(changes: SimpleChanges): void { - this.log.push('ngOnChanges'); - this.log.push('onlySetter', changes['onlySetter']); - } - - static ngDirectiveDef = defineDirective({ - type: MyDirective, - selectors: [['', 'myDir', '']], - factory: () => new MyDirective(), - features: [NgOnChangesFeature()], - inputs: {onlySetter: 'onlySetter'} - }); - } - - const myDir = (MyDirective.ngDirectiveDef as DirectiveDefInternal) - .factory() as MyDirective; - myDir.onlySetter = 'someValue'; - expect(myDir.onlySetter).toBeUndefined(); - (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); - const changeSetter = new SimpleChange(undefined, 'someValue', true); - expect(myDir.log).toEqual(['someValue', 'ngOnChanges', 'onlySetter', changeSetter]); - }); - }); - }); -}); diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 96e34f7cdc..2db6233f48 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -995,7 +995,7 @@ describe('di', () => { selectors: [['', 'myIf', '']], factory: () => new IfDirective(injectTemplateRef(), injectViewContainerRef()), inputs: {myIf: 'myIf'}, - features: [PublicFeature, NgOnChangesFeature()] + features: [PublicFeature, NgOnChangesFeature] }); } diff --git a/packages/core/test/render3/jit/directive_spec.ts b/packages/core/test/render3/jit/directive_spec.ts new file mode 100644 index 0000000000..85c50a613a --- /dev/null +++ b/packages/core/test/render3/jit/directive_spec.ts @@ -0,0 +1,39 @@ +/** + * @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 {extendsDirectlyFromObject} from '../../../src/render3/jit/directive'; + +describe('extendsDirectlyFromObject', () => { + it('should correctly behave with instanceof', () => { + expect(new Child() instanceof Object).toBeTruthy(); + expect(new Child() instanceof Parent).toBeTruthy(); + expect(new Parent() instanceof Child).toBeFalsy(); + + expect(new Child5() instanceof Object).toBeTruthy(); + expect(new Child5() instanceof Parent5).toBeTruthy(); + expect(new Parent5() instanceof Child5).toBeFalsy(); + }); + + it('should detect direct inheritance form Object', () => { + expect(extendsDirectlyFromObject(Parent)).toBeTruthy(); + expect(extendsDirectlyFromObject(Child)).toBeFalsy(); + + expect(extendsDirectlyFromObject(Parent5)).toBeTruthy(); + expect(extendsDirectlyFromObject(Child5)).toBeFalsy(); + }); +}); + +// Inheritance Example using Classes +class Parent {} +class Child extends Parent {} + +// Inheritance Example using Function +const Parent5 = function Parent5() {} as any as{new (): {}}; +const Child5 = function Child5() {} as any as{new (): {}}; +Child5.prototype = new Parent5; +Child5.prototype.constructor = Child5; diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index 3a190ccd0a..bec48f81db 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -1932,8 +1932,8 @@ describe('lifecycles', () => { type: Component, selectors: [[name]], factory: () => new Component(), - features: [NgOnChangesFeature({b: 'val2'})], - inputs: {a: 'val1', b: 'publicName'}, template, + features: [NgOnChangesFeature], + inputs: {a: 'val1', b: ['publicName', 'val2']}, template, directives: directives }); }; @@ -1953,8 +1953,8 @@ describe('lifecycles', () => { type: Directive, selectors: [['', 'dir', '']], factory: () => new Directive(), - features: [NgOnChangesFeature({b: 'val2'})], - inputs: {a: 'val1', b: 'publicName'} + features: [NgOnChangesFeature], + inputs: {a: 'val1', b: ['publicName', 'val2']} }); } @@ -1976,11 +1976,10 @@ describe('lifecycles', () => { renderToHtml(Template, {val1: '1', val2: 'a'}, defs); expect(events).toEqual(['comp=comp val1=1 val2=a - changed=[val1,val2]']); + events.length = 0; + renderToHtml(Template, {val1: '2', val2: 'b'}, defs); - expect(events).toEqual([ - 'comp=comp val1=1 val2=a - changed=[val1,val2]', - 'comp=comp val1=2 val2=b - changed=[val1,val2]' - ]); + expect(events).toEqual(['comp=comp val1=2 val2=b - changed=[val1,val2]']); }); it('should call parent onChanges before child onChanges', () => { @@ -2336,7 +2335,7 @@ describe('lifecycles', () => { selectors: [[name]], factory: () => new Component(), inputs: {val: 'val'}, template, - features: [NgOnChangesFeature()], + features: [NgOnChangesFeature], directives: directives }); }; diff --git a/packages/core/test/render3/ng_on_changes_feature_spec.ts b/packages/core/test/render3/ng_on_changes_feature_spec.ts new file mode 100644 index 0000000000..719a5dde5b --- /dev/null +++ b/packages/core/test/render3/ng_on_changes_feature_spec.ts @@ -0,0 +1,325 @@ +/** + * @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 {DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges} from '../../src/core'; +import {InheritDefinitionFeature} from '../../src/render3/features/inherit_definition_feature'; +import {DirectiveDefInternal, NgOnChangesFeature, defineComponent, defineDirective} from '../../src/render3/index'; + +describe('NgOnChangesFeature', () => { + it('should patch class', () => { + class MyDirective implements OnChanges, DoCheck { + public log: Array = []; + public valA: string = 'initValue'; + public set valB(value: string) { this.log.push(value); } + + public get valB() { return 'works'; } + + ngDoCheck(): void { this.log.push('ngDoCheck'); } + ngOnChanges(changes: SimpleChanges): void { + this.log.push('ngOnChanges'); + this.log.push('valA', changes['valA']); + this.log.push('valB', changes['valB']); + } + + static ngDirectiveDef = defineDirective({ + type: MyDirective, + selectors: [['', 'myDir', '']], + factory: () => new MyDirective(), + features: [NgOnChangesFeature], + inputs: {valA: 'valA', valB: 'valB'} + }); + } + + const myDir = + (MyDirective.ngDirectiveDef as DirectiveDefInternal).factory() as MyDirective; + myDir.valA = 'first'; + expect(myDir.valA).toEqual('first'); + myDir.valB = 'second'; + expect(myDir.log).toEqual(['second']); + expect(myDir.valB).toEqual('works'); + myDir.log.length = 0; + (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); + const changeA = new SimpleChange(undefined, 'first', true); + const changeB = new SimpleChange(undefined, 'second', true); + expect(myDir.log).toEqual(['ngOnChanges', 'valA', changeA, 'valB', changeB, 'ngDoCheck']); + }); + + it('should inherit the behavior from super class', () => { + const log: any[] = []; + + class SuperDirective implements OnChanges, DoCheck { + valA = 'initValue'; + + set valB(value: string) { log.push(value); } + + get valB() { return 'works'; } + + ngDoCheck(): void { log.push('ngDoCheck'); } + ngOnChanges(changes: SimpleChanges): void { + log.push('ngOnChanges'); + log.push('valA', changes['valA']); + log.push('valB', changes['valB']); + log.push('valC', changes['valC']); + } + + static ngDirectiveDef = defineDirective({ + type: SuperDirective, + selectors: [['', 'superDir', '']], + factory: () => new SuperDirective(), + features: [NgOnChangesFeature], + inputs: {valA: 'valA', valB: 'valB'}, + }); + } + + class SubDirective extends SuperDirective { + valC = 'initValue'; + + static ngDirectiveDef = defineDirective({ + type: SubDirective, + selectors: [['', 'subDir', '']], + factory: () => new SubDirective(), + features: [InheritDefinitionFeature], + inputs: {valC: 'valC'}, + }); + } + + const myDir = (SubDirective.ngDirectiveDef as DirectiveDefInternal) + .factory() as SubDirective; + myDir.valA = 'first'; + expect(myDir.valA).toEqual('first'); + + myDir.valB = 'second'; + expect(myDir.valB).toEqual('works'); + + myDir.valC = 'third'; + expect(myDir.valC).toEqual('third'); + + log.length = 0; + (SubDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); + const changeA = new SimpleChange(undefined, 'first', true); + const changeB = new SimpleChange(undefined, 'second', true); + const changeC = new SimpleChange(undefined, 'third', true); + + expect(log).toEqual( + ['ngOnChanges', 'valA', changeA, 'valB', changeB, 'valC', changeC, 'ngDoCheck']); + }); + + it('should not run the parent doCheck if it is not called explicitly on super class', () => { + const log: any[] = []; + + class SuperDirective implements OnChanges, DoCheck { + valA = 'initValue'; + + ngDoCheck(): void { log.push('ERROR: Child overrides it without super call'); } + ngOnChanges(changes: SimpleChanges): void { log.push(changes.valA, changes.valB); } + + static ngDirectiveDef = defineDirective({ + type: SuperDirective, + selectors: [['', 'superDir', '']], + factory: () => new SuperDirective(), + features: [NgOnChangesFeature], + inputs: {valA: 'valA'}, + }); + } + + class SubDirective extends SuperDirective implements DoCheck { + valB = 'initValue'; + + ngDoCheck(): void { log.push('sub ngDoCheck'); } + + static ngDirectiveDef = defineDirective({ + type: SubDirective, + selectors: [['', 'subDir', '']], + factory: () => new SubDirective(), + features: [InheritDefinitionFeature], + inputs: {valB: 'valB'}, + }); + } + + const myDir = (SubDirective.ngDirectiveDef as DirectiveDefInternal) + .factory() as SubDirective; + myDir.valA = 'first'; + myDir.valB = 'second'; + + (SubDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); + const changeA = new SimpleChange(undefined, 'first', true); + const changeB = new SimpleChange(undefined, 'second', true); + expect(log).toEqual([changeA, changeB, 'sub ngDoCheck']); + }); + + it('should run the parent doCheck if it is inherited from super class', () => { + const log: any[] = []; + + class SuperDirective implements OnChanges, DoCheck { + valA = 'initValue'; + + ngDoCheck(): void { log.push('super ngDoCheck'); } + ngOnChanges(changes: SimpleChanges): void { log.push(changes.valA, changes.valB); } + + static ngDirectiveDef = defineDirective({ + type: SuperDirective, + selectors: [['', 'superDir', '']], + factory: () => new SuperDirective(), + features: [NgOnChangesFeature], + inputs: {valA: 'valA'}, + }); + } + + class SubDirective extends SuperDirective implements DoCheck { + valB = 'initValue'; + + static ngDirectiveDef = defineDirective({ + type: SubDirective, + selectors: [['', 'subDir', '']], + factory: () => new SubDirective(), + features: [InheritDefinitionFeature], + inputs: {valB: 'valB'}, + }); + } + + const myDir = (SubDirective.ngDirectiveDef as DirectiveDefInternal) + .factory() as SubDirective; + myDir.valA = 'first'; + myDir.valB = 'second'; + + (SubDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); + const changeA = new SimpleChange(undefined, 'first', true); + const changeB = new SimpleChange(undefined, 'second', true); + expect(log).toEqual([changeA, changeB, 'super ngDoCheck']); + }); + + it('should apply the feature to inherited properties if on sub class', () => { + const log: any[] = []; + + class SuperDirective { + valC = 'initValue'; + + static ngDirectiveDef = defineDirective({ + type: SuperDirective, + selectors: [['', 'subDir', '']], + factory: () => new SuperDirective(), + features: [], + inputs: {valC: 'valC'}, + }); + } + + class SubDirective extends SuperDirective implements OnChanges, DoCheck { + valA = 'initValue'; + + set valB(value: string) { log.push(value); } + + get valB() { return 'works'; } + + ngDoCheck(): void { log.push('ngDoCheck'); } + ngOnChanges(changes: SimpleChanges): void { + log.push('ngOnChanges'); + log.push('valA', changes['valA']); + log.push('valB', changes['valB']); + log.push('valC', changes['valC']); + } + + static ngDirectiveDef = defineDirective({ + type: SubDirective, + selectors: [['', 'superDir', '']], + factory: () => new SubDirective(), + // Inheritance must always be before OnChanges feature. + features: [ + InheritDefinitionFeature, + NgOnChangesFeature, + ], + inputs: {valA: 'valA', valB: 'valB'} + }); + } + + const myDir = (SubDirective.ngDirectiveDef as DirectiveDefInternal) + .factory() as SubDirective; + myDir.valA = 'first'; + expect(myDir.valA).toEqual('first'); + + myDir.valB = 'second'; + expect(log).toEqual(['second']); + expect(myDir.valB).toEqual('works'); + + myDir.valC = 'third'; + expect(myDir.valC).toEqual('third'); + + log.length = 0; + (SubDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); + const changeA = new SimpleChange(undefined, 'first', true); + const changeB = new SimpleChange(undefined, 'second', true); + const changeC = new SimpleChange(undefined, 'third', true); + expect(log).toEqual( + ['ngOnChanges', 'valA', changeA, 'valB', changeB, 'valC', changeC, 'ngDoCheck']); + }); + + it('correctly computes firstChange', () => { + class MyDirective implements OnChanges { + public log: Array = []; + public valA: string = 'initValue'; + // TODO(issue/24571): remove '!'. + public valB !: string; + + ngOnChanges(changes: SimpleChanges): void { + this.log.push('valA', changes['valA']); + this.log.push('valB', changes['valB']); + } + + static ngDirectiveDef = defineDirective({ + type: MyDirective, + selectors: [['', 'myDir', '']], + factory: () => new MyDirective(), + features: [NgOnChangesFeature], + inputs: {valA: 'valA', valB: 'valB'} + }); + } + + const myDir = + (MyDirective.ngDirectiveDef as DirectiveDefInternal).factory() as MyDirective; + myDir.valA = 'first'; + myDir.valB = 'second'; + (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); + const changeA1 = new SimpleChange(undefined, 'first', true); + const changeB1 = new SimpleChange(undefined, 'second', true); + expect(myDir.log).toEqual(['valA', changeA1, 'valB', changeB1]); + + myDir.log.length = 0; + myDir.valA = 'third'; + (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); + const changeA2 = new SimpleChange('first', 'third', false); + expect(myDir.log).toEqual(['valA', changeA2, 'valB', undefined]); + }); + + it('should not create a getter when only a setter is originally defined', () => { + class MyDirective implements OnChanges { + public log: Array = []; + + public set onlySetter(value: string) { this.log.push(value); } + + ngOnChanges(changes: SimpleChanges): void { + this.log.push('ngOnChanges'); + this.log.push('onlySetter', changes['onlySetter']); + } + + static ngDirectiveDef = defineDirective({ + type: MyDirective, + selectors: [['', 'myDir', '']], + factory: () => new MyDirective(), + features: [NgOnChangesFeature], + inputs: {onlySetter: 'onlySetter'} + }); + } + + const myDir = + (MyDirective.ngDirectiveDef as DirectiveDefInternal).factory() as MyDirective; + myDir.onlySetter = 'someValue'; + expect(myDir.onlySetter).toBeUndefined(); + (MyDirective.ngDirectiveDef as DirectiveDefInternal).doCheck !.call(myDir); + const changeSetter = new SimpleChange(undefined, 'someValue', true); + expect(myDir.log).toEqual(['someValue', 'ngOnChanges', 'onlySetter', changeSetter]); + }); +}); diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 6f34b01f80..7ec49e6f32 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -11,7 +11,7 @@ import {stringifyElement} from '@angular/platform-browser/testing/src/browser_ut import {Injector} from '../../src/di/injector'; import {CreateComponentOptions} from '../../src/render3/component'; import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition'; -import {ComponentDefInternal, ComponentTemplate, ComponentType, DirectiveDefInternal, DirectiveType, PublicFeature, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index'; +import {ComponentTemplate, ComponentType, DirectiveDefInternal, DirectiveType, PublicFeature, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index'; import {NG_HOST_SYMBOL, renderTemplate} from '../../src/render3/instructions'; import {DirectiveDefList, DirectiveDefListOrFactory, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeDefListOrFactory, PipeTypesOrFactory} from '../../src/render3/interfaces/definition'; import {LElementNode} from '../../src/render3/interfaces/node'; diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 0285415566..34a300ab4b 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -966,7 +966,7 @@ describe('ViewContainerRef', () => { textBinding(0, interpolation1('', cmp.name, '')); } }, - features: [NgOnChangesFeature()], + features: [NgOnChangesFeature], inputs: {name: 'name'} }); }