├── .babelrc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── __tests__ ├── __snapshots__ │ └── index.js.snap └── index.js ├── package-lock.json ├── package.json └── src ├── index.js └── render-styles.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | indent_size = 2 8 | end_of_line = lf 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | dist 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 6 5 | - 8 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dr. X 2 | 3 | _Declarative React Experiments_ 4 | 5 | [![Build Status](https://secure.travis-ci.org/joshwnj/drx.png)](http://travis-ci.org/joshwnj/drx) 6 | 7 | ## Install 8 | 9 | ``` 10 | npm install --save drx 11 | ``` 12 | 13 | ## Let's see the example right away pls 14 | 15 | [-> codepen](https://codepen.io/joshwnj/pen/zdLree?editors=0011#) 16 | 17 | ## Q. Is this ready to use in production? 18 | 19 | Nope! Things are most likely going to change. 20 | 21 | ## Q. What is this? Is it even a good idea? 22 | 23 | I'm still figuring that out :D But I'm discovering a lot of useful patterns arising from this idea of writing components (structure & logic) declaratively. 24 | 25 | In short, the goal is to provide a way of creating components by describing the dependencies between their props. 26 | 27 | Take a look at the examples below if you're keen, and let me know what you think. 28 | 29 | ## Getting started 30 | 31 | Let's start with the simplest possible example: 32 | 33 | ```js 34 | import x from 'drx' 35 | 36 | export default x({ 37 | className: 'message', 38 | children: '' 39 | }) 40 | ``` 41 | 42 | This is the rough equivalent of: 43 | 44 | ```js 45 | import React, { PureComponent } from 'react' 46 | 47 | export default class extends PureComponent { 48 | render () { 49 | const { children, className } = this.props 50 | 51 | return ( 52 |
53 | {children} 54 |
55 | ) 56 | } 57 | } 58 | ``` 59 | 60 | So we saved a few lines by using `drx`. That won't always be the case, sometimes we'll end up with more lines than a traditional approach. But I hope we can get to the point of gaining much more value from those extra lines. 61 | 62 | ## Example: translating props 63 | 64 | A common bit of display logic in components is translating props from a parent to a child. For example, this component receives an `imageUrl` prop which becomes the `src` of a child image element: 65 | 66 | ```jsx 67 | import React, { PureComponent } from 'react' 68 | 69 | export default class extends PureComponent { 70 | render () { 71 | const { children, imageUrl } = this.props 72 | 73 | return ( 74 |
75 | { imageUrl && } 76 |

{props.heading || 'Default Heading'}

77 | { children } 78 |
79 | ) 80 | } 81 | } 82 | ``` 83 | 84 | There are 3 prop translations happening in the example above: 85 | 86 | 1. `imageUrl` becomes the `src` of the image 87 | 1. `heading` becomes the `children` of the h1 (with fallback to a default value) 88 | 1. `children` becomes the `children` of the span 89 | 90 | We've also got some display logic to say we don't want to render an `` element if we don't have an `imageUrl`. 91 | 92 | To write the above with `drx` we'll get something like this: 93 | 94 | ```js 95 | import x from 'drx' 96 | 97 | const Root = x({ 98 | className: 'message', 99 | imageUrl: '', 100 | children: '', 101 | heading: 'Default Heading' 102 | }) 103 | 104 | const Image = x.img({ 105 | className: 'message__image', 106 | src: Root.imageUrl 107 | }) 108 | 109 | const Heading = x.h1({ 110 | className: 'message__heading', 111 | children: Root.heading 112 | }) 113 | 114 | const Text = x.span({ 115 | className: 'message__text', 116 | children: Root.children 117 | }) 118 | 119 | Root.children( 120 | Heading, 121 | props => props.imageUrl && Image, 122 | Text 123 | ) 124 | 125 | export default Root 126 | ``` 127 | 128 | Reading from the top: 129 | 130 | - a component `Root`, with some default props 131 | 132 | - a component `Image` 133 | - renders an `` with classname `message__image` 134 | - maps the `Root.imageUrl` prop to the `src` attribute of the `` 135 | 136 | - a component `Heading` 137 | - renders an `

