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 |
--------------------------------------------------------------------------------