├── .npmignore
├── .gitignore
├── examples
├── _hot
│ ├── model.js
│ ├── view.js
│ ├── index.html
│ ├── index.js
│ ├── package.json
│ ├── webpack.config.js
│ └── component.js
├── react-basic
│ ├── app.js
│ ├── index.html
│ └── greeting.js
├── _async
│ ├── README.md
│ ├── index.js
│ ├── index.html
│ └── webpack.config.js
├── complex
│ ├── app.js
│ ├── baz-logger.js
│ ├── index.html
│ └── baz-complex.js
└── gifflix
│ ├── counter.js
│ ├── app.js
│ ├── star.js
│ └── index.html
├── .github
└── workflows
│ └── node.js.yml
├── LICENSE
├── spec
├── karma.conf.js
├── main
│ ├── wrapperHMRSpec.js
│ ├── wrapperSpec.js
│ ├── mainThrowSpec.js
│ ├── mainSpec.js
│ ├── disposeSpec.js
│ ├── mainWatchSpec.js
│ └── mainRebindSpec.js
└── helpers
│ ├── getChildrenWithDataSpec.js
│ └── getAttrSpec.js
├── webpack.config.examples.js
├── docs
├── hot-reloadable-bazfuncs.md
├── helpers.md
└── README.md
├── package.json
├── README.md
├── CHANGELOG.md
└── src
├── helpers.js
└── main.js
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !src/*
3 | !docs/*
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | /dist
4 |
--------------------------------------------------------------------------------
/examples/_hot/model.js:
--------------------------------------------------------------------------------
1 | export default function model() {
2 | return {
3 | count: 10,
4 | };
5 | }
6 |
--------------------------------------------------------------------------------
/examples/_hot/view.js:
--------------------------------------------------------------------------------
1 | // modify this text for some HOT magic
2 | export default state => '- ' + state.count + ' -';
3 |
--------------------------------------------------------------------------------
/examples/react-basic/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Baz = require('bazooka');
3 |
4 | Baz.register({
5 | greeting: require('greeting'),
6 | });
7 |
8 | Baz.refresh();
9 |
--------------------------------------------------------------------------------
/examples/_async/README.md:
--------------------------------------------------------------------------------
1 | # async example
2 |
3 | ```bash
4 | $ cd bazooka
5 | $ npm start -- --config=examples/_async/webpack.config.js
6 | ```
7 |
8 | example will be running at http://localhost:8081/
9 |
--------------------------------------------------------------------------------
/examples/_async/index.js:
--------------------------------------------------------------------------------
1 | import Baz from 'bazooka';
2 |
3 | Baz.register({
4 | 'init-time': function(node) {
5 | node.innerText = node.innerText + new Date().toString();
6 | },
7 | });
8 |
9 | Baz.watch();
10 |
--------------------------------------------------------------------------------
/examples/complex/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Baz = require('bazooka');
3 |
4 | Baz.register({
5 | 'baz-complex': require('baz-complex'),
6 | 'baz-logger': require('baz-logger'),
7 | });
8 |
9 | var unwatch = Baz.watch();
10 |
--------------------------------------------------------------------------------
/examples/complex/baz-logger.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var info = console.info.bind(console, '[baz-logger]');
4 |
5 | function bazFunc(node) {
6 | node.onclick = info.bind(null, 'click');
7 | }
8 |
9 | module.exports = {
10 | bazFunc: bazFunc,
11 | info: info,
12 | };
13 |
--------------------------------------------------------------------------------
/examples/_hot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/_hot/index.js:
--------------------------------------------------------------------------------
1 | import Baz from 'bazooka';
2 | import component from './component.js';
3 |
4 | Baz.register({
5 | 'hot-component': component,
6 | });
7 |
8 | Baz.watch();
9 |
10 | if (module.hot) {
11 | module.hot.accept('./component.js', () =>
12 | Baz.rebind({ 'hot-component': component }));
13 | }
14 |
--------------------------------------------------------------------------------
/examples/_hot/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "_hot",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "webpack": "webpack",
8 | "start" : "webpack-dev-server"
9 | },
10 | "devDependencies": {
11 | "webpack": "^2.3.3",
12 | "webpack-dev-server": "^2.4.2"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/complex/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Complex component
8 | Universal component
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/react-basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello React!
5 |
6 |
7 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/complex/baz-complex.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Baz = require('bazooka');
4 | var logger = require('baz-logger');
5 |
6 | var infoClicked = function(ev) {
7 | logger.info('click on bazId =', Baz(ev.target).id);
8 | };
9 |
10 | function bazFunc(node) {
11 | node.onclick = infoClicked;
12 | }
13 |
14 | module.exports = {
15 | bazFunc: bazFunc,
16 | };
17 |
--------------------------------------------------------------------------------
/examples/react-basic/greeting.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var React = require('react');
4 | var ReactDOM = require('react-dom');
5 |
6 | var GreetingComponent = React.createClass({
7 | render: function() {
8 | return React.DOM.h1(null, this.props.message + ', world!');
9 | },
10 | });
11 |
12 | var greeting = function(element) {
13 | ReactDOM.render(
14 | React.createElement(GreetingComponent, {
15 | message: element.getAttribute('data-message'),
16 | }),
17 | element
18 | );
19 | };
20 |
21 | module.exports = greeting;
22 |
--------------------------------------------------------------------------------
/examples/_async/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | async example
5 |
8 |
9 |
10 | async example
11 | sync time:
12 | async time:
13 | sync time:
14 | async time:
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/examples/_async/webpack.config.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const webpack = require('webpack');
3 |
4 | module.exports = {
5 | context: __dirname,
6 |
7 | entry: ['./index.js'],
8 |
9 | output: {
10 | filename: 'bundle.js',
11 | path: resolve(__dirname, 'dist'),
12 | publicPath: '/dist',
13 | },
14 |
15 | devtool: 'inline-source-map',
16 |
17 | devServer: {
18 | contentBase: __dirname,
19 | publicPath: '/dist',
20 | },
21 |
22 | module: {},
23 |
24 | resolve: {
25 | alias: {
26 | bazooka: resolve(__dirname, '../../src/main.js'),
27 | },
28 | },
29 |
30 | plugins: [new webpack.NamedModulesPlugin()],
31 | };
32 |
--------------------------------------------------------------------------------
/examples/gifflix/counter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | function counterBazFunc(node) {
3 | window.appBus
4 | .filter(function(msg) {
5 | return msg.tell == 'favsUpdate';
6 | })
7 | .map(function(msg) {
8 | return msg.favs;
9 | })
10 | .toProperty(function() {
11 | return {};
12 | })
13 | .map(function(favs) {
14 | var count = 0;
15 | for (var key in favs) {
16 | if (favs[key]) {
17 | count++;
18 | }
19 | }
20 |
21 | return count;
22 | })
23 | .onValue(function(count) {
24 | node.textContent = count;
25 | });
26 | }
27 |
28 | module.exports = {
29 | bazFunc: counterBazFunc,
30 | };
31 |
--------------------------------------------------------------------------------
/examples/_hot/webpack.config.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const webpack = require('webpack');
3 |
4 | module.exports = {
5 | context: __dirname,
6 |
7 | entry: [
8 | 'webpack-dev-server/client?http://localhost:8080',
9 | 'webpack/hot/only-dev-server',
10 | './index.js',
11 | ],
12 | output: {
13 | filename: 'bundle.js',
14 | path: resolve(__dirname, 'dist'),
15 | publicPath: '/dist',
16 | },
17 |
18 | devtool: 'inline-source-map',
19 |
20 | devServer: {
21 | hot: true,
22 | contentBase: __dirname,
23 | publicPath: '/dist',
24 | },
25 |
26 | module: {},
27 |
28 | plugins: [
29 | new webpack.HotModuleReplacementPlugin(),
30 | new webpack.NamedModulesPlugin(),
31 | ],
32 | };
33 |
--------------------------------------------------------------------------------
/examples/gifflix/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Baz = require('bazooka');
3 | var Kefir = require('kefir');
4 |
5 | window.appBus = Kefir.pool();
6 | window.appBus.push = function(value) {
7 | window.appBus.plug(Kefir.constant(value));
8 | };
9 |
10 | window.appBus
11 | .filter(function(msg) {
12 | return msg.tell == 'singleFavUpdate';
13 | })
14 | .scan(
15 | function(acc, msg) {
16 | acc[msg.id] = msg.active;
17 | return acc;
18 | },
19 | {}
20 | )
21 | .map(function(favs) {
22 | return {
23 | tell: 'favsUpdate',
24 | favs: favs,
25 | };
26 | })
27 | .onValue(function(msg) {
28 | window.appBus.push(msg);
29 | });
30 |
31 | Baz.register({
32 | star: require('star'),
33 | counter: require('counter'),
34 | });
35 |
36 | var unwatch = Baz.watch();
37 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [10.x, 12.x, 14.x, 15.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v2
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v1
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | - run: npm ci
29 | - run: npm run build --if-present
30 | - run: npm test
31 |
--------------------------------------------------------------------------------
/examples/_hot/component.js:
--------------------------------------------------------------------------------
1 | import Baz from 'bazooka';
2 |
3 | import view from './view.js';
4 | import model from './model.js';
5 |
6 | function mockRerender(node, state) {
7 | node.textContent = view(state);
8 | }
9 |
10 | function clickHandler(node, state) {
11 | return function(event) {
12 | // modify this expression for some HOT magic
13 | state.count = state.count + 1;
14 | mockRerender(node, state);
15 | };
16 | }
17 |
18 | export default function hotComponent(node) {
19 | const state = module.hot
20 | ? Baz(node).HMRState(module.hot, prev => prev || model())
21 | : model();
22 |
23 | if (module.hot) {
24 | // reload page if `./model.js` is changed
25 | module.hot.decline('./model.js');
26 | }
27 |
28 | mockRerender(node, state);
29 |
30 | const boundHandler = clickHandler(node, state);
31 | node.addEventListener('click', boundHandler);
32 |
33 | return () => {
34 | node.removeEventListener('click', boundHandler);
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/examples/gifflix/star.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var Kefir = require('kefir');
3 | var Baz = require('bazooka');
4 |
5 | var getAttrs = Baz.h.getAttrs('star');
6 |
7 | function starBazFunc(node) {
8 | Kefir.fromEvents(node, 'click')
9 | .map(function(e) {
10 | return {
11 | tell: 'singleFavUpdate',
12 | id: getAttrs(e.target).id,
13 | active: !getAttrs(e.target).active,
14 | };
15 | })
16 | .onValue(function(msg) {
17 | window.appBus.push(msg);
18 | });
19 |
20 | window.appBus
21 | .filter(function(msg) {
22 | return msg.tell == 'favsUpdate';
23 | })
24 | .map(function(msg) {
25 | return msg.favs;
26 | })
27 | .toProperty(function() {
28 | return {};
29 | })
30 | .onValue(function(favs) {
31 | favs[getAttrs(node).id]
32 | ? node.setAttribute('data-star-active', 1)
33 | : node.removeAttribute('data-star-active');
34 | });
35 | }
36 |
37 | module.exports = {
38 | bazFunc: starBazFunc,
39 | };
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Igor Mozharovsky, Anton Verinov
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 |
--------------------------------------------------------------------------------
/spec/karma.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = function(config) {
5 | config.set({
6 | basePath: '',
7 |
8 | frameworks: ['jasmine'],
9 |
10 | files: ['**/*Spec.js'],
11 |
12 | exclude: [],
13 |
14 | preprocessors: {
15 | '**/*Spec.js': ['webpack'],
16 | },
17 |
18 | reporters: ['progress'],
19 |
20 | port: 9876,
21 |
22 | colors: true,
23 |
24 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
25 | logLevel: config.LOG_INFO,
26 |
27 | autoWatch: false,
28 |
29 | browsers: ['PhantomJS'],
30 |
31 | plugins: ['karma-webpack', 'karma-jasmine', 'karma-phantomjs-launcher'],
32 |
33 | webpack: {
34 | resolve: {
35 | alias: {
36 | bazooka: path.join(__dirname, '..', 'src', 'main.js'),
37 | },
38 | modules: ['node_modules', 'src'],
39 | },
40 | plugins: [
41 | new webpack.DefinePlugin({
42 | 'process.env.NODE_ENV': 'window.NODE_ENV',
43 | }),
44 | ],
45 | },
46 |
47 | webpackMiddleware: {
48 | noInfo: true,
49 | },
50 |
51 | singleRun: false,
52 | });
53 | };
54 |
--------------------------------------------------------------------------------
/webpack.config.examples.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs');
4 | var path = require('path');
5 | var zipObject = require('lodash.zipobject');
6 |
7 | var EXAMPLES_BASE_DIR = path.join(__dirname, 'examples');
8 |
9 | var getDirectories = function(srcPath) {
10 | return fs.readdirSync(srcPath).filter(function(file) {
11 | return fs.statSync(path.join(srcPath, file)).isDirectory() &&
12 | file.indexOf('_') !== 0;
13 | });
14 | };
15 |
16 | var makeFullPath = function(p) {
17 | return path.join(EXAMPLES_BASE_DIR, p);
18 | };
19 |
20 | var makeAppPath = function(dir) {
21 | return path.join(dir, 'app.js');
22 | };
23 |
24 | var examplesNames = getDirectories(EXAMPLES_BASE_DIR);
25 | var examplesPaths = examplesNames.map(makeFullPath);
26 | var examplesAppPaths = examplesPaths.map(makeAppPath);
27 |
28 | var entry = zipObject(examplesNames, examplesAppPaths);
29 |
30 | var modulesDirectories = ['node_modules', 'src'];
31 | modulesDirectories = modulesDirectories.concat(examplesPaths);
32 |
33 | module.exports = {
34 | entry: entry,
35 | output: {
36 | path: path.join(__dirname, 'dist'),
37 | filename: '[name].js',
38 | },
39 | resolve: {
40 | alias: {
41 | bazooka: path.join(__dirname, 'src', 'main.js'),
42 | },
43 | modules: modulesDirectories,
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/docs/hot-reloadable-bazfuncs.md:
--------------------------------------------------------------------------------
1 | # Hot reloadable `bazFunc`s
2 |
3 | To make a component hot reloadable, you will need to:
4 | 1. (optionally) return `dispose` function from `bazFunc`. This function cleans up eventListeners, timers, etc.
5 | ```diff
6 | import model from './model.js';
7 |
8 | export default function hotBaz(node) {
9 | const state = model();
10 |
11 | render(node, state);
12 | node.addEventListener('click', clickHandler);
13 |
14 | + return () => {
15 | + node.removeEventListener('click', clickHandler);
16 | + };
17 | };
18 | ```
19 | 2. (optionally) call `Baz(node).HMRState(moduleHot, stateCallback)` method to preserve `state` between hot reloads. `stateCallback` is called without arguments on initial execution and with preserved state after each hot reload
20 | ```diff
21 | +import Baz from 'bazooka';
22 | import model from './model.js';
23 |
24 | export default function hotBaz(node) {
25 | - const state = model();
26 | + const state = module.hot
27 | + ? Baz(node).HMRState(module.hot, prev => prev || model())
28 | + : model();
29 |
30 | mockRerender(node, state);
31 |
32 | const boundHandler = clickHandler(node, state);
33 | node.addEventListener('click', clickHandler);
34 |
35 | return () => {
36 | node.removeEventListener('click', clickHandler);
37 | };
38 | }
39 | ```
40 | 3. write `module.hot.accept` handler in init script ("near" `Baz.watch`/`Baz.refresh`), which calls new `Baz.rebind` function
41 | ```diff
42 | import component from './component.js';
43 |
44 | Baz.register({ 'hot-component': component });
45 | Baz.watch();
46 |
47 | +if (module.hot) {
48 | + module.hot.accept('./component.js', () =>
49 | + Baz.rebind({ 'hot-component': component }));
50 | +}
51 | ```
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bazooka",
3 | "version": "0.9.1",
4 | "description": "Simple tool for declarative binding applications to HTML nodes.",
5 | "main": "src/main.js",
6 | "directories": {
7 | "example": "examples"
8 | },
9 | "scripts": {
10 | "test": "npm run lint && npm run test-karma",
11 | "karma": "karma",
12 | "test-karma": "karma start spec/karma.conf.js --single-run",
13 | "lint": "npm run prettier -- -l",
14 | "fmt": "npm run prettier -- --write",
15 | "prettier": "prettier \"{src,spec,examples}/**/*.js\" \"webpack.config.examples.js\" --single-quote --trailing-comma=es5",
16 | "preversion": "npm test && npm run docs",
17 | "postversion": "git push && git push --tags",
18 | "docs": "mkdir -p ./docs && jsdoc2md src/main.js > ./docs/README.md && jsdoc2md src/helpers.js > ./docs/helpers.md",
19 | "examples": "webpack --config=webpack.config.examples.js",
20 | "start": "webpack-dev-server"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/seedofjoy/bazooka.git"
25 | },
26 | "author": "Igor Mozharovsky ",
27 | "contributors": [
28 | {
29 | "name": "Igor Mozharovsky",
30 | "email": "igor.mozharovsky@gmail.com"
31 | },
32 | {
33 | "name": "Anton Verinov",
34 | "url": "http://anton.codes",
35 | "email": "anton@verinov.com"
36 | }
37 | ],
38 | "license": "MIT",
39 | "bugs": {
40 | "url": "https://github.com/seedofjoy/bazooka/issues"
41 | },
42 | "homepage": "https://github.com/seedofjoy/bazooka",
43 | "devDependencies": {
44 | "jasmine-core": "^2.3.4",
45 | "jsdoc-to-markdown": "^1.2.0",
46 | "karma": "1.0.0",
47 | "karma-jasmine": "^0.3.6",
48 | "karma-phantomjs-launcher": "1.0.4",
49 | "karma-webpack": "^2.0.6",
50 | "kefir": "^3.1.0",
51 | "lodash.zipobject": "4.1.3",
52 | "phantomjs-prebuilt": "2.1.16",
53 | "prettier": "^0.22.0",
54 | "react": "^0.14.2",
55 | "react-dom": "^0.14.2",
56 | "webpack": "^2.7.0",
57 | "webpack-dev-server": "^2.9.5"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/spec/main/wrapperHMRSpec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function appendDiv(dataBazooka) {
4 | var node = document.createElement('div');
5 | node.setAttribute('test-node', '');
6 | if (dataBazooka) {
7 | node.setAttribute('data-bazooka', dataBazooka);
8 | }
9 | document.body.appendChild(node);
10 | return node;
11 | }
12 |
13 | describe('BazookaWrapper.prototype.HMRState', function() {
14 | var Baz = require('bazooka');
15 |
16 | afterEach(function() {
17 | Array.prototype.forEach.call(
18 | document.querySelectorAll('[test-node]'),
19 | function(el) {
20 | document.body.removeChild(el);
21 | }
22 | );
23 | });
24 |
25 | it('should return initial state', function() {
26 | var node = appendDiv('bazFunc');
27 |
28 | var checker = jasmine.createSpy('checker');
29 | var mockModuleHot = {
30 | dispose: function() {},
31 | data: {},
32 | };
33 |
34 | var registerObj = {
35 | bazFunc: function(node) {
36 | var state = Baz(node).HMRState(mockModuleHot, function(prev) {
37 | return prev || { count: 0 };
38 | });
39 | checker(state.count);
40 | state.count++;
41 | },
42 | };
43 |
44 | Baz.register(registerObj);
45 | Baz.refresh();
46 |
47 | expect(checker.calls.allArgs()).toEqual([[0]]);
48 | });
49 |
50 | it('should save and load data between HMRs', function() {
51 | var node = appendDiv('bazFunc');
52 |
53 | var checker = jasmine.createSpy('checker');
54 | var mockModuleHot = {
55 | dispose: function(cb) {
56 | mockModuleHot.disposes.push(cb);
57 | },
58 | data: {},
59 | disposes: [],
60 | };
61 |
62 | var registerObj = {
63 | bazFunc: function(node) {
64 | var state = Baz(node).HMRState(mockModuleHot, function(prev) {
65 | return prev || { count: 0 };
66 | });
67 | checker(state.count);
68 | state.count++;
69 | },
70 | };
71 |
72 | Baz.register(registerObj);
73 | Baz.refresh();
74 |
75 | mockModuleHot.disposes.forEach(function(cb) {
76 | cb(mockModuleHot.data);
77 | });
78 | mockModuleHot.disposes = [];
79 |
80 | Baz.rebind(registerObj);
81 | expect(checker.calls.allArgs()).toEqual([[0], [1]]);
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/docs/helpers.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## h
4 |
5 | * [h](#Bazooka.module_h)
6 | * [.getAttrs(prefix, [node])](#Bazooka.module_h.getAttrs) ⇒ function | object
7 | * [.getChildrenWithData(parentNode, dataKey, [dataValue])](#Bazooka.module_h.getChildrenWithData) ⇒ NodeList
8 |
9 |
10 |
11 | ### h.getAttrs(prefix, [node]) ⇒ function | object
12 | Get all prefixed `data-` attributes as an object
13 |
14 | **Kind**: static method of [h](#Bazooka.module_h)
15 | **Returns**: function | object - - curried function for parsing node with passed prefix or parsed attrs
16 |
17 | | Param | Type | Description |
18 | | --- | --- | --- |
19 | | prefix | string | `data-`attribute prefix |
20 | | [node] | HTMLNode | target node |
21 |
22 | **Example**
23 | ```javascript
24 | //
25 |
26 | Baz.h.getAttrs('x', window.n) // => {a: "lol", b: 1}
27 | Baz.h.getAttrs('y', window.n) // => {y: {key: 1}, composedAttr: true}
28 |
29 | const xAttrs = Baz.h.getAttrs('x')
30 | xAttrs(window.n) // => {x: "lol", b: 1}
31 | ```
32 |
33 |
34 | ### h.getChildrenWithData(parentNode, dataKey, [dataValue]) ⇒ NodeList
35 | Query children with specific `data-`attribute
36 |
37 | **Kind**: static method of [h](#Bazooka.module_h)
38 |
39 | | Param | Type | Description |
40 | | --- | --- | --- |
41 | | parentNode | HTMLNode | |
42 | | dataKey | string | – data-key. `data-baz-key`, `baz-key` and `bazKey` are equivalent |
43 | | [dataValue] | string | value of a `data-`attribute |
44 |
45 | **Example**
46 | ```javascript
47 | //
48 | //
yep
49 | //
nope
50 | //
51 |
52 | Baz.h.getChildrenWithData(window.parent, 'data-user-id', 1)[0].textContent === 'yep'
53 | Baz.h.getChildrenWithData(window.parent, 'user-id', 1)[0].textContent === 'yep'
54 | Baz.h.getChildrenWithData(window.parent, 'userId', 2)[0].textContent === 'nope'
55 | ```
56 |
--------------------------------------------------------------------------------
/spec/main/wrapperSpec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function appendDiv(dataBazooka) {
4 | var node = document.createElement('div');
5 | node.setAttribute('test-node', '');
6 | if (dataBazooka) {
7 | node.setAttribute('data-bazooka', dataBazooka);
8 | }
9 | document.body.appendChild(node);
10 | return node;
11 | }
12 |
13 | var componentsRegistry = {
14 | exampleBazFunc: function() {},
15 | exampleBazFunc2: function() {},
16 | exampleComplexBazComponent: {
17 | bazFunc: function() {},
18 | },
19 | exampleComplexBazFunclessComponent: {
20 | triggers: ['click'],
21 | },
22 | };
23 |
24 | describe('BazookaWrapper', function() {
25 | var Baz = require('bazooka');
26 |
27 | beforeEach(function() {
28 | Baz.register(componentsRegistry);
29 | });
30 |
31 | afterEach(function() {
32 | Array.prototype.forEach.call(
33 | document.querySelectorAll('[test-node]'),
34 | function(el) {
35 | document.body.removeChild(el);
36 | }
37 | );
38 | });
39 |
40 | it('should return wrapper node', function() {
41 | var node = appendDiv();
42 | var $baz = Baz(node);
43 | expect($baz instanceof Baz.BazookaWrapper).toBe(true);
44 | });
45 |
46 | it('should increment bazId for new node', function() {
47 | var node = appendDiv();
48 | var $baz = Baz(node);
49 |
50 | var node2 = appendDiv();
51 | var $baz2 = Baz(node2);
52 |
53 | expect(parseInt($baz2.id, 10)).toBe(parseInt($baz.id, 10) + 1);
54 | });
55 |
56 | it('should do nothing to bazId of already wrapped nodes', function() {
57 | var node = appendDiv();
58 | var $baz = Baz(node);
59 | var $baz2 = Baz(node);
60 |
61 | expect($baz2.id).toBe($baz.id);
62 | });
63 |
64 | it('should return bound components', function() {
65 | var node = appendDiv(
66 | 'exampleBazFunc exampleComplexBazComponent exampleComplexBazFunclessComponent'
67 | );
68 | Baz.refresh();
69 |
70 | var nodeComponents = Baz(node).getComponents();
71 |
72 | expect(nodeComponents.exampleBazFunc.bazFunc).toBe(
73 | componentsRegistry.exampleBazFunc
74 | );
75 |
76 | expect(nodeComponents.exampleComplexBazComponent).toBe(
77 | componentsRegistry.exampleComplexBazComponent
78 | );
79 |
80 | expect(nodeComponents.exampleComplexBazFunclessComponent).toBe(
81 | componentsRegistry.exampleComplexBazFunclessComponent
82 | );
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bazooka [](https://github.com/seedofjoy/bazooka/actions/node.js.yml)
2 | Simple tool for declarative binding applications to HTML nodes.
3 |
4 |
5 | ## Installation
6 |
7 | ```bash
8 | $ npm install bazooka
9 | ```
10 |
11 | ### Browser Support
12 |
13 | Bazooka uses [`MutationObserver`](https://developer.mozilla.org/en/docs/Web/API/MutationObserver) to watch for DOM updates. If you want to use `Baz.watch()` and need to support [browsers without `MutationObserver`](http://caniuse.com/#feat=mutationobserver), you'll need any `MutationObserver` polyfill (we recommend [this one](https://www.npmjs.com/package/mutation-observer))
14 |
15 | Also, Bazooka can initiate components asynchriously (when component's node comes into viewport, via `data-baz-async="viewport"` HTML attribute). For that, Bazooka uses [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). In [browsers without `IntersectionObserver` support](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Browser_compatibility), falls back to the equivalent of `setTimeout(bazFunc, 1, node)`
16 |
17 |
18 | ## Examples
19 |
20 | To view the examples, clone the bazooka repo, install the dependencies and compile examples:
21 |
22 | ```bash
23 | $ git clone git://github.com/seedofjoy/bazooka.git
24 | $ cd bazooka
25 | $ npm install
26 | $ npm run examples
27 | ```
28 |
29 | Then run whichever example you want by opening index.html in `/examples/` subdirectories:
30 | ```bash
31 | $ cd examples
32 | ```
33 |
34 | * **complex** — universal component to work with and without bazooka
35 | * **react-basic** — bazooka + [react](https://facebook.github.io/react/)
36 | * **gifflix** — bazooka + frp (via [kefir.js](https://rpominov.github.io/kefir/))
37 |
38 |
39 | ## [Docs](docs)
40 | - [API](docs/README.md)
41 | - [Helpers (`Baz.h`)](docs/helpers.md)
42 | - [Hot Reloadable `bazFunc`s](docs/hot-reloadable-bazfuncs.md)
43 |
44 | ## [Changelog](CHANGELOG.md)
45 |
46 |
47 | ## Tests
48 |
49 | To run the test suite, first install the dependencies, then run `npm test`:
50 |
51 | ```bash
52 | $ npm install
53 | $ npm test
54 | ```
55 |
56 | ## Lint
57 |
58 | Bazooka uses [prettier](https://github.com/prettier/prettier) linter. To conform with it, just run before creating a commit:
59 |
60 | ```bash
61 | $ npm run fmt
62 | ```
63 |
64 |
65 | ## License
66 |
67 | [MIT](LICENSE)
68 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.9.1
2 |
3 | * :wrench: [fixed] component was not disposed when parent node is removed from a page (#46)
4 |
5 | ## 0.9.0
6 |
7 | * :heavy_plus_sign: [added] support for async bazFunc calls via [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). In [browsers without `IntersectionObserver` support](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Browser_compatibility), falls back to the equivalent of `setTimeout(bazFunc, 1, node)`. Check out [\_async example](/examples/_async)
8 |
9 | ```html
10 | /* (old) sync call */
11 |
12 | will be called on `Bazooka.refresh()` / `Bazooka.watch()`
13 |
14 |
15 | /* (new) async call */
16 |
17 | will be called when in viewport
18 |
19 | ```
20 |
21 | ## 0.8.0
22 |
23 | * :x: [removed] `Baz.h.getAttrs(node)`. Use `Baz.h.getAttrs(prefix, node)` or `Baz.h.getAttrs(prefix)(node)` instead _(deprecated since 0.4.1)_
24 | * :wrench: [fixed] parsing pretty/multiline JSON (like `{\n"a": 1\n}`) by `Baz.h.getAttrs`:
25 |
26 | ```javascript
27 | // node =
28 |
29 | Baz.h.getAttrs('baz', node).json
30 | // prior 0.8.0
31 | // => '{\n"a": 1\n}'
32 |
33 | // after 0.8.0
34 | // => { "a": 1 }
35 | ```
36 |
37 | ## 0.7.0
38 |
39 | [Hot Reload](https://github.com/seedofjoy/bazooka/blob/v0.7.0/docs/hot-reloadable-bazfuncs.md)
40 |
41 | * :heavy_plus_sign: [added] return `dispose` functions from `bazFunc`
42 | * :heavy_plus_sign: [added] `Bazooka.rebind` to update already bound `bazFunc`s
43 | * :heavy_plus_sign: [added] `BazookaWrapper.prototype.HMRState` to preserve state between hot reloads
44 |
45 | ## 0.6.1
46 |
47 | * :wrench: [changed] rethrow first exception from `Bazooka.refresh`
48 |
49 | ## 0.6.0
50 |
51 | * :heavy_plus_sign: [added] wrapped `bazFunc` calls into `try/catch`
52 |
53 | ## 0.5.0
54 |
55 | * :x: [removed] `MutationObserver` and `Function.prototype.bind` polyfills
56 |
57 | ## 0.4.1
58 |
59 | * :warning: [deprecated] `Baz.h.getAttrs(node)`. Use `Baz.h.getAttrs(prefix, node)` or `Baz.h.getAttrs(prefix)(node)` instead
60 |
61 | ## 0.4.0
62 |
63 | * :wrench: [fixed] `data-bazooka` value with multiple whitespaces
64 | * :heavy_plus_sign: [added] support for components without `bazFunc`
65 | * :heavy_plus_sign: [added] `BazookaWrapper.prototype.getComponents`
66 |
67 | ## 0.3.0
68 |
69 | * :heavy_plus_sign: [added] support for binding multiple bazComponents to a single node
70 | * :x: [removed] automatic bazComponent loading via `require()`. Use `Baz.register()` instead
71 |
--------------------------------------------------------------------------------
/examples/gifflix/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | gifflix
6 |
12 |
13 |
14 |
15 |
16 | |
17 |
18 | [0]
19 | |
20 |
21 |
22 |
23 | id:0
24 |
25 | 
26 |
27 | |
28 |
29 | id:1
30 |
31 | 
32 |
33 | |
34 |
35 | id:2
36 |
37 | 
38 |
39 | |
40 |
41 | | |
42 |
43 |
44 | id:2
45 |
46 | 
47 |
48 | |
49 |
50 | id:3
51 |
52 | 
53 |
54 | |
55 |
56 | id:4
57 |
58 | 
59 |
60 | |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/spec/main/mainThrowSpec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function appendDiv(dataBazooka) {
4 | var node = document.createElement('div');
5 | node.setAttribute('test-node', '');
6 | if (dataBazooka) {
7 | node.setAttribute('data-bazooka', dataBazooka);
8 | }
9 | document.body.appendChild(node);
10 | return node;
11 | }
12 |
13 | var componentsRegistry = {
14 | errorousBazFunc: function() {
15 | throw new Error('lol');
16 | },
17 | goodBazFunc: function(node) {
18 | node.setAttribute('data-called', 'yes');
19 | },
20 | };
21 |
22 | describe('Baz', function() {
23 | var Baz = require('bazooka');
24 |
25 | beforeEach(function() {
26 | spyOn(componentsRegistry, 'errorousBazFunc').and.callThrough();
27 | spyOn(componentsRegistry, 'goodBazFunc').and.callThrough();
28 | Baz.register(componentsRegistry);
29 | });
30 |
31 | afterEach(function() {
32 | Array.prototype.forEach.call(
33 | document.querySelectorAll('[test-node]'),
34 | function(el) {
35 | document.body.removeChild(el);
36 | }
37 | );
38 | });
39 |
40 | it('should bind goodBazFunc', function() {
41 | var node = appendDiv('goodBazFunc');
42 | Baz.refresh();
43 |
44 | expect(componentsRegistry.goodBazFunc).toHaveBeenCalledWith(node);
45 | });
46 |
47 | it('should catch error from errorousBazFunc', function() {
48 | console.error = spyOn(console, 'error');
49 |
50 | var node = appendDiv('errorousBazFunc');
51 |
52 | expect(function() {
53 | Baz.refresh();
54 | }).toThrow();
55 |
56 | expect(componentsRegistry.errorousBazFunc).toHaveBeenCalledWith(node);
57 | });
58 |
59 | it('error from errorousBazFunc should not stop goodBazFunc', function() {
60 | console.error = spyOn(console, 'error');
61 |
62 | var node = appendDiv('goodBazFunc errorousBazFunc');
63 | var node2 = appendDiv('errorousBazFunc goodBazFunc');
64 | var node3 = appendDiv('goodBazFunc');
65 |
66 | expect(function() {
67 | Baz.refresh();
68 | }).toThrow();
69 |
70 | expect(componentsRegistry.errorousBazFunc).toHaveBeenCalledWith(node);
71 | expect(componentsRegistry.errorousBazFunc).toHaveBeenCalledWith(node2);
72 | expect(componentsRegistry.errorousBazFunc).not.toHaveBeenCalledWith(node3);
73 |
74 | expect(componentsRegistry.goodBazFunc).toHaveBeenCalledWith(node);
75 | expect(componentsRegistry.goodBazFunc).toHaveBeenCalledWith(node2);
76 | expect(componentsRegistry.goodBazFunc).toHaveBeenCalledWith(node3);
77 |
78 | expect(node.getAttribute('data-called')).toBe('yes');
79 | expect(node2.getAttribute('data-called')).toBe('yes');
80 | expect(node3.getAttribute('data-called')).toBe('yes');
81 | });
82 |
83 | it('should not try to rebind errorous component', function() {
84 | console.error = spyOn(console, 'error');
85 |
86 | var node = appendDiv('errorousBazFunc');
87 |
88 | expect(function() {
89 | Baz.refresh();
90 | }).toThrow();
91 | expect(componentsRegistry.errorousBazFunc).toHaveBeenCalledWith(node);
92 |
93 | Baz.refresh();
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/spec/main/mainSpec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function appendDiv(dataBazooka) {
4 | var node = document.createElement('div');
5 | node.setAttribute('test-node', '');
6 | if (dataBazooka) {
7 | node.setAttribute('data-bazooka', dataBazooka);
8 | }
9 | document.body.appendChild(node);
10 | return node;
11 | }
12 |
13 | var componentsRegistry = {
14 | exampleBazFunc: function() {},
15 | exampleBazFunc2: function() {},
16 | exampleComplexBazComponent: {
17 | bazFunc: function() {},
18 | },
19 | exampleComplexBazFunclessComponent: {
20 | triggers: ['click'],
21 | },
22 | };
23 |
24 | describe('Baz', function() {
25 | var Baz = require('bazooka');
26 |
27 | beforeEach(function() {
28 | spyOn(componentsRegistry, 'exampleBazFunc');
29 | spyOn(componentsRegistry, 'exampleBazFunc2');
30 | spyOn(componentsRegistry.exampleComplexBazComponent, 'bazFunc');
31 | Baz.register(componentsRegistry);
32 | });
33 |
34 | afterEach(function() {
35 | Array.prototype.forEach.call(
36 | document.querySelectorAll('[test-node]'),
37 | function(el) {
38 | document.body.removeChild(el);
39 | }
40 | );
41 | });
42 |
43 | it('should bind simple component to node', function() {
44 | var node = appendDiv('exampleBazFunc');
45 | Baz.refresh();
46 |
47 | expect(componentsRegistry.exampleBazFunc).toHaveBeenCalledWith(node);
48 | });
49 |
50 | it('should not bind incorrect component to node', function() {
51 | var node = appendDiv('exampleBazFunc');
52 | Baz.refresh();
53 |
54 | expect(componentsRegistry.exampleBazFunc2).not.toHaveBeenCalled();
55 | });
56 |
57 | it('should bind complex component to node', function() {
58 | var node = appendDiv('exampleComplexBazComponent');
59 | Baz.refresh();
60 |
61 | expect(
62 | componentsRegistry.exampleComplexBazComponent.bazFunc
63 | ).toHaveBeenCalledWith(node);
64 | });
65 |
66 | it('should bind multiple components to node', function() {
67 | var node = appendDiv('exampleBazFunc exampleComplexBazComponent');
68 | Baz.refresh();
69 |
70 | expect(componentsRegistry.exampleBazFunc).toHaveBeenCalledWith(node);
71 | expect(
72 | componentsRegistry.exampleComplexBazComponent.bazFunc
73 | ).toHaveBeenCalledWith(node);
74 | expect(componentsRegistry.exampleBazFunc2).not.toHaveBeenCalled();
75 | });
76 |
77 | it('should strip extra whitespaces', function() {
78 | var node = appendDiv('exampleBazFunc exampleBazFunc2 ');
79 | var node2 = appendDiv(
80 | 'exampleBazFunc \
81 | exampleBazFunc2'
82 | );
83 | Baz.refresh();
84 |
85 | expect(componentsRegistry.exampleBazFunc).toHaveBeenCalledWith(node);
86 | expect(componentsRegistry.exampleBazFunc2).toHaveBeenCalledWith(node);
87 |
88 | expect(componentsRegistry.exampleBazFunc).toHaveBeenCalledWith(node2);
89 | expect(componentsRegistry.exampleBazFunc2).toHaveBeenCalledWith(node2);
90 | });
91 |
92 | it('should bind bazFuncless component to node', function() {
93 | var node = appendDiv('exampleComplexBazFunclessComponent');
94 | Baz.refresh();
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/spec/main/disposeSpec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // https://github.com/seedofjoy/bazooka/issues/46
4 | describe('dispose', function() {
5 | var Baz = require('bazooka');
6 |
7 | it('should dispose if parent is removed', function(done) {
8 | var node = document.createElement('div');
9 | node.innerHTML = '
';
10 |
11 | Baz.register({
12 | component: function(node) {
13 | node.innerText = 'ok';
14 |
15 | return function() {
16 | done();
17 | };
18 | },
19 | });
20 |
21 | Baz.refresh(node);
22 |
23 | expect(node.innerText).toBe('ok');
24 | expect(node.childNodes[0].tagName).toBe('P');
25 | expect(node.childNodes[0].childNodes[0].tagName).toBe('SPAN');
26 |
27 | node.removeChild(node.childNodes[0]);
28 | Baz.refresh(node);
29 | });
30 |
31 | it('should dispose if direct child is removed', function(done) {
32 | var node = document.createElement('div');
33 | node.innerHTML = '';
34 |
35 | Baz.register({
36 | component: function(node) {
37 | node.innerText = 'ok';
38 |
39 | return function() {
40 | done();
41 | };
42 | },
43 | });
44 |
45 | Baz.refresh(node);
46 | expect(node.innerText).toBe('ok');
47 |
48 | node.innerHTML = '';
49 | Baz.refresh(node);
50 | });
51 |
52 | it('should dispose if node is removed', function(done) {
53 | var node = document.createElement('div');
54 | node.innerHTML = '
';
55 |
56 | Baz.register({
57 | component: function(node) {
58 | node.innerText = 'ok';
59 |
60 | return function() {
61 | done();
62 | };
63 | },
64 | });
65 |
66 | Baz.refresh(node);
67 |
68 | expect(node.innerText).toBe('ok');
69 | expect(node.childNodes[0].tagName).toBe('P');
70 | expect(node.childNodes[0].childNodes[0].tagName).toBe('SPAN');
71 |
72 | node.childNodes[0].removeChild(node.childNodes[0].childNodes[0]);
73 | Baz.refresh(node);
74 | });
75 |
76 | it("shouldn't dispose if node is moved inside the rootNode", function() {
77 | var node = document.createElement('div');
78 | node.innerHTML = '
';
79 |
80 | var disposeSpy = jasmine.createSpy('disposeSpy');
81 |
82 | Baz.register({
83 | component: function(node) {
84 | node.innerText = node.innerText + 'ok';
85 |
86 | return disposeSpy;
87 | },
88 | });
89 |
90 | Baz.refresh(node);
91 |
92 | var pNode = node.childNodes[0];
93 | var divNode = node.childNodes[1];
94 | var spanNode = node.childNodes[0].childNodes[0];
95 |
96 | expect(pNode.tagName).toBe('P');
97 | expect(divNode.tagName).toBe('DIV');
98 | expect(spanNode.tagName).toBe('SPAN');
99 | expect(pNode.innerText).toBe('ok');
100 | expect(divNode.innerText).toBe('');
101 |
102 | divNode.appendChild(spanNode);
103 | Baz.refresh(node);
104 |
105 | expect(disposeSpy).not.toHaveBeenCalled();
106 | expect(pNode.innerText).toBe('');
107 | expect(divNode.innerText).toBe('ok');
108 | });
109 | });
110 |
--------------------------------------------------------------------------------
/spec/main/mainWatchSpec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function appendDiv(dataBazooka) {
4 | var node = document.createElement('div');
5 | node.setAttribute('test-node', '');
6 | if (dataBazooka) {
7 | node.setAttribute('data-bazooka', dataBazooka);
8 | }
9 | document.body.appendChild(node);
10 | return node;
11 | }
12 |
13 | var componentsRegistry = {
14 | bf: function() {},
15 | bf2: function() {},
16 | };
17 |
18 | describe('Baz.watch', function() {
19 | var Baz = require('bazooka');
20 | var observer = null;
21 |
22 | beforeEach(function() {
23 | spyOn(componentsRegistry, 'bf');
24 | spyOn(componentsRegistry, 'bf2');
25 | Baz.register(componentsRegistry);
26 | });
27 |
28 | afterEach(function() {
29 | Array.prototype.forEach.call(
30 | document.querySelectorAll('[test-node]'),
31 | function(el) {
32 | document.body.removeChild(el);
33 | }
34 | );
35 |
36 | if (observer) {
37 | observer();
38 | observer = null;
39 | }
40 | });
41 |
42 | it('should bind existing nodes', function() {
43 | var node = appendDiv('bf');
44 | var node2 = appendDiv('bf2');
45 | observer = Baz.watch();
46 |
47 | expect(componentsRegistry.bf.calls.allArgs()).toEqual([[node]]);
48 | expect(componentsRegistry.bf2.calls.allArgs()).toEqual([[node2]]);
49 | });
50 |
51 | it('should bind added nodes', function(done) {
52 | var node = appendDiv('bf');
53 | observer = Baz.watch();
54 |
55 | componentsRegistry.bf.and.callFake(function() {
56 | expect(componentsRegistry.bf.calls.allArgs()).toEqual([[node], [node2]]);
57 |
58 | done();
59 | });
60 |
61 | var node2 = appendDiv('bf');
62 | });
63 |
64 | it('should dispose removed nodes', function(done) {
65 | componentsRegistry.bf.and.callFake(function() {
66 | return function() {
67 | done();
68 | };
69 | });
70 |
71 | var node = appendDiv('bf');
72 | observer = Baz.watch();
73 |
74 | document.body.removeChild(node);
75 | });
76 |
77 | it('should dispose children of removed nodes', function(done) {
78 | componentsRegistry.bf.and.callFake(function() {
79 | return function() {
80 | done();
81 | };
82 | });
83 |
84 | var node = appendDiv();
85 | node.innerHTML = '';
86 | observer = Baz.watch();
87 |
88 | document.body.removeChild(node);
89 | });
90 |
91 | it("shouldn't dispose children on new heighbours", function(done) {
92 | var disposeSpy = jasmine.createSpy('disposeSpy');
93 |
94 | componentsRegistry.bf.and.callFake(function(node) {
95 | return disposeSpy;
96 | });
97 |
98 | var node = appendDiv();
99 | node.innerHTML = '';
100 | observer = Baz.watch();
101 |
102 | var newNode = document.createElement();
103 | newNode.innerHTML = '';
104 | node.appendChild(newNode);
105 |
106 | setTimeout(
107 | function() {
108 | expect(componentsRegistry.bf.calls.count()).toBe(2);
109 | expect(componentsRegistry.bf.calls.allArgs()).toEqual([
110 | [node.childNodes[0]],
111 | [newNode.childNodes[0]],
112 | ]);
113 | expect(disposeSpy).not.toHaveBeenCalled();
114 | done();
115 | },
116 | 0
117 | );
118 | });
119 | });
120 |
--------------------------------------------------------------------------------
/spec/helpers/getChildrenWithDataSpec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* global describe, beforeEach, afterEach, it, expect */
3 | /* eslint max-nested-callbacks:0 */
4 |
5 | var Baz = require('bazooka');
6 | var getChildrenWithData = Baz.h.getChildrenWithData;
7 |
8 | describe('Baz.h.getChildrenWithData', function() {
9 | var node;
10 |
11 | beforeEach(function() {
12 | node = document.createElement('div');
13 | });
14 |
15 | afterEach(function() {
16 | node = null;
17 | });
18 |
19 | it('should return an empty array', function() {
20 | expect(getChildrenWithData(node, 'data-x-x').length).toEqual(0);
21 | expect(getChildrenWithData(node, 'x-x').length).toEqual(0);
22 | expect(getChildrenWithData(node, 'xX').length).toEqual(0);
23 | });
24 |
25 | it('should get child with data attibute', function() {
26 | var childWithData = document.createElement('div');
27 | childWithData.setAttribute('data-x');
28 | node.appendChild(childWithData);
29 | expect(getChildrenWithData(node, 'data-x')[0]).toEqual(childWithData);
30 | });
31 |
32 | it('should not get child with different data attibute', function() {
33 | var childWithY = document.createElement('div');
34 | var childWithX = document.createElement('div');
35 | childWithY.setAttribute('data-y');
36 | childWithX.setAttribute('data-x');
37 |
38 | node.appendChild(childWithY);
39 | expect(getChildrenWithData(node, 'data-x').length).toEqual(0);
40 |
41 | node.appendChild(childWithX);
42 | expect(getChildrenWithData(node, 'data-x').length).toEqual(1);
43 | expect(getChildrenWithData(node, 'data-x')[0]).toEqual(childWithX);
44 | });
45 |
46 | it('should prefix data attribute key', function() {
47 | var childWithData = document.createElement('div');
48 | childWithData.setAttribute('data-camel-case');
49 | node.appendChild(childWithData);
50 |
51 | expect(getChildrenWithData(node, 'data-camel-case')[0]).toEqual(
52 | childWithData
53 | );
54 | expect(getChildrenWithData(node, 'camel-case')[0]).toEqual(childWithData);
55 | expect(getChildrenWithData(node, 'camelCase')[0]).toEqual(childWithData);
56 | });
57 |
58 | it('should get child with data key and value', function() {
59 | var childWithData = document.createElement('div');
60 | childWithData.setAttribute('data-camel-case', 'value');
61 | node.appendChild(childWithData);
62 |
63 | expect(getChildrenWithData(node, 'camelCase', 'value')[0]).toEqual(
64 | childWithData
65 | );
66 | });
67 |
68 | it('should not get child with data key and different value', function() {
69 | var childWithCorrectValue = document.createElement('div');
70 | var childWithDifferentValue = document.createElement('div');
71 | childWithCorrectValue.setAttribute('data-camel-case', 'value');
72 | childWithDifferentValue.setAttribute('data-camel-case', 'different');
73 |
74 | node.appendChild(childWithDifferentValue);
75 | expect(getChildrenWithData(node, 'camelCase', 'value').length).toEqual(0);
76 |
77 | node.appendChild(childWithCorrectValue);
78 | expect(getChildrenWithData(node, 'camelCase', 'value').length).toEqual(1);
79 | expect(getChildrenWithData(node, 'camelCase', 'value')[0]).toEqual(
80 | childWithCorrectValue
81 | );
82 | });
83 |
84 | it('should throw on empty dataKey', function() {
85 | expect(getChildrenWithData.bind(null, node)).toThrow();
86 | expect(getChildrenWithData.bind(null, node, '')).toThrow();
87 | expect(getChildrenWithData.bind(null, node, null)).toThrow();
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/spec/main/mainRebindSpec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function appendDiv(dataBazooka) {
4 | var node = document.createElement('div');
5 | node.setAttribute('test-node', '');
6 | if (dataBazooka) {
7 | node.setAttribute('data-bazooka', dataBazooka);
8 | }
9 | document.body.appendChild(node);
10 | return node;
11 | }
12 |
13 | var componentsRegistry = {
14 | exampleBazFunc: function() {},
15 | exampleBazFunc2: function() {},
16 | };
17 |
18 | describe('Baz.rebind', function() {
19 | var Baz = require('bazooka');
20 |
21 | beforeEach(function() {
22 | spyOn(componentsRegistry, 'exampleBazFunc').and.callThrough();
23 | spyOn(componentsRegistry, 'exampleBazFunc2').and.callThrough();
24 | Baz.register(componentsRegistry);
25 | });
26 |
27 | afterEach(function() {
28 | Array.prototype.forEach.call(
29 | document.querySelectorAll('[test-node]'),
30 | function(el) {
31 | document.body.removeChild(el);
32 | }
33 | );
34 | });
35 |
36 | it('should rebind only new components', function() {
37 | var node = appendDiv('exampleBazFunc');
38 | var node2 = appendDiv('exampleBazFunc exampleBazFunc2');
39 | Baz.refresh();
40 |
41 | expect(componentsRegistry.exampleBazFunc).toHaveBeenCalledWith(node);
42 | expect(componentsRegistry.exampleBazFunc).toHaveBeenCalledWith(node2);
43 | expect(componentsRegistry.exampleBazFunc2).toHaveBeenCalledWith(node2);
44 |
45 | componentsRegistry.exampleBazFunc.calls.reset();
46 | componentsRegistry.exampleBazFunc2.calls.reset();
47 |
48 | var newRegisterObj = {
49 | exampleBazFunc: function(node) {},
50 | };
51 | spyOn(newRegisterObj, 'exampleBazFunc').and.callThrough();
52 |
53 | Baz.rebind(newRegisterObj);
54 |
55 | expect(componentsRegistry.exampleBazFunc.calls.count()).toEqual(0);
56 | expect(componentsRegistry.exampleBazFunc2.calls.count()).toEqual(0);
57 | expect(newRegisterObj.exampleBazFunc).toHaveBeenCalledWith(node);
58 | expect(newRegisterObj.exampleBazFunc).toHaveBeenCalledWith(node2);
59 |
60 | newRegisterObj.exampleBazFunc.calls.reset();
61 |
62 | Baz.rebind({ exampleBazFunc: componentsRegistry.exampleBazFunc });
63 |
64 | expect(newRegisterObj.exampleBazFunc.calls.count()).toEqual(0);
65 | expect(componentsRegistry.exampleBazFunc2.calls.count()).toEqual(0);
66 | expect(componentsRegistry.exampleBazFunc).toHaveBeenCalledWith(node);
67 | expect(componentsRegistry.exampleBazFunc).toHaveBeenCalledWith(node2);
68 | });
69 |
70 | it('should call dispose', function() {
71 | var node = appendDiv('exampleBazFunc');
72 | var node2 = appendDiv('exampleBazFunc exampleBazFunc2');
73 | Baz.refresh();
74 |
75 | componentsRegistry.exampleBazFunc.calls.reset();
76 | componentsRegistry.exampleBazFunc2.calls.reset();
77 |
78 | var disposeSpy = jasmine.createSpy('dispose');
79 | var newRegisterObj = {
80 | exampleBazFunc: function(node) {
81 | return function() {
82 | disposeSpy(node);
83 | };
84 | },
85 | };
86 | spyOn(newRegisterObj, 'exampleBazFunc').and.callThrough();
87 |
88 | Baz.rebind(newRegisterObj);
89 | expect(disposeSpy.calls.count()).toEqual(0);
90 |
91 | Baz.rebind({ exampleBazFunc: componentsRegistry.exampleBazFunc });
92 | expect(disposeSpy).toHaveBeenCalledWith(node);
93 | expect(disposeSpy).toHaveBeenCalledWith(node2);
94 | });
95 |
96 | it('should call bazFuncs a correct number of times', function() {
97 | var node = appendDiv('exampleBazFunc');
98 | var node2 = appendDiv('exampleBazFunc2');
99 | var node3 = appendDiv('exampleBazFunc exampleBazFunc2');
100 | Baz.refresh();
101 |
102 | componentsRegistry.exampleBazFunc.calls.reset();
103 | componentsRegistry.exampleBazFunc2.calls.reset();
104 |
105 | var disposeSpy = jasmine.createSpy('dispose');
106 | var newRegisterObj = {
107 | exampleBazFunc: function(node) {
108 | return function() {
109 | disposeSpy(node);
110 | };
111 | },
112 | };
113 | spyOn(newRegisterObj, 'exampleBazFunc').and.callThrough();
114 |
115 | Baz.rebind(newRegisterObj);
116 | expect(disposeSpy.calls.count()).toEqual(0);
117 | expect(newRegisterObj.exampleBazFunc.calls.count()).toEqual(2);
118 | expect(componentsRegistry.exampleBazFunc.calls.count()).toEqual(0);
119 | expect(componentsRegistry.exampleBazFunc2.calls.count()).toEqual(0);
120 | expect(newRegisterObj.exampleBazFunc).toHaveBeenCalledWith(node);
121 | expect(newRegisterObj.exampleBazFunc).toHaveBeenCalledWith(node3);
122 | });
123 | });
124 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var IGNORED_ATTRS = ['data-bazid', 'data-bazooka'];
4 |
5 | // `[\s\S]` instead of `.` to allow multiline/pretty JSON
6 | // in other words, because `/./.test('\n') == false`
7 | var rbrace = /^(?:\{[\s\S]*\}|\[[\s\S]*\])$/;
8 | var rdataAttr = /^data-([a-z\d\-]+)$/;
9 | var rdashAlpha = /-([a-z])/gi;
10 | var fcamelCase = function(all, letter) {
11 | return letter.toUpperCase();
12 | };
13 |
14 | function _parseAttr(prefix, parsedAttrs, attr) {
15 | if (typeof attr.value !== 'string') {
16 | return parsedAttrs;
17 | }
18 |
19 | if (!rdataAttr.test(attr.name) || IGNORED_ATTRS.indexOf(attr.name) !== -1) {
20 | return parsedAttrs;
21 | }
22 |
23 | var attrName = attr.name.match(rdataAttr)[1];
24 |
25 | if (prefix) {
26 | prefix = prefix.concat('-');
27 | if (prefix === attrName.slice(0, prefix.length)) {
28 | attrName = attrName.slice(prefix.length);
29 | } else {
30 | return parsedAttrs;
31 | }
32 | }
33 |
34 | var camelCaseName = attrName.replace(rdashAlpha, fcamelCase);
35 |
36 | var data;
37 |
38 | switch (attr.value) {
39 | case 'true':
40 | data = true;
41 | break;
42 | case 'false':
43 | data = false;
44 | break;
45 | case 'null':
46 | data = null;
47 | break;
48 | default:
49 | try {
50 | if (attr.value === +attr.value + '') {
51 | data = +attr.value;
52 | } else if (rbrace.test(attr.value)) {
53 | data = JSON.parse(attr.value);
54 | } else {
55 | data = attr.value;
56 | }
57 | } catch (e) {
58 | return parsedAttrs;
59 | }
60 | }
61 |
62 | parsedAttrs[camelCaseName] = data;
63 | return parsedAttrs;
64 | }
65 |
66 | function _getPrefixedAttrs(prefix, node) {
67 | return Array.prototype.reduce.call(
68 | node.attributes,
69 | _parseAttr.bind(null, prefix),
70 | {}
71 | );
72 | }
73 |
74 | /**
75 | * @module h
76 | * @memberof Bazooka
77 | */
78 | var h = {};
79 |
80 | /**
81 | * Get all prefixed `data-` attributes as an object
82 | * @func getAttrs
83 | * @static
84 | * @param {string} prefix - `data-`attribute prefix
85 | * @param {HTMLNode} [node] - target node
86 | * @returns {function|object} - curried function for parsing node with passed prefix or parsed attrs
87 | * @example
88 | * ```javascript
89 | * //
90 | *
91 | * Baz.h.getAttrs('x', window.n) // => {a: "lol", b: 1}
92 | * Baz.h.getAttrs('y', window.n) // => {y: {key: 1}, composedAttr: true}
93 | *
94 | * const xAttrs = Baz.h.getAttrs('x')
95 | * xAttrs(window.n) // => {x: "lol", b: 1}
96 | * ```
97 | */
98 | h.getAttrs = function(prefix, node) {
99 | if (typeof prefix === 'string' && node === void 0) {
100 | return _getPrefixedAttrs.bind(null, prefix);
101 | }
102 |
103 | return _getPrefixedAttrs(prefix, node);
104 | };
105 |
106 | function _prefixDataKey(dataKey) {
107 | if (!dataKey) {
108 | throw new Error('dataKey must be non empty');
109 | }
110 |
111 | if (dataKey.indexOf('data-') === 0) {
112 | return dataKey;
113 | } else if (dataKey.indexOf('-') >= 0) {
114 | return 'data-' + dataKey;
115 | } else {
116 | return 'data-' + dataKey.replace(/([A-Z])/g, '-$1').toLowerCase();
117 | }
118 | }
119 |
120 | /**
121 | * Query children with specific `data-`attribute
122 | * @func getChildrenWithData
123 | * @static
124 | * @param {HTMLNode} parentNode
125 | * @param {string} dataKey – data-key. `data-baz-key`, `baz-key` and `bazKey` are equivalent
126 | * @param {string} [dataValue] - value of a `data-`attribute
127 | * @returns {NodeList}
128 | * @example
129 | * ```javascript
130 | * //
131 | * //
yep
132 | * //
nope
133 | * //
134 | *
135 | * Baz.h.getChildrenWithData(window.parent, 'data-user-id', 1)[0].textContent === 'yep'
136 | * Baz.h.getChildrenWithData(window.parent, 'user-id', 1)[0].textContent === 'yep'
137 | * Baz.h.getChildrenWithData(window.parent, 'userId', 2)[0].textContent === 'nope'
138 | * ```
139 | */
140 | h.getChildrenWithData = function(parentNode, dataKey, dataValue) {
141 | var prefixedDataKey = _prefixDataKey(dataKey);
142 | var query;
143 |
144 | if (dataValue === void 0) {
145 | query = '[' + prefixedDataKey + ']';
146 | } else {
147 | query = '[' + prefixedDataKey + '="' + dataValue + '"]';
148 | }
149 |
150 | return parentNode.querySelectorAll(query);
151 | };
152 |
153 | module.exports = h;
154 |
--------------------------------------------------------------------------------
/spec/helpers/getAttrSpec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* global describe, beforeEach, afterEach, it, expect */
3 | /* eslint max-nested-callbacks:0 */
4 |
5 | var Baz = require('bazooka');
6 | var getAttrs = Baz.h.getAttrs;
7 |
8 | describe('Baz.h.getAttrs', function() {
9 | var node;
10 |
11 | beforeEach(function() {
12 | spyOn(console, 'warn');
13 | node = document.createElement('div');
14 | });
15 |
16 | afterEach(function() {
17 | node = null;
18 | });
19 |
20 | it('should return an empty object', function() {
21 | expect(getAttrs('', node)).toEqual({});
22 | });
23 |
24 | it('should skip empty keys', function() {
25 | node.setAttribute('data-', 0);
26 | expect(getAttrs('', node)).toEqual({});
27 | });
28 |
29 | it('should skip bazooka keys', function() {
30 | node.setAttribute('data-bazooka', 'test');
31 | node.setAttribute('data-bazid', 12);
32 | expect(getAttrs('', node)).toEqual({});
33 | });
34 |
35 | it('should parse create camelCase keys', function() {
36 | node.setAttribute('data-a', 0);
37 | node.setAttribute('data-abc', 0);
38 | node.setAttribute('data-ab-c-de', 0);
39 |
40 | var attrs = getAttrs('', node);
41 | expect(attrs.a).toBeDefined();
42 | expect(attrs.abc).toBeDefined();
43 | expect(attrs.abCDe).toBeDefined();
44 | });
45 |
46 | it('should parse `true`', function() {
47 | node.setAttribute('data-bool', true);
48 | node.setAttribute('data-bool-string', 'true');
49 |
50 | expect(getAttrs('', node)).toEqual({
51 | bool: true,
52 | boolString: true,
53 | });
54 | });
55 |
56 | it('should parse `false`', function() {
57 | node.setAttribute('data-bool', false);
58 | node.setAttribute('data-bool-string', 'false');
59 |
60 | expect(getAttrs('', node)).toEqual({
61 | bool: false,
62 | boolString: false,
63 | });
64 | });
65 |
66 | it('should parse `null`', function() {
67 | node.setAttribute('data-nul', null);
68 | node.setAttribute('data-nul-string', 'null');
69 |
70 | expect(getAttrs('', node)).toEqual({
71 | nul: null,
72 | nulString: null,
73 | });
74 | });
75 |
76 | it('should parse numbers', function() {
77 | var numbers = [0, 0.1, 100, 123, -0.2];
78 | for (var i = 0; i < numbers.length; i++) {
79 | node.setAttribute('data-num', numbers[i]);
80 | node.setAttribute('data-num-string', numbers[i].toString());
81 |
82 | expect(getAttrs('', node)).toEqual({
83 | num: numbers[i],
84 | numString: numbers[i],
85 | });
86 | }
87 | });
88 |
89 | it('should parse objects', function() {
90 | var objects = ['{}', '{"a": 1}', '{"b": []}', '{\n}'];
91 | for (var i = 0; i < objects.length; i++) {
92 | node.setAttribute('data-obj', objects[i]);
93 |
94 | expect(getAttrs('', node)).toEqual({
95 | obj: JSON.parse(objects[i]),
96 | });
97 | }
98 | });
99 |
100 | it('should parse arrays', function() {
101 | var arrays = ['[]', '[1,2]', '[{}]', '[\n]'];
102 | for (var i = 0; i < arrays.length; i++) {
103 | node.setAttribute('data-arr', arrays[i]);
104 |
105 | expect(getAttrs('', node)).toEqual({
106 | arr: JSON.parse(arrays[i]),
107 | });
108 | }
109 | });
110 |
111 | it('should parse strings', function() {
112 | var strings = ['a', 'sdbsdh', 'слово', ''];
113 | for (var i = 0; i < strings.length; i++) {
114 | node.setAttribute('data-str', strings[i]);
115 |
116 | expect(getAttrs('', node)).toEqual({
117 | str: strings[i],
118 | });
119 | }
120 | });
121 |
122 | it('should bail on invalid json', function() {
123 | var cases = [
124 | ['{', { invalid: '{' }],
125 | ['[][', { invalid: '[][' }],
126 | ['{b: 1}', {}],
127 | ['[]]', {}],
128 | ['{\n]', { invalid: '{\n]' }],
129 | ];
130 | var attr, result;
131 | for (var i = 0; i < cases.length; i++) {
132 | attr = cases[i][0];
133 | result = cases[i][1];
134 | node.setAttribute('data-invalid', attr);
135 |
136 | expect(getAttrs('', node)).toEqual(result);
137 | }
138 | });
139 |
140 | it('should curry over prefix', function() {
141 | var getAttrsCurried = getAttrs('tt');
142 | expect(typeof getAttrsCurried).toBe('function');
143 | expect(getAttrsCurried.length).toBe(1);
144 | });
145 |
146 | it('should apply curried prefix', function() {
147 | var getAttrsCurried = getAttrs('tt');
148 |
149 | node.setAttribute('data-tt-a', 0);
150 | node.setAttribute('data-tt-abc', 0);
151 | node.setAttribute('data-tt-ab-c-de', 0);
152 |
153 | var attrs = getAttrsCurried(node);
154 | expect(attrs.a).toBeDefined();
155 | expect(attrs.abc).toBeDefined();
156 | expect(attrs.abCDe).toBeDefined();
157 | });
158 |
159 | it('should apply prefix', function() {
160 | node.setAttribute('data-tt-a', 0);
161 | node.setAttribute('data-tt-abc', 0);
162 | node.setAttribute('data-tt-ab-c-de', 0);
163 |
164 | var attrs = getAttrs('tt', node);
165 | expect(attrs.a).toBeDefined();
166 | expect(attrs.abc).toBeDefined();
167 | expect(attrs.abCDe).toBeDefined();
168 | });
169 | });
170 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ## Modules
2 |
3 |
4 | - BazComponent
5 | Interface of component, required by Bazooka.refresh
6 |
7 | - Bazooka ⇒
BazookaWrapper
8 | Bazooka
9 |
10 |
11 |
12 | ## Typedefs
13 |
14 |
15 | - HMRStateCallback ⇒
Object
16 | Callback to get state between Webpack's hot module reloads (HMR)
17 |
18 |
19 |
20 |
21 |
22 | ## BazComponent
23 | Interface of component, required by [Bazooka.refresh](#module_Bazooka.refresh)
24 |
25 |
26 | * [BazComponent](#module_BazComponent)
27 | * [~simple](#module_BazComponent..simple) ⇒ function
28 | * [~universal](#module_BazComponent..universal)
29 |
30 |
31 |
32 | ### BazComponent~simple ⇒ function
33 | CommonJS module written only with Bazooka interface to be used with `data-bazooka`
34 |
35 | **Kind**: inner interface of [BazComponent](#module_BazComponent)
36 | **Returns**: function - `dispose` callback to cleanup component's `eventListeners`, timers, etc. after [Bazooka.rebind](#module_Bazooka.rebind) or removal of the node from DOM
37 |
38 | | Type | Description |
39 | | --- | --- |
40 | | node | bound DOM node |
41 |
42 | **Example**
43 | ```javascript
44 | module.exports = function bazFunc(node) {}
45 | ```
46 |
47 |
48 | ### BazComponent~universal
49 | CommonJS module with Bazooka interface, so it can be used both in `data-bazooka`
50 | and in another CommonJS modules via `require()`
51 |
52 | **Kind**: inner interface of [BazComponent](#module_BazComponent)
53 | **Example**
54 | ```javascript
55 | function trackEvent(category, action, label) {}
56 | module.exports = {
57 | bazFunc: function bazFunc(node) { node.onclick = trackEvent.bind(…) },
58 | trackEvent: trackEvent,
59 | }
60 | ```
61 |
62 |
63 | ## Bazooka ⇒ [BazookaWrapper](#BazookaWrapper)
64 | Bazooka
65 |
66 |
67 | | Param | Type | Description |
68 | | --- | --- | --- |
69 | | value | node | [BazookaWrapper](#BazookaWrapper) | DOM node or wrapped node |
70 |
71 | **Example**
72 | ```javascript
73 | var Baz = require('bazooka');
74 | var $baz = Baz(node);
75 | ```
76 |
77 | * [Bazooka](#module_Bazooka) ⇒ [BazookaWrapper](#BazookaWrapper)
78 | * _static_
79 | * [.register(componentsObj)](#module_Bazooka.register)
80 | * [.refresh([rootNode])](#module_Bazooka.refresh)
81 | * [.rebind(componentsObj)](#module_Bazooka.rebind)
82 | * [.watch([rootNode])](#module_Bazooka.watch) ⇒ function
83 | * _inner_
84 | * [~BazookaWrapper](#module_Bazooka..BazookaWrapper)
85 |
86 |
87 |
88 | ### Bazooka.register(componentsObj)
89 | Register components names
90 |
91 | **Kind**: static method of [Bazooka](#module_Bazooka)
92 |
93 | | Param | Type | Description |
94 | | --- | --- | --- |
95 | | componentsObj | Object | object with names as keys and components as values |
96 |
97 |
98 |
99 | ### Bazooka.refresh([rootNode])
100 | Parse and bind bazooka components to nodes without bound components
101 |
102 | **Kind**: static method of [Bazooka](#module_Bazooka)
103 |
104 | | Param | Type | Default | Description |
105 | | --- | --- | --- | --- |
106 | | [rootNode] | node | document.body | DOM node, children of which will be checked for `data-bazooka` |
107 |
108 |
109 |
110 | ### Bazooka.rebind(componentsObj)
111 | Rebind existing components. Nodes with already bound component will be [disposed](BazFunc.dispose) and bound again to a new `bazFunc`
112 |
113 | **Kind**: static method of [Bazooka](#module_Bazooka)
114 |
115 | | Param | Type | Description |
116 | | --- | --- | --- |
117 | | componentsObj | Object | object with new components |
118 |
119 | **Example**
120 | ```javascript
121 | import bazFunc from './bazFunc.js'
122 |
123 | Baz.register({
124 | bazFunc: bazFunc,
125 | });
126 |
127 | Baz.watch();
128 |
129 | if (module.hot) {
130 | module.hot.accept('./bazFunc.js', () => Baz.rebind({ bazFunc: bazFunc }));
131 | // or, if you prefer `require()`
132 | // module.hot.accept('./bazFunc.js', () => Baz.rebind({ bazFunc: require('./bazFunc.js') }));
133 | }
134 | ```
135 |
136 |
137 | ### Bazooka.watch([rootNode]) ⇒ function
138 | Watch for new nodes with `data-bazooka`. No need to run [Bazooka.refresh](#module_Bazooka.refresh) before this. It will be called automatically.
139 |
140 | **Kind**: static method of [Bazooka](#module_Bazooka)
141 | **Returns**: function - Unwatch function
142 |
143 | | Param | Type | Default | Description |
144 | | --- | --- | --- | --- |
145 | | [rootNode] | node | document.body | DOM node, children of which will be watched for `data-bazooka` |
146 |
147 |
148 |
149 | ### Bazooka~BazookaWrapper
150 | Reference to [BazookaWrapper](#BazookaWrapper) class
151 |
152 | **Kind**: inner property of [Bazooka](#module_Bazooka)
153 |
154 |
155 | ## HMRStateCallback ⇒ Object
156 | Callback to get state between Webpack's hot module reloads (HMR)
157 |
158 | **Kind**: global typedef
159 | **Returns**: Object - whatever state should be after HMR
160 |
161 | | Param | Type | Description |
162 | | --- | --- | --- |
163 | | previous | Object | state. `undefined` on first call |
164 |
165 |
166 |
167 | ## ~BazookaWrapper
168 | **Kind**: inner class
169 |
170 | * [~BazookaWrapper](#BazookaWrapper)
171 | * [.getComponents()](#BazookaWrapper+getComponents) ⇒ Object.<string, BazComponent>
172 | * [.HMRState(moduleHot, stateCallback)](#BazookaWrapper+HMRState) ⇒ Object
173 |
174 |
175 |
176 | ### bazookaWrapper.getComponents() ⇒ Object.<string, BazComponent>
177 | **Kind**: instance method of [BazookaWrapper](#BazookaWrapper)
178 | **Returns**: Object.<string, BazComponent> - object of the bound to the wrapped node [BazComponents](#module_BazComponent)
179 |
180 |
181 | ### bazookaWrapper.HMRState(moduleHot, stateCallback) ⇒ Object
182 | Helper method to preserve component's state between Webpack's hot module reloads (HMR)
183 |
184 | **Kind**: instance method of [BazookaWrapper](#BazookaWrapper)
185 | **Returns**: Object - value from `stateCallback`
186 |
187 | | Param | Type | Description |
188 | | --- | --- | --- |
189 | | moduleHot | webpackHotModule | [module.hot](https://github.com/webpack/webpack/blob/e7c13d75e4337cf166d421c153804892c49511bd/lib/HotModuleReplacement.runtime.js#L80) of the component |
190 | | stateCallback | [HMRStateCallback](#HMRStateCallback) | callback to create state. Called with undefined `prev` on initial binding and with `prev` equal latest component state after every HMR |
191 |
192 | **Example**
193 | ```javascript
194 | const state = module.hot
195 | ? Baz(node).HMRState(module.hot, prev => prev || model())
196 | : model();
197 | ```
198 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _bazId = 0;
4 | var nodesComponentsRegistry = {};
5 | var componentsRegistry = {};
6 | var wrappersRegistry = {};
7 |
8 | var _interObs = null;
9 |
10 | function _onIntersection(entries, observer) {
11 | var entry;
12 | var caughtException;
13 |
14 | for (var i = 0; i < entries.length; i++) {
15 | entry = entries[i];
16 | if (entry.intersectionRatio > 0) {
17 | observer.unobserve(entry.target);
18 | try {
19 | _wrapAndBindNode(entry.target);
20 | } catch (e) {
21 | if (!caughtException) {
22 | caughtException = e;
23 | }
24 | }
25 | }
26 | }
27 |
28 | if (caughtException) {
29 | throw caughtException;
30 | }
31 | }
32 |
33 | var _getIntersectionObserver = 'IntersectionObserver' in window
34 | ? function() {
35 | if (_interObs) {
36 | return _interObs;
37 | }
38 |
39 | return (_interObs = new IntersectionObserver(_onIntersection, {
40 | rootMargin: '20px 0px',
41 | }));
42 | }
43 | : function() {
44 | return null;
45 | };
46 |
47 | function _id(value) {
48 | return value;
49 | }
50 |
51 | function _getComponent(name) {
52 | if (!componentsRegistry[name]) {
53 | throw new Error(
54 | name + ' component is not registered. Use `Baz.register()` to do it'
55 | );
56 | }
57 |
58 | return componentsRegistry[name];
59 | }
60 |
61 | function _bindComponentToNode(wrappedNode, componentName) {
62 | var bazId = wrappedNode.id;
63 |
64 | if (!componentName) {
65 | return;
66 | }
67 |
68 | if (!nodesComponentsRegistry[bazId]) {
69 | nodesComponentsRegistry[bazId] = {};
70 | }
71 |
72 | if (!nodesComponentsRegistry[bazId][componentName]) {
73 | nodesComponentsRegistry[bazId][componentName] = true;
74 | }
75 | }
76 |
77 | function _applyComponentToNode(componentName, wrappedNode) {
78 | var bazId = wrappedNode.id;
79 | var component = _getComponent(componentName);
80 | var dispose;
81 |
82 | if (component.bazFunc) {
83 | dispose = component.bazFunc(wrappedNode.__wrapped__);
84 |
85 | if (typeof dispose === 'function') {
86 | wrappedNode.__disposesMap__[componentName] = dispose;
87 | } else if (wrappedNode.__disposesMap__[componentName]) {
88 | wrappedNode.__disposesMap__[componentName] = null;
89 | }
90 | }
91 | }
92 |
93 | /**
94 | * @class BazookaWrapper
95 | * @inner
96 | */
97 | function BazookaWrapper(node) {
98 | var bazId = node.getAttribute('data-bazid');
99 |
100 | if (bazId == null) {
101 | bazId = (_bazId++).toString();
102 | node.setAttribute('data-bazid', bazId);
103 | wrappersRegistry[bazId] = this;
104 | this.__disposesMap__ = {};
105 | } else {
106 | this.__disposesMap__ = wrappersRegistry[bazId].__disposesMap__;
107 | }
108 |
109 | /**
110 | * Internal id
111 | * @name Bazooka.id
112 | * @type {string}
113 | * @memberof Bazooka
114 | * @instance
115 | */
116 | this.id = bazId;
117 | this.__wrapped__ = node;
118 | }
119 |
120 | /**
121 | * @ignore
122 | * @constructor
123 | * @param {node} — DOM node with a bound components
124 | */
125 | BazookaWrapper.prototype.constructor = BazookaWrapper;
126 |
127 | /**
128 | * @returns {Object.} object of the bound to the wrapped node [BazComponents]{@link module:BazComponent}
129 | */
130 | BazookaWrapper.prototype.getComponents = function() {
131 | var components = {};
132 |
133 | for (var componentName in nodesComponentsRegistry[this.id]) {
134 | components[componentName] = _getComponent(componentName);
135 | }
136 |
137 | return components;
138 | };
139 |
140 | /**
141 | * Callback to get state between Webpack's hot module reloads (HMR)
142 | *
143 | * @callback HMRStateCallback
144 | * @param {Object?} previous state. `undefined` on first call
145 | * @returns {Object} whatever state should be after HMR
146 | */
147 |
148 | /**
149 | * Helper method to preserve component's state between Webpack's hot module reloads (HMR)
150 | * @param {webpackHotModule} moduleHot - [module.hot](https://github.com/webpack/webpack/blob/e7c13d75e4337cf166d421c153804892c49511bd/lib/HotModuleReplacement.runtime.js#L80) of the component
151 | * @param {HMRStateCallback} stateCallback - callback to create state. Called with undefined `prev` on initial binding and with `prev` equal latest component state after every HMR
152 | * @example
153 | * ```javascript
154 | * const state = module.hot
155 | * ? Baz(node).HMRState(module.hot, prev => prev || model())
156 | * : model();
157 | * ```
158 | * @returns {Object} value from `stateCallback`
159 | */
160 | BazookaWrapper.prototype.HMRState = function(moduleHot, stateCallback) {
161 | // moduleHot is bazFunc's `module.hot` (with method related to *that* bazFunc)
162 | var state;
163 | moduleHot.dispose(
164 | function(data) {
165 | data[this.id] = state;
166 | }.bind(this)
167 | );
168 |
169 | if (moduleHot.data && moduleHot.data[this.id]) {
170 | state = stateCallback(moduleHot.data[this.id]);
171 | moduleHot.data[this.id] = null;
172 | } else {
173 | state = stateCallback();
174 | }
175 |
176 | return state;
177 | };
178 |
179 | function _wrapAndBindNode(node) {
180 | var dataBazooka = (node.getAttribute('data-bazooka') || '').trim();
181 | var wrappedNode;
182 | var componentNames;
183 | var componentName;
184 | var caughtException;
185 |
186 | if (dataBazooka) {
187 | componentNames = dataBazooka.split(' ');
188 | wrappedNode = new BazookaWrapper(node);
189 |
190 | for (var i = 0; i < componentNames.length; i++) {
191 | _bindComponentToNode(wrappedNode, componentNames[i].trim());
192 | }
193 |
194 | for (var componentName in nodesComponentsRegistry[wrappedNode.id]) {
195 | try {
196 | _applyComponentToNode(componentName, wrappedNode);
197 | } catch (e) {
198 | console.error(
199 | componentName + ' component throws during initialization.',
200 | e
201 | );
202 | if (!caughtException) {
203 | caughtException = e;
204 | }
205 | }
206 | }
207 |
208 | if (caughtException) {
209 | throw caughtException;
210 | }
211 | }
212 | }
213 |
214 | function _observeNodeForWrap(node) {
215 | var dataBazooka = (node.getAttribute('data-bazooka') || '').trim();
216 |
217 | if (dataBazooka) {
218 | if (node.getAttribute('data-baz-async') === 'viewport') {
219 | var intersectionObserver = _getIntersectionObserver();
220 |
221 | new BazookaWrapper(node); // to avoid double call of _wrapAndBindNode
222 |
223 | if (intersectionObserver) {
224 | intersectionObserver.observe(node);
225 | } else {
226 | setTimeout(_wrapAndBindNode, 1, node);
227 | }
228 | } else {
229 | _wrapAndBindNode(node);
230 | }
231 | }
232 | }
233 |
234 | /**
235 | * @interface BazComponent
236 | * @exports BazComponent
237 | * @description Interface of component, required by [Bazooka.refresh]{@link module:Bazooka.refresh}
238 | */
239 |
240 | /**
241 | * @name simple
242 | * @func
243 | * @interface
244 | * @param {node} - bound DOM node
245 | * @description CommonJS module written only with Bazooka interface to be used with `data-bazooka`
246 | * @returns {function} `dispose` callback to cleanup component's `eventListeners`, timers, etc. after [Bazooka.rebind]{@link module:Bazooka.rebind} or removal of the node from DOM
247 | * @example
248 | * ```javascript
249 | * module.exports = function bazFunc(node) {}
250 | * ```
251 | */
252 |
253 | /**
254 | * @name universal
255 | * @interface
256 | * @description CommonJS module with Bazooka interface, so it can be used both in `data-bazooka`
257 | * and in another CommonJS modules via `require()`
258 | * @example
259 | * ```javascript
260 | * function trackEvent(category, action, label) {}
261 | * module.exports = {
262 | * bazFunc: function bazFunc(node) { node.onclick = trackEvent.bind(…) },
263 | * trackEvent: trackEvent,
264 | * }
265 | * ```
266 | */
267 |
268 | /**
269 | * @name bazFunc
270 | * @abstract
271 | * @memberof BazComponent.universal
272 | * @func
273 | * @param {node} - bound DOM node
274 | * @description Component's binding function
275 | * @returns {function} `dispose` callback to cleanup component's `eventListeners`, timers, etc. after [Bazooka.rebind]{@link module:Bazooka.rebind} or removal of the node from DOM
276 | */
277 |
278 | /**
279 | * @module {function} Bazooka
280 | * @param {node|BazookaWrapper} value - DOM node or wrapped node
281 | * @returns {BazookaWrapper}
282 | * @example
283 | * ```javascript
284 | * var Baz = require('bazooka');
285 | * var $baz = Baz(node);
286 | * ```
287 | */
288 | var Bazooka = function(value) {
289 | if (value instanceof BazookaWrapper) {
290 | return value;
291 | }
292 |
293 | return new BazookaWrapper(value);
294 | };
295 |
296 | /**
297 | * Reference to {@link BazookaWrapper} class
298 | * @name BazookaWrapper
299 | */
300 | Bazooka.BazookaWrapper = BazookaWrapper;
301 |
302 | Bazooka.h = require('./helpers');
303 |
304 | /**
305 | * Register components names
306 | * @func register
307 | * @param {Object} componentsObj - object with names as keys and components as values
308 | * @static
309 | */
310 | Bazooka.register = function(componentsObj) {
311 | for (var name in componentsObj) {
312 | if (typeof componentsObj[name] === 'function') {
313 | componentsRegistry[name] = {
314 | bazFunc: componentsObj[name],
315 | };
316 | } else {
317 | componentsRegistry[name] = componentsObj[name];
318 | }
319 | }
320 | };
321 |
322 | function _nodeMovedOutOfRoot(wrappedNode, rootNode) {
323 | return !wrappedNode.parentNode || !rootNode.contains(wrappedNode);
324 | }
325 |
326 | /**
327 | * Parse and bind bazooka components to nodes without bound components
328 | * @func refresh
329 | * @param {node} [rootNode=document.body] - DOM node, children of which will be checked for `data-bazooka`
330 | * @static
331 | */
332 | Bazooka.refresh = function(rootNode, _watchRoot) {
333 | rootNode = rootNode || document.body;
334 | var nodes;
335 | var caughtException;
336 | var wrapper;
337 |
338 | for (var bazId in wrappersRegistry) {
339 | wrapper = wrappersRegistry[bazId];
340 | if (
341 | wrapper &&
342 | _nodeMovedOutOfRoot(wrapper.__wrapped__, _watchRoot || rootNode)
343 | ) {
344 | for (var disposableComponentName in wrapper.__disposesMap__) {
345 | if (
346 | typeof wrapper.__disposesMap__[disposableComponentName] === 'function'
347 | ) {
348 | wrapper.__disposesMap__[disposableComponentName]();
349 | wrapper.__disposesMap__[disposableComponentName] = null;
350 | }
351 | }
352 |
353 | wrappersRegistry[bazId] = null;
354 | nodesComponentsRegistry[bazId] = {};
355 | }
356 | }
357 |
358 | wrapper = null;
359 |
360 | nodes = Array.prototype.map.call(
361 | rootNode.querySelectorAll('[data-bazooka]:not([data-bazid])'),
362 | _id
363 | );
364 |
365 | for (var i = 0; i < nodes.length; i++) {
366 | try {
367 | _observeNodeForWrap(nodes[i]);
368 | } catch (e) {
369 | if (!caughtException) {
370 | caughtException = e;
371 | }
372 | }
373 | }
374 |
375 | if (caughtException) {
376 | throw caughtException;
377 | }
378 | };
379 |
380 | /**
381 | * Rebind existing components. Nodes with already bound component will be [disposed]{@link BazFunc.dispose} and bound again to a new `bazFunc`
382 | * @func rebind
383 | * @param {Object} componentsObj - object with new components
384 | * @example
385 | * ```javascript
386 | * import bazFunc from './bazFunc.js'
387 | *
388 | * Baz.register({
389 | * bazFunc: bazFunc,
390 | * });
391 | *
392 | * Baz.watch();
393 | *
394 | * if (module.hot) {
395 | * module.hot.accept('./bazFunc.js', () => Baz.rebind({ bazFunc: bazFunc }));
396 | * // or, if you prefer `require()`
397 | * // module.hot.accept('./bazFunc.js', () => Baz.rebind({ bazFunc: require('./bazFunc.js') }));
398 | * }
399 | * ```
400 | * @static
401 | */
402 | Bazooka.rebind = function rebind(componentsObj) {
403 | var wrappedNode;
404 |
405 | Bazooka.register(componentsObj);
406 |
407 | for (var componentName in componentsObj) {
408 | for (var bazId in wrappersRegistry) {
409 | wrappedNode = wrappersRegistry[bazId];
410 |
411 | if (!wrappedNode) {
412 | continue;
413 | }
414 |
415 | if (!nodesComponentsRegistry[bazId][componentName]) {
416 | continue;
417 | }
418 |
419 | if (
420 | wrappedNode &&
421 | typeof wrappedNode.__disposesMap__[componentName] === 'function'
422 | ) {
423 | wrappedNode.__disposesMap__[componentName]();
424 | wrappedNode.__disposesMap__[componentName] = null;
425 | }
426 |
427 | _applyComponentToNode(componentName, wrappedNode);
428 | }
429 | }
430 | };
431 |
432 | function _MutationObserverCallback(mutations, rootNode) {
433 | for (var i = 0; i < mutations.length; i++) {
434 | Bazooka.refresh(mutations[i].target, rootNode);
435 | }
436 | }
437 |
438 | /**
439 | * Watch for new nodes with `data-bazooka`. No need to run [Bazooka.refresh]{@link module:Bazooka.refresh} before this. It will be called automatically.
440 | * @func watch
441 | * @param {node} [rootNode=document.body] - DOM node, children of which will be watched for `data-bazooka`
442 | * @static
443 | * @returns {function} Unwatch function
444 | */
445 | Bazooka.watch = function(rootNode) {
446 | var observer = new MutationObserver(function(mutations) {
447 | return _MutationObserverCallback(mutations, rootNode);
448 | });
449 | rootNode = rootNode || document.body;
450 |
451 | Bazooka.refresh(rootNode);
452 | observer.observe(rootNode, { childList: true, subtree: true });
453 |
454 | return observer.disconnect.bind(observer);
455 | };
456 |
457 | module.exports = Bazooka;
458 |
--------------------------------------------------------------------------------