From 490f2bebbcd354b7b9d85020b9ce3abe5f376f13 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 15 Jun 2021 11:33:27 -0500 Subject: [PATCH] Add `<>` fragment support for expressions (#433) * feat: add support for `<>` and `` Fragments * docs: explain Fragments * test: add fragment test --- .changeset/mean-ligers-nail.md | 6 ++ docs/syntax.md | 58 +++++++++++++------ packages/astro-parser/src/parse/state/tag.ts | 4 +- packages/astro/src/compiler/codegen/index.ts | 6 ++ packages/astro/test/astro-expr.test.js | 11 ++++ .../src/pages/multiple-children.astro | 12 +++- 6 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 .changeset/mean-ligers-nail.md diff --git a/.changeset/mean-ligers-nail.md b/.changeset/mean-ligers-nail.md new file mode 100644 index 000000000..68739635f --- /dev/null +++ b/.changeset/mean-ligers-nail.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'@astrojs/parser': patch +--- + +Add support for Fragments with `<>` and `` syntax diff --git a/docs/syntax.md b/docs/syntax.md index edf854046..358644f17 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -113,28 +113,52 @@ export let name; ``` +### Fragments + +At the top-level of an `.astro` file, you may render any number of elements. + +```html + +
+
+
+``` + +Inside of an expression, you must wrap multiple elements in a Fragment. Fragments must open with `<>` and close with ``. + +```jsx +
+{[0, 1, 2].map(id => ( + <> +
+
+
+ +))} +
+``` + ### `.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 | `` | `` | -| Expression Syntax | `{}` | `{}` | -| Spread Attributes | `{...props}` | `{...props}` | -| Boolean Attributes | `autocomplete` === `autocomplete={true}` | `autocomplete` === `autocomplete={true}` | -| Inline Functions | `{items.map(item =>
  • {item}
  • )}` | `{items.map(item =>
  • {item}
  • )}` | -| 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 `` or `<>` | -| Multiple frameworks per-file | Yes | No | -| Modifying `` | Just use `` | Per-framework (``, ``, etc) | -| Comment Style | `` | `{/* JavaScript */}` | -| Special Characters | ` ` | `{'\xa0'}` or `{String.fromCharCode(160)}` | -| Attributes | `dash-case` | `camelCase` | +| Feature | Astro | JSX | +| ---------------------------- | -------------------------------------------- | -------------------------------------------------- | +| File extension | `.astro` | `.jsx` or `.tsx` | +| User-Defined Components | `` | `` | +| Expression Syntax | `{}` | `{}` | +| Spread Attributes | `{...props}` | `{...props}` | +| Boolean Attributes | `autocomplete` === `autocomplete={true}` | `autocomplete` === `autocomplete={true}` | +| Inline Functions | `{items.map(item =>
  • {item}
  • )}` | `{items.map(item =>
  • {item}
  • )}` | +| 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 `` or `<>` | +| Multiple frameworks per-file | Yes | No | +| Modifying `` | Just use `` | Per-framework (``, ``, etc) | +| Comment Style | `` | `{/* JavaScript */}` | +| Special Characters | ` ` | `{'\xa0'}` or `{String.fromCharCode(160)}` | +| Attributes | `dash-case` | `camelCase` | -### TODO: Styling ### TODO: Composition (Slots) diff --git a/packages/astro-parser/src/parse/state/tag.ts b/packages/astro-parser/src/parse/state/tag.ts index b8c3e63ad..28783df67 100644 --- a/packages/astro-parser/src/parse/state/tag.ts +++ b/packages/astro-parser/src/parse/state/tag.ts @@ -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' diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts index 140d3bfc5..b31fbf4a0 100644 --- a/packages/astro/src/compiler/codegen/index.ts +++ b/packages/astro/src/compiler/codegen/index.ts @@ -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': diff --git a/packages/astro/test/astro-expr.test.js b/packages/astro/test/astro-expr.test.js index 485d58d9b..727abd1c8 100644 --- a/packages/astro/test/astro-expr.test.js +++ b/packages/astro/test/astro-expr.test.js @@ -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); diff --git a/packages/astro/test/fixtures/astro-expr/src/pages/multiple-children.astro b/packages/astro/test/fixtures/astro-expr/src/pages/multiple-children.astro index fb0fafd4a..4e0b5e746 100644 --- a/packages/astro/test/fixtures/astro-expr/src/pages/multiple-children.astro +++ b/packages/astro/test/fixtures/astro-expr/src/pages/multiple-children.astro @@ -10,5 +10,15 @@ let title = 'My Site';

    {title}

    {false ?

    #t

    :

    #f

    } + +
    + {( + <> +
    +
    +
    + + )} +
    - \ No newline at end of file +