Fix falsy values (#275)
* fix(#274): improve attribute handling * chore: add test for JSX expressions * fix: falsy expressions should not render * chore: add changeset * test: update expression tests * fix: render 0 if value is {0}
This commit is contained in:
parent
e08abacfee
commit
3d20623c32
9 changed files with 88 additions and 6 deletions
7
.changeset/cold-paws-remember.md
Normal file
7
.changeset/cold-paws-remember.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed a bug where Astro did not conform to JSX Expressions' [`&&`](https://reactjs.org/docs/conditional-rendering.html#inline-if-with-logical--operator) syntax.
|
||||||
|
|
||||||
|
Also fixed a bug where `<span data-attr="" />` would render as `<span data-attr="undefined" />`.
|
|
@ -11,6 +11,7 @@ export function getAttr(attributes: Attribute[], name: string): Attribute | unde
|
||||||
|
|
||||||
/** Get TemplateNode attribute by value */
|
/** Get TemplateNode attribute by value */
|
||||||
export function getAttrValue(attributes: Attribute[], name: string): string | undefined {
|
export function getAttrValue(attributes: Attribute[], name: string): string | undefined {
|
||||||
|
if (attributes.length === 0) return '';
|
||||||
const attr = getAttr(attributes, name);
|
const attr = getAttr(attributes, name);
|
||||||
if (attr) {
|
if (attr) {
|
||||||
return attr.value[0]?.data;
|
return attr.value[0]?.data;
|
||||||
|
|
|
@ -58,6 +58,10 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
|
||||||
// note: attr.value shouldn’t be `undefined`, but a bad transform would cause a compile error here, so prevent that
|
// note: attr.value shouldn’t be `undefined`, but a bad transform would cause a compile error here, so prevent that
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (attr.value.length === 0) {
|
||||||
|
result[attr.name] = '""';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (attr.value.length > 1) {
|
if (attr.value.length > 1) {
|
||||||
result[attr.name] =
|
result[attr.name] =
|
||||||
'(' +
|
'(' +
|
||||||
|
@ -418,6 +422,8 @@ function dedent(str: string) {
|
||||||
return !arr || !first ? str : str.replace(new RegExp(`^[ \\t]{0,${first}}`, 'gm'), '');
|
return !arr || !first ? str : str.replace(new RegExp(`^[ \\t]{0,${first}}`, 'gm'), '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FALSY_EXPRESSIONS = new Set(['false','null','undefined','void 0']);
|
||||||
|
|
||||||
/** Compile page markup */
|
/** Compile page markup */
|
||||||
async function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions): Promise<string> {
|
async function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions): Promise<string> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -475,10 +481,12 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
|
||||||
}
|
}
|
||||||
// TODO Do we need to compile this now, or should we compile the entire module at the end?
|
// TODO Do we need to compile this now, or should we compile the entire module at the end?
|
||||||
let code = compileExpressionSafe(raw).trim().replace(/\;$/, '');
|
let code = compileExpressionSafe(raw).trim().replace(/\;$/, '');
|
||||||
if (state.markers.insideMarkdown) {
|
if (!FALSY_EXPRESSIONS.has(code)) {
|
||||||
buffers[curr] += `{${code}}`;
|
if (state.markers.insideMarkdown) {
|
||||||
} else {
|
buffers[curr] += `{${code}}`;
|
||||||
buffers[curr] += `,(${code})`;
|
} else {
|
||||||
|
buffers[curr] += `,(${code})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.skip();
|
this.skip();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -19,7 +19,9 @@ function* _h(tag: string, attrs: HProps, children: Array<HChild>) {
|
||||||
yield `<${tag}`;
|
yield `<${tag}`;
|
||||||
if (attrs) {
|
if (attrs) {
|
||||||
for (let [key, value] of Object.entries(attrs)) {
|
for (let [key, value] of Object.entries(attrs)) {
|
||||||
yield ` ${key}="${value}"`;
|
if (value === '') yield ` ${key}=""`;
|
||||||
|
else if (value == null) yield '';
|
||||||
|
else yield ` ${key}="${value}"`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield '>';
|
yield '>';
|
||||||
|
@ -37,7 +39,7 @@ function* _h(tag: string, attrs: HProps, children: Array<HChild>) {
|
||||||
yield child();
|
yield child();
|
||||||
} else if (typeof child === 'string') {
|
} else if (typeof child === 'string') {
|
||||||
yield child;
|
yield child;
|
||||||
} else if (!child) {
|
} else if (!child && child !== 0) {
|
||||||
// do nothing, safe to ignore falsey values.
|
// do nothing, safe to ignore falsey values.
|
||||||
} else {
|
} else {
|
||||||
yield child;
|
yield child;
|
||||||
|
|
28
packages/astro/test/astro-attrs.test.js
Normal file
28
packages/astro/test/astro-attrs.test.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { suite } from 'uvu';
|
||||||
|
import * as assert from 'uvu/assert';
|
||||||
|
import { doc } from './test-utils.js';
|
||||||
|
import { setup } from './helpers.js';
|
||||||
|
|
||||||
|
const Attributes = suite('Attributes test');
|
||||||
|
|
||||||
|
setup(Attributes, './fixtures/astro-attrs');
|
||||||
|
|
||||||
|
Attributes('Passes attributes to elements as expected', async ({ runtime }) => {
|
||||||
|
const result = await runtime.load('/');
|
||||||
|
if (result.error) throw new Error(result.error);
|
||||||
|
|
||||||
|
const $ = doc(result.contents);
|
||||||
|
|
||||||
|
const ids = ['false-str', 'true-str', 'false', 'true', 'empty', 'null', 'undefined'];
|
||||||
|
const specs = ['false', 'true', 'false', 'true', '', undefined, undefined];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
for (const id of ids) {
|
||||||
|
const spec = specs[i];
|
||||||
|
const attr = $(`#${id}`).attr('attr');
|
||||||
|
assert.equal(attr, spec, `Passes ${id} as "${spec}"`);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Attributes.run();
|
|
@ -58,4 +58,17 @@ Expressions('Allows multiple JSX children in mustache', async ({ runtime }) => {
|
||||||
assert.ok(result.contents.includes('#f') && !result.contents.includes('#t'));
|
assert.ok(result.contents.includes('#f') && !result.contents.includes('#t'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Expressions('Does not render falsy values using &&', async ({ runtime }) => {
|
||||||
|
const result = await runtime.load('/falsy');
|
||||||
|
if (result.error) throw new Error(result.error);
|
||||||
|
|
||||||
|
const $ = doc(result.contents);
|
||||||
|
|
||||||
|
assert.equal($('#true').length, 1, `Expected {true && <span id="true" />} to render`);
|
||||||
|
assert.equal($('#zero').text(), '0', `Expected {0 && "VALUE"} to render "0"`);
|
||||||
|
assert.equal($('#false').length, 0, `Expected {false && <span id="false" />} not to render`);
|
||||||
|
assert.equal($('#null').length, 0, `Expected {null && <span id="null" />} not to render`);
|
||||||
|
assert.equal($('#undefined').length, 0, `Expected {undefined && <span id="undefined" />} not to render`);
|
||||||
|
});
|
||||||
|
|
||||||
Expressions.run();
|
Expressions.run();
|
||||||
|
|
3
packages/astro/test/fixtures/astro-attrs/snowpack.config.json
vendored
Normal file
3
packages/astro/test/fixtures/astro-attrs/snowpack.config.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"workspaceRoot": "../../../../../"
|
||||||
|
}
|
8
packages/astro/test/fixtures/astro-attrs/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/astro-attrs/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<span id="false-str" attr="false" />
|
||||||
|
<span id="true-str" attr="true" />
|
||||||
|
<span id="true" attr={true} />
|
||||||
|
<span id="false" attr={false} />
|
||||||
|
<span id="empty" attr="" />
|
||||||
|
<span id="null" attr={null} />
|
||||||
|
<span id="undefined" attr={undefined} />
|
||||||
|
|
12
packages/astro/test/fixtures/astro-expr/src/pages/falsy.astro
vendored
Normal file
12
packages/astro/test/fixtures/astro-expr/src/pages/falsy.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>My site</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{false && <span id="false" />}
|
||||||
|
{null && <span id="null" />}
|
||||||
|
{undefined && <span id="undefined" />}
|
||||||
|
{true && <span id="true" />}
|
||||||
|
<span id="zero">{0 && "VALUE"}</span>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Add table
Reference in a new issue