├── cjs ├── package.json └── index.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── rollup.config.js ├── LICENSE ├── test ├── index.html ├── effect.html └── test.js ├── esm └── index.js ├── package.json ├── README.md ├── index.d.ts ├── min.js └── index.js /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .DS_Store 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | img/ 3 | node_modules/ 4 | test/ 5 | _config.yml 6 | .DS_Store 7 | .gitignore 8 | .travis.yml 9 | package-lock.json 10 | rollup.config.js 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master 9 | - /^greenkeeper/.*$/ 10 | after_success: 11 | - "npm run coveralls" 12 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | export default { 4 | input: 'esm/index.js', 5 | plugins: [ 6 | resolve(), 7 | babel({ 8 | runtimeHelpers: true, 9 | presets: ['@babel/preset-env'] 10 | }) 11 | ], 12 | context: 'null', 13 | moduleContext: 'null', 14 | output: { 15 | exports: 'named', 16 | file: 'index.js', 17 | format: 'iife', 18 | name: 'augmentor' 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018-8-8-8-8-8-8-8-, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DOM Augmentor - useHTML example 8 | 9 | 10 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/effect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 52 | 53 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | import CustomEvent from '@ungap/custom-event'; 3 | import WeakSet from '@ungap/weakset'; 4 | 5 | import disconnected from 'disconnected'; 6 | 7 | import { 8 | augmentor as $augmentor, 9 | dropEffect, hasEffect 10 | } from 'augmentor'; 11 | 12 | const find = node => { 13 | let {firstChild} = node; 14 | while (firstChild && firstChild.nodeType !== 1) 15 | firstChild = firstChild.nextSibling; 16 | if (firstChild) 17 | return firstChild; 18 | throw 'unobservable'; 19 | }; 20 | 21 | const observe = disconnected({Event: CustomEvent, WeakSet}); 22 | 23 | const observer = (element, handler) => { 24 | const {nodeType} = element; 25 | if (nodeType) { 26 | const node = nodeType === 1 ? element : find(element); 27 | observe(node); 28 | node.addEventListener('disconnected', handler, false); 29 | } 30 | else { 31 | const value = element.valueOf(); 32 | // give a chance to facades to return a reasonable value 33 | if (value !== element) 34 | observer(value, handler); 35 | } 36 | }; 37 | 38 | export const augmentor = fn => { 39 | let handler = null; 40 | const hook = $augmentor(fn); 41 | return function () { 42 | const node = hook.apply(this, arguments); 43 | if (hasEffect(hook)) 44 | observer(node, handler || (handler = dropEffect.bind(null, hook))); 45 | return node; 46 | }; 47 | }; 48 | 49 | export { 50 | contextual, 51 | useState, 52 | useEffect, useLayoutEffect, 53 | useContext, createContext, 54 | useReducer, 55 | useCallback, 56 | useMemo, 57 | useRef 58 | } from 'augmentor'; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-augmentor", 3 | "version": "2.0.8", 4 | "description": "DOM oriented useEffect hooks for the augmentor", 5 | "main": "cjs/index.js", 6 | "module": "esm/index.js", 7 | "unpkg": "min.js", 8 | "scripts": { 9 | "build": "npm run rollup && npm run cjs && npm run min && npm run test && npm run size", 10 | "cjs": "ascjs esm cjs", 11 | "coveralls": "cat ./coverage/lcov.info | coveralls", 12 | "min": "echo '/*! (c) Andrea Giammarchi - ISC */' > min.js && uglifyjs index.js -c -m >> min.js", 13 | "rollup": "rollup --config rollup.config.js && drop-babel-typeof index.js", 14 | "size": "cat index.js | wc -c;cat min.js | wc -c;gzip -c9 min.js | wc -c", 15 | "test": "istanbul cover test/test.js" 16 | }, 17 | "keywords": [ 18 | "hooks", 19 | "dom", 20 | "observer", 21 | "augmentor" 22 | ], 23 | "author": "Andrea Giammarchi", 24 | "license": "ISC", 25 | "greenkeeper": { 26 | "ignore": [ 27 | "rollup", 28 | "rollup-plugin-babel", 29 | "rollup-plugin-node-resolve" 30 | ] 31 | }, 32 | "dependencies": { 33 | "@ungap/custom-event": "^0.2.0", 34 | "@ungap/weakset": "^0.1.5", 35 | "augmentor": "^2.2.0", 36 | "disconnected": "^0.2.1" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.9.0", 40 | "@babel/preset-env": "^7.9.5", 41 | "ascjs": "^3.1.2", 42 | "basichtml": "^2.2.3", 43 | "coveralls": "^3.0.11", 44 | "drop-babel-typeof": "^1.0.3", 45 | "http-server": "^0.12.1", 46 | "istanbul": "^0.4.5", 47 | "rollup": "^2.6.0", 48 | "rollup-plugin-babel": "^4.4.0", 49 | "rollup-plugin-node-resolve": "^5.2.0", 50 | "uglify-es": "^3.3.9" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dom-augmentor 2 | 3 | [![Build Status](https://travis-ci.com/WebReflection/dom-augmentor.svg?branch=master)](https://travis-ci.com/WebReflection/dom-augmentor) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/dom-augmentor/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/dom-augmentor?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/WebReflection/dom-augmentor.svg)](https://greenkeeper.io/) ![WebReflection status](https://offline.report/status/webreflection.svg) 4 | 5 | **Social Media Photo by [stephan sorkin](https://unsplash.com/@sorkin) on [Unsplash](https://unsplash.com/)** 6 | 7 | This is exactly the same as the [augmentor](https://github.com/WebReflection/augmentor) module, except it handles automatically effects per DOM nodes. 8 | 9 | Compatible with any function that returns a DOM node, or a fragment, or a hyperhtml like Wire instance. 10 | 11 | 12 | 13 | ### Breaking in v1 14 | 15 | * the default export has been removed, it's `import {augmentor} from 'augmentor'` now 16 | * the `augmentor` library is the v1 one 17 | * effects now work more reliably and there are no constrains for the guards 18 | 19 | 20 | 21 | ### Example 22 | 23 | **[Live Demo](https://codepen.io/WebReflection/pen/maQXwq)** 24 | 25 | ```js 26 | const {augmentor: $, useEffect, useRef, useState} = augmentor; 27 | const {render, hook} = lighterhtml; 28 | const {html, svg} = hook(useRef); 29 | 30 | const Button = (text) => $(() => { 31 | useEffect( 32 | () => { 33 | console.log('connected'); 34 | return () => console.log('disconnected'); 35 | }, 36 | [] 37 | ); 38 | const [i, increment] = useState(0); 39 | return html` 40 | `; 43 | }); 44 | 45 | const button = Button('hello'); 46 | 47 | render(document.body, button); 48 | // alternatively 49 | // document.body.appendChild(button()); 50 | ``` 51 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! (c) Andrea Giammarchi - ISC */ 3 | const CustomEvent = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('@ungap/custom-event')); 4 | const WeakSet = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('@ungap/weakset')); 5 | 6 | const disconnected = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('disconnected')); 7 | 8 | const { 9 | augmentor: $augmentor, 10 | dropEffect, 11 | hasEffect 12 | } = require('augmentor'); 13 | 14 | const find = node => { 15 | let {firstChild} = node; 16 | while (firstChild && firstChild.nodeType !== 1) 17 | firstChild = firstChild.nextSibling; 18 | if (firstChild) 19 | return firstChild; 20 | throw 'unobservable'; 21 | }; 22 | 23 | const observe = disconnected({Event: CustomEvent, WeakSet}); 24 | 25 | const observer = (element, handler) => { 26 | const {nodeType} = element; 27 | if (nodeType) { 28 | const node = nodeType === 1 ? element : find(element); 29 | observe(node); 30 | node.addEventListener('disconnected', handler, false); 31 | } 32 | else { 33 | const value = element.valueOf(); 34 | // give a chance to facades to return a reasonable value 35 | if (value !== element) 36 | observer(value, handler); 37 | } 38 | }; 39 | 40 | const augmentor = fn => { 41 | let handler = null; 42 | const hook = $augmentor(fn); 43 | return function () { 44 | const node = hook.apply(this, arguments); 45 | if (hasEffect(hook)) 46 | observer(node, handler || (handler = dropEffect.bind(null, hook))); 47 | return node; 48 | }; 49 | }; 50 | exports.augmentor = augmentor; 51 | 52 | (m => { 53 | exports.contextual = m.contextual; 54 | exports.useState = m.useState; 55 | exports.useEffect = m.useEffect; 56 | exports.useLayoutEffect = m.useLayoutEffect; 57 | exports.useContext = m.useContext; 58 | exports.createContext = m.createContext; 59 | exports.useReducer = m.useReducer; 60 | exports.useCallback = m.useCallback; 61 | exports.useMemo = m.useMemo; 62 | exports.useRef = m.useRef; 63 | })(require('augmentor')); 64 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | type Context = { value: T; provide: (value: T) => void; }; 2 | 3 | type RefList = ReadonlyArray; 4 | 5 | type EffectCallback = () => (void | (() => void | undefined)); 6 | 7 | type SetStateAction = S | ((prevState: S) => S); 8 | type Dispatch = (value: A) => void; 9 | type Reducer = (prevState: S, action: A) => S; 10 | type ReducerState > = 11 | R extends Reducer ? S : never; 12 | type ReducerAction > = 13 | R extends Reducer ? A : never; 14 | 15 | type RefObject = { current: T | null; }; 16 | type MutableRefObject = { current: T; }; 17 | 18 | declare module "dom-augmentor" { 19 | 20 | function augmentor any> (input: T): T; 21 | 22 | function useContext (context: Context): T; 23 | 24 | function createContext (initialValue: T): Context; 25 | 26 | function useMemo (callback: () => T, refs: ReadonlyArray): T; 27 | 28 | function useCallback any> (callback: T, refs: RefList): T; 29 | 30 | function useEffect (effect: EffectCallback, refs?: RefList): void; 31 | 32 | function useLayoutEffect (effect: EffectCallback, refs?: RefList): void; 33 | 34 | function useReducer, I>( 35 | reducer: R, initializerArg: I & ReducerState, initializer: (arg: I & ReducerState) => ReducerState, 36 | ): [ReducerState, Dispatch>]; 37 | 38 | function useReducer, I>( 39 | reducer: R, initializerArg: I, initializer: (arg: I) => ReducerState, 40 | ): [ReducerState, Dispatch>]; 41 | 42 | function useReducer>( 43 | reducer: R, initialState: ReducerState, initializer?: undefined, 44 | ): [ReducerState, Dispatch>]; 45 | 46 | function useRef (initialValue: T): MutableRefObject; 47 | function useRef (initialValue: T | null): RefObject; 48 | function useRef (): MutableRefObject; 49 | 50 | function useState (initialState: S | (() => S)): [S, Dispatch>]; 51 | function useState (): [S | undefined, Dispatch>]; 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | require('basichtml').init(); 2 | const { 3 | augmentor, 4 | useEffect, 5 | useRef, 6 | useState 7 | } = require('../cjs'); 8 | 9 | const NoEffect = () => augmentor(text => { 10 | const [i, increment] = useState(0); 11 | const {current: button} = useRef(createButton()); 12 | button.textContent = `${text} ${i}`; 13 | button.onclick = () => increment(i + 1); 14 | return button; 15 | }); 16 | 17 | NoEffect()('nope'); 18 | 19 | const Button = () => augmentor(text => { 20 | useEffect( 21 | () => { 22 | console.log('connected'); 23 | return () => console.log('disconnected'); 24 | }, 25 | [] 26 | ); 27 | const [i, increment] = useState(0); 28 | const {current: button} = useRef(createButton()); 29 | button.textContent = `${text} ${i}`; 30 | button.onclick = () => increment(i + 1); 31 | return button; 32 | }); 33 | 34 | const Unknown = () => augmentor(text => { 35 | useEffect( 36 | () => { 37 | console.log('connected'); 38 | return () => console.log('disconnected'); 39 | }, 40 | [] 41 | ); 42 | const [i, increment] = useState(0); 43 | return { 44 | valueOf: () => createButton() 45 | }; 46 | }); 47 | 48 | const Nope = () => augmentor(text => { 49 | useEffect( 50 | () => { 51 | console.log('connected'); 52 | return () => console.log('disconnected'); 53 | }, 54 | [] 55 | ); 56 | return {}; 57 | }); 58 | 59 | const Maybe = () => augmentor(text => { 60 | useEffect( 61 | () => { 62 | console.log('connected'); 63 | return () => console.log('disconnected'); 64 | } 65 | ); 66 | return {}; 67 | }); 68 | 69 | const Never = () => augmentor(text => { 70 | useEffect( 71 | () => { 72 | console.log('connected'); 73 | return () => console.log('disconnected'); 74 | }, 75 | [1] 76 | ); 77 | return {}; 78 | }); 79 | 80 | const Many = () => augmentor(text => { 81 | useEffect( 82 | () => { 83 | console.log('connected'); 84 | return () => console.log('disconnected'); 85 | }, 86 | [] 87 | ); 88 | const fragment = document.createDocumentFragment(); 89 | fragment.appendChild(document.createTextNode()); 90 | fragment.appendChild(createButton()).textContent = text; 91 | return fragment; 92 | }); 93 | 94 | const Thrower = () => augmentor(() => { 95 | useEffect( 96 | () => { 97 | console.log('connected'); 98 | return () => console.log('disconnected'); 99 | }, 100 | [] 101 | ); 102 | return document.createDocumentFragment(); 103 | }); 104 | 105 | const button = Button()('hello'); 106 | const unknown = Unknown()('darkness'); 107 | 108 | Nope()(); 109 | 110 | Maybe()(); 111 | Never()(); 112 | 113 | 114 | document.body.appendChild(button); 115 | document.body.appendChild(unknown); 116 | document.body.appendChild(Many()('test')); 117 | 118 | button.click(); 119 | button.dispatchEvent(event('connected')); 120 | button.dispatchEvent(event('disconnected')); 121 | 122 | try { 123 | Thrower()(); 124 | console.assert(false); 125 | } catch(ok) { 126 | console.log('all good'); 127 | } 128 | 129 | function createButton() { 130 | return document.createElement('button'); 131 | } 132 | 133 | function event(type) { 134 | const e = document.createEvent('Event'); 135 | e.initEvent(type, false, false); 136 | return e; 137 | } 138 | -------------------------------------------------------------------------------- /min.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | var augmentor=function(n){"use strict";var t={};t.CustomEvent="function"==typeof CustomEvent?CustomEvent:function(n){return t.prototype=new t("").constructor.prototype,t;function t(n,t){t||(t={});var e=document.createEvent("CustomEvent");return e.initCustomEvent(n,!!t.bubbles,!!t.cancelable,t.detail),e}}();var e=t.CustomEvent,r={};try{r.WeakSet=WeakSet}catch(n){!function(n){var t=new n,e=u.prototype;function u(e){t.set(this,new n),e&&e.forEach(this.add,this)}e.add=function(n){return t.get(this).set(n,1),this},e.delete=function(n){return t.get(this).delete(n)},e.has=function(n){return t.get(this).has(n)},r.WeakSet=u}(WeakMap)}var u=r.WeakSet;var o="function"==typeof cancelAnimationFrame,a=o?cancelAnimationFrame:clearTimeout,i=o?requestAnimationFrame:setTimeout;function c(n){var t,e,r,u,c;return f(),function(n,o,a){return r=n,u=o,c=a,e||(e=i(s)),--t<0&&l(!0),l};function s(){f(),r.apply(u,c||[])}function f(){t=n||1/0,e=o?0:null}function l(n){var t=!!e;return t&&(a(e),n&&s()),t}}var s=function(n){return{get:function(t){return n.get(t)},set:function(t,e){return n.set(t,e),e}}},f=null,l=function(n){var t=[];return function e(){var r=f,u=[];f={hook:e,args:arguments,stack:t,i:0,length:t.length,after:u};try{return n.apply(null,arguments)}finally{f=r;for(var o=0,a=u.length;o