If you've used react-native
before then you may know that "css" you're writing isn't actually CSS. It's a descriptor for a layout engine. That layout engine is css-layout which was created by @vjeux and compiled to Java/Objective-C.
Layout is difficult. There have been attempts at various constraint based layouts in JavaScript, GSS is one of those. Which is a port of Apples Cassowary
constraint solver which was also ported to JavaScript https://github.com/slightlyoff/cassowary.js.
Now this is all fine and well but from the bit I've read constraints fall down sometimes. This usually happens when you don't specify enough constraints based on the current layout.
Parent layouts get effected by their child layouts. CSS is weird in that you can remove items from layout with position: absolute
but ultimately the top parent width/height
is directly effected by it's children.
This is an over simplification but many times in React
you have to hook into componentDidMount
get the child width and take action.
An example would be even width labels in a form. We could measure the text but depending on font
, font-size
it could measure each value differently. So we hide the form on initial render, get the max label width and set it on state.
It takes a subset of flexbox and some other styling parameters and returns width
, and the left/top
offsets for each item and it's children.
This example is taken directly from the css-layout
git repo.
computeLayout( {style: {padding: 50}, children: [ {style: {padding: 10, alignSelf: 'stretch'}} ]} ); // => { width: 120, height: 120, top: 0, left: 0, children: [{ width: 20, height: 20, top: 50, left: 50 }] }
So explaining this. We have an item with a padding of 50
, so 50px
around the entire item.
It's child item has a padding of 10
, so 10px
all the way around.
Therefore the parent has 50*2 = 100
initial width/height. Now we take into account the children.
Width/Height:
Parent: `50*2 = 100` + `10*2 = 20` = `100 + 120`; Child: `10*2 = 20`;
Position:
Parent: `x = 0, y = 0` since we are starting there. Child: Parent padding `50` so our child is inset at `x = 50, y = 50`
Lets change the child width and see what happens
computeLayout( { style: { padding: 50 }, children: [{ style: { padding: 10, width: 1000, alignSelf: 'stretch' } }] } ); // => { "width": 1100, "height": 120, "top": 0, "left": 0, "children": [{ "width": 1000, "height": 20, "top": 50, "left": 50 }] }
Because our child defined a width of 1000
we then add on our 50*2
of padding on both sides and now the parent has a width of 1100
.
There are some other nuances that you can read about in the css-layout
repo.
First off we'll need a component tree. Now in React they transpile JSX, and build the component tree for us. However in our case we can just create a JSON tree.
var componentTree = { style: { padding: 10, }, component: Rectangle, children: [ { style: { padding: 10, flexDirection: "column", alignItems: "center", }, component: Rectangle, children: [ { style: { width: 30, height: 30, }, component: Rectangle, }, { style: { margin: 10, width: 50, height: 50, alignItems: "center", justifyContent: "center", }, component: Circle, children: [ { style: { width: 10, height: 10, }, component: Circle, }, ], }, ], }, ], };
We add a component
parameter to the tree. This is the thing that will be rendered.
We could add additional properties here, maybe create custom renderers but we'll keep it simple.
The break down is like so
10px
of padding all around10px
of padding, and it's children in a column
based layout.center
width = 30, height = 30
width = 50, height = 50
and a surrounding 10px
margin and it's children centered vertically and horizontallywidth = 10
and `height = 10
We'll have to do some basic math on the Circle to compute the radius, and center it correctly.Now we'll need to traverse the layout. We'll do that with a function that calls itself
function traverseLayout(componentTree, layout) { var Component = componentTree.component; return ( <Group x={layout.left} y={layout.top}> <Component {...getProps(Component, layout)} /> <Group x={0} y={0}> {!componentTree.children ? null : componentTree.children.map(function (child, index) { return traverseLayout(child, layout.children[index]); })} </Group> </Group> ); }
This is a super crude layout renderer but it works for our purposes.
It creates a group and we pass in our top/left
to x/y
of the group. This is necessary to make our children top/left
work correctly.
Then renders the component with selected props. We'll just render stroke="#000"
and a stroke=1.
Then if we have children we will call ourself with the child component and layout.
To process the props we need to render different props for specific components.
Rectangle
needs width/height
which we have.
Circle
needs the radius computed, and to then be ofset by the radius. So we just divide the width/2
and for positioning y
we divide the height/2
.
function getProps(component, layout) { var props = { x: 0, y: 0, }; if (Rectangle === component) { props.width = layout.width; props.height = layout.height; } else if (Circle === component) { props.radius = layout.width / 2; props.x += layout.width / 2; props.y += layout.height / 2; } props.stroke = "#000"; // Just to visualize props.strokeWidth = 1; // Just to visualize return props; }
That is it, now we can render a tree of Rectangles
and Circles
. The complete code is below.
var React = require("react"); var ReactArt = require("react-art"), Surface = ReactArt.Surface, Group = ReactArt.Group, computeLayout = require("css-layout"), Circle = require("react-art/shapes/circle"), Rectangle = require("react-art/shapes/rectangle"); var Surface = ReactArt.Surface; var viewportWidth = function () { return window.innerWidth - 100; }; var viewportHeight = function () { return window.innerHeight - 100; }; var componentTree = { style: { padding: 10, }, component: Rectangle, children: [ { style: { padding: 10, flexDirection: "column", alignItems: "center", }, component: Rectangle, children: [ { style: { width: 30, height: 30, }, component: Rectangle, }, { style: { margin: 10, width: 50, height: 50, alignItems: "center", justifyContent: "center", }, component: Circle, children: [ { style: { width: 10, height: 10, }, component: Circle, }, ], }, ], }, ], }; var layout = computeLayout(componentTree); function traverseLayout(componentTree, layout) { var Component = componentTree.component; return ( <Group x={layout.left} y={layout.top}> <Component {...getProps(Component, layout)} /> <Group x={0} y={0}> {!componentTree.children ? null : componentTree.children.map(function (child, index) { return traverseLayout(child, layout.children[index]); })} </Group> </Group> ); } function getProps(component, layout) { var props = { x: 0, y: 0, }; if (Rectangle === component) { props.width = layout.width; props.height = layout.height; } else if (Circle === component) { props.radius = layout.width / 2; props.x += layout.width / 2; props.y += layout.height / 2; } props.stroke = "#000"; props.strokeWidth = 1; return props; } var Demo = React.createClass({ getInitialState: function () { return {}; }, getRenderLayout: function () { return traverseLayout(componentTree, layout); }, render: function () { return ( <div> <Surface width={viewportWidth()} height={viewportHeight()}> {this.getRenderLayout()} </Surface> </div> ); }, }); module.exports = Demo;
Tic-tac-toe? Heh.
This could be made to layout components arbitrarily much like we do with react-native
. It's not perfect and could only be used in specific scenarios but it's still a fun prototype.