sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
79.34k stars 4.18k forks source link

Transitions on WebComponents not working #4735

Closed msaglietto closed 4 years ago

msaglietto commented 4 years ago

Describe the bug When you define a transition on an element that is a customElement (web component) the transitions animations dont show since the css for the animation are not applied

Logs It fails silently

To Reproduce Check repo but is just the transition example made web component https://github.com/msaglietto/svelte-transitions-issue Or just

<script>
  import { fade } from 'svelte/transition';
  let visible = true;
</script>

<svelte:options tag="test-transitions" />

<label>
  <input type="checkbox" bind:checked={visible}>
  visible
</label>

{#if visible}
  <p transition:fade="{{ duration: 3000 }}">
    Fades in and out not working
  </p>
{/if}

Expected behavior Transitions working fine in web compoent with the shadow root restrictions

Information about your Svelte project:

Severity It was blocking us from using svelte but we found a work around

Additional context The issue happen because svelte insert the css in the head of the ownerDocument: https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/style_manager.ts#L32 In the case of web component the elementes inside of the shadow-root can not read outside of it so the head css dont apply

I think maybe it could be solved if you can configure where svelte insert the css

A quite ugly workaround is to redefine node ownerDocument and its head element on the transition

  function workaround(node, params) {
     if (!node.hasOwnProperty('ownerDocument')) {
        Object.defineProperty(node, 'ownerDocument', { get: function() { return node.parentElement; } });
        node.parentElement.head = node.parentElement
     }
    return fly(node, params)
  }
Conduitry commented 4 years ago

Sounds like a duplicate of #1825.

msaglietto commented 4 years ago

You are right my bad I serached as Web Component not Custom Element =S Well maybe the workaround helps someone

Samuel-Martineau commented 4 years ago

Thanks for your workaround!

crisward commented 4 years ago

The ugly workaround can be made a little bit more reusable with this. I lookup the top parent element with while loop as the above fix doesn't always work with nested transitions.

// transfix.js
export default function fix(transtion) {
  return function(node, params){
    if (!node.hasOwnProperty('ownerDocument')) {
      Object.defineProperty(node, 'ownerDocument', { get: function() { return node.parentElement; } });
      let elem = node
      while(elem.parentElement){ elem = elem.parentElement }
      node.parentElement.head = elem
    }
    return transtion(node, params)
  }
}

Then your transition can be applied with. This should work with any transition.

<script>
import { fly } from 'svelte/transition';
import fix from './transfix.js'
</script>

{#if visible}
    <p transition:fix(fly)="{{ duration: 3000 }}">
      Fly in and out working
    </p>
{/if}
cie commented 4 years ago

Thanks, this helped! I needed a slightly modified version though:

// transfix.js
export default function fix(transtion) {
  return function(node, params){
    Object.defineProperty(node, 'ownerDocument', { get: function() { return {head: node.parentNode}; } });
    return transtion(node, params)
  }
}
yannkost commented 4 years ago

Here to get a fix for the following transitions in typescript: fade, scale, blur, fly, slide (I only tested fade...) Still this has to be fixed.

import type { fade, fly, scale, slide } from "svelte/types/runtime/transition";

declare type EasingFunction = (t: number) => number;

interface FadeParams {
  delay?: number;
  duration?: number;
  easing?: EasingFunction;
}

interface BlurParams {
    delay?: number;
    duration?: number;
    easing?: EasingFunction;
    amount?: number;
    opacity?: number;
}

interface FadeParams {
    delay?: number;
    duration?: number;
    easing?: EasingFunction;
}

interface FlyParams {
    delay?: number;
    duration?: number;
    easing?: EasingFunction;
    x?: number;
    y?: number;
    opacity?: number;
}

interface SlideParams {
    delay?: number;
    duration?: number;
    easing?: EasingFunction;
}

interface ScaleParams {
    delay?: number;
    duration?: number;
    easing?: EasingFunction;
    start?: number;
    opacity?: number;
}

export type TransitionFunctions = typeof fade | typeof scale | typeof blur | typeof fly | typeof slide 
export type TransitionParams = FadeParams  | ScaleParams | SlideParams | FlyParams | BlurParams

export default function fix(transtion:TransitionFunctions) {
    return function(node:Element, params: TransitionParams){
      Object.defineProperty(node, 'ownerDocument', { get: function() { return {head: node.parentNode}; } });
      return transtion(node, params) 
    }
}

transition:fix(fade)={{duration:200}}
OClement commented 3 years ago

@yannkost Thanks for this snippet We're considering using svelte to build our in-house component library and I'm working on a (very) small PoC of building Custom Elements with it. Since I'm just getting started with Svelte, I'm not sure how to use that workaround. I'm trying to implement a button with a ripple effect (à la Material) and the animation, using tweened isn't working at this point; I'm not sure if this workaround applies?

Would it be possible for you (or anyone, really) to quickly explain how to use this "fix"?''

Thanks in advance!

msaglietto commented 3 years ago

@OClement The problem of using custom elements is that the css of the tweened animation is applied to the document header .. since for web components the outside css doent apply the animation is not played What this workaround is doing is patching the node.ownerDocument to instead of return the document from the global scope to return the parent node of the element ... so the css is inserted in the parent node that is sill inside the web component

So if you want to use it on your button component .. you will have to apply the workaround when you mount the button or use the fixed transitions from the examples .. but you will have to make sure that the "workaround" code return an element that is still inside the web component

hope this help

ivanhofer commented 3 years ago

I created a PR that makes it possible to use svelte inside a shadow dom. All styles and animations will work like in a normal svelte-application. You could install svelte from this branch to use it until it gets merged.

OClement commented 3 years ago

@msaglietto

Great explanation, this clarifies a lot, thanks a bunch!

yannkost commented 3 years ago

@OClement Just so you know how to use the fix, personnally I put the definitions/interfaces in a fix.ts file, import the fix function where needed, then i'm applying the fix when I use a Svelte transition the following way:
<div transition:fix(slide)={{ duration: 200 }}>

ESLint will still give you an error, but it still works for as far as I know.

mahdimaiche commented 3 years ago

@yannkost your fix works but try using a transition on hover for example on a node that is not a direct child of a webcomponent (it has another parent which will be then it's ownerDocument). It will keep on creating style tags and appending them under this component each time you hover. @crisward fix works better.

lights0123 commented 3 years ago

An alternative method that doesn't require modification of components:

interface ExtendedDoc extends Document {
  __svelte_stylesheet: StyleShim;
}
class StyleShim {
  cssRules: string[] = [];
  private _stylesheets: CSSStyleSheet[] = [];
  constructor() {
    this.register(
      document.head.appendChild(document.createElement("style")).sheet
    );
  }
  insertRule(rule: string, index = 0) {
    this.cssRules.splice(index, 0, rule);
    for (const sheet of this._stylesheets) {
      sheet.insertRule(rule, index);
    }
  }
  deleteRule(index: number) {
    this.cssRules.splice(index, 1);
    for (const sheet of this._stylesheets) {
      sheet.deleteRule(index);
    }
  }
  register(sheet: CSSStyleSheet) {
    this._stylesheets.push(sheet);
  }
  unregister(sheet: CSSStyleSheet) {
    const i = this._stylesheets.findIndex((s) => s === sheet);
    if (i !== -1) this._stylesheets.splice(i, 1);
  }
}
const shim = new StyleShim();
(document as ExtendedDoc).__svelte_stylesheet = shim;
export default shim;

Then, bind:this on any element in your component, insert a new stylesheet and register it onMount:

const styleSheet = element.parentNode.appendChild(
  document.createElement("style")
).sheet;
StyleShim.register(styleSheet);
return () => StyleShim.unregister(styleSheet);
Wambosa commented 2 years ago

necro post The above didn't work for me, but I was able to get it working on svelte 3.48.0 with:

export default function fix(transtion:TransitionFunctions) {
  return function(node:Element, params: TransitionParams) {
    node.getRootNode = () => node.parentElement
    return transtion(node, params)
  }
}