van icon indicating copy to clipboard operation
van copied to clipboard

Custom Elements support

Open OFurnell opened this issue 1 year ago • 1 comments

The concise but powerful API of VanJS would make it very good alternative to template literals for Web Components.

I've been evaluating it for this use case but there are a couple of advanced use cases that aren't supported that would be great to see.

  • Non primitive data types for properties (e.g. arrays, objects and functions)
  • Custom event handling

I've attempted at implementing the Custom Elements Everywhere tests:

Link to CodePen

If you consider this useful I'd happily create a PR to add these into the existing test suite, as well as assisting with any API changes.

Look forward to seeing how this library progresses.

OFurnell avatar May 19 '23 21:05 OFurnell

Thank you for your feedback!

I apologize for not being aware of the custom element feature in web standards when VanJS was developed. Extending VanJS to support custom property types and event handling seems reasonable to me, so patches are indeed welcome! 😀

VanJS was only published a week ago, thus the patching process hasn't been documented yet. However, I can provide the general steps involved in the development process for VanJS:

  1. Upgrade the version in the package.json file. Considering the significance of this feature, it might be suitable to increment the minor version.
  2. Modify source files in the src/ directory.
  3. Execute bash publish.sh within the src/ directory. (While it might seem unusual to run a command within a subdirectory, fixing that isn't currently a priority.) Make sure the bash script completes with no error messages. This action generates the published bundles corresponding to the new version.
  4. Perform a browser-based test. Essentially, you need a static HTTP server to host the root directory. Then, attempt to load http://localhost:{port}/test/van.test.html in your browser to see if all tests pass.
  5. Develop new tests to encompass the new APIs.
  6. If applicable, update the corresponding .d.ts file as well.
  7. Ideally, providing sample web pages to showcase the new feature would be beneficial.

Since this is an advanced feature I would encourage to implement it as an extension (in a separate file other than van.js) to keep the core library lean.

If you have any questions regarding the development process, feel free to let me know.

Thank you so much for the feedback!

Tao-VanJS avatar May 20 '23 00:05 Tao-VanJS

Working through the instructions, running bash publish.sh errors with node_modules are needed so a npm/pnpm/yarn install step is missing in the instructions.

publish.sh: line 11: ../node_modules/esbuild/bin/esbuild: No such file or directory

Not a problem, just a comment to improve the steps needed to run the test suite. :-)

andrewgryan avatar May 30 '23 20:05 andrewgryan

Hmmm, I'm getting a sed issue. I'll try to figure out what's going on.

bash publish.sh

  ../public/van-0.12.0.nomodule.js  3.3kb

⚡ Done in 9ms

  ../public/van-0.12.0.nomodule.debug.js  8.3kb

⚡ Done in 13ms
sed: -e expression #1, char 1: unknown command: `.'

Solved My version of sed (4.7) works without a space between -i and .bak

andrewgryan avatar May 30 '23 20:05 andrewgryan

After fixing sed there was only 1 warning related to a direct eval

bash publish.sh

  ../public/van-0.12.0.nomodule.js  3.2kb

⚡ Done in 4ms

  ../public/van-0.12.0.nomodule.debug.js  8.2kb

⚡ Done in 9ms
▲ [WARNING] Using direct eval with a bundler is not recommended and may cause problems [direct-eval]

    ../test/van.test.js:789:119:
      789 │ ...nst van = await (type === "es6" ? import(path).then(r => r.default) : fetch(path).then(r => r.text()).then(t => (eval(t), window.van)));
          ╵                                                                                                                     ~~~~

  You can read more about direct eval and bundling here: https://esbuild.github.io/link/direct-eval

1 warning

  ../test/van.test.nomodule.js  38.3kb

Sorry for spamming this issue, but this felt like the most appropriate place to leave feedback for the current build/test process.

andrewgryan avatar May 30 '23 20:05 andrewgryan

I started a new thread : #37

Maybe we can discuss there.

Tao-VanJS avatar May 30 '23 23:05 Tao-VanJS

VanJS integrates with WebComponents easily:

This is a counter component:

const { button, span } = van.tags

class MyCounter extends HTMLElement {
    // Elements and functions
    connectedCallback() {

        this.value = van.state(0)
        van.add(this,
            button({
                onclick: () => {
                    --this.value.val;
                    if (this.onchange)
                        this.onchange(this.value)
                }
            }, "-"),
            span(this.value),
            button({
                onclick: () => {
                    ++this.value.val
                    if (this.onchange)
                        this.onchange(this.value)
                }
            }, "+"))
    }
}

customElements.define('my-counter', MyCounter);

And this is how you call it from within HTML:

  <script>
    function mychange(value){
       out.textContent = `The counter is ${value.val}`
    }
  </script>
   <my-counter onchange = "mychange(value)"/>
   <h2 id="out">output</h2>

