Skip to content

Conversation

@johnhaley81
Copy link
Contributor

@johnhaley81 johnhaley81 commented Aug 22, 2025

Summary

  • Allows for adding data attributes onto base level elements (div, span, form, etc...)
  • Compile time PPX transformation injects key/value into object being passed into React
  • Seamless integration with existing props (className, style, id, etc...)

Usage

let getGuid = () => /* gen a guid */;
<div data_testid="my-test-id" data_calculated_value=getGuid() className="my-class">
  {React.string("Hello")}
</div>

Once rendered, the element would look like:

<div class="my-class" data-testid="my-test-id" data-calculated-value="some guid">
  Hello
</div>

Code generation:

<div
  className="user-card"
  id="user-123"
  data_testid="user-profile"
  data_role="button"
  onClick={handleClick}>
  {React.string("Click me")}
</div>

Would get transformed into this at compile time:

external makeProps_div_a1b2c3d4 : // makeProps_{el-name}_{hash-of-props}
  className:string ->
  id:string ->
  data_testid:string [@mel.as "data-testid"] ->
  data_role:string [@mel.as "data-role"] ->
  onClick:('event -> unit) ->
  unit ->
  < className: string;
    id: string;
    data_testid: string;
    data_role: string;
    onClick: ('event -> unit) > Js.t = ""
[@@mel.obj] [@@warning "-32"] [@@merlin.hide]

ReactDOM.jsx "div"
  (((makeProps_div_a1b2c3d4)[@merlin.hide ])
    ~className:"user-card"
    ~id:"user-123"
    ~data_testid:"user-profile"
    ~data_role:"button"
    ~onClick:handleClick
    ())

If there are no existing data_* props, the PPX behaves exactly as it did before.

Test Coverage

  • Basic PPX Transformation Testing
    • Single & multiple data attributes on various element types (div, span, button, input)
    • Kebab-case transformation: Verifies data_test_id → "data-test-id" conversion
    • Mixed props compilation: Data attributes alongside standard props (className, id, style, onClick)
    • Different element types: Ensures transformation works across all HTML elements
  • Dynamic Values & Expression Handling
    • Variable assignments: data_testid={testId} with dynamic string variables
    • String operations: Complex expressions like data_testid={prefix ++ "-" ++ userId ++ "-profile"}
    • Function results: data_env={getEnvironment()} and data_score={calculateScore(15, 27)}
    • Pattern matching: Using switch expressions to generate data attribute values
  • React Integration & Component Context
    • Nested module compilation: Data attributes within deeply nested ReasonML modules
    • Component composition: Parent-child component relationships with data attribute inheritance
    • Complex DOM structures: Multi-level nested elements with mixed element types and data attributes
    • Prop passing: Data attributes that receive values from component props
  • Edge Cases & Deduplication
    • Attribute reuse: Same data_category attribute used across multiple elements without conflicts
    • Different element types: Same data attributes on different HTML elements (div vs span)
    • Index-based attributes: Dynamic attributes with iterative values (data_index="1", data_index="2")
    • Runtime verification: All tests use ReactTestingLibrary.render() and DOM getAttribute() to verify
      actual browser behavior

Since I don't think you can do a merge train between PRs from different forks, this PR also includes the formatting commit from #900 so the changes seem a lot larger than they are. I split out the actual work from the formatting in b3f1f52 so you can review that in isolation.

johnhaley81 and others added 3 commits August 22, 2025 02:50
- Apply consistent formatting across the codebase using OCamlformat 0.27.0
- Update indentation and spacing in OCaml files
- Ensure code style consistency

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Add dataAttrs field to domProps type (option(Js.Dict.t(string)))
- Implement JSX runtime wrappers to process dataAttrs into data-* attributes
- Transform dictionary keys like 'testid' to 'data-testid' in rendered HTML
- Add comprehensive test suite covering single/multiple attributes and edge cases
- Add demo component showcasing dataAttrs functionality
- Maintain backward compatibility with existing props and JSX patterns
- Format demo/main.re with consistent indentation and line breaks
- Format src/ReactDOM.re and src/ReactDOM.rei with updated style
- Format test/ReactDOM__test.re with improved readability
- Ensure all code follows OCamlformat 0.27.0 conventions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@@ -1 +1 @@
version = 0.26.0
version = 0.27.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't change the ocamlformat version in a PR that adds a feature

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had that split out to #900, if that PR is good then this change will be removed from this PR when that other is added. I don't know how to do a merge chain from multiple PRs from a fork :(

Comment on lines 1710 to 1722
[@mel.module "react/jsx-runtime"]
external jsxKeyed: (string, domProps, ~key: string=?, unit) => React.element =
"jsx";
let jsxKeyed: (string, domProps, ~key: string=?, unit) => React.element;

[@mel.module "react/jsx-runtime"]
external jsx: (string, domProps) => React.element = "jsx";
let jsx: (string, domProps) => React.element;

[@mel.module "react/jsx-runtime"]
external jsxs: (string, domProps) => React.element = "jsxs";
let jsxs: (string, domProps) => React.element;

[@mel.module "react/jsx-runtime"]
external jsxsKeyed: (string, domProps, ~key: string=?, unit) => React.element =
"jsxs";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this change intentional?

Copy link
Contributor Author

@johnhaley81 johnhaley81 Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Since I wrapped these functions, this interface definition was no longer valid. The [@mel.module "react/jsx-runtime"] external was not true so I had to remove it. Functionally, I don't think there is a change however.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean with The [@mel.module "react/jsx-runtime"] external was not true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is removed now

src/ReactDOM.re Outdated
suppressHydrationWarning: option(bool),
/* data attributes */
[@mel.optional]
dataAttrs: option(Js.Dict.t(string)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dataAttrs isn't a proper name imo. The HTML name feels right for it "dataset" (per https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset)

I like the idea of Js.dict instead of Js.t

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the dataset prop isn't supported in React, this would need to be a ppx transformation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converted this to a PPX transformation

src/ReactDOM.re Outdated
external jsxKeyed: (string, domProps, ~key: string=?, unit) => React.element =
"jsx";
let jsxKeyed = (component: string, props: domProps, ~key=?, ()) =>
jsxKeyed(component, processDataAttrs(props), ~key?, ());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As ReactDOM tries not to have any runtime, this application is undesired

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migrated to a PPX transformation instead of a runtime one

src/ReactDOM.re Outdated
"createElement";

// Helper function to process dataAttrs
let processDataAttrs = (props: domProps): domProps => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside from the runtime cost, this implementation contains too much of everything. 3 Obj.magics in 30 lines:

  1. to cast props into to Js.Dict.t
  2. because some casting has been made, dataAttrs needs to be treated as Js.Dict.t again
  3. to cast Js.Dict.t back to props

also, iterating over all props when there's data-* feels mega wrong

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this is removed now

);
let html = ReactDOMServer.renderToString(element);

// All data attributes should be present
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all this comments look AI-generated, which is fine but there's no need for them

Comment on lines 211 to 220
let element =
ReactDOM.jsx(
"div",
ReactDOM.domProps(
~dataAttrs=
[("alpha", "1"), ("beta", "2"), ("gamma", "3")]
|> Js.Dict.fromList,
(),
),
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a reason file, why not use JSX?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all removed now and made a bit more of a thorough testing suite

johnhaley81 and others added 11 commits August 23, 2025 18:36
Added CLAUDE.md with project overview, development commands,
structure, and workflow guidance for Claude Code integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
This commit implements compile-time data attribute transformation with zero runtime overhead and full backwards compatibility.

## Features
- **Zero Runtime Overhead**: Data attributes are transformed at compile-time using external functions with [@mel.obj] and [@mel.as] annotations
- **Full Backwards Compatibility**: JSX elements without data attributes continue using the standard ReactDOM.domProps path
- **Automatic Detection**: PPX automatically detects data_* attributes in JSX and generates appropriate external functions
- **Name Transformation**: Converts data_testid to data-testid, data_custom to data-custom, etc.

## Implementation Details
- **PPX Detection**: `isDataProp` function detects data attributes by checking for "data_" prefix
- **External Generation**: Creates unique external functions per JSX element with data attributes
- **Module Injection**: Injects external declarations at module level for proper scoping
- **Hash-based Naming**: Uses element type + props hash for unique function names to avoid conflicts

## Examples
```reason
// Compile-time transformation with zero runtime overhead
<div data_testid="foo" className="bar" />

// Generates:
external makeProps_div_xyz :
  data_testid:((string)[@mel.as "data-testid"]) ->
  className:string -> unit -> 'a = "" [@@mel.obj]

// Backwards compatibility - no data attributes
<div className="regular" /> // Uses ReactDOM.domProps
```

## Architecture
Uses external function per JSX element approach for optimal performance:
- Each data attribute JSX element gets its own external function
- Functions are uniquely named using element type + content hash
- External declarations injected at module level
- Melange optimizes external calls to plain JavaScript object creation
Updates the existing docs/adding-data-props.md to reflect the new direct JSX support for data attributes with zero runtime overhead.

## Changes
- Replace old Spread component workaround with direct JSX examples
- Document compile-time transformation of data_testid → data-testid
- Add migration examples from old workarounds to new JSX syntax
- Emphasize zero runtime overhead and backwards compatibility
- Include technical details about PPX transformation

## Examples Added
- Basic usage: <div data_testid="test" />
- Combined with other props: <input data_testid="field" className="form" />
- Migration from <Spread props={"data-cy": "test"}> to <div data_cy="test" />

The documentation now accurately reflects the implemented feature rather than describing validation (which isn't implemented).
Removed 28 comments that merely describe what the code is doing, keeping only 4 comments that explain why or provide important context.

## Removed Comments:
- Function/module header comments that repeat names
- Step-by-step operation descriptions that are obvious from code
- Example labels and transformation descriptions in demo
- Obvious code operation descriptions

## Kept Comments:
- `(* Empty string for [@mel.obj] *)` - explains non-obvious mel.obj requirement
- `(* [@mel.obj] adds unit automatically *)` - explains filtering logic
- `(* Use standard domProps for backwards compatibility *)` - explains why this path exists
- `// Only works on DOM elements, not React components` - important limitation

This reduces comment noise by 87.5% while preserving valuable context.
- Update PPX test snapshots to reflect new ReactDOM.domProps pattern
- Fix syntax error in ReactDOM__test.re cloneElement call
- All tests now compile successfully with data attributes support
- Build completes without errors for entire project

The data attributes feature now works correctly, generating zero-runtime
external functions for JSX elements with data_* props while maintaining
backward compatibility for elements without data attributes.
- Fix extra unit argument bug in domProps external generation
- Fix kebab-case conversion to properly transform data_test_id → data-test-id
- Restore working data attributes tests in ReactDOM__test.re
- Add comprehensive DataAttrs__test.re validation suite
- Update demo to reflect implemented feature
- Add externalExists function to prevent duplicate external declarations
- Implement deduplication check in domProps to ensure unique function names
- Fix kebab-case conversion for data attributes (data_test_id → data-test-id)
- Add comprehensive test suite with 13 data attributes test cases
- Resolve "Unbound value makeProps_div_*" compilation errors when multiple
  JSX elements have identical prop signatures

The PPX now generates unique external function names using element name
and property signature hash, preventing compilation failures while
maintaining full backward compatibility.

All 49 tests pass including comprehensive data attributes scenarios.
Clean up temporary test file that was used during development and testing
of the data attributes feature. The comprehensive data attributes tests
are now properly integrated into the existing test suite.
Ignore Serena project metadata directory to keep repository clean.
@johnhaley81 johnhaley81 marked this pull request as draft August 29, 2025 00:24
@johnhaley81
Copy link
Contributor Author

Putting this back into draft to try and do a PPX implementation with no overhead

johnhaley81 and others added 13 commits September 1, 2025 22:06
… level

**Root Cause**: External declarations for DOM elements with data attributes
were generated at expression level but only injected at structure level,
causing "Unbound value makeProps_div_*" compilation errors.

**Technical Analysis**:
- DOM elements processed in `transformLowercaseCall3` (expression level)
- External declarations stored in `externalDeclarations` ref
- But externals only injected in `method! structure` which resets the ref
- Result: DOM element externals never reach final AST output

**Solution**: Modified structure processing to preserve externals across
nested contexts by:
1. Storing parent externals before processing
2. Collecting all externals (React components + DOM elements)
3. Restoring and accumulating externals properly
4. Injecting all externals at structure boundaries

**Maintains**:
- Existing functionality for React components
- Deduplication logic (externalExists check)
- Zero-runtime performance (compile-time only)
- Kebab-case transformation (data_test_id → data-test-id)

**Fixes**: All DOM elements with data attributes now compile successfully
including nested modules, functions, and complex nested structures.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Documents the root cause, solution, and technical validation of the
DOM element external declarations fix. Provides detailed explanation
of why React components worked but DOM elements failed, and how the
fix preserves externals across nested processing contexts.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
**Summary**: Add comprehensive test suite and working demo that validates
the PPX external declaration fix for DOM elements with data attributes.
This satisfies the "DO NOT DO THE WORKAROUND. THIS IS REQUIRED TO WORK"
requirement through proper PPX-level implementation.

**TDD Approach Completed**:
✓ Created failing tests that reproduced the original compilation errors
✓ Implemented PPX fix (committed in 2a41068)
✓ Verified all tests now pass with zero compilation errors

**Test Coverage Added**:
- DataAttributes_ClientSide__test.re: Client-side compilation validation
- PPX_ExternalGeneration__test.re: PPX external generation mechanics
- DataAttributes_Integration__test.re: React integration scenarios
- DataAttributes_Demo__test.re: Real-world demo compilation tests

**Demo Enhancement**:
- demo/main.re: Updated with working data attributes examples
- Demonstrates single/multiple data attributes in nested modules
- Proves the fix handles complex prop combinations
- Shows before/after comparison of compilation behavior

**Technical Validation**:
- All data attributes compile without "Unbound value makeProps_div_*" errors
- Works correctly in nested modules (original failure case)
- Preserves zero-runtime overhead (compile-time transformation)
- Maintains deduplication and kebab-case conversion
- No regressions to existing React component functionality

**Original Issue Resolution**:
The PPX now properly preserves and injects DOM element external declarations
at structure boundaries, enabling data attributes to work seamlessly across
all module contexts as required.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Remove PPX_FIX_ANALYSIS.md documentation file
- Reset demo/main.re to feature/data-attrs-support state
- Remove all explanatory comments from test files
- Remove implementation comments from ppx/reason_react_ppx.ml
- Keep only essential code without analysis artifacts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Fix critical PPX bug: external function calls now pass correct arguments with unit parameter
- Add proper type handling for children (React.element), style (ReactDOM.Style.t), and event handlers
- Update test files to use proper ReactTestingLibrary patterns instead of anti-patterns
- Replace expect(Js.typeof())->toBe("object") with actual DOM attribute testing
- Remove testAsync patterns in favor of synchronous tests with proper rendering
- Revert demo text from "PPX Fix Working" back to "Zero-Runtime Data Attributes Demo"

The PPX now correctly generates external function signatures and calls them with
matching arguments, resolving the "expression has type React.element but expected
type string" compilation errors.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Remove technical PPX implementation details and focus on real-world usage
- Showcase key use cases: testing automation, analytics tracking, component state, and accessibility
- Demonstrate common patterns like data-testid, data-cy, data-action, data-theme, etc.
- Provide cleaner, more practical examples for developers to reference
- Remove before/after PPX fix explanations to focus on actual feature benefits

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Test variable assignments and expressions in data attributes
- Verify string concatenation and conditional logic work correctly
- Add tests for function results and complex expressions
- Include pattern matching results as data attribute values
- Test conversion functions like string_of_int work properly
- Demonstrate data attributes work beyond just string literals

These tests ensure the PPX correctly handles all forms of dynamic
value assignment, not just static strings, proving robust type-safe
data attribute support.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Add [@mel.send] attribute to getAttribute external declarations in test files
to properly bind to DOM element methods at runtime. This resolves the
"ReferenceError: getAttribute is not defined" error when tests execute.

All data attributes tests now pass successfully:
- PPX_ExternalGeneration__test.re (10 tests)
- DataAttributes_ClientSide__test.re (24 tests)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
…g suppression

- Consolidate multiple test files into focused compilation and React tests
- Add warning suppression attribute to external declarations in PPX
- Remove redundant test files that were testing the same functionality
- Maintain comprehensive test coverage with cleaner organization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Remove test/DataAttributes_React__test.re as it duplicates runtime testing
- Keep test/DataAttributes_Compilation__test.re which provides sufficient coverage
- Both files were testing runtime behavior, not actual compilation vs integration
- Simplifies test suite while maintaining comprehensive data attributes coverage

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Change ppx/test/dune to use %{bin:reason-react-ppx} instead of hardcoded path
- Change ppx/test/ppx.sh to use reason-react-ppx binary from PATH
- PPX tests now work correctly with opam exec environment
- Removes dependency on specific build directory structure

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Apply consistent code formatting across all OCaml/ReasonML files
- Improve readability and maintain coding standards
- Auto-format with dune build @fmt --auto-promote

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Update comment to better explain when standard domProps is used
- Clarify that fallback occurs when data attributes are not present
- More accurate description of conditional logic flow

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Hide generated external declarations with [@merlin.hide] attribute
- Hide external function calls with [@merlin.hide] attribute
- Provides consistent IDE experience between data-attr and standard paths
- Keeps PPX implementation details invisible to developer tooling
- Improves autocomplete and hover experience by hiding generated code

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@johnhaley81 johnhaley81 marked this pull request as ready for review September 3, 2025 21:20
@johnhaley81
Copy link
Contributor Author

@davesnx ok I was able to get the PPX version of this working with no runtime overhead. Added a bunch of tests to make sure that it was able to handle lots of different scenarios and configurations.

The PPX is exactly the same if you don't use any data_* props.

There are a bunch of commits on this PR as I was trying to figure out how to get this done. LMK if you want me to clean those up. Like I said, I put the code format changes into #900 so if you are good with that and merge it, it should remove most of the code formatting changes in this PR.

Comment on lines +1519 to +1530
let parentExternals = !externalDeclarations in
externalDeclarations := [];

let processedStru =
super#structure ctxt (reactComponentTransform ~ctxt self stru)
in

let allExternals = List.rev !externalDeclarations in

externalDeclarations := allExternals @ parentExternals;

allExternals @ processedStru
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add a new externals?

Comment on lines +111 to +129
let rec buildArrowType ~loc props =
match props with
| [] ->
Builder.ptyp_arrow ~loc Nolabel
(Builder.ptyp_constr ~loc { txt = Lident "unit"; loc } [])
(Builder.ptyp_var ~loc "a")
| (label, _) :: rest ->
let propName = getLabelOrEmpty label in
let propType =
if propName = "children" then
Builder.ptyp_constr ~loc
{ txt = Ldot (Lident "React", "element"); loc }
[]
else if propName = "style" then
Builder.ptyp_constr ~loc
{ txt = Ldot (Ldot (Lident "ReactDOM", "Style"), "t"); loc }
[]
else if propName = "onClick" then
Builder.ptyp_arrow ~loc Nolabel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't followed the transformations yet, but this looks extremly ad-hoc for 3 props?

I don't know if this is intentional, ai artifact or not even used, but I suspect this isn't the quality we can afford in a ppx that carries a ton of tech debt already.

(Builder.pexp_ident ~loc:applyLoc ~attrs:merlinHideAttrs
{ loc; txt = Ldot (Lident "ReactDOM", "domProps") })
props
let domProps ~applyLoc ~loc ?(elementName = "element") props =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module is just the API of the reason-react to be accesssed into the ppx. Don't add logic to it.

# Editor
/.idea/
_opam
.serena/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's serena?

Copy link
Member

@davesnx davesnx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned about this PR because it doesn't explain how, and there are no cram tests to explore the ml output (which may affect DX).

There's also the approach of supporting data_*, which is rather weird to not discuss other syntaxes. dataUppercase has problems, but also data_* does, which aren't mentioned.

But I won't spend more time reviewing this since there's clearly an abuse of LLM coding and a lack of care that reason-react-ppx needs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants