├── .github └── workflows │ └── blank.yml ├── .gitignore ├── API.md ├── Gulpfile.js ├── README.md ├── demo.html ├── index.browser.js ├── jsdoc.json ├── lib ├── index.js └── index.spec.js ├── package-lock.json ├── package.json └── webpack.config.js /.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: borales/actions-yarn@v2.1.0 12 | with: 13 | cmd: install 14 | - uses: borales/actions-yarn@v2.1.0 15 | with: 16 | cmd: test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .idea 4 | jblocks.js 5 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## jBlocks : object 4 | Methods to define components 5 | using declaration and create new instances. 6 | Also helps to find and destroy components. 7 | 8 | **Kind**: global namespace 9 | 10 | * [jBlocks](#jBlocks) : object 11 | * [.Component](#jBlocks.Component) 12 | * [new Component(node, name, props)](#new_jBlocks.Component_new) 13 | * _instance_ 14 | * [.name](#jBlocks.Component+name) : String 15 | * [.node](#jBlocks.Component+node) : HTMLElement 16 | * [.props](#jBlocks.Component+props) : Object 17 | * _static_ 18 | * [.on(event, callback)](#jBlocks.Component.on) ⇒ [Component](#jBlocks.Component) 19 | * [.off(event, callback)](#jBlocks.Component.off) ⇒ [Component](#jBlocks.Component) 20 | * [.emit(event, data)](#jBlocks.Component.emit) ⇒ [Component](#jBlocks.Component) 21 | * [.once(event, callback)](#jBlocks.Component.once) ⇒ [Component](#jBlocks.Component) 22 | * [.destroy()](#jBlocks.Component.destroy) ⇒ [Component](#jBlocks.Component) 23 | * [.destroy(node)](#jBlocks.destroy) ⇒ [jBlocks](#jBlocks) 24 | * [.forget(name)](#jBlocks.forget) ⇒ [jBlocks](#jBlocks) 25 | * [.get(node)](#jBlocks.get) ⇒ [Component](#jBlocks.Component) 26 | 27 | 28 | 29 | ### jBlocks.Component 30 | **Kind**: static class of [jBlocks](#jBlocks) 31 | 32 | * [.Component](#jBlocks.Component) 33 | * [new Component(node, name, props)](#new_jBlocks.Component_new) 34 | * _instance_ 35 | * [.name](#jBlocks.Component+name) : String 36 | * [.node](#jBlocks.Component+node) : HTMLElement 37 | * [.props](#jBlocks.Component+props) : Object 38 | * _static_ 39 | * [.on(event, callback)](#jBlocks.Component.on) ⇒ [Component](#jBlocks.Component) 40 | * [.off(event, callback)](#jBlocks.Component.off) ⇒ [Component](#jBlocks.Component) 41 | * [.emit(event, data)](#jBlocks.Component.emit) ⇒ [Component](#jBlocks.Component) 42 | * [.once(event, callback)](#jBlocks.Component.once) ⇒ [Component](#jBlocks.Component) 43 | * [.destroy()](#jBlocks.Component.destroy) ⇒ [Component](#jBlocks.Component) 44 | 45 | 46 | 47 | #### new Component(node, name, props) 48 | 49 | | Param | Type | 50 | | --- | --- | 51 | | node | HTMLElement | 52 | | name | String | 53 | | props | Object | 54 | 55 | 56 | 57 | #### component.name : String 58 | Name of the components used in decl 59 | 60 | **Kind**: instance property of [Component](#jBlocks.Component) 61 | 62 | 63 | #### component.node : HTMLElement 64 | Node which component is binded with 65 | 66 | **Kind**: instance property of [Component](#jBlocks.Component) 67 | 68 | 69 | #### component.props : Object 70 | Props of the component gained from data-props 71 | 72 | **Kind**: instance property of [Component](#jBlocks.Component) 73 | 74 | 75 | #### Component.on(event, callback) ⇒ [Component](#jBlocks.Component) 76 | Attach an event handler function for the given event 77 | 78 | **Kind**: static method of [Component](#jBlocks.Component) 79 | 80 | | Param | Type | 81 | | --- | --- | 82 | | event | String | 83 | | callback | function | 84 | 85 | 86 | 87 | #### Component.off(event, callback) ⇒ [Component](#jBlocks.Component) 88 | Remove an event handler function for the given event 89 | 90 | **Kind**: static method of [Component](#jBlocks.Component) 91 | 92 | | Param | Type | 93 | | --- | --- | 94 | | event | String | 95 | | callback | function | 96 | 97 | 98 | 99 | #### Component.emit(event, data) ⇒ [Component](#jBlocks.Component) 100 | Execute all handlers attached for the given event 101 | 102 | **Kind**: static method of [Component](#jBlocks.Component) 103 | 104 | | Param | Type | 105 | | --- | --- | 106 | | event | String | 107 | | data | \* | 108 | 109 | 110 | 111 | #### Component.once(event, callback) ⇒ [Component](#jBlocks.Component) 112 | Attach an event handler function for the given event 113 | which will be called only once 114 | 115 | **Kind**: static method of [Component](#jBlocks.Component) 116 | 117 | | Param | Type | 118 | | --- | --- | 119 | | event | String | 120 | | callback | function | 121 | 122 | 123 | 124 | #### Component.destroy() ⇒ [Component](#jBlocks.Component) 125 | Destroy the instance 126 | 127 | **Kind**: static method of [Component](#jBlocks.Component) 128 | 129 | 130 | ### jBlocks.destroy(node) ⇒ [jBlocks](#jBlocks) 131 | Destroy instance binded to the node 132 | 133 | **Kind**: static method of [jBlocks](#jBlocks) 134 | 135 | | Param | Type | 136 | | --- | --- | 137 | | node | HTMLElement | 138 | 139 | 140 | 141 | ### jBlocks.forget(name) ⇒ [jBlocks](#jBlocks) 142 | Remove declaration from cache 143 | 144 | **Kind**: static method of [jBlocks](#jBlocks) 145 | 146 | | Param | Type | Description | 147 | | --- | --- | --- | 148 | | name | String | name of component | 149 | 150 | 151 | 152 | ### jBlocks.get(node) ⇒ [Component](#jBlocks.Component) 153 | Create and return a new instance of component 154 | 155 | **Kind**: static method of [jBlocks](#jBlocks) 156 | **Returns**: [Component](#jBlocks.Component) - a new instance 157 | 158 | | Param | Type | 159 | | --- | --- | 160 | | node | HTMLElement | 161 | 162 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var mocha = require('gulp-mocha-phantomjs'); 3 | 4 | gulp.task('test', function() { 5 | return gulp 6 | .src('tests/index.html') 7 | .pipe(mocha({reporter: 'nyan'})); 8 | }); 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jBlocks 2 | 3 | [![NPM version](https://badge.fury.io/js/jblocks.svg)](http://badge.fury.io/js/jblocks) 4 | ![Tests Status](https://github.com/vitkarpov/jblocks/workflows/Unit%20Tests/badge.svg) 5 | 6 | - **[Codepen DEMO](http://codepen.io/vitkarpov/pen/eZReaE?editors=0010)** 7 | - **[Full API Doc created from source](https://github.com/vitkarpov/jblocks/blob/master/API.md)** 8 | 9 | jBlocks helps to create interface components in a functional programming flavour. 10 | 11 | It's build on the following simple rules: 12 | 13 | - declare your components 14 | - set special data-attributes in HTML to bind an instance of the component (will be created in the future) to the node 15 | - interact with components using API, events and other components 16 | 17 | ## Give me an example 18 | 19 | Let we have a simple component. 20 | 21 | Counter with 2 buttons to increase and decrease its value. It could be a lot of independent counters on a page with different initials values. 22 | 23 | At first we need to declare a component in JavaScript and then mark some nodes in HTML using special data-attributes. 24 | 25 | ## Declare a component in JavaScript 26 | 27 | Declare a component: 28 | 29 | ```js 30 | jBlocks.define('counter', { 31 | events: { 32 | 'click .js-inc': 'inc', 33 | 'click .js-dec': 'dec' 34 | }, 35 | 36 | methods: { 37 | oninit: function() { 38 | this._currentValue = Number(this.props.initialValue); 39 | }, 40 | ondestroy: function() { 41 | this._currentValue = null; 42 | }, 43 | /** 44 | * Increases the counter, emits changed event 45 | */ 46 | inc: function() { 47 | this._currentValue++; 48 | this.emit('changed', { 49 | value: this._currentValue 50 | }); 51 | }, 52 | /** 53 | * Decreases the counter, emits changed event 54 | */ 55 | dec: function() { 56 | this._currentValue--; 57 | this.emit('changed', { 58 | value: this._currentValue 59 | }); 60 | }, 61 | /** 62 | * Returns the current value 63 | * @return {Number} 64 | */ 65 | getCurrentValue: function() { 66 | return this._currentValue; 67 | } 68 | } 69 | }) 70 | ``` 71 | 72 | ## Declare a component in HTML 73 | 74 | To make some node as a root node of the component we should set special `data` attributes: 75 | 76 | - `data-component` — name of the given component (`counter` in this case) 77 | - `data-props` — initial properties (`{ "initialValue": 2 }` in this case) 78 | 79 | ```html 80 |
81 | 82 | 83 |
84 | ``` 85 | 86 | ## Create instances and interact with them 87 | 88 | After describing all components in declarative way it's time to create an instance and interact with it using API: 89 | 90 | ```js 91 | // somewhere in my program... 92 | 93 | var counter = jBlocks.get(document.querySelector('.js-counter')); 94 | 95 | // use event to react on what happens during lifecycle 96 | counter.on('changed', function() { 97 | console.log('hello, world!'); 98 | }); 99 | 100 | // ... when user clicks on inc button 101 | // log => 'hello, world!' 102 | 103 | // log => 3, cause the counter has been increased 104 | counter.getCurrentValue(); 105 | 106 | // ... then I decided to decrease it using API 107 | counter.dec(); 108 | 109 | // log => 2 110 | counter.getCurrentValue(); 111 | 112 | // If I remove nodes from DOM instance should be destroyed 113 | counter.destroy(); 114 | ``` 115 | 116 | ## Usage 117 | 118 | ### CDN 119 | 120 | Include the library: 121 | 122 | ```html 123 | 124 | ``` 125 | 126 | :warning: You may use any available version instead of `latest` in the URL above. 127 | 128 | `jBlocks` namespace is now in global scope. 129 | 130 | ### Commonjs 131 | 132 | First of all, get the package using npm: 133 | 134 | ``` 135 | npm install jblocks 136 | ``` 137 | 138 | After the package ends up in you `node_modules`: 139 | 140 | ```js 141 | var jblocks = require('jblocks'); 142 | ``` 143 | 144 | You can use the **[full API Doc generated from source](http://vitkarpov.com/jblocks)**. 145 | 146 | Also, feel free to drop me a line — [viktor.s.karpov@gmail.com](mailto:viktor.s.karpov@gmail.com) 147 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jBlocks demo 8 | 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 73 | 74 | -------------------------------------------------------------------------------- /index.browser.js: -------------------------------------------------------------------------------- 1 | global.jBlocks = require('./lib/index.js'); 2 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true 4 | }, 5 | "source": { 6 | "includePattern": ".+\\.js(doc|x)?$", 7 | "excludePattern": "(^|\\/|\\\\)_" 8 | }, 9 | "plugins": [], 10 | "templates": { 11 | "cleverLinks": true, 12 | "monospaceLinks": false, 13 | "default": { 14 | "outputSourceFiles": true 15 | }, 16 | "systemName" : "DocStrap", 17 | "footer" : "", 18 | "copyright" : "Do you need a JavaScript hacker? Drop me a line", 19 | "navType" : "inline", 20 | "theme" : "cerulean", 21 | "linenums" : true, 22 | "collapseSymbols" : false, 23 | "inverseNav" : true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var instances = {}; 2 | var declarations = {}; 3 | var gid = 0; 4 | var noop = function() {}; 5 | 6 | /** 7 | * @see https://github.com/oleggromov/true-pubsub 8 | * @private 9 | * @type {Function} 10 | */ 11 | var PubSub = require('true-pubsub'); 12 | 13 | /** 14 | * Returns constuctor for component with the given name. 15 | * Implement the right chain of prototypes: 16 | * instance of the component -> methods from decl -> base component methods 17 | * @private 18 | * @param {String} name 19 | * @return {Function} 20 | */ 21 | var getComponentConstructor = function(name) { 22 | var F = function() { 23 | Component.apply(this, arguments); 24 | }; 25 | var decl = declarations[name] || {}; 26 | var methods = decl.methods || {}; 27 | 28 | methods.oninit = methods.oninit || noop; 29 | methods.ondestroy = methods.ondestroy || noop; 30 | 31 | F.prototype = Object.create(Component.prototype); 32 | F.prototype.constuctor = Component; 33 | 34 | for (var method in methods) { 35 | if (methods.hasOwnProperty(method)) { 36 | F.prototype[method] = methods[method]; 37 | } 38 | } 39 | return F; 40 | }; 41 | 42 | /** 43 | * @namespace 44 | * @name jBlocks 45 | * @description 46 | * Methods to define components 47 | * using declaration and create new instances. 48 | * Also helps to find and destroy components. 49 | */ 50 | var jBlocks = {}; 51 | 52 | /** 53 | * Destroy instance binded to the node 54 | * @memberof jBlocks 55 | * @param {HTMLElement} node 56 | * @return {jBlocks} 57 | */ 58 | jBlocks.destroy = function(node) { 59 | this.get(node).destroy(); 60 | return this; 61 | }; 62 | 63 | /** 64 | * Define a new component 65 | * @memberof jBlocks 66 | * @param {String} name 67 | * @param {Object} declaration 68 | * @return {jBlocks} 69 | */ 70 | jBlocks.define = function(name, declaration) { 71 | if (declarations[name]) { 72 | throw new Error(name + ' has already been declared'); 73 | } 74 | declarations[name] = declaration || {}; 75 | return this; 76 | }, 77 | 78 | /** 79 | * Remove declaration from cache 80 | * @memberof jBlocks 81 | * @param {String} name name of component 82 | * @return {jBlocks} 83 | */ 84 | jBlocks.forget = function(name) { 85 | declarations[name] = null; 86 | return this; 87 | }, 88 | 89 | /** 90 | * Create and return a new instance of component 91 | * @memberof jBlocks 92 | * @param {HTMLElement} node 93 | * @return {jBlocks.Component} a new instance 94 | */ 95 | jBlocks.get = function(node) { 96 | if (!node) { 97 | throw new Error('invalid node'); 98 | } 99 | var name = node.getAttribute('data-component'); 100 | if (!name) { 101 | throw new Error('data-component attribute is missing') 102 | } 103 | var instanceId = node.getAttribute('data-instance'); 104 | if (!instanceId) { 105 | try { 106 | var props = JSON.parse(node.getAttribute('data-props')); 107 | } catch (e) { 108 | throw e; 109 | } 110 | var Component = getComponentConstructor(name); 111 | var instance = new Component(node, name, props); 112 | var instanceId = instance.__id; 113 | 114 | if (!instances[name]) { 115 | instances[name] = {}; 116 | } 117 | instances[name][instanceId] = instance; 118 | } 119 | return instances[name][instanceId]; 120 | }; 121 | 122 | /** 123 | * @class 124 | * @memberof jBlocks 125 | * @param {HTMLElement} node 126 | * @param {String} name 127 | * @param {Object} props 128 | */ 129 | var Component = function(node, name, props) { 130 | /** 131 | * Name of the components used in decl 132 | * @type {String} 133 | */ 134 | this.name = name; 135 | /** 136 | * Node which component is binded with 137 | * @type {HTMLElement} 138 | */ 139 | this.node = node; 140 | /** 141 | * Props of the component gained from data-props 142 | * @type {Object} 143 | */ 144 | this.props = props || {}; 145 | 146 | this.__decl = declarations[this.name] || {}; 147 | this.__id = ++gid; 148 | 149 | this.node.setAttribute('data-instance', this.__id); 150 | 151 | this.__handlerDomEvents = this.__handlerDomEvents.bind(this); 152 | this.__bindDomEvents(); 153 | this.__pubsub = new PubSub(); 154 | 155 | this.oninit(); 156 | }; 157 | jBlocks.Component = Component; 158 | 159 | Component.prototype = 160 | /** 161 | * @lends jBlocks.Component 162 | */ 163 | { 164 | /** 165 | * Attach an event handler function for the given event 166 | * @param {String} event 167 | * @param {Function} callback 168 | * @return {jBlocks.Component} 169 | */ 170 | on: function(event, callback) { 171 | this.__pubsub.on(event, callback); 172 | return this; 173 | }, 174 | 175 | /** 176 | * Remove an event handler function for the given event 177 | * @param {String} event 178 | * @param {Function} callback 179 | * @return {jBlocks.Component} 180 | */ 181 | off: function(event, callback) { 182 | this.__pubsub.off(event, callback); 183 | return this; 184 | }, 185 | 186 | /** 187 | * Execute all handlers attached for the given event 188 | * @param {String} event 189 | * @param {*} data 190 | * @return {jBlocks.Component} 191 | */ 192 | emit: function(event, data) { 193 | this.__pubsub.emit(event, data); 194 | return this; 195 | }, 196 | 197 | /** 198 | * Attach an event handler function for the given event 199 | * which will be called only once 200 | * @param {String} event 201 | * @param {Function} callback 202 | * @return {jBlocks.Component} 203 | */ 204 | once: function(event, callback) { 205 | this.__pubsub.once(event, callback); 206 | return this; 207 | }, 208 | 209 | /** 210 | * Destroy the instance 211 | * @return {jBlocks.Component} 212 | */ 213 | destroy: function() { 214 | instances[this.name][this.__id] = null; 215 | this.node.removeAttribute('data-instance'); 216 | this.__unbindDomEvents(); 217 | this.__events = null; 218 | this.ondestroy(); 219 | return null; 220 | }, 221 | 222 | /** 223 | * Bind DOM Events from decl 224 | * @private 225 | * @return {jBlocks.Component} 226 | */ 227 | __bindDomEvents: function() { 228 | return this.__forEachEvent(function(event) { 229 | this.node.addEventListener(event, this.__handlerDomEvents); 230 | }); 231 | }, 232 | /** 233 | * Unbind DOM Events from decl 234 | * @private 235 | * @return {jBlocks.Component} 236 | */ 237 | __unbindDomEvents: function() { 238 | return this.__forEachEvent(function(event) { 239 | this.node.removeEventListener(event, this.__handlerDomEvents); 240 | }); 241 | }, 242 | 243 | /** 244 | * Iterate for each event from decl 245 | * @private 246 | * @param {Function} callback 247 | * @return {jBlocks.Component} 248 | */ 249 | __forEachEvent: function(callback) { 250 | var events = this.__decl.events || {}; 251 | 252 | for (var name in events) { 253 | if (events.hasOwnProperty(name)) { 254 | var parts = name.split(' ', 2); 255 | var event = parts[0]; 256 | var selector = parts[1]; 257 | var callbackName = events[name]; 258 | 259 | callback.call(this, event, selector, callbackName); 260 | } 261 | } 262 | return this; 263 | }, 264 | 265 | /** 266 | * Handler for each distinct event from decl 267 | * @private 268 | * @param {Event} e 269 | * @return {jBlocks.Component} 270 | */ 271 | __handlerDomEvents: function(e) { 272 | this.__forEachEvent(function(_, selector, callbackName) { 273 | if (selector) { 274 | var node = this.node.querySelector(selector); 275 | if (this.__contains(node, e.target)) { 276 | this.__tryCall(callbackName, e); 277 | } 278 | } else { 279 | this.__tryCall(callbackName, e); 280 | } 281 | }); 282 | }, 283 | 284 | /** 285 | * Safely try to call method of component 286 | * @private 287 | * @param {String} method 288 | * @return {*} 289 | */ 290 | __tryCall: function(method) { 291 | var args = [].slice.call(arguments, 1); 292 | 293 | try { 294 | return this[method].apply(this, args); 295 | } catch (e) { 296 | throw new Error(e.message + '. Check out ' + method); 297 | } 298 | }, 299 | 300 | /** 301 | * Check is one element down from another in DOM 302 | * @private 303 | * @param {HTMLElement} root 304 | * @param {HTMLElement} child 305 | * @return {Boolean} 306 | */ 307 | __contains: function(root, child) { 308 | return root === child || root.contains(child); 309 | } 310 | }; 311 | 312 | module.exports = jBlocks; 313 | -------------------------------------------------------------------------------- /lib/index.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | var jBlocks = require('../'); 6 | 7 | var html = [ 8 | '
', 9 | '
', 10 | '
', 11 | '
', 12 | '
' 13 | ].join(); 14 | 15 | jBlocks.define('foo', { 16 | methods: { 17 | oninit: function() { 18 | this.inited = true; 19 | }, 20 | ondestroy: function() { 21 | this.inited = false; 22 | } 23 | } 24 | }); 25 | jBlocks.define('bar', { 26 | methods: { 27 | sayhi: function() { 28 | this.emit('hello', 'hello, world!'); 29 | } 30 | } 31 | }); 32 | jBlocks.define('baz', { 33 | events: { 34 | 'click': 'onClickSelf', 35 | 'click .foo': 'onClickFoo', 36 | 'click .bar': 'onClickBar' 37 | }, 38 | methods: { 39 | oninit: function() { 40 | this.clickedOnSelf = false; 41 | this.clickedOnFoo = false; 42 | this.clickedOnBar = false; 43 | }, 44 | onClickSelf: function() { 45 | this.clickedOnSelf = true; 46 | }, 47 | onClickFoo: function() { 48 | this.clickedOnFoo = true; 49 | }, 50 | onClickBar: function() { 51 | this.clickedOnBar = true; 52 | } 53 | } 54 | }); 55 | jBlocks.define('counter', { 56 | methods: { 57 | inc: function() { 58 | this.emit('changed'); 59 | } 60 | } 61 | }); 62 | 63 | describe('jblocks', function() { 64 | let app; 65 | 66 | beforeEach(function() { 67 | app = document.createElement('div'); 68 | app.innerHTML = html; 69 | document.body.appendChild(app); 70 | }); 71 | afterEach(function() { 72 | document.body.innerHTML = ''; 73 | }); 74 | 75 | describe('#get', function() { 76 | it('should create and return an instance', function() { 77 | var instance = jBlocks.get(document.querySelector('.js-bar')); 78 | 79 | expect(instance.name).toBe('bar'); 80 | }); 81 | it('should return an instance of jBlocks.Component', function() { 82 | var instance = jBlocks.get(document.querySelector('.js-bar')); 83 | 84 | expect(instance instanceof jBlocks.Component).toBe(true); 85 | }); 86 | }); 87 | describe('#destroy', function() { 88 | var node, instance, oldId; 89 | 90 | beforeEach(function() { 91 | node = document.querySelector('.js-bar'); 92 | instance = jBlocks.get(node); 93 | oldId = instance.__id; 94 | }); 95 | it('should destroy an instance binded to node', function() { 96 | jBlocks.destroy(node); 97 | var newId = jBlocks.get(node).__id; 98 | 99 | expect(newId).not.toBe(oldId); 100 | }); 101 | it('should destroy an instance called on itself', function() { 102 | instance.destroy(); 103 | var newId = jBlocks.get(node).__id; 104 | 105 | expect(newId).not.toBe(oldId); 106 | }); 107 | }); 108 | describe('#define', function() { 109 | beforeEach(function() { 110 | app.innerHTML += '
'; 111 | jBlocks.define('mega-component'); 112 | }); 113 | afterEach(function() { 114 | jBlocks.forget('mega-component'); 115 | }) 116 | it('should decl a new component', function() { 117 | var instance = jBlocks.get(document.querySelector('.js-mega-component')); 118 | expect(instance.name).toBe('mega-component'); 119 | }); 120 | it('should throw an error if component has been already declared', function() { 121 | expect(() => jBlocks.define('foo')).toThrow('foo has already been declared'); 122 | }); 123 | }); 124 | describe('#forget', function() { 125 | beforeEach(function() { 126 | jBlocks.define('mega-foo', {}); 127 | }); 128 | it('should remove existing declaration', function() { 129 | jBlocks.forget('mega-foo'); 130 | expect(() => jBlocks.define('mega-foo')).not.toThrow(); 131 | }); 132 | }); 133 | describe('#lifecycle', function() { 134 | describe('oninit', function() { 135 | it('should be called when new instance has been created', function() { 136 | var instance = jBlocks.get(document.querySelector('.js-foo-1')); 137 | 138 | expect(instance.inited).toBe(true); 139 | }); 140 | }); 141 | describe('ondestroy', function() { 142 | it('should be called when a new instance has been destroyed', function() { 143 | var instance = jBlocks.get(document.querySelector('.js-foo-1')); 144 | 145 | instance.destroy(); 146 | expect(instance.inited).toBe(false); 147 | }); 148 | }); 149 | }); 150 | describe('#events', function() { 151 | describe('emit', function() { 152 | it('should emit a new event for all subscribers', function() { 153 | var instanceBar = jBlocks.get(document.querySelector('.js-bar')); 154 | var instanceBaz = jBlocks.get(document.querySelector('.js-baz')); 155 | 156 | instanceBar.on('my-custon-event', function(data) { 157 | expect(data.a).toBe(2); 158 | }); 159 | instanceBaz.emit('my-custom-event', { 160 | a: 2 161 | }); 162 | }); 163 | it('should call a handler when component emits an event inside its method (bug #13)', function() { 164 | var counter = jBlocks.get(document.querySelector('.js-counter')); 165 | var called = false; 166 | 167 | counter.on('changed', function() { 168 | called = true; 169 | }); 170 | counter.inc(); 171 | 172 | expect(called).toBe(true); 173 | }); 174 | }); 175 | describe('on', function() { 176 | it('should subsribe component for an event', function() { 177 | var instance = jBlocks.get(document.querySelector('.js-bar')); 178 | 179 | instance.on('hello', function(data) { 180 | expect(data).toBe('hello, world!'); 181 | }); 182 | instance.sayhi(); 183 | }); 184 | }); 185 | describe('events section', function() { 186 | describe('should handle dom events', function() { 187 | it('for root element', function() { 188 | var baz = jBlocks.get(document.querySelector('.js-baz')); 189 | document.querySelector('.js-baz').click(); 190 | 191 | expect(baz.clickedOnSelf).toBe(true); 192 | }); 193 | it('for the specifying element', function() { 194 | var baz = jBlocks.get(document.querySelector('.js-baz')); 195 | document.querySelector('.js-baz .foo').click(); 196 | 197 | expect(baz.clickedOnFoo).toBe(true); 198 | expect(baz.clickedOnBar).toBe(false); 199 | }); 200 | it('for element inside the specifying one', function() { 201 | var baz = jBlocks.get(document.querySelector('.js-baz')); 202 | document.querySelector('.js-baz .bar-inner').click(); 203 | 204 | expect(baz.clickedOnBar).toBe(true); 205 | expect(baz.clickedOnFoo).toBe(false); 206 | }); 207 | it('should pass event as an argument to the handler', function() { 208 | var rootNode = document.querySelector('.js-baz') 209 | var counter = jBlocks.get(rootNode); 210 | var _onClickSelf = counter.onClickSelf; 211 | counter.onClickSelf = function(e) { 212 | expect(e.target).toBe(rootNode); 213 | } 214 | rootNode.click(); 215 | counter.onClickSelf = _onClickSelf; 216 | }); 217 | }); 218 | }); 219 | }); 220 | describe('#props', function() { 221 | it('should be attached to the instance', function() { 222 | var foo1 = jBlocks.get(document.querySelector('.js-foo-1')); 223 | var foo2 = jBlocks.get(document.querySelector('.js-foo-2')); 224 | 225 | expect(foo1.props.step).toBe(1); 226 | expect(foo2.props.step).toBe(2); 227 | }); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jblocks", 3 | "version": "1.2.2", 4 | "description": "A tiny JavaScript library that helps you building UI components", 5 | "author": "Viktor Karpov ", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "jest": "^29.3.1", 9 | "jest-environment-jsdom": "^29.3.1", 10 | "jsdoc-to-markdown": "^8.0.0", 11 | "release": "^6.3.1", 12 | "request": "^2.88.2", 13 | "webpack": "^5.75.0", 14 | "webpack-cli": "^5.0.1" 15 | }, 16 | "main": "lib/index.js", 17 | "files": [ 18 | "jblocks.js" 19 | ], 20 | "directories": { 21 | "test": "tests" 22 | }, 23 | "scripts": { 24 | "build": "npx webpack --mode=production", 25 | "test": "node_modules/.bin/jest", 26 | "doc": "npx jsdoc2md lib/index.js > API.md" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/vitkarpov/jblocks.git" 31 | }, 32 | "keywords": [ 33 | "UI" 34 | ], 35 | "bugs": { 36 | "url": "https://github.com/vitkarpov/jblocks/issues" 37 | }, 38 | "homepage": "https://github.com/vitkarpov/jblocks", 39 | "dependencies": { 40 | "true-pubsub": "^1.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | jblocks: './index.browser.js', 4 | }, 5 | output: { 6 | path: __dirname, 7 | filename: 'jblocks.js' 8 | } 9 | } --------------------------------------------------------------------------------