Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom JSX/TSX components #245

Open
mszmida opened this issue Mar 27, 2024 · 2 comments
Open

Custom JSX/TSX components #245

mszmida opened this issue Mar 27, 2024 · 2 comments
Labels
question Further information is requested

Comments

@mszmida
Copy link
Contributor

mszmida commented Mar 27, 2024

Hello,

I really appreciate your vision @tipiirai to create finally framework that works closely with the web standards, however in many cases your documentation is still very laconic. Looks like an area that needs improvement 😄

Currently I am interested in integration with some headless UI library that works for vanilla JS and not only for React. I found Zag which claims is a framework agnostic library for implementing even complex UI components. Unfortunately looks like they do not have any dedicated tools for integration with pure JavaScript implementation. Looking through your documentation I spotted this section Custom components and more importantly this section TSX components that shows how to create custom TSX components which looks really promising. I have been trying to create simple component, but looks like I do not understand something or violate some core concepts of your framework.

My main idea is to create a custom component that integrates with the Zag framework according to their docs Number Input (React example):

import * as numberInput from "@zag-js/number-input"
import { useMachine, normalizeProps } from "@zag-js/react"

export function NumberInput() {
  const [state, send] = useMachine(numberInput.machine({ id: "1" }))
  
  const api = numberInput.connect(state, send, normalizeProps)

  return (
    <div {...api.rootProps}>
      <label {...api.labelProps}>Enter number:</label>
      <div>
        <button {...api.decrementTriggerProps}>DEC</button>
        <input {...api.inputProps} />
        <button {...api.incrementTriggerProps}>INC</button>
      </div>
    </div>
  )
}

As visible above all properties are passed to the HTML elements with the spread operator syntax which works in JSX.

In my case I wasn't even able to create the simplest custom JSX/TSX component:

export function MyCustomComponent(props: { message: string }) {
  return (
    <h1 style={{ color: 'red' }}>{ props.message }</h1>
  );
}

I couldn't even import the package correctly to make it work like below:

// tailing "/index.js" is needed under Bun (v1.0.21)
import { nuemark } from 'nuemark/index.js'

const { html } = nuemark(`Hello, World`)

console.info(html) // 

Hello, World

I was trying to run this code in the browser context by implementing library.js file and then use custom component in my reactive island. I have a feeling that you had another idea behind this API.

Bunch of questions below:

  1. Could you elaborate in details how this API supposed to be run?
  2. Is it possible to create custom JSX/TSX components and then use them in other reactive components? This would open a way to integrate with any other framework that works based on JSX/TSX.
  3. Is there any other way to integrate with Zag framework?
  4. Do you know other headless UI library that could integrate better with Nue?

I am aware of your development focus and roadmap, but I really would like to start building product with your framework. Any details and help from your side will be very appreciated!

Regards

@nobkd nobkd added the question Further information is requested label Mar 27, 2024
@tipiirai
Copy link
Contributor

Documentation is indeed very laconic currently. It's both lagging behind the code and fails to explain some of the core concepts.

Here is a JSX working test case for building a custom Nuemark extension:

https://github.com/nuejs/nue/blob/master/packages/nuemark/test/nuemark.test.js#L430

That's all there is now with regards to extension. Hope that helps. The idea is to finish the core pieces first and launch a v1.0, before moving into extensions and integrations. Nue is still an early beta.

@mszmida
Copy link
Contributor Author

mszmida commented Apr 17, 2024

Thank you very much for your response and advice @tipiirai!

At the beginning I would like to sorry for my late response and share my results so maybe it can help also other developers to understand better the problem and solution.

Zag integration results

Let's forget about Zag. I was able to implement working integration with a simple Number Input component by myself, but I was not happy with the code. One of the biggest reason is the lack of the spread properties feature available in JSX/TSX. I had to implement my own solution for that. Another reason is that this library has its own logic implementation for each component based on the state machines. This is another custom abstraction layer and the library itself is also very much tightly coupled to the React, Solid, Vue and heavily relies on the concepts from these frameworks.

Because the main reason of the Nue framework is to be closer to the web standards I decided to search something else and I found Shoelace which is a collection of UI components build on web components standard. Thanks to this the components implementation is fully framework agnostic. Library looks mature and I am going to test these components together with Nue and built SPA with forms. It should eliminate any need to create custom integrations and so on.

