diff --git a/packages/compiler/src/render3/view/styling_builder.ts b/packages/compiler/src/render3/view/styling_builder.ts
index 78c4afbe4c..8eb0b566d3 100644
--- a/packages/compiler/src/render3/view/styling_builder.ts
+++ b/packages/compiler/src/render3/view/styling_builder.ts
@@ -201,6 +201,10 @@ export class StylingBuilder {
const entry:
BoundStylingEntry = {name: property, value, sourceSpan, hasOverrideFlag, unit: null};
if (isMapBased) {
+ if (this._classMapInput) {
+ throw new Error(
+ '[class] and [className] bindings cannot be used on the same element simultaneously');
+ }
this._classMapInput = entry;
} else {
(this._singleClassInputs = this._singleClassInputs || []).push(entry);
diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts
index 5e1c9c9936..114351d6ca 100644
--- a/packages/core/src/render3/instructions/element.ts
+++ b/packages/core/src/render3/instructions/element.ts
@@ -19,7 +19,7 @@ import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation';
import {decreaseElementDepthCount, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state';
import {setUpAttributes} from '../util/attrs_utils';
-import {getInitialStylingValue, hasClassInput, hasStyleInput} from '../util/styling_utils';
+import {getInitialStylingValue, hasClassInput, hasStyleInput, selectClassBasedInputName} from '../util/styling_utils';
import {getNativeByTNode, getTNode} from '../util/view_utils';
import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, renderInitialStyling, resolveDirectives, saveResolvedLocalsInData, setInputsForProperty} from './shared';
@@ -132,7 +132,8 @@ export function ɵɵelementEnd(): void {
}
if (hasClassInput(tNode)) {
- setDirectiveStylingInput(tNode.classes, lView, tNode.inputs !['class']);
+ const inputName: string = selectClassBasedInputName(tNode.inputs !);
+ setDirectiveStylingInput(tNode.classes, lView, tNode.inputs ![inputName]);
}
if (hasStyleInput(tNode)) {
diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts
index da605a6df1..85157d0be3 100644
--- a/packages/core/src/render3/instructions/shared.ts
+++ b/packages/core/src/render3/instructions/shared.ts
@@ -853,7 +853,7 @@ function initializeInputAndOutputAliases(tView: TView, tNode: TNode): void {
}
if (inputsStore !== null) {
- if (inputsStore.hasOwnProperty('class')) {
+ if (inputsStore.hasOwnProperty('class') || inputsStore.hasOwnProperty('className')) {
tNode.flags |= TNodeFlags.hasClassInput;
}
if (inputsStore.hasOwnProperty('style')) {
diff --git a/packages/core/src/render3/instructions/styling.ts b/packages/core/src/render3/instructions/styling.ts
index 159db4d871..ef2b50c894 100644
--- a/packages/core/src/render3/instructions/styling.ts
+++ b/packages/core/src/render3/instructions/styling.ts
@@ -19,7 +19,7 @@ import {activateStylingMapFeature} from '../styling/map_based_bindings';
import {attachStylingDebugObject} from '../styling/styling_debug';
import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils';
-import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, patchConfig, setValue, stylingMapToString} from '../util/styling_utils';
+import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, hasClassInput, hasStyleInput, hasValueChanged, isContextLocked, isHostStylingActive, isStylingContext, normalizeIntoStylingMap, patchConfig, selectClassBasedInputName, setValue, stylingMapToString} from '../util/styling_utils';
import {getNativeByTNode, getTNode} from '../util/view_utils';
@@ -402,7 +402,7 @@ function updateDirectiveInputValue(
// directive input(s) in the event that it is falsy during the
// first update pass.
if (newValue || isContextLocked(context, false)) {
- const inputName = isClassBased ? 'class' : 'style';
+ const inputName: string = isClassBased ? selectClassBasedInputName(tNode.inputs !) : 'style';
const inputs = tNode.inputs ![inputName] !;
const initialValue = getInitialStylingValue(context);
const value = normalizeStylingDirectiveInputValue(initialValue, newValue, isClassBased);
diff --git a/packages/core/src/render3/util/styling_utils.ts b/packages/core/src/render3/util/styling_utils.ts
index 1a4e26a2c7..f96f53c155 100644
--- a/packages/core/src/render3/util/styling_utils.ts
+++ b/packages/core/src/render3/util/styling_utils.ts
@@ -5,7 +5,7 @@
* 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 {TNode, TNodeFlags} from '../interfaces/node';
+import {PropertyAliases, TNode, TNodeFlags} from '../interfaces/node';
import {LStylingData, StylingMapArray, StylingMapArrayIndex, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling';
import {NO_CHANGE} from '../tokens';
@@ -433,3 +433,9 @@ export function normalizeIntoStylingMap(
return stylingMapArr;
}
+
+// TODO (matsko|AndrewKushnir): refactor this once we figure out how to generate separate
+// `input('class') + classMap()` instructions.
+export function selectClassBasedInputName(inputs: PropertyAliases): string {
+ return inputs.hasOwnProperty('class') ? 'class' : 'className';
+}
\ No newline at end of file
diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts
index 1a6c4a2d65..e0c02739c6 100644
--- a/packages/core/test/acceptance/styling_spec.ts
+++ b/packages/core/test/acceptance/styling_spec.ts
@@ -551,6 +551,74 @@ describe('styling', () => {
expect(capturedMyClassBindingCount).toEqual(1);
});
+ it('should write to a `className` input binding', () => {
+ @Component({
+ selector: 'comp',
+ template: `{{className}}`,
+ })
+ class Comp {
+ @Input() className: string = '';
+ }
+ @Component({
+ template: ``,
+ })
+ class App {
+ }
+
+ TestBed.configureTestingModule({declarations: [Comp, App]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+ expect(fixture.debugElement.nativeElement.firstChild.innerHTML).toBe('my-className');
+ });
+
+ onlyInIvy('only ivy combines static and dynamic class-related attr values')
+ .it('should write to a `className` input binding, when static `class` is present', () => {
+ @Component({
+ selector: 'comp',
+ template: `{{className}}`,
+ })
+ class Comp {
+ @Input() className: string = '';
+ }
+
+ @Component({
+ template: ``,
+ })
+ class App {
+ }
+
+ TestBed.configureTestingModule({declarations: [Comp, App]});
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+ expect(fixture.debugElement.nativeElement.firstChild.innerHTML).toBe('static my-className');
+ });
+
+ onlyInIvy('in Ivy [class] and [className] bindings on the same element are not allowed')
+ .it('should throw an error in case [class] and [className] bindings are used on the same element',
+ () => {
+ @Component({
+ selector: 'comp',
+ template: `{{class}} - {{className}}`,
+ })
+ class Comp {
+ @Input() class: string = '';
+ @Input() className: string = '';
+ }
+ @Component({
+ template: ``,
+ })
+ class App {
+ }
+
+ TestBed.configureTestingModule({declarations: [Comp, App]});
+ expect(() => {
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+ })
+ .toThrowError(
+ '[class] and [className] bindings cannot be used on the same element simultaneously');
+ });
+
onlyInIvy('only ivy persists static class/style attrs with their binding counterparts')
.it('should write to a `class` input binding if there is a static class value and there is a binding value',
() => {
diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
index 1edbb3f222..ec7b4c2d66 100644
--- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
@@ -584,6 +584,9 @@
{
"name": "saveResolvedLocalsInData"
},
+ {
+ "name": "selectClassBasedInputName"
+ },
{
"name": "selectIndexInternal"
},
diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json
index a03e73d82e..2a8f0e48d6 100644
--- a/packages/core/test/bundling/todo/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json
@@ -1184,6 +1184,9 @@
{
"name": "searchTokensOnInjector"
},
+ {
+ "name": "selectClassBasedInputName"
+ },
{
"name": "selectIndexInternal"
},