It is possible, but if you create your page with VanJS, you do not need WC. You can use JS-Classes instead.

efpage avatar Jun 02 '23 19:06 efpage

After a little more digging, my previous comment on not being able to pass non primitive data types isn't entirely true. Arrays and functions do seem to work if the initial value isn't undefined. If we take the example component that @efpage created and try to add it with the following code it does work:

const { h2, "my-counter": myCounter } = van.tags;
const textContent = van.state("The counter is 0");
van.add(
  document.body, 
  myCounter({
    onchange: (value) => textContent.val = `The counter is ${value.val}`
  }), 
  h2(textContent)
)

If we modify MyCounter so that that it also has the capability for fine grain controls such as onadd/onsubtract it doesn't pass down these new functions.

const { button, span } = van.tags

class MyCounter extends HTMLElement {
    onadd; // Added
    onsubtract; // Added

    // Elements and functions
    connectedCallback() {

        this.value = van.state(0)
        van.add(this,
            button({
                onclick: () => {
                    --this.value.val;
                    this.onsubtract?.(this.value) // Added
                    if (this.onchange)
                        this.onchange(this.value)
                }
            }, "-"),
            span(this.value),
            button({
                onclick: () => {
                    ++this.value.val
                    this.onadd?.(this.value) // Added
                    if (this.onchange)
                        this.onchange(this.value)
                }
            }, "+"))
    }
}

customElements.define('my-counter', MyCounter);

Taking a look at the setter logic:

let setter = dom[k] !== _undefined ? v => dom[k] = v : v => dom.setAttribute(k, v)

The function onchange exists as a property on HTMLElement with a default value of null, but onadd/onsubtract have defaults of undefined so therefore get passed down as string using setAttribute. It's quite common to see this pattern in 3rd party components. One possible change could be to use the in operator, e.g.

let setter = k in dom ? v => dom[k] = v : v => dom.setAttribute(k, v)

Objects seem to have a different issue, they are always treated as state derived properties. I think we could potentially do a similar thing here, by potentially changing this line:

else if (protoOf(v) === objProto) bind(...v["deps"], (...deps) => (setter(v["f"](...deps)), dom))

To something that checks the existence of one or both required properties for state derivation, e.g.

else if ("deps" in v) bind(...v["deps"], (...deps) => (setter(v["f"](...deps)), dom))

OFurnell avatar Jun 02 '23 22:06 OFurnell

Hi @OFurnell,

In 0.12.0 we changed the way of when to set prop value via property or via setAttribute, see the current code. Not sure if it can solve the problem of custom event handler.

The issue with state-derived properties will likely be solved in 1.0 as part of the redesign of state-binding mechanism.

Tao-VanJS avatar Jun 09 '23 18:06 Tao-VanJS

Any updates on this since v1? I'm interested in using VanJS to document some web components and wondering about setting non-primitive properties and binding to custom events too

dominictobias avatar Jan 22 '24 22:01 dominictobias

Hi @sekoyo,

Though not officially tested, I believe it should be possible. You can try it out to see if it works.

Tao-VanJS avatar Jan 22 '24 22:01 Tao-VanJS

Properties worked great, the custom event isn't caught however. Just to be sure I caught it successfully on the doc body:

document.body.addEventListener('myevent', () => {
  console.log('caught my event on body')
})

Fired like this in web component:

connectedCallback() {
  this.dispatchEvent(
    new CustomEvent('myevent', {
      composed: true,
      bubbles: true,
      detail: 'Hello from custom event',
    })
  )
}

set someProp(value: any) {
  console.log('set someProp', value)
}

And attempted to catch like this:

const { 'lui-button': luiButton } = van.tags

luiButton(
  {
    someProp: { anObject: true },
    onmyevent: e => console.log('myevent event fired', e),
  },
  'A button'
)

dominictobias avatar Jan 22 '24 23:01 dominictobias

The support for custom event handers was added in 1.2.8: https://github.com/vanjs-org/van/discussions/246.

Tao-VanJS avatar Jan 23 '24 23:01 Tao-VanJS

Very nice! 👏 Thanks

dominictobias avatar Jan 23 '24 23:01 dominictobias

@all-contributors please add @sekoyo for bug

Tao-VanJS avatar Jan 23 '24 23:01 Tao-VanJS

@Tao-VanJS

I've put up a pull request to add @sekoyo! :tada:

allcontributors[bot] avatar Jan 24 '24 00:01 allcontributors[bot]

@all-contributors please add @OFurnell for bug

Tao-VanJS avatar Jan 24 '24 00:01 Tao-VanJS

@Tao-VanJS

I've put up a pull request to add @OFurnell! :tada:

allcontributors[bot] avatar Jan 24 '24 00:01 allcontributors[bot]