ryansolid / babel-plugin-jsx-dom-expressions

A JSX to DOM plugin that wraps expressions for fine grained change detection
MIT License
60 stars 10 forks source link

Idea: html-dom-expressions #17

Closed trusktr closed 4 years ago

trusktr commented 5 years ago

There isn't a repo for this yet, so thought I'd post here.

It'd be sweet if there was a fourth type of dom-expressions compiler: one that compiles from pure HTML markup (or from DOM, because DOM is practically an AST for HTML for all intents and purposes).

This would be similar to the one Vue uses, where expressions are all valid HTML inside of the vue files (and Vue components can even be compiled from actual DOM).

I would like to take a crack at this unless you get to it first. :)

trusktr commented 5 years ago

As an example, taking one from the README here,

<ul>
  {( list().map(item => <li model={item.id} onClick={handler} />) )}
</ul>

it might look something like:

<ul>
  <li $for="item of list()" @click="handler" :model="item.id"></li>
</ul>

or

<ul>
  <li (for)="item of list()" (click)="handler" (model)="item.id"></li>
</ul>

or

<ul>
  <li v-for="item of list()" v-click="handler" v-model="item.id"></li>
</ul>

etc. Basically all of those examples are valid HTML. Maybe there could be some options for configuring the DSL that is used within the valid HTML (f.e. specifying to use a special prefix like v- or @, etc, or specifying some syntax like (attribute-name)=, etc.)

ryansolid commented 5 years ago

You are right. I've actually written something like this before. It was what I was doing before I came across Surplus. At first, I did a runtime solution, and then moved to precompiled. Surplus also started here and discontinued it it for the JSX version.

For me, the final straw was when I was including a JS parser so I could parse the expressions in the bindings. Of course, that is all avoidable if you avoid complex scenarios like inlining function handlers. My background was Knockout which pretty much allows all of that so I was also. You also have to manage your own sense of context in this model. Ultimately I found the performance lacking. Of course, then domc completely disproves that. So truthfully I think there is some potential here.

That being said I think when it comes to String templates Svelte-like is a better fit with DOM Expressions. The runtime expects things to be functions, and stuff like control flow has been delegated to the library (that what the recent refactor did). HTML is very restrictive, and with strings we can keep things like Capitalized Component names etc.. In that way libraries that use Components for everything like Solid could continue and for libraries that wish to use functions for iteration you could stipulate the signature but have say:

