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:
Nate Moore 2021-05-28 17:02:04 -05:00 committed by GitHub
parent e08abacfee
commit 3d20623c32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 88 additions and 6 deletions

View 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" />`.

View file

@ -11,6 +11,7 @@ export function getAttr(attributes: Attribute[], name: string): Attribute | unde
/** Get TemplateNode attribute by value */
export function getAttrValue(attributes: Attribute[], name: string): string | undefined {
if (attributes.length === 0) return '';
const attr = getAttr(attributes, name);
if (attr) {
return attr.value[0]?.data;

View file

@ -58,6 +58,10 @@ function getAttributes(attrs: Attribute[]): Record<string, string> {
// note: attr.value shouldnt be `undefined`, but a bad transform would cause a compile error here, so prevent that
continue;
}
if (attr.value.length === 0) {
result[attr.name] = '""';
continue;
}
if (attr.value.length > 1) {
result[attr.name] =
'(' +
@ -418,6 +422,8 @@ function dedent(str: string) {
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 */
async function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions): Promise<string> {
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?
let code = compileExpressionSafe(raw).trim().replace(/\;$/, '');
if (state.markers.insideMarkdown) {
buffers[curr] += `{${code}}`;
} else {
buffers[curr] += `,(${code})`;
if (!FALSY_EXPRESSIONS.has(code)) {
if (state.markers.insideMarkdown) {
buffers[curr] += `{${code}}`;
} else {
buffers[curr] += `,(${code})`;
}
}
this.skip();
break;

View file

@ -19,7 +19,9 @@ function* _h(tag: string, attrs: HProps, children: Array<HChild>) {
yield `<${tag}`;
if (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 '>';
@ -37,7 +39,7 @@ function* _h(tag: string, attrs: HProps, children: Array<HChild>) {
yield child();
} else if (typeof child === 'string') {
yield child;
} else if (!child) {
} else if (!child && child !== 0) {
// do nothing, safe to ignore falsey values.
} else {
yield child;

View 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();

View file

@ -58,4 +58,17 @@ Expressions('Allows multiple JSX children in mustache', async ({ runtime }) => {
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();

View file

@ -0,0 +1,3 @@
{
"workspaceRoot": "../../../../../"
}

View 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} />

View 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>