How it works
Blocks is a parser, transformer, and renderer/compiler.
It's unique because it reads in valid, production-ready JSX code and treats the AST as its data structure. Information is queried from the AST and then changes to the canvas transform the AST.
It uses Babel and its plugin API for parsing and transforming. Events that happen in the canvas are emitted and run corresponding plugins on the source code.
Note: Blocks is still extremely alpha software so a wide array of optimizations and other enhancements are missing which includes maintaining the source as the AST structure rather than a JSX string and maintaining an internal representation for rendering after the first inline function evaluation.
Preparing the source code
Blocks essentially maintains two states of the source code internally. One is a slight transform from the original source code with a UUID added. This is used to determine what JSX elements to modify. The second state of source code is the transformed version which is used to render the canvas.
Internal representation
The internal representation of the source code looks pretty similar to
what was passed in. The only noticeable change is the addition of a
UUID that's passed as a prop (___uuid
).
Input
import React from 'react'import Blocks from 'blocks-ui/components'export default () => (<Blocks.Root><h1>Hello, world!</h1></Blocks.Root>)
Output
import React from 'react'import Blocks from 'blocks-ui/components'export default () => (<Blocks.Root><h1 ___uuid="123abc">Hello, world!</h1></Blocks.Root>)
Renderable representation
The render transform takes the internal representation and then does the following:
- converts JSX to function calls (using the Blocks pragma)
- replaces
Blocks.Root
with a specialBLOCKS_Root
for drag and drop - injects the implementation of
BLOCKS_Root
- removes imports
- rewrites the default export to be a
BLOCKS_Container
variable - rewrites all named exports to be variable declarations
- wraps all blocks on the canvas in the draggable implementation
Input
import React from 'react'import Blocks from 'blocks-ui/components'export default () => (<Blocks.Root><h1 ___uuid="123abc">Hello, world!</h1></Blocks.Root>)
Output
TODO
Rendering the canvas
After the transform for a renderable output, Blocks uses a custom pragma, AST metadata, custom scope (including blocks passed to the editor) and inline function evaluation to render the canvas.
Pragma
Blocks uses its own custom pragma which is used when rendering to the canvas. This is used for handling the UUID, element selection, and styling based on selection/hover/focus state.
It's a light wrapper around Theme UI's custom pragma. It's rough implementation looks something like:
export default elementSelectionHandler => (type, props, ...children) => {// Grab the current selected element from contextconst element = useElement() || {}props = props || {}const { ___uuid: id, sx = {} } = propsdelete props.___uuidconst isCurrentElement = id && id === element.idreturn jsx(type,{...props,sx: {...sx,boxShadow: isCurrentElement? 'inset 0px 0px 0px 2px #0079FF': sx.boxShadow},onClick: e => {e.stopPropagation()if (id) {elementSelectionHandler(id)}}},...children)}
Renderer
The renderer itself receives the transformed source code and the scope of the canvas and its Blocks. Then, it initializes a function and evaluates it inline:
const fn = new Function('React',...Object.keys(scope),`${code};return React.createElement(BLOCKS_Container)`)return fn(React, ...Object.values(scope))
This is wrapped up in a component so the API is nicer:
<InlineRender scope={scope} code={transformedCode} theme={theme} />
Querying element information
In addition to Babel transforms, Blocks needs to query information from the AST in order to query information about a selected element. Blocks uses information including:
- element name
- parent UUID
- child names and UUIDs
- text (if all children are text nodes)
In order to achieve this, a Babel plugin is run to search for a JSX element with a matching UUID and then pulling information from the node in the AST.
{JSXOpeningElement: path => {const id = path.node.attributes.find(// uuidName is the matching uuid property and it's equal to ___uuidnode => node && node.name && node.name.name === uuidName)if (!id || id.value.value !== elementId) {return}const children = path.container.children || []const hasElements = children.some(n => !t.isJSXText(n))const element = {id: elementId,name: getElementName(path.node),props: getElementProps(path.node.attributes),parentId: getParentId(path)}// ...}}
In addition to the current element, Blocks queries other metadata from the AST including imports, exports, and the Blocks for the canvas.
Queries are wrapped up into a queries
module which can be used
like so:
queries.getCurrentElement(code, elementId)
Returning new JSX
The new, modified JSX code is returned in an onChange
event.
It takes the internal representation of the JSX and runs a
Babel plugin to remove the UUID and any other Blocks metadata.
This results in
Input
import React from 'react'import Blocks from 'blocks-ui/components'export default () => (<Blocks.Root><h1___uuid="123abc"sx={{color: 'tomato'}}>Hello, world!</h1></Blocks.Root>)
Output
import React from 'react'import Blocks from 'blocks-ui/components'export default () => (<Blocks.Root><h1 sx={{ color: 'tomato' }}>Hello, world!</h1></Blocks.Root>)