justin-schroeder / arrow-js

Reactivity without the framework
https://arrow-js.com
MIT License
2.38k stars 49 forks source link

Component from the DOM #102

Open lucafabbian opened 6 months ago

lucafabbian commented 6 months ago

Is there a way to mount the current DOM as a component? Would it ever be?

I would like to create a component from the webpage itself instead of declaring it as a string. This may be really beneficial for SEOs, and also provide a nice way to get syntax highlighting. It would be game changing from the developer experience.

A quick way to achieve this would be to get the component's innerHTML, parse it, find special tags and then eval the result, I was wondering if you'll be interested in something like this. If you wish, I may work on this and propose a pull request.

justin-schroeder commented 6 months ago

There is not currently a way to do that, but this is squarely on the path for adding ssr/hydration support. That said, arrow is still quite experimental and doesn’t have any funding so time is quite limited — in otherwords dont hold your breath waiting for it 😂

lucafabbian commented 6 months ago

What if we add a stupid svelte-like syntax that just acts as a shortcut and could then by parsed just using string replacement + eval? In this way it will add nearly any code to arrowjs.

for example:

import {evalTemplate} from "arrow-js"
const appElement = document.getElementById('app');

window.data = reactive({
  name: 'hello'
})

const template = evalTemplate(appElement.outerHTML)
template(appElement)
<div id="app">
  {#if name == 'hello'}
    <div>hello</div>
  {:else}
    <div>not hello </div>
  {/if}

  { app.name } 
</div>
justin-schroeder commented 6 months ago

I sorta doubt we’ll ever do control flow tags/templates in arrow. instead they would be more like hydration hints. The actual control flow would always be in javascript. Simple pseudo code example:

html`<div>Hello <span>${() => state.location}<span></div>`

this would server render something like:

<div data-fragment="812ye79gh" data-ssr-pos="[[0,1,0]]">Hello <span>world</span></div>

This could then be re-hydrated directly from the dom:

const el = document.querySelector('[data-fragment="812ye79gh"]')
rehydrate(el, [() => state.location])

Obviously it gets much more complicated than that, but there is the basic idea. The control flow would stay in pure js that ships along with the html.

lucafabbian commented 5 months ago

That looks great, but it does not address the issue I'm facing. I don't want to use arrow-js inside a server side rendered pipeline, I would argue the best selling point of arrow-js is the lack of a build step.

You throw your interface on a static file server and you are done. Simple and beautiful. Yet, terrible for SEOs and not very developer friendly - no syntax highlighting and so on. If we were able to build components directly from the innerHTML of an element, it would be way nicer. Maybe not as a core feature of arrow-js, but as a plugin (even though, the template parser would be just like 100 bytes).

I've made a proof of concept with this and other extensions bundled here: https://github.com/lucafabbian/archeryjs/blob/main/src/index.ts With the relevant function being:

const extractTemplate = (template : string) => {
    let a = 'html`' + template + '`'

  const replacements = [
    ['&gt;', '>'],
    ['&lt;', '<'],
    ['&amp;', '&'],

    [/{#if(.*?)}/g, (_, a) => '${ () => '+ a + ' && html`'],
    ['{/if}', '`}'],

    [/{#for(.*?)of(.*?)}/g, (_, b, a) => '${ () =>'+ a + '.map( (' + b + ') => html`'],
    ['{/for}', '`)}'],

    ['{{', '${($event)=>'],
    ['}}', '}'],
    ['\\{', '{'],
    ['\\}', '}'],
  ]

  for( const [old,replace] of replacements){ // @ts-ignore
    a = a.replaceAll(old, replace)
  }

  return window.eval(a);
}

I understand this is not in the original project scope, yet I've included this in several small projects at work, and it works like a charm.

The way you would use this would be something like

const elem = // get element from DOM
const template= extractTemplate(elem.innerHTML)
template(elem)
justin-schroeder commented 5 months ago

Gotchya, I hear what you’re saying. There may be some way to do this well. I’m curious what would draw you to using Arrow for this use case vs something like Apline?

lucafabbian commented 4 months ago

In the end, we actually switched to Alpine, and we are kinda happy with that. ArrowJS would be better because there is less "magic" behind. You know exactly what's happening and where. Alpine auto-mounts things, and its syntax it's html-centric, whereas we would prefer a JavaScript first approach.

For example, we were using RunCSS to write html with tailwind without preprocessing. ArrowJS let us introduce a hook and process tailwind classes there, while with Alpine we resolved to just watch the whole html tree with a mutation observer.

justin-schroeder commented 4 months ago

ouch, yeah thats a big observer haha. Well, arrow is a passion project for me mostly, sadly i dont get nearly enough time to work on it compared to the other OSS work im doing. That said, its my favorite and if I’m ever able to get some time I plan to really flesh it out, so stay tuned for future updates.