53 | ```
54 |
55 |
56 | ## Dynamic CSS
57 |
58 | JSX++ will dynamically generate scoped CSS for your nodes.
59 |
60 | ```jsx
61 |
Hover me!
67 | ```
68 |
69 | Result:
70 |
71 | ```css
72 | [data-css-123] {
73 | color: red;
74 | }
75 | [data-css-123]:hover {
76 | color: blue;
77 | }
78 | ```
79 |
80 | ```html
81 |
Hover me!
82 | ```
83 |
84 |
85 | ## DOM Element Props
86 |
87 | Sets props on native DOM elements.
88 |
89 | ```jsx
90 |
91 | ```
92 |
93 | Result:
94 |
95 | ```html
96 |
foobar
97 | ```
98 |
99 |
100 | ## DOM Element Attributes
101 |
102 | Sets attributes of DOM elements.
103 |
104 | ```jsx
105 |
106 | ```
107 |
108 | Result:
109 |
110 | ```html
111 |
112 | ```
113 |
114 |
115 | ## Native DOM Events
116 |
117 | Add listeners to native DOM events.
118 |
119 | ```jsx
120 |
121 | ```
122 |
123 |
124 | ## Micro Life-cycles
125 |
126 | Add micro life-cycles to React DOM string elements.
127 |
128 | ```jsx
129 |
console.log('element attached: ', el, props)}
131 | $update={(el, props, oldProps) => console.log('element updated: ', el, props, oldProps)}
132 | $detach={(el, oldProps) => console.log('element detached: ', el, oldProps)}
133 | />
134 | ```
135 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jsx-plus-plus",
3 | "version": "0.1.0",
4 | "description": "Better JSX for busy developers",
5 | "main": "lib/patch.js",
6 | "scripts": {
7 | "start": "npm run storybook",
8 | "clean": "rimraf lib",
9 | "build": "babel src --out-dir lib",
10 | "test": "jest",
11 | "test:coverage": "jest --coverage",
12 | "storybook": "start-storybook -p 6006",
13 | "build-storybook": "build-storybook",
14 | "prettier": "prettier --write '**/*.js'",
15 | "precommit": "lint-staged"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/streamich/jsx-plus-plus.git"
20 | },
21 | "dependencies": {
22 | "nano-css": "^2.0.0",
23 | "classnames": "^2.2.5",
24 | "react-micro-lifecycles": "^1.0.0"
25 | },
26 | "devDependencies": {
27 | "@storybook/addon-actions": "9.0.3",
28 | "@storybook/addon-links": "9.0.3",
29 | "@storybook/addons": "7.6.17",
30 | "@storybook/react": "9.0.3",
31 | "babel-cli": "6.26.0",
32 | "babel-core": "6.26.3",
33 | "babel-polyfill": "6.26.0",
34 | "babel-preset-es2015": "6.24.1",
35 | "babel-preset-es2016": "6.24.1",
36 | "babel-preset-es2017": "6.24.1",
37 | "babel-preset-flow": "6.23.0",
38 | "babel-preset-stage-0": "6.24.1",
39 | "jest": "29.7.0",
40 | "jest-tap-reporter": "1.9.0",
41 | "prettier": "3.5.3",
42 | "react": "19.1.0",
43 | "react-dom": "19.1.0",
44 | "rimraf": "6.0.1",
45 | "lint-staged": "16.1.0",
46 | "mol-conventional-changelog": "2.0.0",
47 | "nano-css": "2.2.0"
48 | },
49 | "lint-staged": {
50 | "**/*.js": [
51 | "prettier --write",
52 | "git add"
53 | ]
54 | },
55 | "prettier": {
56 | "printWidth": 120,
57 | "tabWidth": 4,
58 | "useTabs": false,
59 | "semi": true,
60 | "singleQuote": true,
61 | "trailingComma": "es5",
62 | "bracketSpacing": false,
63 | "jsxBracketSameLine": false
64 | },
65 | "jest": {
66 | "transformIgnorePatterns": [],
67 | "testRegex": ".*/__tests__/.*\\.(test|spec)\\.(jsx?)$",
68 | "setupFiles": [
69 | "./src/__tests__/setup.js"
70 | ],
71 | "moduleFileExtensions": [
72 | "js",
73 | "jsx",
74 | "json"
75 | ],
76 | "reporters": [
77 | "jest-tap-reporter"
78 | ]
79 | },
80 | "babel": {
81 | "presets": [
82 | "es2015",
83 | "es2016",
84 | "es2017",
85 | "stage-0",
86 | "flow"
87 | ],
88 | "comments": false
89 | },
90 | "config": {
91 | "commitizen": {
92 | "path": "./node_modules/mol-conventional-changelog"
93 | }
94 | },
95 | "keywords": [
96 | "jsx",
97 | "jsxpp",
98 | "jsx-plus-plus",
99 | "jsx++",
100 | "better-jsx",
101 | "hyperscript",
102 | "react",
103 | "h",
104 | "attributes",
105 | "dom-props",
106 | "native-events",
107 | "css",
108 | "styles"
109 | ]
110 | }
111 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "automerge": true,
6 | "pinVersions": false,
7 | "major": {
8 | "automerge": false
9 | },
10 | "devDependencies": {
11 | "automerge": true,
12 | "pinVersions": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.js:
--------------------------------------------------------------------------------
1 | describe('works', () => {
2 | it('runs', () => {});
3 | });
--------------------------------------------------------------------------------
/src/__tests__/setup.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'production';
2 |
--------------------------------------------------------------------------------
/src/applyPlugins.js:
--------------------------------------------------------------------------------
1 | const applyPlugins = (h, plugins) => {
2 | return (...args) => {
3 | const type = args[0];
4 | const props = args[1];
5 |
6 | if (props && (typeof type === 'string')) {
7 | for (let i = 0; i < plugins.length; i++)
8 | args = plugins[i](args);
9 | }
10 |
11 | return h(...args);
12 | };
13 | };
14 |
15 | export default applyPlugins;
16 |
--------------------------------------------------------------------------------
/src/patch.js:
--------------------------------------------------------------------------------
1 | import createHyperscriptStable from 'react-micro-lifecycles/lib/createHyperscriptStable';
2 | import React from 'react';
3 | import applyPlugins from './applyPlugins';
4 | import classnames from './plugins/classnames';
5 | import dom from './plugins/dom';
6 | import attr from './plugins/attr';
7 | import prefixer from './plugins/prefixer';
8 | import css from './plugins/css';
9 | import on from './plugins/on';
10 |
11 | const plugins = [
12 | prefixer,
13 | classnames,
14 | dom,
15 | attr,
16 | css,
17 | on,
18 | ];
19 |
20 | var h = React.createElement;
21 |
22 | h = createHyperscriptStable(h, React.Component);
23 | h = applyPlugins(h, plugins);
24 |
25 | React.createElement = h;
26 |
--------------------------------------------------------------------------------
/src/plugins/attr.js:
--------------------------------------------------------------------------------
1 | const plugin = (args) => {
2 | const props = args[1];
3 |
4 | if (!props) return args;
5 |
6 | const {$attr} = props;
7 |
8 | if (!$attr) return args;
9 |
10 | delete props.$attr;
11 |
12 | if (process.env.NODE_ENV !== 'production') {
13 | if (typeof $attr !== 'object') {
14 | console.error(
15 | `JSX++ $attr plugin expected $attr prop to be an object, received "${typeof $attr}".`
16 | );
17 | }
18 | }
19 |
20 | const {$attach, $update, $detach} = props;
21 |
22 | props.$attach = (el, props) => {
23 | if ($attach) $attach(el, props);
24 |
25 | for (const prop in $attr)
26 | el.setAttribute(prop, $attr[prop]);
27 | };
28 |
29 | props.$update = (el, props, oldProps) => {
30 | if ($update) $update(el, props, oldProps);
31 |
32 | for (const prop in $attr)
33 | el.setAttribute(prop, $attr[prop]);
34 |
35 | const oldAtts = oldProps.$attr || {};
36 |
37 | for (const prop in oldAttrs) {
38 | if (!(prop in $attr)) {
39 | el.removeAttribute(prop);
40 | }
41 | }
42 | };
43 |
44 | props.$detach = (el, oldProps) => {
45 | if ($detach) $detach(el, oldProps);
46 |
47 | const oldAttrs = oldProps.$attr || {};
48 |
49 | for (const prop in oldAttrs) {
50 | el.removeAttribute(prop);
51 | }
52 | };
53 |
54 | return args;
55 | };
56 |
57 | export default plugin;
58 |
--------------------------------------------------------------------------------
/src/plugins/classnames.js:
--------------------------------------------------------------------------------
1 | import cx from 'classnames';
2 |
3 | const plugin = (args) => {
4 | const props = args[1];
5 |
6 | if (!props) return args;
7 |
8 | let className = props.className || props.class;
9 |
10 | delete props.class;
11 |
12 | if (className) {
13 | className = cx(className);
14 | props.className = className;
15 | }
16 |
17 | return args;
18 | };
19 |
20 | export default plugin;
21 |
--------------------------------------------------------------------------------
/src/plugins/css.js:
--------------------------------------------------------------------------------
1 | import {create} from 'nano-css';
2 | import {addon as addonRule} from 'nano-css/addon/rule';
3 | import {addon as addonPipe} from 'nano-css/addon/pipe';
4 |
5 | const nano = create({
6 | pfx: 'jsxpp-'
7 | });
8 |
9 | addonRule(nano);
10 | addonPipe(nano);
11 |
12 | const pipes = new WeakMap;
13 |
14 |
15 | const plugin = (args) => {
16 | const props = args[1];
17 |
18 | if (!props) return args;
19 |
20 | const {$css} = props;
21 |
22 | if (!$css) return args;
23 |
24 | delete props.$css;
25 |
26 | if (process.env.NODE_ENV !== 'production') {
27 | if (typeof $css !== 'object') {
28 | console.error(
29 | `JSX++ $css plugin expected $css prop to be an object, received "${typeof $css}".`
30 | );
31 | }
32 | }
33 |
34 | const {$attach, $update, $detach} = props;
35 |
36 | props.$attach = (el, props) => {
37 | if ($attach) $attach(el, props);
38 |
39 | const pipe = nano.pipe();
40 |
41 | pipes.set(el, pipe);
42 |
43 | const self = {};
44 | let added = false;
45 |
46 | for (const prop in $css) {
47 | const value = $css[prop];
48 |
49 | if (typeof value !== 'object') {
50 | added = true;
51 | self[prop] = value;
52 | }
53 | }
54 |
55 | if (added) {
56 | $css['&'] = Object.assign($css['&'] || {}, self);
57 | }
58 |
59 | pipe.css($css);
60 | el.setAttribute(pipe.attr, '');
61 | };
62 |
63 | props.$update = (el, props, oldProps) => {
64 | if ($update) $update(el, props, oldProps);
65 |
66 | let pipe = pipes.get(el);
67 |
68 | if (!pipe) {
69 | pipe = nano.pipe();
70 | pipes.set(el, pipe);
71 | el.setAttribute(pipe.attr, '');
72 | }
73 |
74 | const self = {};
75 | let added = false;
76 |
77 | for (const prop in $css) {
78 | const value = $css[prop];
79 |
80 | if (typeof value !== 'object') {
81 | added = true;
82 | self[prop] = value;
83 | }
84 | }
85 |
86 | if (added) {
87 | $css['&'] = Object.assign($css['&'] || {}, self);
88 | }
89 |
90 | pipe.css($css);
91 | };
92 |
93 | props.$detach = (el, oldProps) => {
94 | if ($detach) $detach(el, oldProps);
95 |
96 | const pipe = pipes.get(el);
97 |
98 | pipes.delete(el);
99 |
100 | if (pipe) {
101 | el.removeAttribute(pipe.attr);
102 | pipe.remove();
103 | }
104 | };
105 |
106 | return args;
107 | };
108 |
109 | export default plugin;
110 |
--------------------------------------------------------------------------------
/src/plugins/dom.js:
--------------------------------------------------------------------------------
1 | const plugin = (args) => {
2 | const props = args[1];
3 |
4 | if (!props) return args;
5 |
6 | const {$dom} = props;
7 |
8 | if (!$dom) return args;
9 |
10 | delete props.$dom;
11 |
12 | if (process.env.NODE_ENV !== 'production') {
13 | if (typeof $dom !== 'object') {
14 | console.error(
15 | `JSX++ $dom plugin expected $dom prop to be an object, received "${typeof $dom}".`
16 | );
17 | }
18 | }
19 |
20 | const {$attach, $update, $detach} = props;
21 |
22 | props.$attach = (el, props) => {
23 | if ($attach) $attach(el, props);
24 |
25 | for (const prop in $dom)
26 | el[prop] = $dom[prop];
27 | };
28 |
29 | props.$update = (el, props, oldProps) => {
30 | if ($update) $update(el, props, oldProps);
31 |
32 | for (const prop in $dom)
33 | el[prop] = $dom[prop];
34 |
35 | const oldDom = (oldProps || {}).$dom || {};
36 |
37 | for (const prop in oldProps) {
38 | if (!(prop in $dom)) {
39 | delete el[prop];
40 | }
41 | }
42 | };
43 |
44 | props.$detach = (el, oldProps) => {
45 | if ($detach) $detach(el, oldProps);
46 |
47 | const oldDom = (oldProps || {}).$dom || {};
48 |
49 | for (const prop in oldProps) {
50 | delete el[prop];
51 | }
52 | };
53 |
54 | return args;
55 | };
56 |
57 | export default plugin;
58 |
--------------------------------------------------------------------------------
/src/plugins/on.js:
--------------------------------------------------------------------------------
1 | const noop = () => {};
2 | const mapListeners = new WeakMap;
3 | const mapOn = new WeakMap;
4 |
5 | const plugin = (args) => {
6 | const props = args[1];
7 |
8 | if (!props) return args;
9 |
10 | const {$on} = props;
11 |
12 | if (!$on) return args;
13 |
14 | delete props.$on;
15 |
16 | if (process.env.NODE_ENV !== 'production') {
17 | if (typeof $on !== 'object') {
18 | console.error(
19 | `JSX++ $on plugin expected $on prop to be an object, received "${typeof $on}".`
20 | );
21 | }
22 | }
23 |
24 | const {$attach, $update, $detach} = props;
25 |
26 | props.$attach = (el, props) => {
27 | if ($attach) $attach(el, props);
28 |
29 | mapOn.set(el, $on);
30 |
31 | let listeners = mapListeners.get(el);
32 |
33 | if (!listeners) {
34 | listeners = {};
35 | mapListeners.set(el, listeners);
36 | }
37 |
38 | for (const prop in $on) {
39 | const listener = function () {
40 | const on = mapOn.get(el);
41 |
42 | (on[prop] || noop).apply(this, arguments);
43 | };
44 |
45 | listeners[prop] = listener;
46 | el.addEventListener(prop, listener);
47 | }
48 | };
49 |
50 | props.$update = (el, props, oldProps) => {
51 | if ($update) $update(el, props, oldProps);
52 |
53 | mapOn.set(el, $on);
54 |
55 | let listeners = mapListeners.get(el);
56 |
57 | if (!listeners) {
58 | listeners = {};
59 | mapListeners.set(el, listeners);
60 | }
61 |
62 | for (const prop in $on) {
63 | if (!(prop in listeners)) {
64 | const listener = function () {
65 | const on = mapOn.get(el);
66 |
67 | (on[prop] || noop).apply(this, arguments);
68 | };
69 |
70 | listeners[prop] = listener;
71 | el.addEventListener(prop, listener);
72 | }
73 | }
74 |
75 | for (const prop in listeners) {
76 | if (!(prop in $on)) {
77 | el.removeEventListener(prop, listeners[prop]);
78 | }
79 | }
80 | };
81 |
82 | props.$detach = (el, oldProps) => {
83 | if ($detach) $detach(el, props);
84 |
85 | const listeners = mapListeners.get(el) || {};
86 |
87 | mapListeners.delete(el);
88 |
89 | for (const prop in listeners) {
90 | el.removeEventListener(prop, listeners[prop]);
91 | }
92 | };
93 |
94 | return args;
95 | };
96 |
97 | export default plugin;
98 |
--------------------------------------------------------------------------------
/src/plugins/prefixer.js:
--------------------------------------------------------------------------------
1 | import Prefixer from 'inline-style-prefixer'
2 |
3 | const prefixer = new Prefixer();
4 |
5 | const plugin = (args) => {
6 | const props = args[1];
7 |
8 | if (props && props.style) {
9 | props.style = prefixer.prefix(props.style);
10 | }
11 |
12 | return args;
13 | };
14 |
15 | export default plugin;
16 |
--------------------------------------------------------------------------------