-tags in the markup
70 | // ATTENTION: all custom element tag names MUST contain a dash
71 | // use it anywhere like this:
72 | //
73 | register(YourComponent, 'your-component', [
74 | // these attribute values will be json parsed
75 | 'name',
76 | // this will define a boolean attribute
77 | '!!isDisabled',
78 | ],
79 | // handlers can be created here. The functions of the object can return any
80 | // new instance of `Event` or `CustomEvent`
81 | {
82 | onButtonClick: e => new Event('buttonclick', { bubbles: true }),
83 | }
84 | );
85 | ```
86 |
87 | ## API
88 |
89 | ### Register
90 |
91 | The `register` function takes a ReactComponent and registers it using
92 | `customElements.define(...)`.
93 |
94 |
95 | ```
96 | register(reactComponent, nodeName, propsList, eventMappers = {})
97 | ```
98 | | Param | Description |
99 | | --- | --- |
100 | | `reactComponent` | An actual React Component Class / Stateless Function |
101 | | `nodeName` | A tag name for the web component to be created (Must contain a dash)|
102 | | `propsList` | An array of strings which represents the props that should be wired up to React. There are 3 ways to declare a prop.
- `'propName'`: with a regular name the attribute value be JSON parsed and passed to React, if that fails, then it will be passed as a String. This let you pass arbitrary data to the React component, even through DOM attributes.
- `'!!propName'`: With leading bangs this property will be considered a boolean and pass `true` to React or `false` if the attribute is not present on the DOM node.
- `'propName()'`: With trailing parens the property will be considered an event handler and set up event proxying between react and the DOM, so that it's possible to listen to React props handlers from the DOM.|
103 | | `eventMappers` | An optional object with function values. The keys are handler property names (e.g. `onChange`) and the values are functions with the following signature `(...args) => Event\|null`. The returned event will then be dispatched on the Web Component. If null is returned nothing is dispatched. *Note: EventMappers will override any event definitions in the propertyList parameter.* |
104 | | `options` | An optional object. The only options right now is `useShadowDOM` which defaults to true. You can opt out of using shadow DOM by setting this to false. |
105 | | returns `WebComponent class` | An class which is already registered using `customElements.define(...)` |
106 |
107 | ### Convert
108 |
109 | Convert is almost the same as `register` except you have to register the
110 | Component yourself. Do this when you want further extend the component before
111 | registering it.
112 |
113 | ```
114 | // This function will return you a webcomponent instance
115 | convert(reactComponent, propsList, eventMappers = {})
116 | ```
117 | | Param | Description |
118 | | --- | --- |
119 | | `reactComponent` | An actual React Component Class / Stateless Function |
120 | | `propsList` | An array of strings which represents the props that should be wired up to React. There are 3 ways to declare a prop.
- `'propName'`: with a regular name the attribute value be JSON parsed and passed to React, if that fails, then it will be passed as a String. This let you pass arbitrary data to the React component, even through DOM attributes.
- `'!!propName'`: With leading bangs this property will be considered a boolean and pass `true` to React or `false` if the attribute is not present on the DOM node.
- `'propName()'`: With trailing parens the property will be considered an event handler and set up event proxying between react and the DOM, so that it's possible to listen to React props handlers from the DOM.|
121 | | `eventMappers` | An optional object with function values. The keys are handler property names (e.g. `onChange`) and the values are functions with the following signature `(...args) => Event\|null`. The returned event will then be dispatched on the Web Component. If null is returned nothing is dispatched. *Note: EventMappers will override any event definitions in the propertyList parameter.* |
122 | | `options` | An optional object. The only options right now is `useShadowDOM` which defaults to true. You can opt out of using shadow DOM by setting this to false. |
123 | | returns `WebComponent class` | An class which can then be used to register using `customElements.define(...)` |
124 |
125 | You can the use it like this:
126 | ```js
127 | import { convert } from 'web-react-components';
128 |
129 | const MyComponent = convert(MyReactComponet, ['name', 'type'],
130 | {
131 | onChange: (e) => new Event('crazyChange', { bubbles: true }),
132 | });
133 |
134 | // register it here
135 | customElements.define('my-component', MyComponent);
136 | ```
137 | ## Examples
138 |
139 | Then you can render the component from anywhere (even Elm, React, plain HTML, Angular if you really have to :-))
140 |
141 | Elm:
142 | ```elm
143 | -- In the view do this:
144 | ...
145 | type Msg
146 | = ...
147 | | ButtonClick
148 |
149 | {-| Define a shortcut for your component -}
150 | yourComponent : List (Attribute msg) -> List (Html msg) -> Html msg
151 | yourComponent = node "your-component"
152 |
153 | view model = div [] [
154 | yourComponent
155 | [ attribute "name" "Peter"
156 | , property "isDisabled" (Json.Encode.bool True)
157 | , on "buttonclick" (Decode.succeed ButtonClick)
158 | ]
159 | [ span [style ("color", "green")] [text "Click Me"]
160 | ]
161 | ]
162 | ```
163 |
164 | Plain HTML:
165 | ```html
166 | ...
167 |
168 |
169 |
170 | Click Me
171 |
172 |
173 | ```
174 |
175 | ## Passing props
176 |
177 | ### Attributes and Properties
178 | Since in HTML attribute values can only be strings, other values need to be
179 | encoded. The created web component will try a `JSON.parse()` on each attribute, so all
180 | JSON values are valid inside the string. If the parsing fails the value will
181 | just be passed to React as a string.
182 |
183 | `Example: passing '{ "name": Peter }' is fine.`
184 |
185 | For each attribute you register, a matching property will be defined on the DOM
186 | node. These properties will have getters and setters that automatically do JSON
187 | parsing and updating the corresponding attribute as well.
188 |
189 | You can also use JS to pass properties like this:
190 | ```js
191 | document.getElementById('your-dom-id').numbers = [1, 2, 3, 4];
192 | ```
193 |
194 | ### Events
195 |
196 | Events can be listened to in 3 different ways that you should be familiar with from
197 | the DOM.
198 |
199 | ```js
200 | // with `addEventListener()`. The event name will be used here, so use the type of
201 | the returned event from the `eventMappers` parameter.
202 | document.getElementById('#my-component').addEventListener('change', function() { ... }, false);
203 |
204 | // with the DOM Property (notice the uppercase `C`, because the name has to be the same as
205 | // the property in React)
206 | document.getElementById('#my-component').onChange = function() { ... };
207 |
208 | // with the HTML Attributes
209 |
210 | ```
211 | To access data from the original event from React you will have to
212 | do something like this:
213 |
214 | ```js
215 | document.getElementById('#my-component').addEventListener('change', function(event) {
216 | // data is an array of arguments that were passed to the react event handler
217 | const data = event.detail;
218 | // log the first arg of the react event handler
219 | console.log(data[0]);
220 | }, false);
221 | ```
222 |
223 | ### Children
224 | Children are passed like you would expect by simple add child nodes to the
225 | element or programmatically changing the `innerHMTL` or `childNodes` of a
226 | custom compoenent.
227 |
228 | The children will be part of the shadow DOM of the custom components and are rendered
229 | into a [``-tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot).
230 | That `` will be passed to the React components as `children`,
231 | that you can render wherever you want.
232 |
233 | ```html
234 |
235 | I am a child // will be passed as `children` to React
236 |
237 | ```
238 |
239 | ## What about CSS?
240 |
241 | Since the React components, which are wrapped by the Web Component, will live in the
242 | [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM),
243 | global css will not have any effect on them(at least in browsers, which are correcly
244 | implementing it). Thus we recommend shipping the components with inline styles, an internal stylesheet, or
245 | or if you want to include an external stylesheet, use an `@import` declaration in
246 | an internal style tag, like this.
247 |
248 | ```html
249 | // inside render method of your React component
250 |
253 | ```
254 |
255 | ## How does it work under the hood?
256 |
257 | For the ultimate source of truth, the source code is pretty much all this this
258 | [file](https://github.com/ChristophP/web-react-components/blob/master/src/index.js).
259 |
260 | But here is a quick write-up:
261 |
262 | The whole React component will be inserted into the Shadow DOM.
263 | For each property that is declared with the exposed register function, a DOM
264 | attribute is created, that is being listened to for changes through
265 | the [`attributeChangedCallback`](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements).
266 | Also, a corresponding DOM node property is set up with getters and setters, that
267 | keeps the property and the attribute in sync. Registering a property with a
268 | leading `!!` will declare a boolean attribute. Then the getters and setters
269 | will work slightly different and pass a boolean value to React depending on the
270 | existence of the attribute.
271 |
272 | When a property is registered with a trailing `()`, a handler will be created.
273 | A handler is attached to the wrapped React component, that will
274 | trigger a [`CustomEvent`](https://developer.mozilla.org/de/docs/Web/API/CustomEvent)
275 | on the actual web component DOM node and proxy data data to the web component.
276 | This allows you to listen to react event simply by listening to DOM events.
277 |
278 | Children of the web component somehow have to be inserted into the children
279 | of the React components. For this, we use a ``-tag, which is standard
280 | web component shadow DOM technology and built to handle cases like that.
281 |
282 | ## More Examples
283 |
284 | You can see an example [here](https://github.com/ChristophP/web-react-components/blob/master/dev-assets/index.html).
285 | You can also clone the repo and run `npm i` and `npm start`.
286 | Open your browser at `http://localhost:8080`
287 |
288 | There are even more examples in the
289 | [`examples` folder](https://github.com/ChristophP/web-react-components/tree/master/examples).
290 |
291 | ## Contributing
292 |
293 | PRs are highly welcome. If you need features or find bugs please submit an
294 | issue.
295 |
296 | ## Credits
297 |
298 | Made with countless hours of bouncing around ideas with `layflags`. Also intially
299 | inspired by talks with `tkreis` and `rtfeldman`, `tomekwi` at the Elm Europe 2017.
300 |
--------------------------------------------------------------------------------
/dev-assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Web React Components
16 |
17 |
18 |
27 | I'm a native child
28 |
29 |
30 |
108 |
109 |
--------------------------------------------------------------------------------
/examples/Select.js:
--------------------------------------------------------------------------------
1 | function Select(props) {
2 | var h = React.createElement;
3 | return h('div', null,
4 | // childern will be rendern into the label
5 | h('label', { style: props.hasError ? { color: 'red' } : {} }, props.children),
6 | h('select', {
7 | name: props.name,
8 | value: props.value,
9 | disabled: props.disabled,
10 | onChange: props.onChange,
11 | },
12 | props.options && props.options.map(function(item) {
13 | return h('option', { value: item.value }, item.label);
14 | })
15 | )
16 | )
17 | };
18 |
--------------------------------------------------------------------------------
/examples/elm/Main.elm:
--------------------------------------------------------------------------------
1 | module Main exposing (..)
2 |
3 | import Html exposing (Html, Attribute, h3, div, label, beginnerProgram, input, button, text, node)
4 | import Html.Attributes exposing (style, id, name, value, disabled, attribute, property)
5 | import Html.Events exposing (onClick, onInput, on)
6 | import Json.Encode as Encode
7 | import Json.Decode as Decode exposing (Decoder)
8 |
9 |
10 | main : Program Never Model Msg
11 | main =
12 | beginnerProgram { model = model, update = update, view = view }
13 |
14 |
15 | type alias Model =
16 | { disabled : Bool
17 | , value : String
18 | , name : String
19 | , hasError : Bool
20 | }
21 |
22 |
23 | type Msg
24 | = ToggleDisabled
25 | | ToggleValue
26 | | ChangeValue String
27 | | ChangeName String
28 | | ToggleError
29 |
30 |
31 | {-| Encoded JSON data to pass to the web component
32 | -}
33 | optionsValue : Encode.Value
34 | optionsValue =
35 | Encode.list
36 | [ Encode.object
37 | [ ( "value", Encode.string "one" )
38 | , ( "label", Encode.string "One" )
39 | ]
40 | , Encode.object
41 | [ ( "value", Encode.string "two" )
42 | , ( "label", Encode.string "Two" )
43 | ]
44 | ]
45 |
46 |
47 | model : Model
48 | model =
49 | Model False "one" "form-field-name" False
50 |
51 |
52 | update : Msg -> Model -> Model
53 | update msg model =
54 | case msg of
55 | ToggleDisabled ->
56 | { model | disabled = not model.disabled }
57 |
58 | ToggleValue ->
59 | { model
60 | | value =
61 | if model.value == "one" then
62 | "two"
63 | else
64 | "one"
65 | }
66 |
67 | ChangeValue val ->
68 | let
69 | _ =
70 | Debug.log "value changed from Elm" val
71 | in
72 | { model | value = val }
73 |
74 | ChangeName name ->
75 | { model | name = name }
76 |
77 | ToggleError ->
78 | { model | hasError = not model.hasError }
79 |
80 |
81 | onChange : (String -> Msg) -> Html.Attribute Msg
82 | onChange tagger =
83 | on "onChange" (Decode.map tagger detailTargetValueDecoder)
84 |
85 |
86 | {-| IMPORTANT: We need to get the data of the original React event like
87 | this : event.detail[0].target.value
88 | -}
89 | detailTargetValueDecoder : Decoder String
90 | detailTargetValueDecoder =
91 | Decode.field "detail" <|
92 | Decode.index 0 <|
93 | Decode.at [ "target", "value" ] Decode.string
94 |
95 |
96 | {-| Create shorthand for custom element
97 | -}
98 | customSelect : List (Attribute msg) -> List (Html msg) -> Html msg
99 | customSelect =
100 | node "custom-select"
101 |
102 |
103 | {-| Create shorthand for custom property. Property is easier to use
104 | than attribute for Boolean values.
105 | -}
106 | hasError : Bool -> Attribute Msg
107 | hasError =
108 | property "hasError" << Encode.bool
109 |
110 |
111 | {-| Create shorthand for options property
112 | -}
113 | options : Encode.Value -> Attribute Msg
114 | options =
115 | property "options"
116 |
117 |
118 | view : Model -> Html Msg
119 | view model =
120 | div []
121 | [ h3 [] [ text "Custom select component" ]
122 | , div []
123 | [ customSelect
124 | [ id "customComponent"
125 | , name model.name
126 | , value model.value
127 | , disabled model.disabled
128 | , hasError model.hasError
129 | , options optionsValue
130 | , onChange ChangeValue
131 | ]
132 | [ text "Select cool stuff" ]
133 | ]
134 | , div []
135 | [ button [ onClick ToggleDisabled ] [ text "toggle disabled" ]
136 | , button [ onClick ToggleValue ] [ text "toggle value" ]
137 | , button [ onClick ToggleError ] [ text "toggle error" ]
138 | , label []
139 | [ text "Change name"
140 | , input [ onInput ChangeName, value model.name ] []
141 | ]
142 | ]
143 | ]
144 |
--------------------------------------------------------------------------------
/examples/elm/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "helpful summary of your project, less than 80 characters",
4 | "repository": "https://github.com/user/project.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | "."
8 | ],
9 | "exposed-modules": [],
10 | "dependencies": {
11 | "elm-lang/core": "5.1.1 <= v < 6.0.0",
12 | "elm-lang/html": "2.0.0 <= v < 3.0.0"
13 | },
14 | "elm-version": "0.18.0 <= v < 0.19.0"
15 | }
16 |
--------------------------------------------------------------------------------
/examples/elm/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
19 |
20 |
21 |
22 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-react-components",
3 | "repository": {
4 | "type": "git",
5 | "url": "https://github.com/ChristophP/web-react-components"
6 | },
7 | "version": "1.4.2",
8 | "description": "A web component wrapper around React Components",
9 | "main": "dist/bundle.js",
10 | "scripts": {
11 | "start": "webpack-dev-server --mode=development",
12 | "build": "webpack --mode=production",
13 | "test": "echo \"TODO: add tests\"",
14 | "lint": "eslint webpack.config.js src",
15 | "prepublishOnly": "npm run build"
16 | },
17 | "files": [
18 | "dist/bundle.js"
19 | ],
20 | "author": "",
21 | "license": "ISC",
22 | "peerDependencies": {
23 | "react": ">=0.14.0",
24 | "react-dom": ">=0.14.0"
25 | },
26 | "devDependencies": {
27 | "@babel/core": "^7.4.4",
28 | "@babel/preset-env": "^7.4.4",
29 | "babel-loader": "^8.0.5",
30 | "eslint": "^5.16.0",
31 | "eslint-config-airbnb": "^17.1.0",
32 | "eslint-config-prettier": "^4.2.0",
33 | "eslint-plugin-import": "^2.17.2",
34 | "eslint-plugin-jsx-a11y": "^6.2.1",
35 | "eslint-plugin-prettier": "^3.0.1",
36 | "eslint-plugin-react": "^7.13.0",
37 | "pre-commit": "^1.2.2",
38 | "prettier": "^1.17.0",
39 | "webpack": "^4.30.0",
40 | "webpack-cli": "^3.3.1"
41 | },
42 | "pre-commit": {
43 | "run": "lint, test",
44 | "silent": true
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-unresolved
2 | import React from "react";
3 | // eslint-disable-next-line import/no-unresolved
4 | import ReactDOM from "react-dom";
5 | import {
6 | pipe,
7 | isBoolConvention,
8 | isHandlerConvention,
9 | objectFromArray,
10 | mapObject,
11 | mapObjectKeys,
12 | sanitizeAttributeName,
13 | setBooleanAttribute,
14 | } from "./util";
15 |
16 | const Types = {
17 | bool: "bool",
18 | event: "event",
19 | json: "json",
20 | };
21 |
22 | const mapAttributeToProp = (node, name) =>
23 | // return 'undefined' instead of 'null' for missing attributes / properties
24 | // so that React's default props apply
25 | node[name] === null ? undefined : node[name];
26 |
27 | const mapEventToProp = (node, name) => {
28 | // accessing properties instead of attributes here
29 | // (autom. attribute parsing)
30 | const handler = node[name];
31 |
32 | return (...origArgs) => {
33 | // dispatch DOM event
34 | const domEvent = new CustomEvent(name, {
35 | bubbles: true,
36 | cancelable: true,
37 | detail: origArgs, // store original arguments from handler
38 | });
39 | node.dispatchEvent(domEvent);
40 |
41 | // call event handler if defined
42 | if (typeof handler === "function") {
43 | handler.call(node, domEvent);
44 | }
45 | };
46 | };
47 |
48 | const mapToProps = (node, mapping) => {
49 | const mapFunc = (type, name) =>
50 | type === Types.event
51 | ? mapEventToProp(node, name)
52 | : mapAttributeToProp(node, name);
53 | return mapObject(mapFunc, mapping);
54 | };
55 |
56 | const mapToPropertyDescriptor = (name, type) => {
57 | const defaults = { enumerable: true, configurable: true };
58 |
59 | // handlers
60 | if (type === Types.event) {
61 | let eventHandler;
62 | return {
63 | ...defaults,
64 | get() {
65 | // return event handler assigned via propery if available
66 | if (typeof eventHandler !== "undefined") return eventHandler;
67 |
68 | // return null if event handler attribute wasn't defined
69 | const value = this.getAttribute(name);
70 | if (value === null) return null;
71 |
72 | // try to return a function representation of the event handler attr.
73 | try {
74 | // eslint-disable-next-line no-new-func
75 | return new Function(value);
76 | } catch (err) {
77 | return null;
78 | }
79 | },
80 | set(value) {
81 | eventHandler = typeof value === "function" ? value : null;
82 | this.attributeChangedCallback();
83 | },
84 | };
85 | }
86 |
87 | // booleans
88 | if (type === Types.bool) {
89 | return {
90 | ...defaults,
91 | get() {
92 | return this.hasAttribute(name);
93 | },
94 | set(value) {
95 | setBooleanAttribute(this, name, value);
96 | },
97 | };
98 | }
99 |
100 | // json
101 | return {
102 | ...defaults,
103 | get() {
104 | const value = this.getAttribute(name);
105 |
106 | // try to parse as JSON
107 | try {
108 | return JSON.parse(value);
109 | } catch (e) {
110 | return value; // original string as fallback
111 | }
112 | },
113 | set(value) {
114 | const str = typeof value === "string" ? value : JSON.stringify(value);
115 | this.setAttribute(name, str);
116 | },
117 | };
118 | };
119 |
120 | const definePropertiesFor = (WebComponent, mapping) => {
121 | Object.keys(mapping).forEach(name => {
122 | const type = mapping[name];
123 |
124 | Object.defineProperty(
125 | WebComponent.prototype,
126 | name,
127 | mapToPropertyDescriptor(name, type)
128 | );
129 | });
130 | };
131 |
132 | const getType = name => {
133 | if (isBoolConvention(name)) {
134 | return Types.bool;
135 | }
136 | if (isHandlerConvention(name)) {
137 | return Types.event;
138 | }
139 | return Types.json;
140 | };
141 |
142 | /**
143 | * Function to convert a React Components to a Web Components
144 | * @param {class} ReactComponent - A react component
145 | * @param {string[]} [propNames] - An optional list of property names to be
146 | * connected with the React component.
147 | * @param {Object} [eventMappers] - An optional map of functions which can
148 | * return an event to be dispatched
149 | * @returns {class} - The custom element class
150 | */
151 | function convert(
152 | ReactComponent,
153 | propNames = [],
154 | eventMappers = {},
155 | options = { useShadowDOM: true }
156 | ) {
157 | const createMap = obj => objectFromArray(getType, obj);
158 | const cleanKeys = obj => mapObjectKeys(sanitizeAttributeName, obj);
159 | const mapping = pipe(
160 | createMap,
161 | cleanKeys
162 | )(propNames);
163 |
164 | const attributeNames = Object.keys(mapping).map(name => name.toLowerCase());
165 |
166 | const dispatcher = component => mapper => (...args) => {
167 | const event = mapper(...args);
168 | if (event) {
169 | component.dispatchEvent(event);
170 | }
171 | };
172 |
173 | // render should be private
174 | const render = component => {
175 | const props = {
176 | ...mapToProps(component, mapping),
177 | // add event mappers, will possibly override the ones in attribute
178 | ...mapObject(dispatcher(component), eventMappers),
179 | };
180 |
181 | const rootElement = options.useShadowDOM ? component.shadowRoot : component;
182 |
183 | ReactDOM.render(
184 | React.createElement(ReactComponent, props, React.createElement("slot")),
185 | rootElement
186 | );
187 | };
188 |
189 | class WebReactComponent extends HTMLElement {
190 | static get observedAttributes() {
191 | return attributeNames;
192 | }
193 |
194 | constructor() {
195 | super();
196 | if (options.useShadowDOM) {
197 | this.attachShadow({ mode: "open" });
198 | }
199 | }
200 |
201 | connectedCallback() {
202 | render(this);
203 | }
204 |
205 | attributeChangedCallback() {
206 | render(this);
207 | }
208 |
209 | disconnectedCallback() {
210 | const rootElement = options.useShadowDOM ? this.shadowRoot : this;
211 |
212 | ReactDOM.unmountComponentAtNode(rootElement);
213 | }
214 | }
215 |
216 | // dynamically create property getters and setters for attributes
217 | // and event handlers
218 | definePropertiesFor(WebReactComponent, mapping);
219 |
220 | return WebReactComponent;
221 | }
222 |
223 | /**
224 | * Function to register React Components as Web Components
225 | * @param {class} ReactComponent - A react component
226 | * @param {string} tagName - A name for the new custom tag
227 | * @param {string[]} [propNames] - An optional list of property names to be
228 | * connected with the React component.
229 | * @param {Object} [eventMappers] - An optional map of functions which can
230 | * return an event to be dispatched
231 | * @returns {class} - The custom element class
232 | */
233 | function register(
234 | ReactComponent,
235 | tagName,
236 | propNames = [],
237 | eventMappers = {},
238 | options = { useShadowDOM: true }
239 | ) {
240 | return customElements.define(
241 | tagName,
242 | convert(ReactComponent, propNames, eventMappers, options)
243 | );
244 | }
245 |
246 | export default {
247 | register,
248 | convert,
249 | };
250 |
251 | export { register, convert };
252 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | // GENERAL PURPOSE
2 |
3 | // compose two functions left to right
4 | export const pipe = (f, g) => x => g(f(x));
5 |
6 | // create an object from an array where the keys are the array items
7 | // and the values are created from a function that is passed which gets
8 | // the array entry as an argument
9 | export const objectFromArray = (createValue, arr) =>
10 | arr.reduce((obj, val) => ({ ...obj, [val]: createValue(val) }), {});
11 |
12 | // map an object's values, the callback function gets the value,
13 | // the key and the input object
14 | export const mapObject = (fn, obj) =>
15 | Object.keys(obj).reduce(
16 | (acc, key) => ({ ...acc, [key]: fn(obj[key], key, obj) }),
17 | {}
18 | );
19 |
20 | // map an object's keys, the callback function gets the current key
21 | export const mapObjectKeys = (fn, obj) =>
22 | Object.keys(obj).reduce((acc, key) => ({ ...acc, [fn(key)]: obj[key] }), {});
23 |
24 | // ATTRIBUTE CONVENTIONS
25 | const boolRegex = /^!!/;
26 | const handlerRegex = /\(\)$/;
27 |
28 | // check if a propName corresponds to a boolean convention,
29 | // starting with "!!"
30 | export const isBoolConvention = prop => boolRegex.test(prop);
31 |
32 | // check if a propName corresponds to a handler convention,
33 | // ending in "()"
34 | export const isHandlerConvention = prop => handlerRegex.test(prop);
35 |
36 | // clean an attribute name from modifiers like !! and ()
37 | export const sanitizeAttributeName = prop =>
38 | prop.replace(boolRegex, "").replace(handlerRegex, "");
39 |
40 | // IMPURE
41 |
42 | // properly `set the value` for boolean attributes
43 | export const setBooleanAttribute = (node, name, value) => {
44 | if (value) {
45 | node.setAttribute(name, "");
46 | } else {
47 | node.removeAttribute(name);
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | entry: "./src/index.js",
5 | output: {
6 | path: path.resolve("dist"),
7 | filename: "bundle.js",
8 | library: "WebReactComponents",
9 | libraryTarget: "umd",
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.js$/,
15 | exclude: /node_modules/,
16 | loaders: ["babel-loader"],
17 | },
18 | ],
19 | },
20 | externals: {
21 | react: {
22 | commonjs: "react",
23 | commonjs2: "react",
24 | amd: "react",
25 | root: "React",
26 | },
27 | "react-dom": {
28 | commonjs: "react-dom",
29 | commonjs2: "react-dom",
30 | amd: "react-dom",
31 | root: "ReactDOM",
32 | },
33 | },
34 | devServer: {
35 | contentBase: path.resolve("dev-assets"),
36 | port: process.env.PORT || 8080,
37 | host: "0.0.0.0",
38 | disableHostCheck: true,
39 | },
40 | };
41 |
--------------------------------------------------------------------------------