├── packages ├── defi │ ├── babel.config.js │ ├── src │ │ ├── _core │ │ │ ├── defs.js │ │ │ ├── defineprop.js │ │ │ └── init.js │ │ ├── on │ │ │ ├── _splitbyspaceregexp.js │ │ │ ├── _domeventregexp.js │ │ │ ├── _delegatelistener │ │ │ │ ├── changehandler.js │ │ │ │ └── index.js │ │ │ ├── _createdomeventhandler.js │ │ │ ├── _addtreelistener.js │ │ │ ├── _adddomlistener.js │ │ │ ├── index.js │ │ │ └── _addlistener.js │ │ ├── binders │ │ │ ├── progress.js │ │ │ ├── textarea.js │ │ │ ├── index.js │ │ │ ├── output.js │ │ │ ├── select.js │ │ │ └── input.js │ │ ├── _helpers │ │ │ ├── foreach.js │ │ │ ├── map.js │ │ │ ├── is.js │ │ │ ├── forown.js │ │ │ ├── assign.js │ │ │ ├── slice.js │ │ │ ├── reduce.js │ │ │ ├── checkobjecttype.js │ │ │ ├── deepfind.js │ │ │ ├── debounce.js │ │ │ └── defierror.js │ │ ├── _mq │ │ │ ├── _data.js │ │ │ ├── parsehtml.js │ │ │ ├── index.js │ │ │ ├── add.js │ │ │ ├── _html2nodelist.js │ │ │ ├── _init.js │ │ │ ├── off.js │ │ │ └── on.js │ │ ├── index.js │ │ ├── lookforbinder.js │ │ ├── trigger │ │ │ ├── _triggeronedomevent.js │ │ │ ├── _triggerone.js │ │ │ ├── _triggerdomevent.js │ │ │ └── index.js │ │ ├── _lib.js │ │ ├── bindnode │ │ │ ├── _getnodes.js │ │ │ ├── _createobjecthandler.js │ │ │ ├── _createnodehandler.js │ │ │ ├── _createbindingswitcher.js │ │ │ ├── _selectnodes.js │ │ │ └── index.js │ │ ├── defaultbinders.js │ │ ├── bound.js │ │ ├── off │ │ │ ├── _removetreelistener.js │ │ │ ├── _removedomlistener.js │ │ │ ├── index.js │ │ │ ├── _undelegatelistener.js │ │ │ └── _removelistener.js │ │ ├── chain.js │ │ ├── calc │ │ │ ├── _addsource.js │ │ │ └── _createcalchandler.js │ │ ├── unbindnode │ │ │ └── _removebinding.js │ │ ├── mediate.js │ │ └── remove.js │ ├── test │ │ ├── browser-test │ │ │ ├── jasmine-2.4.1 │ │ │ │ └── jasmine_favicon.png │ │ │ └── SpecRunner.html │ │ ├── helpers │ │ │ ├── createspy.js │ │ │ ├── simulateclick.js │ │ │ ├── deepfind.js │ │ │ └── makeobject.js │ │ ├── spec │ │ │ ├── common_spec.js │ │ │ ├── mq │ │ │ │ ├── add_spec.js │ │ │ │ ├── parsehtml_spec.js │ │ │ │ └── init_spec.js │ │ │ ├── set_spec.js │ │ │ ├── chain_spec.js │ │ │ ├── mediate_spec.js │ │ │ ├── remove_spec.js │ │ │ └── events │ │ │ │ ├── events_core_spec.js │ │ │ │ ├── events_change_spec.js │ │ │ │ └── tree_change_spec.js │ │ ├── node-test │ │ │ └── jasmine.js │ │ ├── index.js │ │ ├── webpack-test.config.js │ │ └── karma-test │ │ │ └── karma.conf.js │ ├── README.md │ ├── package-lock.json │ ├── tools │ │ ├── generate-package.js │ │ └── banner-and-footer-webpack-plugin.js │ ├── webpack.config.js │ ├── package.json │ └── .eslintrc.js ├── router │ ├── babel.config.js │ ├── .gitignore │ ├── src │ │ └── index.js │ ├── test │ │ ├── index.js │ │ └── spec │ │ │ ├── hash_router_spec.js │ │ │ ├── simple_router_spec.js │ │ │ ├── history_router_spec.js │ │ │ ├── gapped_router_spec.js │ │ │ └── summary_spec.js │ ├── webpack.config.js │ ├── package-lock.json │ ├── LICENSE │ ├── package.json │ └── README.md ├── file-binders │ ├── babel.config.js │ ├── test │ │ ├── spec │ │ │ ├── createspy.js │ │ │ ├── dragover_spec.js │ │ │ ├── dropfiles_spec.js │ │ │ └── file_spec.js │ │ └── index.js │ ├── src │ │ ├── index.js │ │ ├── dragover.js │ │ ├── _get-filereader-method-name.js │ │ ├── file.js │ │ ├── _read-files.js │ │ └── dropfiles.js │ ├── webpack.config.js │ ├── .gitignore │ ├── package.json │ ├── package-lock.json │ └── LICENSE ├── codemirror-binder │ ├── babel.config.js │ ├── .gitignore │ ├── test │ │ ├── index.js │ │ └── spec │ │ │ └── common_spec.js │ ├── webpack.config.js │ ├── src.js │ ├── LICENSE │ ├── package.json │ ├── README.md │ └── package-lock.json ├── common-binders │ ├── babel.config.js │ ├── src │ │ ├── attr.js │ │ ├── text.js │ │ ├── style.js │ │ ├── index.js │ │ ├── classname.js │ │ ├── prop.js │ │ ├── html.js │ │ ├── display.js │ │ ├── dataset.js │ │ ├── _classlist.js │ │ └── existence.js │ ├── test │ │ ├── index.js │ │ └── spec │ │ │ └── existence_binder_spec.js │ ├── package-lock.json │ ├── webpack.config.js │ ├── package.json │ ├── .gitignore │ └── LICENSE └── react │ ├── babel.config.js │ ├── src │ ├── Context.ts │ ├── index.ts │ ├── useStore.ts │ ├── useSet.ts │ ├── .eslintrc.js │ ├── getStoreSlice.ts │ ├── useTrigger.ts │ ├── useOn.ts │ └── useChange.ts │ ├── test │ ├── spec │ │ ├── getWrapper.js │ │ ├── createspy.js │ │ ├── useStore.spec.js │ │ ├── useSet.spec.js │ │ ├── useChange.spec.js │ │ ├── useOn.spec.js │ │ └── useTrigger.spec.js │ └── index.js │ ├── tsconfig.json │ ├── .gitignore │ ├── LICENSE │ └── package.json ├── lerna.json ├── .github ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── .gitignore ├── .travis.yml ├── .eslintignore ├── .editorconfig ├── babel.config.js ├── test └── post-publish │ ├── README.md │ ├── package.json │ └── post-publish.js ├── .eslintrc.js └── LICENSE /packages/defi/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../babel.config.js'); 2 | -------------------------------------------------------------------------------- /packages/router/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../babel.config.js'); 2 | -------------------------------------------------------------------------------- /packages/defi/src/_core/defs.js: -------------------------------------------------------------------------------- 1 | // object definitions 2 | export default new WeakMap(); 3 | -------------------------------------------------------------------------------- /packages/file-binders/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../babel.config.js'); 2 | -------------------------------------------------------------------------------- /packages/router/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .directory 4 | coverage 5 | npm 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "1.3.8" 6 | } 7 | -------------------------------------------------------------------------------- /packages/codemirror-binder/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../babel.config.js'); 2 | -------------------------------------------------------------------------------- /packages/common-binders/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../babel.config.js'); 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/codemirror-binder/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .directory 4 | coverage 5 | .coveralls.yml 6 | npm 7 | bundle 8 | /npm 9 | -------------------------------------------------------------------------------- /packages/defi/src/on/_splitbyspaceregexp.js: -------------------------------------------------------------------------------- 1 | // allows to split by spaces not inclusing ones inside of brackers 2 | export default /\s+(?![^(]*\))/g; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .directory 4 | coverage 5 | .coveralls.yml 6 | bundle 7 | npm 8 | .npmrc 9 | .eslintcache 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '14' 4 | branches: 5 | except: 6 | - /^v\d+\.\d+\.\d+/ 7 | - /^v\d+\.\d+\.\d+-bundle$/ 8 | - gh-pages 9 | -------------------------------------------------------------------------------- /packages/defi/test/browser-test/jasmine-2.4.1/jasmine_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finom/defi/HEAD/packages/defi/test/browser-test/jasmine-2.4.1/jasmine_favicon.png -------------------------------------------------------------------------------- /packages/react/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 3 | plugins: ['@babel/plugin-transform-runtime'], 4 | }; 5 | -------------------------------------------------------------------------------- /packages/defi/README.md: -------------------------------------------------------------------------------- 1 | # defi.js 2 | 3 | This is a README placeholder. An actual README which is also going to be published at NPM is placed [in the root of the project](../../README.md). 4 | -------------------------------------------------------------------------------- /packages/defi/src/binders/progress.js: -------------------------------------------------------------------------------- 1 | import input from './input'; 2 | 3 | // returns a binder for textarea element 4 | export default function progress() { 5 | return input(); 6 | } 7 | -------------------------------------------------------------------------------- /packages/defi/src/on/_domeventregexp.js: -------------------------------------------------------------------------------- 1 | // the regexp allows to parse things like "click::x(.y)" 2 | // it's shared between few modules 3 | export default /([^::]+)::([^()]+)?(?:\((.*)\))?/; 4 | -------------------------------------------------------------------------------- /packages/react/src/Context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const Context = createContext(null as { [key: string]: unknown }); 4 | export const { Provider } = Context; 5 | export default Context; 6 | -------------------------------------------------------------------------------- /packages/react/test/spec/getWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from '../../npm'; 3 | 4 | export default (value) => ({ children }) => React.createElement(Provider, { value }, children); 5 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/foreach.js: -------------------------------------------------------------------------------- 1 | export default function forEach(arr, callback) { 2 | let i = 0; 3 | const l = arr.length; 4 | 5 | for (; i < l; i++) { 6 | callback(arr[i], i); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/router/src/index.js: -------------------------------------------------------------------------------- 1 | const Router = require('./router'); 2 | 3 | function router(obj, route, type) { 4 | Router[type || 'hash'].subscribe(obj, route); 5 | return obj; 6 | } 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "noImplicitAny": true, 5 | "module": "commonjs", 6 | "target": "es6", 7 | "jsx": "react" 8 | } 9 | } -------------------------------------------------------------------------------- /packages/react/test/spec/createspy.js: -------------------------------------------------------------------------------- 1 | export default function createSpy(spy = () => {}) { 2 | const spyName = 'function'; 3 | const spyObj = {}; 4 | spyObj[spyName] = spy; 5 | return spyOn(spyObj, spyName).and.callThrough(); 6 | } 7 | -------------------------------------------------------------------------------- /packages/defi/src/binders/textarea.js: -------------------------------------------------------------------------------- 1 | import input from './input'; 2 | 3 | // returns a binder for textarea element 4 | export default function textarea() { 5 | // textarea behaves just like text input 6 | return input('text'); 7 | } 8 | -------------------------------------------------------------------------------- /packages/react/test/index.js: -------------------------------------------------------------------------------- 1 | const Jasmine = require('jasmine'); 2 | 3 | const jasmine = new Jasmine(); 4 | 5 | jasmine.loadConfig({ 6 | spec_dir: 'test', 7 | spec_files: ['spec/*.spec.js'], 8 | }); 9 | 10 | jasmine.execute(); 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | bundle/**/*.js 2 | coverage/**/*.js 3 | packages/defi/npm/**/*.js 4 | packages/*/coverage/**/*.js 5 | packages/*/npm/**/*.js 6 | packages/defi/test/browser-test/**/*.js 7 | packages/defi/test/karma-test/vendor-dom-libraries/**/*.js 8 | -------------------------------------------------------------------------------- /packages/defi/test/helpers/createspy.js: -------------------------------------------------------------------------------- 1 | export default function createSpy(spy = () => {}) { 2 | const spyName = 'function'; 3 | const spyObj = {}; 4 | spyObj[spyName] = spy; 5 | return spyOn(spyObj, spyName).and.callThrough(); 6 | } 7 | -------------------------------------------------------------------------------- /packages/file-binders/test/spec/createspy.js: -------------------------------------------------------------------------------- 1 | export default function createSpy(spy = () => {}) { 2 | const spyName = 'function'; 3 | const spyObj = {}; 4 | spyObj[spyName] = spy; 5 | return spyOn(spyObj, spyName).and.callThrough(); 6 | } 7 | -------------------------------------------------------------------------------- /packages/defi/src/_mq/_data.js: -------------------------------------------------------------------------------- 1 | // an object allows to share data between modules; it's needed because we use 2 | // simplified ES modules there and cannot import and share a number 3 | export default { 4 | nodeIndex: 0, 5 | allEvents: {} 6 | }; 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /packages/defi/src/_mq/parsehtml.js: -------------------------------------------------------------------------------- 1 | import html2nodeList from './_html2nodelist'; 2 | import Init from './_init'; 3 | 4 | // parses given HTML and returns mq instance 5 | export default function parseHTML(html) { 6 | return new Init(html2nodeList(html)); 7 | } 8 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/map.js: -------------------------------------------------------------------------------- 1 | export default function map(arr, callback) { 2 | let i = 0; 3 | const l = arr.length; 4 | const result = []; 5 | 6 | for (; i < l; i++) { 7 | result.push(callback(arr[i], i)); 8 | } 9 | return result; 10 | } 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { modules: false }, 6 | ], 7 | ], 8 | plugins: [ 9 | 'babel-plugin-transform-es2015-modules-simple-commonjs', '@babel/plugin-transform-runtime', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/file-binders/src/index.js: -------------------------------------------------------------------------------- 1 | const file = require('./file'); 2 | const dropFiles = require('./dropfiles'); 3 | const dragOver = require('./dragover'); 4 | 5 | // export these binders in CJS environment 6 | module.exports = { 7 | file, 8 | dropFiles, 9 | dragOver, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/is.js: -------------------------------------------------------------------------------- 1 | // determines whether two values are the same value 2 | /* istanbul ignore next */ 3 | const isPolyfill = (v1, v2) => v1 === 0 && v2 === 0 ? 1 / v1 === 1 / v2 : v1 !== v1 && v2 !== v2 || v1 === v2; // eslint-disable-line 4 | 5 | export default Object.is || isPolyfill; 6 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/forown.js: -------------------------------------------------------------------------------- 1 | export default function forOwn(obj, callback) { 2 | const keys = Object.keys(obj); 3 | const l = keys.length; 4 | let i = 0; 5 | let key; 6 | 7 | while (i < l) { 8 | key = keys[i++]; 9 | callback(obj[key], key); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/assign.js: -------------------------------------------------------------------------------- 1 | export default function assign(target, source) { 2 | const keys = Object.keys(source); 3 | let i = keys.length; 4 | let key; 5 | 6 | while (--i >= 0) { 7 | key = keys[i]; 8 | target[key] = source[key]; 9 | } 10 | 11 | return target; 12 | } 13 | -------------------------------------------------------------------------------- /packages/defi/src/binders/index.js: -------------------------------------------------------------------------------- 1 | import input from './input'; 2 | import output from './output'; 3 | import textarea from './textarea'; 4 | import select from './select'; 5 | import progress from './progress'; 6 | 7 | export { 8 | input, 9 | output, 10 | textarea, 11 | select, 12 | progress 13 | }; 14 | -------------------------------------------------------------------------------- /packages/defi/src/index.js: -------------------------------------------------------------------------------- 1 | import * as functions from './_lib'; 2 | 3 | import lookForBinder from './lookforbinder'; 4 | import chain from './chain'; 5 | import defaultBinders from './defaultbinders'; 6 | 7 | export default ({ 8 | ...functions, 9 | lookForBinder, 10 | chain, 11 | defaultBinders 12 | }); 13 | -------------------------------------------------------------------------------- /test/post-publish/README.md: -------------------------------------------------------------------------------- 1 | The folder contains a test which only checks if all packages correctly deployed to NPM. More specifically, if we didn't mess up with exporting (so you don't need to use `require('foo').default`) and if we've published `/npm` folder instead of package's root. Tests for libraries contain at `/packages/*/test`. 2 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Context, Provider } from './Context'; 2 | export { default as useChange } from './useChange'; 3 | export { default as useStore } from './useStore'; 4 | export { default as useTrigger } from './useTrigger'; 5 | export { default as useOn } from './useOn'; 6 | export { default as useSet } from './useSet'; 7 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/slice.js: -------------------------------------------------------------------------------- 1 | export default function slice(arrLike, start, end) { 2 | const l = arrLike.length; 3 | let i = start || 0; 4 | const _end = end || l; 5 | const arr = Array(_end - i); 6 | let j = 0; 7 | 8 | while (i < _end) { 9 | arr[j++] = arrLike[i++]; 10 | } 11 | 12 | return arr; 13 | } 14 | -------------------------------------------------------------------------------- /packages/defi/test/helpers/simulateclick.js: -------------------------------------------------------------------------------- 1 | // simulates click on a node 2 | export default function simulateClick(node) { 3 | const evt = window.document.createEvent('MouseEvent'); 4 | evt.initMouseEvent( 5 | 'click', true, true, window, 0, 0, 0, 0, 0, 6 | false, false, false, false, 0, null 7 | ); 8 | node.dispatchEvent(evt); 9 | } 10 | -------------------------------------------------------------------------------- /packages/file-binders/src/dragover.js: -------------------------------------------------------------------------------- 1 | module.exports = function dragOver() { 2 | return { 3 | on: 'dragover dragenter dragleave dragend drop', 4 | getValue({ domEvent }) { 5 | const eventType = domEvent && domEvent.type; 6 | 7 | return eventType === 'dragover' || eventType === 'dragenter'; 8 | }, 9 | setValue: null, 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/reduce.js: -------------------------------------------------------------------------------- 1 | export default function reduce(arr, callback, initialize) { 2 | let i = 1; 3 | const l = arr.length; 4 | let val = initialize; 5 | const result = []; 6 | if (initialize) { 7 | val = callback(initialize, arr[0], 0); 8 | } 9 | for (; i < l; i++) { 10 | val = callback(val, arr[i], i); 11 | } 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /packages/defi/src/lookforbinder.js: -------------------------------------------------------------------------------- 1 | import defaultBinders from './defaultbinders'; 2 | 3 | // tries to find a binder for given node 4 | export default function lookForBinder(node) { 5 | for (let i = 0; i < defaultBinders.length; i++) { 6 | const binder = defaultBinders[i].call(node, node); 7 | if (binder) { 8 | return binder; 9 | } 10 | } 11 | 12 | return undefined; 13 | } 14 | -------------------------------------------------------------------------------- /test/post-publish/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seemple-post-publish", 3 | "dependencies": { 4 | "defi": "*", 5 | "defi-router": "*", 6 | "defi-react": "*", 7 | "codemirror-binder": "*", 8 | "common-binders": "*", 9 | "file-binders": "*", 10 | "codemirror": "*", 11 | "react": "*" 12 | }, 13 | "devDependencies": { 14 | "expect.js": "^0.3.1", 15 | "jsdom": "^16.2.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | extends: [ 4 | 'airbnb-base', 5 | ], 6 | rules: { 7 | indent: ['error', 2, { SwitchCase: 1 }], 8 | 'no-var': 'error', 9 | 'import/no-extraneous-dependencies': 0, 10 | 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], 11 | }, 12 | env: { 13 | jasmine: true, 14 | }, 15 | globals: { 16 | window: true, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/common-binders/src/attr.js: -------------------------------------------------------------------------------- 1 | // returns a binder for element attribute 2 | export default function attr(attributeName, mappingFn) { 3 | return { 4 | on: null, 5 | getValue() { 6 | return this.getAttribute(attributeName); 7 | }, 8 | setValue(value) { 9 | const val = typeof mappingFn === 'function' ? mappingFn(value) : value; 10 | this.setAttribute(attributeName, val); 11 | }, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/defi/test/helpers/deepfind.js: -------------------------------------------------------------------------------- 1 | export default function deepFind(obj, path) { 2 | const paths = typeof path === 'string' ? path.split('.') : path; 3 | let current = obj; 4 | 5 | for (let i = 0; i < paths.length; ++i) { 6 | if (typeof current[paths[i]] === 'undefined') { 7 | return undefined; 8 | } 9 | 10 | current = current[paths[i]]; 11 | } 12 | 13 | return current; 14 | } 15 | -------------------------------------------------------------------------------- /packages/defi/src/binders/output.js: -------------------------------------------------------------------------------- 1 | // returns a binder for output element 2 | export default function output() { 3 | return { 4 | on: null, 5 | getValue() { 6 | return this.value || this.textContent; 7 | }, 8 | setValue(value) { 9 | const property = 'form' in this ? 'value' : 'textContent'; 10 | this[property] = value === null ? '' : `${value}`; 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/common-binders/src/text.js: -------------------------------------------------------------------------------- 1 | // returns a binder for textContent of an element 2 | export default function text(mappingFn) { 3 | return { 4 | on: 'input', // the event name fires only in contenteditable mode 5 | getValue() { 6 | return this.textContent; 7 | }, 8 | setValue(value) { 9 | const val = typeof mappingFn === 'function' ? mappingFn(value) : value; 10 | this.textContent = `${val}`; 11 | }, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/common-binders/test/index.js: -------------------------------------------------------------------------------- 1 | const Jasmine = require('jasmine'); 2 | const { JSDOM } = require('jsdom'); 3 | 4 | const jasmine = new Jasmine(); 5 | 6 | global.window = new JSDOM('', { 7 | url: 'http://localhost', 8 | }).window; 9 | 10 | global.document = global.window.document; 11 | 12 | jasmine.loadConfig({ 13 | spec_dir: 'test', 14 | spec_files: ['spec/*_spec.js'], 15 | }); 16 | 17 | jasmine.execute(); 18 | -------------------------------------------------------------------------------- /packages/file-binders/test/index.js: -------------------------------------------------------------------------------- 1 | const Jasmine = require('jasmine'); 2 | const { JSDOM } = require('jsdom'); 3 | 4 | const jasmine = new Jasmine(); 5 | 6 | global.window = new JSDOM('', { 7 | url: 'http://localhost', 8 | }).window; 9 | 10 | global.document = global.window.document; 11 | 12 | jasmine.loadConfig({ 13 | spec_dir: 'test', 14 | spec_files: ['spec/*_spec.js'], 15 | }); 16 | 17 | jasmine.execute(); 18 | -------------------------------------------------------------------------------- /packages/common-binders/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common-binders", 3 | "version": "1.3.8", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "defi": { 8 | "version": "1.3.7", 9 | "resolved": "https://registry.npmjs.org/defi/-/defi-1.3.7.tgz", 10 | "integrity": "sha512-VKKm4F6KTW6YHMt2nSQdJdrlTigE9dhFxhC5bXIoiYrtAc8UPKKe3GodcG8xwAoVJlbyW4iSkNn9lED9eY7DDQ==", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/defi/src/_mq/index.js: -------------------------------------------------------------------------------- 1 | import Init from './_init'; 2 | import parseHTML from './parsehtml'; 3 | import on from './on'; 4 | import off from './off'; 5 | import add from './add'; 6 | import assign from '../_helpers/assign'; 7 | 8 | // a tiny jQuery-like library 9 | export default function mq(selector, context) { 10 | return new Init(selector, context); 11 | } 12 | 13 | mq.parseHTML = parseHTML; 14 | 15 | assign(Init.prototype, { 16 | on, 17 | off, 18 | add 19 | }); 20 | -------------------------------------------------------------------------------- /packages/common-binders/src/style.js: -------------------------------------------------------------------------------- 1 | // returns a binder for style properties 2 | export default function style(property, mappingFn) { 3 | return { 4 | on: null, 5 | getValue() { 6 | return this.style[property] 7 | || window.getComputedStyle(this).getPropertyValue(property); 8 | }, 9 | setValue(value) { 10 | const val = typeof mappingFn === 'function' ? mappingFn(value) : value; 11 | this.style[property] = val; 12 | }, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/common-binders/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | devtool: 'source-map', 5 | entry: './src/index', 6 | output: { 7 | path: path.resolve(__dirname, '../../bundle'), 8 | filename: 'common-binders.min.js', 9 | libraryTarget: 'var', 10 | library: 'commonBinders', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | use: ['babel-loader'], 17 | }, 18 | ], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/file-binders/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: './src/index', 7 | output: { 8 | path: path.resolve(__dirname, '../../bundle'), 9 | filename: 'file-binders.min.js', 10 | libraryTarget: 'umd', 11 | library: 'fileBinders', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | use: ['babel-loader'], 18 | }, 19 | ], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/common-binders/src/index.js: -------------------------------------------------------------------------------- 1 | import html from './html'; 2 | import display from './display'; 3 | import className from './classname'; 4 | import prop from './prop'; 5 | import attr from './attr'; 6 | import text from './text'; 7 | import style from './style'; 8 | import dataset from './dataset'; 9 | import existence from './existence'; 10 | 11 | export { 12 | html, 13 | display, 14 | className, 15 | prop, 16 | attr, 17 | text, 18 | style, 19 | dataset, 20 | existence, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/checkobjecttype.js: -------------------------------------------------------------------------------- 1 | import defiError from './defierror'; 2 | 3 | // checks type of a variable and throws an error if its type is not an object 4 | export default function checkObjectType(object, method) { 5 | const typeofObject = object === null ? 'null' : typeof object; 6 | 7 | if (typeofObject !== 'object' && typeofObject !== 'function') { 8 | throw defiError('common:object_type', { 9 | object, 10 | method 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/react/src/useStore.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import Context from './Context'; 4 | 5 | export interface StoreSelector { 6 | (store: { [key: string]: unknown }): { [key: string]: unknown }; 7 | } 8 | 9 | 10 | export default function useStore(storeSelector?: StoreSelector): { [key: string]: unknown } { 11 | const store = useContext(Context); 12 | if (store.__ERROR__) throw store.__ERROR__; 13 | return typeof storeSelector === 'function' ? storeSelector(store) : store; 14 | } 15 | -------------------------------------------------------------------------------- /packages/defi/src/trigger/_triggeronedomevent.js: -------------------------------------------------------------------------------- 1 | // triggers given DOM event on given node 2 | export default function triggerOneDOMEvent({ 3 | node, 4 | eventName, 5 | triggerArgs 6 | }) { 7 | const { Event } = window; 8 | const event = new Event(eventName, { 9 | bubbles: true, 10 | cancelable: true 11 | }); 12 | 13 | // defiTriggerArgs will be used in a handler created by addDOMListener 14 | event.defiTriggerArgs = triggerArgs; 15 | 16 | node.dispatchEvent(event); 17 | } 18 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/deepfind.js: -------------------------------------------------------------------------------- 1 | // gets value of a property in nested object 2 | // eg "d" from a.b.c.d 3 | export default function deepFind(obj, givenPath) { 4 | const paths = typeof givenPath === 'string' ? givenPath.split('.') : givenPath; 5 | let current = obj; 6 | 7 | for (let i = 0; i < paths.length; ++i) { 8 | if (typeof current[paths[i]] === 'undefined') { 9 | return undefined; 10 | } 11 | 12 | current = current[paths[i]]; 13 | } 14 | 15 | return current; 16 | } 17 | -------------------------------------------------------------------------------- /packages/router/test/index.js: -------------------------------------------------------------------------------- 1 | const Jasmine = require('jasmine'); 2 | const { JSDOM } = require('jsdom'); 3 | const { SpecReporter } = require('jasmine-spec-reporter'); 4 | 5 | const jasmine = new Jasmine(); 6 | 7 | global.window = new JSDOM('', { 8 | url: 'http://localhost', 9 | }).window; 10 | 11 | jasmine.loadConfig({ 12 | random: false, 13 | spec_dir: 'test/spec', 14 | spec_files: [ 15 | '**/*_spec.js', 16 | ], 17 | }); 18 | 19 | jasmine.addReporter(new SpecReporter()); 20 | 21 | jasmine.execute(); 22 | -------------------------------------------------------------------------------- /packages/defi/test/helpers/makeobject.js: -------------------------------------------------------------------------------- 1 | // creates nested object based on path and lastValue 2 | // example: makeObject('a.b.c', 42) -> {a: {b: {c; 42}}} 3 | export default function makeObject(givenPath = '', lastValue = {}) { 4 | const path = givenPath ? givenPath.split('.') : []; 5 | const result = {}; 6 | let obj = result; 7 | let key; 8 | 9 | while (path.length > 1) { 10 | key = path.shift(); 11 | obj = obj[key] = { myNameIs: key }; 12 | } 13 | 14 | obj[path.shift()] = lastValue; 15 | 16 | return result; 17 | } 18 | -------------------------------------------------------------------------------- /packages/file-binders/src/_get-filereader-method-name.js: -------------------------------------------------------------------------------- 1 | module.exports = function getFileReaderMethodName(readAs) { 2 | const { FileReader } = window; 3 | /* istanbul ignore if */ 4 | if (typeof FileReader === 'undefined') { 5 | throw Error('FileReader is not supported by this browser'); 6 | } 7 | 8 | const methodName = `readAs${readAs[0].toUpperCase()}${readAs.slice(1)}`; 9 | 10 | if (!FileReader.prototype[methodName]) { 11 | throw Error(`Method ${methodName} is not supported by FileReader`); 12 | } 13 | 14 | return methodName; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/router/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | devtool: 'source-map', 5 | entry: './src/index', 6 | optimization: { minimize: true }, 7 | output: { 8 | path: path.resolve(__dirname, '../../bundle'), 9 | filename: 'defi-router.min.js', 10 | libraryTarget: 'var', 11 | library: 'defiRouter', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | use: ['babel-loader'], 18 | }, 19 | ], 20 | }, 21 | externals: { 22 | defi: 'defi', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/common-binders/src/classname.js: -------------------------------------------------------------------------------- 1 | import { 2 | toggle, 3 | contains, 4 | } from './_classlist'; 5 | 6 | // returns a binder for className of an element 7 | // switcher makes possible to turn property value 8 | export default function className(elementClassName, switcher = true) { 9 | return { 10 | on: null, 11 | getValue() { 12 | const value = contains(this, elementClassName); 13 | return switcher ? value : !value; 14 | }, 15 | setValue(value) { 16 | toggle(this, elementClassName, switcher ? !!value : !value); 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/common-binders/src/prop.js: -------------------------------------------------------------------------------- 1 | // returns a binder to change properties of an element 2 | export default function prop(propertyName, mappingFn) { 3 | return { 4 | on: null, 5 | getValue() { 6 | return this[propertyName]; 7 | }, 8 | setValue(value) { 9 | const val = typeof mappingFn === 'function' ? mappingFn(value) : value; 10 | // in case when you're trying to set read-only property 11 | try { 12 | this[propertyName] = val; 13 | } catch (e) { 14 | // cannot set given property (eg tagName) 15 | } 16 | }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/common-binders/src/html.js: -------------------------------------------------------------------------------- 1 | // returns a binder for innerHTML of an element 2 | export default function html(mappingFn) { 3 | return { 4 | on: 'input', // the event name fires only in contenteditable mode 5 | getValue() { 6 | return this.innerHTML; 7 | }, 8 | setValue(value) { 9 | const val = typeof mappingFn === 'function' ? mappingFn(value) : value; 10 | if (val instanceof window.HTMLElement) { 11 | this.innerHTML = ''; 12 | this.appendChild(val); 13 | } else { 14 | this.innerHTML = `${val}`; 15 | } 16 | }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/defi/test/spec/common_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import defi from 'src/index'; 3 | 4 | describe('common tests', () => { 5 | it('includes all documented members', () => { 6 | [ 7 | 'bindNode', 'bound', 'calc', 'chain', 'lookForBinder', 'set', 8 | 'mediate', 'off', 'on', 'remove', 'trigger', 'unbindNode' 9 | ].forEach((methodName) => { 10 | expect(typeof defi[methodName]).toEqual('function'); 11 | }); 12 | 13 | expect(typeof defi.defaultBinders[0]).toEqual('function'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/defi/src/_lib.js: -------------------------------------------------------------------------------- 1 | import on from './on'; 2 | import off from './off'; 3 | import trigger from './trigger'; 4 | import calc from './calc'; 5 | import bindNode from './bindnode'; 6 | import bound from './bound'; 7 | import unbindNode from './unbindnode'; 8 | import set from './set'; 9 | import remove from './remove'; 10 | import mediate from './mediate'; 11 | 12 | 13 | // the following methods can be used as static methods and as instance methods 14 | export { 15 | on, 16 | off, 17 | trigger, 18 | calc, 19 | bindNode, 20 | bound, 21 | unbindNode, 22 | set, 23 | remove, 24 | mediate 25 | }; 26 | -------------------------------------------------------------------------------- /packages/defi/test/browser-test/SpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jasmine Spec Runner v2.4.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/react/src/useSet.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | // @ts-ignore 3 | import { set } from 'defi'; 4 | import getStoreSlice from './getStoreSlice'; 5 | 6 | export interface StoreSelector { 7 | (store: { [key: string]: unknown }): { [key: string]: unknown }; 8 | } 9 | 10 | export default function useSet( 11 | storeSlice: { [key: string]: unknown } | StoreSelector, 12 | key: string, 13 | options?: { [key: string]: unknown }, 14 | ): (value: any) => void { 15 | const slice = getStoreSlice(storeSlice); 16 | 17 | const setValue = useCallback((val) => set(slice, key, val, options), []); 18 | 19 | return setValue; 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: [ 4 | '@typescript-eslint', 5 | ], 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/eslint-recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | rules: { 12 | '@typescript-eslint/ban-ts-comment': 0, 13 | '@typescript-eslint/no-explicit-any': 0, 14 | 'no-underscore-dangle': 0, 15 | 'prefer-destructuring': 0, 16 | 'import/extensions': 0, 17 | }, 18 | settings: { 19 | 'import/resolver': { 20 | typescript: {}, // this loads /tsconfig.json to eslint 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/defi/test/spec/mq/add_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import $ from 'src/_mq'; 3 | 4 | describe('mq.fn.add', () => { 5 | it('adds once', () => { 6 | const el1 = window.document.createElement('div'); 7 | const el2 = window.document.createElement('div'); 8 | const el3 = window.document.createElement('div'); 9 | const el4 = window.document.createElement('div'); 10 | const el5 = window.document.createElement('div'); 11 | const result = Array.from($([el1, el2, el3]).add([el2, el3, el4, el5])); 12 | 13 | expect(result).toEqual([el1, el2, el3, el4, el5]); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/defi/test/node-test/jasmine.js: -------------------------------------------------------------------------------- 1 | const Jasmine = require('jasmine'); 2 | const path = require('path'); 3 | const { JSDOM } = require('jsdom'); 4 | const appModulePath = require('app-module-path'); 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | const jasmine = new Jasmine(); 8 | 9 | global.window = new JSDOM('', { 10 | url: 'http://localhost' 11 | }).window; 12 | 13 | appModulePath.addPath(path.resolve(__dirname, '../..')); 14 | 15 | jasmine.loadConfig({ 16 | spec_dir: 'test/spec', 17 | spec_files: [ 18 | '**/*_spec.js' 19 | ] 20 | }); 21 | 22 | jasmine.addReporter(new SpecReporter()); 23 | 24 | jasmine.execute(); 25 | -------------------------------------------------------------------------------- /packages/defi/src/bindnode/_getnodes.js: -------------------------------------------------------------------------------- 1 | import selectNodes from './_selectnodes'; 2 | import $ from '../_mq'; 3 | 4 | const htmlReg = / { 5 | it('parses HTML', () => { 6 | const result = $.parseHTML('
'); 7 | 8 | expect(result.length).toEqual(2); 9 | expect(result[0].tagName).toEqual('DIV'); 10 | expect(result[1].tagName).toEqual('SPAN'); 11 | }); 12 | 13 | it('parses contextual elements', () => { 14 | const result = $.parseHTML(''); 15 | 16 | expect(result.length).toEqual(2); 17 | expect(result[0].tagName).toEqual('TD'); 18 | expect(result[1].tagName).toEqual('TD'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/codemirror-binder/test/index.js: -------------------------------------------------------------------------------- 1 | const Jasmine = require('jasmine'); 2 | const { JSDOM } = require('jsdom'); 3 | 4 | const jasmine = new Jasmine(); 5 | 6 | global.window = new JSDOM('', { 7 | url: 'http://localhost', 8 | }).window; 9 | 10 | global.document = global.window.document; 11 | 12 | global.navigator = global.window.navigator; 13 | 14 | global.document.createRange = () => ({ 15 | setEnd() {}, 16 | setStart() {}, 17 | getBoundingClientRect() { 18 | return { right: 0 }; 19 | }, 20 | getClientRects() { 21 | return { right: 0 }; 22 | }, 23 | }); 24 | 25 | jasmine.loadConfig({ 26 | spec_dir: 'test/spec', 27 | spec_files: [ 28 | '**/**_spec.js', 29 | ], 30 | }); 31 | 32 | jasmine.execute(); 33 | -------------------------------------------------------------------------------- /packages/defi/test/index.js: -------------------------------------------------------------------------------- 1 | // This gets replaced by karma webpack with the updated files on rebuild 2 | const __karmaWebpackManifest__ = []; 3 | 4 | // require all modules from the 5 | // current directory and all subdirectories 6 | const testsContext = require.context('./spec/', true, /.*\.js$/); 7 | 8 | function inManifest(path) { 9 | return __karmaWebpackManifest__.indexOf(path) >= 0; 10 | } 11 | 12 | let runnable = testsContext.keys().filter(inManifest); 13 | 14 | // Run all tests if we didn't find any changes 15 | if (!runnable.length) { 16 | runnable = testsContext.keys(); 17 | } 18 | 19 | runnable.forEach(testsContext); 20 | 21 | const componentsContext = require.context('../src/', true, /.*\.js$/); 22 | componentsContext.keys().forEach(componentsContext); 23 | -------------------------------------------------------------------------------- /packages/common-binders/src/display.js: -------------------------------------------------------------------------------- 1 | // returns a binder to switch visibility of an element 2 | export default function display(switcher = true) { 3 | return { 4 | on: null, 5 | getValue() { 6 | const value = this.style.display 7 | || window.getComputedStyle(this).getPropertyValue('display'); 8 | const none = value === 'none'; 9 | return switcher ? !none : none; 10 | }, 11 | setValue(value) { 12 | const { style } = this; 13 | if (typeof switcher === 'function') { 14 | style.display = switcher(value) ? '' : 'none'; 15 | } else if (switcher) { 16 | style.display = value ? '' : 'none'; 17 | } else { 18 | style.display = value ? 'none' : ''; 19 | } 20 | }, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/defi/src/defaultbinders.js: -------------------------------------------------------------------------------- 1 | import input from './binders/input'; 2 | import textarea from './binders/textarea'; 3 | import select from './binders/select'; 4 | import progress from './binders/progress'; 5 | import output from './binders/output'; 6 | 7 | // defaultBinders collection by default contains only one function-checker 8 | export default [(node) => { 9 | switch (node.tagName) { 10 | case 'INPUT': 11 | return input(node.type); 12 | case 'TEXTAREA': 13 | return textarea(); 14 | case 'SELECT': 15 | return select(node.multiple); 16 | case 'PROGRESS': 17 | return progress(); 18 | case 'OUTPUT': 19 | return output(); 20 | default: 21 | return null; 22 | } 23 | }]; 24 | -------------------------------------------------------------------------------- /packages/defi/src/bound.js: -------------------------------------------------------------------------------- 1 | import defs from './_core/defs'; 2 | import checkObjectType from './_helpers/checkobjecttype'; 3 | 4 | // the function returns bound node(s) 5 | export default function bound(object, key, { all } = { all: false }) { 6 | // throw error when object type is wrong 7 | checkObjectType(object, 'bound'); 8 | 9 | // if no key or falsy key is given 10 | if (!key) { 11 | return all ? [] : null; 12 | } 13 | 14 | const def = defs.get(object); 15 | const propDef = def.props[key]; 16 | 17 | let nodes; 18 | 19 | if (propDef) { 20 | const { bindings } = propDef; 21 | nodes = (bindings && bindings.map(({ node }) => node)) || []; 22 | } else { 23 | nodes = []; 24 | } 25 | 26 | return all ? nodes : nodes[0] || null; 27 | } 28 | -------------------------------------------------------------------------------- /packages/defi/test/spec/set_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import set from 'src/set'; 3 | 4 | describe('set', () => { 5 | it('throws an error if an object is null', () => { 6 | expect(() => { 7 | set(null, 'x', 1); 8 | }).toThrow(); 9 | }); 10 | 11 | it('sets', () => { 12 | const obj = {}; 13 | set(obj, 'x', 42); 14 | expect(obj.x).toEqual(42); 15 | 16 | set(obj, { 17 | y: 1, 18 | z: 2 19 | }); 20 | expect(obj.y).toEqual(1); 21 | expect(obj.z).toEqual(2); 22 | }); 23 | 24 | it('does not throw if key is falsy', () => { 25 | expect(() => { 26 | set({}, null, 1); 27 | }).not.toThrow(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/codemirror-binder/webpack.config.js: -------------------------------------------------------------------------------- 1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: './src', 7 | externals: { 8 | codemirror: { 9 | root: 'CodeMirror', 10 | commonjs2: 'codemirror', 11 | commonjs: 'codemirror', 12 | amd: 'codemirror', 13 | }, 14 | }, 15 | output: { 16 | path: path.resolve(__dirname, '../../bundle'), 17 | filename: 'codemirror-binder.min.js', 18 | libraryTarget: 'var', 19 | library: 'codeMirrorBinder', 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | loaders: ['babel-loader'], 26 | }, 27 | ], 28 | }, 29 | 30 | plugins: [ 31 | new UglifyJsPlugin(), 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /packages/common-binders/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common-binders", 3 | "version": "1.3.8", 4 | "scripts": { 5 | "test": "../../node_modules/.bin/babel-node ../../node_modules/.bin/babel-istanbul cover test/index.js", 6 | "npm-compile": "babel src -d npm && cp package.json npm/package.json && cp README.md npm/README.md", 7 | "build": "webpack --mode=production" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/finom/defi.git" 12 | }, 13 | "author": "Andrey Gubanov", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/finom/defi/issues" 17 | }, 18 | "homepage": "https://github.com/finom/defi#readme", 19 | "devDependencies": { 20 | "defi": "^1.3.8" 21 | }, 22 | "gitHead": "5d73b7d6892730283893fe296dea35cdef74f461" 23 | } 24 | -------------------------------------------------------------------------------- /packages/defi/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "defi", 3 | "version": "1.3.8", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/runtime": { 8 | "version": "7.10.2", 9 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz", 10 | "integrity": "sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==", 11 | "requires": { 12 | "regenerator-runtime": "^0.13.4" 13 | } 14 | }, 15 | "regenerator-runtime": { 16 | "version": "0.13.5", 17 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", 18 | "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/defi/src/off/_removetreelistener.js: -------------------------------------------------------------------------------- 1 | import undelegateListener from './_undelegatelistener'; 2 | 3 | // removes tree listener from all object tree of fiven path 4 | export default function removeTreeListener(object, deepPath, handler) { 5 | if (typeof deepPath === 'string') { 6 | deepPath = deepPath.split('.'); // eslint-disable-line no-param-reassign 7 | } 8 | 9 | // iterate over keys of the path and undelegate given handler (can be undefined) 10 | for (let i = 0; i < deepPath.length; i++) { 11 | // TODO: Array.prototype.slice is slow 12 | const listenedPath = deepPath.slice(0, i); 13 | 14 | undelegateListener( 15 | object, 16 | listenedPath, 17 | `_change:tree:${deepPath[i]}`, 18 | handler 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | /bundle 40 | /npm 41 | !/.eslintrc.js 42 | !/webpack.config.js 43 | -------------------------------------------------------------------------------- /packages/common-binders/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | /bundle 40 | /npm 41 | !/.eslintrc.js 42 | !/webpack.config.js 43 | -------------------------------------------------------------------------------- /packages/file-binders/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | /bundle 40 | /npm 41 | !/.eslintrc.js 42 | !/webpack.config.js 43 | -------------------------------------------------------------------------------- /packages/react/src/getStoreSlice.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import Context from './Context'; 3 | 4 | export interface StoreSelector { 5 | (store: { [key: string]: unknown }): { [key: string]: unknown }; 6 | } 7 | 8 | export default ( 9 | storeSlice: { [key: string]: unknown } | StoreSelector, 10 | ): { [key: string]: unknown } => { 11 | const contextValue = useContext(Context); 12 | let slice; 13 | 14 | if (!storeSlice) { 15 | throw new Error('storeSlice argument is required'); 16 | } 17 | 18 | if (typeof storeSlice === 'function') { 19 | slice = storeSlice(contextValue); 20 | if (slice === null || typeof slice !== 'object') { 21 | throw new Error('storeSlice selector returned non-object value'); 22 | } 23 | } else { 24 | slice = storeSlice; 25 | } 26 | 27 | return slice; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/defi/tools/generate-package.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const sourcePackage = require('../package'); 4 | 5 | const npmPackage = {}; 6 | 7 | for (const key of [ 8 | 'name', 9 | 'version', 10 | 'author', 11 | 'repository', 12 | 'license', 13 | 'bugs', 14 | 'homepage', 15 | 'description' 16 | ]) { 17 | const value = sourcePackage[key]; 18 | if (!value) { 19 | throw Error(`"${key}" is not specified at package.json`); 20 | } 21 | 22 | npmPackage[key] = value; 23 | } 24 | 25 | console.log('generating package.json'); // eslint-disable-line no-console 26 | 27 | const npmPackageString = JSON.stringify(npmPackage, null, '\t'); 28 | 29 | fs.writeFileSync(path.resolve(__dirname, '../npm/package.json'), npmPackageString, { 30 | encoding: 'utf8' 31 | }); 32 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/debounce.js: -------------------------------------------------------------------------------- 1 | // Returns a function, that, as long as it continues to be invoked, will not 2 | // be triggered. The function will be called after it stops being called for 3 | // N milliseconds. 4 | // (c) https://davidwalsh.name/javascript-debounce-function 5 | 6 | export default function debounce(func, givenDelay, thisArg) { 7 | let timeout; 8 | let delay; 9 | if (typeof givenDelay !== 'number') { 10 | thisArg = givenDelay; // eslint-disable-line no-param-reassign 11 | delay = 0; 12 | } else { 13 | delay = givenDelay || 0; 14 | } 15 | 16 | return function debounced() { 17 | const args = arguments; 18 | const callContext = thisArg || this; 19 | 20 | clearTimeout(timeout); 21 | 22 | timeout = setTimeout(() => func.apply(callContext, args), delay); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/defi/test/spec/chain_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import chain from 'src/chain'; 3 | 4 | describe('chain', () => { 5 | it('has all needed methods', () => { 6 | const inst = chain({}); 7 | 8 | `on, 9 | off, 10 | trigger, 11 | calc, 12 | bindNode, 13 | unbindNode, 14 | set, 15 | bound, 16 | remove, 17 | mediate`.split(/\s*,\s*/) 18 | .forEach((name) => { 19 | expect(typeof inst[name]).toEqual('function'); 20 | }); 21 | }); 22 | 23 | it('can call calc and set as proof of chain work', () => { 24 | const obj = { a: 1 }; 25 | chain(obj) 26 | .calc('b', 'a', (a) => a * 2, { debounceCalc: false }) 27 | .set('a', 2); 28 | 29 | expect(obj.b).toEqual(4); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/react/test/spec/useStore.spec.js: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { useStore } from '../../npm'; 3 | import getWrapper from './getWrapper'; 4 | 5 | describe('useStore', () => { 6 | it('Should work', () => { 7 | const store = { x: 1 }; 8 | const wrapper = getWrapper(store); 9 | const { result } = renderHook(() => useStore(), { wrapper }); 10 | 11 | expect(result.current).toBe(store); 12 | }); 13 | 14 | it('Should use store selector', () => { 15 | const store = { x: { y: 1 } }; 16 | const wrapper = getWrapper(store); 17 | const { result } = renderHook(() => useStore(({ x }) => x), { wrapper }); 18 | 19 | expect(result.current).toBe(store.x); 20 | }); 21 | 22 | it('Should throw error if not wrapped by a provider', () => { 23 | const { result: { error } } = renderHook(() => useStore()); 24 | 25 | expect(error).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/common-binders/src/dataset.js: -------------------------------------------------------------------------------- 1 | // replace namesLikeThis with names-like-this 2 | const replacer = (u) => `-${u.toLowerCase()}`; 3 | const toDashed = (name) => `data-${name.replace(/([A-Z])/g, replacer)}`; 4 | 5 | // returns a binder for dataset of an element 6 | // old browsers are also supported @IE9 @IE10 7 | export default function dataset(prop, mappingFn) { 8 | return { 9 | on: null, 10 | getValue() { 11 | if (this.dataset) { 12 | return this.dataset[prop]; 13 | } 14 | 15 | /* istanbul ignore next */ 16 | return this.getAttribute(toDashed(prop)); 17 | }, 18 | setValue(value) { 19 | const val = typeof mappingFn === 'function' ? mappingFn(value) : value; 20 | 21 | if (this.dataset) { 22 | this.dataset[prop] = val; 23 | } else { 24 | /* istanbul ignore next */ 25 | this.setAttribute(toDashed(prop), val); 26 | } 27 | }, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/react/src/useTrigger.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback } from 'react'; 2 | // @ts-ignore 3 | import { on, off, trigger } from 'defi'; 4 | import getStoreSlice from './getStoreSlice'; 5 | 6 | export interface StoreSelector { 7 | (store: { [key: string]: unknown }): { [key: string]: unknown }; 8 | } 9 | 10 | export default function useTrigger( 11 | storeSlice: { [key: string]: unknown } | StoreSelector, 12 | eventName: string, 13 | ): (...args: any) => void { 14 | const slice = getStoreSlice(storeSlice); 15 | 16 | const fire = useCallback((...args: any[]) => { 17 | trigger(slice, eventName, ...args); 18 | }, []); 19 | 20 | useEffect(() => { 21 | const handler = (...args: any[]) => { 22 | // @ts-ignore 23 | fire.latest = args[0]; 24 | // @ts-ignore 25 | fire.latestAll = args; 26 | }; 27 | on(slice, eventName, handler); 28 | return () => { off(slice, eventName, handler); }; 29 | }); 30 | 31 | return fire; 32 | } 33 | -------------------------------------------------------------------------------- /packages/defi/src/_core/defineprop.js: -------------------------------------------------------------------------------- 1 | import defs from './defs'; 2 | import set from '../set'; 3 | 4 | // the function defines needed descriptor for given property 5 | export default function defineProp(object, key) { 6 | const def = defs.get(object); 7 | 8 | // if no object definition do nothing 9 | if (!def) { 10 | return null; 11 | } 12 | 13 | if (!def.props[key]) { 14 | const propDef = def.props[key] = { 15 | value: object[key], 16 | mediator: null, 17 | bindings: null 18 | }; 19 | 20 | Object.defineProperty(object, key, { 21 | configurable: true, 22 | enumerable: true, 23 | get() { 24 | return propDef.value; 25 | }, 26 | set(v) { 27 | return set(object, key, v, { 28 | fromSetter: true 29 | }); 30 | } 31 | }); 32 | } 33 | 34 | return def.props[key]; 35 | } 36 | -------------------------------------------------------------------------------- /packages/codemirror-binder/src.js: -------------------------------------------------------------------------------- 1 | const CodeMirror = require('codemirror'); 2 | 3 | function codeMirrorBinder(config) { 4 | let instance; 5 | let changeCallback; 6 | 7 | return { 8 | on(callback) { 9 | changeCallback = callback; 10 | instance.on('change', changeCallback); 11 | }, 12 | getValue() { 13 | instance.save(); 14 | return instance.getValue(); 15 | }, 16 | setValue(value) { 17 | instance.setValue(value); 18 | instance.save(); 19 | }, 20 | initialize() { 21 | /* istanbul ignore if */ 22 | if (!this.parentNode) { 23 | throw new Error('parentNode isn\'n found' 24 | + ' you need to insert textarea into the document before binder use'); 25 | } 26 | 27 | instance = CodeMirror.fromTextArea(this, config); 28 | }, 29 | destroy() { 30 | instance.off('change', changeCallback); 31 | instance.toTextArea(); 32 | }, 33 | }; 34 | } 35 | 36 | module.exports = codeMirrorBinder; 37 | -------------------------------------------------------------------------------- /packages/defi/src/on/_delegatelistener/changehandler.js: -------------------------------------------------------------------------------- 1 | import undelegateListener from '../../off/_undelegatelistener'; 2 | import triggerOne from '../../trigger/_triggerone'; 3 | 4 | // the function is called when some part of a path is changed 5 | // it delegates event listener for new branch of an object and undelegates it for old one 6 | // used for non-asterisk events 7 | export default function changeHandler({ 8 | previousValue, 9 | value 10 | }, { 11 | path, 12 | name, 13 | callback, 14 | info 15 | } = triggerOne.latestEvent.info.delegatedData) { 16 | if (value && typeof value === 'object') { 17 | const delegateListenerReq = require('.'); // fixing circular ref 18 | const delegateListener = delegateListenerReq.default || delegateListenerReq; 19 | delegateListener(value, path, name, callback, info); 20 | } 21 | 22 | if (previousValue && typeof previousValue === 'object') { 23 | undelegateListener(previousValue, path, name, callback, info); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/defi/src/bindnode/_createobjecthandler.js: -------------------------------------------------------------------------------- 1 | // returns a function which is called when property value is changed 2 | export default function createObjectHandler({ 3 | node, 4 | propDef, 5 | binder, 6 | bindingOptions 7 | }) { 8 | return function objectHandler(eventOptions = {}) { 9 | const { value } = propDef; 10 | const { onChangeValue, changedNode, binder: evtBinder } = eventOptions; 11 | const { setValue } = binder; 12 | // a dirty hack for https://github.com/matreshkajs/matreshka/issues/19 13 | const dirtyHackValue = onChangeValue === 'string' && typeof value === 'number' 14 | ? `${value}` : value; 15 | 16 | // don't call setValue if a property is changed via getValue of the same binder 17 | if (changedNode === node && onChangeValue === dirtyHackValue && evtBinder === binder) { 18 | return; 19 | } 20 | 21 | setValue.call(node, value, { 22 | value, 23 | ...bindingOptions 24 | }); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/react/src/useOn.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useCallback } from 'react'; 2 | // @ts-ignore 3 | import { on, off, trigger } from 'defi'; 4 | import getStoreSlice from './getStoreSlice'; 5 | 6 | export interface StoreSelector { 7 | (store: { [key: string]: unknown }): { [key: string]: unknown }; 8 | } 9 | 10 | 11 | export default function useOn( 12 | storeSlice: { [key: string]: unknown } | StoreSelector, 13 | eventName: string, 14 | ): (...args: any) => void { 15 | const slice = getStoreSlice(storeSlice); 16 | const [, forceRender] = useState(0); 17 | 18 | const fire = useCallback((...args: any[]) => trigger(slice, eventName, ...args), []); 19 | 20 | useEffect(() => { 21 | const handler = (...args: any[]) => { 22 | // @ts-ignore 23 | fire.latest = args[0]; 24 | // @ts-ignore 25 | fire.latestAll = args; 26 | 27 | forceRender((f) => f + 1); 28 | }; 29 | on(slice, eventName, handler); 30 | return () => { off(slice, eventName, handler); }; 31 | }); 32 | 33 | return fire; 34 | } 35 | -------------------------------------------------------------------------------- /packages/defi/src/_mq/add.js: -------------------------------------------------------------------------------- 1 | import Init from './_init'; 2 | import data from './_data'; 3 | 4 | // adds unique nodes to mq collection 5 | export default function add(selector) { 6 | const idMap = {}; 7 | 8 | let result; 9 | 10 | const nodes = new Init(selector); 11 | 12 | if (this.length) { 13 | result = new Init(); 14 | for (let i = 0; i < this.length; i++) { 15 | const node = this[i]; 16 | const nodeID = node.b$ = node.b$ || ++data.nodeIndex; // eslint-disable-line no-plusplus 17 | idMap[nodeID] = 1; 18 | result.push(node); 19 | } 20 | 21 | for (let i = 0; i < nodes.length; i++) { 22 | const node = nodes[i]; 23 | const nodeID = node.b$ = node.b$ || ++data.nodeIndex; // eslint-disable-line no-plusplus 24 | if (!idMap[nodeID]) { 25 | idMap[nodeID] = 1; 26 | result.push(node); 27 | } 28 | } 29 | } else { 30 | result = nodes; 31 | } 32 | 33 | return result; 34 | } 35 | -------------------------------------------------------------------------------- /packages/defi/webpack.config.js: -------------------------------------------------------------------------------- 1 | const UnminifiedWebpackPlugin = require('unminified-webpack-plugin'); 2 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 3 | const path = require('path'); 4 | const BannerAndFooterWebpackPlugin = require('./tools/banner-and-footer-webpack-plugin'); 5 | 6 | module.exports = { 7 | devtool: 'source-map', 8 | entry: './src/index', 9 | output: { 10 | path: path.resolve(__dirname, '../../bundle'), 11 | filename: 'defi.min.js', 12 | libraryTarget: 'var', 13 | library: 'defi' 14 | }, 15 | module: { 16 | rules: [{ 17 | test: /\.js$/, 18 | use: ['babel-loader'] 19 | }] 20 | }, 21 | plugins: [ 22 | new UnminifiedWebpackPlugin(), 23 | new BannerAndFooterWebpackPlugin(), 24 | new UglifyJSPlugin({ 25 | sourceMap: true, 26 | uglifyOptions: { 27 | // keep banner there 28 | comments: /------------------------------/ 29 | } 30 | }) 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /packages/file-binders/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-binders", 3 | "version": "1.2.0", 4 | "scripts": { 5 | "test": "npm run cover", 6 | "cover": "../../node_modules/.bin/babel-node ../../node_modules/.bin/babel-istanbul cover test/index.js", 7 | "unit": "../../node_modules/.bin/babel-node test/index.js", 8 | "npm-compile": "../../node_modules/.bin/babel src -d npm && cp package.json npm/package.json && cp README.md npm/README.md", 9 | "build": "../../node_modules/.bin/webpack --mode=production" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/finom/defi.git" 14 | }, 15 | "author": "Andrey Gubanov", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/finom/defi/issues" 19 | }, 20 | "homepage": "https://github.com/finom/defi/tree/master/packages/file-binders", 21 | "devDependencies": { 22 | "makeelement": "^0.1.0" 23 | }, 24 | "dependencies": { 25 | "@babel/runtime": "^7.10.2" 26 | }, 27 | "gitHead": "5d73b7d6892730283893fe296dea35cdef74f461" 28 | } 29 | -------------------------------------------------------------------------------- /packages/router/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "defi-router", 3 | "version": "1.3.8", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/runtime": { 8 | "version": "7.10.2", 9 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz", 10 | "integrity": "sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==", 11 | "requires": { 12 | "regenerator-runtime": "^0.13.4" 13 | } 14 | }, 15 | "defi": { 16 | "version": "1.3.7", 17 | "resolved": "https://registry.npmjs.org/defi/-/defi-1.3.7.tgz", 18 | "integrity": "sha512-VKKm4F6KTW6YHMt2nSQdJdrlTigE9dhFxhC5bXIoiYrtAc8UPKKe3GodcG8xwAoVJlbyW4iSkNn9lED9eY7DDQ==", 19 | "dev": true 20 | }, 21 | "regenerator-runtime": { 22 | "version": "0.13.5", 23 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", 24 | "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/defi/test/spec/mediate_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import mediate from 'src/mediate'; 3 | 4 | describe('mediate', () => { 5 | it('throws an error if an object is null', () => { 6 | expect(() => { 7 | mediate(null, 'a', String); 8 | }).toThrow(); 9 | }); 10 | 11 | it('mediates', () => { 12 | const obj = {}; 13 | 14 | mediate(obj, 'a', (v) => Number(v)); 15 | mediate(obj, ['b', 'c'], (v) => Number(v)); 16 | 17 | obj.a = obj.b = obj.c = '123'; 18 | 19 | expect(typeof obj.a).toEqual('number'); 20 | expect(typeof obj.b).toEqual('number'); 21 | expect(typeof obj.c).toEqual('number'); 22 | }); 23 | 24 | it('mediates using key-mediator object', () => { 25 | const obj = {}; 26 | 27 | mediate(obj, { 28 | a: (v) => Number(v), 29 | b: (v) => Number(v) 30 | }); 31 | 32 | obj.a = obj.b = '123'; 33 | 34 | expect(typeof obj.a).toEqual('number'); 35 | expect(typeof obj.b).toEqual('number'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/file-binders/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-binders", 3 | "version": "1.2.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/runtime": { 8 | "version": "7.10.2", 9 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz", 10 | "integrity": "sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==", 11 | "requires": { 12 | "regenerator-runtime": "^0.13.4" 13 | } 14 | }, 15 | "makeelement": { 16 | "version": "0.1.0", 17 | "resolved": "https://registry.npmjs.org/makeelement/-/makeelement-0.1.0.tgz", 18 | "integrity": "sha512-7a9XW9yv4Y6c4esWvWl0in0qI/y/dYwok92yWt3Vnu5DJf9GvaQCx2EX1rC8esk+UbnJ5S9zAWj00zneFPATFQ==", 19 | "dev": true 20 | }, 21 | "regenerator-runtime": { 22 | "version": "0.13.5", 23 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", 24 | "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/file-binders/src/file.js: -------------------------------------------------------------------------------- 1 | const getFileReaderMethodName = require('./_get-filereader-method-name'); 2 | const readFiles = require('./_read-files'); 3 | 4 | function createFileChangeHandler({ 5 | callback, 6 | methodName, 7 | }) { 8 | return function fileChangeHandler() { 9 | const { files } = this; 10 | 11 | if (files.length) { 12 | readFiles(files, methodName, callback); 13 | } else { 14 | callback([]); 15 | } 16 | }; 17 | } 18 | 19 | module.exports = function fileBinder(readAs) { 20 | const methodName = readAs ? getFileReaderMethodName(readAs) : null; 21 | let fileChangeHandler; 22 | 23 | return { 24 | on(callback) { 25 | fileChangeHandler = createFileChangeHandler({ 26 | callback, 27 | methodName, 28 | }); 29 | this.addEventListener('change', fileChangeHandler); 30 | }, 31 | destroy() { 32 | this.removeEventListener('change', fileChangeHandler); 33 | }, 34 | getValue({ domEvent }) { 35 | const files = domEvent || []; 36 | return this.multiple ? files : files[0] || null; 37 | }, 38 | setValue: null, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2016 Andrey Gubanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 11 | of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 14 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 16 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /packages/react/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andrey Gubanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/react/src/useChange.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useCallback } from 'react'; 2 | // @ts-ignore 3 | import { on, off, set } from 'defi'; 4 | import getStoreSlice from './getStoreSlice'; 5 | 6 | export interface StoreSelector { 7 | (store: { [key: string]: unknown }): { [key: string]: unknown }; 8 | } 9 | 10 | export default function useChange( 11 | storeSlice: { [key: string]: unknown } | StoreSelector, 12 | key: string, 13 | ): [any, (value: any) => void] { 14 | const slice = getStoreSlice(storeSlice); 15 | 16 | const [stateValue, setStateValue] = useState(slice[key]); 17 | const setValue = useCallback( 18 | (value) => set(slice, key, value, { fromHook: true }), 19 | [slice, key], 20 | ); 21 | 22 | useEffect(() => { 23 | const changeEventName = `change:${key}`; 24 | 25 | const handler = () => { 26 | setStateValue(slice[key]); 27 | }; 28 | 29 | if (slice[key] !== stateValue) { 30 | handler(); 31 | } 32 | 33 | on(slice, changeEventName, handler); 34 | 35 | return () => { off(slice, changeEventName, handler); }; 36 | }, [key, slice]); 37 | 38 | return [stateValue, setValue]; 39 | } 40 | -------------------------------------------------------------------------------- /packages/router/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andrey Gubanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/common-binders/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andrey Gubanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/file-binders/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andrey Gubanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/file-binders/src/_read-files.js: -------------------------------------------------------------------------------- 1 | module.exports = function readFiles(files, fileReaderMethodName, callback) { 2 | const { length } = files; 3 | const arrayOfFiles = Array(length); 4 | let j = 0; 5 | 6 | function createLoadendHandler({ 7 | file, 8 | reader, 9 | }) { 10 | return function loadendHandler() { 11 | file.readerResult = reader.result; // eslint-disable-line no-param-reassign 12 | j += 1; 13 | if (j === length) { 14 | callback(arrayOfFiles); 15 | } 16 | 17 | reader.removeEventListener('loadend', loadendHandler); 18 | }; 19 | } 20 | 21 | if (fileReaderMethodName) { 22 | for (let i = 0; i < length; i += 1) { 23 | const reader = new window.FileReader(); 24 | const file = files[i]; 25 | 26 | arrayOfFiles[i] = file; 27 | 28 | reader.addEventListener('loadend', createLoadendHandler({ 29 | file, 30 | reader, 31 | })); 32 | 33 | reader[fileReaderMethodName](file); 34 | } 35 | } else { 36 | for (let i = 0; i < length; i += 1) { 37 | arrayOfFiles[i] = files[i]; 38 | } 39 | 40 | callback(arrayOfFiles); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /packages/codemirror-binder/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andrey Gubanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/defi/src/trigger/_triggerone.js: -------------------------------------------------------------------------------- 1 | import defs from '../_core/defs'; 2 | 3 | // triggers one event 4 | export default function triggerOne(object, name, triggerArgs) { 5 | const def = defs.get(object); 6 | const events = def && def.events[name]; 7 | 8 | if (events) { 9 | const l = events.length; 10 | let i = 0; 11 | 12 | // allow to pass both array of args and single arg as triggerArgs 13 | if (triggerArgs instanceof Array) { 14 | while (i < l) { 15 | const event = triggerOne.latestEvent = events[i]; 16 | const { callback } = event; 17 | callback.apply(object, triggerArgs); 18 | i += 1; 19 | } 20 | } else { 21 | while (i < l) { 22 | const event = triggerOne.latestEvent = events[i]; 23 | const { callback } = event; 24 | callback.call(object, triggerArgs); 25 | i += 1; 26 | } 27 | } 28 | } 29 | } 30 | 31 | // latestEvent is used as required hack in somemethods 32 | triggerOne.latestEvent = { 33 | info: {}, 34 | name: null 35 | }; 36 | -------------------------------------------------------------------------------- /packages/router/test/spec/hash_router_spec.js: -------------------------------------------------------------------------------- 1 | import Router from '../../src/router'; 2 | 3 | const { document } = window; 4 | 5 | describe('Hash routing', () => { 6 | const obj = { a: 'foo' }; 7 | new Router('hash').subscribe(obj, 'a/b/c/d'); 8 | 9 | it('initializes correctly', (done) => { 10 | expect(obj.a).toEqual('foo'); 11 | expect(obj.b).toEqual(null); 12 | expect(obj.c).toEqual(null); 13 | expect(obj.d).toEqual(null); 14 | 15 | setTimeout(() => { 16 | expect(document.location.hash).toEqual('#!/foo/'); 17 | done(); 18 | }, 100); 19 | }); 20 | 21 | it('changes properties when URL (hash) is changed', (done) => { 22 | document.location.hash = '#!/bar/baz/qux/'; 23 | 24 | setTimeout(() => { 25 | expect(obj.a).toEqual('bar'); 26 | expect(obj.b).toEqual('baz'); 27 | expect(obj.c).toEqual('qux'); 28 | expect(obj.d).toEqual(null); 29 | done(); 30 | }, 100); 31 | }); 32 | 33 | it('changes URL (hash) when property is changed', (done) => { 34 | obj.b = 'lol'; 35 | setTimeout(() => { 36 | expect(document.location.hash).toEqual('#!/bar/lol/qux/'); 37 | done(); 38 | }, 100); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/router/test/spec/simple_router_spec.js: -------------------------------------------------------------------------------- 1 | import Router from '../../src/router'; 2 | 3 | describe('Simple router (API test)', () => { 4 | const obj = { a: 'foo' }; 5 | const router = new Router(null).subscribe(obj, 'a/b/c/d'); 6 | 7 | 8 | it('initializes correctly', () => { 9 | expect(obj.a).toEqual('foo'); 10 | expect(obj.b).toEqual(null); 11 | expect(obj.c).toEqual(null); 12 | expect(obj.d).toEqual(null); 13 | }); 14 | 15 | it('changes properties when URL is changed', () => { 16 | router.path = '/bar/baz/qux/'; 17 | 18 | expect(obj.a).toEqual('bar'); 19 | expect(obj.b).toEqual('baz'); 20 | expect(obj.c).toEqual('qux'); 21 | expect(obj.d).toEqual(null); 22 | }); 23 | 24 | it('changes URL when property is changed', () => { 25 | obj.b = 'lol'; 26 | expect(router.path).toEqual('/bar/lol/qux/'); 27 | expect(router.hashPath).toEqual('#!/bar/lol/qux/'); 28 | }); 29 | 30 | it('sets further parts as null if one of parts is null', () => { 31 | obj.b = null; 32 | 33 | expect(obj.a).toEqual('bar'); 34 | expect(obj.b).toEqual(null); 35 | expect(obj.c).toEqual(null); 36 | expect(obj.d).toEqual(null); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/defi/tools/banner-and-footer-webpack-plugin.js: -------------------------------------------------------------------------------- 1 | const ConcatSource = require('webpack-core/lib/ConcatSource'); 2 | 3 | const date = new Date().toUTCString(); 4 | 5 | const banner = `/* 6 | -------------------------------------------------------------- 7 | defi.js v${process.env.npm_package_version} (${date}) 8 | By Andrey Gubanov http://github.com/finom 9 | Released under the MIT license 10 | More info: https://defi.js.org 11 | -------------------------------------------------------------- 12 | */ 13 | 14 | `; 15 | 16 | // a hack to make 2nd global variable 17 | const footer = ''; 18 | 19 | class BannerAndFooterWebpackPlugin { 20 | apply(compiler) { 21 | compiler.plugin('compilation', (compilation) => { 22 | compilation.plugin('optimize-chunk-assets', (chunks, callback) => { 23 | Object.keys(compilation.assets).forEach((file) => { 24 | const newSource = new ConcatSource(banner, compilation.assets[file], footer); 25 | compilation.assets[file] = newSource; 26 | }); 27 | 28 | callback(); 29 | }); 30 | }); 31 | } 32 | } 33 | 34 | module.exports = BannerAndFooterWebpackPlugin; 35 | -------------------------------------------------------------------------------- /packages/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "defi-router", 3 | "version": "1.3.8", 4 | "description": "A router for defi.js", 5 | "main": "index", 6 | "scripts": { 7 | "test": "npm run unit", 8 | "unit": "../../node_modules/.bin/babel-node ../../node_modules/.bin/babel-istanbul cover test/index.js", 9 | "npm-compile": "../../node_modules/.bin/babel src -d npm && cp package.json npm/package.json && cp README.md npm/README.md", 10 | "build": "../../node_modules/.bin/webpack --mode production", 11 | "release": "npm run release-bundle && npm run release-npm" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/finom/defi.git" 16 | }, 17 | "keywords": [ 18 | "defi", 19 | "router" 20 | ], 21 | "author": "Andrey Gubanov", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/finom/defi/issues" 25 | }, 26 | "homepage": "https://github.com/finom/defi/tree/master/packages/router", 27 | "devDependencies": { 28 | "defi": "^1.3.8" 29 | }, 30 | "dependencies": { 31 | "@babel/runtime": "^7.10.2" 32 | }, 33 | "peerDependencies": { 34 | "defi": "*" 35 | }, 36 | "gitHead": "5d73b7d6892730283893fe296dea35cdef74f461" 37 | } 38 | -------------------------------------------------------------------------------- /packages/router/test/spec/history_router_spec.js: -------------------------------------------------------------------------------- 1 | import Router from '../../src/router'; 2 | 3 | const { document } = window; 4 | 5 | describe('HTML5 History routing', () => { 6 | const obj = { a: 'foo' }; 7 | let router; 8 | 9 | beforeAll(() => { 10 | router = new Router('history').subscribe(obj, 'a/b/c/d'); 11 | }); 12 | 13 | it('initializes correctly', (done) => { 14 | expect(obj.a).toEqual('foo'); 15 | expect(obj.b).toEqual(null); 16 | expect(obj.c).toEqual(null); 17 | expect(obj.d).toEqual(null); 18 | 19 | setTimeout(() => { 20 | expect(document.location.pathname).toEqual('/foo/'); 21 | done(); 22 | }, 100); 23 | }); 24 | 25 | it('changes properties when URL (pathname) is changed', (done) => { 26 | router.path = '/bar/baz/qux/'; 27 | 28 | setTimeout(() => { 29 | expect(obj.a).toEqual('bar'); 30 | expect(obj.b).toEqual('baz'); 31 | expect(obj.c).toEqual('qux'); 32 | expect(obj.d).toEqual(null); 33 | done(); 34 | }, 100); 35 | }); 36 | 37 | it('changes URL (pathname) when property is changed', (done) => { 38 | obj.b = 'lol'; 39 | setTimeout(() => { 40 | expect(document.location.pathname).toEqual('/bar/lol/qux/'); 41 | done(); 42 | }, 100); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/defi/src/_mq/_html2nodelist.js: -------------------------------------------------------------------------------- 1 | // converts HTML string to NodeList instance 2 | export default function html2nodeList(givenHTML) { 3 | // wrapMap is taken from jQuery 4 | const wrapMap = { 5 | option: [1, ''], 6 | legend: [1, '
', '
'], 7 | thead: [1, '', '
'], 8 | tr: [2, '', '
'], 9 | td: [3, '', '
'], 10 | col: [2, '', '
'], 11 | area: [1, '', ''], 12 | _: [0, '', ''] 13 | }; 14 | 15 | const html = givenHTML.replace(/^\s+|\s+$/g, ''); 16 | let node = window.document.createElement('div'); 17 | let i; 18 | 19 | wrapMap.optgroup = wrapMap.option; 20 | wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; 21 | wrapMap.th = wrapMap.td; 22 | 23 | const ex = /<([\w:]+)/.exec(html); 24 | const wrapper = (ex && wrapMap[ex[1]]) || wrapMap._; 25 | 26 | node.innerHTML = wrapper[1] + html + wrapper[2]; 27 | 28 | i = wrapper[0]; 29 | 30 | while (i) { 31 | i -= 1; 32 | node = node.children[0]; 33 | } 34 | 35 | return node.childNodes; 36 | } 37 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | There are few important requirements: 4 | 5 | - Commit messages must follow simplified **AngularJS Git Commit Message Conventions**. It's needed for automatic releases using [semantic-release](https://github.com/semantic-release/semantic-release). Simplified means that it's not required to specify a scope and a footer in a commit message. 6 | 7 | Example commit message: 8 | ``` 9 | fix: Make developers happy 10 | ``` 11 | 12 | A body is desirable but not required as well. It can explain **why** did you make such change but should not explain what did you do. 13 | 14 | ``` 15 | fix: Make developers happy 16 | 17 | Happy developers are better than angry developers 18 | ``` 19 | 20 | Don't worry to make a mistake. Git hook throws an error when bad commit message is used. Also you can run ``npm run commit`` instead of ``git commit`` to commit your changes via CLI prompt powered by [commitizen](https://github.com/commitizen/cz-cli). 21 | 22 | - It is required to have one commit per fix/feature/chore etc. 23 | - Fixes (more than just a fix of a typo) and features (any # of lines) must be followed by a test. 24 | - The coverage must not be lower after your commit (Coveralls integration will warn about it). 25 | - New features need to be discussed first (open an issue). 26 | -------------------------------------------------------------------------------- /packages/defi/src/on/_createdomeventhandler.js: -------------------------------------------------------------------------------- 1 | // returns DOM event handler 2 | export default function createDomEventHandler({ 3 | key, 4 | object, 5 | callback 6 | }) { 7 | return function domEventHandler(domEvent) { 8 | const originalEvent = domEvent.originalEvent || domEvent; 9 | // defiTriggerArgs are created when DOM event is triggered by trigger method 10 | const triggerArgs = originalEvent.defiTriggerArgs; 11 | const { 12 | which, target, ctrlKey, altKey 13 | } = domEvent; 14 | 15 | if (triggerArgs) { 16 | // if args are passed to trigger method then pass them to an event handler 17 | callback.apply(object, triggerArgs); 18 | } else { 19 | // use the following object as an arg for event handler 20 | callback.call(object, { 21 | self: object, 22 | node: this, 23 | preventDefault: () => domEvent.preventDefault(), 24 | stopPropagation: () => domEvent.stopPropagation(), 25 | key, 26 | domEvent, 27 | originalEvent, 28 | which, 29 | target, 30 | ctrlKey, 31 | altKey 32 | }); 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/defi/src/off/_removedomlistener.js: -------------------------------------------------------------------------------- 1 | import defs from '../_core/defs'; 2 | import removeListener from './_removelistener'; 3 | import $ from '../_mq'; 4 | import forEach from '../_helpers/foreach'; 5 | 6 | // removes dom listener from nodes bound to given key 7 | export default function removeDomListener( 8 | object, 9 | key, 10 | eventName, 11 | selector, 12 | callback, 13 | info 14 | ) { 15 | const def = defs.get(object); 16 | 17 | if (!def) { 18 | return object; 19 | } 20 | 21 | const { props } = def; 22 | const propDef = props[key]; 23 | 24 | if (!propDef) { 25 | return object; 26 | } 27 | 28 | const { bindings } = propDef; 29 | 30 | if (bindings) { 31 | // collect bound nodes and remove DOM event listener 32 | const nodes = Array(bindings.length); 33 | const eventNamespace = def.id + key; 34 | 35 | forEach(bindings, (binding, index) => { 36 | nodes[index] = binding.node; 37 | }); 38 | 39 | $(nodes).off(`${eventName}.${eventNamespace}`, selector, callback); 40 | } 41 | 42 | // remove bind and unbind listeners from given key 43 | removeListener(object, `bind:${key}`, callback, info); 44 | removeListener(object, `unbind:${key}`, callback, info); 45 | 46 | return object; 47 | } 48 | -------------------------------------------------------------------------------- /packages/codemirror-binder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codemirror-binder", 3 | "version": "1.3.8", 4 | "description": "CodeMirror binder for defi.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run cover", 8 | "cover": "../../node_modules/.bin/babel-node ../../node_modules/.bin/babel-istanbul cover test/index.js", 9 | "unit": "../../node_modules/.bin/babel-node test/index.js", 10 | "npm-compile": "../../node_modules/.bin/babel src.js --out-file npm/index.js && cp package.json npm/package.json && cp README.md npm/README.md", 11 | "build": "../../node_modules/.bin/webpack --mode=production" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/finom/defi.git" 16 | }, 17 | "keywords": [ 18 | "defi", 19 | "codemirror" 20 | ], 21 | "author": "Andrey Gubanov", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/finom/defi/issues" 25 | }, 26 | "homepage": "https://github.com/finom/defi/tree/master/packages/codemirror-binder", 27 | "dependencies": { 28 | "@babel/runtime": "^7.10.2" 29 | }, 30 | "devDependencies": { 31 | "codemirror": "^5.53.2", 32 | "defi": "^1.3.8" 33 | }, 34 | "peerDependencies": { 35 | "codemirror": "*", 36 | "defi": "*" 37 | }, 38 | "gitHead": "5d73b7d6892730283893fe296dea35cdef74f461" 39 | } 40 | -------------------------------------------------------------------------------- /packages/defi/test/webpack-test.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | context: __dirname, 7 | entry: [ 8 | './index' 9 | ], 10 | output: { 11 | path: path.resolve(__dirname, '../bundle/test'), 12 | filename: 'bundle.js' 13 | }, 14 | resolve: { 15 | alias: { 16 | src: path.resolve('./src') 17 | } 18 | }, 19 | module: { 20 | rules: [ 21 | // transpile all files except testing sources with babel as usual 22 | { 23 | test: /\.js$/, 24 | include: path.resolve('test/'), 25 | exclude: [ 26 | path.resolve('src/'), 27 | path.resolve('node_modules/') 28 | ], 29 | use: ['babel-loader'] 30 | }, 31 | // transpile and instrument only testing sources with babel-istanbul 32 | { 33 | test: /\.js$/, 34 | include: path.resolve('src/'), 35 | use: ['babel-loader'] 36 | } 37 | ] 38 | }, 39 | plugins: [ 40 | new CopyWebpackPlugin([{ 41 | from: path.resolve(__dirname, 'browser-test') 42 | }]) 43 | ] 44 | }; 45 | -------------------------------------------------------------------------------- /packages/codemirror-binder/README.md: -------------------------------------------------------------------------------- 1 | # codemirror-binder [![npm version](https://badge.fury.io/js/codemirror-binder.svg)](https://badge.fury.io/js/codemirror-binder) 2 | 3 | > [CodeMirror](http://codemirror.net/) binder creator for defi.js 4 | 5 | The binder creator returns a binder which initializes and binds CodeMirror instance (created using ``fromTextArea`` function) to a property. 6 | 7 | ## Usage 8 | 9 | ``` 10 | npm i codemirror-binder 11 | ``` 12 | 13 | ```js 14 | import { bindNode } from 'defi'; 15 | import codeMirrorBinder from 'codemirror-binder'; 16 | 17 | // ... 18 | bindNode(obj, 'code', textarea, codeMirrorBinder()); 19 | ``` 20 | 21 | 22 | ### Usage in a browser environment 23 | 24 | For non-CJS environment the bundle can be downloaded at [gh-pages branch](https://github.com/finom/defi/tree/gh-pages/). 25 | 26 | In the browser environment the script exports a global variable `codeMirrorBinder`. 27 | 28 | ```html 29 | 30 | ``` 31 | ------------- 32 | 33 | 34 | ### Configuration 35 | 36 | The function accepts one argument: configuration object which is passed into the internal call of ``CodeMirror.fromTextArea``. Read the CodeMirror documentation for more info. 37 | 38 | ```js 39 | bindNode(obj, 'code', textarea, codeMirror({ 40 | lineNumbers: true, 41 | mode: 'htmlmixed' 42 | })); 43 | ``` 44 | -------------------------------------------------------------------------------- /packages/common-binders/src/_classlist.js: -------------------------------------------------------------------------------- 1 | // @IE9 2 | 3 | let add; 4 | let remove; 5 | let contains; // eslint-disable-line import/no-mutable-exports 6 | 7 | /* istanbul ignore else */ 8 | if (window.document.createElement('div').classList) { 9 | add = (node, name) => node.classList.add(name); 10 | remove = (node, name) => node.classList.remove(name); 11 | contains = (node, name) => node.classList.contains(name); 12 | } else { 13 | add = (node, name) => { 14 | const re = new RegExp(`(^|\\s)${name}(\\s|$)`, 'g'); 15 | if (!re.test(node.className)) { 16 | // eslint-disable-next-line no-param-reassign 17 | node.className = `${node.className} ${name}` 18 | .replace(/\s+/g, ' ') 19 | .replace(/(^ | $)/g, ''); 20 | } 21 | }; 22 | 23 | remove = (node, name) => { 24 | const re = new RegExp(`(^|\\s)${name}(\\s|$)`, 'g'); 25 | // eslint-disable-next-line no-param-reassign 26 | node.className = node.className 27 | .replace(re, '$1') 28 | .replace(/\s+/g, ' ') 29 | .replace(/(^ | $)/g, ''); 30 | }; 31 | 32 | contains = (node, name) => new RegExp(`(\\s|^)${name}(\\s|$)`).test(node.className); 33 | } 34 | 35 | const toggle = (node, name, switcher) => { 36 | if (switcher) { 37 | add(node, name); 38 | } else { 39 | remove(node, name); 40 | } 41 | }; 42 | 43 | export { 44 | toggle, 45 | contains, 46 | }; 47 | -------------------------------------------------------------------------------- /packages/defi/src/trigger/_triggerdomevent.js: -------------------------------------------------------------------------------- 1 | import triggerOneDOMEvent from './_triggeronedomevent'; 2 | import defs from '../_core/defs'; 3 | import forEach from '../_helpers/foreach'; 4 | 5 | // triggers DOM event on bound nodes 6 | export default function triggerDOMEvent(object, key, eventName, selector, triggerArgs) { 7 | const def = defs.get(object); 8 | 9 | if (!def) { 10 | return; 11 | } 12 | 13 | const { props } = def; 14 | const propDef = props[key]; 15 | 16 | if (!propDef) { 17 | return; 18 | } 19 | 20 | const { bindings } = propDef; 21 | 22 | if (!bindings) { 23 | return; 24 | } 25 | 26 | forEach(bindings, ({ node }) => { 27 | if (selector) { 28 | // if selector is given trigger an event on all node descendants 29 | const descendants = node.querySelectorAll(selector); 30 | forEach(descendants, (descendant) => { 31 | triggerOneDOMEvent({ 32 | node: descendant, 33 | eventName, 34 | triggerArgs 35 | }); 36 | }); 37 | } else { 38 | // trigger an event for single node 39 | triggerOneDOMEvent({ 40 | node, 41 | eventName, 42 | triggerArgs 43 | }); 44 | } 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /packages/defi/src/_core/init.js: -------------------------------------------------------------------------------- 1 | import defs from './defs'; 2 | 3 | let objectId = 0; 4 | 5 | // this is a common function which associates an object with its defi definition 6 | export default function initDefi(object) { 7 | let def = defs.get(object); 8 | if (!def) { 9 | def = { 10 | // a property name of "events" object is an event name 11 | // and a value is an array of event handlers 12 | events: { 13 | /* example: { 14 | callback: function, 15 | name: "example", 16 | info: { ...extra data for an event... } 17 | } */ 18 | }, 19 | // "props" contains special information about properties (getters, setters etc) 20 | props: { 21 | /* example: { 22 | value: object[key], 23 | mediator: null, 24 | bindings: [{ 25 | node, 26 | binder, 27 | nodeHandler, 28 | objectHandler, 29 | ...other required info 30 | }] 31 | } */ 32 | }, 33 | id: objectId 34 | }; 35 | 36 | objectId += 1; 37 | 38 | defs.set(object, def); 39 | } 40 | 41 | return def; 42 | } 43 | -------------------------------------------------------------------------------- /packages/codemirror-binder/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codemirror-binder", 3 | "version": "1.3.8", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/runtime": { 8 | "version": "7.10.2", 9 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz", 10 | "integrity": "sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==", 11 | "requires": { 12 | "regenerator-runtime": "^0.13.4" 13 | } 14 | }, 15 | "codemirror": { 16 | "version": "5.53.2", 17 | "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.53.2.tgz", 18 | "integrity": "sha512-wvSQKS4E+P8Fxn/AQ+tQtJnF1qH5UOlxtugFLpubEZ5jcdH2iXTVinb+Xc/4QjshuOxRm4fUsU2QPF1JJKiyXA==", 19 | "dev": true 20 | }, 21 | "defi": { 22 | "version": "1.3.7", 23 | "resolved": "https://registry.npmjs.org/defi/-/defi-1.3.7.tgz", 24 | "integrity": "sha512-VKKm4F6KTW6YHMt2nSQdJdrlTigE9dhFxhC5bXIoiYrtAc8UPKKe3GodcG8xwAoVJlbyW4iSkNn9lED9eY7DDQ==", 25 | "dev": true 26 | }, 27 | "regenerator-runtime": { 28 | "version": "0.13.5", 29 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", 30 | "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/defi/src/chain.js: -------------------------------------------------------------------------------- 1 | import checkObjectType from './_helpers/checkobjecttype'; 2 | import * as functions from './_lib'; 3 | import forEach from './_helpers/foreach'; 4 | 5 | // create a prototype of ChainClass 6 | // store target object at "object" property 7 | const prototype = { 8 | constructor(object) { 9 | this.object = object; 10 | } 11 | }; 12 | 13 | const funcNames = Object.keys(functions); 14 | 15 | // iterate over all universal methods 16 | for (let i = 0; i < funcNames.length; i++) { 17 | const funcName = funcNames[i]; 18 | const method = functions[funcName]; 19 | 20 | // create every chained method 21 | prototype[funcName] = function chainedMethod() { 22 | const args = [this.object]; 23 | 24 | forEach(arguments, (argument) => { 25 | args.push(argument); 26 | }); 27 | 28 | method(...args); 29 | 30 | // returning this is important for chained calls 31 | return this; 32 | }; 33 | } 34 | 35 | 36 | const ChainClass = function ChainClass(object) { 37 | this.object = object; 38 | }; 39 | 40 | ChainClass.prototype = prototype; 41 | 42 | // the function allows to chain static function calls on any object 43 | export default function chain(object) { 44 | // check for type and throw an error if it is not an object and is not a function 45 | checkObjectType(object, 'chain'); 46 | 47 | return new ChainClass(object); 48 | } 49 | -------------------------------------------------------------------------------- /packages/defi/src/_mq/_init.js: -------------------------------------------------------------------------------- 1 | import html2nodeList from './_html2nodelist'; 2 | 3 | // function-constructor of mq library 4 | // accepts many kinds of arguments (selector, html, function) 5 | function MQInit(selector, context) { 6 | const win = window; 7 | 8 | let result; 9 | 10 | if (selector) { 11 | if (selector.nodeType || (typeof win === 'object' && selector === win)) { 12 | result = [selector]; 13 | } else if (typeof selector === 'string') { 14 | if (/ { 6 | const { Event } = window; 7 | 8 | let node; 9 | 10 | beforeEach(() => { 11 | node = makeElement('div'); 12 | }); 13 | 14 | it('bound property gets correct values on corresponding events', () => { 15 | const obj = {}; 16 | 17 | bindNode(obj, 'dragovered', node, dragOver(), { 18 | debounceGetValue: false, 19 | }); 20 | 21 | expect(obj.dragovered).toEqual(false, 'should be false by default'); 22 | 23 | node.dispatchEvent(new Event('dragover')); 24 | expect(obj.dragovered).toEqual(true, 'should become true on dragover'); 25 | 26 | node.dispatchEvent(new Event('drop')); 27 | expect(obj.dragovered).toEqual(false, 'should become false on drop'); 28 | 29 | node.dispatchEvent(new Event('foobar')); 30 | expect(obj.dragovered).toEqual(false, 'should not be changed on foobar'); 31 | 32 | node.dispatchEvent(new Event('dragenter')); 33 | expect(obj.dragovered).toEqual(true, 'should become true on dragenter'); 34 | 35 | node.dispatchEvent(new Event('foobar')); 36 | expect(obj.dragovered).toEqual(true, 'should not be changed on foobar'); 37 | 38 | node.dispatchEvent(new Event('dragleave')); 39 | expect(obj.dragovered).toEqual(false, 'should become false on dragleave'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/file-binders/src/dropfiles.js: -------------------------------------------------------------------------------- 1 | const getFileReaderMethodName = require('./_get-filereader-method-name'); 2 | const readFiles = require('./_read-files'); 3 | 4 | function createDropHandler({ 5 | callback, 6 | methodName, 7 | }) { 8 | return function dropHandler(event) { 9 | event.preventDefault(); 10 | const { files } = event.dataTransfer; 11 | 12 | readFiles(files, methodName, callback); 13 | }; 14 | } 15 | 16 | function createDragoverHandler() { 17 | return function dragoverHandler(event) { 18 | event.preventDefault(); 19 | if (event.dataTransfer) { 20 | event.dataTransfer.dropEffect = 'copy'; // eslint-disable-line no-param-reassign 21 | } 22 | }; 23 | } 24 | 25 | module.exports = function dropFilesBinder(readAs) { 26 | const methodName = readAs ? getFileReaderMethodName(readAs) : null; 27 | let dropHandler; 28 | let dragoverHandler; 29 | 30 | return { 31 | on(callback) { 32 | dropHandler = createDropHandler({ 33 | callback, 34 | methodName, 35 | }); 36 | dragoverHandler = createDragoverHandler(); 37 | 38 | this.addEventListener('drop', dropHandler); 39 | this.addEventListener('dragover', dragoverHandler); 40 | }, 41 | destroy() { 42 | this.removeEventListener('drop', dropHandler); 43 | this.removeEventListener('dragover', dragoverHandler); 44 | }, 45 | getValue({ domEvent }) { 46 | return domEvent || []; 47 | }, 48 | setValue: null, 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /packages/defi/test/karma-test/karma.conf.js: -------------------------------------------------------------------------------- 1 | const files = ['index.js']; 2 | 3 | module.exports = (config) => { 4 | config.set({ 5 | basePath: '..', 6 | frameworks: ['jasmine'], 7 | plugins: [ 8 | require('karma-jasmine'), 9 | require('karma-coverage'), 10 | require('karma-webpack-with-fast-source-maps'), 11 | require('karma-sourcemap-loader'), 12 | require('karma-chrome-launcher') 13 | ], 14 | files, 15 | exclude: [], 16 | port: 9876, 17 | colors: true, 18 | logLevel: config.LOG_INFO, 19 | autoWatch: true, 20 | browsers: process.env.TRAVIS ? ['Chrome_travis_ci'] : ['Chrome'], 21 | customLaunchers: { 22 | Chrome_travis_ci: { 23 | base: 'Chrome', 24 | flags: ['--no-sandbox'] 25 | } 26 | }, 27 | reporters: ['progress', 'coverage'], 28 | singleRun: false, 29 | preprocessors: { 30 | 'index.js': ['sourcemap', 'webpack'] 31 | }, 32 | coverageReporter: { 33 | dir: 'coverage', 34 | reporters: [{ 35 | type: 'lcov', 36 | subdir: '.' 37 | }] 38 | }, 39 | webpack: Object.assign(require('../webpack-test.config'), { 40 | devtool: 'cheap-module-source-map', 41 | entry: [ 42 | '../test/index' 43 | ] 44 | }) 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "defi-react", 3 | "version": "1.3.8", 4 | "scripts": { 5 | "test": "npm run cover", 6 | "cover": "../../node_modules/.bin/babel-node ../../node_modules/.bin/babel-istanbul cover test/index.js", 7 | "unit": "../../node_modules/.bin/babel-node test/index.js", 8 | "generate-types": "npx tsc --emitDeclarationOnly -d --rootDir ./src --outDir ./npm", 9 | "npm-compile": "rm -rf npm && ../../node_modules/.bin/babel --extensions \".ts\" src -d npm && cp package.json npm/package.json && cp README.md npm/README.md && npm run generate-types", 10 | "build": "echo 'defi-react does not produce JS bundle'", 11 | "get-toc": "./gh-md-toc ./README.md" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/finom/defi.git" 16 | }, 17 | "author": "Andrey Gubanov", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/finom/defi/issues" 21 | }, 22 | "homepage": "https://github.com/finom/defi/tree/master/packages/react", 23 | "devDependencies": { 24 | "@babel/preset-typescript": "^7.10.1", 25 | "@testing-library/react-hooks": "^3.3.0", 26 | "defi": "^1.3.8", 27 | "react": "^16.13.1", 28 | "react-test-renderer": "^16.13.1", 29 | "typescript": "^3.9.5" 30 | }, 31 | "dependencies": { 32 | "@babel/runtime": "^7.10.2" 33 | }, 34 | "gitHead": "5d73b7d6892730283893fe296dea35cdef74f461", 35 | "peerDependencies": { 36 | "defi": "*", 37 | "react": "*" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/defi/src/trigger/index.js: -------------------------------------------------------------------------------- 1 | import domEventReg from '../on/_domeventregexp'; 2 | import checkObjectType from '../_helpers/checkobjecttype'; 3 | import defs from '../_core/defs'; 4 | import triggerOne from './_triggerone'; 5 | import triggerDomEvent from './_triggerdomevent'; 6 | import forEach from '../_helpers/foreach'; 7 | 8 | // triggers an event 9 | export default function trigger(object, givenNames, ...triggerArgs) { 10 | // throw error when object type is wrong 11 | checkObjectType(object, 'trigger'); 12 | 13 | // allow to use either a string or an array of events 14 | const names = givenNames instanceof Array ? givenNames : [givenNames]; 15 | 16 | const def = defs.get(object); 17 | 18 | // if no definition do nothing 19 | if (!def) { 20 | return object; 21 | } 22 | 23 | const { events: allEvents } = def; 24 | 25 | if (!allEvents) { 26 | return object; 27 | } 28 | 29 | forEach(names, (name) => { 30 | const domEvtExecResult = typeof name === 'string' && domEventReg.exec(name); 31 | 32 | if (domEvtExecResult) { 33 | // if EVT::KEY(SELECTOR) ia passed as event name then trigger DOM event 34 | const [, eventName, key, selector] = domEvtExecResult; 35 | triggerDomEvent(object, key, eventName, selector, triggerArgs); 36 | } else { 37 | // trigger ordinary event 38 | triggerOne(object, name, triggerArgs); 39 | } 40 | }); 41 | 42 | return object; 43 | } 44 | -------------------------------------------------------------------------------- /packages/common-binders/src/existence.js: -------------------------------------------------------------------------------- 1 | export default function existence(switcher = true) { 2 | let comment; 3 | 4 | return { 5 | setValue(value) { 6 | const node = this; 7 | const { 8 | tagName, id, classList, className, 9 | } = node; 10 | 11 | if (!comment) { 12 | let commentText = tagName; 13 | 14 | 15 | if (id) { 16 | commentText += `#${id}`; 17 | } 18 | 19 | if (className) { 20 | commentText += `.${[].slice.apply(classList).join('.')}`; 21 | } 22 | 23 | comment = window.document.createComment(commentText); 24 | } 25 | 26 | if (typeof switcher === 'function') { 27 | value = switcher(value); // eslint-disable-line no-param-reassign 28 | } else if (!switcher) { 29 | value = !value; // eslint-disable-line no-param-reassign 30 | } 31 | 32 | if (value) { 33 | // eslint-disable-next-line no-underscore-dangle 34 | delete node.__replacedByNode; 35 | if (comment.parentNode) { 36 | comment.parentNode.insertBefore(node, comment); 37 | comment.parentNode.removeChild(comment); 38 | } 39 | } 40 | 41 | if (!value) { 42 | // eslint-disable-next-line no-underscore-dangle 43 | node.__replacedByNode = comment; 44 | if (node.parentNode) { 45 | node.parentNode.insertBefore(comment, node); 46 | node.parentNode.removeChild(node); 47 | } 48 | } 49 | }, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /packages/router/test/spec/gapped_router_spec.js: -------------------------------------------------------------------------------- 1 | import Router from '../../src/router'; 2 | 3 | describe('Gapped router (API test)', () => { 4 | const obj = { 5 | a: 'foo', 6 | b: 'bar', 7 | c: 'baz', 8 | }; 9 | const router = new Router(null).subscribe(obj, 'a/*/c/*/e/f'); 10 | 11 | it('initializes correctly', () => { 12 | expect(obj.a).toEqual('foo'); 13 | expect(obj.b).toEqual('bar'); 14 | expect(obj.c).toEqual(null); // because 2nd part is not set 15 | expect(obj.d).toEqual(undefined); 16 | expect(obj.e).toEqual(null); 17 | expect(obj.f).toEqual(null); 18 | }); 19 | 20 | it('changes properties when URL is changed', () => { 21 | router.path = '/bar/baz/qux/eggs/bat/lol/'; 22 | 23 | expect(obj.a).toEqual('bar'); 24 | expect(obj.b).toEqual('bar'); 25 | expect(obj.c).toEqual('qux'); 26 | expect(obj.d).toEqual(undefined); 27 | expect(obj.e).toEqual('bat'); 28 | expect(obj.f).toEqual('lol'); 29 | }); 30 | 31 | it('changes URL when property is changed', () => { 32 | obj.c = 'poo'; 33 | expect(router.path).toEqual('/bar/baz/poo/eggs/bat/lol/'); 34 | expect(router.hashPath).toEqual('#!/bar/baz/poo/eggs/bat/lol/'); 35 | }); 36 | 37 | it('sets further parts as null if one of parts is null', () => { 38 | obj.c = null; 39 | 40 | expect(obj.a).toEqual('bar'); 41 | expect(obj.b).toEqual('bar'); 42 | expect(obj.c).toEqual(null); 43 | expect(obj.d).toEqual(undefined); 44 | expect(obj.e).toEqual(null); 45 | expect(obj.f).toEqual(null); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/defi/src/binders/select.js: -------------------------------------------------------------------------------- 1 | // returns a binder for select element 2 | export default function select(multiple) { 3 | if (multiple) { 4 | return { 5 | on: 'change', 6 | getValue() { 7 | const { options } = this; 8 | const result = []; 9 | 10 | for (let i = 0; options.length > i; i++) { 11 | if (options[i].selected) { 12 | result.push(options[i].value); 13 | } 14 | } 15 | 16 | return result; 17 | }, 18 | setValue(givenValue) { 19 | const { options } = this; 20 | const value = typeof givenValue === 'string' ? [givenValue] : givenValue; 21 | for (let i = options.length - 1; i >= 0; i--) { 22 | options[i].selected = ~value.indexOf(options[i].value); 23 | } 24 | } 25 | }; 26 | } 27 | 28 | return { 29 | on: 'change', 30 | getValue() { 31 | return this.value; 32 | }, 33 | setValue(value) { 34 | this.value = value; 35 | 36 | if (!value) { 37 | const { options } = this; 38 | for (let i = options.length - 1; i >= 0; i--) { 39 | if (!options[i].value) { 40 | options[i].selected = true; 41 | break; 42 | } 43 | } 44 | } 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/defi/test/spec/remove_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import remove from 'src/remove'; 3 | import on from 'src/on'; 4 | import bindNode from 'src/bindnode'; 5 | import bound from 'src/bound'; 6 | import trigger from 'src/trigger'; 7 | import createSpy from '../helpers/createspy'; 8 | 9 | describe('remove', () => { 10 | it('throws an error if an object is null', () => { 11 | expect(() => { 12 | remove(null, 'a'); 13 | }).toThrow(); 14 | }); 15 | 16 | it('removes a property', () => { 17 | const obj = { 18 | a: 1 19 | }; 20 | 21 | remove(obj, 'a'); 22 | expect('a' in obj).toBe(false); 23 | }); 24 | 25 | it('removes a property and its events', () => { 26 | const obj = { 27 | a: 1 28 | }; 29 | const handler = createSpy(); 30 | 31 | on(obj, 'change:a', handler); 32 | trigger(obj, 'change:a'); 33 | expect(handler).toHaveBeenCalledTimes(1); 34 | remove(obj, 'a'); 35 | trigger(obj, 'change:a'); 36 | expect(handler).toHaveBeenCalledTimes(1); 37 | expect('a' in obj).toBe(false); 38 | }); 39 | 40 | it('removes a property and its bindings', () => { 41 | const obj = { 42 | a: 1 43 | }; 44 | const node = window.document.createElement('div'); 45 | 46 | bindNode(obj, 'a', node); 47 | expect(bound(obj, 'a')).toEqual(node); 48 | remove(obj, 'a'); 49 | expect(bound(obj, 'a')).toEqual(null); 50 | expect('a' in obj).toBe(false); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/defi/src/calc/_addsource.js: -------------------------------------------------------------------------------- 1 | import addListener from '../on/_addlistener'; 2 | import addTreeListener from '../on/_addtreelistener'; 3 | import defiError from '../_helpers/defierror'; 4 | 5 | // adds a source to a source list and adds needed event listener to a it 6 | export default function addSource({ 7 | calcHandler, 8 | allSources, 9 | sourceKey, 10 | sourceObject, 11 | eventOptions 12 | }) { 13 | let { exactKey = false } = eventOptions; 14 | let isDelegated = false; 15 | 16 | // source key must be a string 17 | if (typeof sourceKey !== 'string') { 18 | throw defiError('calc:source_key_type', { sourceKey }); 19 | } 20 | 21 | // source object must be an object 22 | if (!sourceObject || typeof sourceObject !== 'object') { 23 | throw defiError('calc:source_object_type', { sourceObject }); 24 | } 25 | 26 | if (!exactKey) { 27 | const deepPath = sourceKey.split('.'); 28 | 29 | // if something like a.b.c is used as a key 30 | if (deepPath.length > 1) { 31 | isDelegated = true; 32 | // TODO: Avoid collisions with bindings by using another event name 33 | // ... instead of _change:tree:xxx 34 | addTreeListener(sourceObject, deepPath, calcHandler); 35 | } else { 36 | exactKey = true; 37 | } 38 | } 39 | 40 | 41 | if (exactKey) { 42 | // normal handler 43 | addListener(sourceObject, `_change:deps:${sourceKey}`, calcHandler); 44 | } 45 | 46 | allSources.push({ 47 | sourceKey, 48 | sourceObject, 49 | isDelegated 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /packages/defi/src/on/_delegatelistener/index.js: -------------------------------------------------------------------------------- 1 | import addListener from '../_addlistener'; 2 | import changeHandler from './changehandler'; 3 | import slice from '../../_helpers/slice'; 4 | 5 | // adds delegated event listener to an object by given path 6 | // TODO Handler uses wrong context 7 | export default function delegateListener(object, givenPath, name, callback, info = {}) { 8 | // if typeof path is string and path is not empty string then split it 9 | let path = typeof givenPath === 'string' && givenPath !== '' ? givenPath.split('.') : givenPath; 10 | 11 | if (!path || !path.length) { 12 | // if no path then add simple listener 13 | addListener(object, name, callback, info); 14 | } else { 15 | // else do all magic 16 | const key = path[0]; 17 | let pathStr; // needed for undelegation 18 | 19 | if (path.length > 1) { 20 | path = slice(path, 1); 21 | pathStr = path.join('.'); 22 | } else { 23 | path = []; 24 | pathStr = path[0] || ''; 25 | } 26 | 27 | const delegatedData = { 28 | path, 29 | name, 30 | callback, 31 | info, 32 | object 33 | }; 34 | 35 | // the event is triggered by "set"; 36 | // a new function is created as a handler to make possible 37 | // to add the handler multiple times for one key 38 | addListener(object, `_change:delegated:${key}`, (evt) => changeHandler(evt), { 39 | delegatedData, 40 | pathStr 41 | }); 42 | 43 | // call handler manually 44 | changeHandler({ 45 | value: object[key] 46 | }, delegatedData); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/defi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "defi", 3 | "version": "1.3.8", 4 | "description": "Data binding without framework", 5 | "main": "./index.js", 6 | "types": "./index.d.ts", 7 | "scripts": { 8 | "test": "npm run node-cover && npm run check-coverage", 9 | "node-test": "BABEL_ENV=test babel-node test/node-test/jasmine.js", 10 | "node-cover": "BABEL_ENV=test ../../node_modules/.bin/babel-node ../../node_modules/.bin/babel-istanbul cover test/node-test/jasmine.js", 11 | "check-coverage": "../../node_modules/.bin/babel-istanbul check-coverage --lines 85", 12 | "develop": "karma start test/karma-test/karma.conf.js", 13 | "karma-test": "BABEL_ENV=test karma start test/karma-test/karma.conf.js --single-run --no-auto-watch --no-sandbox", 14 | "build": "../../node_modules/.bin/webpack --config ./webpack.config.js --mode production", 15 | "watch": "webpack --config ./webpack.config.js --watch --mode development", 16 | "watch-browser-test": "webpack --config test/webpack-test.config.js --watch --mode development", 17 | "npm-compile": "shx rm -rf npm && babel src -d npm --source-maps && shx cp ../../README.md npm/README.md && shx cp src/index.d.ts npm/index.d.ts && node ./tools/generate-package" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/finom/defi.git" 22 | }, 23 | "author": { 24 | "name": "Andrey Gubanov", 25 | "email": "andrey.a.gubanov@gmail.com" 26 | }, 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/finom/defi/issues" 30 | }, 31 | "homepage": "https://github.com/finom/defi#readme", 32 | "gitHead": "5d73b7d6892730283893fe296dea35cdef74f461", 33 | "dependencies": { 34 | "@babel/runtime": "^7.10.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/react/test/spec/useSet.spec.js: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { useSet } from '../../npm'; 3 | import getWrapper from './getWrapper'; 4 | 5 | describe('useSet', () => { 6 | it('Should work', () => { 7 | const store = { x: 1 }; 8 | let renderedTimes = 0; 9 | const { result, rerender } = renderHook(() => { 10 | renderedTimes += 1; 11 | return useSet(store, 'x'); 12 | }); 13 | 14 | const returnedSet = result.current; 15 | expect(typeof result.current === 'function').toBeTrue(); 16 | expect(store.x).toBe(1); 17 | expect(renderedTimes).toBe(1); 18 | 19 | act(() => { result.current(2); }); 20 | 21 | expect(store.x).toBe(2); 22 | expect(renderedTimes).toBe(1); 23 | expect(returnedSet).toBe(result.current); 24 | 25 | rerender(); 26 | 27 | expect(store.x).toBe(2); 28 | expect(renderedTimes).toBe(2); 29 | expect(returnedSet).toBe(result.current); 30 | }); 31 | 32 | it('Should use store selector', () => { 33 | const store = { x: { y: 1 } }; 34 | const wrapper = getWrapper(store); 35 | let renderedTimes = 0; 36 | const { result } = renderHook(() => { 37 | renderedTimes += 1; 38 | return useSet(({ x }) => x, 'y'); 39 | }, { wrapper }); 40 | 41 | expect(typeof result.current === 'function').toBeTrue(); 42 | expect(store.x.y).toBe(1); 43 | expect(renderedTimes).toBe(1); 44 | 45 | act(() => { result.current(2); }); 46 | 47 | expect(store.x.y).toBe(2); 48 | expect(renderedTimes).toBe(1); 49 | }); 50 | 51 | it('Should throw error if store selector is null', () => { 52 | const { result: { error } } = renderHook(() => useSet(null, 'y')); 53 | 54 | expect(error).toBeTruthy(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /packages/defi/src/calc/_createcalchandler.js: -------------------------------------------------------------------------------- 1 | import set from '../set'; 2 | import deepFind from '../_helpers/deepfind'; 3 | import forEach from '../_helpers/foreach'; 4 | 5 | // creates event handler for target object which will be fired when a source is changed 6 | export default function createCalcHandler({ 7 | object, 8 | eventOptions, 9 | allSources, 10 | target, 11 | def, 12 | handler 13 | }) { 14 | return function calcHandler(changeEvent = {}) { 15 | const values = []; 16 | const { protector = {} } = changeEvent; 17 | const protectKey = target + def.id; 18 | const { promiseCalc } = eventOptions; 19 | const setEventOptions = { 20 | protector, 21 | ...eventOptions, 22 | ...changeEvent 23 | }; 24 | 25 | if (protectKey in protector) { 26 | return; 27 | } 28 | 29 | protector[protectKey] = true; 30 | 31 | forEach(allSources, ({ 32 | sourceObject, 33 | sourceKey, 34 | isDelegated 35 | }) => { 36 | const value = isDelegated ? deepFind(sourceObject, sourceKey) : sourceObject[sourceKey]; 37 | values.push(value); 38 | }); 39 | 40 | let targetValue = handler.apply(object, values); 41 | 42 | if (promiseCalc) { 43 | if (!(targetValue instanceof Promise)) { 44 | targetValue = Promise.resolve(targetValue); 45 | } 46 | 47 | targetValue 48 | .then((promiseResult) => set(object, target, promiseResult, setEventOptions)) 49 | .catch((e) => { 50 | throw Error(e); 51 | }); 52 | } else { 53 | set(object, target, targetValue, setEventOptions); 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /packages/defi/src/unbindnode/_removebinding.js: -------------------------------------------------------------------------------- 1 | import removeListener from '../off/_removelistener'; 2 | import triggerOne from '../trigger/_triggerone'; 3 | import forEach from '../_helpers/foreach'; 4 | 5 | const spaceReg = /\s+/; 6 | 7 | // the function removes single binding for single object 8 | // called by unbindNode 9 | export default function removeBinding({ 10 | object, 11 | key, 12 | eventOptions, 13 | binding 14 | }) { 15 | const { 16 | bindingOptions, 17 | binder, 18 | node, 19 | nodeHandler, 20 | objectHandler 21 | } = binding; 22 | const { destroy, on } = binder; 23 | const { silent } = eventOptions; 24 | 25 | // if "on" is a function then disable it 26 | // we cannot "turn off" custom listener defined by a programmer 27 | // programmer needs to remove custom listener maually inside binder.destroy 28 | if (typeof on === 'function') { 29 | nodeHandler.disabled = true; 30 | } else if (typeof on === 'string') { 31 | // remove DOM event listener 32 | // removeEventListener is faster than "on" method from any DOM library 33 | forEach( 34 | on.split(spaceReg), 35 | (evtName) => node.removeEventListener(evtName, nodeHandler) 36 | ); 37 | } 38 | 39 | // remove object event listener 40 | removeListener(object, `_change:bindings:${key}`, objectHandler); 41 | 42 | // if binder.destroy is given call it 43 | if (destroy) { 44 | destroy.call(node, bindingOptions); 45 | } 46 | 47 | // fire events 48 | if (!silent) { 49 | const extendedEventOptions = { 50 | key, 51 | node, 52 | ...eventOptions 53 | }; 54 | 55 | triggerOne(object, `unbind:${key}`, extendedEventOptions); 56 | triggerOne(object, 'unbind', extendedEventOptions); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/react/test/spec/useChange.spec.js: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { useChange } from '../../npm'; 3 | import getWrapper from './getWrapper'; 4 | 5 | describe('useChange', () => { 6 | it('Should work', () => { 7 | const store = { x: 1 }; 8 | let renderedTimes = 0; 9 | const { result } = renderHook(() => { 10 | renderedTimes += 1; 11 | return useChange(store, 'x'); 12 | }); 13 | 14 | expect(result.current[0]).toBe(1); 15 | expect(store.x).toBe(1); 16 | expect(renderedTimes).toBe(1); 17 | 18 | act(() => { result.current[1](2); }); 19 | 20 | expect(result.current[0]).toBe(2); 21 | expect(store.x).toBe(2); 22 | expect(renderedTimes).toBe(2); 23 | 24 | act(() => { result.current[1](2); }); 25 | 26 | expect(result.current[0]).toBe(2); 27 | expect(store.x).toBe(2); 28 | expect(renderedTimes).toBe(2); 29 | }); 30 | 31 | it('Should use store selector', () => { 32 | const store = { x: { y: 1 } }; 33 | const wrapper = getWrapper(store); 34 | let renderedTimes = 0; 35 | const { result } = renderHook(() => { 36 | renderedTimes += 1; 37 | return useChange(({ x }) => x, 'y'); 38 | }, { wrapper }); 39 | 40 | expect(result.current[0]).toBe(1); 41 | expect(renderedTimes).toBe(1); 42 | expect(store.x.y).toBe(1); 43 | 44 | act(() => { result.current[1](2); }); 45 | 46 | expect(result.current[0]).toBe(2); 47 | expect(renderedTimes).toBe(2); 48 | expect(store.x.y).toBe(2); 49 | 50 | 51 | act(() => { result.current[1](2); }); 52 | 53 | expect(result.current[0]).toBe(2); 54 | expect(renderedTimes).toBe(2); 55 | expect(store.x.y).toBe(2); 56 | }); 57 | 58 | it('Should throw error if store selector is null', () => { 59 | const { result: { error } } = renderHook(() => useChange(null, 'y')); 60 | 61 | expect(error).toBeTruthy(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /packages/codemirror-binder/test/spec/common_spec.js: -------------------------------------------------------------------------------- 1 | import bindNode from '../../../defi/npm/bindnode'; 2 | import unbindNode from '../../../defi/npm/unbindnode'; 3 | import codeMirror from '../../src'; 4 | 5 | const noDebounceFlag = { debounceGetValue: false, debounceSetValue: false }; 6 | 7 | describe('Common', () => { 8 | let obj; 9 | let textarea; 10 | 11 | const getCodeMirrorInstance = () => { 12 | if (textarea.nextElementSibling) { 13 | return textarea.nextElementSibling.CodeMirror; 14 | } 15 | return null; 16 | }; 17 | beforeEach(() => { 18 | const { document } = window; 19 | obj = {}; 20 | textarea = document.body.appendChild(document.createElement('textarea')); 21 | }); 22 | 23 | it('should update textarea and CodeMirror when bound property is changed', () => { 24 | bindNode(obj, 'x', textarea, codeMirror(), noDebounceFlag); 25 | obj.x = 'foo'; 26 | 27 | expect(textarea.value).toEqual(obj.x); 28 | expect(getCodeMirrorInstance().getValue()).toEqual(obj.x); 29 | }); 30 | 31 | it('should update property and textarea value when CodeMirror is changed', () => { 32 | bindNode(obj, 'x', textarea, codeMirror(), noDebounceFlag); 33 | 34 | getCodeMirrorInstance().setValue('foo'); 35 | 36 | expect(textarea.value).toEqual(obj.x); 37 | expect(getCodeMirrorInstance().getValue()).toEqual(obj.x); 38 | }); 39 | 40 | it('should destroy when unbindNode is called', () => { 41 | bindNode(obj, 'x', textarea, codeMirror(), noDebounceFlag); 42 | unbindNode(obj, 'x', textarea); 43 | 44 | obj.x = 'foo'; 45 | 46 | expect(textarea.value).toEqual(''); 47 | expect(getCodeMirrorInstance()).toEqual(null); 48 | }); 49 | 50 | it('allows to pass config', () => { 51 | bindNode(obj, 'x', textarea, codeMirror({ 52 | foo: 'bar', 53 | }), noDebounceFlag); 54 | 55 | expect(getCodeMirrorInstance().getOption('foo')).toEqual('bar'); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/defi/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'airbnb-base', 4 | plugins: ['output-todo-comments'], 5 | parser: 'babel-eslint', 6 | rules: { 7 | indent: ['error', 4, { SwitchCase: 1 }], 8 | 'no-var': 'error', 9 | 'no-console': 'error', 10 | 'prefer-rest-params': 0, // arguments work faster 11 | 'no-param-reassign': ['error', { props: false }], 12 | 'no-underscore-dangle': 0, // for some hacks and array methods underscore prefix/suffix is required 13 | 'no-use-before-define': 0, // impossible to follow 14 | 'global-require': 0, // allow to fix circular refs 15 | 'new-cap': ['error', { capIsNewExceptions: ['Class'] }], 16 | 'comma-dangle': ['error', 'never'], // personal preference 17 | 'no-continue': 0, // continue statements are useful to flatten nested blocks 18 | 'import/no-extraneous-dependencies': 0, 19 | 'import/no-unresolved': ['error', { ignore: ['^src'] }], // allow to use 'src/' in tests 20 | 'no-cond-assign': ['error', 'except-parens'], // sometimes it's needed in while() 21 | 'max-lines': ['error', 210], // we may want to decrease this number later 22 | 'no-plusplus': 0, // x++ is used very often in loops 23 | 'class-methods-use-this': 0, // it't not required to use this in class methods 24 | 'no-bitwise': ['error', { allow: ['~'] }], // allow to use ~x.indexOf 25 | 'no-restricted-syntax': 0, // for..of is used at tests 26 | 'no-multi-assign': 0, // allow x = y = z 27 | 'prefer-destructuring': 0, // allow things like x = y[z] 28 | 'output-todo-comments/output-todo-comments': [ 29 | 'warn', { 30 | terms: ['todo'], 31 | location: 'start' 32 | } 33 | ] 34 | }, 35 | env: { 36 | jasmine: true 37 | }, 38 | globals: { 39 | window: true 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /packages/defi/src/binders/input.js: -------------------------------------------------------------------------------- 1 | // returns a binder for input element based on its type 2 | export default function input(type) { 3 | let on; 4 | switch (type) { 5 | case 'checkbox': 6 | return { 7 | on: 'click keyup', 8 | getValue() { 9 | return this.checked; 10 | }, 11 | setValue(value) { 12 | this.checked = value; 13 | } 14 | }; 15 | case 'radio': 16 | return { 17 | on: 'click keyup', 18 | getValue() { 19 | return this.value; 20 | }, 21 | setValue(value) { 22 | this.checked = typeof value !== 'undefined' && this.value === value; 23 | } 24 | }; 25 | case 'submit': 26 | case 'button': 27 | case 'image': 28 | case 'reset': 29 | return {}; 30 | case 'hidden': 31 | on = null; 32 | break; 33 | case 'file': 34 | on = 'change'; 35 | break; 36 | 37 | /* 38 | case 'text': 39 | case 'password': 40 | case 'date': 41 | case 'datetime': 42 | case 'datetime-local': 43 | case 'month': 44 | case 'time': 45 | case 'week': 46 | case 'range': 47 | case 'color': 48 | case 'search': 49 | case 'email': 50 | case 'tel': 51 | case 'url': 52 | case 'file': 53 | case 'number': */ 54 | default: // other future (HTML6+) inputs 55 | on = 'input'; 56 | } 57 | 58 | return { 59 | on, 60 | getValue() { 61 | return this.value; 62 | }, 63 | setValue(value) { 64 | this.value = value; 65 | } 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /packages/defi/src/_helpers/defierror.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-template, max-len */ 2 | const bindingErrorPrefix = 'Binding error:'; 3 | const calcErrorPrefix = 'Calc error:'; 4 | 5 | const getType = (variable) => { 6 | if (variable === null) { 7 | return 'null'; 8 | } 9 | 10 | return typeof variable; 11 | }; 12 | const getTypeError = (variable, variableName, expectedType) => `${variableName} must have type "${expectedType}" but got "${getType(variable)}" instead.`; 13 | 14 | const errors = { 15 | 'common:object_type': ({ object, method }) => `Error in ${method}: ` 16 | + getTypeError(object, 'object', 'object'), 17 | 18 | 'binding:node_missing': ({ key, node }) => { 19 | const selectorInfo = typeof node === 'string' ? ` (given selector is "${node}")` : ''; 20 | return `${bindingErrorPrefix} node is missing for key "${key}"${selectorInfo}.`; 21 | }, 22 | 'binding:falsy_key': () => `${bindingErrorPrefix} "key" arg cannot be falsy`, 23 | 'calc:target_type': ({ target }) => `${calcErrorPrefix} ${getTypeError(target, 'target key', 'string')}`, 24 | 'calc:source_key_type': ({ sourceKey }) => `${calcErrorPrefix} ${getTypeError(sourceKey, 'source key', 'string')}`, 25 | 'calc:source_object_type': ({ sourceObject }) => `${calcErrorPrefix} ${getTypeError(sourceObject, 'source object', 'object')}`, 26 | 'calc:source_type': ({ source }) => `${calcErrorPrefix} ${getTypeError(source, 'source', 'object')}`, 27 | 28 | 'remove:key_type': ({ key }) => `Error in remove: ${getTypeError(key, 'key', 'string')}`, 29 | 30 | 'mediate:key_type': ({ key }) => `Error in mediate: ${getTypeError(key, 'key', 'string')}` 31 | }; 32 | 33 | export default function defiError(key, data) { 34 | const getError = errors[key]; 35 | if (!getError) { 36 | /* istanbul ignore next */ 37 | throw Error(`Unknown error "${key}". Please report about this on Github.`); 38 | } 39 | 40 | return new Error(getError(data)); 41 | } 42 | -------------------------------------------------------------------------------- /packages/defi/src/bindnode/_createnodehandler.js: -------------------------------------------------------------------------------- 1 | import is from '../_helpers/is'; 2 | import set from '../set'; 3 | 4 | // returns a function which called when bound node state is changed (eg DOM event is fired) 5 | export default function createNodeHandler({ 6 | object, 7 | key, 8 | node, 9 | propDef, 10 | binder, 11 | bindingOptions 12 | }) { 13 | return function nodeHandler(domEvent = {}) { 14 | // nodeHandler.disabled = true is set in unbindNode 15 | // we cannot "turn off" binder.on when its value is a function 16 | // developer needs to clean memory ("turn off" callback) manualy in binder.destroy 17 | if (nodeHandler.disabled) { 18 | return; 19 | } 20 | 21 | const previousValue = propDef.value; 22 | const { 23 | which, target, ctrlKey, altKey 24 | } = domEvent; 25 | const { getValue } = binder; 26 | const value = getValue.call(node, { 27 | previousValue, 28 | domEvent, 29 | originalEvent: domEvent.originalEvent || domEvent, // jQuery thing 30 | // will throw "preventDefault is not a function" when domEvent is empty object 31 | preventDefault: () => domEvent.preventDefault(), 32 | // will throw "stopPropagation is not a function" when domEvent is empty object 33 | stopPropagation: () => domEvent.stopPropagation(), 34 | which, 35 | target, 36 | ctrlKey, 37 | altKey, 38 | ...bindingOptions 39 | }); 40 | 41 | if (!is(value, previousValue)) { 42 | set(object, key, value, { 43 | fromNode: true, 44 | // the following properties are needed to avoid circular changes 45 | // they are used at objectHandler 46 | changedNode: node, 47 | onChangeValue: value, 48 | binder 49 | }); 50 | } 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /packages/defi/src/mediate.js: -------------------------------------------------------------------------------- 1 | import initDefi from './_core/init'; 2 | import defineProp from './_core/defineprop'; 3 | import checkObjectType from './_helpers/checkobjecttype'; 4 | import set from './set'; 5 | import defiError from './_helpers/defierror'; 6 | import forOwn from './_helpers/forown'; 7 | import forEach from './_helpers/foreach'; 8 | 9 | // creates property mediator 10 | function createMediator({ 11 | object, 12 | propDef, 13 | key, 14 | mediator 15 | }) { 16 | return function propMediator(value) { 17 | // args: value, previousValue, key, object itself 18 | return mediator.call(object, value, propDef.value, key, object); 19 | }; 20 | } 21 | 22 | // transforms property value on its changing 23 | export default function mediate(object, givenKeys, mediator) { 24 | // throw error when object type is wrong 25 | checkObjectType(object, 'mediate'); 26 | 27 | const isKeysArray = givenKeys instanceof Array; 28 | 29 | // allow to use key-mediator object as another method variation 30 | if (typeof givenKeys === 'object' && !isKeysArray) { 31 | forOwn(givenKeys, (objVal, objKey) => mediate(object, objKey, objVal)); 32 | return object; 33 | } 34 | 35 | initDefi(object); 36 | 37 | // allow to use both single key and an array of keys 38 | const keys = isKeysArray ? givenKeys : [givenKeys]; 39 | 40 | forEach(keys, (key) => { 41 | // if non-string is passed as a key 42 | if (typeof key !== 'string') { 43 | throw defiError('mediate:key_type', { key }); 44 | } 45 | 46 | const propDef = defineProp(object, key); 47 | 48 | const propMediator = propDef.mediator = createMediator({ 49 | object, 50 | propDef, 51 | key, 52 | mediator 53 | }); 54 | 55 | // set new value 56 | set(object, key, propMediator(propDef.value), { 57 | fromMediator: true 58 | }); 59 | }); 60 | 61 | return object; 62 | } 63 | -------------------------------------------------------------------------------- /packages/defi/src/on/_addtreelistener.js: -------------------------------------------------------------------------------- 1 | import delegateListener from './_delegatelistener'; 2 | import removeTreeListener from '../off/_removetreelistener'; 3 | 4 | // creates tree listener 5 | function createTreeListener({ handler, restPath }) { 6 | const newHandler = function treeListener(changeEvent) { 7 | const extendedChangeEvent = { 8 | restPath, 9 | ...changeEvent 10 | }; 11 | const { previousValue, value } = changeEvent; 12 | 13 | // removes listener for all branches of the path on old object 14 | if (previousValue && typeof previousValue === 'object') { 15 | removeTreeListener(previousValue, restPath, handler); 16 | } 17 | 18 | // adds listener for all branches of "restPath" path on newly assigned object 19 | if (value && typeof value === 'object') { 20 | addTreeListener(value, restPath, handler); 21 | } 22 | 23 | // call original handler 24 | handler.call(this, extendedChangeEvent); 25 | }; 26 | 27 | newHandler._callback = handler; 28 | 29 | return newHandler; 30 | } 31 | 32 | // listens changes for all branches of given path 33 | // one of the most hard functions to understand 34 | export default function addTreeListener(object, deepPath, handler) { 35 | if (typeof deepPath === 'string') { 36 | deepPath = deepPath.split('.'); // eslint-disable-line no-param-reassign 37 | } 38 | 39 | // iterate over all keys and delegate listener for all objects of given branch 40 | for (let i = 0; i < deepPath.length; i++) { 41 | // TODO: Array.prototype.slice method is slow 42 | const listenPath = deepPath.slice(0, i); 43 | const restPath = deepPath.slice(i + 1); 44 | 45 | delegateListener( 46 | object, 47 | listenPath, 48 | `_change:tree:${deepPath[i]}`, 49 | createTreeListener({ 50 | handler, 51 | restPath 52 | }) 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/defi/src/off/index.js: -------------------------------------------------------------------------------- 1 | import checkObjectType from '../_helpers/checkobjecttype'; 2 | import forEach from '../_helpers/foreach'; 3 | import forOwn from '../_helpers/forown'; 4 | import defs from '../_core/defs'; 5 | import removeListener from './_removelistener'; 6 | import undelegateListener from './_undelegatelistener'; 7 | import $ from '../_mq'; 8 | 9 | // removes event listener 10 | export default function off(object, givenNames, callback) { 11 | // throw error when object type is wrong 12 | checkObjectType(object, 'off'); 13 | 14 | const isNamesVarArray = givenNames instanceof Array; 15 | const def = defs.get(object); 16 | 17 | // allow to pass name-handler object 18 | // TODO: Name-handler object passed to off method is non-documented feature 19 | if (givenNames && typeof givenNames === 'object' && !isNamesVarArray) { 20 | forOwn(givenNames, (namesObjCallback, namesObjName) => off( 21 | object, namesObjName, namesObjCallback, callback 22 | )); 23 | return object; 24 | } 25 | 26 | 27 | if (!givenNames && !callback) { 28 | def.events = {}; 29 | 30 | forOwn(def.props, ({ bindings }, propName) => { 31 | if (bindings) { 32 | forEach(bindings, ({ node }) => { 33 | const eventNamespace = def.id + propName; 34 | $(node).off(`.${eventNamespace}`); 35 | }); 36 | } 37 | }); 38 | 39 | return object; 40 | } 41 | 42 | // convert a single event name into array 43 | const names = isNamesVarArray ? givenNames : [givenNames]; 44 | 45 | forEach(names, (name) => { 46 | const delegatedEventParts = typeof name === 'string' && name.split('@'); 47 | if (delegatedEventParts.length > 1) { 48 | const [path, delegatedName] = delegatedEventParts; 49 | undelegateListener(object, path, delegatedName, callback); 50 | } else { 51 | removeListener(object, name, callback); 52 | } 53 | }); 54 | 55 | return object; 56 | } 57 | -------------------------------------------------------------------------------- /packages/defi/src/off/_undelegatelistener.js: -------------------------------------------------------------------------------- 1 | import defs from '../_core/defs'; 2 | import removeListener from './_removelistener'; 3 | import slice from '../_helpers/slice'; 4 | import forEach from '../_helpers/foreach'; 5 | 6 | // the function removes internally used events such as _asterisk:add 7 | function detatchDelegatedLogic({ 8 | delegatedEventName, 9 | pathStr, 10 | allEvents 11 | }) { 12 | const retain = []; 13 | const events = allEvents[delegatedEventName]; 14 | 15 | forEach(events, (event) => { 16 | // pathStr is assigned to info in delegateListener 17 | if (event.info.pathStr !== pathStr) { 18 | retain.push(event); 19 | } 20 | }); 21 | 22 | if (retain.length) { 23 | allEvents[delegatedEventName] = retain; 24 | } else { 25 | delete allEvents[delegatedEventName]; 26 | } 27 | } 28 | 29 | // removes delegated event listener from an object by given path 30 | export default function undelegateListener(object, givenPath, name, callback, info = {}) { 31 | const def = defs.get(object); 32 | 33 | // if no definition do nothing 34 | if (!def) { 35 | return; 36 | } 37 | 38 | const { events: allEvents } = def; 39 | 40 | let path = typeof givenPath === 'string' && givenPath !== '' ? givenPath.split('.') : givenPath; 41 | 42 | if (!path || !path.length) { 43 | // if no path then remove listener 44 | removeListener(object, name, callback, info); 45 | } else { 46 | // else do all magic 47 | const key = path[0]; 48 | let pathStr; 49 | 50 | if (path.length > 1) { 51 | path = slice(path, 1); 52 | pathStr = path.join('.'); 53 | } else { 54 | path = []; 55 | pathStr = path[0] || ''; 56 | } 57 | 58 | 59 | const delegatedChangeEvtName = `_change:delegated:${key}`; 60 | if (allEvents[delegatedChangeEvtName]) { 61 | detatchDelegatedLogic({ 62 | delegatedEventName: delegatedChangeEvtName, 63 | pathStr, 64 | allEvents 65 | }); 66 | } 67 | 68 | if (typeof object[key] === 'object') { 69 | undelegateListener(object[key], path, name, callback, info); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/defi/test/spec/mq/init_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import $ from 'src/_mq'; 3 | 4 | describe('mq initialization', () => { 5 | let testSandbox; 6 | 7 | beforeEach(() => { 8 | testSandbox = window.document.createElement('div'); 9 | 10 | testSandbox.innerHTML = ` 11 |
12 |
13 |
14 |
15 |
16 | `; 17 | }); 18 | 19 | it('accepts window', () => { 20 | const result = $(window); 21 | expect(result.length).toEqual(1); 22 | expect(result[0]).toEqual(window); 23 | }); 24 | 25 | it('accepts document', () => { 26 | const result = $(window.document); 27 | expect(result.length).toEqual(1); 28 | expect(result[0]).toEqual(window.document); 29 | }); 30 | 31 | it('parses HTML', () => { 32 | const result = $('
'); 33 | 34 | expect(result.length).toEqual(2); 35 | expect(result[0].tagName).toEqual('DIV'); 36 | expect(result[1].tagName).toEqual('SPAN'); 37 | }); 38 | 39 | it('converts array-like', () => { 40 | const children = testSandbox.querySelectorAll('*'); 41 | const result = $(children); 42 | 43 | expect(children.length).toEqual(result.length); 44 | 45 | for (let i = 0; i < children.length; i++) { 46 | expect(children[i]).toEqual(result[i]); 47 | } 48 | }); 49 | 50 | it('converts one element', () => { 51 | const element = window.document.querySelector('*'); 52 | const result = $(element); 53 | 54 | expect(result.length).toEqual(1); 55 | expect(element).toEqual(result[0]); 56 | }); 57 | 58 | it('uses context', () => { 59 | expect($('.test-1', testSandbox).length).toEqual(1); 60 | }); 61 | 62 | it('does not use wrong context', () => { 63 | expect($('.test-1', '.wrong-context').length).toEqual(0); 64 | }); 65 | 66 | it('allows to pass null', () => { 67 | expect($(null).length).toEqual(0); 68 | }); 69 | 70 | it('allows to pass nothing', () => { 71 | expect($().length).toEqual(0); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/defi/src/on/_adddomlistener.js: -------------------------------------------------------------------------------- 1 | import initDefi from '../_core/init'; 2 | import defineProp from '../_core/defineprop'; 3 | import addListener from './_addlistener'; 4 | import $ from '../_mq'; 5 | import createDomEventHandler from './_createdomeventhandler'; 6 | import forEach from '../_helpers/foreach'; 7 | 8 | // returns an object with event handlers used at addDomListener 9 | function createBindingHandlers({ 10 | fullEventName, 11 | domEventHandler, 12 | selector 13 | }) { 14 | return { 15 | bindHandler(evt = {}) { 16 | const { node } = evt; 17 | if (node) { 18 | $(node).on(fullEventName, selector, domEventHandler); 19 | } 20 | }, 21 | unbindHandler(evt = {}) { 22 | const { node } = evt; 23 | if (node) { 24 | $(node).off(fullEventName, selector, domEventHandler); 25 | } 26 | } 27 | }; 28 | } 29 | 30 | // adds DOM event listener for nodes bound to given property 31 | export default function addDomListener(object, key, eventName, selector, callback, info) { 32 | const def = initDefi(object); 33 | const propDef = defineProp(object, key); 34 | 35 | const domEventHandler = createDomEventHandler({ 36 | key, 37 | object, 38 | callback 39 | }); 40 | 41 | // making possible to remove this event listener 42 | domEventHandler._callback = callback; 43 | 44 | const eventNamespace = def.id + key; 45 | const fullEventName = `${eventName}.${eventNamespace}`; 46 | const { bindHandler, unbindHandler } = createBindingHandlers({ 47 | fullEventName, 48 | domEventHandler, 49 | selector 50 | }); 51 | const addBindListenerResult = addListener(object, `bind:${key}`, bindHandler, info); 52 | const addUnbindListenerResult = addListener(object, `unbind:${key}`, unbindHandler, info); 53 | 54 | // if events are added successfully then run bindHandler for every node immediately 55 | // TODO: Describe why do we need addBindListenerResult and addUnbindListenerResult 56 | if (addBindListenerResult && addUnbindListenerResult) { 57 | const { bindings } = propDef; 58 | if (bindings) { 59 | forEach(bindings, ({ node }) => bindHandler({ node })); 60 | } 61 | } 62 | 63 | return object; 64 | } 65 | -------------------------------------------------------------------------------- /test/post-publish/post-publish.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | execSync('rm -rf node_modules && npm i --no-package-lock', { cwd: __dirname }); 4 | // remove root dependencies to avoid usage of them 5 | execSync('rm -rf ../../node_modules', { cwd: __dirname }); 6 | 7 | const { JSDOM } = require('jsdom'); 8 | // eslint-disable-next-line import/no-unresolved 9 | const expect = require('expect.js'); 10 | 11 | 12 | global.window = new JSDOM('
', { 13 | url: 'http://localhost', 14 | }).window; 15 | 16 | global.document = global.window.document; 17 | global.navigator = global.window.navigator; 18 | 19 | const defi = require('defi'); // eslint-disable-line import/no-unresolved 20 | const Router = require('defi-router/router'); // eslint-disable-line import/no-unresolved 21 | const initRouter = require('defi-router'); // eslint-disable-line import/no-unresolved 22 | 23 | const codemirrorBinder = require('codemirror-binder'); // eslint-disable-line import/no-unresolved 24 | const commonBinders = require('common-binders'); // eslint-disable-line import/no-unresolved 25 | const fileBinders = require('file-binders'); // eslint-disable-line import/no-unresolved 26 | const defiReact = require('defi-react'); // eslint-disable-line import/no-unresolved 27 | 28 | // check if defi itself is OK 29 | const obj = { b: 3 }; 30 | defi.calc(obj, 'a', 'b', (b) => b * 2); 31 | expect(obj.a).to.eql(6); 32 | 33 | // check if router is OK 34 | const customRouter = new Router('custom'); 35 | customRouter.subscribe(obj, '/a/'); 36 | expect(customRouter.path).to.eql('/6/'); 37 | expect(typeof initRouter === 'function'); 38 | 39 | 40 | // check if binders are OK 41 | expect(typeof codemirrorBinder === 'function').to.be(true); 42 | expect(typeof codemirrorBinder().setValue === 'function').to.be(true); 43 | 44 | expect(typeof commonBinders.html === 'function').to.be(true); 45 | expect(typeof commonBinders.html().setValue === 'function').to.be(true); 46 | 47 | expect(typeof fileBinders.file === 'function').to.be(true); 48 | expect(typeof fileBinders.file().getValue === 'function').to.be(true); 49 | 50 | // check if defi-react is OK 51 | expect(typeof defiReact.useChange === 'function').to.be(true); 52 | 53 | // return main dependencies back 54 | execSync('npm install --prefix ../..', { cwd: __dirname }); 55 | -------------------------------------------------------------------------------- /packages/defi/src/_mq/off.js: -------------------------------------------------------------------------------- 1 | import data from './_data'; 2 | 3 | const splitBySpaceReg = /\s+/; 4 | const splitByDotReg = /\.(.+)/; 5 | 6 | // removes event handler from a set of elements 7 | export default function off(namesStr, selector, handler) { 8 | if (typeof selector === 'function') { 9 | handler = selector; // eslint-disable-line no-param-reassign 10 | selector = null; // eslint-disable-line no-param-reassign 11 | } 12 | 13 | const names = namesStr.split(splitBySpaceReg); 14 | 15 | for (let i = 0; i < names.length; i++) { 16 | const [name, namespace] = names[i].split(splitByDotReg); 17 | 18 | for (let j = 0; j < this.length; j++) { 19 | const node = this[j]; 20 | 21 | if (!name && namespace) { 22 | for (let k = 0, keys = Object.keys(data.allEvents); k < keys.length; k++) { 23 | const events = data.allEvents[keys[k]]; 24 | 25 | for (let l = 0; l < events.length; l++) { 26 | const event = events[i]; 27 | if (event.namespace === namespace && event.nodeID === node.b$) { 28 | node.removeEventListener(event.name, event.delegate || event.handler); 29 | events.splice(l, 1); 30 | l -= 1; 31 | } 32 | } 33 | } 34 | 35 | continue; 36 | } 37 | 38 | const events = data.allEvents[name + node.b$]; 39 | if (events) { 40 | for (let k = 0; k < events.length; k++) { 41 | const event = events[k]; 42 | if ( 43 | (!handler || handler === event.handler || handler === event.delegate) 44 | && (!namespace || namespace === event.namespace) 45 | && (!selector || selector === event.selector) 46 | ) { 47 | node.removeEventListener(name, event.delegate || event.handler); 48 | events.splice(k, 1); 49 | k -= 1; 50 | } 51 | } 52 | } else if (!namespace && !selector) { 53 | node.removeEventListener(name, handler); 54 | } 55 | } 56 | } 57 | 58 | return this; 59 | } 60 | -------------------------------------------------------------------------------- /packages/react/test/spec/useOn.spec.js: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { trigger } from 'defi'; 3 | import { useOn } from '../../npm'; 4 | import getWrapper from './getWrapper'; 5 | 6 | describe('useOn', () => { 7 | it('Should work', () => { 8 | const store = {}; 9 | let renderedTimes = 0; 10 | 11 | const { result } = renderHook(() => { 12 | renderedTimes += 1; 13 | return useOn(store, 'foo'); 14 | }); 15 | const returnedTrigger = result.current; 16 | 17 | expect(typeof returnedTrigger === 'function').toBeTrue(); 18 | expect(renderedTimes).toBe(1); 19 | 20 | let arg = { a: 'b' }; 21 | act(() => { trigger(store, 'foo', arg); }); 22 | 23 | expect(returnedTrigger).toBe(result.current); 24 | expect(renderedTimes).toBe(2); 25 | expect(arg).toBe(returnedTrigger.latest); 26 | expect([arg]).toEqual(returnedTrigger.latestAll); 27 | act(() => { trigger(store, 'bar', arg); }); 28 | 29 | expect(returnedTrigger).toBe(result.current); 30 | expect(renderedTimes).toBe(2); 31 | expect(arg).toBe(returnedTrigger.latest); 32 | expect([arg]).toEqual(returnedTrigger.latestAll); 33 | 34 | arg = { c: 'd' }; 35 | act(() => { trigger(store, 'foo', arg); }); 36 | 37 | expect(returnedTrigger).toBe(result.current); 38 | expect(renderedTimes).toBe(3); 39 | expect(arg).toBe(returnedTrigger.latest); 40 | expect([arg]).toEqual(returnedTrigger.latestAll); 41 | }); 42 | 43 | it('Should use store selector', () => { 44 | const store = { x: { y: 1 } }; 45 | const wrapper = getWrapper(store); 46 | let renderedTimes = 0; 47 | const { result } = renderHook(() => { 48 | renderedTimes += 1; 49 | return useOn(({ x }) => x, 'foo'); 50 | }, { wrapper }); 51 | 52 | const returnedTrigger = result.current; 53 | 54 | expect(renderedTimes).toBe(1); 55 | 56 | const arg = { a: 'b' }; 57 | act(() => { trigger(store.x, 'foo', arg); }); 58 | 59 | expect(returnedTrigger).toBe(result.current); 60 | expect(renderedTimes).toBe(2); 61 | expect(arg).toBe(returnedTrigger.latest); 62 | expect([arg]).toEqual(returnedTrigger.latestAll); 63 | act(() => { trigger(store.x, 'bar', arg); }); 64 | }); 65 | 66 | it('Should throw error if store selector is null', () => { 67 | const { result: { error } } = renderHook(() => useOn(null, 'y')); 68 | 69 | expect(error).toBeTruthy(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/defi/test/spec/events/events_core_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import addListener from 'src/on/_addlistener'; 3 | import removeListener from 'src/off/_removelistener'; 4 | import triggerOne from 'src/trigger/_triggerone'; 5 | import createSpy from '../../helpers/createspy'; 6 | 7 | describe('Events core (addListener, removeListener, triggerOne)', () => { 8 | let obj; 9 | let handler; 10 | 11 | beforeEach(() => { 12 | obj = {}; 13 | handler = createSpy(); 14 | }); 15 | 16 | it('fires', () => { 17 | addListener(obj, 'someevent', handler); 18 | triggerOne(obj, 'someevent'); 19 | expect(handler).toHaveBeenCalled(); 20 | }); 21 | 22 | it('uses correct context', () => { 23 | addListener(obj, 'someevent', function handle() { 24 | expect(obj === this).toEqual(true); 25 | }); 26 | triggerOne(obj, 'someevent'); 27 | }); 28 | 29 | it('avoids conflicts', () => { 30 | let i = 0; 31 | // eslint-disable-next-line no-return-assign 32 | addListener(obj, 'someevent', () => i += 1e0); 33 | // eslint-disable-next-line no-return-assign 34 | addListener(obj, 'someevent', () => i += 1e1); 35 | // eslint-disable-next-line no-return-assign 36 | addListener(obj, 'someevent', () => i += 1e2); 37 | triggerOne(obj, 'someevent'); 38 | 39 | expect(i).toEqual(111); 40 | }); 41 | 42 | it('removes all', () => { 43 | addListener(obj, 'someevent', handler); 44 | removeListener(obj); 45 | triggerOne(obj, 'someevent'); 46 | expect(handler).not.toHaveBeenCalled(); 47 | }); 48 | 49 | it('removes by name', () => { 50 | addListener(obj, 'someevent', handler); 51 | removeListener(obj, 'someevent'); 52 | triggerOne(obj, 'someevent'); 53 | expect(handler).not.toHaveBeenCalled(); 54 | }); 55 | 56 | it('removes by callback', () => { 57 | addListener(obj, 'someevent', handler); 58 | removeListener(obj, 'someevent', handler); 59 | triggerOne(obj, 'someevent'); 60 | expect(handler).not.toHaveBeenCalled(); 61 | }); 62 | 63 | it('removes by callback but keeps when callbacks are not same', () => { 64 | addListener(obj, 'someevent', handler); 65 | removeListener(obj, 'someevent', () => {}); 66 | triggerOne(obj, 'someevent'); 67 | expect(handler).toHaveBeenCalled(); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /packages/defi/src/on/index.js: -------------------------------------------------------------------------------- 1 | import checkObjectType from '../_helpers/checkobjecttype'; 2 | import off from '../off'; 3 | import debounce from '../_helpers/debounce'; 4 | import forEach from '../_helpers/foreach'; 5 | import forOwn from '../_helpers/forown'; 6 | import addListener from './_addlistener'; 7 | import delegateListener from './_delegatelistener'; 8 | 9 | // adds event listener 10 | export default function on(object, givenNames, givenCallback, options) { 11 | // throw error when object type is wrong 12 | checkObjectType(object, 'on'); 13 | 14 | const isNamesVarArray = givenNames instanceof Array; 15 | 16 | // allow to pass name-handler object 17 | if (givenNames && typeof givenNames === 'object' && !isNamesVarArray) { 18 | forOwn(givenNames, (namesObjCallback, namesObjName) => on( 19 | object, namesObjName, namesObjCallback, givenCallback, options 20 | )); 21 | return object; 22 | } 23 | 24 | // convert a single event name into array 25 | const names = isNamesVarArray ? givenNames : [givenNames]; 26 | 27 | const { triggerOnInit, once, debounce: debounceOption } = options || {}; 28 | let callback; 29 | if (once) { 30 | callback = function onceCallback() { 31 | givenCallback.apply(this, arguments); 32 | // remove event listener after its call 33 | off(object, names, onceCallback); 34 | }; 35 | 36 | // allow to remove event listener py passing original callback to "off" 37 | callback._callback = givenCallback; 38 | } else if (typeof debounceOption === 'number' || debounceOption === true) { 39 | callback = debounce(givenCallback, debounceOption === true ? 0 : debounceOption, object); 40 | } else { 41 | callback = givenCallback; 42 | } 43 | 44 | forEach(names, (name) => { 45 | const delegatedEventParts = typeof name === 'string' && name.split('@'); 46 | 47 | if (delegatedEventParts.length > 1) { 48 | // if @ exists in event name then this is delegated event 49 | const [path, delegatedName] = delegatedEventParts; 50 | delegateListener(object, path, delegatedName, callback); 51 | } else { 52 | // if not, this is simple event 53 | addListener(object, name, callback); 54 | } 55 | }); 56 | 57 | // call callback immediatelly if triggerOnInit is true 58 | if (triggerOnInit) { 59 | callback.call(object, options); 60 | } 61 | 62 | return object; 63 | } 64 | -------------------------------------------------------------------------------- /packages/defi/src/bindnode/_createbindingswitcher.js: -------------------------------------------------------------------------------- 1 | import unbindNode from '../unbindnode'; 2 | 3 | // returns a function which re-adds binding when object branch is changed 4 | // the function is called by bindNode when something like 5 | // 'foo.bar.baz' is passed to it as key argument value 6 | // this is one of the hardest things in the framework to understand 7 | export default function createBindingSwitcher({ 8 | object, 9 | deepPath, 10 | $nodes, 11 | binder, 12 | eventOptions, 13 | bindNode 14 | }) { 15 | return function bindingSwitcher(changeEvent = {}) { 16 | const deepPathLength = deepPath.length; 17 | const lastDeepPathItem = deepPath[deepPathLength - 1]; 18 | const { 19 | value, // new value of a branch 20 | previousValue, // previous value of a branch 21 | restPath // path starting currently changed branch (passed by addTreeListener) 22 | } = changeEvent; 23 | let target; // an object to call bindNode 24 | let previousTarget; // an object to call unbindNode 25 | 26 | 27 | if (value && typeof value === 'object' && restPath) { 28 | // if rest path is given and new value is an object 29 | target = value; 30 | for (let i = 0; i < restPath.length; i++) { 31 | target = target[restPath[i]]; 32 | if (!target) { 33 | break; 34 | } 35 | } 36 | } else { 37 | // if rest path is not given 38 | target = object; 39 | for (let i = 0; i < deepPathLength - 1; i++) { 40 | target = target[deepPath[i]]; 41 | if (!target) { 42 | break; 43 | } 44 | } 45 | } 46 | 47 | // if rest path is given and previous value is an object 48 | if (previousValue && typeof previousValue === 'object' && restPath) { 49 | previousTarget = previousValue; 50 | for (let i = 0; i < restPath.length; i++) { 51 | previousTarget = previousTarget[restPath[i]]; 52 | if (!previousTarget) { 53 | break; 54 | } 55 | } 56 | } 57 | 58 | // add binding for new target 59 | if (target && typeof target === 'object') { 60 | bindNode(target, lastDeepPathItem, $nodes, binder, eventOptions); 61 | } 62 | 63 | // remove binding for previously used object 64 | if (previousTarget && typeof previousTarget === 'object') { 65 | unbindNode(previousTarget, lastDeepPathItem, $nodes); 66 | } 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /packages/react/test/spec/useTrigger.spec.js: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react-hooks'; 2 | import { trigger, on } from 'defi'; 3 | import { useTrigger } from '../../npm'; 4 | import getWrapper from './getWrapper'; 5 | 6 | describe('useTrigger', () => { 7 | it('Should work', () => { 8 | const store = {}; 9 | let renderedTimes = 0; 10 | let triggeredTimes = 0; 11 | 12 | on(store, 'foo', () => { triggeredTimes += 1; }); 13 | 14 | const { result } = renderHook(() => { 15 | renderedTimes += 1; 16 | return useTrigger(store, 'foo'); 17 | }); 18 | const returnedTrigger = result.current; 19 | 20 | expect(typeof returnedTrigger === 'function').toBeTrue(); 21 | expect(renderedTimes).toBe(1); 22 | expect(triggeredTimes).toBe(0); 23 | 24 | let arg = { a: 'b' }; 25 | act(() => { trigger(store, 'foo', arg); }); 26 | 27 | expect(returnedTrigger).toBe(result.current); 28 | expect(renderedTimes).toBe(1); 29 | expect(triggeredTimes).toBe(1); 30 | expect(arg).toBe(returnedTrigger.latest); 31 | expect([arg]).toEqual(returnedTrigger.latestAll); 32 | act(() => { trigger(store, 'bar', arg); }); 33 | 34 | expect(returnedTrigger).toBe(result.current); 35 | expect(renderedTimes).toBe(1); 36 | expect(triggeredTimes).toBe(1); 37 | expect(arg).toBe(returnedTrigger.latest); 38 | expect([arg]).toEqual(returnedTrigger.latestAll); 39 | 40 | arg = { c: 'd' }; 41 | act(() => { trigger(store, 'foo', arg); }); 42 | 43 | expect(returnedTrigger).toBe(result.current); 44 | expect(renderedTimes).toBe(1); 45 | expect(triggeredTimes).toBe(2); 46 | expect(arg).toBe(returnedTrigger.latest); 47 | expect([arg]).toEqual(returnedTrigger.latestAll); 48 | }); 49 | 50 | it('Should use store selector', () => { 51 | const store = { x: { y: 1 } }; 52 | const wrapper = getWrapper(store); 53 | let renderedTimes = 0; 54 | const { result } = renderHook(() => { 55 | renderedTimes += 1; 56 | return useTrigger(({ x }) => x, 'foo'); 57 | }, { wrapper }); 58 | 59 | const returnedTrigger = result.current; 60 | 61 | expect(renderedTimes).toBe(1); 62 | 63 | const arg = { a: 'b' }; 64 | act(() => { trigger(store.x, 'foo', arg); }); 65 | 66 | expect(returnedTrigger).toBe(result.current); 67 | expect(renderedTimes).toBe(1); 68 | expect(arg).toBe(returnedTrigger.latest); 69 | expect([arg]).toEqual(returnedTrigger.latestAll); 70 | }); 71 | 72 | it('Should throw error if store selector is null', () => { 73 | const { result: { error } } = renderHook(() => useTrigger(null, 'y')); 74 | 75 | expect(error).toBeTruthy(); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /packages/defi/src/remove.js: -------------------------------------------------------------------------------- 1 | import unbindNode from './unbindnode'; 2 | import triggerOne from './trigger/_triggerone'; 3 | import removeListener from './off/_removelistener'; 4 | import defs from './_core/defs'; 5 | import checkObjectType from './_helpers/checkobjecttype'; 6 | import defiError from './_helpers/defierror'; 7 | import forEach from './_helpers/foreach'; 8 | 9 | // removes a property, its bindings and its events 10 | // TODO: remove function does not correctly removes delegated events, bindings, tree listeners etc 11 | export default function remove(object, givenKey, eventOptions) { 12 | // throw error when object type is wrong 13 | checkObjectType(object, 'remove'); 14 | 15 | eventOptions = eventOptions || {}; // eslint-disable-line no-param-reassign 16 | const def = defs.get(object); 17 | const { silent } = eventOptions; 18 | // allow to pass single key or an array of keys 19 | const keys = givenKey instanceof Array ? givenKey : [givenKey]; 20 | 21 | for (let i = 0; i < keys.length; i++) { 22 | const key = keys[i]; 23 | 24 | // if non-string is passed as a key 25 | if (typeof key !== 'string') { 26 | throw defiError('remove:key_type', { key }); 27 | } 28 | 29 | const props = def && def.props; 30 | const propDef = props && props[key]; 31 | 32 | // if no object definition then simply delete the property 33 | if (!propDef) { 34 | delete object[key]; 35 | continue; 36 | } 37 | 38 | const { value } = propDef; 39 | 40 | // remove all bindings 41 | unbindNode(object, key); 42 | 43 | // TODO: Manual listing of event prefixes may cause problems in future 44 | const removeEventPrefies = [ 45 | '_change:deps', 46 | '_change:bindings', 47 | '_change:delegated', 48 | '_change:tree', 49 | 'change', 50 | 'beforechange', 51 | 'bind', 52 | 'unbind' 53 | ]; 54 | 55 | // remove all events 56 | forEach(removeEventPrefies, (prefix) => removeListener(object, `${prefix}:${key}`)); 57 | 58 | // delete property definition 59 | delete props[key]; 60 | 61 | // delete the property itself 62 | delete object[key]; 63 | 64 | const extendedEventOptions = { 65 | key, 66 | value, 67 | ...eventOptions 68 | }; 69 | 70 | // trigger delegated events logic removal for asterisk events (*.*.*@foo) 71 | triggerOne(object, '_delete:delegated', extendedEventOptions); 72 | 73 | // fire events if "silent" is not true 74 | if (!silent) { 75 | triggerOne(object, 'delete', extendedEventOptions); 76 | triggerOne(object, `delete:${key}`, extendedEventOptions); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/defi/src/on/_addlistener.js: -------------------------------------------------------------------------------- 1 | import initDefi from '../_core/init'; 2 | import triggerOne from '../trigger/_triggerone'; 3 | import defineProp from '../_core/defineprop'; 4 | import domEventReg from './_domeventregexp'; 5 | 6 | // property modifier event regexp 7 | // eslint-disable-next-line max-len 8 | const propModEventReg = /^_change:deps:|^_change:bindings:|^_change:delegated:|^_change:common:|^_change:tree:|^change:|^beforechange:/; 9 | 10 | // adds simple event listener 11 | // used as core of event engine 12 | export default function addListener(object, name, callback, info = {}) { 13 | const { events: allEvents } = initDefi(object); 14 | const events = allEvents[name]; 15 | const event = { 16 | callback, name, info 17 | }; 18 | const nameIsString = typeof name === 'string'; 19 | 20 | // skipChecks is used by internal methods for better performance 21 | const { skipChecks = false } = info; 22 | 23 | if (!skipChecks) { 24 | const domEventExecResult = nameIsString && domEventReg.exec(name); 25 | 26 | if (domEventExecResult) { 27 | const [, eventName, key, selector] = domEventExecResult; 28 | // fixing circular reference issue 29 | const addDomListenerReq = require('./_adddomlistener'); 30 | const addDomListener = addDomListenerReq.default || addDomListenerReq; 31 | addDomListener(object, key, eventName, selector, callback, info); 32 | 33 | return true; 34 | } 35 | } 36 | 37 | // if there are events with the same name 38 | if (events) { 39 | if (!skipChecks) { 40 | // if there are events with the same data, return false 41 | for (let i = 0; i < events.length; i++) { 42 | const existingEvent = events[i]; 43 | const argCallback = (callback && callback._callback) || callback; 44 | const eventCallback = existingEvent.callback._callback || existingEvent.callback; 45 | if (argCallback === eventCallback) { 46 | return false; 47 | } 48 | } 49 | } 50 | 51 | // if the event isn't found add it to the event list 52 | events.push(event); 53 | } else { 54 | // if there are no events with the same name, create an array with only one event 55 | allEvents[name] = [event]; 56 | } 57 | 58 | if (nameIsString && propModEventReg.test(name)) { 59 | // define needed accessors for KEY 60 | defineProp(object, name.replace(propModEventReg, '')); 61 | } 62 | 63 | // names prefixed by underscore mean "private" events 64 | if (!skipChecks && name[0] !== '_') { 65 | if (nameIsString) { 66 | triggerOne(object, `addevent:${name}`, event); 67 | } 68 | 69 | triggerOne(object, 'addevent', event); 70 | } 71 | 72 | // if event is added successfully return true 73 | return true; 74 | } 75 | -------------------------------------------------------------------------------- /packages/defi/src/_mq/on.js: -------------------------------------------------------------------------------- 1 | import data from './_data'; 2 | 3 | const splitBySpaceReg = /\s+/; 4 | const splitByDotReg = /\.(.+)/; 5 | const randomID = `${Math.random().toString().replace('0.', 'x')}y`; // x12345y 6 | 7 | // checks an element against a selector 8 | function is(node, selector) { 9 | return (node.matches 10 | || node.webkitMatchesSelector 11 | || node.mozMatchesSelector 12 | || node.msMatchesSelector 13 | || node.oMatchesSelector).call(node, selector); 14 | } 15 | 16 | // the function is used when a selector is given 17 | function delegateHandler(evt, selector, handler) { 18 | const scopeSelector = `[${randomID}="${randomID}"] `; 19 | const splittedSelector = selector.split(','); 20 | 21 | let matching = ''; 22 | 23 | for (let i = 0; i < splittedSelector.length; i++) { 24 | const sel = splittedSelector[i]; 25 | matching += `${i === 0 ? '' : ','}${scopeSelector}${sel},${scopeSelector}${sel} *`; 26 | } 27 | 28 | 29 | this.setAttribute(randomID, randomID); 30 | 31 | if (is(evt.target, matching)) { 32 | handler.call(this, evt); 33 | } 34 | 35 | this.removeAttribute(randomID); 36 | } 37 | 38 | // adds event listener to a set of elemnts 39 | export default function on(namesStr, selector, handler) { 40 | const names = namesStr.split(splitBySpaceReg); 41 | let delegate; 42 | 43 | if (typeof selector === 'function') { 44 | handler = selector; // eslint-disable-line no-param-reassign 45 | selector = null; // eslint-disable-line no-param-reassign 46 | } 47 | 48 | if (selector) { 49 | delegate = function uniqueDelegateHandler(evt) { 50 | delegateHandler.call(this, evt, selector, handler); 51 | }; 52 | } 53 | 54 | for (let i = 0; i < names.length; i++) { 55 | const [name, namespace] = names[i].split(splitByDotReg); 56 | 57 | for (let j = 0; j < this.length; j++) { 58 | const node = this[j]; 59 | const nodeID = node.b$ = node.b$ || ++data.nodeIndex; // eslint-disable-line no-plusplus 60 | const events = data.allEvents[name + nodeID] = data.allEvents[name + nodeID] || []; 61 | 62 | let exist = false; 63 | 64 | for (let k = 0; k < events.length; k++) { 65 | const event = events[k]; 66 | 67 | if (handler === event.handler && (!selector || selector === event.selector)) { 68 | exist = true; 69 | break; 70 | } 71 | } 72 | 73 | if (!exist) { 74 | events.push({ 75 | delegate, 76 | handler, 77 | namespace, 78 | selector, 79 | nodeID, 80 | name 81 | }); 82 | 83 | node.addEventListener(name, delegate || handler, false); 84 | } 85 | } 86 | } 87 | 88 | return this; 89 | } 90 | -------------------------------------------------------------------------------- /packages/defi/src/bindnode/_selectnodes.js: -------------------------------------------------------------------------------- 1 | import defs from '../_core/defs'; 2 | import $ from '../_mq'; 3 | import forEach from '../_helpers/foreach'; 4 | 5 | const customSelectorReg = /\s*:bound\(([^(]*)\)\s*([\S\s]*)\s*/; 6 | const randomAttr = `${Math.random().toString().replace('0.', 'x')}y`; // x12345y 7 | 8 | // the function selects nodes based on a selector (including custom values, eg :bound) 9 | // TODO: selectNodes looks not good, it needs to be refactored and accelerated if possible 10 | export default function selectNodes(object, givenSelector) { 11 | const { props } = defs.get(object); 12 | const selectors = givenSelector.split(','); 13 | let result = $(); 14 | 15 | forEach(selectors, (selector) => { 16 | const execResult = customSelectorReg.exec(selector); 17 | if (execResult) { 18 | const boundKey = execResult[1]; 19 | const subSelector = execResult[2]; 20 | const propDef = props[boundKey]; 21 | 22 | if (propDef) { 23 | const { bindings } = propDef; 24 | if (bindings) { 25 | const boundNodes = Array(bindings.length); 26 | forEach(bindings, (binding, i) => { 27 | boundNodes[i] = binding.node; 28 | }); 29 | 30 | // if native selector passed after :bound(KEY) is not empty string 31 | // for example ":bound(KEY) .my-selector" 32 | if (subSelector) { 33 | // if native selector contains children selector 34 | // for example ":bound(KEY) > .my-selector" 35 | if (subSelector.indexOf('>') === 0) { 36 | // selecting children 37 | forEach(boundNodes, (node) => { 38 | node.setAttribute(randomAttr, randomAttr); 39 | const selected = node.querySelectorAll(`[${randomAttr}="${randomAttr}"] ${subSelector}`); 40 | result = result.add(selected); 41 | node.removeAttribute(randomAttr); 42 | }); 43 | } else { 44 | // if native selector doesn't contain children selector 45 | forEach(boundNodes, (node) => { 46 | const selected = node.querySelectorAll(subSelector); 47 | result = result.add(selected); 48 | }); 49 | } 50 | } else { 51 | // if native selector is empty string just add bound nodes to result 52 | result = result.add(boundNodes); 53 | } 54 | } 55 | } 56 | } else { 57 | // if it's native selector (no custom things) 58 | result = result.add(selector); 59 | } 60 | }); 61 | 62 | return result; 63 | } 64 | -------------------------------------------------------------------------------- /packages/defi/src/off/_removelistener.js: -------------------------------------------------------------------------------- 1 | import defs from '../_core/defs'; 2 | import triggerOne from '../trigger/_triggerone'; 3 | import domEventReg from '../on/_domeventregexp'; 4 | import forEach from '../_helpers/foreach'; 5 | import forOwn from '../_helpers/forown'; 6 | 7 | // removes simple event listener from an object 8 | export default function removeListener(object, name, callback, info) { 9 | const def = defs.get(object); 10 | 11 | // if no definition do nothing 12 | if (!def) { 13 | return false; 14 | } 15 | 16 | const { events: allEvents } = def; 17 | const events = allEvents[name]; 18 | const retain = []; 19 | const noTrigger = name ? name[0] === '_' : false; 20 | const nameIsString = typeof name === 'string'; 21 | const domEventExecResult = nameIsString ? domEventReg.exec(name) : null; 22 | 23 | if (domEventExecResult) { 24 | const [, eventName, key, selector] = domEventExecResult; 25 | // fixing circular reference issue 26 | const removeDomListenerReq = require('./_removedomlistener'); 27 | const removeDomListener = removeDomListenerReq.default || removeDomListenerReq; 28 | removeDomListener(object, key, eventName, selector, callback, info); 29 | 30 | return true; 31 | } 32 | 33 | // if all events need to be removed 34 | if (typeof name === 'undefined') { 35 | if (!noTrigger) { 36 | forOwn(allEvents, (allEventsItem, allEventsName) => { 37 | forEach(allEventsItem, (event) => { 38 | const removeEventData = { 39 | allEventsName, 40 | callback: event.callback 41 | }; 42 | 43 | triggerOne(object, `removeevent:${name}`, removeEventData); 44 | triggerOne(object, 'removeevent', removeEventData); 45 | }); 46 | }); 47 | } 48 | 49 | // restore default value of "events" 50 | def.events = {}; 51 | } else if (events) { 52 | // if events with given name are found 53 | forEach(events, (event) => { 54 | const argCallback = (callback && callback._callback) || callback; 55 | const eventCallback = event.callback._callback || event.callback; 56 | 57 | if (argCallback && argCallback !== eventCallback) { 58 | // keep event 59 | retain.push(event); 60 | } else { 61 | const removeEventData = { 62 | name, 63 | callback: event.callback 64 | }; 65 | 66 | if (!noTrigger) { 67 | if (nameIsString) { 68 | triggerOne(object, `removeevent:${name}`, removeEventData); 69 | } 70 | 71 | triggerOne(object, 'removeevent', removeEventData); 72 | } 73 | } 74 | }); 75 | 76 | if (retain.length) { 77 | allEvents[name] = retain; 78 | } else { 79 | delete def.events[name]; 80 | } 81 | } 82 | 83 | return false; 84 | } 85 | -------------------------------------------------------------------------------- /packages/file-binders/test/spec/dropfiles_spec.js: -------------------------------------------------------------------------------- 1 | import makeElement from 'makeelement'; 2 | import bindNode from '../../../defi/npm/bindnode'; 3 | import unbindNode from '../../../defi/npm/unbindnode'; 4 | import on from '../../../defi/npm/on'; 5 | import dropFiles from '../../src/dropfiles'; 6 | import createSpy from './createspy'; 7 | 8 | describe('dropFiles binder', () => { 9 | const { Event, Blob } = window; 10 | 11 | it('allows to bind and drop', (done) => { 12 | const obj = {}; 13 | const node = makeElement('div'); 14 | const handler = createSpy(() => { 15 | expect(obj.files[0].readerResult).toEqual('foo'); 16 | expect(obj.files[1].readerResult).toEqual('bar'); 17 | done(); 18 | }); 19 | 20 | bindNode(obj, 'files', node, dropFiles('text')); 21 | 22 | on(obj, 'change:files', handler); 23 | 24 | node.dispatchEvent(new Event('dragover')); 25 | 26 | node.dispatchEvent(Object.assign(new Event('drop'), { 27 | dataTransfer: { 28 | files: [ 29 | new Blob(['foo'], { 30 | type: 'text/plain', 31 | }), 32 | new Blob(['bar'], { 33 | type: 'text/plain', 34 | }), 35 | ], 36 | }, 37 | })); 38 | }); 39 | 40 | it('allows bind and drop with no reading', (done) => { 41 | const obj = {}; 42 | const node = makeElement('div'); 43 | const handler = createSpy(() => { 44 | expect(obj.files[0].readerResult).toEqual(undefined); 45 | done(); 46 | }); 47 | 48 | bindNode(obj, 'files', node, dropFiles()); 49 | 50 | on(obj, 'change:files', handler); 51 | 52 | node.dispatchEvent(new Event('dragover')); 53 | 54 | node.dispatchEvent(Object.assign(new Event('drop'), { 55 | dataTransfer: { 56 | files: [ 57 | new Blob(['foo'], { 58 | type: 'text/plain', 59 | }), 60 | ], 61 | }, 62 | })); 63 | }); 64 | 65 | it('removes DOM event handlers when unbindNode is called', (done) => { 66 | const obj = {}; 67 | const node = makeElement('div'); 68 | const handler = createSpy(() => { 69 | expect(obj.files[0].readerResult).toEqual(undefined); 70 | }); 71 | 72 | bindNode(obj, 'files', node, dropFiles()); 73 | 74 | on(obj, 'change:files', handler); 75 | 76 | node.dispatchEvent(new Event('dragover')); 77 | 78 | node.dispatchEvent(Object.assign(new Event('drop'), { 79 | dataTransfer: { 80 | files: [ 81 | new Blob(['foo'], { 82 | type: 'text/plain', 83 | }), 84 | ], 85 | }, 86 | })); 87 | 88 | unbindNode(obj, 'files', node); 89 | 90 | setTimeout(() => { 91 | node.dispatchEvent(new Event('dragover')); 92 | 93 | node.dispatchEvent(Object.assign(new Event('drop'), { 94 | dataTransfer: { 95 | files: [ 96 | new Blob(['bar'], { 97 | type: 'text/plain', 98 | }), 99 | ], 100 | }, 101 | })); 102 | 103 | setTimeout(() => { 104 | expect(handler).toHaveBeenCalledTimes(1); 105 | done(); 106 | }, 200); 107 | }, 200); 108 | }); 109 | 110 | it('throws an error if filereader method does not exist', () => { 111 | const obj = {}; 112 | const node = makeElement('div'); 113 | 114 | expect(() => { 115 | bindNode(obj, 'files', node, dropFiles('wat')); 116 | }).toThrow(); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /packages/defi/test/spec/events/events_change_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import addListener from 'src/on/_addlistener'; 3 | import set from 'src/set'; 4 | import delegateListener from 'src/on/_delegatelistener'; 5 | import undelegateListener from 'src/off/_undelegatelistener'; 6 | import removeListener from 'src/off/_removelistener'; 7 | import makeObject from '../../helpers/makeobject'; 8 | import createSpy from '../../helpers/createspy'; 9 | 10 | describe('Change event (simple and delegated)', () => { 11 | let handler; 12 | 13 | beforeEach(() => { 14 | handler = createSpy(); 15 | }); 16 | 17 | it('fires common "change" event when "forceDefine" is used used at defi.set', () => { 18 | const obj = { x: 1 }; 19 | 20 | addListener(obj, 'change', handler); 21 | set(obj, 'x', 2, { define: true }); 22 | expect(handler).toHaveBeenCalled(); 23 | }); 24 | 25 | it('doesn\'t fire common "change" event when "forceDefine" isn\'t used at defi.set', () => { 26 | const obj = { x: 1 }; 27 | 28 | addListener(obj, 'change', handler); 29 | set(obj, 'x', 2); 30 | expect(handler).not.toHaveBeenCalled(); 31 | }); 32 | 33 | it('fires simple "change:x" event', () => { 34 | const obj = { x: 1 }; 35 | 36 | addListener(obj, 'change:x', handler); 37 | obj.x = 2; 38 | expect(handler).toHaveBeenCalled(); 39 | }); 40 | 41 | it('fires delegated (a.x)', () => { 42 | const obj = makeObject('a.x', 1); 43 | 44 | delegateListener(obj, 'a', 'change:x', handler); 45 | obj.a.x = 2; 46 | expect(handler).toHaveBeenCalled(); 47 | }); 48 | 49 | it('fires delegated (a.b.x)', () => { 50 | const obj = makeObject('a.b.x', 1); 51 | 52 | delegateListener(obj, 'a.b', 'change:x', handler); 53 | obj.a.b.x = 2; 54 | expect(handler).toHaveBeenCalled(); 55 | }); 56 | 57 | it('removes simple', () => { 58 | const obj = { x: 1 }; 59 | 60 | addListener(obj, 'change:x', handler); 61 | removeListener(obj, 'change:x', handler); 62 | obj.x = 2; 63 | expect(handler).not.toHaveBeenCalled(); 64 | }); 65 | 66 | it('removes delegated (a.x)', () => { 67 | const obj = makeObject('a.x', 1); 68 | 69 | delegateListener(obj, 'a', 'change:x', handler); 70 | undelegateListener(obj, 'a', 'change:x', handler); 71 | obj.a.x = 2; 72 | expect(handler).not.toHaveBeenCalled(); 73 | }); 74 | 75 | it('removes delegated (a.b.x)', () => { 76 | const obj = makeObject('a.b.x', 1); 77 | 78 | delegateListener(obj, 'a.b', 'change:x', handler); 79 | undelegateListener(obj, 'a.b', 'change:x', handler); 80 | obj.a.b.x = 2; 81 | expect(handler).not.toHaveBeenCalled(); 82 | }); 83 | 84 | it('fires delegated (a.b.x)', () => { 85 | const obj = makeObject('a.b.x', 1); 86 | 87 | delegateListener(obj, 'a.b', 'change:x', handler); 88 | obj.a.b.x = 2; 89 | expect(handler).toHaveBeenCalled(); 90 | }); 91 | 92 | it('accepts null target (a.b.c, reassign b)', () => { 93 | const obj = makeObject('a.b.c.x', 1); 94 | delegateListener(obj, 'a.b.c', 'someevent', handler); 95 | 96 | expect(() => { 97 | obj.a.b = null; 98 | }).not.toThrow(); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /packages/common-binders/test/spec/existence_binder_spec.js: -------------------------------------------------------------------------------- 1 | import { bindNode } from '../../../defi/npm'; 2 | import { existence } from '../../src'; 3 | 4 | describe('Existence binder', () => { 5 | const noDebounceFlag = { 6 | debounceSetValue: false, 7 | debounceGetValue: false, 8 | }; 9 | 10 | let obj; 11 | let node; 12 | let parent; 13 | 14 | beforeEach(() => { 15 | obj = {}; 16 | node = window.document.createElement('div'); 17 | node.innerHTML = '
'; 18 | parent = window.document.createElement('div'); 19 | parent.appendChild(node); 20 | }); 21 | 22 | it('should allow to use exitence binder', () => { 23 | node.id = 'foo'; 24 | node.className = 'bar baz'; 25 | 26 | obj.x = false; 27 | bindNode(obj, 'x', node, existence(), noDebounceFlag); 28 | 29 | expect(parent.childNodes.length).toEqual(1); 30 | expect(parent.childNodes[0].nodeName).toEqual('#comment'); 31 | expect(parent.childNodes[0].nodeValue).toEqual('DIV#foo.bar.baz'); 32 | 33 | obj.x = true; 34 | 35 | expect(parent.childNodes.length).toEqual(1); 36 | expect(parent.childNodes[0].tagName).toEqual('DIV'); 37 | 38 | obj.x = false; // try again 39 | 40 | expect(parent.childNodes.length).toEqual(1); 41 | expect(parent.childNodes[0].nodeName).toEqual('#comment'); 42 | expect(parent.childNodes[0].nodeValue).toEqual('DIV#foo.bar.baz'); 43 | 44 | obj.x = true; // try again 45 | 46 | expect(parent.childNodes.length).toEqual(1); 47 | expect(parent.childNodes[0].tagName).toEqual('DIV'); 48 | }); 49 | 50 | it('should allow to use exitence binder with reverse behavior', () => { 51 | node.id = 'foo'; 52 | node.className = 'bar baz'; 53 | 54 | obj.x = false; 55 | bindNode(obj, 'x', node, existence(false), noDebounceFlag); 56 | 57 | expect(parent.childNodes.length).toEqual(1); 58 | expect(parent.childNodes[0].nodeName).toEqual('DIV'); 59 | 60 | obj.x = true; 61 | 62 | expect(parent.childNodes.length).toEqual(1); 63 | expect(parent.childNodes[0].nodeName).toEqual('#comment'); 64 | expect(parent.childNodes[0].nodeValue).toEqual('DIV#foo.bar.baz'); 65 | 66 | obj.x = false; // try again 67 | 68 | expect(parent.childNodes.length).toEqual(1); 69 | expect(parent.childNodes[0].nodeName).toEqual('DIV'); 70 | 71 | obj.x = true; // try again 72 | 73 | expect(parent.childNodes.length).toEqual(1); 74 | expect(parent.childNodes[0].nodeName).toEqual('#comment'); 75 | expect(parent.childNodes[0].nodeValue).toEqual('DIV#foo.bar.baz'); 76 | }); 77 | 78 | it('should allow to use exitence binder with function as a switcher', () => { 79 | node.id = 'foo'; 80 | node.className = 'bar baz'; 81 | 82 | obj.x = 'kek'; 83 | bindNode(obj, 'x', node, existence((v) => v === 'kek'), noDebounceFlag); 84 | 85 | expect(parent.childNodes.length).toEqual(1); 86 | expect(parent.childNodes[0].nodeName).toEqual('DIV'); 87 | 88 | obj.x = 'lol'; 89 | 90 | expect(parent.childNodes.length).toEqual(1); 91 | expect(parent.childNodes[0].nodeName).toEqual('#comment'); 92 | expect(parent.childNodes[0].nodeValue).toEqual('DIV#foo.bar.baz'); 93 | 94 | obj.x = 'kek'; // try again 95 | 96 | expect(parent.childNodes.length).toEqual(1); 97 | expect(parent.childNodes[0].nodeName).toEqual('DIV'); 98 | 99 | obj.x = 'wow'; // try again 100 | 101 | expect(parent.childNodes.length).toEqual(1); 102 | expect(parent.childNodes[0].nodeName).toEqual('#comment'); 103 | expect(parent.childNodes[0].nodeValue).toEqual('DIV#foo.bar.baz'); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /packages/router/README.md: -------------------------------------------------------------------------------- 1 | A router for defi.js [![npm version](https://badge.fury.io/js/defi-router.svg)](https://badge.fury.io/js/defi-router) 2 | ============ 3 | 4 | [Demo](https://finom.github.io/defi/router-demo.html#!/foo/bar/baz/) 5 | 6 | Installing: 7 | ``` 8 | npm i defi-router 9 | ``` 10 | 11 | A bundle (downloadable version) lives at [gh-pages branch](https://github.com/finom/defi/tree/gh-pages) 12 | 13 | # tl;dr 14 | 15 | The library turns on two-way data binding between properties and parts of URL. 16 | 17 | ```js 18 | // location.hash is used there 19 | defiRouter(object, '/a/b/c/'); 20 | object.a = 'foo'; 21 | object.b = 'bar'; 22 | object.c = 'baz'; 23 | 24 | // makes location.hash to be #!/foo/bar/baz/ 25 | ``` 26 | 27 | If you need to use History API instead of hash, pass ``"history"`` as the second argument. 28 | 29 | ```js 30 | defiRouter(object, '/a/b/c/', 'history'); 31 | ``` 32 | 33 | CJS module import: 34 | 35 | ```js 36 | const defiRouter = require('defi-router'); 37 | defiRouter(object, '/a/b/c/', 'history'); 38 | ``` 39 | 40 | -------- 41 | 42 | 43 | How does a "traditional" routing works? A developer defines a rule (route) and defines a function which will be called when current path fits the given rule. 44 | 45 | ```js 46 | route("books/:id", id => { 47 | // do something 48 | }); 49 | ``` 50 | 51 | The principle of **defi-router** is different. You define which part of URL (both [hash](https://developer.mozilla.org/ru/docs/Web/API/Window/location), and [HTML5 History](https://developer.mozilla.org/ru/docs/Web/API/History_API) are supported) need to be synchronized with a given property. 52 | 53 | Let's say you need to synchronize ``"x"`` with the first part of ``location.hash`` and ``"y"`` with the second. 54 | 55 | ```js 56 | defiRouter(object, '/x/y/'); 57 | ``` 58 | 59 | Now when you type... 60 | 61 | ```js 62 | object.x = 'foo'; 63 | object.y = 'bar'; 64 | ``` 65 | 66 | ...``location.hash`` is automatically changed to ``#!/foo/bar/`` 67 | 68 | 69 | And vice versa. When the URL is changed manually or via back and forward buttons, the properties will be changed automatically. 70 | 71 | ```js 72 | location.hash = '#!/baz/qux/'; 73 | 74 | // ... after a moment 75 | console.log(object.x, object.y); // ‘baz’, ‘qux’ 76 | ``` 77 | 78 | As usually you can listen property changes with [defi.on](http://defi.js.org/#!defi.on) method. 79 | 80 | ```js 81 | defi.on(object, 'change:x', handler); 82 | ``` 83 | 84 | ## An asterisk syntax 85 | 86 | You can pass a string which contain asterisks to ``defiRouter`` if you don't need to synchronize some part of the path with a property. 87 | 88 | ```js 89 | defiRouter(object, '/x/*/y'); 90 | ``` 91 | 92 | If the hash looks like ``#!/foo/bar/baz/``, then ``object.x = "foo"`` and ``object.y = "baz"``. 93 | 94 | This feature is useful in cases when two or more modules control different parts of the path. 95 | 96 | 97 | **script1.js** 98 | 99 | ```js 100 | defiRouter(object1, '/x/*/'); 101 | ``` 102 | 103 | **script2.js** 104 | 105 | ```js 106 | defiRouter(object2, '/*/y/'); 107 | ``` 108 | 109 | ## Two important things to know 110 | 111 | **1.** If a property has truthy value then URL will be changed immediately after ``defiRouter`` call. 112 | 113 | ```js 114 | object.x = 'foo'; 115 | 116 | defiRouter(object, '/x/y/'); 117 | ``` 118 | 119 | **2.** If a property gets falsy value then all next listed properties will get ``null`` as new values. 120 | 121 | ```js 122 | defiRouter(object, '/x/y/z/u/'); 123 | 124 | object.y = null; // makes object.z and object.u to be null as well 125 | ``` 126 | 127 | The idea is to get actual state of URL. It could be weird to get ``"z"`` with value ``"foo"`` in case of non-existing bound part of URL. 128 | 129 | ## HTML5 History API 130 | 131 | The plugin supports HTML5 History as well. To initialize it you need to pass an optional argument ``type`` with ``"history"`` value to the ``defiRouter`` function (by default ``type`` is ``"hash"``). 132 | 133 | ```js 134 | defiRouter(object, 'x/y/z/', 'history'); 135 | ``` 136 | -------------------------------------------------------------------------------- /packages/defi/test/spec/events/tree_change_spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import addTreeListner from 'src/on/_addtreelistener'; 3 | import removeTreeListner from 'src/off/_removetreelistener'; 4 | import makeObject from '../../helpers/makeobject'; 5 | import createSpy from '../../helpers/createspy'; 6 | 7 | describe('Tree change events (internal feature)', () => { 8 | it('should listen tree and should remove listeners from previous subtree', () => { 9 | const obj = makeObject('a.b.c.d.e'); 10 | const handler = createSpy(); 11 | addTreeListner(obj, 'a.b.c.d.e', handler); 12 | 13 | obj.a.b.c.d.e = {}; 14 | expect(handler).toHaveBeenCalledTimes(1); 15 | 16 | // once again 17 | obj.a.b.c.d.e = {}; 18 | expect(handler).toHaveBeenCalledTimes(2); 19 | 20 | const d = obj.a.b.c.d; 21 | obj.a.b.c.d = makeObject('e'); 22 | d.e = {}; 23 | expect(handler).toHaveBeenCalledTimes(3); 24 | 25 | 26 | const c = obj.a.b.c; 27 | obj.a.b.c = makeObject('d.e'); 28 | c.d = makeObject('e'); 29 | expect(handler).toHaveBeenCalledTimes(4); 30 | 31 | const b = obj.a.b; 32 | obj.a.b = makeObject('c.d.e'); 33 | b.c = makeObject('d.e'); 34 | expect(handler).toHaveBeenCalledTimes(5); 35 | 36 | const a = obj.a; 37 | obj.a = makeObject('b.c.d.e'); 38 | a.b = makeObject('c.d.e'); 39 | expect(handler).toHaveBeenCalledTimes(6); 40 | 41 | obj.a.b.c.d.e = {}; 42 | expect(handler).toHaveBeenCalledTimes(7); 43 | 44 | obj.a.b.c.d = {}; 45 | expect(handler).toHaveBeenCalledTimes(8); 46 | 47 | obj.a.b.c = {}; 48 | expect(handler).toHaveBeenCalledTimes(9); 49 | 50 | obj.a.b = {}; 51 | expect(handler).toHaveBeenCalledTimes(10); 52 | 53 | obj.a = {}; 54 | expect(handler).toHaveBeenCalledTimes(11); 55 | 56 | obj.a.b = {}; 57 | expect(handler).toHaveBeenCalledTimes(12); 58 | 59 | obj.a.b.c = {}; 60 | expect(handler).toHaveBeenCalledTimes(13); 61 | 62 | obj.a.b.c.d = {}; 63 | expect(handler).toHaveBeenCalledTimes(14); 64 | 65 | obj.a.b.c.d.e = {}; 66 | expect(handler).toHaveBeenCalledTimes(15); 67 | 68 | obj.a = {}; 69 | expect(handler).toHaveBeenCalledTimes(16); 70 | }); 71 | 72 | it('should remove tree listener by callback', () => { 73 | const obj = makeObject('a.b.c'); 74 | const handler = createSpy(); 75 | addTreeListner(obj, 'a.b.c', handler); 76 | removeTreeListner(obj, 'a.b.c', handler); 77 | 78 | obj.a.b.c = {}; 79 | expect(handler).not.toHaveBeenCalled(); 80 | 81 | obj.a.b = makeObject('c'); 82 | expect(handler).not.toHaveBeenCalled(); 83 | 84 | obj.a = makeObject('b.c'); 85 | expect(handler).not.toHaveBeenCalled(); 86 | }); 87 | 88 | it('should remove tree listener without callback', () => { 89 | const obj = makeObject('a.b.c'); 90 | const handler = createSpy(); 91 | addTreeListner(obj, 'a.b.c', handler); 92 | removeTreeListner(obj, 'a.b.c'); 93 | 94 | obj.a.b.c = {}; 95 | expect(handler).not.toHaveBeenCalled(); 96 | 97 | obj.a.b = makeObject('c'); 98 | expect(handler).not.toHaveBeenCalled(); 99 | 100 | obj.a = makeObject('b.c'); 101 | expect(handler).not.toHaveBeenCalled(); 102 | }); 103 | 104 | it('should not remove tree listener by wrong callback', () => { 105 | const obj = makeObject('a.b.c'); 106 | const handler = createSpy(); 107 | addTreeListner(obj, 'a.b.c', handler); 108 | removeTreeListner(obj, 'a.b.c', () => {}); 109 | 110 | obj.a.b.c = {}; 111 | expect(handler).toHaveBeenCalledTimes(1); 112 | 113 | obj.a.b = makeObject('c'); 114 | expect(handler).toHaveBeenCalledTimes(2); 115 | 116 | obj.a = makeObject('b.c'); 117 | expect(handler).toHaveBeenCalledTimes(3); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /packages/router/test/spec/summary_spec.js: -------------------------------------------------------------------------------- 1 | import Router from '../../src/router'; 2 | import initRouter from '../../src'; 3 | 4 | const { 5 | document, location, history, Event, 6 | } = window; 7 | 8 | describe('Summary', () => { 9 | beforeEach(() => { 10 | Router.history.path = ''; 11 | Router.hash.path = ''; 12 | }); 13 | 14 | it('has correct instances', () => { 15 | expect(Router.hash instanceof Router).toBeTruthy(); 16 | expect(Router.history instanceof Router).toBeTruthy(); 17 | 18 | expect(Router.hash.type).toEqual('hash'); 19 | expect(Router.history.type).toBeTruthy('history'); 20 | }); 21 | 22 | it('allows to subscribe via static method', (done) => { 23 | const obj = { 24 | a: 'foo', 25 | b: 'bar', 26 | c: 'baz', 27 | }; 28 | 29 | initRouter(obj, '/a/b/c/'); 30 | 31 | expect(Router.hash.path).toEqual('/foo/bar/baz/'); 32 | 33 | setTimeout(() => { 34 | expect(document.location.hash).toEqual('#!/foo/bar/baz/'); 35 | done(); 36 | }, 300); 37 | }); 38 | 39 | it('doesn\'t make collisions when an object' 40 | + 'subscribes to both hash and history router', (done) => { 41 | const obj = { 42 | a: 'cfoo', 43 | b: 'cbar', 44 | c: 'cbaz', 45 | d: 'cqux', 46 | e: 'cpoo', 47 | f: 'czum', 48 | }; 49 | 50 | initRouter(obj, '/a/b/c/'); 51 | initRouter(obj, '/d/e/f/', 'history'); 52 | 53 | expect(Router.hash.path).toEqual('/cfoo/cbar/cbaz/'); 54 | expect(Router.history.path).toEqual('/cqux/cpoo/czum/'); 55 | 56 | setTimeout(() => { 57 | expect(document.location.hash).toEqual('#!/cfoo/cbar/cbaz/'); 58 | expect(document.location.pathname).toEqual('/cqux/cpoo/czum/'); 59 | done(); 60 | }, 50); 61 | }); 62 | 63 | it('allows to walk thru the history via hash router', (done) => { 64 | const obj = { 65 | a: 'wfoo', 66 | b: 'wbar', 67 | c: 'wbaz', 68 | }; 69 | 70 | initRouter(obj, '/a/b/c/'); 71 | 72 | setTimeout(() => { 73 | expect(document.location.hash).toEqual('#!/wfoo/wbar/wbaz/'); 74 | obj.a = 'wzoo'; 75 | 76 | setTimeout(() => { 77 | expect(document.location.hash).toEqual('#!/wzoo/wbar/wbaz/'); 78 | expect(obj.a).toEqual('wzoo'); 79 | history.back(); 80 | 81 | setTimeout(() => { 82 | expect(document.location.hash).toEqual('#!/wfoo/wbar/wbaz/'); 83 | 84 | expect(obj.a).toEqual('wfoo'); 85 | done(); 86 | }, 50); 87 | }, 50); 88 | }, 50); 89 | }); 90 | 91 | it('allows to walk thru the history via history router', (done) => { 92 | const obj = { 93 | a: 'wqux', 94 | b: 'wpoo', 95 | c: 'wzum', 96 | }; 97 | 98 | initRouter(obj, '/a/b/c/', 'history'); 99 | 100 | setTimeout(() => { 101 | expect(document.location.pathname).toEqual('/wqux/wpoo/wzum/'); 102 | obj.a = 'wzoo'; 103 | 104 | setTimeout(() => { 105 | expect(document.location.pathname).toEqual('/wzoo/wpoo/wzum/'); 106 | expect(obj.a).toEqual('wzoo'); 107 | 108 | history.back(); 109 | 110 | setTimeout(() => { 111 | expect(document.location.pathname).toEqual('/wqux/wpoo/wzum/'); 112 | 113 | expect(obj.a).toEqual('wqux'); 114 | 115 | done(); 116 | }, 50); 117 | }, 50); 118 | }, 50); 119 | }); 120 | 121 | it('gets default value of hash on initialization', (done) => { 122 | history.pushState({}, '', '/pfoo/pbar/pbaz/'); 123 | location.hash = '#!/hfoo/hbar/hbaz/'; 124 | 125 | const popstateEvent = new Event('popstate'); 126 | popstateEvent.state = { 127 | validPush: true, 128 | }; 129 | window.dispatchEvent(popstateEvent); 130 | 131 | const obj = { 132 | c: 'quu', 133 | f: 'boo', 134 | }; 135 | 136 | setTimeout(() => { 137 | initRouter(obj, '/a/b/c/'); 138 | initRouter(obj, '/d/e/f/', 'history'); 139 | 140 | setTimeout(() => { 141 | expect(location.hash).toEqual('#!/hfoo/hbar/quu/'); 142 | expect(location.pathname).toEqual('/pfoo/pbar/boo/'); 143 | 144 | expect(obj.a).toEqual('hfoo'); 145 | expect(obj.b).toEqual('hbar'); 146 | expect(obj.c).toEqual('quu'); 147 | expect(obj.d).toEqual('pfoo'); 148 | expect(obj.e).toEqual('pbar'); 149 | expect(obj.f).toEqual('boo'); 150 | 151 | done(); 152 | }, 50); 153 | }, 50); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /packages/file-binders/test/spec/file_spec.js: -------------------------------------------------------------------------------- 1 | import makeElement from 'makeelement'; 2 | import bindNode from '../../../defi/npm/bindnode'; 3 | import unbindNode from '../../../defi/npm/unbindnode'; 4 | import on from '../../../defi/npm/on'; 5 | import file from '../../src/file'; 6 | import createSpy from './createspy'; 7 | 8 | describe('file binder', () => { 9 | const { Event, Blob } = window; 10 | 11 | it('allows to bind file input', (done) => { 12 | const obj = {}; 13 | const node = makeElement('input', { 14 | type: 'file', 15 | multiple: false, 16 | }); 17 | const handler = createSpy(() => { 18 | expect(obj.file.readerResult).toEqual('foo'); 19 | done(); 20 | }); 21 | 22 | Object.defineProperty(node, 'files', { 23 | value: [ 24 | new Blob(['foo'], { 25 | type: 'text/plain', 26 | }), 27 | ], 28 | }); 29 | 30 | bindNode(obj, 'file', node, file('text')); 31 | 32 | on(obj, 'change:file', handler); 33 | 34 | node.dispatchEvent(new Event('change')); 35 | }); 36 | 37 | it('removes DOM event handlers when unbindNode is called', (done) => { 38 | const obj = {}; 39 | const node = makeElement('input', { 40 | type: 'file', 41 | multiple: false, 42 | }); 43 | const handler = createSpy(() => { 44 | expect(obj.file.readerResult).toEqual('foo'); 45 | }); 46 | 47 | Object.defineProperty(node, 'files', { 48 | value: [ 49 | new Blob(['foo'], { 50 | type: 'text/plain', 51 | }), 52 | ], 53 | }); 54 | 55 | bindNode(obj, 'file', node, file('text')); 56 | 57 | on(obj, 'change:file', handler); 58 | 59 | node.dispatchEvent(new Event('change')); 60 | 61 | setTimeout(() => { 62 | unbindNode(obj, 'file', node, file('text')); 63 | 64 | node.dispatchEvent(new Event('change')); 65 | 66 | setTimeout(() => { 67 | expect(handler).toHaveBeenCalledTimes(1); 68 | done(); 69 | }, 200); 70 | }, 200); 71 | }); 72 | 73 | it('allows to bind file input with multiple=true', (done) => { 74 | const obj = {}; 75 | const node = makeElement('input', { 76 | type: 'file', 77 | multiple: true, 78 | }); 79 | const handler = createSpy(() => { 80 | expect(obj.files[0].readerResult).toEqual('foo'); 81 | expect(obj.files[1].readerResult).toEqual('bar'); 82 | done(); 83 | }); 84 | 85 | Object.defineProperty(node, 'files', { 86 | value: [ 87 | new Blob(['foo'], { 88 | type: 'text/plain', 89 | }), 90 | new Blob(['bar'], { 91 | type: 'text/plain', 92 | }), 93 | ], 94 | }); 95 | 96 | bindNode(obj, 'files', node, file('text')); 97 | 98 | on(obj, 'change:files', handler); 99 | 100 | node.dispatchEvent(new Event('change')); 101 | }); 102 | 103 | it('allows to bind file input with no reading', (done) => { 104 | const obj = {}; 105 | const node = makeElement('input', { 106 | type: 'file', 107 | multiple: false, 108 | }); 109 | const handler = createSpy(() => { 110 | expect(obj.file.readerResult).toEqual(undefined); 111 | done(); 112 | }); 113 | 114 | Object.defineProperty(node, 'files', { 115 | value: [ 116 | new Blob(['foo'], { 117 | type: 'text/plain', 118 | }), 119 | ], 120 | }); 121 | 122 | bindNode(obj, 'file', node, file()); 123 | 124 | on(obj, 'change:file', handler); 125 | 126 | node.dispatchEvent(new Event('change')); 127 | }); 128 | 129 | it('assigns null to bound property if files are not esist', () => { 130 | const obj = {}; 131 | const node = makeElement('input', { 132 | type: 'file', 133 | multiple: false, 134 | }); 135 | 136 | Object.defineProperty(node, 'files', { 137 | value: [], 138 | }); 139 | 140 | bindNode(obj, 'file', node, file('text')); 141 | 142 | node.dispatchEvent(new Event('change')); 143 | 144 | expect(obj.file).toEqual(null); 145 | }); 146 | 147 | it('throws an error if filereader method does not exist', () => { 148 | const obj = {}; 149 | const node = makeElement('input', { 150 | type: 'file', 151 | multiple: false, 152 | }); 153 | 154 | Object.defineProperty(node, 'files', { 155 | value: [], 156 | }); 157 | 158 | expect(() => { 159 | bindNode(obj, 'file', node, file('wat')); 160 | }).toThrow(); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /packages/defi/src/bindnode/index.js: -------------------------------------------------------------------------------- 1 | import initDefi from '../_core/init'; 2 | import defineProp from '../_core/defineprop'; 3 | import getNodes from './_getnodes'; 4 | import createBindingSwitcher from './_createbindingswitcher'; 5 | import bindSingleNode from './_bindsinglenode'; 6 | import checkObjectType from '../_helpers/checkobjecttype'; 7 | import defiError from '../_helpers/defierror'; 8 | import forEach from '../_helpers/foreach'; 9 | import forOwn from '../_helpers/forown'; 10 | import addTreeListener from '../on/_addtreelistener'; 11 | 12 | // initializes binsing between a property of an object to HTML node 13 | export default function bindNode(object, key, node, binder, eventOptions) { 14 | // throw error when object type is wrong 15 | checkObjectType(object, 'bindNode'); 16 | 17 | eventOptions = eventOptions || {}; // eslint-disable-line no-param-reassign 18 | binder = binder || {}; // eslint-disable-line no-param-reassign 19 | 20 | initDefi(object); 21 | 22 | // throw an error when key is falsy 23 | if (!key) { 24 | throw defiError('binding:falsy_key'); 25 | } 26 | 27 | if (key instanceof Array) { 28 | /* 29 | * accept array of keys 30 | * this.bindNode(['a', 'b', 'c'], node) 31 | */ 32 | forEach(key, (itemKey) => bindNode(object, itemKey, node, binder, eventOptions)); 33 | 34 | return object; 35 | } 36 | 37 | 38 | if (typeof key === 'object') { 39 | forOwn(key, (keyObjValue, keyObjKey) => { 40 | // binder means eventOptions 41 | eventOptions = binder; // eslint-disable-line no-param-reassign 42 | 43 | if ( 44 | keyObjValue 45 | && keyObjValue.constructor === Object 46 | && 'node' in keyObjValue 47 | ) { 48 | // this.bindNode({ key: { node: $(), binder } ) }, { on: 'evt' }, { silent: true }); 49 | bindNode( 50 | object, keyObjKey, keyObjValue.node, 51 | keyObjValue.binder || node, eventOptions 52 | ); 53 | } else if ( 54 | keyObjValue 55 | && keyObjValue.constructor === Array 56 | && keyObjValue.length 57 | && keyObjValue[0].constructor === Object 58 | && 'node' in keyObjValue[0] 59 | ) { 60 | // this.bindNode({ key: [{ 61 | // node: $(), 62 | // binder 63 | // }] ) }, { on: 'evt' }, { silent: true }); 64 | forEach(keyObjValue, (keyObjValueItem) => { 65 | bindNode( 66 | object, keyObjKey, keyObjValueItem.node, 67 | keyObjValueItem.binder || node, eventOptions 68 | ); 69 | }); 70 | } else { 71 | // this.bindNode({ key: $() }, { on: 'evt' }, { silent: true }); 72 | bindNode(object, keyObjKey, keyObjValue, node, eventOptions); 73 | } 74 | }); 75 | 76 | return object; 77 | } 78 | 79 | const { 80 | optional = false, 81 | exactKey = false 82 | } = eventOptions; 83 | const $nodes = getNodes(object, node); 84 | 85 | // check node existence 86 | if (!$nodes.length) { 87 | if (optional) { 88 | return object; 89 | } 90 | 91 | throw defiError('binding:node_missing', { key, node }); 92 | } 93 | 94 | if (!exactKey) { 95 | const deepPath = key.split('.'); 96 | const deepPathLength = deepPath.length; 97 | 98 | if (deepPathLength > 1) { 99 | // handle binding when key arg includes dots (eg "a.b.c.d") 100 | const bindingSwitcher = createBindingSwitcher({ 101 | object, 102 | deepPath, 103 | $nodes, 104 | binder, 105 | eventOptions, 106 | bindNode 107 | }); 108 | 109 | addTreeListener(object, deepPath.slice(0, deepPathLength - 1), bindingSwitcher); 110 | 111 | bindingSwitcher(); 112 | 113 | return object; 114 | } 115 | } 116 | 117 | const propDef = defineProp(object, key); 118 | 119 | // handle binding for every node separately 120 | forEach($nodes, (oneNode) => bindSingleNode(object, { 121 | $nodes, 122 | node: oneNode, 123 | key, 124 | eventOptions, 125 | binder, 126 | propDef 127 | })); 128 | 129 | return object; 130 | } 131 | --------------------------------------------------------------------------------