Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive Components in Playground UI MVP #75

Open
mariuswilms opened this issue Jun 27, 2019 · 1 comment · May be fixed by #139
Open

Interactive Components in Playground UI MVP #75

mariuswilms opened this issue Jun 27, 2019 · 1 comment · May be fixed by #139

Comments

@mariuswilms
Copy link
Member

mariuswilms commented Jun 27, 2019

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

  • Users can embed React user components into documents inside a playground
  • Users must be able to embed a user component by simply providing i.e. <Avatar> through familiar JSX syntax
  • camelCased props are supported on user components
  • Event handlers are automatically provided and must not be manually configured or passed into the user component
  • Our example component library should be loadable by DSK for demonstration purposes
  • Must be able to load CSS and JS and other assets required by the component library
  • We may support non-scalar values in props of user components
  • Keep runtime nature of DSK
  • Reuses Playground component
  • Entirely isolates styles and ... with iFrames!
  • Playground has fixed size
  • Playground provides an action log console

Non-Goals

  • Older browsers support
  • Support for old bundle formats
  • Get into other people's build business
  • Support for web components, vue components
  • Support multiple component libraries
  • More configuration
  • Support automatic event handlers for custom events
  • Support user components outside playgrounds
  • Support "real coding" inside playgrounds
  • User components outside playgrounds
  • Documentation components inside playgrounds
  • Add additional namespacing, we don't allow user components outside playgrounds, so they are automatically namespaced by the fact they are inside the playground (only user components can exist in playgrounds)

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 via NodeDoc.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
  3. adding implied code (see below)
  4. using esbuild transform JSX syntax into JavaScript
  5. now the HTML document for the iframe is build
  6. 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:

  • a DOM element that can be used to attach a component (tree) to
  • tag to load the playground runtime JavaScript as an ESM
  • tag to load the user component library JavaScript as an ESM
  • inlined JavaScript of the playground using the two above
  • tag to load the user component library CSS
<!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

This comment has been minimized.

@mariuswilms mariuswilms changed the title Support design system components inside documents Interactive Components in Playground UI Apr 23, 2021
@mariuswilms mariuswilms changed the title Interactive Components in Playground UI Interactive Components in Playground UI MVP Apr 30, 2021
@mariuswilms mariuswilms linked a pull request May 2, 2021 that will close this issue
3 tasks
@mariuswilms mariuswilms linked a pull request May 2, 2021 that will close this issue
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant