WebReflection / wicked-elements

Components for the DOM as you've never seen before

Home Page:https://medium.com/@WebReflection/a-wicked-custom-elements-alternative-6d1504b5857f

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Async wickedElements

davidmerrique opened this issue Β· comments

Hey! I think it would be very useful to be able to create async components. The definition could be a function that returns a promise like this:

import { define } from 'wicked-elements';

define('.my-component', () => import('./components/my-async-component'));

Just like Vue's async components: https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components

That way the definition is only loaded when needed and the initial bundle can be much smaller.

Thank you

Actually, I just looked at #26 and looks like I could just use when-elements like this:

import { whenAdded } from 'when-elements';

whenAdded('.my-component', () => {
  import('./components/my-wicked-component')
});

It'd be nice if a similar feature was built in to wicked-elements though.

I noticed that in #26 you mentioned that you want to mimic customElements as close as possible, but in V1 you dropped support for classes. Maybe, since this library doesn't adhere so much to customElements anymore, this can be done?

I'm not following the ergonomics of this question/idea ... a whole example of loader/define might help

also ...

I noticed that in #26 you mentioned that you want to mimic customElements as close as possible

yeah, the API is identical, that is the purpose ... classes have nothing to do with this request or my intent in making it close to CE though, as every element here is already live, nothing to extend, no class to use ...

I'm just looking for a way to define an element and asynchronously load the code for the element only if the element exists.

Just like Vue.

Like in my first example:

// app.js
import { define } from 'wicked-elements';

define('.my-component', () => import('./components/my-async-component'));
// components/my-async-component.js
// this will only be loaded if '.my-component' exists
export default {
  init() {...},
  onclick() {...}
}

Sorry if I made the issue unclear!

The ergonomics are still not clear to me, as wickedElements API has {define, get, upgrade, whenDefined}, and both get and whenDefined might be needed in the component.

Importing the library per each asynchronous component is likely a no-go to me, see https://medium.com/@WebReflection/some-web-components-hint-75dce339ac6b

Accordingly, I wonder if it'd make sense to export as such:

export default ({define, get, upgrade, whenDefined}) => ({
  init() {...},
  onclick() {...}
});

The async component is a function that receives the library, and returns it's definition.

Whit would allow me to Promise.resolve(definition()).then(fn => { registry.add(fn(wickedElements)) }) or something similar.

Would this work for you?

P.S. whenDefined is also asynchronuos, so maybe the Vue solution with explicit resolve would work better ... as I don't want to bloat this library too much to handle all possible asynchronous gotchas ... will think about this

edit

I think I've solved in a much better way internally, using a similar, but better, approach described in the second part of this previous answer.


One option is to use QSO

import QSO from 'qso';
import {define, get, upgrade, whenDefined} from './index.js';

const observed = [];

const addComponent = (selector, {default: component}) => {
  define(
    selector,
    typeof component === 'function' ?
      component({define, get, upgrade, whenDefined}) :
      component
  );
};

const so = new QSO(records => {
  for (let i = 0, {length} = records; i < length; i++) {
    for (let {addedNodes} = records[i], j = 0, {length} = addedNodes; j < length; j++) {
      for (let node = addedNodes[j], k = 0, {length} = observed; k < length; k++) {
        const {selector, callback} = observed[k];
        if (node.matches(selector)) {
          observed.splice(k, 1);
          callback().then($ => addComponent(selector, $));
          k = length;
        }
      }
    }
  }
});

export default (selector, callback) => {
  if (-1 < selectors.indexOf(selector))
    throw new Error('duplicated ' + selector);
  so.observe(selector);
  observed.push({selector, callback});
};

but this won't ever land in here, as the whole purpose is to keep it around 1K

Another option is to do this:

import { define, get } from 'wicked-elements';

define('.my-component', () => {
  init() {
    import('./components/my-async-component').then(definition => {
      if (get('*.my-component'))
        return;
      define('*.my-component', definition);
    });
  }
});

This could be simplified as utility:

import { define, get } from 'wicked-elements';
const defineAsync = (selector, fn) => {
  define(`*.${selector}`, {init() {
    fn().then(definition => {get(selector) || define(selector, definition)});
  }});
};

defineAsync('.my-component', () => import('./components/my-async-component'));

