Hopp til hovedinnhold

Storybook tilbyr en rå, interaktiv playground for å teste ulike props, men det kommer med en pris: med det følger en tvangstrøye av et design.

Ved å kombinere React Live og litt strengkonkatenering kan du lage en løsning du har full kontroll over, samtidig som du gir brukerne dine en effektiv måte å få oversikt over en komponents muligheter.

Hva består en playground av?

Det jeg hadde lyst på var en playground, som lot meg teste ulike props og å se hvordan komponenten ville se ut, lignende det Storybook tilbyr:

Endrer forhåndsvisning ved å endre et skjema
I Storybook kan du endre skjema props i skjema for å endre forhåndsvisning av komponent

Vi hadde valgt bort Storybook til fordel for fleksibilitet, og måtte derfor lage noe tilsvarende på egenhånd. Så hvordan bygger du noe sånt fra scratch?

Vi kan dele det opp i noen enklere funksjonaliteter:

  • Forhåndsvisning av komponent
  • Kodeeditor
  • Endre komponenten via skjema
  • Generere skjema utifra props

Forhåndsvisning og kodeeditor

La oss starte med React Live, som huker av for to krav. Det er et bibliotek som tilbyr både forhåndsvisning av kode og en kodeeditor. Koden som blir vist er styrt av code-propen i LiveProvider:

const code = `<Button variant="secondary" size="medium">Knapp</Button>`;

<LiveProvider code={code}>
 <LivePreview />
 <LiveEditor />
</LiveProvider>

Her er hvordan dette ser ut rendret på en side:

Endring i editor i React live endrer forhåndsvisning
Med React Live får du forhåndsvisning og editor

Når koden endres, oppdateres forhåndsvisningen. Det skjer også om en bruker endrer på teksten i editoren.

Men vi ønsker ikke å tvinge brukerne til å skrive ut alle varianter selv via editoren. Så hvordan kan vi endre koden utenfor selve kodeeditoren?

Hvordan endre på komponenten med et skjema

Siden forhåndsvisningen automatisk endrer seg når koden i LiveProvider endrer seg, trenger vi bare å sette koden for LiveProvider i en variabel, så vi senere kan oppdatere den:

const [code, setCode] = useState<string>("");

Vi kan så lage en variabel componentProps for å holde styr på props. Vi lager det som et objekt, så vi kan holde styr på hvilken prop som har hvilken verdi. Her initert med variant og children:

type ComponentProps = Record<string, string>;

const [componentProps, setComponentProps] = useState<ComponentProps>({
  variant: "secondary",
  children: "knapp"
});

Vi kan så oppdatere code-variabelen når componentProps endres. Dette gjør vi via en useEffect.

Siden LiveProvider tar imot en streng, gjør vi om objektet til en streng med key-value-par. Så putter vi den strengen i komponentnavnet for å rendre komponenten riktig:

useEffect(() => {
  const propsString = Object.entries(componentProps)
    .map(([key, value]) => `${key}="${value}"`)
    .join(" ");
  setCode(`<Button ${propsString} />`);
}, [componentProps]);

Her er resultatet:

Playground og editor med props
Vi har fått til å rendre komponenten ved å iterere over forhåndsdefinerte props

Vi har nå gått fra å hardkode en streng, til å danne strengen via props definert i et objekt. Resultatet er det samme, men omskrivningen vår gjør det lettere for oss å legge til noe viktig: interaktivitet.

Hvordan får vi til interaktivitet?

For å få til interaktivitet bruker vi et skjemaelement som vil oppdatere componentProps. Vi lager en handler handlePropChange som tar imot propnavnet vi vil oppdatere og den nye verdien.

Her legger vi handleren på en select:

// 👇 En enkel funksjon som oppdaterer en nøkkel i et objekt, vårt propnavn, med en ny verdi
const handlePropChange = (propName: string, value: string): void => {
  setComponentProps({ ...componentProps, [propName]: value });
};

// ...mer kode

