diff --git a/README.md b/README.md index f39c21d..e8e61c2 100644 --- a/README.md +++ b/README.md @@ -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 )() @@ -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 +

hello!

+``` + + +### 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! @@ -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 diff --git a/package.json b/package.json index bd4501d..ff2c7b2 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/njsx.js b/src/njsx.js index 6e172b4..3e8b0a7 100644 --- a/src/njsx.js +++ b/src/njsx.js @@ -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) @@ -20,5 +24,5 @@ export default function njsx(type, props={}, children=[]) { component.isNJSXComponent = true - return component + return njsx.dynamicSelectorHandler ? asDynamic(component) : component } \ No newline at end of file diff --git a/src/react.js b/src/react.js index 04fd0db..47d5fe2 100644 --- a/src/react.js +++ b/src/react.js @@ -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') diff --git a/src/rules.js b/src/rules.js index 9d6802a..d1cba90 100644 --- a/src/rules.js +++ b/src/rules.js @@ -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 } + } + } \ No newline at end of file diff --git a/test/njsx.test.js b/test/njsx.test.js index 1074e89..c895260 100644 --- a/test/njsx.test.js +++ b/test/njsx.test.js @@ -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() + expect(element).to.deep.equal() }) 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() + expect(element).to.deep.equal() }) it('should be refinable by passing a string representing content', () => { @@ -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')(,)() + const element = njsx('foo')(,)() - expect(element).to.deep.equal() + expect(element).to.deep.equal() }) 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() + expect(element).to.deep.equal() }) 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() + expect(element).to.deep.equal() }) it('should ignore null arguments', () => { @@ -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() + }) + + 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() + }) + + 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) + }) }) }) \ No newline at end of file