fix(ivy): support for #id bootstrap selectors (#33784)

Fixes: #33485

PR Close #33784
This commit is contained in:
Misko Hevery 2019-11-12 21:32:58 -08:00 committed by Alex Rickabaugh
parent c5a75fd807
commit ab0bcee144
4 changed files with 111 additions and 17 deletions

View File

@ -675,6 +675,35 @@ describe('compiler compliance', () => {
expectEmit(source, OtherDirectiveFactory, 'Incorrect OtherDirective.ɵfac');
});
it('should convert #my-app selector to ["", "id", "my-app"]', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({selector: '#my-app', template: ''})
export class SomeComponent {}
@NgModule({declarations: [SomeComponent]})
export class MyModule{}
`
}
};
// SomeDirective definition should be:
const SomeDirectiveDefinition = `
SomeComponent.ɵcmp = $r3$.ɵɵdefineComponent({
type: SomeComponent,
selectors: [["", "id", "my-app"]],
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, SomeDirectiveDefinition, 'Incorrect SomeComponent.ɵcomp');
});
it('should support components without selector', () => {
const files = {
app: {

View File

@ -9,17 +9,31 @@
import {getHtmlTagDefinition} from './ml_parser/html_tags';
const _SELECTOR_REGEXP = new RegExp(
'(\\:not\\()|' + //":not("
'([-\\w]+)|' + // "tag"
'(?:\\.([-\\w]+))|' + // ".class"
'(\\:not\\()|' + // 1: ":not("
'(([\\.\\#]?)[-\\w]+)|' + // 2: "tag"; 3: "."/"#";
// "-" should appear first in the regexp below as FF31 parses "[.-\w]" as a range
// 4: attribute; 5: attribute_string; 6: attribute_value
'(?:\\[([-.\\w*]+)(?:=([\"\']?)([^\\]\"\']*)\\5)?\\])|' + // "[name]", "[name=value]",
// "[name="value"]",
// "[name='value']"
'(\\))|' + // ")"
'(\\s*,\\s*)', // ","
'(\\))|' + // 7: ")"
'(\\s*,\\s*)', // 8: ","
'g');
/**
* These offsets should match the match-groups in `_SELECTOR_REGEXP` offsets.
*/
const enum SelectorRegexp {
ALL = 0, // The whole match
NOT = 1,
TAG = 2,
PREFIX = 3,
ATTRIBUTE = 4,
ATTRIBUTE_STRING = 5,
ATTRIBUTE_VALUE = 6,
NOT_END = 7,
SEPARATOR = 8,
}
/**
* A css selector contains an element name,
* css classes and attribute/value pairs with the purpose
@ -57,28 +71,37 @@ export class CssSelector {
let inNot = false;
_SELECTOR_REGEXP.lastIndex = 0;
while (match = _SELECTOR_REGEXP.exec(selector)) {
if (match[1]) {
if (match[SelectorRegexp.NOT]) {
if (inNot) {
throw new Error('Nesting :not is not allowed in a selector');
throw new Error('Nesting :not in a selector is not allowed');
}
inNot = true;
current = new CssSelector();
cssSelector.notSelectors.push(current);
}
if (match[2]) {
current.setElement(match[2]);
const tag = match[SelectorRegexp.TAG];
if (tag) {
const prefix = match[SelectorRegexp.PREFIX];
if (prefix === '#') {
// #hash
current.addAttribute('id', tag.substr(1));
} else if (prefix === '.') {
// Class
current.addClassName(tag.substr(1));
} else {
// Element
current.setElement(tag);
}
}
if (match[3]) {
current.addClassName(match[3]);
const attribute = match[SelectorRegexp.ATTRIBUTE];
if (attribute) {
current.addAttribute(attribute, match[SelectorRegexp.ATTRIBUTE_VALUE]);
}
if (match[4]) {
current.addAttribute(match[4], match[6]);
}
if (match[7]) {
if (match[SelectorRegexp.NOT_END]) {
inNot = false;
current = cssSelector;
}
if (match[8]) {
if (match[SelectorRegexp.SEPARATOR]) {
if (inNot) {
throw new Error('Multiple selectors in :not are not supported');
}

View File

@ -330,6 +330,12 @@ import {el} from '@angular/platform-browser/testing/src/browser_util';
expect(cssSelector.toString()).toEqual('[attrname=attrvalue]');
});
it('should detect #some-value syntax and treat as attribute', () => {
const cssSelector = CssSelector.parse('#some-value')[0];
expect(cssSelector.attrs).toEqual(['id', 'some-value']);
expect(cssSelector.toString()).toEqual('[id=some-value]');
});
it('should detect attr values with single quotes', () => {
const cssSelector = CssSelector.parse('[attrname=\'attrvalue\']')[0];
expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']);
@ -381,7 +387,7 @@ import {el} from '@angular/platform-browser/testing/src/browser_util';
it('should throw when nested :not', () => {
expect(() => {
CssSelector.parse('sometag:not(:not([attrname=attrvalue].someclass))')[0];
}).toThrowError('Nesting :not is not allowed in a selector');
}).toThrowError('Nesting :not in a selector is not allowed');
});
it('should throw when multiple selectors in :not', () => {

View File

@ -0,0 +1,36 @@
/**
* @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 {Component, NgModule} from '@angular/core';
import {getComponentDef} from '@angular/core/src/render3/definition';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {onlyInIvy, withBody} from '@angular/private/testing';
describe('bootstrap', () => {
it('should bootstrap using #id selector', withBody('<div #my-app>', async() => {
try {
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(MyAppModule);
expect(document.body.textContent).toEqual('works!');
ngModuleRef.destroy();
} catch (err) {
console.error(err);
}
}));
});
@Component({
selector: '#my-app',
template: 'works!',
})
export class MyAppComponent {
}
@NgModule({imports: [BrowserModule], declarations: [MyAppComponent], bootstrap: [MyAppComponent]})
export class MyAppModule {
}