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'); 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', () => { it('should support components without selector', () => {
const files = { const files = {
app: { app: {

View File

@ -9,17 +9,31 @@
import {getHtmlTagDefinition} from './ml_parser/html_tags'; import {getHtmlTagDefinition} from './ml_parser/html_tags';
const _SELECTOR_REGEXP = new RegExp( const _SELECTOR_REGEXP = new RegExp(
'(\\:not\\()|' + //":not(" '(\\:not\\()|' + // 1: ":not("
'([-\\w]+)|' + // "tag" '(([\\.\\#]?)[-\\w]+)|' + // 2: "tag"; 3: "."/"#";
'(?:\\.([-\\w]+))|' + // ".class"
// "-" should appear first in the regexp below as FF31 parses "[.-\w]" as a range // "-" 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]", '(?:\\[([-.\\w*]+)(?:=([\"\']?)([^\\]\"\']*)\\5)?\\])|' + // "[name]", "[name=value]",
// "[name="value"]", // "[name="value"]",
// "[name='value']" // "[name='value']"
'(\\))|' + // ")" '(\\))|' + // 7: ")"
'(\\s*,\\s*)', // "," '(\\s*,\\s*)', // 8: ","
'g'); '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, * A css selector contains an element name,
* css classes and attribute/value pairs with the purpose * css classes and attribute/value pairs with the purpose
@ -57,28 +71,37 @@ export class CssSelector {
let inNot = false; let inNot = false;
_SELECTOR_REGEXP.lastIndex = 0; _SELECTOR_REGEXP.lastIndex = 0;
while (match = _SELECTOR_REGEXP.exec(selector)) { while (match = _SELECTOR_REGEXP.exec(selector)) {
if (match[1]) { if (match[SelectorRegexp.NOT]) {
if (inNot) { 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; inNot = true;
current = new CssSelector(); current = new CssSelector();
cssSelector.notSelectors.push(current); cssSelector.notSelectors.push(current);
} }
if (match[2]) { const tag = match[SelectorRegexp.TAG];
current.setElement(match[2]); 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]) { const attribute = match[SelectorRegexp.ATTRIBUTE];
current.addClassName(match[3]); if (attribute) {
current.addAttribute(attribute, match[SelectorRegexp.ATTRIBUTE_VALUE]);
} }
if (match[4]) { if (match[SelectorRegexp.NOT_END]) {
current.addAttribute(match[4], match[6]);
}
if (match[7]) {
inNot = false; inNot = false;
current = cssSelector; current = cssSelector;
} }
if (match[8]) { if (match[SelectorRegexp.SEPARATOR]) {
if (inNot) { if (inNot) {
throw new Error('Multiple selectors in :not are not supported'); 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]'); 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', () => { it('should detect attr values with single quotes', () => {
const cssSelector = CssSelector.parse('[attrname=\'attrvalue\']')[0]; const cssSelector = CssSelector.parse('[attrname=\'attrvalue\']')[0];
expect(cssSelector.attrs).toEqual(['attrname', 'attrvalue']); 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', () => { it('should throw when nested :not', () => {
expect(() => { expect(() => {
CssSelector.parse('sometag:not(:not([attrname=attrvalue].someclass))')[0]; 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', () => { 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 {
}