├── 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 | const customSelectorReg = /:bound\(([^(]*)\)/;
6 |
7 | // the function works just like DOM library accepting any kind of arg
8 | // (HTML string, Node, NodeList etc) bu allows to pass custom selector
9 | // eg :bound(KEY)
10 | export default function getNodes(object, selector) {
11 | let nodes;
12 |
13 | if (
14 | typeof selector === 'string'
15 | && !htmlReg.test(selector)
16 | && customSelectorReg.test(selector)
17 | ) {
18 | nodes = selectNodes(object, selector);
19 | } else {
20 | nodes = $(selector);
21 | }
22 |
23 | return nodes;
24 | }
25 |
--------------------------------------------------------------------------------
/packages/defi/test/spec/mq/parsehtml_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */
2 | import $ from 'src/_mq';
3 |
4 | describe('mq.parseHTML', () => {
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 [](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 |
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 [](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 |
--------------------------------------------------------------------------------