return (
  <LiveProvider code={code}>
    <form>
      <label>
      variant
        <select
          {/* 👇 Vi bruker handleren for å oppdatere prop-verdien */}
          onChange={(e: ChangeEvent<HTMLSelectElement>): void =>
            handlePropChange("variant", e.target.value)
          }
          value={componentProps.variant}
        >
          {/* 👇 Vi viser de tilgjengelige prop-verdiene */}
          {["primary", "secondary"].map((option) => (
            <option key={option} value={option}>
              {option}
            </option>
          ))}
        </select>
      </label>
    </form>
    <LivePreview />
    <LiveEditor />
  </LiveProvider>
);

Nå har vi fått til interaktivitet for en av propsene våre:

Endrer variant via en select som oppdateres i forhådsvisning
Nå kan vi enkelt endre visning av knappen for propen variant

Men ulike komponenter vil ha ulike inputs avhengig av props. Hvordan kan vi generere skjemaelementer basert på props?

Generere skjema utifra props

En måte er å definere hvilke props en knapp har, og hvilke verdier vi ønsker å vise frem. Legg merke til at vi også definerer type, som vi vil bruke for å switche hvilket skjema-element vi rendrer verdiene i:

interface PropRenderOption {
  propName: string;
  type: "select" | "textInput";
  options?: string[];
}

const propRenderOptions: PropRenderOption[] = [
  {
    propName: "variant",
    type: "select",
    options: ["primary", "ghost"]
  },
  {
    propName: "children",
    type: "textInput"
  }
];

Etter å ha definert typer, kan vi switche over props og rendre passende skjema-elementer, her med eksempel select og text-input:

const inputs = propRenderOptions.map((prop) => {
  switch (prop.type) {
    case "textInput": // 👈 Vi rendrer en passende skjema-input avhengig av typen vi har satt
      return (
        <div key={prop.propName}>
          <label>{prop.propName}</label>
          <input
            // 👇 Ved endringer oppdaterer vi en prop med ny verdi
            onChange={(e: ChangeEvent<HTMLInputElement>): void =>
              handlePropChange(prop.propName, e.target.value)
            }
            type="text"
            value={componentProps[prop.propName] || ""}
          />
        </div>
      );
    case "select": // 👈 Samme handler brukes for select
      return (
        <div key={prop.propName}>
          <label>{prop.propName}
            <select
              onChange={(e: ChangeEvent<HTMLSelectElement>): void =>
                handlePropChange(prop.propName, e.target.value)
              }
              value={componentProps[prop.propName] || ""}
            >
              {prop.options?.map((option) => (
                <option key={option} value={option}>
                  {option}
                </option>
              ))}
            </select>
          </label>
        </div>
      );
    default:
      return null;
  }
});

return (
  <LiveProvider code={code}>
    <form>{inputs}</form>
    <LivePreview />
    <LiveEditor />
  </LiveProvider>
);

Her er resultatet:

Endrer variant og children via skjema, som igjen oppdaterer forhåndsvisning av knapp
Playground med forhåndsvisning, editor og skjema lagd med React live

Avsluttende ord

En playground er et utrolig nyttig verktøy som effektivt demonstrerer mulighetene til en komponent. Med bruk av React Live og litt streng-konkatenering, har vi sett hvor langt vi kan ta funksjonaliteten.

Over har jeg vist en basal løsning for å få prinsippene frem, men her er noen forslag til videre forbedringer:

  • Flytt playgroundProps ut i egen fil for oversiktlighet
  • Legg også til inititialProps, for bedre startpunkt over hva komponenten kan gjøre
  • Ikke rendre children som en prop, men mellom opening- og closing tag.
  • Støtt sammensatte komponenter
  • Finn en automagisk måte å hente ut komponenter sine props (det har dessverre ikke jeg funnet, så rop ut om du finner en løsning!)

💛 Denne playgrounden er inspirert av Enturs playground. Tusen takk til Magnus Rand som pekte meg i retning av hvordan deres var lagd, så jeg kunne lage min egen versjon.

Liker du innlegget?

Del gjerne med kollegaer og venner