Custom data-attributes in JSX

Hi,

Disclaimer: I am new to Rescript world!

At my workplace we are creating a design-system that manipulates the behavior of a component using custom data-attributes that we add to JSX elements.

In the TypeScript/JavaScript world, it looks roughly like this:

const Component = () => <div data-custom-state="true" />

Unfortunately, I’ve tried several different ways to port this to a React component written in Rescript, and unfortunately, each time the compiler has a problem with declared data-attributes.

Without this functionality, I won’t be able to use Rescript at work, and I care a lot about it.

Is there a special syntax for using data-attributes in JSX in Rescript that I don’t know?

Thanks for any help/answers!

1 Like

The upcoming ReScript version 11.1.0 is for you! It will ship both with a generic JSX transform which includes among other things the possibility to extend the attributes of lowercase JSX tags and also finally allows hyphens in tag names. Although that already worked before with a special escape syntax:

let component = () => <div \"data-custom-state"=true />

I created a small example to show how it is now possible to extend JSX attributes:

module MyOverrides = {
  module Elements = {
    // Extend the available lowercase JSX props here.
    type props = {
      ...JsxDOM.domProps,
      \"data-custom-state": bool,
    }

    @module("react")
    external jsx: (string, props) => Jsx.element = "jsx"
  }
}

@@jsxConfig({module_: "MyOverrides"})

let component = () => <div \"data-custom-state"=true />

[Playground]

What is put in the jsxConfig annotation here is usually done in rescript.json where you can add the name of a special file/module like the MyOverrides one above to make it available in your whole codebase. It’s incomplete though, since I don’t know myself what is missing to make it fully work with React for instance, but maybe the creator of that feature (@zth) can explain further?

4 Likes

Here are some docs for the generic JSX transform: JSX | ReScript Language Manual

One thing to add is that you don’t need to use hyphens to make that work, you can use @as:

type props = {
  ...JsxDOM.domProps,
  @as("data-custom-state") dataCustomState: bool,
}

But other than that, @fham covers it very well. You can of course configure the JSX module at the rescript.json level too so you don’t need to do the file level config. And don’t be afraid of copy pasting and vendoring things, including making modifications to them, from other libs like RescriptReact. It’s a great way to tailor things the way you need for your specific use case.

3 Likes

Thank you very much for the information, it helped me a lot!

By the way, when is the planned release of version 11.1?

1 Like

Should be in the next couple of days. We are preparing the next main release currently. But do not hesitate to install one of the release candidates and try it now.

Btw.: When you use npm create rescript-app it suggests experimental versions.

1 Like

Hi,

I want to propose Rescript in the company, starting with small individual components.

The project I am working on uses libraries that rely heavily on data-attributes. In our own components we also use data-attributes to store state information, control CSS styles, etc.

I’m new to Rescript and don’t really understand the solution you’ve showed, ie. from my point of view each component should have data-attributes defined only for it, and in the code example you propose you create MyOverrides module that overwrites the whole Elements module giving all components "data-custom-state’': bool (at least I understand it that way)

Doesn’t Rescript really allow you to work with legitimate HTML tags like data-attributes in a more “normal” way?

Below I have prepared an example of a component in JSX/TypeScript that uses various data-attributes.

Could you use it and convert it to Rescript/JSX to demonstrate how to work with Rescript in practice in applications with a strong emphasis on data-attributes? Thank you for your help!

TSX:

import React from 'react';

// Types for variants and checkbox states
type Variant = 'warning' | 'error' | 'info';
type CheckboxState = 'checked' | 'unchecked';

// Icon.tsx component that changes based on data-variant
const Icon: React.FC<{ variant: Variant }> = ({ variant }) => {
  return (
    <span data-variant={variant} className="icon">
      {variant === 'warning' && '⚠️'}
      {variant === 'error' && '❌'}
      {variant === 'info' && 'ℹ️'}
    </span>
  );
};

