fix(ivy): compiler should generate bindings to host attrs properly (#26973)

PR Close #26973
This commit is contained in:
Kara Erickson 2018-11-06 19:05:06 -08:00
parent a4398aa17f
commit 9e26216c40
7 changed files with 336 additions and 212 deletions

View File

@ -559,87 +559,6 @@ describe('compiler compliance', () => {
expectEmit(source, OtherDirectiveDefinition, 'Incorrect OtherDirective.ngDirectiveDef'); expectEmit(source, OtherDirectiveDefinition, 'Incorrect OtherDirective.ngDirectiveDef');
}); });
it('should support host bindings', () => {
const files = {
app: {
'spec.ts': `
import {Directive, HostBinding, NgModule} from '@angular/core';
@Directive({selector: '[hostBindingDir]'})
export class HostBindingDir {
@HostBinding('id') dirId = 'some id';
}
@NgModule({declarations: [HostBindingDir]})
export class MyModule {}
`
}
};
const HostBindingDirDeclaration = `
HostBindingDir.ngDirectiveDef = $r3$.ɵdefineDirective({
type: HostBindingDir,
selectors: [["", "hostBindingDir", ""]],
factory: function HostBindingDir_Factory(t) { return new (t || HostBindingDir)(); },
hostBindings: function HostBindingDir_HostBindings(dirIndex, elIndex) {
$r3$.ɵelementProperty(elIndex, "id", $r3$.ɵbind($r3$.ɵload(dirIndex).dirId));
},
hostVars: 1
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, HostBindingDirDeclaration, 'Invalid host binding code');
});
it('should support host bindings with pure functions', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'host-binding-comp',
host: {
'[id]': '["red", id]'
},
template: ''
})
export class HostBindingComp {
id = 'some id';
}
@NgModule({declarations: [HostBindingComp]})
export class MyModule {}
`
}
};
const HostBindingCompDeclaration = `
const $ff$ = function ($v$) { return ["red", $v$]; };
HostBindingComp.ngComponentDef = $r3$.ɵdefineComponent({
type: HostBindingComp,
selectors: [["host-binding-comp"]],
factory: function HostBindingComp_Factory(t) { return new (t || HostBindingComp)(); },
hostBindings: function HostBindingComp_HostBindings(dirIndex, elIndex) {
$r3$.ɵelementProperty(elIndex, "id", $r3$.ɵbind($r3$.ɵpureFunction1(1, $ff$, $r3$.ɵload(dirIndex).id)));
},
hostVars: 3,
consts: 0,
vars: 0,
template: function HostBindingComp_Template(rf, ctx) {}
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, HostBindingCompDeclaration, 'Invalid host binding code');
});
it('should not treat ElementRef, ViewContainerRef, or ChangeDetectorRef specially when injecting', it('should not treat ElementRef, ViewContainerRef, or ChangeDetectorRef specially when injecting',
() => { () => {
const files = { const files = {

View File

@ -122,6 +122,166 @@ describe('compiler compliance: bindings', () => {
}); });
}); });
describe('host bindings', () => {
it('should support host bindings', () => {
const files = {
app: {
'spec.ts': `
import {Directive, HostBinding, NgModule} from '@angular/core';
@Directive({selector: '[hostBindingDir]'})
export class HostBindingDir {
@HostBinding('id') dirId = 'some id';
}
@NgModule({declarations: [HostBindingDir]})
export class MyModule {}
`
}
};
const HostBindingDirDeclaration = `
HostBindingDir.ngDirectiveDef = $r3$.ɵdefineDirective({
type: HostBindingDir,
selectors: [["", "hostBindingDir", ""]],
factory: function HostBindingDir_Factory(t) { return new (t || HostBindingDir)(); },
hostBindings: function HostBindingDir_HostBindings(dirIndex, elIndex) {
$r3$.ɵelementProperty(elIndex, "id", $r3$.ɵbind($r3$.ɵload(dirIndex).dirId));
},
hostVars: 1
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, HostBindingDirDeclaration, 'Invalid host binding code');
});
it('should support host bindings with pure functions', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'host-binding-comp',
host: {
'[id]': '["red", id]'
},
template: ''
})
export class HostBindingComp {
id = 'some id';
}
@NgModule({declarations: [HostBindingComp]})
export class MyModule {}
`
}
};
const HostBindingCompDeclaration = `
const $ff$ = function ($v$) { return ["red", $v$]; };
HostBindingComp.ngComponentDef = $r3$.ɵdefineComponent({
type: HostBindingComp,
selectors: [["host-binding-comp"]],
factory: function HostBindingComp_Factory(t) { return new (t || HostBindingComp)(); },
hostBindings: function HostBindingComp_HostBindings(dirIndex, elIndex) {
$r3$.ɵelementProperty(elIndex, "id", $r3$.ɵbind($r3$.ɵpureFunction1(1, $ff$, $r3$.ɵload(dirIndex).id)));
},
hostVars: 3,
consts: 0,
vars: 0,
template: function HostBindingComp_Template(rf, ctx) {}
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, HostBindingCompDeclaration, 'Invalid host binding code');
});
it('should support host attribute bindings', () => {
const files = {
app: {
'spec.ts': `
import {Directive, NgModule} from '@angular/core';
@Directive({
selector: '[hostAttributeDir]',
host: {
'[attr.required]': 'required'
}
})
export class HostAttributeDir {
required = true;
}
@NgModule({declarations: [HostAttributeDir]})
export class MyModule {}
`
}
};
const HostAttributeDirDeclaration = `
HostAttributeDir.ngDirectiveDef = $r3$.ɵdefineDirective({
type: HostAttributeDir,
selectors: [["", "hostAttributeDir", ""]],
factory: function HostAttributeDir_Factory(t) { return new (t || HostAttributeDir)(); },
hostBindings: function HostAttributeDir_HostBindings(dirIndex, elIndex) {
$r3$.ɵelementAttribute(elIndex, "required", $r3$.ɵbind($r3$.ɵload(dirIndex).required));
},
hostVars: 1
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, HostAttributeDirDeclaration, 'Invalid host attribute code');
});
it('should support host attributes', () => {
const files = {
app: {
'spec.ts': `
import {Directive, NgModule} from '@angular/core';
@Directive({
selector: '[hostAttributeDir]',
host: {
'aria-label': 'label'
}
})
export class HostAttributeDir {
}
@NgModule({declarations: [HostAttributeDir]})
export class MyModule {}
`
}
};
const HostAttributeDirDeclaration = `
HostAttributeDir.ngDirectiveDef = $r3$.ɵdefineDirective({
type: HostAttributeDir,
selectors: [["", "hostAttributeDir", ""]],
factory: function HostAttributeDir_Factory(t) { return new (t || HostAttributeDir)(); },
attributes: ["aria-label", "label"]
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, HostAttributeDirDeclaration, 'Invalid host attribute code');
});
});
describe('non bindable behavior', () => { describe('non bindable behavior', () => {
const getAppFiles = (template: string = ''): MockDirectory => ({ const getAppFiles = (template: string = ''): MockDirectory => ({
app: { app: {

View File

@ -477,7 +477,7 @@ describe('ngtsc behavioral tests', () => {
env.driveMain(); env.driveMain();
const jsContents = env.getContents('test.js'); const jsContents = env.getContents('test.js');
expect(jsContents) expect(jsContents)
.toContain(`i0.ɵelementProperty(elIndex, "attr.hello", i0.ɵbind(i0.ɵload(dirIndex).foo));`); .toContain(`i0.ɵelementAttribute(elIndex, "hello", i0.ɵbind(i0.ɵload(dirIndex).foo));`);
expect(jsContents) expect(jsContents)
.toContain(`i0.ɵelementProperty(elIndex, "prop", i0.ɵbind(i0.ɵload(dirIndex).bar));`); .toContain(`i0.ɵelementProperty(elIndex, "prop", i0.ɵbind(i0.ɵload(dirIndex).bar));`);
expect(jsContents) expect(jsContents)

View File

@ -32,6 +32,10 @@ import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, co
const EMPTY_ARRAY: any[] = []; const EMPTY_ARRAY: any[] = [];
// This regex matches any binding names that contain the "attr." prefix, e.g. "attr.required"
// If there is a match, the first matching group will contain the attribute name to bind.
const ATTR_REGEX = /attr\.([^\]]+)/;
function baseDirectiveFields( function baseDirectiveFields(
meta: R3DirectiveMetadata, constantPool: ConstantPool, meta: R3DirectiveMetadata, constantPool: ConstantPool,
bindingParser: BindingParser): {definitionMap: DefinitionMap, statements: o.Statement[]} { bindingParser: BindingParser): {definitionMap: DefinitionMap, statements: o.Statement[]} {
@ -625,11 +629,14 @@ function createHostBindingsFunction(
const bindingExpr = convertPropertyBinding( const bindingExpr = convertPropertyBinding(
null, bindingContext, value, 'b', BindingForm.TrySimple, null, bindingContext, value, 'b', BindingForm.TrySimple,
() => error('Unexpected interpolation')); () => error('Unexpected interpolation'));
const {bindingName, instruction} = getBindingNameAndInstruction(binding.name);
statements.push(...bindingExpr.stmts); statements.push(...bindingExpr.stmts);
statements.push(o.importExpr(R3.elementProperty) statements.push(o.importExpr(instruction)
.callFn([ .callFn([
o.variable('elIndex'), o.variable('elIndex'),
o.literal(binding.name), o.literal(bindingName),
o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]), o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]),
]) ])
.toStmt()); .toStmt());
@ -649,6 +656,22 @@ function createHostBindingsFunction(
return null; return null;
} }
function getBindingNameAndInstruction(bindingName: string):
{bindingName: string, instruction: o.ExternalReference} {
let instruction !: o.ExternalReference;
// Check to see if this is an attr binding or a property binding
const attrMatches = bindingName.match(ATTR_REGEX);
if (attrMatches) {
bindingName = attrMatches[1];
instruction = R3.elementAttribute;
} else {
instruction = R3.elementProperty;
}
return {bindingName, instruction};
}
function createFactoryExtraStatementsFn(meta: R3DirectiveMetadata, bindingParser: BindingParser): function createFactoryExtraStatementsFn(meta: R3DirectiveMetadata, bindingParser: BindingParser):
((instance: o.Expression) => o.Statement[])|null { ((instance: o.Expression) => o.Statement[])|null {
const eventBindings = const eventBindings =
@ -698,8 +721,8 @@ const HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))|(\@[-\w]+)$/;
// Represents the groups in the above regex. // Represents the groups in the above regex.
const enum HostBindingGroup { const enum HostBindingGroup {
// group 1: "prop" from "[prop]" // group 1: "prop" from "[prop]", or "attr.role" from "[attr.role]"
Property = 1, Binding = 1,
// group 2: "event" from "(event)" // group 2: "event" from "(event)"
Event = 2, Event = 2,
@ -724,8 +747,8 @@ export function parseHostBindings(host: {[key: string]: string}): {
const matches = key.match(HOST_REG_EXP); const matches = key.match(HOST_REG_EXP);
if (matches === null) { if (matches === null) {
attributes[key] = value; attributes[key] = value;
} else if (matches[HostBindingGroup.Property] != null) { } else if (matches[HostBindingGroup.Binding] != null) {
properties[matches[HostBindingGroup.Property]] = value; properties[matches[HostBindingGroup.Binding]] = value;
} else if (matches[HostBindingGroup.Event] != null) { } else if (matches[HostBindingGroup.Event] != null) {
listeners[matches[HostBindingGroup.Event]] = value; listeners[matches[HostBindingGroup.Event]] = value;
} else if (matches[HostBindingGroup.Animation] != null) { } else if (matches[HostBindingGroup.Animation] != null) {

View File

@ -622,4 +622,28 @@ describe('host bindings', () => {
expect(hostElement.title).toBe('other title'); expect(hostElement.title).toBe('other title');
}); });
it('should support host attributes', () => {
// host: {
// 'role': 'listbox'
// }
class HostAttributeDir {
static ngDirectiveDef = defineDirective({
selectors: [['', 'hostAttributeDir', '']],
type: HostAttributeDir,
factory: () => new HostAttributeDir(),
attributes: ['role', 'listbox']
});
}
// <div hostAttributeDir></div>
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'div', ['hostAttributeDir', '']);
}
}, 1, 0, [HostAttributeDir]);
const fixture = new ComponentFixture(App);
expect(fixture.html).toEqual(`<div hostattributedir="" role="listbox"></div>`);
});
}); });

View File

@ -1918,81 +1918,80 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec';
expect(form.valid).toEqual(true); expect(form.valid).toEqual(true);
}); });
fixmeIvy('host attribute instructions are not generated properly') && it('changes on bound properties should change the validation state of the form', () => {
it('changes on bound properties should change the validation state of the form', () => { const fixture = initTest(ValidationBindingsForm);
const fixture = initTest(ValidationBindingsForm); const form = new FormGroup({
const form = new FormGroup({ 'login': new FormControl(''),
'login': new FormControl(''), 'min': new FormControl(''),
'min': new FormControl(''), 'max': new FormControl(''),
'max': new FormControl(''), 'pattern': new FormControl('')
'pattern': new FormControl('') });
}); fixture.componentInstance.form = form;
fixture.componentInstance.form = form; fixture.detectChanges();
fixture.detectChanges();
const required = fixture.debugElement.query(By.css('[name=required]')); const required = fixture.debugElement.query(By.css('[name=required]'));
const minLength = fixture.debugElement.query(By.css('[name=minlength]')); const minLength = fixture.debugElement.query(By.css('[name=minlength]'));
const maxLength = fixture.debugElement.query(By.css('[name=maxlength]')); const maxLength = fixture.debugElement.query(By.css('[name=maxlength]'));
const pattern = fixture.debugElement.query(By.css('[name=pattern]')); const pattern = fixture.debugElement.query(By.css('[name=pattern]'));
required.nativeElement.value = ''; required.nativeElement.value = '';
minLength.nativeElement.value = '1'; minLength.nativeElement.value = '1';
maxLength.nativeElement.value = '1234'; maxLength.nativeElement.value = '1234';
pattern.nativeElement.value = '12'; pattern.nativeElement.value = '12';
dispatchEvent(required.nativeElement, 'input'); dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input'); dispatchEvent(pattern.nativeElement, 'input');
expect(form.hasError('required', ['login'])).toEqual(false); expect(form.hasError('required', ['login'])).toEqual(false);
expect(form.hasError('minlength', ['min'])).toEqual(false); expect(form.hasError('minlength', ['min'])).toEqual(false);
expect(form.hasError('maxlength', ['max'])).toEqual(false); expect(form.hasError('maxlength', ['max'])).toEqual(false);
expect(form.hasError('pattern', ['pattern'])).toEqual(false); expect(form.hasError('pattern', ['pattern'])).toEqual(false);
expect(form.valid).toEqual(true); expect(form.valid).toEqual(true);
fixture.componentInstance.required = true; fixture.componentInstance.required = true;
fixture.componentInstance.minLen = 3; fixture.componentInstance.minLen = 3;
fixture.componentInstance.maxLen = 3; fixture.componentInstance.maxLen = 3;
fixture.componentInstance.pattern = '.{3,}'; fixture.componentInstance.pattern = '.{3,}';
fixture.detectChanges(); fixture.detectChanges();
dispatchEvent(required.nativeElement, 'input'); dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input'); dispatchEvent(pattern.nativeElement, 'input');
expect(form.hasError('required', ['login'])).toEqual(true); expect(form.hasError('required', ['login'])).toEqual(true);
expect(form.hasError('minlength', ['min'])).toEqual(true); expect(form.hasError('minlength', ['min'])).toEqual(true);
expect(form.hasError('maxlength', ['max'])).toEqual(true); expect(form.hasError('maxlength', ['max'])).toEqual(true);
expect(form.hasError('pattern', ['pattern'])).toEqual(true); expect(form.hasError('pattern', ['pattern'])).toEqual(true);
expect(form.valid).toEqual(false); expect(form.valid).toEqual(false);
expect(required.nativeElement.getAttribute('required')).toEqual(''); expect(required.nativeElement.getAttribute('required')).toEqual('');
expect(fixture.componentInstance.minLen.toString()) expect(fixture.componentInstance.minLen.toString())
.toEqual(minLength.nativeElement.getAttribute('minlength')); .toEqual(minLength.nativeElement.getAttribute('minlength'));
expect(fixture.componentInstance.maxLen.toString()) expect(fixture.componentInstance.maxLen.toString())
.toEqual(maxLength.nativeElement.getAttribute('maxlength')); .toEqual(maxLength.nativeElement.getAttribute('maxlength'));
expect(fixture.componentInstance.pattern.toString()) expect(fixture.componentInstance.pattern.toString())
.toEqual(pattern.nativeElement.getAttribute('pattern')); .toEqual(pattern.nativeElement.getAttribute('pattern'));
fixture.componentInstance.required = false; fixture.componentInstance.required = false;
fixture.componentInstance.minLen = null !; fixture.componentInstance.minLen = null !;
fixture.componentInstance.maxLen = null !; fixture.componentInstance.maxLen = null !;
fixture.componentInstance.pattern = null !; fixture.componentInstance.pattern = null !;
fixture.detectChanges(); fixture.detectChanges();
expect(form.hasError('required', ['login'])).toEqual(false); expect(form.hasError('required', ['login'])).toEqual(false);
expect(form.hasError('minlength', ['min'])).toEqual(false); expect(form.hasError('minlength', ['min'])).toEqual(false);
expect(form.hasError('maxlength', ['max'])).toEqual(false); expect(form.hasError('maxlength', ['max'])).toEqual(false);
expect(form.hasError('pattern', ['pattern'])).toEqual(false); expect(form.hasError('pattern', ['pattern'])).toEqual(false);
expect(form.valid).toEqual(true); expect(form.valid).toEqual(true);
expect(required.nativeElement.getAttribute('required')).toEqual(null); expect(required.nativeElement.getAttribute('required')).toEqual(null);
expect(required.nativeElement.getAttribute('minlength')).toEqual(null); expect(required.nativeElement.getAttribute('minlength')).toEqual(null);
expect(required.nativeElement.getAttribute('maxlength')).toEqual(null); expect(required.nativeElement.getAttribute('maxlength')).toEqual(null);
expect(required.nativeElement.getAttribute('pattern')).toEqual(null); expect(required.nativeElement.getAttribute('pattern')).toEqual(null);
}); });
it('should support rebound controls with rebound validators', () => { it('should support rebound controls with rebound validators', () => {
const fixture = initTest(ValidationBindingsForm); const fixture = initTest(ValidationBindingsForm);

View File

@ -1420,77 +1420,76 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
expect(form.control.hasError('minlength', ['tovalidate'])).toBeTruthy(); expect(form.control.hasError('minlength', ['tovalidate'])).toBeTruthy();
})); }));
fixmeIvy('host attribute instructions are not generated properly') && it('changes on bound properties should change the validation state of the form',
it('changes on bound properties should change the validation state of the form', fakeAsync(() => {
fakeAsync(() => { const fixture = initTest(NgModelValidationBindings);
const fixture = initTest(NgModelValidationBindings); fixture.detectChanges();
fixture.detectChanges(); tick();
tick();
const required = fixture.debugElement.query(By.css('[name=required]')); const required = fixture.debugElement.query(By.css('[name=required]'));
const minLength = fixture.debugElement.query(By.css('[name=minlength]')); const minLength = fixture.debugElement.query(By.css('[name=minlength]'));
const maxLength = fixture.debugElement.query(By.css('[name=maxlength]')); const maxLength = fixture.debugElement.query(By.css('[name=maxlength]'));
const pattern = fixture.debugElement.query(By.css('[name=pattern]')); const pattern = fixture.debugElement.query(By.css('[name=pattern]'));
required.nativeElement.value = ''; required.nativeElement.value = '';
minLength.nativeElement.value = '1'; minLength.nativeElement.value = '1';
maxLength.nativeElement.value = '1234'; maxLength.nativeElement.value = '1234';
pattern.nativeElement.value = '12'; pattern.nativeElement.value = '12';
dispatchEvent(required.nativeElement, 'input'); dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input'); dispatchEvent(pattern.nativeElement, 'input');
const form = fixture.debugElement.children[0].injector.get(NgForm); const form = fixture.debugElement.children[0].injector.get(NgForm);
expect(form.control.hasError('required', ['required'])).toEqual(false); expect(form.control.hasError('required', ['required'])).toEqual(false);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(false); expect(form.control.hasError('minlength', ['minlength'])).toEqual(false);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(false); expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(false);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(false); expect(form.control.hasError('pattern', ['pattern'])).toEqual(false);
expect(form.valid).toEqual(true); expect(form.valid).toEqual(true);
fixture.componentInstance.required = true; fixture.componentInstance.required = true;
fixture.componentInstance.minLen = 3; fixture.componentInstance.minLen = 3;
fixture.componentInstance.maxLen = 3; fixture.componentInstance.maxLen = 3;
fixture.componentInstance.pattern = '.{3,}'; fixture.componentInstance.pattern = '.{3,}';
fixture.detectChanges(); fixture.detectChanges();
dispatchEvent(required.nativeElement, 'input'); dispatchEvent(required.nativeElement, 'input');
dispatchEvent(minLength.nativeElement, 'input'); dispatchEvent(minLength.nativeElement, 'input');
dispatchEvent(maxLength.nativeElement, 'input'); dispatchEvent(maxLength.nativeElement, 'input');
dispatchEvent(pattern.nativeElement, 'input'); dispatchEvent(pattern.nativeElement, 'input');
expect(form.control.hasError('required', ['required'])).toEqual(true); expect(form.control.hasError('required', ['required'])).toEqual(true);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(true); expect(form.control.hasError('minlength', ['minlength'])).toEqual(true);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(true); expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(true);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(true); expect(form.control.hasError('pattern', ['pattern'])).toEqual(true);
expect(form.valid).toEqual(false); expect(form.valid).toEqual(false);
expect(required.nativeElement.getAttribute('required')).toEqual(''); expect(required.nativeElement.getAttribute('required')).toEqual('');
expect(fixture.componentInstance.minLen.toString()) expect(fixture.componentInstance.minLen.toString())
.toEqual(minLength.nativeElement.getAttribute('minlength')); .toEqual(minLength.nativeElement.getAttribute('minlength'));
expect(fixture.componentInstance.maxLen.toString()) expect(fixture.componentInstance.maxLen.toString())
.toEqual(maxLength.nativeElement.getAttribute('maxlength')); .toEqual(maxLength.nativeElement.getAttribute('maxlength'));
expect(fixture.componentInstance.pattern.toString()) expect(fixture.componentInstance.pattern.toString())
.toEqual(pattern.nativeElement.getAttribute('pattern')); .toEqual(pattern.nativeElement.getAttribute('pattern'));
fixture.componentInstance.required = false; fixture.componentInstance.required = false;
fixture.componentInstance.minLen = null !; fixture.componentInstance.minLen = null !;
fixture.componentInstance.maxLen = null !; fixture.componentInstance.maxLen = null !;
fixture.componentInstance.pattern = null !; fixture.componentInstance.pattern = null !;
fixture.detectChanges(); fixture.detectChanges();
expect(form.control.hasError('required', ['required'])).toEqual(false); expect(form.control.hasError('required', ['required'])).toEqual(false);
expect(form.control.hasError('minlength', ['minlength'])).toEqual(false); expect(form.control.hasError('minlength', ['minlength'])).toEqual(false);
expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(false); expect(form.control.hasError('maxlength', ['maxlength'])).toEqual(false);
expect(form.control.hasError('pattern', ['pattern'])).toEqual(false); expect(form.control.hasError('pattern', ['pattern'])).toEqual(false);
expect(form.valid).toEqual(true); expect(form.valid).toEqual(true);
expect(required.nativeElement.getAttribute('required')).toEqual(null); expect(required.nativeElement.getAttribute('required')).toEqual(null);
expect(required.nativeElement.getAttribute('minlength')).toEqual(null); expect(required.nativeElement.getAttribute('minlength')).toEqual(null);
expect(required.nativeElement.getAttribute('maxlength')).toEqual(null); expect(required.nativeElement.getAttribute('maxlength')).toEqual(null);
expect(required.nativeElement.getAttribute('pattern')).toEqual(null); expect(required.nativeElement.getAttribute('pattern')).toEqual(null);
})); }));
fixmeIvy('ngModelChange callback never called') && fixmeIvy('ngModelChange callback never called') &&
it('should update control status', fakeAsync(() => { it('should update control status', fakeAsync(() => {