BuilderIO / mitosis

Write components once, run everywhere. Compiles to React, Vue, Qwik, Solid, Angular, Svelte, and more.
https://mitosis.builder.io
MIT License
12.52k stars 555 forks source link

Usage of `useRef` is broken for `lit` #1149

Open ohkimur opened 1 year ago

ohkimur commented 1 year ago

I am interested in helping provide a fix!

Yes

Which generators are impacted?

Reproduction case

https://mitosis.builder.io/?outputTab=DYSwLkA%3D&code=JYWwDg9gTgLgBAbzhAdgWQgVxTANMlAVTABMBDGAU30wGdKAlSgMxvoGUZpK4BfOZlAgg4AcgACAI0zAANiUpQAdMAgB6EMC61gtUQCh9MAJ5geAYQizotOAF5E%2BuHDBRQZKMYD8ALji0YNxQAcyd%2FSgBjVHJPX39A4BD9XkNEqihmMgieAEkolAAFITBbBDDgfL8AoNDnHQAvSjjRAA89OAAfMRASUU64FEwQSUUwkzNm2itgXv7RWUTKA2cPYDIAGTIR2TjqxNq4KOsoXYSk5yPodkjoj28qs9CU%2FUoWyFg4BUzMWXhmbAiMFUKDgeVQAApXBASn4wYVirQAJSOC6oAJwAAWEACTGY9jgdEYLAAPAAJAAqaHWABFgAA3ACiskoIEoOAAfODBrJZIjDKiUOiAhQeA5CZxuOCys46pQYJZjrRwZcoLQ%2FAqbMjpTLnMA8eCAIRYnEsLVhHXOKByzBQFDmmUpC1wPVwZVWGxKVzuTxmp3OY0wXFKALGZnBuVFaGKEzg0QAWjjKrjXpAd1E%2BBVtE9blTPvtzn4lFk9BRfoDQZDYatIAgdMokbMsGMsYTSZTab5TueTpdbsV4fyMWMvrL2MDLGDJjD9BgDejzfzFvjifdUDj9EHadwi51mYHt08O7gnYtheLPG1TvLE8rlCU1dr9eK85bK%2BO65uKCHohPOsdDu3As%2BTCVAMGwGBwXBZE7HZUs6hgEVw3lVclUvGV208PwoRKJQVUAi0NwPYwsIRXDV2uTdPHwoCwl4YDnFQYhyCoSDoNgtDhSoJCNVVKVFww4iXFIvDF0Ir87hI6EsxVCiiOovgT14fAAG1sOk1d8DUsjjlk8TPAAXXouArRgG0QXBe1iRIelF1WMg41kLYizsBAtLszZtn%2FHUrWYFzr2YLy4HZRdiVoOkDidOlgEoAB3AAhCAWjsUQAAY4DSgAmAAWOBsuWP1mDkWRkpQVAlkXdliTUMLgmCnUqusuk6uPZJ9CAA%3D%3D%3D

Expected Behaviour

For the following input:

import { onMount, onUpdate, useRef, useStore } from '@builder.io/mitosis'

type Colors = {
  primary?: string
  secondary?: string
}

interface IconProps {
  icon: string
  size?: 'xs' | 'md' | number
  type?: 'solid' | 'line'
  ariaLabel?: string
  color?: string
  colorSecondary?: string
}

export default function Icon(props: IconProps) {
  const hostRef = useRef<HTMLDivElement>(null)

  const state = useStore({
    setColors(colors: Colors) {
      if (!hostRef) {
        return
      }
      if (colors.primary) {
        hostRef.style.setProperty('--color-primary', colors.primary)
      } else {
        hostRef.style.removeProperty('--color-primary')
      }

      if (colors.secondary) {
        hostRef.style.setProperty(
          '--color-secondary',
          colors.secondary
        )
      } else {
        hostRef.style.removeProperty('--color-secondary')
      }
    },
  })

  onMount(() => {
    state.setColors({
      primary: props.color,
      secondary: props.colorSecondary,
    })
  })

  onUpdate(() => {
    state.setColors({
      primary: props.color,
      secondary: props.colorSecondary,
    })
  }, [props.color, props.colorSecondary])

  return (
    <div
      aria-label={props.ariaLabel}
      ref={hostRef}
    >
      <svg
        viewBox='0 0 24 24'
        fill='none'
      ></svg>
    </div>
  )
}

the compiler should output:

import { LitElement, html, css } from "lit";
import { customElement, property, state, query } from "lit/decorators.js";

