├── .eslintignore ├── tests ├── .eslintrc └── index-test.js ├── .gitignore ├── nwb.config.js ├── .travis.yml ├── demo └── src │ ├── index.html │ ├── index.js │ └── web-component.js ├── .eslintrc.js ├── CONTRIBUTING.md ├── package.json ├── src └── index.js ├── LICENSE.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | .npmrc 9 | .idea 10 | yarn.lock 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: { 6 | global: 'JSX_CUSTOM_ELEMENTS', 7 | externals: { 8 | react: 'React' 9 | } 10 | } 11 | }, 12 | webpack: { 13 | html: { 14 | template: './demo/src/index.html' 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 8 6 | 7 | before_install: 8 | - npm install codecov.io coveralls 9 | 10 | after_success: 11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | jsx-native-events 0.0.2 Demo 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "standard", 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 2018, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "react" 20 | ], 21 | "rules": { 22 | } 23 | }; -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx nativeEvents */ 2 | import React, { useState } from 'react' 3 | import { render } from 'react-dom' 4 | 5 | import nativeEvents from '../../src' 6 | 7 | import './web-component' 8 | 9 | export default function SomeComponent (props) { 10 | const [ name, setName ] = useState('') 11 | 12 | return
13 |

My name is {name}

14 | 15 | setName(e.detail) } 17 | > 18 |
19 | } 20 | 21 | render(, document.querySelector('#demo')) 22 | -------------------------------------------------------------------------------- /demo/src/web-component.js: -------------------------------------------------------------------------------- 1 | export class WebComponent extends HTMLElement { 2 | constructor () { 3 | super() 4 | this.attachShadow({ mode: 'open' }) 5 | } 6 | 7 | connectedCallback () { 8 | this.shadowRoot.innerHTML = ` 9 | 10 | ` 11 | this.shadowRoot.querySelector('input').addEventListener('input', e => { 12 | const customEvent = new CustomEvent('custom-event', { 13 | detail: e.target.value 14 | }) 15 | 16 | this.dispatchEvent(customEvent) 17 | }) 18 | } 19 | } 20 | 21 | if (!customElements.get('web-component')) { 22 | customElements.define('web-component', WebComponent) 23 | } 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= 6 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the component's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /tests/index-test.js: -------------------------------------------------------------------------------- 1 | /** @jsx nativeEvents */ 2 | import expect from 'expect' 3 | import React from 'react' 4 | import { unmountComponentAtNode } from 'react-dom' 5 | import { render, fireEvent } from '@testing-library/react' 6 | 7 | import nativeEvents from 'src/' 8 | 9 | function callback (value) { return value } 10 | const mocks = { callback } 11 | 12 | export default function Component () { 13 | function dispatch ({ target, value }) { 14 | target.dispatchEvent(new CustomEvent('some-event', { 15 | detail: value 16 | })) 17 | } 18 | 19 | return
20 | 21 |
22 | } 23 | 24 | describe('The nativeEvents pragma', () => { 25 | let node 26 | 27 | beforeEach(() => { 28 | expect.spyOn(mocks, 'callback') 29 | node = document.createElement('div') 30 | }) 31 | 32 | afterEach(() => { 33 | unmountComponentAtNode(node) 34 | }) 35 | 36 | it('displays a welcome message', () => { 37 | const { baseElement } = render() 38 | 39 | const input = baseElement.querySelector('input') 40 | 41 | input.value = 'abc123' 42 | fireEvent.change(input) 43 | 44 | expect(mocks.callback).toHaveBeenCalled() 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx-native-events", 3 | "version": "2.0.0", 4 | "description": "Add native event handling to React's JSX to support custom event types", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "css", 9 | "es", 10 | "lib", 11 | "umd" 12 | ], 13 | "scripts": { 14 | "build": "nwb build-react-component", 15 | "clean": "nwb clean-module && nwb clean-demo", 16 | "prepublishOnly": "npm run build", 17 | "start": "nwb serve-react-demo", 18 | "test": "nwb test-react", 19 | "test:coverage": "nwb test-react --coverage", 20 | "test:watch": "nwb test-react --server", 21 | "release": "standard-version", 22 | "postrelease": "git push --follow-tags origin master; npm publish" 23 | }, 24 | "dependencies": {}, 25 | "peerDependencies": { 26 | "react": "16.x" 27 | }, 28 | "devDependencies": { 29 | "@testing-library/react": "^8.0.1", 30 | "eslint": "^5.16.0", 31 | "eslint-config-standard": "^12.0.0", 32 | "eslint-plugin-import": "^2.17.3", 33 | "eslint-plugin-node": "^9.1.0", 34 | "eslint-plugin-promise": "^4.1.1", 35 | "eslint-plugin-react": "^7.13.0", 36 | "eslint-plugin-standard": "^4.0.0", 37 | "nwb": "^0.25.2", 38 | "react": "^16.8.6", 39 | "react-dom": "^16.8.6", 40 | "standard-version": "^8.0.1" 41 | }, 42 | "author": "Caleb D. Williams ", 43 | "license": "MIT", 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/calebdwilliams/jsx-native-events.git" 47 | }, 48 | "keywords": [ 49 | "react-component", 50 | "jsx", 51 | "dom-events", 52 | "events", 53 | "dom", 54 | "react", 55 | "web components", 56 | "webcomponents", 57 | "custom elements", 58 | "customElements", 59 | "shadowDOM" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | /** 4 | * Convert a string from camelCase to kebab-case 5 | * @param {string} string - The base string (ostensibly camelCase) 6 | * @return { string } - A kebab-case string 7 | */ 8 | const toKebabCase = string => string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase() 9 | 10 | /** @type {Symbol} - Used to save reference to active listeners */ 11 | const listeners = Symbol('jsx-native-events/event-listeners') 12 | 13 | const eventPattern = /^onEvent/ 14 | 15 | export default function jsx (type, props, ...children) { 16 | const newProps = { ...props } 17 | if (typeof type === 'string') { 18 | newProps.ref = (element) => { 19 | // merge existing ref prop 20 | if (props && props.ref) { 21 | if (typeof props.ref === 'function') { 22 | props.ref(element) 23 | } else if (typeof props.ref === 'object') { 24 | props.ref.current = element 25 | } 26 | } 27 | 28 | if (element) { 29 | if (props) { 30 | const keys = Object.keys(props) 31 | /** Get all keys that have the `onEvent` prefix */ 32 | keys 33 | .filter(key => key.match(eventPattern)) 34 | .map(key => 35 | ({ 36 | key, 37 | eventName: toKebabCase( 38 | key.replace('onEvent', '') 39 | ).replace('-', '') 40 | }) 41 | ) 42 | .map(({ eventName, key }) => { 43 | /** Add the listeners Map if not present */ 44 | if (!element[listeners]) { 45 | element[listeners] = new Map() 46 | } 47 | 48 | /** If the listener hasn't be attached, attach it */ 49 | if (!element[listeners].has(eventName)) { 50 | element.addEventListener(eventName, props[key]) 51 | /** Save a reference to avoid listening to the same value twice */ 52 | element[listeners].set(eventName, props[key]) 53 | } 54 | }) 55 | } 56 | } 57 | } 58 | } 59 | 60 | return React.createElement.apply(null, [type, newProps, ...children]) 61 | } 62 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Caleb D. Williams 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | Uses some code from nwb: 21 | 22 | MIT License 23 | 24 | Copyright (c) 2015, Jonny Buchanan 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining a copy of 27 | this software and associated documentation files (the "Software"), to deal in 28 | the Software without restriction, including without limitation the rights to 29 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 30 | the Software, and to permit persons to whom the Software is furnished to do so, 31 | subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in all 34 | copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 37 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 38 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 39 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 40 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 41 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [2.0.0](https://github.com/calebdwilliams/jsx-native-events/compare/v0.0.2...v2.0.0) (2020-10-04) 6 | 7 | 8 | ### ⚠ BREAKING CHANGES 9 | 10 | * Initial full release 11 | 12 | ### Features 13 | 14 | * Add tests ([9d2360b](https://github.com/calebdwilliams/jsx-native-events/commit/9d2360bec3e386daf261ed3c374f7ae19e688042)) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * **pragma:** The pragma ignores nodes whose type is not a string (and thus a descendant of EventTarget) ([171e0cd](https://github.com/calebdwilliams/jsx-native-events/commit/171e0cd0f402fd58e41e7278d6a13f55a3c90938)) 20 | * **ref:** :bug: fix ignoring existing ref prop ([bbab5e6](https://github.com/calebdwilliams/jsx-native-events/commit/bbab5e685458bc31d8c95038cd7bd3dbdd54b78f)) 21 | 22 | ### [1.0.2](https://github.com/calebdwilliams/jsx-native-events/compare/v1.0.1...v1.0.2) (2019-08-02) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **ref:** :bug: fix ignoring existing ref prop ([bbab5e6](https://github.com/calebdwilliams/jsx-native-events/commit/bbab5e6)) 28 | 29 | 30 | 31 | ### [1.0.1](https://github.com/calebdwilliams/jsx-native-events/compare/v0.1.0...v1.0.1) (2019-06-17) 32 | 33 | 34 | 35 | ## [0.1.0](https://github.com/calebdwilliams/jsx-native-events/compare/v0.0.2...v0.1.0) (2019-06-17) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * **pragma:** The pragma ignores nodes whose type is not a string (and thus a descendant of EventTarget) ([171e0cd](https://github.com/calebdwilliams/jsx-native-events/commit/171e0cd)) 41 | 42 | 43 | ### Features 44 | 45 | * Add tests ([9d2360b](https://github.com/calebdwilliams/jsx-native-events/commit/9d2360b)) 46 | 47 | 48 | ### BREAKING CHANGES 49 | 50 | * Initial full release 51 | 52 | 53 | 54 | ### [0.0.2](https://github.com/calebdwilliams/jsx-native-events/compare/v0.0.1...v0.0.2) (2019-06-15) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * Add comments to src file ([b1f072a](https://github.com/calebdwilliams/jsx-native-events/commit/b1f072a)) 60 | 61 | 62 | 63 | ### 0.0.1 (2019-06-15) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * Build exports default function ([6e5be87](https://github.com/calebdwilliams/jsx-native-events/commit/6e5be87)) 69 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at caleb.d.williams@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsx-native-events 2 | 3 | [![Travis][build-badge]][build] 4 | [![npm package][npm-badge]][npm] 5 | [![Coveralls][coveralls-badge]][coveralls] 6 | 7 | This module adds a custom JSX pragma enabling native DOM events to be handled declaratively in JSX. In traditional JSX, events need to be handled by passing down props to elements such as `onClick` or `onChange` that will be attached to the compiled DOM element at some point during the application's lifecycle. For standard events, this works great; however, for events that aren't as common or for [constructed events](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events) or instances of [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent), the prop API falls short. 8 | 9 | This JSX pragma allows users to declaratively attach event listeners to elements using the `onEvent` syntax where `` would be replaced by a [camelCase](https://en.wikipedia.org/wiki/Camel_case) version of the event's name. So, a `'click'` event would use the prop `onEventClick` or a custom event with a name of `accordion-toggle` would use the `onEventAccordionToggle` prop. 10 | 11 | ## Why onEvent? 12 | 13 | The use of `onEvent`, though a bit verbose, was intentional to minimize conflicts with existing code in the React ecosystem while still keeping the syntax familiar. Using `on` would require double checking for the native JSX events. Likewise, using syntax such as `on-accordion-toggle` would feel foreign to existing JSX codebases. The `onEvent` prefix seemed like the best option in the short term. 14 | 15 | ## Installing 16 | 17 | The recommended installation method of this package is through [npm](http://npmjs.com). If you are unfamiliar with the npm ecosystem, there is some great [documentation available on the npm website](https://docs.npmjs.com/cli/install). 18 | 19 | If you are familiar with npm, you can install this package using the command 20 | 21 | `npm i -D jsx-native-events` 22 | 23 | ## Usage 24 | 25 | ### [See this example on StackBlitz](https://stackblitz.com/edit/jsx-native-events-demo) 26 | 27 | Because the primary output of this package is a JSX pragma, you will first need to include the `/** @jsx */` syntax in your file. 28 | 29 | Or add `pragma: "nativeEvents"` to your [`@babel/preset-react`](https://babeljs.io/docs/en/babel-preset-react) or [`@babel/plugin-transform-react-jsx`](https://babeljs.io/docs/en/babel-plugin-transform-react-jsx) babel config. 30 | 31 | ```jsx 32 | /** @jsx nativeEvents */ 33 | import React, { useState } from 'react' 34 | import nativeEvents from 'jsx-native-events' 35 | 36 | export default function SomeComponent (props) { 37 | const [ name, setName ] = useState('') 38 | 39 | return
40 |

My name is {name}

41 | 42 | setName(e.detail) }> 43 |
44 | } 45 | ``` 46 | 47 | In the above example, `` is an example of a [custom element](https://css-tricks.com/an-introduction-to-web-components/) that dispatches an event called `custom-event`. In our React application, we want to listen for that custom event and set the name every time the event is emitted. 48 | 49 | Using the `/** @jsx nativeEvents */` pragma at the top of the file lets JSX know that we want to use the function imported in line 3 (`import nativeEvents from 'jsx-native-events'`) as an addition to React's built-in JSX engine. 50 | 51 | The new props will only work for implementations of [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget), so the new props are not ignored on React components, but should work on all DOM elements represented by React's JSX. 52 | 53 | [build-badge]: https://img.shields.io/travis/user/repo/master.png?style=flat-square 54 | [build]: https://travis-ci.org/user/repo 55 | 56 | [npm-badge]: https://img.shields.io/npm/v/npm-package.png?style=flat-square 57 | [npm]: https://www.npmjs.org/package/npm-package 58 | 59 | [coveralls-badge]: https://img.shields.io/coveralls/user/repo/master.png?style=flat-square 60 | [coveralls]: https://coveralls.io/github/user/repo 61 | --------------------------------------------------------------------------------