├── .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 | [](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 |

106 |
107 |
110 |

115 |
116 |
117 | `;
118 |
119 | exports[`Props selected from grandparent 1`] = `
120 |
123 |
126 |

130 |
131 | The Image
132 |
133 |
134 |
135 | `;
136 |
137 | exports[`Props selected from parent 1`] = `
138 |
141 |

145 |
146 | The Image
147 |
148 |
149 | `;
150 |
151 | exports[`Reducing a single prop with a transform function 1`] = `
152 |
155 |

159 |
160 | `;
161 |
162 | exports[`Reducing multiple props with a transform function 1`] = `
163 |
166 |
169 |

173 |
174 |
175 | `;
176 |
177 | exports[`Reducing multiple props with a transform function 2`] = `
178 |
181 |
184 |

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 |
--------------------------------------------------------------------------------