type Colors = {
  primary?: string;
  secondary?: string;
};
interface IconProps {
  icon: string;
  size?: "xs" | "md" | number;
  type?: "solid" | "line";
  ariaLabel?: string;
  color?: string;
  colorSecondary?: string;
}

@customElement("my-icon")
export default class Icon extends LitElement {
  createRenderRoot() {
    return this;
  }

  @query('[ref="hostRef"]')
  hostRef!: HTMLElement;

  @property() ariaLabel: any;
  @property() color: any;
  @property() colorSecondary: any;

  setColors(colors: Colors) {
    if (!this.hostRef) {
      return;
    }

    if (colors.primary) {
      this.hostRef.style.setProperty("--color-primary", colors.primary);
    } else {
      this.hostRef.style.removeProperty("--color-primary");
    }

    if (colors.secondary) {
      this.hostRef.style.setProperty("--color-secondary", colors.secondary);
    } else {
      this.hostRef.style.removeProperty("--color-secondary");
    }
  }

  connectedCallback() {
    this.setColors({
      primary: this.color,
      secondary: this.colorSecondary,
    });
  }

  updated() {
    this.setColors({
      primary: this.color,
      secondary: this.colorSecondary,
    });
  }

  render() {
    return html`

          <div aria-label="${this.ariaLabel}" ref="hostRef">
          <svg viewBox="0 0 24 24" fill="none"></svg>
        </div>

        `;
  }
}

Actual Behaviour

For the following input:

import { onMount, onUpdate, useRef, useStore } from '@builder.io/mitosis'

type Colors = {
  primary?: string
  secondary?: string
}

interface IconProps {
  icon: string
  size?: 'xs' | 'md' | number
  type?: 'solid' | 'line'
  ariaLabel?: string
  color?: string
  colorSecondary?: string
}

export default function Icon(props: IconProps) {
  const hostRef = useRef<HTMLDivElement>(null)

  const state = useStore({
    setColors(colors: Colors) {
      if (!hostRef) {
        return
      }
      if (colors.primary) {
        hostRef.style.setProperty('--color-primary', colors.primary)
      } else {
        hostRef.style.removeProperty('--color-primary')
      }

      if (colors.secondary) {
        hostRef.style.setProperty(
          '--color-secondary',
          colors.secondary
        )
      } else {
        hostRef.style.removeProperty('--color-secondary')
      }
    },
  })

  onMount(() => {
    state.setColors({
      primary: props.color,
      secondary: props.colorSecondary,
    })
  })

  onUpdate(() => {
    state.setColors({
      primary: props.color,
      secondary: props.colorSecondary,
    })
  }, [props.color, props.colorSecondary])

  return (
    <div
      aria-label={props.ariaLabel}
      ref={hostRef}
    >
      <svg
        viewBox='0 0 24 24'
        fill='none'
      ></svg>
    </div>
  )
}

the compiler outputs:

import { LitElement, html, css } from "lit";
import { customElement, property, state, query } from "lit/decorators.js";

type Colors = {
  primary?: string;
  secondary?: string;
};
interface IconProps {
  icon: string;
  size?: "xs" | "md" | number;
  type?: "solid" | "line";
  ariaLabel?: string;
  color?: string;
  colorSecondary?: string;
}

@customElement("my-icon")
export default class Icon extends LitElement {
  createRenderRoot() {
    return this;
  }

  @query('[ref="hostRef"]')
  hostRef!: HTMLElement;

  @property() ariaLabel: any;
  @property() color: any;
  @property() colorSecondary: any;

  setColors(colors: Colors) {
    if (!hostRef) {
      return;
    }

    if (colors.primary) {
      hostRef.style.setProperty("--color-primary", colors.primary);
    } else {
      hostRef.style.removeProperty("--color-primary");
    }

    if (colors.secondary) {
      hostRef.style.setProperty("--color-secondary", colors.secondary);
    } else {
      hostRef.style.removeProperty("--color-secondary");
    }
  }

  connectedCallback() {
    this.setColors({
      primary: this.color,
      secondary: this.colorSecondary,
    });
  }

  updated() {
    this.setColors({
      primary: this.color,
      secondary: this.colorSecondary,
    });
  }

  render() {
    return html`

          <div aria-label="${this.ariaLabel}" ref="hostRef">
          <svg viewBox="0 0 24 24" fill="none"></svg>
        </div>

        `;
  }
}

Additional Information

Basically, the compiler should add this before the useRef reference.

samijaber commented 1 year ago

This is the line that should be doing that: https://github.com/BuilderIO/mitosis/blob/d7e1c999b57316a061ef4f4b5d8cd635d790cf39/packages/core/src/generators/lit/generate.ts#L140

There seems to be a bug where it's not acting properly 🤔