Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for nested invocation with :: #59

Merged
merged 6 commits into from
Apr 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# ember-angle-bracket-invocation-polyfill

This addon provides a polyfill for angle bracket invocation syntax as described in
[RFC 311](https://github.com/emberjs/rfcs/pull/311). It's the same components you
know and love, no longer surrounded by mustaches. \o/
This addon provides a polyfill for angle bracket invocation syntax as described
in [RFC 311](https://github.com/emberjs/rfcs/pull/311) and [RFC
457](https://emberjs.github.io/rfcs/0457-nested-lookups.html). It's the same
components you know and love, no longer surrounded by mustaches. \o/

[![Build Status](https://travis-ci.org/rwjblue/ember-angle-bracket-invocation-polyfill.svg?branch=master)](https://travis-ci.org/rwjblue/ember-angle-bracket-invocation-polyfill)

Expand All @@ -16,7 +17,9 @@ You will additionally need to ensure ember-cli-htmlbars-inline-precompile is at

## Usage

The best usage guide is [the RFC itself](https://github.com/emberjs/rfcs/blob/master/text/0311-angle-bracket-invocation.md),
The best usage guide are the RFCs themselves
([emberjs/rfcs#311](https://emberjs.github.io/rfcs/0311-angle-bracket-invocation.html)
[emberjs/rfcs#457](https://emberjs.github.io/rfcs/0457-nested-lookups.html)),
but here are a few examples of "before"/"after" to whet your appetite:

**Before**:
Expand Down Expand Up @@ -116,8 +119,14 @@ but here are a few examples of "before"/"after" to whet your appetite:
<Title />
```

- Completely innert when running Ember 3.4 or higher
- Supports Ember 2.12, 2.16, 2.18, 3.1, 3.2, 3.3
- Supports invoking components nested in subfolders:

```
<Foo::Bar />
```

- Completely innert when running Ember 3.10 or higher
- Supports Ember 2.12, 2.16, 2.18, 3.1, 3.2, 3.3, 3.4, 3.8, 3.9
- Test all the features listed above 😘

## Limitations
Expand Down
19 changes: 19 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
let emberVersion = checker.forEmber();

this.shouldPolyfill = emberVersion.lt('3.4.0-alpha.1');
this.shouldPolyfillNested = emberVersion.lt('3.10.0-alpha.1');

let parentChecker = new VersionChecker(this.parent);
let precompileVersion = parentChecker.for('ember-cli-htmlbars-inline-precompile');
Expand All @@ -33,6 +34,14 @@ module.exports = {
params: {},
};
registry.add('htmlbars-ast-plugin', pluginObj);
} else if (this.shouldPolyfillNested) {
let pluginObj = this._buildNestedPlugin();
pluginObj.parallelBabel = {
requireFile: __filename,
buildUsing: '_buildPlugin',
params: {},
};
registry.add('htmlbars-ast-plugin', pluginObj);
}
},

Expand All @@ -46,6 +55,16 @@ module.exports = {
};
},

_buildNestedPlugin() {
return {
name: 'nested-component-invocation-support',
plugin: require('./lib/ast-nested-transform'),
baseDir() {
return __dirname;
},
};
},

included() {
this._super.included.apply(this, arguments);

Expand Down
90 changes: 90 additions & 0 deletions lib/ast-nested-transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use strict';

const reLines = /(.*?(?:\r\n?|\n|$))/gm;
const ALPHA = /[A-Za-z]/;

class AngleBracketPolyfill {
constructor(options) {
this.syntax = null;
this.sourceLines = options.contents && options.contents.match(reLines);
}

transform(ast) {
let b = this.syntax.builders;

// in order to debug in https://https://astexplorer.net/#/gist/0590eb883edfcd163b183514df4cc717
// **** copy from here ****

function dasherize(string) {
return string.replace(/[A-Z]/g, function(char, index) {
if (index === 0 || !ALPHA.test(string[index - 1])) {
return char.toLowerCase();
}

return `-${char.toLowerCase()}`;
});
}

let rootProgram;
let letBlock;
let yieldedComponents = new Map();

function ensureLetWrapper() {
if (!letBlock) {
letBlock = b.block('let', [], b.hash([]), b.program(rootProgram.body), null, null);
rootProgram.body = [letBlock];
}
}

let counter = 0;
function localNameForYieldedComponent(tag) {
let localName = yieldedComponents.get(tag);
if (!localName) {
localName = tag.replace(/::/g, '') + '_ANGLE_' + counter++;
let transformedPath = dasherize(tag.replace(/::/g, '/'));

let positionalArg = b.sexpr(b.path('component'), [b.string(transformedPath)]);
letBlock.params.push(positionalArg);
letBlock.program.blockParams.push(localName);

yieldedComponents.set(tag, localName);
}

return localName;
}

let visitor = {
// supports [email protected]
Template(node) {
rootProgram = node;
},

// supports glimmer-vm < 0.39
Program(node) {
// on older ember versions `Program` is used for both the "wrapping
// template" and for each block
if (!rootProgram) {
rootProgram = node;
}
},

ElementNode(node) {
let tag = node.tag;

if (tag.indexOf('::') !== -1) {
ensureLetWrapper();

let localName = localNameForYieldedComponent(tag);
node.tag = localName;
}
},
};
// **** copy to here ****

this.syntax.traverse(ast, visitor);

return ast;
}
}

module.exports = AngleBracketPolyfill;
11 changes: 8 additions & 3 deletions lib/ast-transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class AngleBracketPolyfill {
});
}

function replaceNestedComponents(string) {
return string.replace(/::/g, '/');
}

function isSimple(mustache) {
return mustache.params.length === 0 && mustache.hash.pairs.length === 0;
}
Expand Down Expand Up @@ -142,7 +146,8 @@ class AngleBracketPolyfill {
let selfClosing = getSelfClosing(element);
let hasAttrSplat = element.attributes.find(n => n.name === '...attributes');
let dasherizedComponentName = dasherize(tag);
let singleWordComponent = dasherizedComponentName.indexOf('-') === -1;
let nestedComponentName = replaceNestedComponents(dasherizedComponentName);
let singleWordComponent = nestedComponentName.indexOf('-') === -1;

if (isLocal || isNamedArgument || isThisPath) {
let path = b.path(tag);
Expand All @@ -160,7 +165,7 @@ class AngleBracketPolyfill {
hasAttrSplat,
};
} else if (isUpperCase && singleWordComponent) {
let path = b.string(dasherizedComponentName);
let path = b.string(nestedComponentName);

return {
kind: 'DynamicComponent',
Expand All @@ -171,7 +176,7 @@ class AngleBracketPolyfill {
} else if (isUpperCase) {
return {
kind: 'StaticComponent',
componentName: dasherizedComponentName,
componentName: nestedComponentName,
selfClosing,
hasAttrSplat,
};
Expand Down
33 changes: 33 additions & 0 deletions tests/integration/components/angle-bracket-invocation-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,39 @@ module('Integration | Component | angle-bracket-invocation', function(hooks) {
assert.dom('h2').hasText("rwjblue's component");
assert.dom('p').hasText('Contents');
});

test('nested paths do not conflict with non-nested paths with similar names', async function(assert) {
this.owner.register('template:components/foo/bar', hbs`hi rwjblue!`);
this.owner.register('template:components/foo-bar', hbs`hi rtablada!`);

await render(hbs`
<Foo::Bar data-foo="bar"/>
<FooBar data-foo="baz" />
`);

assert.dom('[data-foo="bar"]').hasText('hi rwjblue!');
assert.dom('[data-foo="baz"]').hasText('hi rtablada!');
});

test('invoke nested path', async function(assert) {
this.owner.register('template:components/foo/bar', hbs`hi rwjblue!`);

await render(hbs`
<Foo::Bar data-foo="bar"/>
`);

assert.dom('[data-foo="bar"]').exists();
});

test('invoke deeply nested path', async function(assert) {
this.owner.register('template:components/foo/bar/baz/qux', hbs`hi rwjblue!`);

await render(hbs`
<Foo::Bar::Baz::Qux data-foo="bar"/>
`);

assert.dom('[data-foo="bar"]').exists();
});
});

module('dynamic component support', function() {
Expand Down