Skip to content

Commit

Permalink
v2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
nscarcella committed Mar 22, 2017
1 parent 9250000 commit 79add83
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 46 deletions.
52 changes: 38 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,19 @@ Each argument is process by a set of configurable rules to decide what change it

- **Hashes** will become component's properties.

```jsx
```js
img({src: somePath, onClick: someCallback})
```

Applying a builder with a second hash will result in the new properties merging with the old, keeping the later in case of repetition.

```jsx
```js
img({src: thisWillBeLost, onClick: thisWillRemain})({src: thisWillOverrideThePreviousPath})
```

- **Strings**, **Numbers**, **Booleans**, **React Components** and even other **NJSX Builders** will become childrens.

```jsx
```js
div(
div('the answer is ', 42) // <- No need for building
)()
Expand All @@ -100,7 +100,18 @@ Each argument is process by a set of configurable rules to decide what change it
Any unsuported argument application will raise a `TypeError`.


## Advanced Customization
If the running environment [supports ES6's Proxy](https://kangax.github.io/compat-table/es6/#test-Proxy), component's property access can be used to further refine an existing component. By default, in *react* projects, this is set to make each property access yield a new component with the property name as a class:

```jsx
//all these yield the same component
p.highlighted.small("hello!")
p['highlighted small']("hello!")
p("hello!").highlighted.small
<p className="highlighted small">hello!</p>
```


### Advanced Customization

You don't like the way arguments are being handled? No problem! You can customize the rules *NJSX* uses for interpreting arguments to fine tune it to your needs. Add or remove supported argument applications, change the way they are processed or throw all away and start from scratch!
Expand All @@ -109,27 +120,40 @@ import njsx from 'njsx/njsx' // This is NJSX core. Booth React and ReactNative
import Rules from 'njsx/rules' // This module exports some common rule examples.
```
Each *rule* is just an array with two functions. The first one tells if the rule can handle an argument, the other one extracts any property or child from it.
Each *rule* is just an object with two methods:
- `appliesTo(argument)`: Tells if the rule can handle an argument applied to a component builder.
- `apply(argument, {props,children})`: Takes the argument applied to a component builder and the curent state of the builder (denoted by an object containing a `props` hash and a `children` array) and returns the next builder state.
```js
Rules.STRING_AS_CHILD = [
arg => typeof arg === 'string', // This rule only applies to arguments of type string.
arg => [ {}, [arg] ] // Applying this rule adds the string to the children array (but it doesn't change the properties).
]
Rules.STRING_AS_CHILD = {
// This rule only applies to arguments of type string.
appliesTo(arg) { return typeof arg === 'string' },
// Applying this rule adds the string to the children array (but it doesn't change the properties).
apply(arg, {props, children}) { return {props, children: [...children, arg] }}
}
```

So you could easily create, for example, a rule that can handle anything that defines a `toString()` function and adds its result as a child.

```js
const strigableAsChild = [
arg => arg.toString && typeof(arg.toString) === 'function',
arg => [ {}, [arg.toString()] ]
]
const strigableAsChild = {
appliesTo(arg) { return arg.toString && typeof(arg.toString) === 'function' },
apply(arg, {props, children}) { return {props, children: [...children, arg.toString()] }}
}

njsx.rules.push(stringableAsChild) // From now on, all builders will support this rule.
```

Take into account that **only one** rule will be applied to each argument, and each rule will be tried in same order as it appears in the `njsx.rules` array, so be carefull to leave the more generic rules at the bottom.
Take into account that **only one** rule will be applied to each argument, and each rule will be tested for applicability in the same order as it appears in the `njsx.rules` array, so be carefull to leave the more generic rules at the bottom.

Finally, if you want to change how property access is handled by the builders, you can do so by setting the `njsx.dynamicSelectorHandler` property to a function that takes the accessed property name and the current builder state and returns the next state. For example, if you want the accesses to be treated as class names in a *react-native* project, you can do so by adding this line:

```js
njsx.dynamicSelectorHandler = Rules.STRING_AS_CLASS.apply
```

You can also set this property to a *falsy* value to disable the whole property access behavior for builders.


## Contributions
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "njsx",
"version": "1.0.0",
"version": "2.0.0",
"description": "No-JSX: A customizable interface for creating React and React-Native components without JSX syntax.",
"repository": "uqbar-project/njsx",
"scripts": {
"test": "mocha --compilers js:babel-register",
"construct": "rm -r lib && babel --presets react-native src -d lib && cp ./package.json ./lib/ && cd lib && npm pack"
"release": "rm -r lib && babel --presets react-native src -d lib && cp -t ./lib/ ./package.json ./README.md && cd lib && npm publish ./"
},
"dependencies": {
"react": "^15.4.2"
Expand Down
22 changes: 13 additions & 9 deletions src/njsx.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ import {createElement} from 'react'

const {isArray} = Array

const flatten = (array) => array.reduce((acum, elem) => [...acum, ...isArray(elem) ? elem : [elem]], [])
const asDynamic = (component) => !Proxy ? component : new Proxy(component, {
get(target, name) {
const {type, props: {children = [], ...props}} = component()
const next = njsx.dynamicSelectorHandler(name, {props, children})
return asDynamic( njsx(type, next.props, next.children) )
}
})

export default function njsx(type, props={}, children=[]) {
const component = (...args) => {
const [finalProps, finalChildren] = args
.reduce((acum, arg) => [...acum, ...isArray(arg) ? arg : [arg]], [])
.reduce(([oldProps, oldChildren], arg) => {
const [,materialize] = njsx.rules.find(([appliesTo,]) => appliesTo(arg))
const [newProps, newChildren] = materialize(arg)

return [{...oldProps, ...newProps}, [...oldChildren, ...newChildren]]
}, [props,children])
const {props: finalProps, children: finalChildren} = flatten(args).reduce((previous, arg) =>
njsx.rules.find(rule => rule.appliesTo(arg)).apply(arg, previous)
, {props,children})

return args.length === 0
? createElement(type, finalProps, ...finalChildren)
Expand All @@ -20,5 +24,5 @@ export default function njsx(type, props={}, children=[]) {

component.isNJSXComponent = true

return component
return njsx.dynamicSelectorHandler ? asDynamic(component) : component
}
2 changes: 2 additions & 0 deletions src/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const DEFAULT_REACT_RULES = [
]

njsx.rules = DEFAULT_REACT_RULES
njsx.dynamicSelectorHandler = RULES.STRING_AS_CLASS.apply


export const a = njsx('a')
export const abbr = njsx('abbr')
Expand Down
57 changes: 46 additions & 11 deletions src/rules.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,48 @@
export const Rule = (appliesTo) => (materialize) => [appliesTo, materialize]

export default {
HASH_AS_ATRIBUTES: Rule(a => typeof a === 'object')(a => [ a, [] ]),
STRING_AS_CLASS: Rule(a => typeof a === 'string' && a.trim().startsWith('.'))(a => [ {className: a.split('.').join(' ').trim()}, [] ]),
STRING_AS_CHILD: Rule(a => typeof a === 'string')(a => [ {}, [a] ]),
NUMBER_AS_CHILD: Rule(a => typeof a === 'number')(a => [ {}, [a.toString()] ]),
BOOLEAN_AS_CHILD: Rule(a => typeof a === 'boolean')(a => [ {}, [a.toString()] ]),
NJSX_COMPONENT_AS_CHILD: Rule(a => a.isNJSXComponent)(a => [ {}, [a()] ]),
REACT_COMPONENT_AS_CHILD: Rule(a => typeof a === 'object' && a.props)(a => [ {}, [a] ]),
IGNORE_NULL: Rule(a => a === null)(a => [ {}, [] ]),
IGNORE_UNDEFINED: Rule(a => a === undefined)(a => [ {}, [] ])

HASH_AS_ATRIBUTES: {
appliesTo(arg) { return typeof arg === 'object' },
apply(arg, {props, children}) { return {props: {...props, ...arg}, children} }
},

STRING_AS_CLASS: {
appliesTo(arg) { return typeof arg === 'string' && arg.trim().startsWith('.') },
apply(arg, {props: {className = '', otherProps}, children}) { return {props: {...otherProps, className: [...className.split(' '), ...arg.split('.')].map(c=> c.trim()).filter(String).join(' ')} , children } }
},

STRING_AS_CHILD: {
appliesTo(arg) { return typeof arg === 'string' },
apply(arg, {props, children}) { return {props, children: [...children, arg] }}
},

NUMBER_AS_CHILD: {
appliesTo(arg) { return typeof arg === 'number' },
apply(arg, {props, children}) { return { props, children: [...children, arg.toString()] }}
},

BOOLEAN_AS_CHILD: {
appliesTo(arg) { return typeof arg === 'boolean' },
apply(arg, {props, children}) { return {props, children: [...children, arg.toString()] }}
},

NJSX_COMPONENT_AS_CHILD: {
appliesTo(arg) { return arg.isNJSXComponent },
apply(arg, {props, children}) { return {props, children: [...children, arg()] }}
},

REACT_COMPONENT_AS_CHILD: {
appliesTo(arg) { return typeof arg === 'object' && arg.props },
apply(arg, {props, children}) { return {props, children: [...children, arg] }}
},

IGNORE_NULL: {
appliesTo(arg) { return arg === null },
apply(arg, previous) { return previous }
},

IGNORE_UNDEFINED: {
appliesTo(arg) { return arg === undefined },
apply(arg, previous) { return previous }
}

}
41 changes: 31 additions & 10 deletions test/njsx.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ describe('NJSX', () => {

it('should be refinable by passing attributes as a hash', () => {
njsx.rules = [Rules.HASH_AS_ATRIBUTES]
const element = njsx('foo')({bar: 'meh'})()
const element = njsx('foo')({bar: 'baz'})()

expect(element).to.deep.equal(<foo bar='meh'/>)
expect(element).to.deep.equal(<foo bar='baz'/>)
})

it('should be refinable by passing a string representing a class name', () => {
njsx.rules = [Rules.STRING_AS_CLASS]
const element = njsx('foo')('.bar.meh')()
const element = njsx('foo')('.bar.baz')('.qux')()

expect(element).to.deep.equal(<foo className="bar meh"/>)
expect(element).to.deep.equal(<foo className="bar baz qux"/>)
})

it('should be refinable by passing a string representing content', () => {
Expand All @@ -65,23 +65,23 @@ describe('NJSX', () => {

it('should be refinable by passing other React elements as children', () => {
njsx.rules = [Rules.REACT_COMPONENT_AS_CHILD]
const element = njsx('foo')(<bar/>,<meh/>)()
const element = njsx('foo')(<bar/>,<baz/>)()

expect(element).to.deep.equal(<foo><bar/><meh/></foo>)
expect(element).to.deep.equal(<foo><bar/><baz/></foo>)
})

it('should be refinable by passing other NJSX elements as children', () => {
njsx.rules = [Rules.NJSX_COMPONENT_AS_CHILD]
const element = njsx('foo')(njsx('bar'), njsx('meh'))()
const element = njsx('foo')(njsx('bar'), njsx('baz'))()

expect(element).to.deep.equal(<foo><bar/><meh/></foo>)
expect(element).to.deep.equal(<foo><bar/><baz/></foo>)
})

it('should be refinable by passing an array of children', () => {
njsx.rules = [Rules.NJSX_COMPONENT_AS_CHILD]
const element = njsx('foo')([njsx('bar'), njsx('meh')])()
const element = njsx('foo')([njsx('bar'), njsx('baz')])()

expect(element).to.deep.equal(<foo><bar/><meh/></foo>)
expect(element).to.deep.equal(<foo><bar/><baz/></foo>)
})

it('should ignore null arguments', () => {
Expand All @@ -103,6 +103,27 @@ describe('NJSX', () => {

expect(() => element((invalid) => 'argument')).to.throw(TypeError)
})

it('should be refinable by dynamic messages if a handler is defined', () => {
njsx.dynamicSelectorHandler = Rules.STRING_AS_CLASS.apply
const element = njsx('foo').bar.baz.qux()

expect(element).to.deep.equal(<foo className="bar baz qux"/>)
})

it('should be refinable by property key accessing if a handler is defined', () => {
njsx.dynamicSelectorHandler = Rules.STRING_AS_CLASS.apply
const element = njsx('foo')['.bar']['baz qux']()

expect(element).to.deep.equal(<foo className="bar baz qux"/>)
})

it('should not be refinable by dynamic messages if a handler is not defined', () => {
njsx.dynamicSelectorHandler = undefined
const element = njsx('foo').bar

expect(element).to.deep.equal(undefined)
})
})

})

0 comments on commit 79add83

Please sign in to comment.