Repository: alloy-framework/alloy
Pull request #71 (2025-03-27 18:48:07)

Fix issues with ArrayExpression and falsy elements
#### Summary
This PR introduces enhancements to the `For`, `List`, and `ArrayExpression` components to improve their handling of falsy values and provide more flexibility in rendering. Additionally, new test cases have been added to ensure the correctness of these changes.

#### Changes
1. **`For` Component**:
   - Added support for a `skipFalsy` prop, allowing users to control whether falsy values are skipped during iteration. Defaults to `true` if not specified.

2. **`List` Component**:
   - Introduced the `skipFalsy` prop in the `BaseListProps` interface.

3. **`ArrayExpression` Component**:
   - Updated to explicitly pass `skipFalsy={false}` to the `For` component, ensuring falsy values are rendered when required.
   - Refactored to use the `Show` component for conditional rendering of children, improving readability and maintainability.

4. **Tests**:
   - Added comprehensive test cases in array.test.tsx to validate the behavior of `ArrayExpression` with falsy values and children, including combinations of visible and invisible falsy children.

#### Motivation
We were seeing issues like
```tsx
<ArrayExpression jsValue={[false]}>
   Foo
</ArrayExpressiom>
 ```
 
Where the expected output would be `[false, Foo]` but it outputs `[, Foo]`

#### Example Usage
```tsx
<For each={[1, null, 2]} skipFalsy={false}>
  {(item) => item}
</For>


// Output: 1, null, 2
```

github-actions[bot]: <!--chronus-github-change-commenter-->
All changed packages have been documented.
 - :white_check_mark: `@alloy-js/core`
 - :white_check_mark: `@alloy-js/typescript`

<details>
<summary>Show changes</summary>

### `@alloy-js/core` - _fix_ [✏️](https://github.com/joheredi/alloy/edit/fix/ts-array-expression/.chronus/changes/fix-ts-array-expression-2025-2-27-18-43-4.md?pr=/alloy-framework/alloy/pull/71)
> Fix issues with ArrayExpression and falsy elements
### `@alloy-js/typescript` - _fix_ [✏️](https://github.com/joheredi/alloy/edit/fix/ts-array-expression/.chronus/changes/fix-ts-array-expression-2025-2-27-18-43-4.md?pr=/alloy-framework/alloy/pull/71)
> Fix issues with ArrayExpression and falsy elements
</details>

pkg-pr-new[bot]: 
[Open in Stackblitz](https://pkg.pr.new/template/1ec84b24-3092-4971-a9b7-8fa2ccc66a39)


<details><summary><b>@alloy-js/babel-plugin</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-plugin@71
```

</p></details>
      
    

<details><summary><b>@alloy-js/babel-plugin-jsx-dom-expressions</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-plugin-jsx-dom-expressions@71
```

</p></details>
      
    

<details><summary><b>@alloy-js/core</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/core@71
```

</p></details>
      
    

<details><summary><b>@alloy-js/babel-preset</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-preset@71
```

</p></details>
      
    

<details><summary><b>@alloy-js/create</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/create@71
```

</p></details>
      
    

<details><summary><b>@alloy-js/csharp</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/csharp@71
```

</p></details>
      
    

<details><summary><b>@alloy-js/java</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/java@71
```

</p></details>
      
    

<details><summary><b>@alloy-js/json</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/json@71
```

</p></details>
      
    

<details><summary><b>@alloy-js/prettier-plugin-alloy</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/prettier-plugin-alloy@71
```

</p></details>
      
    

<details><summary><b>@alloy-js/typescript</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/typescript@71
```

</p></details>
      
    

_commit: <a href="https://github.com/alloy-framework/alloy/runs/39541072753"><code>bde78dc</code></a>_


Patch:
diff --git a/.chronus/changes/fix-ts-array-expression-2025-2-27-18-43-4.md b/.chronus/changes/fix-ts-array-expression-2025-2-27-18-43-4.md
new file mode 100644
index 0000000..d0cb29e
--- /dev/null
+++ b/.chronus/changes/fix-ts-array-expression-2025-2-27-18-43-4.md
@@ -0,0 +1,7 @@
+---
+changeKind: fix
+packages:
+  - "@alloy-js/typescript"
+---
+
+Fix ArrayExpression not rendering falsy elements when passed a `jsValue`.
diff --git a/.chronus/changes/fix-ts-array-expression-2025-3-4-11-47-41.md b/.chronus/changes/fix-ts-array-expression-2025-3-4-11-47-41.md
new file mode 100644
index 0000000..bb09017
--- /dev/null
+++ b/.chronus/changes/fix-ts-array-expression-2025-3-4-11-47-41.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@alloy-js/core"
+---
+
+Add `skipFalsy` prop to `For` component for controlling whether to skip falsy values. Previously this defaulted to true, it now defaults to false (which likely just fixes bugs).
\ No newline at end of file
diff --git a/packages/core/src/components/For.tsx b/packages/core/src/components/For.tsx
index cdb75ba..363d4dc 100644
--- a/packages/core/src/components/For.tsx
+++ b/packages/core/src/components/For.tsx
@@ -27,6 +27,13 @@ export interface ForProps<
    * A function to call for each item.
    */
   children: (...args: [...ForCallbackArgs<T>, index: number]) => U;
+
+  /**
+   * Whether to skip falsy values. By default, falsy values are mapped. However,
+   * when mapping children, it is useful to skip falsy values, as it enables
+   * omitting list elements via patterns like `{condition && <ListItem />}`.
+   */
+  skipFalsy?: boolean;
 }
 
 export type ForSupportedCollections = any[] | Map<any, any> | Set<any>;
