No-JSX
A pure function based interface for creating React and React Native components without JSX tags.
If you love React but don't quite like the embeded HTML tags this library may be what you are looking for. Construct your components with code only in a clean, declarative way.
const myView () =>
div.app(
div.header(
img({src: logo, alt:'logo'}),
h2('Welcome to NJSX')
)
)()
Table of Content
Installation
NJSX is available on npm, just pick between the React and React Native flavours and add it to your project's dependencies.
For React Projects:
npm install njsx-react --save
For React Native Projects:
npm install njsx-react-native --save
Usage
NJSX is super easy to use: It's all about Builder Functions.
You can use Builders to to cleanly instantiate React and React Native elements, or further refine your component configuration just by applying them.
Getting a Builder
Default Component Builders
NJSX provides Builder Functions for all the default React and React Native components. Just import whatever element you need from the react
or react-native
modules and you are ready to go:
import {div, p} from 'njsx-react'
import {View, Text} from 'njsx-react-native'
Third-Party Component Builders
NJSX is not just for default components! You can get a builder for any component just by wrapping it with the njsx
adapter.
import njsx from 'njsx'
import {SomeThirdPartyComponent} from 'someLibrary'
const SomeFunctionalComponent = (props) => ...
class SomeStatefulComponent extends React.Component {
render() { ... }
}
const someComponent = njsx(SomeComponent)
const someFunctionalComponent = njsx(SomeFunctionalComponent)
const someStatefulComponent = njsx(SomeFunctionalComponent)
const aDivBuilder = njsx('div')
Creating Elements
Each NJSX builder, once applied with no arguments, will return a ReactElement just as if you had used the component inside a JSX tag:
import {div} from 'njsx-react'
<div></div>
div()
This means that JSX and NJSX elements are completely interchangeable. You can use components created from builders as children for JSX's tags, or refine a builder with tag shaped children, so you can try NJSX on any react based project and integrate it gradually.
Refining Builders
Of course, an empty element is not that useful, so how do you customize it?
When a Builder is applied with one or more arguments, these will be used to configure the building component. Refining a Builder this way returns another Builder, so you can keep refining your component any number of times.
import {p} from 'njsx-react'
p('some text')
p('some', ' ', 'text')
p('some')(' ')('text')
p(['some', ' ', 'text'])
<p>some text</p>
It's important to note that refining a builder causes no side effects or state changes at all. This means you can safely reuse Builders, or partially refine one and pass it forward.
Builder Arguments
Builders will get refined in a different way, depending on what arguments you apply them with:
-
Basic Objects
are treated as Component Properties. Refining a builder with a second set of properties will result in the merge of both, favoring the later in case of repetition.
img({src: path, onClick: cb})
img({src: path}, {onClick: cb})
img({src: thisWillBeLost, onClick: cb})({src: path})
<img src={path} onClick:{cb}></img>
-
Strings
, Numbers
, React Elements
and even other Builders
will become Component Children.
div(
div('the answer is ', 42)
)
<div><div>the answer is 42</div></div>
Notice that, since Builders can be children too, most of the time you won't be needing to apply them with no arguments to instantiate elements.
-
null
, undefined
and Booleans
will be ignored. This allows for a clean way to conditionally set properties and children using &&
and ||
.
div(null)
div(undefined)
div(false && "this won't show")
<div/>
-
Arrays
of any valid argument will be handled as a sequence of refinements.
const guards = ['Nobby', 'Colon', 'Carrot']
ul(guards.map(guard => li(guard)))
ul(guards.map(li))
<ul>{guards.map(guard => <li>{guard}</li>)}</ul>
-
Finally, you can also pass a Refinement Function
, which should take the previous Component Properties (including the children
field) and return the next one.
const myRefinement = (src, text) => (prev) =>
{...prev, {src, children: text} }
img(myRefinement(foo, bar))
<img src={foo}>bar</img>
To wrap it all, any unsuported argument application will raise a TypeError
.
Dynamic Selectors
You can also refine a Builder by accessing any keyword as if it was a property. A common use for this is to treat the keyword as a className
, so you can add classes to components by just naming them:
p.highlighted.small("Nice!")
p['highlighted']['small']("Nice!")
p['highlighted small']("Nice!")
p("Nice!").highlighted['.small']
<p className="highlighted small">Nice!</p>
Treating these selectors as class names is the default behavior of NJSX, but you can change it to whatever you want by changing the NJSXConfig. dynamicSelectorHandler
setting:
import { NJSXConfig } from 'njsx'
NJSXConfig.dynamicSelectorHandler = (key: string) => key
div.foo.bar
<div>foobar</div>
NJSXConfig.dynamicSelectorHandler = (id: string) => (prev) =>
{...prev, id}
div.baz
<div id="baz"/>
NJSXConfig.dynamicSelectorHandler = undefined
Notice that this feature can only be used on environments that support ES6's Proxy so, sadly, it's not available on React-Native projects.
Argument Transformation
You don't like the way arguments are being handled? No problem! You can customize the way NJSX's Builders interpret arguments to fine tune it to your needs.
The NJSXConfig
object can be used to specify Argument Transformations, which are just functions that take each argument and return whatever you want that argument to be. These functions are automatically called each time a Builder is applied.
import { NJSXConfig } from 'njsx'
import {p} from 'njsx-react'
const translations = {
"this should be translated": "ook"
}
NJSXConfig.argumentTransformations.push( arg =>
typeof arg === 'string' && arg.startsWith('!')
? translations[arg]
: arg
)
p("!this should be translated")
<p>ook</p>
Please take into account that all transformations are reduced on every argument, so don't overdo it and mind the order.
NJSX comes with some of these transformations set up by default:
-
In React projects, Strings starting with a dot will be interpreted as a classNames:
div('.foo .bar')(
'Some content'
)
<div className="foo bar">Some content</div>
-
In React-Native projects, StyleSheet arguments are interpreted as styles (Just import StyleSheet
from njsx-react-native
instead of react-native
).
import {StyleSheet, View, Text} from 'njsx-react-native'
StyleSheet.create({
container: { }
description: { }
})
View(styles.container)(
Text(style.description)("These are styled!")
)
If you rather all your arguments to just be interpreted as they are, you can disable this feature by setting the NJSXConfig.argumentTransformations
to an empty array.
Working with older versions
If you are working with an older release this documentation might not be of any use to you. We follow the semantic versioning standard so any difference on the Major version will probably imply some incompatibilities. Please refer to your version's branch README file.
Contributions
Please report any bugs, requests or ideas on the issues section of this repository and we will try to see to it as soon as possible.
Pull requests are always welcome! Just try to keep them small and clean.
License
This code is open source software licensed under the ISC License by The Uqbar Foundation. Feel free to use it accordingly.