This would attach two definitions per each asynchronous component, but one will be basically a no-op, while the second one would upgrade all components for real.

Would this work? If it does, I might export a defineAsync that implements a similar dance.

OK, it's in. For the time being, I've kept it simple, so the component must export its definition by default.

I hope this is fine, let me know if you have any other idea/concern/issue πŸ‘‹

You never cease to amaze. Thanks, I'll try it out today!

@WebReflection I think I found a bug, here's a pen https://codepen.io/davidmerrique/pen/wvMRZEY

In the console I'm getting Uncaught (in promise) Error: duplicated .test2

Maybe you can use :root like this:

const defineAsync = (selector, fn) => {
  define(`:root ${selector}`, {
    init() {
      fn().then(({ default: definition }) => {
        get(selector) || define(selector, definition);
      });
    },
  });
};

Seems like the fix is as simple as changing this line:

(_ || define)(selector, definition);

to this:

get(selector) || (_ || define)(selector, definition);

Fixed πŸŽ‰

@davidmerrique FYI I've just published v3 with tiny breaking change, but also tons of improvements behind the scene (it's even smaller) https://github.com/WebReflection/wicked-elements/blob/master/VERSIONS.md

I'd love to know if you had any trouble in switching/using this newer version, before I bump hookedElements too, so thanks in advance for any possible help/test πŸ‘‹

@WebReflection Just updated. So far so good, I'll let you know if anything comes up. Thanks πŸ‘

@WebReflection Actually something came up.

Seems like there are some performance issues, I'm looking into it, but I'm not sure how to debug.
A simple interaction that used to happen instantly took 17 seconds after this update.

Here's a screenshot of Chrome's Performace panel.

Screen Shot 2020-07-27 at 3 05 21 PM

is it possible to have a screenshot with v2 of the exact same situation?

@WebReflection Much less crowded
Screen Shot 2020-07-27 at 4 44 34 PM

as I've updated as-custom-element to not enforce MutationObserver on defined, and it's a patch, so it should be picked up, can you please try again with v3 and tell me if there's anything different?

P.S. this second screenshot doesn't look right ... nothing regarding wickedElement is in there, but the logic is really not that different right now, so it's quite bizarre

P.S.2 of course if you could provide a scenario that reproduces such performance impact, as I couldn't measure it myself, that'd be ace!

@WebReflection I'm trying to see if I can provide a simple scenario to replicate the performance impact.

In the meantime, here's a zoomed in screenshot of v2:
Screen Shot 2020-07-27 at 5 36 52 PM

and here's v3:
Screen Shot 2020-07-27 at 5 39 04 PM

It's calling setupList and upgrade over and over again.
That Function Call is from shared-document-observer

are you trashing big layouts via innerHTML, by any chance?

will have a look, it might be I'm overlooking at something, as nothing should block the logic that drastically ... are you also defining different selectors/components per each layout trash?

anyway, I suggest you stick with v2 for the time being.

Not sure if this will help, but here's what I have using v2 right now: https://www.mzwallace.com/shop/products/1269x1600/dawn-oxford-mini-metro-tote-deluxe/104883.html

The basket specifically, when you add/remove something from the basket, the whole thing is trashed via innerHTML.
The basket is a wicked element and the + - for each product in the basket is a wicked element.

I'm not defining anything on each layout trash. Everything is define/defineAsync on page load.

Thank you

I've just added to basket and it took a bounce of ms ... any reproducible steps? or do I need a bigger basket?

@WebReflection No no, that's using v2 right now so we can't reproduce the problem there.

I just gave you the link to show how I'm using wicked-elements in this case.

With v3 it takes almost 20 seconds to remove from the basket.

I'm not going to put v3 up there right now πŸ˜…

@davidmerrique by any chance you can tell me if using exact same version as 3.0.1 produces the same result?

@WebReflection 3.0.1 same result.

Here's a Chrome performance profile you can load up if you'd like:

Profile-20200727T181732.json.zip

@davidmerrique I believe current 3.0.3 solved what I think might have been the issue: a too late WeakMap set in the initialization. If this is not the case, I'd like to ask you if you can try the following code before any other (top of the page):

Object.defineProperties(Object.prototype, {
  connectedCallback: {get() { return this.connected; }},
  disconnectedCallback: {get() { return this.disconnected; }},
  attributeChangedCallback: {get() { return this.attributeChanged; }}
});