@@ -66,7 +73,7 @@ export function For<
 >(props: ForProps<T, U>) {
   const cb = props.children;
   const options = baseListPropsToMapJoinArgs(props);
-  options.skipFalsy = true;
+  options.skipFalsy = props.skipFalsy;
   return memo(() => {
     const maybeRef = props.each;
 
diff --git a/packages/core/src/components/List.tsx b/packages/core/src/components/List.tsx
index 5e07d8f..fe2b820 100644
--- a/packages/core/src/components/List.tsx
+++ b/packages/core/src/components/List.tsx
@@ -61,7 +61,7 @@ export function List(props: ListProps) {
     }),
   );
   return (
-    <For each={resolvedChildren} {...forProps}>
+    <For each={resolvedChildren} {...forProps} skipFalsy>
       {(child) => child}
     </For>
   );
diff --git a/packages/typescript/src/components/ArrayExpression.tsx b/packages/typescript/src/components/ArrayExpression.tsx
index 35d2631..0bc428c 100644
--- a/packages/typescript/src/components/ArrayExpression.tsx
+++ b/packages/typescript/src/components/ArrayExpression.tsx
@@ -1,4 +1,4 @@
-import { Children, For, Indent } from "@alloy-js/core";
+import { Children, childrenArray, For, Indent, Show } from "@alloy-js/core";
 import { ValueExpression } from "./ValueExpression.js";
 
 export interface ArrayExpressionProps {
@@ -7,6 +7,9 @@ export interface ArrayExpressionProps {
 }
 
 export function ArrayExpression(props: ArrayExpressionProps) {
+  const hasVisibleChildren =
+    childrenArray(() => props.children).filter((c) => Boolean(c)).length > 0;
+
   return (
     <group>
       [
@@ -14,16 +17,14 @@ export function ArrayExpression(props: ArrayExpressionProps) {
         <For each={props.jsValue ?? []} comma line>
           {(value) => <ValueExpression jsValue={value} />}
         </For>
-        {props.children && (
-          <>
-            {props.jsValue && props.jsValue.length > 0 && (
-              <>
-                ,<br />
-              </>
-            )}
-            {props.children}
-          </>
-        )}
+        <Show when={hasVisibleChildren}>
+          {props.jsValue && props.jsValue.length > 0 && (
+            <>
+              ,<br />
+            </>
+          )}
+          {props.children}
+        </Show>
       </Indent>
       <sbr />]
     </group>


Test patch:
diff --git a/packages/typescript/test/array.test.tsx b/packages/typescript/test/array.test.tsx
index 82a7ec8..b264554 100644
--- a/packages/typescript/test/array.test.tsx
+++ b/packages/typescript/test/array.test.tsx
@@ -45,3 +45,63 @@ it("accepts only children", () => {
     [[4, 5, 6]]
   `);
 });
+
+it("renders a falsy jsValue", () => {
+  expect(
+    toSourceText(<ArrayExpression jsValue={[undefined]}></ArrayExpression>),
+  ).toBe(d`
+    [undefined]
+  `);
+});
+
+it("renders a falsy jsValue with no leading comma", () => {
+  expect(toSourceText(<ArrayExpression jsValue={[null]}></ArrayExpression>))
+    .toBe(d`
+    [null]
+  `);
+});
+
+it("renders a falsy jsValue and children", () => {
+  expect(
+    toSourceText(<ArrayExpression jsValue={[false]}>"Hello"</ArrayExpression>),
+  ).toBe(d`
+    [false, "Hello"]
+  `);
+});
+
+it("renders a falsy jsValue but not invisible children", () => {
+  expect(
+    toSourceText(
+      <ArrayExpression jsValue={[false]}>
+        {null}
+        {undefined}
+      </ArrayExpression>,
+    ),
+  ).toBe(d`
+    [false]
+  `);
+});
+
+it("renders a falsy jsValue and visible children", () => {
+  expect(
+    toSourceText(<ArrayExpression jsValue={[false]}>"Hello"</ArrayExpression>),
+  ).toBe(d`
+    [false, "Hello"]
+  `);
+});
+
+it("renders a falsy jsValue and visible falsy children", () => {
+  expect(
+    toSourceText(<ArrayExpression jsValue={[false]}>false</ArrayExpression>),
+  ).toBe(d`
+      [false, false]
+    `);
+});
+
+it("renders a falsy jsValue but not invisible falsy children", () => {
+  expect(
+    toSourceText(<ArrayExpression jsValue={[false]}>{false}</ArrayExpression>),
+  ).toBe(d`
+      [false]
+    `);
+});