// Title.tsx component
const Title: React.FC<{ title: string }> = ({ title }) => (
  <span className="title">{title}</span>
);

// Header component that contains Icon and Title
const Header: React.FC<{ variant: Variant; title: string }> = ({ variant, title }) => (
  <div className="header">
    <Icon variant={variant} />
    <Title title={title} />
  </div>
);

// Content.tsx component
const Content: React.FC = ({ children }) => (
  <div className="content">
    {children}
  </div>
);

// Checkbox.tsx component with data-state attribute
const Checkbox: React.FC<{ label: string; state: CheckboxState; onChange: () => void }> = ({ label, state, onChange }) => (
  <label data-state={state} className="checkbox">
    <input type="checkbox" checked={state === 'checked'} onChange={onChange} />
    {label}
  </label>
);

// AcceptButton.tsx component
const AcceptButton: React.FC<{ onClick: () => void }> = ({ onClick }) => (
  <button onClick={onClick} className="accept-button">
    Accept
  </button>
);

// Announcement.tsx component with data-variant attribute
const Announcement: React.FC<{ variant: Variant; title: string; content: string }> = ({ variant, title, content }) => {
  const [checkbox1State, setCheckbox1State] = React.useState<CheckboxState>('unchecked');
  const [checkbox2State, setCheckbox2State] = React.useState<CheckboxState>('unchecked');

  return (
    <div className={`announcement ${variant}`} data-variant={variant}>
      <Header variant={variant} title={title} />
      <Content>{content}</Content>
      <Checkbox
        label="I agree to the terms"
        state={checkbox1State}
        onChange={() => setCheckbox1State(checkbox1State === 'checked' ? 'unchecked' : 'checked')}
      />
      <Checkbox
        label="I agree to the privacy policy"
        state={checkbox2State}
        onChange={() => setCheckbox2State(checkbox2State === 'checked' ? 'unchecked' : 'checked')}
      />
      <AcceptButton onClick={() => alert('Accepted')} />
    </div>
  );
};

CSS:

.announcement[data-variant="warning"] {
  background-color: yellow;
  color: black;
}

.announcement[data-variant="error"] {
  background-color: red;
  color: white;
}

.announcement[data-variant="info"] {
  background-color: blue;
  color: white;
}

.icon[data-variant="warning"] {
  color: orange;
}

.icon[data-variant="error"] {
  color: red;
}

.icon[data-variant="info"] {
  color: blue;
}

.checkbox[data-state="checked"] {
  font-weight: bold;
}
1 Like

So you’re saying all your elements don’t expect the same data attributes? How would you do it in a type-safe way in typescript?

My advice would be to override the element attributes with all the data attributes you expect to use, then wrap those lowercase components inside uppercase components that only expect the right data attributes for the given lowercase component.

This way you’d have good DX and type-safety with a minimal runtime overhead.

Don’t hesitate to reply if this doesn’t solve your problem.

You can find bellow a working version in ReScript of your code, don’t forget to enable auto-run if you want to test it live.

You can notice how close to the typescript version it is, it’s almost identical, you basically need to remove the type annotations and the return keyword.

1 Like

Small correction, the binding should obviously not be to “data-custom-state”, but “data-variant”.

1 Like

thanks @fham, I corrected and edited the link!

1 Like

Thank you for the link to playground with the code example.

Now I understand where I was making a mistake. Thank You a lot!

I’ll admit that this overwriting of modules, adding external is a bit confusing.

Do future versions of Rescript have plans to simplify this?

Many of the UI libraries in the React ecosystem use data-attributes, it would be nice if they could be declared without all this boilerplate (in the commercial project I work in, we have about 1200 components in the project - our own and from UI libraries that rely heavily on data-attributes to store meta-data and control CSS styles).

I’m having a hard time imagining arguments to convince the rest of the team that having to define these props override declarations in each component file to add data-attributes is, as you pointed out, “close to a version of typescript” :frowning:

1 Like

No no, you only have to define it once in your whole project!

2 Likes