{ #map item in items() }
<div>{item}</div>
{ /map }

compile to

map(() => items(), item => {
  const el = $tmpl.content.firstChild.cloneNode(true);
  insert(el, () => item);
  return el;
})

Small note but directive control flows often lead to the need of virtual elements vue uses template and consideration of order of application if and for is on the same element. Honestly other than the simplicity of parsing I am not sure why anyone would use a directive based API for Control flow. I've done it before but it's weird logic since once you identify it you need to parse the element again as a child of the original directive, and if an if is on there too, rinse repeat.

There is some awkwardness in that Components with DOM expressions can legally take any sort of children. Like a function as child. So libraries are free to implement that way. Typical template engines don't allow that. Like Solid's control flow for For:

<For each={items()}>{item => <div>{item</div>}</For>

It would be awesome if that was supported. But again that is back to my point what's the benefit of using string when something like JSX gives a clear AST. More flexible syntax mostly.

Anyway, it sounds like there could be something there. You might just get a crack at it as I'm pretty sure other topics will pre-occupy me above this one. I'd love to hear any ideas you have on how things could work.

trusktr commented 5 years ago

(sorry in advance, I wrote the following quickly, in the order of thought. Gotta get to work!)

Ultimately I found the performance lacking

In what sense? Amount of time to compile? Or runtime after it is compiled?

with strings we can keep things like Capitalized Component names etc

I'm thinking to use Custom Elements and shadow DOM as the units of composition, so only customElements.define.

The nice thing about Vue/Angular using valid HTML for templates is that it can allow for design workflow where a CSS designer could paste some template HTML, and style it with CSS, without cruft in the way.

With

{ #map item in items() }
<div>{item}</div>
{ /map }

those { #map item in items() } type things would appear in the output while the designer works. There's something I like about the concept of simple workflows involving just copy/pasted HTML+CSS and not requiring a designer to learn how to build everything.

But, if we can get them to build everything, then all the better anyways; something we should always strive for anyways.

Small note but directive control flows often lead to the need of virtual elements vue uses template and consideration of order of application if and for is on the same element.

Good point. Not too big a problem though. I think we can make some simple rule like if take precedence over for when on the same element, so it is essentially

if (...) {
  for () {...}
}

About the virtual elements, that's a good point. Not sure how I'd get around that (yet?) if using custom elements. F.e. like Vue when using template along with if to avoid having an extra element in the DOM just for the use of the if. Hmmmm.... 🤔

why anyone would use a directive based API for Control flow

Why not? The caveat you mentioned is only a small one, easy to live with.

Well, people seem to Love the developer experience in Vue. I really love it. Compared to the alternatives, and now with the new TypeScript support in the templates, it is quite amazing. Implementation/compilation/technical/performance/small-caveat details aside, the external-facing dev experience is just wonderful.

I've done it before but it's weird logic since once you identify it you need to parse the element again as a child of the original directive, and if an if is on there too, rinse repeat.

Those details I'm not familiar with. Can you expand on why the element needs to be re-parsed? Maybe there's other ways. Maybe they don't need to be directives, but something else. A compiler can output anything it wants.

But it may be only a burden on the implementer. The end user (f.e. in case of Vue) doesn't know about these details, and only knows the developer experience.

It would be awesome if that was supported. (the functions as children)

Well one of the beautiful things about the valid-HTML approach is that it's just markup. All functions and logic aren't mixed into the markup. If a function is needed for something, it could be passed as a variable into the template to pass into a sub-component for example.

I actually like this separation that the valid-HTML approach offers. It encourages people not to put expressions in the markup, but rather (f.e. in the case of Vue) put expressions in computed properties in the JavaScript along with all other logic, and use only the variables in the markup. This is one thing I really like about Vue.

In contrast, with React, templates can get quite "hairy". There are many ways to do the same thing, and it can get hard to understand what is happening in the templates, especially when new people join a team.

With Vue, there's a more guaranteed simplicity across all components. People wanting to understand logic can dive into the JavaScript where other logic co-exists.

trusktr commented 5 years ago

Imagine someone who lives on CodePen, and is a master of the "pure CSS" pens, and maybe they even choose not to write JS if they can avoid it. They might be a CSS design genius.

Imagine that person having to get markup from inside some React components with function children, markup passed into attributes, virtual components used for data flow, and what not.

Also imagine that same person working with markup from Vue. We can imagine which templates that person will like to work with more.

Vue is operated under the guise of a single developer and not a company, yet earned 10k more stars than React in a shorter time frame! I believe the DX is one of the main reasons: people just love it.

ryansolid commented 5 years ago

That's cool. I can respect you having a different opinion. After using directive based binding languages for over a decade, I actually think React's DX is pretty close to the pinnacle of Frontend API design. I actually wrote an article touching on this What Every JavaScript Framework Could Learn from React. So you aren't going to appeal to me on the merits of Vue. I have lot to say on the subject but it doesn't belong here.

Ok, Instead let's look at what this will take. While technically you can do what you are proposing it would be incompatible with any current DOM Expression library implementation. All the current Template APIs work with all libraries that support them. Now we can handle directives with BuiltIns (Solid uses Component control flows that are automatically imported so it could work the same way). The template parser would just need write the Javascript and register the BuiltIns. Custom Directives would take a bit more consideration. Maybe exposing a registry through DOM Expressions is the best way. As long as they have a fixed signature the compiler will know how to run them. I actually think insert could take all the same arguments. Stuff like render props are never actually rendered so this is aside from the DOM Expressions.

Yeah that puts most of the onus on the Template Compiler. All the virtual element and directives can live there since the compiled output will more or less look like what all the other libraries do. We can work with that. I think it gives up a lot in expressiveness, control, finesse, but doable. I actually already wrote an HTML Parser I use in the Tagged Literals version that I used back when I did templating this way. I don't know if I want to take this on at the moment, but it might be the type of thing I could whip up in a few hours...tempting.

ryansolid commented 5 years ago

@trusktr Actually I have a question. Are you thinking the output to be like an executable function or do we have full control and we assume it'll write the whole module? Like should I be thinking a custom file format like .domx?

trusktr commented 5 years ago

You're article, https://medium.com/@ryansolid/how-we-wrote-the-fastest-javascript-ui-frameworks-a96f2636431e, just appeared on my feed. Interesting!

What's your plan now regarding dom-expressions? Seems like you had second thoughts on how it was implemented. Will you be refactoring it?

ryansolid commented 5 years ago

No that article is at the end of spending a month refactoring it. I was saying I was a bit skeptical at first and did it to support features but in the end ended up with not only a better product for end users but one that is more performant.

trusktr commented 5 years ago

that article is at the end of spending a month refactoring it

Ah ok. Cool. I wasn't sure.

whip up in a few hours...tempting

It'll take me days. I've no experience with this. I was thinking of using hast-util-dom-parse and hast-util-parse5 (for from-dom and from-text, respectively).

Are you thinking the output to be like an executable function or do we have full control and we assume it'll write the whole module? Like should I be thinking a custom file format like .domx?

Good question. I suppose if it were going to generate a function that I could use inline, I'd just go with jsx-dom-expressions.

I was imagining more of like "single-file components".

It might be neat if the syntax would be the same for inline expressions though. Maybe that would be something for later, as it would essentially be an alternative to JSX. Or maybe template strings could use the same "sfc" compiler under the hood, but with slightly different semantics.

trusktr commented 5 years ago

I'm also thinking about incremental levels of experience in learning a given development framework:

When I first heard of React, it seemed complicated to me. I was coming from a background where I'd just learned HTML/CSS, and barely jQuery to manipulate the DOM more easily than with regular JS. I was still barely understanding this context, .bind(), .apply(), async callbacks, ES5 classes, and other things.

It may not be easy for a beginner to jump straight into JavaScript with HTML inside of it.

A beginner might have a hard time reading stuff like <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>.

My thought is, that if we can have application boilerplates/scaffolds/starters that allow a beginner to get going with the basics (writing HTML/CSS before JavaScript), then they'll have a better incremental learning experience.

So it'd be nice to have file with just plain HTML/CSS that just works, then allow them to move onto JavaScript and templating features later.

With JSX, that process is reverse: it assumes you're already a developer and understand code.

For you (and now me), using JSX is easy, and powerful, but it isn't always easy to start that way.

My motivation is to make something incrementally learnable, that a teacher could start to teach absolute beginners with, yet also powerful enough for experienced developers.

trusktr commented 5 years ago

Regarding a callable function, that could be one way to settle the scoping issue (like how to map names in the markup to actual references): pass them in as args to a function.

The nice thing about this is that a "single-file component" could compile to basically the stuff from the <script> being automatically passed into a function that the compiler wraps the output with.

Then for inline usage, the compiler just gives you the function reference, but other than that difference the inside would work the same.

trusktr commented 5 years ago

Furthermore, a higher-level abstraction over the inline function output could determine how to map instance properties to the function parameters, f.e. theFunctionThatInstantiatesTheReactiveComputationTemplate(this.foo, this.bar, this.baz) which would pass them to the same-name parameters foo, bar, and baz of the template function.

EDIT: It would have to be theFunctionThatInstantiatesTheReactiveComputationTemplate({foo, bar, baz}), accepting an object with named properties, otherwise guaranteeing the parameter order would get messy.

trusktr commented 5 years ago

Hmmm, maybe how the indentifiers are resolved could be configurable, f.e. the following options:

All three options could work great with compile steps, f.e. a Babel plugin or Webpack loader.

With a template string function factory for prototype uses like on CodePen (as alternative to a compile step), it could accept an options object to specify the modes. In two of the three modes, the output would need to be passed through eval (for this. prefixes, or for in-scope vars). The function output would work great without eval.

Non compiler examples:

import theLib from 'the-lib' // whatever its called

class Foo {
  items = [...]

  render() {
    return eval( // eval not needed if using the compiler
      theLib({mode: 'this'})`
        <div v-for="item of items">{item.prop}</div>
      `
    )
  }
}

Or perhaps mode: 'object:this' so that things like mode: 'object:someObject' are also possible.

import theLib from 'the-lib' // whatever its called

class Foo {
  render() {
    const items = [...]

    return eval( // eval not needed if using the compiler
      theLib({mode: 'scope'})`
        <div v-for="item of items">{item.prop}</div>
      `
    )
  }
}
import theLib from 'the-lib' // whatever its called

class Foo {
  items = [...]

  render() {
    const items2 = [...]

      // default is {mode: 'function'}
      const tmpl = theLib()`
        <div v-for="item of items">{item.prop}</div>
      `

      return tmpl({items})
      // or
      return tmpl({items: this.items})
  }
}
trusktr commented 5 years ago

Compiler examples ("sfc"):

in-scope mode:

<ul component-name="my-component">
  <li @for="item in items"> <!-- syntax configurable, so framework authors can choose the DX -->
  </li>
</ul>

<script>
  // in scope of the whole file
  const items = [...]
</script>

class (uses this. prefix mode, plus maybe it places the template as a static on the class, or in a render method, or something TBD):

<ul component-name="my-component">
  <li @for="item in items"> <!-- syntax configurable, so framework authors can choose the DX -->
  </li>
</ul>

<script>
  export class Foo {
    items = [...]
  }
</script>

Seems like for class mode, we need more configurability options to allow framework authors to define how the template maps onto the class (f.e. a decorator, a static prop, a render method, something else).

function mode:

<ul component-name="my-component">
  <li @for="item in items"> <!-- syntax configurable, so framework authors can choose the DX -->
  </li>
</ul>

then

import foo from './foo.ext' // .ext is configurable for framework authors

const items = S.data([...]) // reactive in this case
// or
const items = ReactiveVar([...]) // Meteor.js

foo({items})
ryansolid commented 5 years ago

Biggest thing making me want to do full file compilation is tree shaking. I can generate only imports needed based on template use. It's basically how Svelte works. Non precompiled would always require importing the whole library and passing it into a generated function call. That's always a cost. HyperScript and Tagged Template Literals work that way. I like the prospect of the non JS person not needing to worry about imports. I'm much more inclined towards compilation only approach since it pushes boundaries. Its interesting. Obviously the compiler could be configurable to support both but there is decent divergence on the shape of the output.

Outputting Components by default is weird though. One of the reason DOM expressions is so performant is Components aren't real. They essentially compile themselves out of the output. I want to do single file but have it just work. It's the export that makes it a Web Component or not. Hmm.. I'd really like to see if there is a way to output something that is not neccessarily a Web Component. Too much responsibility. At that point we are designing a framework not a library. If a consumer wants Web Components thats up to them. If you seen my Web Component library that's how it works. Takes any pre-existing Component system and make Web Components out of it.

Yeah I like single file. That way all this identifier mode stuff is mute. You just write your code and template and it generates a simple Function Component that is exported. We just design a syntax to support these Components that doesn't interfere with Web Components and we are golden.

trusktr commented 5 years ago

If I'm using JSX, then it is super easy to just make my own class with a render() method. It's obviously just a lot more flexible than a SFC.

it generates a simple Function Component that is exported

The thing is, what if I want more than that? Like something like Vue, but with Web Components? Suppose I want to export a class, and then import that class. So expanding my previous example:

<ul component-name="my-component">
  <li @for="item in items"> <!-- syntax configurable, so framework authors can choose the DX -->
  </li>
</ul>

<script>
  export class Foo extends HTMLElement {
    items = reactive([...]) // or S.data, or ReactiveVar, or whatever dep-tracking lib variable.
  }
</script>

then just use it:

import Foo from './Foo'

customElements.define('foo-el', Foo)

document.body.appendChild(document.createElement('foo-el'))

How can we configure the SFC system to allow authors to describe how they want their "component" exported?

trusktr commented 5 years ago

Another question: What if I want to abstract reactive variables behind normal properties?

For example, take this decorator:

export function variable(prototype: any, name: any) {
    const v = Variable()

    Object.defineProperty(prototype, 'v_' + name, {value: v})

    Object.defineProperty(prototype, name, {
        get(): {
            return this['v_' + name]()
        },
        set(v) {
            this['v_' + name](v)
        },
    })
}

where Variable makes a dependency-tracking reactive variable for use in reactive computations.

And then make a class with it:

class Foo extends HTMLElement {
    @variable foo = 123
}

const f = new Foo()
f.foo += 1 // queues the template to re-render

Exporting a function wrapper from the sfc file would mean that for every class that I want to make, I have to have a separate file, instead of just one file. So basically:

<ul component-name="my-component">
  <div>{foo}</div>
</ul>

<script>
  export class Foo extends HTMLElement {
    @variable foo = 123
  }
</script>
import Foo from './Foo'

customElements.define('foo-el', Foo)

let f
document.body.appendChild(f = document.createElement('foo-el'))

f.foo += 1 // queues a reactive computation that updates the template.
ryansolid commented 5 years ago

Right Class/decorator crowd often hand and hand with this. It's way easier if that is abstracted but that does put pressure on the DX following a specific path. See like most Reactive libraries have a function form. So why mess with classes etc. We just need to figure out the syntax for Components. So the entry point can be:

app.js:

import { render } from 's-dx';
import App from 'app.html';

render(App, document.getElementById('main')

app.html

<template>
  <template is='Counter` delay='1000' ></template>
</template>

<script>
import Counter from './counter.html';
</script>

counter.html

<template>
  <div>{( count() )}<div>
</template>

<script>
import S from 's-js';

const count = S.data(1),
  t = setInterval(() => count(count() + 1, $props.delay);

S.cleanup(() => clearInterval(t));
</script>

And if you want to separately make Counter a Web Component:

import { register } from 'component-register';
import Counter from './counter.html';

register('counter-element', { delay: 0 })(Counter);

I'm using my library here, but Swiss works or any sort of wrapper etc.. I know WebComopnent crowd is used to classes but it's a really weird fit here with Reactive libraries that only call their render function once. Basically everything is a run once constructor so why even bother.

ryansolid commented 5 years ago

I realize that probably feels like jumping through unnecessary hoops but I'm just thinking of how the core library can behave generically. The other option is the script tag is just full JS and you are responsible for what you write and all the Compiler does is taking the default export and pass it to the template function it wraps the template portion with. That's probably more straight forward. You can do everything in the one file. Want to define a custom element write it right in there. Use classes go wild, use function go wild. Still the classes wouldn't be web components as they need to be consistent with the libraries component system. But again Component-Register can make that a seemless transition. Like using the existing mobx-jsx library.

app.js:

import { render } from 'mob-jsx';
import App from 'app.html';

render(App, document.getElementById('main'));

app.html

<template>
  <template is='Counter` delay='1000' ></template>
</template>

<script>
import Counter from './counter.html';
</script>

counter.html

<template>
  <div>{( count() )}<div>
</template>

<script>
import { observable } from 'mobx';
import { Component, cleanup } from 'mobx-jsx';

export default class Counter extends Component  {
  @observable count = 0;
  constructor(prop) {
    const t = setInterval(() => count++, props.delay);
    cleanup(() => clearInterval(t));
  }
}
</script>

I don't maybe classes are too weird with no need for a render function. Looking at this more this doesn't seem as straightforaward as I hoped.

ryansolid commented 5 years ago

Ok back to js files approach.. what if it was a babel-plugin-html-dom-expressions. It essentially works identical to the JSX one.. It just looks for instances of specific tagged template literals and converts the code. If you need this then put this.state in the template. It just takes the inserted values and treats them like JS right in context. Mobx-JSX Counter example:

import { observable } from "mobx";
import { render, cleanup, Component } from "mobx-jsx";

class App extends Component {
  @observable counter = 0;
  render() {
    let timer = setInterval(() => this.counter++, 1000);
    cleanup(() => clearInterval(timer));
    return html`<div>{( this.counter )}</div>`;
  }
}

You could keep your markup directive approach and just not escape the Template literals. Non Web-Components are still interesting problem.

trusktr commented 5 years ago

Yeah, that's the nice thing about JSX/template-strings, that they just work in any pattern. It's definitely trickier to imagine how to make a generic sfc pattern that people can adapt to any use case.

1) One possibility could be to chain transforms. F.e. the sfc base transform just exports a function for the template, plus whatever the user exports from the script tag. Then the next transform (which could also be chosen by the framework author) could take both of those exports, and combine them somehow (for example assign the template function onto a static property of a class then re-export the class).

2) Hmm, another option could be that an SFC file always export the template function, plus whatever the the user exports from the script tag, then automatically passes both into a function that the framework author defines, and this could automatically generate a loader. So the SFC tool might be some thing that the framework author imports inside the loader's implementation module, passes the source into the tool along with the source for the handler function, and finally returns the result from the tool.

Idea 1. sounds better, just recommending authors to write additional transforms to process the output how they want.

Or something. Just tossing in ideas as I get them.


A downside of the idea of compiling html`<div>{( this.counter )}</div>` is that some people might find it odd that the expression isn't actually runnable at runtime. I think I just prefer JSX in this case.

Really the main reason for wanting simple HTML syntax is so that it can live by itself without the code. F.e., suppose this component has no code:

<button class="awesome-styling"></button>

^ No JS needed at all, when the desired result is only simple DOM output.

trusktr commented 5 years ago

If we went with compiling html`<div>${ this.counter }</div>`, we may instead want to use ${ this.counter } so that intellisense in editors just works.

ryansolid commented 5 years ago

Just tossing in ideas as I get them.

Same here. I think being generic is too tricky at this point. I agree my last suggestion might as well use JSX. I think the only way this works cleanly with an HTML first mentality is your starting point. Web Components only, Single File Components, etc.. I think we use the template syntax to set tag name and default prop values:

<template tag="counter-element" delay={1000}>
  <div>{( count() )}<div>
</template>

<script>
import S from 's-js';

const count = S.data(1),
  t = setInterval(() => count(count() + 1, props.delay);

S.cleanup(() => clearInterval(t));
</script>

And this just defines <counter-element /> with a delay prop with default value 1000. The only challenge will be standardizing prop wrapping for each library. They might need to provide a prop wrap and prop update method to configure their library. But once setup it will easy to go. I would just use my Component Register in the background basically port Solid Elements implementation as it can be applied to any fine-grained library.

What's nice about that is you can get the html only thing. Like if the prop is a straight pass-through your whole component could be:

<template tag="name-greeting" name="">
  <div>Welcome, {props.name}</div>
</template>

It's a different tact to things but it's consistent. No worry about imports/exports except at the entry point. I mean people would still be able to lazy load stuff by doing dynamic imports in the script tags but the basic case is simple.

trusktr commented 5 years ago

I think we use the template syntax to set tag name and default prop values:

I'd also like to allow the end user of the component to be able to skip using that name, and to define any name they want. For example, if they already have an element named counter-element, they might want to name the new component something else.

What I do in my lib is have a static SomeClass.define() method that uses the default name, or SomeClass.define('new-name') to give it a new name.

If the user wants to use default names for all elements in the library, I also supply a useDefaultNames() function from the index file, that basically iterates all the classes and calls the static define() on them.

I know it isn't so common, but I'd hate for a user to be stuck in a situation where a custom element name from one library conflicts with the name from another library, and there's not much they can do about it (save for forking the library code and modifying it directly).

Web Components only

Totally down for that. My intent is to make custom elements, using ShadowDOM for distribution, and I personally have no intent to make virtual components. I would be happy eschewing virtual components and virtual distribution.

They might need to provide a prop wrap and prop update method to configure their library.

This is what I have with my getter/setter approach (that I also mentioned in the Gitter channel): https://github.com/infamous/infamous/blob/reactivity-with-decorators/src/html/WithUpdate.ts

The WithUpdate mixin class allows the consuming subclass to define props, and how those props map to/from HTML attributes. It is based on SkateJS's approach, but I've modified it and diverged a bunch. See the "Props" section here to understand more about how the prop definitions work. The "Custom prop types" section is "coming soon", but see my custom props over here to get an idea. My version supports decorators to apply props definitions instead of a static props property (though it also supports the static props for plain JS users, which is what the decorators map to).

Those defined props automatically create the observedAttributes array, and use attributeChangedCallback or getters/setters in the WithUpdate mixin to map back and forth (the forth part, reflecting back to an attribute, is optional).

You can peruse some of my classes here to see how the props are defined.

In particular, the setters/getters in Transformable are the more complicated ones, where on Gitter I mentioned they have an underlying object, and the values that are set are "coerced" , but not actually coerced, but fed into the underlying XYZValues object.

By the way, I took a look at Sifrr, and the usage seems to be similar to what I'd like to do (SFC file that makes Custom Elements), except some template parts aren't totally valid HTML. For example, the attribute attr=${this.state.attr} could be written as attr=${ this.state.attr } and in that case the DOM output would be three separate attributes: attr="${", this.state.attr="", and }="". That would complicate parsing from DOM.

Siffr renames by the rule ClassName -> class-name, so that's not flexible for end users to rename things either.

Basically what I like from there is the SFC-WebComponent concept.

For my particular case, I would be fine not making it super flexible for framework authors, having it work in a single way (f.e. export a class from the <script> tag which is used as a Custom Element), not allowing people to customize syntax, output, or even the reactivity system.

But the flexibility is still a nice concept for something like html-to-dom-expressions to have for general purposes.

If I work on this, I'll start with my specific case in mind, then maybe later I can expand to make it more gneric, but the generifying part is where you have more experience.

ryansolid commented 5 years ago

The main reason I was seeing the need for library implementations to provide handle props methods is the difference between observable patterns. For instance if their observables use proxies or object getter/setters, it is the value that is being passed which may or may not need to be wrapped again. Some libraries automatically deep nest. Some scenarios you will find the parent only sets the prop once and its nested reactions that account for everything. In those cases props don't trigger. The trickiest scenario is where it is pass by value and immutable interface and the child needs to diff every update. It all depends on the library so I think you want to keep this configurable. Still all shielded from end user so we are good.

My library already has the base getter/setter mechanism auto observableAttributes bit. It's just all the other scenarios that come from mutable vs immutable interfaces. I mentioned a while back in a different thread that Skates out of the box mixins actually are a poor fit for reactive libraries. Its doable but you can tell they built stuff thinking top down.

Name registry thing is a good point. Awkward as hell since it requires a second file. We could just export the class. I just find it weird when you consider builtins which require more specific arguments. class name mismatches. I've never used 3rd part WC libraries and always handcrafted them myself so I haven't felt that pain. I'm super familiar with making WebComponents work with different styles of libraries, but I've likely been exposed to ecosystem issues less.

trusktr commented 5 years ago

It all depends on the library so I think you want to keep this configurable

For me, at least for now, the end user experience for the component system that I want to make is more important than allowing any author to create any end experience; though I definitely do admire that goal. If you end up making it first, and I can make the end experience that I want (without performance costs), and still allow any other author to make their end experience, that'd be great! If I make it first (but it'll take me time), then happy to share what I learn from it.

Skates out of the box mixins actually are a poor fit for reactive libraries. Its doable but you can tell they built stuff thinking top down.

The reason why I liked it over other options (f.e. Polymer, Stencil, etc) was that I could apply the mixin to my existing stuff, and just move along. I didn't need a paradigm shift or to port all my stuff. It's just a "library" in the sense of framework-vs-library, and it was super easy to add it incrementally to any class that I wanted to add it to, while using my existing web component features. Whether it is developed bottom-up or top-down, may be a secondary thing, compared to that experience as an end user that I had with it without having to re-write all my stuff.

One problem now is that if I want to convert to reactive props with computations, it doesn't really play well with my existing props with events or updated methods, and I'll have to totally switch my props over... Unless I can come up with a way for them to work on top of existing getters/setters.

Maybe, the reactivity system (a decorator) can detect existing accessors, and rely on those to get/set values, while adding the reactivity on top. Then I could incrementally migrate to having just reactive props. Hmmmm....

Name registry thing is a good point. Awkward as hell since it requires a second file.

Current usage for my stuff is like follows. Suppose the end user uses the global build:

<script src="path/to/infamous.js"></script>
<script>infamous.useDefaultNames()</script>

<!-- carry on with element names as defined by the lib -->

But then a user who wants customization can:

<script src="path/to/infamous.js"></script>
<script>
  // get specific classes used by this app
  const {Sphere, Box} = infamous

  // define custom names
  Sphere.define('sphere-mesh')
  Box.define('box-mesh')
</script>

<!-- carry on with custom names -->

The only downside is that if the user forgets to call infamous.useDefaultNames() (or to do it manually for classes that will be used), then it won't work. In contrast, other WC libs just automatically define the elements, so they just work out of the box (with no workaround for potential name clashes).

ryansolid commented 5 years ago

Cool. Yeah I can see the benefit of handling the registry yourself.

Yeah I for sure get the importance of that flexibility of mixins etc. My Component library works very similar to Skate, I just anticipated Reactive libraries like this from the beginning (as that was my starting point when I wrote it back in 2014) so I built all the pieces to compatible with patterns employed regardless. So it isn't that I have any issue with Skate, it is just I already saw this shortcoming. I think the challenge now is you are coming to this from a system that didn't account for it in the first place so it can be pattern changes.

If you haven't ever looked at it take a look at my Web Component library component-register. I suspect you won't like it at all since it completely hides the fact that you are dealing with Web Components and instead lets people write stuff exactly like they would in their framework but it was a great way to support any system right next to each other and play nicely with each other. I have lit-html components, next to knockout, next to react, next solid all working together seamlessly. It just is completely different than most other Web Component libraries.

But you are right my goal with DOM Expressions is to support any reactive library have the tools at their disposal to support workflows. But I understand you are looking to create a specific experience. You can do that too and I hope to support it but it comes down to priorities. I think the clear thing from this conversation is this area is a lot harder to generalize. That definitely will slow down my progress so I do suspect you will get a chance to try stuff out first. If you do I'd love to see what you come up with.