diff --git a/packages/examples/upgrade/static/ts/module.ts b/packages/examples/upgrade/static/ts/module.ts
index 0f6a83f831..6c6d0aeb6f 100644
--- a/packages/examples/upgrade/static/ts/module.ts
+++ b/packages/examples/upgrade/static/ts/module.ts
@@ -22,8 +22,8 @@ interface Hero {
selector: 'ng2-heroes',
// This template uses the upgraded `ng1-hero` component
// Note that because its element is compiled by Angular we must use camelCased attribute names
- template: `
Heroes
-
+ template: `
+
Super Hero
@@ -167,7 +167,8 @@ ng1AppModule.component('exampleApp', {
// inputs and outputs
template: `
- There are {{ $ctrl.heroesService.heroes.length }} heroes.
+
Heroes
+
There are {{ $ctrl.heroesService.heroes.length }} heroes.
`
});
// #enddocregion
diff --git a/packages/upgrade/src/common/angular1.ts b/packages/upgrade/src/common/angular1.ts
index f8fbe433cb..267e8dbac8 100644
--- a/packages/upgrade/src/common/angular1.ts
+++ b/packages/upgrade/src/common/angular1.ts
@@ -205,7 +205,7 @@ let angular: {
bootstrap: (e: Element, modules: (string | IInjectable)[], config: IAngularBootstrapConfig) =>
void,
module: (prefix: string, dependencies?: string[]) => IModule,
- element: (e: Element) => IAugmentedJQuery,
+ element: (e: Element | string) => IAugmentedJQuery,
version: {major: number}, resumeBootstrap?: () => void,
getTestability: (e: Element) => ITestabilityService
} = {
diff --git a/packages/upgrade/src/common/compiler_helpers/README.md b/packages/upgrade/src/common/compiler_helpers/README.md
new file mode 100644
index 0000000000..7c53b572b3
--- /dev/null
+++ b/packages/upgrade/src/common/compiler_helpers/README.md
@@ -0,0 +1,7 @@
+The following code has been copied from the Angular compiler to be used in the upgrade library without
+the need to import the entire compiler:
+
+* `selector.ts`
+* `ml_parser/html_tags.ts`
+* `ml_parser/tags.ts`
+* `createElementCssSelector.ts`
\ No newline at end of file
diff --git a/packages/upgrade/src/common/compiler_helpers/createElementCssSelector.ts b/packages/upgrade/src/common/compiler_helpers/createElementCssSelector.ts
new file mode 100644
index 0000000000..e964ac5b27
--- /dev/null
+++ b/packages/upgrade/src/common/compiler_helpers/createElementCssSelector.ts
@@ -0,0 +1,55 @@
+/**
+ * @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 {CssSelector} from './selector';
+
+/*
+ * The following items are copied from the Angular Compiler to be used here
+ * without the need to import the entire compiler into the build
+ */
+
+const CLASS_ATTR = 'class';
+
+export function createElementCssSelector(
+ elementName: string, attributes: [string, string][]): CssSelector {
+ const cssSelector = new CssSelector();
+ const elNameNoNs = splitNsName(elementName)[1];
+
+ cssSelector.setElement(elNameNoNs);
+
+ for (let i = 0; i < attributes.length; i++) {
+ const attrName = attributes[i][0];
+ const attrNameNoNs = splitNsName(attrName)[1];
+ const attrValue = attributes[i][1];
+
+ cssSelector.addAttribute(attrNameNoNs, attrValue);
+ if (attrName.toLowerCase() == CLASS_ATTR) {
+ const classes = splitClasses(attrValue);
+ classes.forEach(className => cssSelector.addClassName(className));
+ }
+ }
+ return cssSelector;
+}
+
+export function splitNsName(elementName: string): [string, string] {
+ if (elementName[0] != ':') {
+ return [null, elementName];
+ }
+
+ const colonIndex = elementName.indexOf(':', 1);
+
+ if (colonIndex == -1) {
+ throw new Error(`Unsupported format "${elementName}" expecting ":namespace:name"`);
+ }
+
+ return [elementName.slice(1, colonIndex), elementName.slice(colonIndex + 1)];
+}
+
+export function splitClasses(classAttrValue: string): string[] {
+ return classAttrValue.trim().split(/\s+/g);
+}
diff --git a/packages/upgrade/src/common/compiler_helpers/ml_parser/html_tags.ts b/packages/upgrade/src/common/compiler_helpers/ml_parser/html_tags.ts
new file mode 100644
index 0000000000..65a3cc8f6b
--- /dev/null
+++ b/packages/upgrade/src/common/compiler_helpers/ml_parser/html_tags.ts
@@ -0,0 +1,129 @@
+/**
+ * @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 {TagContentType, TagDefinition} from './tags';
+
+export class HtmlTagDefinition implements TagDefinition {
+ private closedByChildren: {[key: string]: boolean} = {};
+
+ closedByParent: boolean = false;
+ requiredParents: {[key: string]: boolean};
+ parentToAdd: string;
+ implicitNamespacePrefix: string;
+ contentType: TagContentType;
+ isVoid: boolean;
+ ignoreFirstLf: boolean;
+ canSelfClose: boolean = false;
+
+ constructor(
+ {closedByChildren, requiredParents, implicitNamespacePrefix,
+ contentType = TagContentType.PARSABLE_DATA, closedByParent = false, isVoid = false,
+ ignoreFirstLf = false}: {
+ closedByChildren?: string[],
+ closedByParent?: boolean,
+ requiredParents?: string[],
+ implicitNamespacePrefix?: string,
+ contentType?: TagContentType,
+ isVoid?: boolean,
+ ignoreFirstLf?: boolean
+ } = {}) {
+ if (closedByChildren && closedByChildren.length > 0) {
+ closedByChildren.forEach(tagName => this.closedByChildren[tagName] = true);
+ }
+ this.isVoid = isVoid;
+ this.closedByParent = closedByParent || isVoid;
+ if (requiredParents && requiredParents.length > 0) {
+ this.requiredParents = {};
+ // The first parent is the list is automatically when none of the listed parents are present
+ this.parentToAdd = requiredParents[0];
+ requiredParents.forEach(tagName => this.requiredParents[tagName] = true);
+ }
+ this.implicitNamespacePrefix = implicitNamespacePrefix;
+ this.contentType = contentType;
+ this.ignoreFirstLf = ignoreFirstLf;
+ }
+
+ requireExtraParent(currentParent: string): boolean {
+ if (!this.requiredParents) {
+ return false;
+ }
+
+ if (!currentParent) {
+ return true;
+ }
+
+ const lcParent = currentParent.toLowerCase();
+ const isParentTemplate = lcParent === 'template' || currentParent === 'ng-template';
+ return !isParentTemplate && this.requiredParents[lcParent] != true;
+ }
+
+ isClosedByChild(name: string): boolean {
+ return this.isVoid || name.toLowerCase() in this.closedByChildren;
+ }
+}
+
+// see http://www.w3.org/TR/html51/syntax.html#optional-tags
+// This implementation does not fully conform to the HTML5 spec.
+const TAG_DEFINITIONS: {[key: string]: HtmlTagDefinition} = {
+ 'base': new HtmlTagDefinition({isVoid: true}),
+ 'meta': new HtmlTagDefinition({isVoid: true}),
+ 'area': new HtmlTagDefinition({isVoid: true}),
+ 'embed': new HtmlTagDefinition({isVoid: true}),
+ 'link': new HtmlTagDefinition({isVoid: true}),
+ 'img': new HtmlTagDefinition({isVoid: true}),
+ 'input': new HtmlTagDefinition({isVoid: true}),
+ 'param': new HtmlTagDefinition({isVoid: true}),
+ 'hr': new HtmlTagDefinition({isVoid: true}),
+ 'br': new HtmlTagDefinition({isVoid: true}),
+ 'source': new HtmlTagDefinition({isVoid: true}),
+ 'track': new HtmlTagDefinition({isVoid: true}),
+ 'wbr': new HtmlTagDefinition({isVoid: true}),
+ 'p': new HtmlTagDefinition({
+ closedByChildren: [
+ 'address', 'article', 'aside', 'blockquote', 'div', 'dl', 'fieldset', 'footer', 'form',
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr',
+ 'main', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'
+ ],
+ closedByParent: true
+ }),
+ 'thead': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot']}),
+ 'tbody': new HtmlTagDefinition({closedByChildren: ['tbody', 'tfoot'], closedByParent: true}),
+ 'tfoot': new HtmlTagDefinition({closedByChildren: ['tbody'], closedByParent: true}),
+ 'tr': new HtmlTagDefinition({
+ closedByChildren: ['tr'],
+ requiredParents: ['tbody', 'tfoot', 'thead'],
+ closedByParent: true
+ }),
+ 'td': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
+ 'th': new HtmlTagDefinition({closedByChildren: ['td', 'th'], closedByParent: true}),
+ 'col': new HtmlTagDefinition({requiredParents: ['colgroup'], isVoid: true}),
+ 'svg': new HtmlTagDefinition({implicitNamespacePrefix: 'svg'}),
+ 'math': new HtmlTagDefinition({implicitNamespacePrefix: 'math'}),
+ 'li': new HtmlTagDefinition({closedByChildren: ['li'], closedByParent: true}),
+ 'dt': new HtmlTagDefinition({closedByChildren: ['dt', 'dd']}),
+ 'dd': new HtmlTagDefinition({closedByChildren: ['dt', 'dd'], closedByParent: true}),
+ 'rb': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
+ 'rt': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
+ 'rtc': new HtmlTagDefinition({closedByChildren: ['rb', 'rtc', 'rp'], closedByParent: true}),
+ 'rp': new HtmlTagDefinition({closedByChildren: ['rb', 'rt', 'rtc', 'rp'], closedByParent: true}),
+ 'optgroup': new HtmlTagDefinition({closedByChildren: ['optgroup'], closedByParent: true}),
+ 'option': new HtmlTagDefinition({closedByChildren: ['option', 'optgroup'], closedByParent: true}),
+ 'pre': new HtmlTagDefinition({ignoreFirstLf: true}),
+ 'listing': new HtmlTagDefinition({ignoreFirstLf: true}),
+ 'style': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}),
+ 'script': new HtmlTagDefinition({contentType: TagContentType.RAW_TEXT}),
+ 'title': new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT}),
+ 'textarea':
+ new HtmlTagDefinition({contentType: TagContentType.ESCAPABLE_RAW_TEXT, ignoreFirstLf: true}),
+};
+
+const _DEFAULT_TAG_DEFINITION = new HtmlTagDefinition();
+
+export function getHtmlTagDefinition(tagName: string): HtmlTagDefinition {
+ return TAG_DEFINITIONS[tagName.toLowerCase()] || _DEFAULT_TAG_DEFINITION;
+}
\ No newline at end of file
diff --git a/packages/upgrade/src/common/compiler_helpers/ml_parser/tags.ts b/packages/upgrade/src/common/compiler_helpers/ml_parser/tags.ts
new file mode 100644
index 0000000000..f83c8fd848
--- /dev/null
+++ b/packages/upgrade/src/common/compiler_helpers/ml_parser/tags.ts
@@ -0,0 +1,310 @@
+/**
+ * @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
+ */
+
+export enum TagContentType {
+ RAW_TEXT,
+ ESCAPABLE_RAW_TEXT,
+ PARSABLE_DATA
+}
+
+// TODO(vicb): read-only when TS supports it
+export interface TagDefinition {
+ closedByParent: boolean;
+ requiredParents: {[key: string]: boolean};
+ parentToAdd: string;
+ implicitNamespacePrefix: string;
+ contentType: TagContentType;
+ isVoid: boolean;
+ ignoreFirstLf: boolean;
+ canSelfClose: boolean;
+
+ requireExtraParent(currentParent: string): boolean;
+
+ isClosedByChild(name: string): boolean;
+}
+
+export function splitNsName(elementName: string): [string, string] {
+ if (elementName[0] != ':') {
+ return [null, elementName];
+ }
+
+ const colonIndex = elementName.indexOf(':', 1);
+
+ if (colonIndex == -1) {
+ throw new Error(`Unsupported format "${elementName}" expecting ":namespace:name"`);
+ }
+
+ return [elementName.slice(1, colonIndex), elementName.slice(colonIndex + 1)];
+}
+
+export function getNsPrefix(fullName: string): string {
+ return fullName === null ? null : splitNsName(fullName)[0];
+}
+
+export function mergeNsAndName(prefix: string, localName: string): string {
+ return prefix ? `:${prefix}:${localName}` : localName;
+}
+
+// see http://www.w3.org/TR/html51/syntax.html#named-character-references
+// see https://html.spec.whatwg.org/multipage/entities.json
+// This list is not exhaustive to keep the compiler footprint low.
+// The `{` / `ƫ` syntax should be used when the named character reference does not exist.
+export const NAMED_ENTITIES: {[k: string]: string} = {
+ 'Aacute': '\u00C1',
+ 'aacute': '\u00E1',
+ 'Acirc': '\u00C2',
+ 'acirc': '\u00E2',
+ 'acute': '\u00B4',
+ 'AElig': '\u00C6',
+ 'aelig': '\u00E6',
+ 'Agrave': '\u00C0',
+ 'agrave': '\u00E0',
+ 'alefsym': '\u2135',
+ 'Alpha': '\u0391',
+ 'alpha': '\u03B1',
+ 'amp': '&',
+ 'and': '\u2227',
+ 'ang': '\u2220',
+ 'apos': '\u0027',
+ 'Aring': '\u00C5',
+ 'aring': '\u00E5',
+ 'asymp': '\u2248',
+ 'Atilde': '\u00C3',
+ 'atilde': '\u00E3',
+ 'Auml': '\u00C4',
+ 'auml': '\u00E4',
+ 'bdquo': '\u201E',
+ 'Beta': '\u0392',
+ 'beta': '\u03B2',
+ 'brvbar': '\u00A6',
+ 'bull': '\u2022',
+ 'cap': '\u2229',
+ 'Ccedil': '\u00C7',
+ 'ccedil': '\u00E7',
+ 'cedil': '\u00B8',
+ 'cent': '\u00A2',
+ 'Chi': '\u03A7',
+ 'chi': '\u03C7',
+ 'circ': '\u02C6',
+ 'clubs': '\u2663',
+ 'cong': '\u2245',
+ 'copy': '\u00A9',
+ 'crarr': '\u21B5',
+ 'cup': '\u222A',
+ 'curren': '\u00A4',
+ 'dagger': '\u2020',
+ 'Dagger': '\u2021',
+ 'darr': '\u2193',
+ 'dArr': '\u21D3',
+ 'deg': '\u00B0',
+ 'Delta': '\u0394',
+ 'delta': '\u03B4',
+ 'diams': '\u2666',
+ 'divide': '\u00F7',
+ 'Eacute': '\u00C9',
+ 'eacute': '\u00E9',
+ 'Ecirc': '\u00CA',
+ 'ecirc': '\u00EA',
+ 'Egrave': '\u00C8',
+ 'egrave': '\u00E8',
+ 'empty': '\u2205',
+ 'emsp': '\u2003',
+ 'ensp': '\u2002',
+ 'Epsilon': '\u0395',
+ 'epsilon': '\u03B5',
+ 'equiv': '\u2261',
+ 'Eta': '\u0397',
+ 'eta': '\u03B7',
+ 'ETH': '\u00D0',
+ 'eth': '\u00F0',
+ 'Euml': '\u00CB',
+ 'euml': '\u00EB',
+ 'euro': '\u20AC',
+ 'exist': '\u2203',
+ 'fnof': '\u0192',
+ 'forall': '\u2200',
+ 'frac12': '\u00BD',
+ 'frac14': '\u00BC',
+ 'frac34': '\u00BE',
+ 'frasl': '\u2044',
+ 'Gamma': '\u0393',
+ 'gamma': '\u03B3',
+ 'ge': '\u2265',
+ 'gt': '>',
+ 'harr': '\u2194',
+ 'hArr': '\u21D4',
+ 'hearts': '\u2665',
+ 'hellip': '\u2026',
+ 'Iacute': '\u00CD',
+ 'iacute': '\u00ED',
+ 'Icirc': '\u00CE',
+ 'icirc': '\u00EE',
+ 'iexcl': '\u00A1',
+ 'Igrave': '\u00CC',
+ 'igrave': '\u00EC',
+ 'image': '\u2111',
+ 'infin': '\u221E',
+ 'int': '\u222B',
+ 'Iota': '\u0399',
+ 'iota': '\u03B9',
+ 'iquest': '\u00BF',
+ 'isin': '\u2208',
+ 'Iuml': '\u00CF',
+ 'iuml': '\u00EF',
+ 'Kappa': '\u039A',
+ 'kappa': '\u03BA',
+ 'Lambda': '\u039B',
+ 'lambda': '\u03BB',
+ 'lang': '\u27E8',
+ 'laquo': '\u00AB',
+ 'larr': '\u2190',
+ 'lArr': '\u21D0',
+ 'lceil': '\u2308',
+ 'ldquo': '\u201C',
+ 'le': '\u2264',
+ 'lfloor': '\u230A',
+ 'lowast': '\u2217',
+ 'loz': '\u25CA',
+ 'lrm': '\u200E',
+ 'lsaquo': '\u2039',
+ 'lsquo': '\u2018',
+ 'lt': '<',
+ 'macr': '\u00AF',
+ 'mdash': '\u2014',
+ 'micro': '\u00B5',
+ 'middot': '\u00B7',
+ 'minus': '\u2212',
+ 'Mu': '\u039C',
+ 'mu': '\u03BC',
+ 'nabla': '\u2207',
+ 'nbsp': '\u00A0',
+ 'ndash': '\u2013',
+ 'ne': '\u2260',
+ 'ni': '\u220B',
+ 'not': '\u00AC',
+ 'notin': '\u2209',
+ 'nsub': '\u2284',
+ 'Ntilde': '\u00D1',
+ 'ntilde': '\u00F1',
+ 'Nu': '\u039D',
+ 'nu': '\u03BD',
+ 'Oacute': '\u00D3',
+ 'oacute': '\u00F3',
+ 'Ocirc': '\u00D4',
+ 'ocirc': '\u00F4',
+ 'OElig': '\u0152',
+ 'oelig': '\u0153',
+ 'Ograve': '\u00D2',
+ 'ograve': '\u00F2',
+ 'oline': '\u203E',
+ 'Omega': '\u03A9',
+ 'omega': '\u03C9',
+ 'Omicron': '\u039F',
+ 'omicron': '\u03BF',
+ 'oplus': '\u2295',
+ 'or': '\u2228',
+ 'ordf': '\u00AA',
+ 'ordm': '\u00BA',
+ 'Oslash': '\u00D8',
+ 'oslash': '\u00F8',
+ 'Otilde': '\u00D5',
+ 'otilde': '\u00F5',
+ 'otimes': '\u2297',
+ 'Ouml': '\u00D6',
+ 'ouml': '\u00F6',
+ 'para': '\u00B6',
+ 'permil': '\u2030',
+ 'perp': '\u22A5',
+ 'Phi': '\u03A6',
+ 'phi': '\u03C6',
+ 'Pi': '\u03A0',
+ 'pi': '\u03C0',
+ 'piv': '\u03D6',
+ 'plusmn': '\u00B1',
+ 'pound': '\u00A3',
+ 'prime': '\u2032',
+ 'Prime': '\u2033',
+ 'prod': '\u220F',
+ 'prop': '\u221D',
+ 'Psi': '\u03A8',
+ 'psi': '\u03C8',
+ 'quot': '\u0022',
+ 'radic': '\u221A',
+ 'rang': '\u27E9',
+ 'raquo': '\u00BB',
+ 'rarr': '\u2192',
+ 'rArr': '\u21D2',
+ 'rceil': '\u2309',
+ 'rdquo': '\u201D',
+ 'real': '\u211C',
+ 'reg': '\u00AE',
+ 'rfloor': '\u230B',
+ 'Rho': '\u03A1',
+ 'rho': '\u03C1',
+ 'rlm': '\u200F',
+ 'rsaquo': '\u203A',
+ 'rsquo': '\u2019',
+ 'sbquo': '\u201A',
+ 'Scaron': '\u0160',
+ 'scaron': '\u0161',
+ 'sdot': '\u22C5',
+ 'sect': '\u00A7',
+ 'shy': '\u00AD',
+ 'Sigma': '\u03A3',
+ 'sigma': '\u03C3',
+ 'sigmaf': '\u03C2',
+ 'sim': '\u223C',
+ 'spades': '\u2660',
+ 'sub': '\u2282',
+ 'sube': '\u2286',
+ 'sum': '\u2211',
+ 'sup': '\u2283',
+ 'sup1': '\u00B9',
+ 'sup2': '\u00B2',
+ 'sup3': '\u00B3',
+ 'supe': '\u2287',
+ 'szlig': '\u00DF',
+ 'Tau': '\u03A4',
+ 'tau': '\u03C4',
+ 'there4': '\u2234',
+ 'Theta': '\u0398',
+ 'theta': '\u03B8',
+ 'thetasym': '\u03D1',
+ 'thinsp': '\u2009',
+ 'THORN': '\u00DE',
+ 'thorn': '\u00FE',
+ 'tilde': '\u02DC',
+ 'times': '\u00D7',
+ 'trade': '\u2122',
+ 'Uacute': '\u00DA',
+ 'uacute': '\u00FA',
+ 'uarr': '\u2191',
+ 'uArr': '\u21D1',
+ 'Ucirc': '\u00DB',
+ 'ucirc': '\u00FB',
+ 'Ugrave': '\u00D9',
+ 'ugrave': '\u00F9',
+ 'uml': '\u00A8',
+ 'upsih': '\u03D2',
+ 'Upsilon': '\u03A5',
+ 'upsilon': '\u03C5',
+ 'Uuml': '\u00DC',
+ 'uuml': '\u00FC',
+ 'weierp': '\u2118',
+ 'Xi': '\u039E',
+ 'xi': '\u03BE',
+ 'Yacute': '\u00DD',
+ 'yacute': '\u00FD',
+ 'yen': '\u00A5',
+ 'yuml': '\u00FF',
+ 'Yuml': '\u0178',
+ 'Zeta': '\u0396',
+ 'zeta': '\u03B6',
+ 'zwj': '\u200D',
+ 'zwnj': '\u200C',
+};
diff --git a/packages/upgrade/src/common/compiler_helpers/selector.ts b/packages/upgrade/src/common/compiler_helpers/selector.ts
new file mode 100644
index 0000000000..62a9c028a5
--- /dev/null
+++ b/packages/upgrade/src/common/compiler_helpers/selector.ts
@@ -0,0 +1,368 @@
+/**
+ * @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 {getHtmlTagDefinition} from './ml_parser/html_tags';
+
+const _SELECTOR_REGEXP = new RegExp(
+ '(\\:not\\()|' + //":not("
+ '([-\\w]+)|' + // "tag"
+ '(?:\\.([-\\w]+))|' + // ".class"
+ // "-" should appear first in the regexp below as FF31 parses "[.-\w]" as a range
+ '(?:\\[([-.\\w*]+)(?:=([^\\]]*))?\\])|' + // "[name]", "[name=value]"
+ '(\\))|' + // ")"
+ '(\\s*,\\s*)', // ","
+ 'g');
+
+/**
+ * A css selector contains an element name,
+ * css classes and attribute/value pairs with the purpose
+ * of selecting subsets out of them.
+ */
+export class CssSelector {
+ element: string = null;
+ classNames: string[] = [];
+ attrs: string[] = [];
+ notSelectors: CssSelector[] = [];
+
+ static parse(selector: string): CssSelector[] {
+ const results: CssSelector[] = [];
+ const _addResult = (res: CssSelector[], cssSel: CssSelector) => {
+ if (cssSel.notSelectors.length > 0 && !cssSel.element && cssSel.classNames.length == 0 &&
+ cssSel.attrs.length == 0) {
+ cssSel.element = '*';
+ }
+ res.push(cssSel);
+ };
+ let cssSelector = new CssSelector();
+ let match: string[];
+ let current = cssSelector;
+ let inNot = false;
+ _SELECTOR_REGEXP.lastIndex = 0;
+ while (match = _SELECTOR_REGEXP.exec(selector)) {
+ if (match[1]) {
+ if (inNot) {
+ throw new Error('Nesting :not is not allowed in a selector');
+ }
+ inNot = true;
+ current = new CssSelector();
+ cssSelector.notSelectors.push(current);
+ }
+ if (match[2]) {
+ current.setElement(match[2]);
+ }
+ if (match[3]) {
+ current.addClassName(match[3]);
+ }
+ if (match[4]) {
+ current.addAttribute(match[4], match[5]);
+ }
+ if (match[6]) {
+ inNot = false;
+ current = cssSelector;
+ }
+ if (match[7]) {
+ if (inNot) {
+ throw new Error('Multiple selectors in :not are not supported');
+ }
+ _addResult(results, cssSelector);
+ cssSelector = current = new CssSelector();
+ }
+ }
+ _addResult(results, cssSelector);
+ return results;
+ }
+
+ isElementSelector(): boolean {
+ return this.hasElementSelector() && this.classNames.length == 0 && this.attrs.length == 0 &&
+ this.notSelectors.length === 0;
+ }
+
+ hasElementSelector(): boolean { return !!this.element; }
+
+ setElement(element: string = null) { this.element = element; }
+
+ /** Gets a template string for an element that matches the selector. */
+ getMatchingElementTemplate(): string {
+ const tagName = this.element || 'div';
+ const classAttr = this.classNames.length > 0 ? ` class="${this.classNames.join(' ')}"` : '';
+
+ let attrs = '';
+ for (let i = 0; i < this.attrs.length; i += 2) {
+ const attrName = this.attrs[i];
+ const attrValue = this.attrs[i + 1] !== '' ? `="${this.attrs[i + 1]}"` : '';
+ attrs += ` ${attrName}${attrValue}`;
+ }
+
+ return getHtmlTagDefinition(tagName).isVoid ? `<${tagName}${classAttr}${attrs}/>` :
+ `<${tagName}${classAttr}${attrs}>${tagName}>`;
+ }
+
+ addAttribute(name: string, value: string = '') {
+ this.attrs.push(name, value && value.toLowerCase() || '');
+ }
+
+ addClassName(name: string) { this.classNames.push(name.toLowerCase()); }
+
+ toString(): string {
+ let res: string = this.element || '';
+ if (this.classNames) {
+ this.classNames.forEach(klass => res += `.${klass}`);
+ }
+ if (this.attrs) {
+ for (let i = 0; i < this.attrs.length; i += 2) {
+ const name = this.attrs[i];
+ const value = this.attrs[i + 1];
+ res += `[${name}${value ? '=' + value : ''}]`;
+ }
+ }
+ this.notSelectors.forEach(notSelector => res += `:not(${notSelector})`);
+ return res;
+ }
+}
+
+/**
+ * Reads a list of CssSelectors and allows to calculate which ones
+ * are contained in a given CssSelector.
+ */
+export class SelectorMatcher {
+ static createNotMatcher(notSelectors: CssSelector[]): SelectorMatcher {
+ const notMatcher = new SelectorMatcher();
+ notMatcher.addSelectables(notSelectors, null);
+ return notMatcher;
+ }
+
+ private _elementMap = new Map();
+ private _elementPartialMap = new Map();
+ private _classMap = new Map();
+ private _classPartialMap = new Map();
+ private _attrValueMap = new Map>();
+ private _attrValuePartialMap = new Map>();
+ private _listContexts: SelectorListContext[] = [];
+
+ addSelectables(cssSelectors: CssSelector[], callbackCtxt?: any) {
+ let listContext: SelectorListContext = null;
+ if (cssSelectors.length > 1) {
+ listContext = new SelectorListContext(cssSelectors);
+ this._listContexts.push(listContext);
+ }
+ for (let i = 0; i < cssSelectors.length; i++) {
+ this._addSelectable(cssSelectors[i], callbackCtxt, listContext);
+ }
+ }
+
+ /**
+ * Add an object that can be found later on by calling `match`.
+ * @param cssSelector A css selector
+ * @param callbackCtxt An opaque object that will be given to the callback of the `match` function
+ */
+ private _addSelectable(
+ cssSelector: CssSelector, callbackCtxt: any, listContext: SelectorListContext) {
+ let matcher: SelectorMatcher = this;
+ const element = cssSelector.element;
+ const classNames = cssSelector.classNames;
+ const attrs = cssSelector.attrs;
+ const selectable = new SelectorContext(cssSelector, callbackCtxt, listContext);
+
+ if (element) {
+ const isTerminal = attrs.length === 0 && classNames.length === 0;
+ if (isTerminal) {
+ this._addTerminal(matcher._elementMap, element, selectable);
+ } else {
+ matcher = this._addPartial(matcher._elementPartialMap, element);
+ }
+ }
+
+ if (classNames) {
+ for (let i = 0; i < classNames.length; i++) {
+ const isTerminal = attrs.length === 0 && i === classNames.length - 1;
+ const className = classNames[i];
+ if (isTerminal) {
+ this._addTerminal(matcher._classMap, className, selectable);
+ } else {
+ matcher = this._addPartial(matcher._classPartialMap, className);
+ }
+ }
+ }
+
+ if (attrs) {
+ for (let i = 0; i < attrs.length; i += 2) {
+ const isTerminal = i === attrs.length - 2;
+ const name = attrs[i];
+ const value = attrs[i + 1];
+ if (isTerminal) {
+ const terminalMap = matcher._attrValueMap;
+ let terminalValuesMap = terminalMap.get(name);
+ if (!terminalValuesMap) {
+ terminalValuesMap = new Map();
+ terminalMap.set(name, terminalValuesMap);
+ }
+ this._addTerminal(terminalValuesMap, value, selectable);
+ } else {
+ const partialMap = matcher._attrValuePartialMap;
+ let partialValuesMap = partialMap.get(name);
+ if (!partialValuesMap) {
+ partialValuesMap = new Map();
+ partialMap.set(name, partialValuesMap);
+ }
+ matcher = this._addPartial(partialValuesMap, value);
+ }
+ }
+ }
+ }
+
+ private _addTerminal(
+ map: Map, name: string, selectable: SelectorContext) {
+ let terminalList = map.get(name);
+ if (!terminalList) {
+ terminalList = [];
+ map.set(name, terminalList);
+ }
+ terminalList.push(selectable);
+ }
+
+ private _addPartial(map: Map, name: string): SelectorMatcher {
+ let matcher = map.get(name);
+ if (!matcher) {
+ matcher = new SelectorMatcher();
+ map.set(name, matcher);
+ }
+ return matcher;
+ }
+
+ /**
+ * Find the objects that have been added via `addSelectable`
+ * whose css selector is contained in the given css selector.
+ * @param cssSelector A css selector
+ * @param matchedCallback This callback will be called with the object handed into `addSelectable`
+ * @return boolean true if a match was found
+ */
+ match(cssSelector: CssSelector, matchedCallback: (c: CssSelector, a: any) => void): boolean {
+ let result = false;
+ const element = cssSelector.element;
+ const classNames = cssSelector.classNames;
+ const attrs = cssSelector.attrs;
+
+ for (let i = 0; i < this._listContexts.length; i++) {
+ this._listContexts[i].alreadyMatched = false;
+ }
+
+ result = this._matchTerminal(this._elementMap, element, cssSelector, matchedCallback) || result;
+ result = this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback) ||
+ result;
+
+ if (classNames) {
+ for (let i = 0; i < classNames.length; i++) {
+ const className = classNames[i];
+ result =
+ this._matchTerminal(this._classMap, className, cssSelector, matchedCallback) || result;
+ result =
+ this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback) ||
+ result;
+ }
+ }
+
+ if (attrs) {
+ for (let i = 0; i < attrs.length; i += 2) {
+ const name = attrs[i];
+ const value = attrs[i + 1];
+
+ const terminalValuesMap = this._attrValueMap.get(name);
+ if (value) {
+ result =
+ this._matchTerminal(terminalValuesMap, '', cssSelector, matchedCallback) || result;
+ }
+ result =
+ this._matchTerminal(terminalValuesMap, value, cssSelector, matchedCallback) || result;
+
+ const partialValuesMap = this._attrValuePartialMap.get(name);
+ if (value) {
+ result = this._matchPartial(partialValuesMap, '', cssSelector, matchedCallback) || result;
+ }
+ result =
+ this._matchPartial(partialValuesMap, value, cssSelector, matchedCallback) || result;
+ }
+ }
+ return result;
+ }
+
+ /** @internal */
+ _matchTerminal(
+ map: Map, name: string, cssSelector: CssSelector,
+ matchedCallback: (c: CssSelector, a: any) => void): boolean {
+ if (!map || typeof name !== 'string') {
+ return false;
+ }
+
+ let selectables: SelectorContext[] = map.get(name) || [];
+ const starSelectables: SelectorContext[] = map.get('*');
+ if (starSelectables) {
+ selectables = selectables.concat(starSelectables);
+ }
+ if (selectables.length === 0) {
+ return false;
+ }
+ let selectable: SelectorContext;
+ let result = false;
+ for (let i = 0; i < selectables.length; i++) {
+ selectable = selectables[i];
+ result = selectable.finalize(cssSelector, matchedCallback) || result;
+ }
+ return result;
+ }
+
+ /** @internal */
+ _matchPartial(
+ map: Map, name: string, cssSelector: CssSelector,
+ matchedCallback: (c: CssSelector, a: any) => void): boolean {
+ if (!map || typeof name !== 'string') {
+ return false;
+ }
+
+ const nestedSelector = map.get(name);
+ if (!nestedSelector) {
+ return false;
+ }
+ // TODO(perf): get rid of recursion and measure again
+ // TODO(perf): don't pass the whole selector into the recursion,
+ // but only the not processed parts
+ return nestedSelector.match(cssSelector, matchedCallback);
+ }
+}
+
+
+export class SelectorListContext {
+ alreadyMatched: boolean = false;
+
+ constructor(public selectors: CssSelector[]) {}
+}
+
+// Store context to pass back selector and context when a selector is matched
+export class SelectorContext {
+ notSelectors: CssSelector[];
+
+ constructor(
+ public selector: CssSelector, public cbContext: any,
+ public listContext: SelectorListContext) {
+ this.notSelectors = selector.notSelectors;
+ }
+
+ finalize(cssSelector: CssSelector, callback: (c: CssSelector, a: any) => void): boolean {
+ let result = true;
+ if (this.notSelectors.length > 0 && (!this.listContext || !this.listContext.alreadyMatched)) {
+ const notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
+ result = !notMatcher.match(cssSelector, null);
+ }
+ if (result && callback && (!this.listContext || !this.listContext.alreadyMatched)) {
+ if (this.listContext) {
+ this.listContext.alreadyMatched = true;
+ }
+ callback(this.selector, this.cbContext);
+ }
+ return result;
+ }
+}
diff --git a/packages/upgrade/src/common/component_info.ts b/packages/upgrade/src/common/component_info.ts
index 130f8f2589..3419fd178e 100644
--- a/packages/upgrade/src/common/component_info.ts
+++ b/packages/upgrade/src/common/component_info.ts
@@ -12,6 +12,7 @@ export interface ComponentInfo {
component: Type;
inputs?: string[];
outputs?: string[];
+ selectors?: string[];
}
/**
diff --git a/packages/upgrade/src/common/downgrade_component.ts b/packages/upgrade/src/common/downgrade_component.ts
index 41899f7542..a25609be84 100644
--- a/packages/upgrade/src/common/downgrade_component.ts
+++ b/packages/upgrade/src/common/downgrade_component.ts
@@ -11,6 +11,7 @@ import {ComponentFactory, ComponentFactoryResolver, Injector, Type} from '@angul
import * as angular from './angular1';
import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants';
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
+import {NgContentSelectorHelper} from './ng_content_selector_helper';
import {controllerKey, getComponentName} from './util';
let downgradeCount = 0;
@@ -44,6 +45,7 @@ let downgradeCount = 0;
* We must do the following:
* * specify the Angular component class that is to be downgraded
* * specify all inputs and outputs that the AngularJS component expects
+ * * specify the selectors used in any `ng-content` elements in the component's template
*
* @description
*
@@ -53,19 +55,28 @@ let downgradeCount = 0;
* The parameter contains information about the Component that is being downgraded:
*
* * `component: Type`: The type of the Component that will be downgraded
- * * `inputs: string[]`: A collection of strings that specify what inputs the component accepts.
- * * `outputs: string[]`: A collection of strings that specify what outputs the component emits.
+ * * `inputs: string[]`: A collection of strings that specify what inputs the component accepts
+ * * `outputs: string[]`: A collection of strings that specify what outputs the component emits
+ * * `selectors: string[]`: A collection of strings that specify what selectors are expected on
+ * `ng-content` elements in the template to enable content projection (a.k.a. transclusion in
+ * AngularJS)
*
* The `inputs` and `outputs` are strings that map the names of properties to camelCased
* attribute names. They are of the form `"prop: attr"`; or simply `"propAndAttr" where the
* property and attribute have the same identifier.
*
+ * The `selectors` are the values of the `select` attribute of each of the `ng-content` elements
+ * that appear in the downgraded component's template.
+ * These selectors must be provided in the order that they appear in the template as they are
+ * mapped by index to the projected nodes.
+ *
* @experimental
*/
export function downgradeComponent(info: /* ComponentInfo */ {
component: Type;
inputs?: string[];
outputs?: string[];
+ selectors?: string[]
}): any /* angular.IInjectable */ {
const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`;
let idCount = 0;
diff --git a/packages/upgrade/src/common/downgrade_component_adapter.ts b/packages/upgrade/src/common/downgrade_component_adapter.ts
index fa536a47e2..b926c13474 100644
--- a/packages/upgrade/src/common/downgrade_component_adapter.ts
+++ b/packages/upgrade/src/common/downgrade_component_adapter.ts
@@ -9,10 +9,12 @@
import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, ReflectiveInjector, SimpleChange, SimpleChanges, Type} from '@angular/core';
import * as angular from './angular1';
+import {createElementCssSelector} from './compiler_helpers/createElementCssSelector';
+import {CssSelector, SelectorMatcher} from './compiler_helpers/selector';
import {ComponentInfo, PropertyBinding} from './component_info';
import {$SCOPE} from './constants';
-import {ContentProjectionHelper} from './content_projection_helper';
-import {getComponentName, hookupNgModel} from './util';
+import {NgContentSelectorHelper} from './ng_content_selector_helper';
+import {getAttributesAsArray, getComponentName, hookupNgModel} from './util';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
@@ -38,12 +40,7 @@ export class DowngradeComponentAdapter {
compileContents(): Node[][] {
const compiledProjectableNodes: Node[][] = [];
-
- // The projected content has to be grouped, before it is compiled.
- const projectionHelper: ContentProjectionHelper =
- this.parentInjector.get(ContentProjectionHelper);
- const projectableNodes: Node[][] = projectionHelper.groupProjectableNodes(
- this.$injector, this.info.component, this.element.contents());
+ const projectableNodes: Node[][] = this.groupProjectableNodes();
const linkFns = projectableNodes.map(nodes => this.$compile(nodes));
this.element.empty();
@@ -186,4 +183,56 @@ export class DowngradeComponentAdapter {
this.component[prop] = currValue;
}
+
+ groupProjectableNodes() {
+ const ngContentSelectorHelper =
+ this.parentInjector.get(NgContentSelectorHelper) as NgContentSelectorHelper;
+ const ngContentSelectors = ngContentSelectorHelper.getNgContentSelectors(this.info);
+
+ if (!ngContentSelectors) {
+ throw new Error('Expecting ngContentSelectors for: ' + getComponentName(this.info.component));
+ }
+
+ return this._groupNodesBySelector(ngContentSelectors, this.element.contents());
+ }
+
+ /**
+ * Group a set of DOM nodes into `ngContent` groups, based on the given content selectors.
+ */
+ private _groupNodesBySelector(ngContentSelectors: string[], nodes: Node[]): Node[][] {
+ const projectableNodes: Node[][] = [];
+ let matcher = new SelectorMatcher();
+ let wildcardNgContentIndex: number;
+
+ for (let i = 0, ii = ngContentSelectors.length; i < ii; ++i) {
+ projectableNodes[i] = [];
+
+ const selector = ngContentSelectors[i];
+ if (selector === '*') {
+ wildcardNgContentIndex = i;
+ } else {
+ matcher.addSelectables(CssSelector.parse(selector), i);
+ }
+ }
+
+ for (let j = 0, jj = nodes.length; j < jj; ++j) {
+ const ngContentIndices: number[] = [];
+ const node = nodes[j];
+ const selector =
+ createElementCssSelector(node.nodeName.toLowerCase(), getAttributesAsArray(node));
+
+ matcher.match(selector, (_, index) => ngContentIndices.push(index));
+ ngContentIndices.sort();
+
+ if (wildcardNgContentIndex !== undefined) {
+ ngContentIndices.push(wildcardNgContentIndex);
+ }
+
+ if (ngContentIndices.length) {
+ projectableNodes[ngContentIndices[0]].push(node);
+ }
+ }
+
+ return projectableNodes;
+ }
}
diff --git a/packages/upgrade/src/common/ng_content_selector_helper.ts b/packages/upgrade/src/common/ng_content_selector_helper.ts
new file mode 100644
index 0000000000..62ea7aa564
--- /dev/null
+++ b/packages/upgrade/src/common/ng_content_selector_helper.ts
@@ -0,0 +1,24 @@
+/**
+ * @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 {ComponentInfo} from './component_info';
+
+/**
+ * This class gives an extension point between the static and dynamic versions
+ * of ngUpgrade:
+ * * In the static version (this one) we must specify them manually as part of
+ * the call to `downgradeComponent(...)`.
+ * * In the dynamic version (`DynamicNgContentSelectorHelper`) we are able to
+ * ask the compiler for the selectors of a component.
+ */
+export class NgContentSelectorHelper {
+ getNgContentSelectors(info: ComponentInfo): string[] {
+ // if no selectors are passed then default to a single "wildcard" selector
+ return info.selectors || ['*'];
+ }
+}
diff --git a/packages/upgrade/src/dynamic/ng_content_selector_helper.ts b/packages/upgrade/src/dynamic/ng_content_selector_helper.ts
new file mode 100644
index 0000000000..085d101afe
--- /dev/null
+++ b/packages/upgrade/src/dynamic/ng_content_selector_helper.ts
@@ -0,0 +1,24 @@
+/**
+ * @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 {Compiler, Injectable} from '@angular/core';
+
+import {ComponentInfo} from '../common/component_info';
+import {NgContentSelectorHelper} from '../common/ng_content_selector_helper';
+
+
+/**
+ * See `NgContentSelectorHelper` for more information about this class.
+ */
+@Injectable()
+export class DynamicNgContentSelectorHelper extends NgContentSelectorHelper {
+ constructor(private compiler: Compiler) { super(); }
+ getNgContentSelectors(info: ComponentInfo): string[] {
+ return this.compiler.getNgContentSelectors(info.component);
+ }
+}
diff --git a/packages/upgrade/src/dynamic/upgrade_adapter.ts b/packages/upgrade/src/dynamic/upgrade_adapter.ts
index 106ac05516..14efd6870d 100644
--- a/packages/upgrade/src/dynamic/upgrade_adapter.ts
+++ b/packages/upgrade/src/dynamic/upgrade_adapter.ts
@@ -13,12 +13,12 @@ import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import * as angular from '../common/angular1';
import {ComponentInfo} from '../common/component_info';
import {$$TESTABILITY, $COMPILE, $INJECTOR, $ROOT_SCOPE, COMPILER_KEY, INJECTOR_KEY, NG_ZONE_KEY} from '../common/constants';
-import {ContentProjectionHelper} from '../common/content_projection_helper';
import {downgradeComponent} from '../common/downgrade_component';
import {downgradeInjectable} from '../common/downgrade_injectable';
+import {NgContentSelectorHelper} from '../common/ng_content_selector_helper';
import {Deferred, controllerKey, onError} from '../common/util';
-import {DynamicContentProjectionHelper} from './content_projection_helper';
+import {DynamicNgContentSelectorHelper} from './ng_content_selector_helper';
import {UpgradeNg1ComponentAdapterBuilder} from './upgrade_ng1_adapter';
let upgradeCount: number = 0;
@@ -561,7 +561,7 @@ export class UpgradeAdapter {
providers: [
{provide: $INJECTOR, useFactory: () => ng1Injector},
{provide: $COMPILE, useFactory: () => ng1Injector.get($COMPILE)},
- {provide: ContentProjectionHelper, useClass: DynamicContentProjectionHelper},
+ {provide: NgContentSelectorHelper, useClass: DynamicNgContentSelectorHelper},
this.upgradedProviders
],
imports: [this.ng2AppModule],
diff --git a/packages/upgrade/src/static/upgrade_module.ts b/packages/upgrade/src/static/upgrade_module.ts
index 5a9c128629..5c32a814c9 100644
--- a/packages/upgrade/src/static/upgrade_module.ts
+++ b/packages/upgrade/src/static/upgrade_module.ts
@@ -10,7 +10,7 @@ import {Injector, NgModule, NgZone, Testability} from '@angular/core';
import * as angular from '../common/angular1';
import {$$TESTABILITY, $DELEGATE, $INJECTOR, $PROVIDE, $ROOT_SCOPE, INJECTOR_KEY, UPGRADE_MODULE_NAME} from '../common/constants';
-import {ContentProjectionHelper} from '../common/content_projection_helper';
+import {NgContentSelectorHelper} from '../common/ng_content_selector_helper';
import {controllerKey} from '../common/util';
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
@@ -130,7 +130,7 @@ import {angular1Providers, setTempInjectorRef} from './angular1_providers';
*
* @experimental
*/
-@NgModule({providers: [angular1Providers, ContentProjectionHelper]})
+@NgModule({providers: [angular1Providers, NgContentSelectorHelper]})
export class UpgradeModule {
/**
* The AngularJS `$injector` for the upgrade application.
diff --git a/packages/upgrade/test/common/downgrade_component_adapter_spec.ts b/packages/upgrade/test/common/downgrade_component_adapter_spec.ts
new file mode 100644
index 0000000000..c92b6f601b
--- /dev/null
+++ b/packages/upgrade/test/common/downgrade_component_adapter_spec.ts
@@ -0,0 +1,96 @@
+/**
+ * @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 * as angular from '@angular/upgrade/src/common/angular1';
+import {DowngradeComponentAdapter} from '@angular/upgrade/src/common/downgrade_component_adapter';
+import {NgContentSelectorHelper} from '@angular/upgrade/src/common/ng_content_selector_helper';
+import {nodes} from './test_helpers';
+
+
+export function main() {
+ describe('DowngradeComponentAdapter', () => {
+ describe('groupNodesBySelector', () => {
+ function createAdapter(selectors: string[], contentNodes: Node[]): DowngradeComponentAdapter {
+ const selectorHelper = new NgContentSelectorHelper();
+ const fakeInjector = {get: function() { return selectorHelper; }};
+ const fakeScope = { $new: function() {} } as any;
+ const element = angular.element('');
+ element.append(contentNodes);
+ return new DowngradeComponentAdapter(
+ 'id', {component: null, selectors}, element, null, fakeScope, null, fakeInjector, null,
+ null, null, null);
+ }
+
+ it('should return an array of node collections for each selector', () => {
+ const contentNodes = nodes(
+ '