diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 2d54280de7..6413b77c2d 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -15,8 +15,8 @@ import {QueryList} from '../linker'; import {Sanitizer} from '../sanitization/security'; import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {Type} from '../type'; +import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../util/ng_reflect'; import {noop} from '../util/noop'; - import {assertDefined, assertEqual, assertLessThan, assertNotEqual} from './assert'; import {attachPatchData, getComponentViewByInstance} from './context_discovery'; import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreateNodeInjectorForNode, injectAttributeImpl} from './di'; @@ -956,6 +956,9 @@ export function elementProperty( if (inputData && (dataValue = inputData[propName])) { setInputsForProperty(viewData, dataValue, value); if (isComponent(tNode)) markDirtyIfOnPush(viewData, index + HEADER_OFFSET); + if (ngDevMode && tNode.type === TNodeType.Element) { + setNgReflectProperties(element as RElement, propName, value); + } } else if (tNode.type === TNodeType.Element) { const renderer = getRenderer(); // It is assumed that the sanitizer is only added when the compiler determines that the property @@ -1025,6 +1028,15 @@ function setInputsForProperty(viewData: LViewData, inputs: PropertyAliasValue, v } } +function setNgReflectProperties(element: RElement, propName: string, value: any) { + const renderer = getRenderer() as ProceduralRenderer3; + const isProcedural = isProceduralRenderer(renderer); + const attrName = normalizeDebugBindingName(propName); + const debugValue = normalizeDebugBindingValue(value); + isProcedural ? renderer.setAttribute(element, attrName, debugValue) : + element.setAttribute(attrName, debugValue); +} + /** * Consolidates all inputs or outputs of all directives on this logical node. * diff --git a/packages/core/src/util/ng_reflect.ts b/packages/core/src/util/ng_reflect.ts new file mode 100644 index 0000000000..2620111236 --- /dev/null +++ b/packages/core/src/util/ng_reflect.ts @@ -0,0 +1,28 @@ +/** + * @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 + */ + +export function normalizeDebugBindingName(name: string) { + // Attribute names with `$` (eg `x-y$`) are valid per spec, but unsupported by some browsers + name = camelCaseToDashCase(name.replace(/[$@]/g, '_')); + return `ng-reflect-${name}`; +} + +const CAMEL_CASE_REGEXP = /([A-Z])/g; + +function camelCaseToDashCase(input: string): string { + return input.replace(CAMEL_CASE_REGEXP, (...m: any[]) => '-' + m[1].toLowerCase()); +} + +export function normalizeDebugBindingValue(value: any): string { + try { + // Limit the size of the value as otherwise the DOM just gets polluted. + return value != null ? value.toString().slice(0, 30) : value; + } catch (e) { + return '[ERROR] Exception while trying to serialize the value'; + } +} diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index db59da6097..93bfda1741 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -18,14 +18,13 @@ import {NgModuleRef} from '../linker/ng_module_factory'; import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api'; import {Sanitizer} from '../sanitization/security'; import {Type} from '../type'; -import {tokenKey} from '../view/util'; - +import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../util/ng_reflect'; import {isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors'; import {resolveDep} from './provider'; import {dirtyParentQueries, getQueryValue} from './query'; import {createInjector, createNgModuleRef, getComponentViewDefinitionFactory} from './refs'; import {ArgumentType, BindingFlags, CheckType, DebugContext, ElementData, NgModuleDefinition, NodeDef, NodeFlags, NodeLogger, ProviderOverride, RootData, Services, ViewData, ViewDefinition, ViewState, asElementData, asPureExpressionData} from './types'; -import {NOOP, isComponentView, renderNode, resolveDefinition, splitDepsDsl, viewParentEl} from './util'; +import {NOOP, isComponentView, renderNode, resolveDefinition, splitDepsDsl, tokenKey, viewParentEl} from './util'; import {checkAndUpdateNode, checkAndUpdateView, checkNoChangesNode, checkNoChangesView, createComponentView, createEmbeddedView, createRootView, destroyView} from './view'; @@ -467,27 +466,6 @@ function debugCheckNoChangesNode( (checkNoChangesNode)(view, nodeDef, argStyle, ...values); } -function normalizeDebugBindingName(name: string) { - // Attribute names with `$` (eg `x-y$`) are valid per spec, but unsupported by some browsers - name = camelCaseToDashCase(name.replace(/[$@]/g, '_')); - return `ng-reflect-${name}`; -} - -const CAMEL_CASE_REGEXP = /([A-Z])/g; - -function camelCaseToDashCase(input: string): string { - return input.replace(CAMEL_CASE_REGEXP, (...m: any[]) => '-' + m[1].toLowerCase()); -} - -function normalizeDebugBindingValue(value: any): string { - try { - // Limit the size of the value as otherwise the DOM just gets polluted. - return value != null ? value.toString().slice(0, 30) : value; - } catch (e) { - return '[ERROR] Exception while trying to serialize the value'; - } -} - function nextDirectiveWithBinding(view: ViewData, nodeIndex: number): number|null { for (let i = nodeIndex; i < view.def.nodes.length; i++) { const nodeDef = view.def.nodes[i]; diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 8070106852..0e1709bb16 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -218,11 +218,11 @@ export function resetDOM() { export function renderToHtml( template: ComponentTemplate, ctx: any, consts: number = 0, vars: number = 0, directives?: DirectiveTypesOrFactory | null, pipes?: PipeTypesOrFactory | null, - providedRendererFactory?: RendererFactory3 | null) { + providedRendererFactory?: RendererFactory3 | null, keepNgReflect = false) { hostView = renderTemplate( containerEl, template, consts, vars, ctx, providedRendererFactory || testRendererFactory, hostView, toDefs(directives, extractDirectiveDef), toDefs(pipes, extractPipeDef)); - return toHtml(containerEl); + return toHtml(containerEl, keepNgReflect); } function toDefs( @@ -263,7 +263,7 @@ export function renderComponent(type: ComponentType, opts?: CreateComponen /** * @deprecated use `TemplateFixture` or `ComponentFixture` */ -export function toHtml(componentOrElement: T | RElement): string { +export function toHtml(componentOrElement: T | RElement, keepNgReflect = false): string { let element: any; if (isComponentInstance(componentOrElement)) { const context = getContext(componentOrElement); @@ -273,13 +273,17 @@ export function toHtml(componentOrElement: T | RElement): string { } if (element) { - return stringifyElement(element) - .replace(/^
(.*)<\/div>$/, '$1') - .replace(/^
(.*)<\/div>$/, '$1') - .replace(/^
(.*)<\/div>$/, '$1') - .replace(' style=""', '') - .replace(//g, '') - .replace(//g, ''); + let html = stringifyElement(element) + .replace(/^
(.*)<\/div>$/, '$1') + .replace(/^
(.*)<\/div>$/, '$1') + .replace(/^
(.*)<\/div>$/, '$1') + .replace(' style=""', '') + .replace(//g, '') + .replace(//g, ''); + if (!keepNgReflect) { + html = html.replace(/\sng-reflect-\S*="[^"]*"/g, ''); + } + return html; } else { return ''; } diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index 1202302e51..29ba5706e9 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -614,15 +614,14 @@ class HiddenModule { }); })); - fixmeIvy('to investigate') && - it('should handle false values on attributes', async(() => { - renderModule(FalseAttributesModule, {document: doc}).then(output => { - expect(output).toBe( - '' + - 'Works!'); - called = true; - }); - })); + it('should handle false values on attributes', async(() => { + renderModule(FalseAttributesModule, {document: doc}).then(output => { + expect(output).toBe( + '' + + 'Works!'); + called = true; + }); + })); it('should handle element property "name"', async(() => { renderModule(NameModule, {document: doc}).then(output => {