fix(ivy): add attributes and classes to host elements based on selector (#34481)

In View Engine, host element of dynamically created component received attributes and classes extracted from component's selector. For example, if component selector is `[attr] .class`, the `attr` attribute and `.class` class will be add to host element. This commit adds similar logic to Ivy, to make sure this behavior is aligned with View Engine.

PR Close #34481
This commit is contained in:
Andrew Kushnir
2019-12-18 16:52:32 -08:00
committed by Alex Rickabaugh
parent 3f4e02b8c7
commit f95b8ce07e
5 changed files with 206 additions and 16 deletions

View File

@ -8,7 +8,7 @@
import {CommonModule, DOCUMENT} from '@angular/common';
import {computeMsgId} from '@angular/compiler';
import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, NO_ERRORS_SCHEMA, NgModule, OnInit, Pipe, PipeTransform, QueryList, Renderer2, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core';
import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, Injector, NO_ERRORS_SCHEMA, NgModule, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core';
import {Input} from '@angular/core/src/metadata';
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
import {TestBed, TestComponentRenderer} from '@angular/core/testing';
@ -251,6 +251,98 @@ describe('ViewContainerRef', () => {
// Also test with selector that has element name in uppercase
runTestWithSelectors('SVG[some-attr]', 'MATH[some-attr]');
});
it('should apply attributes and classes to host element based on selector', () => {
@Component({
selector: '[attr-a=a].class-a:not(.class-b):not([attr-b=b]).class-c[attr-c]',
template: 'Hello'
})
class HelloComp {
}
@NgModule({entryComponents: [HelloComp], declarations: [HelloComp]})
class HelloCompModule {
}
@Component({
template: `
<div id="factory" attr-a="a-original" class="class-original"></div>
<div id="vcr">
<ng-container #container></ng-container>
</div>
`
})
class TestComp {
@ViewChild('container', {read: ViewContainerRef}) vcRef !: ViewContainerRef;
private helloCompFactory = this.cfr.resolveComponentFactory(HelloComp);
constructor(public cfr: ComponentFactoryResolver, public injector: Injector) {}
createComponentViaVCRef() {
this.vcRef.createComponent(this.helloCompFactory); //
}
createComponentViaFactory() {
this.helloCompFactory.create(this.injector, undefined, '#factory');
}
}
TestBed.configureTestingModule({declarations: [TestComp], imports: [HelloCompModule]});
const fixture = TestBed.createComponent(TestComp);
fixture.detectChanges();
fixture.componentInstance.createComponentViaVCRef();
fixture.componentInstance.createComponentViaFactory();
fixture.detectChanges();
// Verify host element for a component created via `vcRef.createComponent` method
const vcrHostElement = fixture.nativeElement.querySelector('#vcr > div');
expect(vcrHostElement.classList.contains('class-a')).toBe(true);
// `class-b` should not be present, since it's wrapped in `:not()` selector
expect(vcrHostElement.classList.contains('class-b')).toBe(false);
expect(vcrHostElement.classList.contains('class-c')).toBe(true);
expect(vcrHostElement.getAttribute('attr-a')).toBe('a');
// `attr-b` should not be present, since it's wrapped in `:not()` selector
expect(vcrHostElement.getAttribute('attr-b')).toBe(null);
expect(vcrHostElement.getAttribute('attr-c')).toBe('');
// Verify host element for a component created using `factory.createComponent` method when
// also passing element selector as an argument
const factoryHostElement = fixture.nativeElement.querySelector('#factory');
if (ivyEnabled) {
// In Ivy, if selector is passed when component is created, matched host node (found using
// this selector) retains all attrs/classes and selector-based attrs/classes should *not* be
// added
// Verify original attrs and classes are still present
expect(factoryHostElement.classList.contains('class-original')).toBe(true);
expect(factoryHostElement.getAttribute('attr-a')).toBe('a-original');
// Make sure selector-based attrs and classes were not added to the host element
expect(factoryHostElement.classList.contains('class-a')).toBe(false);
expect(factoryHostElement.getAttribute('attr-c')).toBe(null);
} else {
// In View Engine, selector-based attrs/classes are *always* added to the host element
expect(factoryHostElement.classList.contains('class-a')).toBe(true);
// `class-b` should not be present, since it's wrapped in `:not()` selector
expect(factoryHostElement.classList.contains('class-b')).toBe(false);
expect(factoryHostElement.classList.contains('class-c')).toBe(true);
// Make sure classes are overridden with ones used in component selector
expect(factoryHostElement.classList.contains('class-original')).toBe(false);
// Note: `attr-a` attr is also present on host element, but we update the value with the
// value from component selector (i.e. using `[attr-a=a]`)
expect(factoryHostElement.getAttribute('attr-a')).toBe('a');
// `attr-b` should not be present, since it's wrapped in `:not()` selector
expect(factoryHostElement.getAttribute('attr-b')).toBe(null);
expect(factoryHostElement.getAttribute('attr-c')).toBe('');
}
});
});
describe('insert', () => {