This should allow you to try regular-elements and eventually confirm both suffer the same issue, which means there's some performance shenanigan I need to solve in as-custom-element.

I'll try to work on it already though and see if there's any easy way to do it regardless.

Thanks for your help and patience.

edit actually there were nested, unnecessary, querySelectorAll calls within nodes already queries ... this should not ever been the case again.

@davidmerrique all the possible optimizations should now be in ... I hope latest version works, as I can't think of anything else that could've caused that mess. Please do let me know whenever you can, thank you again.

@davidmerrique last ping from me: I've benchmarked against v2 and while it's true v3 might be slightly slower (now), it's more robust, and also more reliable than before. There are various micro-optimizations I can consider here, but sharing as-custom-element core looks the best way to go, imho, and performance should be unnoticeable in the real world.

Please do let me know if that's not the case, thanks.

P.S. what a bumpy ride .. 3.0.12 should have it all, in terms of optimizations, I really hope it works.

edit and by 3..0.12 I meant 3.0.13 πŸ˜…

@WebReflection Seems to be working perfectly again with the latest version. Thank you for all the effort!

@davidmerrique no thank you for coming back with real-world failures, as I haven't extensively benchmarked possible regressions, hence it's kinda my fault I've prematurely bumped v3 out.

However, the amount of things I've learned in the process is well worth it, and I'm not excluding that as-custom-element might be ditched in the near future, as unfortunately most of the issues are due to a fundamental problem regarding MutationObserver, which is the only non blocking primitive available I have for these kind of projects.

If that's OK for you, I'd like to ping you again on next minor bump, as the API is not going to change any time soon, but behind the scene there are effectively various ways to reach best performance, and if I can ship this also closer to 1K than 1.3K, it's gonna be a win/win for everyone.

Cheers.

@WebReflection No problem. Happy to help test the next version.

I've been using your libraries for a while and they're always simple and enjoyable to use.

@davidmerrique FYI, without any measurable regression, the very latest patch I've pushed is 20%+ faster than version 2, as I've got rid of the dependency for CE, and made it a portable dependency, even for the very same as-custom-element itself.

All my perf tests outperform v2 and work better now, I hope you'll enjoy it too.

P.S. it's still around 1.3K, as I had to add some extra guard, hence I couldn't shrink it more πŸ˜…

@WebReflection That seems to have broken something.

It's not upgrading elements added to the DOM late after the definition.

https://codepen.io/davidmerrique/pen/NWxVzoO

wickedElements.define('.test', {
  init() {
    this.element.textContent = 'OK';
  }
});

setTimeout(() => {
  document.body.innerHTML = '<div class="test"></div>'
}, 100);

init is never called.

But if you take the document.body.innerHTML line out of the setTimeout it works.

that's extremely unfortunate ... I've pushed back 3.0.14 that should work as expected ... apologies for the inconvenience ... yet I'm so close, I swear!

@davidmerrique hopefully latest version fixes 'em all:

  • multiple handlers per element now works better than 2 and as expected
  • no init issue whatsoever, all elements gets updated as expected
  • no nested querySelectorAll whatsoever, nodes are setup once through the MO when and if needed

If you find any glitch in this latest version, please do let me know.

Thanks again for your patience.

@WebReflection Thanks, I'm trying it out but I'm getting an error:

Uncaught TypeError: addedNodes.filter is not a function

On this line https://github.com/WebReflection/as-ce/blob/860e4d499cef1b443fbd68a83b6ac743601f8adf/esm/index.js#L18

I can't seem to recreate it in a CodePen though πŸ€”

@davidmerrique bad timing .. please try again ;-)

@davidmerrique to be honest, I'm really done with changes. Do not expect anything else unless there is a proper bug.

3.0.19 is the very latest attempt to ship the most robust/tested v3 I could, next up ... this.release() within any handler, to drop it from the element it's dealing with.

This should make wickedElements feature-complete, as there's literally nothing else I could think about that should land in here.

Suggestions welcome though ... in case you've missed some feature.

@davidmerrique btw, if you save https://codepen.io/davidmerrique/pen/NWxVzoO with unpkg.com/wicked-elements instead, it'll pick up the latest and it will work just fine πŸ‘‹

@WebReflection Awesome. It seems to be working well. Thanks so much.