├── .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 [![build status](https://github.com/seedofjoy/bazooka/actions/workflows/node.js.yml/badge.svg)](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 | 20 | 21 | 22 | 28 | 34 | 40 | 41 | 42 | 43 | 49 | 55 | 61 | 62 |
17 |   18 | [0] 19 |
23 | id:0
24 |
25 | 26 |
27 |
29 | id:1
30 |
31 | 32 |
33 |
35 | id:2
36 |
37 | 38 |
39 |
 
44 | id:2
45 |
46 | 47 |
48 |
50 | id:3
51 |
52 | 53 |
54 |
56 | id:4
57 |
58 | 59 |
60 |
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 |
BazookaBazookaWrapper
8 |

Bazooka

9 |
10 |
11 | 12 | ## Typedefs 13 | 14 |
15 |
HMRStateCallbackObject
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 | --------------------------------------------------------------------------------