` with classname `message__heading` 138 | - maps the `Root.heading` prop to the heading's `children`. If no `heading` prop is provided to `Root`, we'll get the default heading value from `Root`'s definition. 139 | 140 | - a component `Text` 141 | - renders a `` with classname `message__text` 142 | - adopts the `children` of the `Root` component as its own `children` 143 | 144 | - finally we tell `Root` to render with `Heading`, `Image` and `Text` as its children 145 | - `Image` will only be rendered if we have a truthy `imageUrl` prop 146 | 147 | ## Example: reducing props 148 | 149 | Sometimes a child's prop is a function of 1 or more parent props. We can declare this with `x.from`. It both defines the dependency (ensuring that the props are passed down) and calls the function to transform or reduce the original values. 150 | 151 | ``` 152 | import x from 'drx' 153 | 154 | const Root = x({ 155 | caption: '', 156 | imageUrl: '', 157 | secure: false 158 | }) 159 | 160 | const Text = x.div({ 161 | children: x.from(Root.caption, p => p.caption.toUpperCase()) 162 | }) 163 | 164 | const Image = x.img({ 165 | alt: Root.caption, 166 | src: x.from( 167 | Root.imageUrl, Root.secure, 168 | p => `${p.secure ? 'https' : 'http'}://example.com/${p.imageUrl}` 169 | ) 170 | }) 171 | 172 | export default Root.children( 173 | Image, 174 | Text 175 | ) 176 | ``` 177 | 178 | ## Inspiration 179 | 180 | - [recompose](https://github.com/acdlite/recompose) 181 | - [styled-components](https://github.com/styled-components/styled-components) 182 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Attributes not rendered on a wrapper component 1`] = `
`; 4 | 5 | exports[`Attributes passed from wrapper component to element 1`] = ` 6 |
7 | 11 |
12 | `; 13 | 14 | exports[`Attributes rendered on an element 1`] = ` 15 |
19 | `; 20 | 21 | exports[`Attributes rendered on an element with parent props 1`] = ` 22 |
26 | `; 27 | 28 | exports[`Classes with a single class 1`] = ` 29 |
32 | `; 33 | 34 | exports[`Classes with conditional classes 1`] = ` 35 |
38 | `; 39 | 40 | exports[`Classes with conditional classes 2`] = ` 41 |
44 | `; 45 | 46 | exports[`Classes with multiple classes 1`] = ` 47 |
50 | `; 51 | 52 | exports[`Classes with no classes 1`] = `
`; 53 | 54 | exports[`Conditional rendering using a child-function 1`] = ` 55 |
58 | `; 59 | 60 | exports[`Conditional rendering using a child-function 2`] = ` 61 |
64 | 67 |
68 | `; 69 | 70 | exports[`Default values with changed defaults 1`] = ` 71 |
74 | `; 75 | 76 | exports[`Default values with changed defaults 2`] = ` 77 |
80 | `; 81 | 82 | exports[`Default values with wrapper component 1`] = ` 83 |
84 | default text 85 |
86 | `; 87 | 88 | exports[`Default values with wrapper component 2`] = ` 89 |
90 | custom text 91 |
92 | `; 93 | 94 | exports[`Lists simple example 1`] = ` 95 |
98 |
101 | Image One 106 |
107 |
110 | Image Two 115 |
116 |
117 | `; 118 | 119 | exports[`Props selected from grandparent 1`] = ` 120 |
123 |
126 | The Image 130 | 131 | The Image 132 | 133 |
134 |
135 | `; 136 | 137 | exports[`Props selected from parent 1`] = ` 138 |
141 | The Image 145 | 146 | The Image 147 | 148 |
149 | `; 150 | 151 | exports[`Reducing a single prop with a transform function 1`] = ` 152 |
155 | The Image 159 |
160 | `; 161 | 162 | exports[`Reducing multiple props with a transform function 1`] = ` 163 |
166 |
169 | The Image 173 |
174 |
175 | `; 176 | 177 | exports[`Reducing multiple props with a transform function 2`] = ` 178 |
181 |
184 | The Image 188 |
189 |
190 | `; 191 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect */ 2 | 3 | import x from '../src' 4 | import renderer from 'react-test-renderer' 5 | import { createElement } from 'react' 6 | 7 | function render (type, props) { 8 | return renderer.create( 9 | createElement(type, props) 10 | ).toJSON() 11 | } 12 | 13 | describe('Classes', () => { 14 | it('with no classes', () => { 15 | const Component = x({}) 16 | expect(render(Component)).toMatchSnapshot() 17 | }) 18 | 19 | it('with a single class', () => { 20 | const Component = x({ 21 | className: 'some-class' 22 | }) 23 | expect(render(Component)).toMatchSnapshot() 24 | }) 25 | 26 | it('with multiple classes', () => { 27 | const Component = x({ 28 | className: [ 29 | 'some-class', 'another-class' 30 | ] 31 | }) 32 | expect(render(Component)).toMatchSnapshot() 33 | }) 34 | 35 | it('with conditional classes', () => { 36 | const Component = x({ 37 | className: [ 38 | 'base-class', 39 | p => p.big ? 'big' : 'small' 40 | ] 41 | }) 42 | expect(render(Component)).toMatchSnapshot() 43 | expect(render(Component, { big: true })).toMatchSnapshot() 44 | }) 45 | }) 46 | 47 | describe('Attributes', () => { 48 | it('not rendered on a wrapper component', () => { 49 | const Component = x({ 50 | id: 'default-id', 51 | 'data-name': 'Default name' 52 | }) 53 | expect(render(Component)).toMatchSnapshot() 54 | }) 55 | 56 | it('rendered on an element', () => { 57 | const Component = x.div({ 58 | id: 'default-id', 59 | 'data-name': 'Default name' 60 | }) 61 | expect(render(Component)).toMatchSnapshot() 62 | }) 63 | 64 | it('rendered on an element with parent props', () => { 65 | const Component = x.div({ 66 | id: 'default-id', 67 | 'data-name': 'Default name' 68 | }) 69 | expect( 70 | render(Component, { 71 | id: 'custom-id', 72 | 'data-name': 'Custom name' 73 | }) 74 | ).toMatchSnapshot() 75 | }) 76 | 77 | it('passed from wrapper component to element', () => { 78 | const Component = x({ 79 | id: 'default-id', 80 | 'data-name': 'Default name' 81 | }) 82 | 83 | Component.children( 84 | x.span({ 85 | id: Component.id, 86 | 'data-attr': 'Other custom attr' 87 | }) 88 | ) 89 | 90 | expect(render(Component)).toMatchSnapshot() 91 | }) 92 | }) 93 | 94 | describe('Props', () => { 95 | it('selected from parent', () => { 96 | const Outer = x({ 97 | className: 'outer', 98 | imageUrl: 'the-image.jpg', 99 | caption: 'The Image' 100 | }) 101 | 102 | const Inner1 = x.img({ 103 | src: Outer.imageUrl, 104 | alt: Outer.caption 105 | }) 106 | 107 | const Inner2 = x.span({ 108 | children: Outer.caption 109 | }) 110 | 111 | Outer.children(Inner1, Inner2) 112 | 113 | expect(render(Outer)).toMatchSnapshot() 114 | }) 115 | 116 | it('selected from grandparent', () => { 117 | const Outer = x({ 118 | className: 'outer', 119 | imageUrl: 'the-image.jpg', 120 | caption: 'The Image' 121 | }) 122 | 123 | const Outer2 = x({ 124 | className: 'outer-2' 125 | }) 126 | 127 | const Inner1 = x.img({ 128 | src: Outer.imageUrl, 129 | alt: Outer.caption 130 | }) 131 | 132 | const Inner2 = x.span({ 133 | children: Outer.caption 134 | }) 135 | 136 | Outer.children( 137 | Outer2.children( 138 | Inner1, 139 | Inner2 140 | ) 141 | ) 142 | 143 | expect(render(Outer)).toMatchSnapshot() 144 | }) 145 | }) 146 | 147 | describe('Reducing', () => { 148 | it('a single prop with a transform function', () => { 149 | const Root = x({ 150 | className: 'root', 151 | imageUrl: 'the-image.jpg', 152 | caption: 'The Image' 153 | }) 154 | 155 | const Image = x.img({ 156 | src: x.from(Root.imageUrl, p => `http://example.com/${p.imageUrl}`), 157 | alt: Root.caption 158 | }) 159 | 160 | Root.children(Image) 161 | 162 | expect(render(Root)).toMatchSnapshot() 163 | }) 164 | 165 | it('multiple props with a transform function', () => { 166 | const Root = x({ 167 | className: 'root', 168 | imageUrl: 'the-image.jpg', 169 | caption: 'The Image', 170 | secure: false 171 | }) 172 | 173 | const Wrapper = x({ 174 | className: 'wrapper' 175 | }) 176 | 177 | const Image = x.img({ 178 | src: x.from(Root.imageUrl, Root.secure, p => ( 179 | `${p.secure ? 'https' : 'http'}://example.com/${p.imageUrl}` 180 | )), 181 | alt: Root.caption 182 | }) 183 | 184 | Root.children( 185 | Wrapper.children( 186 | Image 187 | ) 188 | ) 189 | 190 | expect(render(Root)).toMatchSnapshot() 191 | expect(render(Root, { secure: true })).toMatchSnapshot() 192 | }) 193 | }) 194 | 195 | describe('Conditional rendering', () => { 196 | it('using a child-function', () => { 197 | const Root = x({ 198 | className: 'root', 199 | imageUrl: '', 200 | caption: '' 201 | }) 202 | 203 | const Image = x.img({ 204 | src: Root.imageUrl, 205 | alt: Root.caption 206 | }) 207 | 208 | Root.children( 209 | p => p.imageUrl && Image 210 | ) 211 | 212 | expect(render(Root)).toMatchSnapshot() 213 | expect(render(Root, { imageUrl: 'the-image.jpg' })).toMatchSnapshot() 214 | }) 215 | }) 216 | 217 | describe('Default values', () => { 218 | it('with wrapper component', () => { 219 | const Root = x({ 220 | children: 'default text' 221 | }) 222 | 223 | expect(render(Root)).toMatchSnapshot() 224 | expect(render(Root, { children: 'custom text' })).toMatchSnapshot() 225 | }) 226 | 227 | it('with changed defaults', () => { 228 | const Root = x.div({ 229 | 'data-text': 'default text' 230 | }) 231 | 232 | Root['data-text']('default text 2') 233 | 234 | expect(render(Root)).toMatchSnapshot() 235 | expect(render(Root, { 'data-text': 'custom text' })).toMatchSnapshot() 236 | }) 237 | }) 238 | 239 | describe('Lists', () => { 240 | it('simple example', () => { 241 | const Root = x({ 242 | className: 'root', 243 | items: [] 244 | }) 245 | 246 | const Item = x({ 247 | className: 'item', 248 | imageUrl: '', 249 | caption: '' 250 | }) 251 | 252 | Item.children( 253 | x.img({ 254 | className: 'image', 255 | src: Item.imageUrl, 256 | alt: Item.caption 257 | }) 258 | ) 259 | 260 | Root.children( 261 | x.list(Root.items, Item) 262 | ) 263 | 264 | const items = [ 265 | { 266 | imageUrl: 'image1.jpg', 267 | caption: 'Image One' 268 | }, 269 | { 270 | imageUrl: 'image2.jpg', 271 | caption: 'Image Two' 272 | } 273 | ] 274 | 275 | expect(render(Root, { items })).toMatchSnapshot() 276 | }) 277 | }) 278 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drx", 3 | "version": "0.11.1", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prebuild": "rm -rf lib && mkdir lib", 8 | "build": "babel -d lib src", 9 | "lint": "standard", 10 | "prepublishOnly": "npm run build && npm run umd", 11 | "umd": "npm run umd:prep && npm run umd:build && npm run umd:minify", 12 | "umd:prep": "rm -rf dist && mkdir dist", 13 | "umd:build": "browserify --u react -t browserify-shim --standalone drx -o dist/drx.js lib/index.js", 14 | "umd:minify": "uglifyjs -o dist/drx.min.js dist/drx.js", 15 | "test": "jest" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/joshwnj/drx.git" 20 | }, 21 | "keywords": [], 22 | "author": "joshwnj", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/joshwnj/drx/issues" 26 | }, 27 | "homepage": "https://github.com/joshwnj/drx#readme", 28 | "peerDependencies": { 29 | "react": ">= 0.14.0 < 17.0.0-0" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.26.0", 33 | "babel-preset-es2015": "^6.24.1", 34 | "browserify": "^14.4.0", 35 | "browserify-shim": "^3.8.14", 36 | "jest": "^20.0.4", 37 | "react": "^15.6.1", 38 | "react-test-renderer": "^15.6.1", 39 | "standard": "^10.0.3", 40 | "uglify-js": "^3.0.28" 41 | }, 42 | "browserify-shim": { 43 | "react": "global:React" 44 | }, 45 | "files": [ 46 | "dist", 47 | "lib", 48 | "src" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { createElement, PureComponent, DOM } from 'react' 2 | 3 | const ensureArray = v => Array.isArray(v) ? v : [ v ] 4 | 5 | function createPropRef (component, key) { 6 | const def = component.__x 7 | 8 | // the prop ref is a setter function 9 | // with metadata about the prop & component class 10 | const PropRef = (...args) => { 11 | // special case: children and className can have multiple values 12 | def.props[key] = (key === 'children' || key === 'className') 13 | ? args 14 | : args[0] 15 | 16 | // record which keys have been changed 17 | def.changed[key] = true 18 | 19 | return component 20 | } 21 | PropRef.__x_ref = { key, def } 22 | return PropRef 23 | } 24 | 25 | function resolveProp (parent, ref) { 26 | const { key } = ref 27 | const last = parent.lastProps 28 | const deps = last._dependencies || {} 29 | 30 | return last[key] || deps[key] 31 | } 32 | 33 | function collectDependencies (content, parent, result = {}) { 34 | // no content: terminate with result 35 | if (!content) { return result } 36 | 37 | function collectFromDef (def) { 38 | const propDef = def.props 39 | Object.keys(propDef).forEach(k => { 40 | const p = propDef[k] 41 | 42 | const ref = p.__x_ref 43 | if (ref) { 44 | if (ref.def === parent.def) { 45 | result[ref.key] = p 46 | } 47 | return 48 | } 49 | 50 | const from = p.__x_from 51 | if (from) { 52 | from.forEach(p => { 53 | const ref = p.__x_ref 54 | if (ref) { 55 | if (ref.def === parent.def) { 56 | result[ref.key] = p 57 | } 58 | } 59 | }) 60 | } 61 | }) 62 | 63 | // recurse 64 | if (propDef.children) { 65 | collectDependencies(propDef.children, parent, result) 66 | } 67 | } 68 | 69 | ensureArray(content) 70 | .map(ch => ch.__x) 71 | .filter(Boolean) 72 | .forEach(collectFromDef) 73 | 74 | ensureArray(content) 75 | .map(ch => ch.__x_list) 76 | .filter(Boolean) 77 | .forEach(list => { 78 | const ref = list.ref.__x_ref 79 | if (ref.def === parent.def) { 80 | result[ref.key] = list.ref 81 | } 82 | }) 83 | 84 | return result 85 | } 86 | 87 | function resolveProps (parent, source, props) { 88 | // default: no props 89 | if (!source) { return {} } 90 | 91 | const newProps = {} 92 | Object.keys(source).forEach(k => { 93 | const p = source[k] 94 | 95 | // add 1 or more props to the signature, 96 | // and possibly run them through a reducer: 97 | const from = p.__x_from 98 | if (from) { 99 | const fromValues = {} 100 | from.forEach(f => { 101 | const ref = f.__x_ref 102 | if (ref) { 103 | fromValues[ref.key] = resolveProp(parent, ref) 104 | return 105 | } 106 | 107 | if (typeof f === 'function') { 108 | newProps[k] = f(Object.assign({}, props, fromValues)) 109 | return 110 | } 111 | 112 | console.warn('Unknown argument in drx.from():', f) 113 | }) 114 | 115 | return 116 | } 117 | 118 | const ref = p.__x_ref 119 | if (ref) { 120 | newProps[k] = resolveProp(parent, ref) 121 | return 122 | } 123 | 124 | // special case: we only want to use `props.className` and `props.children` from parent when opted in 125 | if (k === 'children' || k === 'className') { 126 | newProps[k] = p 127 | return 128 | } 129 | 130 | const t = typeof p 131 | switch (t) { 132 | case 'boolean': 133 | case 'string': 134 | case 'number': 135 | case 'function': 136 | newProps[k] = props[k] || p 137 | return 138 | 139 | default: 140 | console.warn('Unknown propref type:', k, p) 141 | } 142 | }) 143 | 144 | // delete undefined props 145 | Object.keys(newProps).forEach(k => { 146 | if (newProps[k] === undefined) { delete newProps[k] } 147 | }) 148 | 149 | return newProps 150 | } 151 | 152 | // declare a component by its props 153 | function create (def) { 154 | const propDef = def.props 155 | def.changed = {} 156 | 157 | class DrxComponent extends PureComponent { 158 | constructor (props) { 159 | super(props) 160 | this.def = def 161 | this.propDef = propDef 162 | } 163 | 164 | getChild (Component, props) { 165 | const def = Component.__x 166 | if (!def) { 167 | console.error('Not a valid DrxComponent', Component) 168 | return null 169 | } 170 | 171 | const propDef = def.props 172 | 173 | // find all props dependencies from sub-children 174 | // so we can make sure the props are passed down 175 | const dependencies = collectDependencies(propDef.children, this) 176 | const hasDependencies = Object.keys(dependencies).length > 0 177 | 178 | // select props for the child 179 | const childProps = resolveProps(this, propDef, props) 180 | 181 | if (hasDependencies) { 182 | const resolvedDeps = resolveProps(this, dependencies, props) 183 | Object.keys(resolvedDeps).forEach(k => { 184 | if (k in childProps) { 185 | childProps[k] = resolvedDeps[k] 186 | } 187 | }) 188 | 189 | childProps._dependencies = resolvedDeps 190 | } 191 | 192 | if (childProps.children) { 193 | childProps.children = ensureArray(childProps.children).reduce((acc, child) => { 194 | const list = child.__x_list 195 | if (!list) { 196 | acc.push(child) 197 | return acc 198 | } 199 | 200 | const items = resolveProp(this, list.ref.__x_ref) 201 | 202 | return acc.concat(items.map((item, i) => ( 203 | this.getChild(list.component, Object.assign({ key: i }, item)) 204 | ))) 205 | }, []) 206 | } 207 | 208 | // if this is an element, with no prop dependencies, 209 | // we can render the element directly rather than wrapping it 210 | // in a DrxComponent 211 | const needsWrapper = !def.elem || hasDependencies 212 | 213 | return needsWrapper 214 | ? createElement(Component, childProps) 215 | : createElement(def.type, childProps) 216 | } 217 | 218 | getChildren (props) { 219 | const { def } = this 220 | 221 | const resolveChild = (ch) => { 222 | if (!ch) { return null } 223 | 224 | if (ch.__x) { return this.getChild(ch, props) } 225 | 226 | if (ch.__x_ref) { 227 | return resolveProp(this, ch.__x_ref) 228 | } 229 | 230 | const list = ch.__x_list 231 | if (list) { 232 | const items = resolveProp(this, list.ref.__x_ref) 233 | 234 | return items.map((item, i) => ( 235 | this.getChild(list.component, Object.assign({ key: i }, item)) 236 | )) 237 | } 238 | 239 | // recurse 240 | if (typeof ch === 'function') { 241 | return resolveChild(ch(props)) 242 | } 243 | 244 | // default: return the child as-is 245 | return ch 246 | } 247 | 248 | // children have already been resolved 249 | if (props.children && !def.changed.children) { 250 | // TODO: handle __x_from 251 | 252 | return ensureArray(props.children) 253 | } 254 | 255 | const children = ensureArray(propDef.children).map(resolveChild) 256 | 257 | if (children.length === 1 && Array.isArray(children[0])) { 258 | return children[0] 259 | } else { 260 | return children || [ props.children ] 261 | } 262 | } 263 | 264 | mergeDefaultProps () { 265 | const { props, propDef } = this 266 | 267 | const newProps = Object.assign({}, props) 268 | 269 | // look at the component definition to get defaults or dependencies 270 | Object.keys(propDef).forEach(k => { 271 | if (newProps[k] === undefined) { 272 | newProps[k] = propDef[k] 273 | } 274 | }) 275 | 276 | const propReducer = (acc, value) => { 277 | const ref = value.__x_ref 278 | if (ref) { 279 | acc[ref.key] = newProps[ref.key] 280 | return acc 281 | } 282 | 283 | if (typeof value === 'function') { 284 | return value(Object.assign({}, newProps, acc)) 285 | } 286 | } 287 | 288 | // resolve x.from values 289 | Object.keys(newProps).forEach(k => { 290 | const value = newProps[k] 291 | const info = value.__x_from 292 | if (!info) { return } 293 | 294 | newProps[k] = info.reduce(propReducer, {}) 295 | }) 296 | 297 | // special case: className 298 | if (newProps.className) { 299 | const join = (val) => Array.isArray(val) ? val.join(' ') : val 300 | 301 | // resolve dynamic classnames 302 | newProps.className = ensureArray(newProps.className).map(c => { 303 | const info = c.__x_from 304 | if (info) { 305 | return join(info.reduce(propReducer, {})) 306 | } 307 | 308 | if (typeof c === 'function') { 309 | return join(c(newProps)) 310 | } 311 | 312 | return join(c) 313 | }) 314 | 315 | // create a string 316 | newProps.className = newProps.className.join(' ') 317 | } 318 | 319 | return newProps 320 | } 321 | 322 | render () { 323 | const propsWithDefaults = this.mergeDefaultProps() 324 | 325 | // render-tree stuff needs to go on the instance, not on the class 326 | this.lastProps = Object.assign({}, propsWithDefaults) 327 | 328 | // children get original props, not translated props 329 | const children = this.getChildren(propsWithDefaults) || [] 330 | 331 | if (!def.type) { 332 | const props = {} 333 | const keys = [ 'className', 'style' ] 334 | keys.forEach(k => { 335 | const v = propsWithDefaults[k] 336 | if (v === undefined || v === null || v === '') { return } 337 | props[k] = v 338 | }) 339 | return createElement('div', props, ...children) 340 | } 341 | 342 | // we don't want the `_dependencies` prop if it's an element 343 | if (def.elem) { 344 | delete propsWithDefaults._dependencies 345 | } 346 | 347 | return createElement(def.type, propsWithDefaults, ...children) 348 | } 349 | } 350 | 351 | const c = DrxComponent 352 | 353 | c.__x = def 354 | 355 | // expose the prop references 356 | Object.keys(def.props).forEach(k => { 357 | c[k] = createPropRef(c, k) 358 | }) 359 | 360 | const keys = ['children', 'className'] 361 | keys.forEach(k => { 362 | if (!c[k]) { 363 | c[k] = createPropRef(c, k) 364 | } 365 | }) 366 | 367 | return c 368 | } 369 | 370 | const x = (props) => create({ props }) 371 | 372 | x.from = (...refs) => ({ __x_from: refs }) 373 | 374 | x.list = (ref, component) => ({ 375 | __x_list: { ref, component } 376 | }) 377 | 378 | const types = Object.keys(DOM) 379 | types.forEach(type => { 380 | x[type] = (props) => create({ type, props, elem: true }) 381 | }) 382 | 383 | export default x 384 | -------------------------------------------------------------------------------- /src/render-styles.js: -------------------------------------------------------------------------------- 1 | module.exports = function renderStyles (self, props) { 2 | if (props.className) { return props.className } 3 | 4 | const styles = self.styles 5 | const classes = [] 6 | 7 | function groupAll (styles) { 8 | if (!Array.isArray(styles)) { 9 | classes.push(styles) 10 | return 11 | } 12 | 13 | styles.forEach(s => { 14 | (typeof s === 'function') ? groupAll(s(props)) : groupAll(s) 15 | }) 16 | } 17 | 18 | groupAll(styles) 19 | 20 | return classes.join(' ') 21 | } 22 | --------------------------------------------------------------------------------