rundsk / dsk

DSK, short for “Design System Kit” – a workbench for collaboratively creating Design Systems
https://rundsk.com
Other
82 stars 8 forks source link

Interactive Components in Playground UI MVP #75

Open mariuswilms opened 5 years ago

mariuswilms commented 5 years ago

This issue is a work in progress design document, core members may edit this description anytime. It tries to outline the design of the feature but is not authoritive. This means implementors are free to find an implementation that allows us to ship the issue.

We've split this implementation effort into three parts as it turned out to have significant complexity.

Having the ability to demonstrate interactive components from a user provided component library inside design system documentation is immensively useful.

Context and scope

User components are components that are "talked about" inside the design system and used wherever the design system is applied. These kind of components are provided by the user and stored next to the DDT.

Documentation components are components that are provided by us by default (Playground is a built-in documentation component).

User components will usually be placed in a playground, where they can be interacted with. Playgrounds can be created by using the Playground documentation component.

<Playground>
   <MyTextField />
</Playground>

Goals

Non-Goals

TBD

  1. Do we inject state handlers?

Overview

Along with the documents that form the design system (the DDT) users may provide a path to a component library, containing components that can be used inside the documents. These are stored outside the DDT in a separate path.

Users will need to provide us with a static folder that (depending on which implementation approach we use) we can use for loading components and serving other assets. The JavaScript bundle may be provided as an ES module bundle.

This is how users will provide the path to a component library on startup:

./dsk -components path/to/component-libary/build path/to/design-system-documents 

The contents of path/to/component-libary/build will need to be served as-is by the backend under an URL prefix i.e. /components/.

Assuming the user provided component library contains an Avatar component, the user will now begin to document the component in a markdown document called readme.md inside an 00_Avatar folder. The user wants to provide an interactive demo of the component and includes a playground.

# Avatar

Lorem impsum...

<Playground>
  <Avatar name="Marius" />
</Playground>

Using the builtin frontend and through the web interface a user views the Avatar node and the readme.md document. The backend will receive an API request for the DDT tree node that contains that document.

GET /tree/Avatar

While building the API response (using the code in internal/ddt/node.go) the backend will process the document (using ìnternal/ddt/node_doc.go) to convert it to a HTML respresentation viaNodeDoc.HTML()`.

After receiving the API response the frontend will begin to render the document's markup. It will use the DocTransfomer to transform the HTML markup into DOM tree that is mountable by React. While doing so it will also ensure that documentation components (including any Playground) provided by DSK itself are mounted.

When documentation components are mounted they usually use their content as the component's children.

Currently the playgound is no exception in that. However we'll change that behavior for the playground. Its contents should not be touched by the transformer and kept as literal text content, similar to the contents of a CodeBlock.

The component will render an iframe maybe along with additonal controls (i.e. action console logs) inside or outside the iframe.

The component will compute a hash over it's contents and use that to construct an URL which will be used as the source for the iframe: sha1('<Playground><Avatar /></Playground>') -> f1d2d2f924e986ac86fdf7b36c94bcdf32beec15

The alogrithm that computes the hash is build around an existing hashing algorithm sha1. Sha1 is already used in other places so it might be a good choice.

Using the hash the playground component will construct the following iframe source URL /tree/Avatar/_playgrounds/f1d2d2f924e986ac86fdf7b36c94bcdf32beec15.html.

<iframe src="..../playgrounds/.....html"></iframe>

When the backend receives the request for the HTML document it will:

  1. find the contents of the playground, by
    1. going over all documents of the Avatar node
    2. and each document's playground component's contents, i.e. using NodeDoc.findComponentsIn..
    3. hashing the contents with the same algorithm as in the frontend
    4. comparing them with the hash provided in request,
    5. to find a match, then
  2. process the contents of the playground
    1. adding implied code (see below)
    2. using esbuild transform JSX syntax into JavaScript
  3. now the HTML document for the iframe is build
  4. later: the HTML is rendred inside the iframe and displayed to the user

The iframe HTML

The iframe's HTMl is a simple document comparable to the ones provided by default by CRA. It consists of:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" type="text/css" href="/components/main.css">
    <script src="/components/main.js"></script>
    <script src="/playground-runtime-js"></script>
    <script>

    </script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Implied playground code

As we want to make it as easy as possible for users (i.e. must not provide any configuration) we imply code when providing the contents for a playground.

We might at a later point allow users to opt out of that easy mode and do real programming inside playgrounds. For the MVP we assume all contents are React components with JSX syntax from the component library provided at startup.

These contents of a playground:

<Avatar />

are treated as JSX with additional implied code.

import { ReactDOM } from 'playground-runtime.js'; // We'll provide that library.
import { Avatar } from '/components/main.js'; // Assuming this is the main entry point and ESM compatible.

ReactDOM.render(
  <Avatar />
  document.getElementById('root')
);

after running it through esbuild will result in:

// We don't bundle the imports with the playground contents.
import { ReactDOM, React } from 'playground-runtime.js';
import { Avatar } from '/components/main.js'; 

ReactDOM.render(
  React.createElement(Avatar, null), 
  document.getElementById('root')
);

Playground Runtime

The playground runtime bundles/provides libraries that can be used by the playground code. Currently this is only ReactDOM + React. The library code must be made available at a URL by the backend i.e. /_js/playground-runtime.js as an ES module.

The runtime will also provide means that can inspect the component's props and inject common event handlers, that are automatically connected to the playground's action log.

import { injectEventHandlers } from 'playground-runtime.js';
/* ... */
React.createElement(Avatar, injectEventHandlers({ /* ... */ })), 

Ideas Pool

How can we load a user component?

(a) Users provide a well known entry point manifest and ES modules.

(b) One possible approach is that we leverage esbuild to combine our frontend code and their component code (provided as a prebuilt bundle) in memory and on request to i.e. /static/js/main.js. Our frontend code is already baked into the binary (but it must be our source) and can be read from memory from there. We'll have to replace our frontend's (CRA) webpack with ebuild.

(c) Another approach would try to keep the component bundle separate.

(d) We could leverage federated bundles:

Multiple separate builds should form a single application. These separate builds should not have dependencies between each other, so they can be developed and deployed individually. This is often known as Micro-Frontends, but is not limited to that.

How can we know what props a component can/must receive?

(a) Inspect the component object we've pulled out of the bundle in part I and reflect which handlers we need to pass. See: https://storybook.js.org/docs/react/essentials/actions

Sources

mariuswilms commented 5 years ago

Will need #76