Custom JSX/TSX components

Coming back now to the original problem. I decided to investigate further the custom components problem inspired by the Custom components section and especially TSX components section. My main idea was to create custom TSX components and render them on the client side in the browser in my SPA. In my previous attempts, I didn't know how to properly import Nuemark API. The reason was that I didn't read carefully Unbundled distribution section which says:

The default is to build, but not bundle. That is: the imported dependencies are not inlined into the file itself to form a single, bigger JS bundle.

So I decided to bundle the Nuemark API. Unfortunately the Bundled distribution section was not quite clear for me. I couldn't understand how the bundle: [ index.ts ] option works. Whenever I tried to import Nuemark API like this:

// *.nue file
import { nuemarkdown } from "nuemark";

nothing happened... After transpiling the project files, the import statement remained unchanged. To understand what was going on, I had to debug Nue CLI code to realize that the *.nue files are not processed by Bun.build() at all. The are just compiled by your custom mechanism into *.js files. Only the *.js and *.ts files are processed by Bun.build() and the bundle option inside *.yaml config files is used to determine in which files from the project, import statements should be inlined after transpilation process. It was eureka! After that I created index.ts file in my SPA project root with the content:

// index.ts
export { nuemarkdown } from "nuemark";

The Nuemark API has been finally inlined into generated index.js file. Victory! ✌

The next step was to create TSX components that I could render in my SPA. According to the TSX components section I added following dependencies to the project folder:

// run in terminal
bun add react
bun add react-dom

and created TSX components library:

// components.tsx

// import of the React SSR (server-side rendering)
import { renderToString } from "react-dom/server";

// first custom tag
function MyTag(props: { message: string }) {
  return <h1 style={{ color: "red" }}>{props.message}</h1>;
}

// second custom tag
function MyTag2(props: { message: string }) {
  return <h1 style={{ color: "blue" }}>{props.message}</h1>;
}

const components = { MyTag, MyTag2 };

// Make lib compatible with Nuemark
export const tsx_lib = Object.keys(components).map((name) => {
  return {
    name,
    render: function (data, lib) {
      return renderToString(components[name](data, lib));
    },
  };
});

After debugging your code I already knew that Nue CLI treats *.tsx files as static and copies them as is to the .dist/dev directory so I had to transpile the file manually with Bun:

bun build ./components.tsx --outdir ./.dist/dev

After that the components.js file appeared with all React dependencies inlined in place and I could finally create custom Nue component that gives possibility to render custom TSX components in my SPA:

// tsx-component.nue
<script>
  import { nuemarkdown } from './index.js'
  import { tsx_lib } from './components.js'
</script>

<div @name="tsx-component">
  <script>
    mounted({ name, data }) {
      const html = nuemarkdown(`[${name}]`, {
        data,
        lib: tsx_lib,
      });

      /**
       * The this.mountChild() method unfortunately is not documented enough
       * so I had to use this ugly solution:
       */
      this.$el.innerHTML = html
    }
  </script>
</div>

All I had to do was consume my amazing custom Nue component in SPA:

// home-view.nue
<section @name="home-view">
  <script>
    tag = { message: "My first custom tag!" }
    tag2 = { message: "My second custom tag!" }
  </script>

  <h1>Hello, World!</h1>

  <tsx-component name="my-tag" :data="tag" />

  <tsx-component name="my-tag2" :data="tag2" />
</section>

The results are visible on the screenshot 🎉

image

Conclusions

I am sorry for such a long answer, but I wanted to share my solution with you @tipiirai and other developers. It might help to shed some light on the problems I faced during implementation of the custom JSX/TSX components and possible solutions. This PoC opens possibility to implement integrations with different frameworks, however I have no idea if your intention was to use Nuemark API in a such way on the client-side components. I have the impression that you prepared this API for server-side components, despite the fact that there is no mention of them in the documentation.

I am fully aware of that the idea is to finish the core pieces first and launch a v1.0, before moving into extensions and integrations, but just wanted to check if I implemented custom TSX components as intended or maybe you had different vision behind it.

Regards

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants