google / incremental-dom

An in-place DOM diffing library
http://google.github.io/incremental-dom/
Apache License 2.0
3.54k stars 180 forks source link

Make some existing nodes untouchable by IDom #323

Open oravecz opened 7 years ago

oravecz commented 7 years ago

I'm using the JSX to idom plugin to create a factory function that uses idom. It works spectacularly well. But (there's always a but).

I am writing web components and my JSK is patched into the shadow root. Shadow root is also where I want to insert my style sheet rules. If I insert rules into shadow dom myself, idom comes along and deletes these nodes that are unknown.

Looking at the skip capability, I could put the responsibility on the template author to add a placeholder for styles in their template, and use the skip attribute.

return <div>
           <style __skip>...</style>
           <div>...component template...</div>
       </div>

But this is problematic for a couple reasons.

  1. If I am using a ShadyCSS polyfill, there is no real shadow root, and those styles are applied to the head.
  2. I have to wrap my component templates in a parent <div> due to the way JSX works. Not a fan, and I don't think this will work in all cases.

What I would really like to do is specify my JSX like so...

return <div>...component template...</div>

And in my component constructor, I would determine if native shadow dom is supported. If it is, I would append the following dom structure to shadow root:

<style __skip>...</style>

What I would be counting on is that there would be a way to inform idom to ignore this DOM element when it comes across it due to the presence of a special attribute.

I don't believe this exists currently, but is there any approach that would offer similar behavior?

sparhami commented 7 years ago

You can use Incremental DOM calls directly from the code calling patch to skip any style nodes if present. This could look something like:

const {
  currentPointer,
  skipNode,
  patch,
} = IncrementalDOM;

function skipStyleNodes() {
  let node;
  while((node = currentPointer()) && node.nodeName === 'STYLE') {
    skipNode();
  }
}

class BaseClass extends HTMLElement {
  update() {
    patch(this.shadowRoot, () => {
      skipStyleNodes();
      this.render();
    });
  }
}

The component extending the base class can call update and just implement their render function without worrying about styles.

For a full example, you can take a look at this.

oravecz commented 7 years ago

Thank you very much for that tip. It works, but I'm not clear on exactly why.

From what I gather, the skipStyleNodes() function traverses the existing DOM structure an marks those nodes I want to skip in a way that the actual idom methods will ignore. However, if this is the case, is the marking of these nodes temporary to this rendering cycle? Iterating each node prior to every render seems extreme when I have a pretty good handle as to exactly where my node is.

Couldn't I mark this style node as a skip one time when I initially render my content? Is there a way to mark it by setting a special attribute on it when I append it to the DOM?

Thanks for the help.

sparhami commented 7 years ago

Thank you very much for that tip. It works, but I'm not clear on exactly why.

From what I gather, the skipStyleNodes() function traverses the existing DOM structure an marks those nodes I want to skip in a way that the actual idom methods will ignore. However, if this is the case, is the marking of these nodes temporary to this rendering cycle?

The way Incremental DOM works in general is that it traverses the DOM and compares the element that it is pointing at to the one you just declared. So the skipStyleNodes function is just advancing the pointer one by one past the style nodes at the start of the shadow root.

Iterating each node prior to every render seems extreme when I have a pretty good handle as to exactly where my node is.

I don't think you need to worry too much about performance, traversing the DOM isn't too slow (and we already do it anyway).

Couldn't I mark this style node as a skip one time when I initially render my content? Is there a way to mark it by setting a special attribute on it when I append it to the DOM?

Even if you did, Incremental DOM would internally do the same thing, checking if it needs to be skipped and traverse past the part in the DOM.

ahumphreys87 commented 7 years ago

I think in order to use an attribute this would be the responsiblity of the template compiler. I have done something similar in my handlebars compiler. Adding the skip attribute will add an IncrementalDOM.skip() call. https://github.com/ahumphreys87/handlebars-idom/blob/master/lib/compiler.js#L144-L146