Add <> fragment support for expressions (#433)

* feat: add support for `<>` and `</>` Fragments

* docs: explain Fragments

* test: add fragment test
This commit is contained in:
Nate Moore 2021-06-15 11:33:27 -05:00 committed by GitHub
parent 28c2d74dc3
commit 490f2bebbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 77 additions and 20 deletions

View file

@ -0,0 +1,6 @@
---
'astro': patch
'@astrojs/parser': patch
---
Add support for Fragments with `<>` and `</>` syntax

View file

@ -113,28 +113,52 @@ export let name;
</main>
```
### Fragments
At the top-level of an `.astro` file, you may render any number of elements.
```html
<!-- Look, no Fragment! -->
<div id="a" />
<div id="b" />
<div id="c" />
```
Inside of an expression, you must wrap multiple elements in a Fragment. Fragments must open with `<>` and close with `</>`.
```jsx
<div>
{[0, 1, 2].map(id => (
<>
<div id={`a-${id}`} />
<div id={`b-${id}`} />
<div id={`c-${id}`} />
</>
))}
</div>
```
### `.astro` versus `.jsx`
`.astro` files can end up looking very similar to `.jsx` files, but there are a few key differences. Here's a comparison between the two formats.
| Feature | Astro | JSX |
| ---------------------------- | ---------------------------------------- | -------------------------------------------------- |
| File extension | `.astro` | `.jsx` or `.tsx` |
| User-Defined Components | `<Capitalized>` | `<Capitalized>` |
| Expression Syntax | `{}` | `{}` |
| Spread Attributes | `{...props}` | `{...props}` |
| Boolean Attributes | `autocomplete` === `autocomplete={true}` | `autocomplete` === `autocomplete={true}` |
| Inline Functions | `{items.map(item => <li>{item}</li>)}` | `{items.map(item => <li>{item}</li>)}` |
| IDE Support | WIP - [VS Code][code-ext] | Phenomenal |
| Requires JS import | No | Yes, `jsxPragma` (`React` or `h`) must be in scope |
| Fragments | Automatic | Wrap with `<Fragment>` or `<>` |
| Multiple frameworks per-file | Yes | No |
| Modifying `<head>` | Just use `<head>` | Per-framework (`<Head>`, `<svelte:head>`, etc) |
| Comment Style | `<!-- HTML -->` | `{/* JavaScript */}` |
| Special Characters | `&nbsp;` | `{'\xa0'}` or `{String.fromCharCode(160)}` |
| Attributes | `dash-case` | `camelCase` |
| Feature | Astro | JSX |
| ---------------------------- | -------------------------------------------- | -------------------------------------------------- |
| File extension | `.astro` | `.jsx` or `.tsx` |
| User-Defined Components | `<Capitalized>` | `<Capitalized>` |
| Expression Syntax | `{}` | `{}` |
| Spread Attributes | `{...props}` | `{...props}` |
| Boolean Attributes | `autocomplete` === `autocomplete={true}` | `autocomplete` === `autocomplete={true}` |
| Inline Functions | `{items.map(item => <li>{item}</li>)}` | `{items.map(item => <li>{item}</li>)}` |
| IDE Support | WIP - [VS Code][code-ext] | Phenomenal |
| Requires JS import | No | Yes, `jsxPragma` (`React` or `h`) must be in scope |
| Fragments | Automatic top-level, `<>` inside functions | Wrap with `<Fragment>` or `<>` |
| Multiple frameworks per-file | Yes | No |
| Modifying `<head>` | Just use `<head>` | Per-framework (`<Head>`, `<svelte:head>`, etc) |
| Comment Style | `<!-- HTML -->` | `{/* JavaScript */}` |
| Special Characters | `&nbsp;` | `{'\xa0'}` or `{String.fromCharCode(160)}` |
| Attributes | `dash-case` | `camelCase` |
### TODO: Styling
### TODO: Composition (Slots)

View file

@ -13,7 +13,7 @@ const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const meta_tags = new Map([
['astro:head', 'Head'],
// ['slot:body', 'Body'],
['', 'SlotTemplate'],
// ['astro:options', 'Options'],
// ['astro:window', 'Window'],
// ['astro:body', 'Body'],
@ -118,7 +118,7 @@ export default function tag(parser: Parser) {
? meta_tags.get(name)
: /[A-Z]/.test(name[0]) || name === 'astro:self' || name === 'astro:component'
? 'InlineComponent'
: name === 'astro:fragment'
: name === ''
? 'SlotTemplate'
: name === 'title' && parent_is_head(parser.stack)
? 'Title'

View file

@ -494,6 +494,11 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
return;
case 'Fragment':
break;
case 'SlotTemplate': {
buffers[curr] += `h(Fragment, null, children`;
paren++;
return;
}
case 'Slot':
case 'Head':
case 'InlineComponent': {
@ -630,6 +635,7 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
case 'CodeSpan':
case 'CodeFence':
return;
case 'SlotTemplate':
case 'Slot':
case 'Head':
case 'Body':

View file

@ -58,6 +58,17 @@ Expressions('Allows multiple JSX children in mustache', async ({ runtime }) => {
assert.ok(result.contents.includes('#f') && !result.contents.includes('#t'));
});
Expressions('Allows <> Fragments in expressions', async ({ runtime }) => {
const result = await runtime.load('/multiple-children');
if (result.error) throw new Error(result.error);
const $ = doc(result.contents);
assert.equal($('#fragment').children().length, 3);
assert.equal($('#fragment').children('#a').length, 1);
assert.equal($('#fragment').children('#b').length, 1);
assert.equal($('#fragment').children('#c').length, 1);
})
Expressions('Does not render falsy values using &&', async ({ runtime }) => {
const result = await runtime.load('/falsy');
if (result.error) throw new Error(result.error);

View file

@ -10,5 +10,15 @@ let title = 'My Site';
<h1>{title}</h1>
{false ? <h1>#t</h1> : <h1>#f</h1>}
<div id="fragment">
{(
<>
<div id="a" />
<div id="b" />
<div id="c" />
</>
)}
</div>
</body>
</html>
</html>