├── .circleci └── config.yml ├── .gitignore ├── LICENSE.txt ├── demo ├── index.css └── index.html ├── dist ├── index.js └── index.js.gz ├── lib ├── Component.js └── index.js ├── package.json ├── readme.md └── rollup.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:11.1.0 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: npm test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /package-lock.json 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vincent Agnano for the Polight team 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 | -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | /* Page Layout */ 2 | body { 3 | font-family: OpenSans, Arial; 4 | color: #555; 5 | margin: 0; 6 | --col-width: 500px; 7 | } 8 | h1 { 9 | text-align: center; 10 | background: #f5f5f5; 11 | padding: 1rem; 12 | box-shadow: 0 0 3px rgba(0, 0, 0, .3); 13 | } 14 | h2 { 15 | text-align: center; 16 | } 17 | article { 18 | background-color: #f5f5f5; 19 | padding: 2rem; 20 | margin: 1rem auto; 21 | max-width: var(--col-width); 22 | box-shadow: 1px 1px 3px rgba(0, 0, 0, .3); 23 | } 24 | article p { 25 | margin-bottom: 1rem; 26 | padding-bottom: 1rem; 27 | border-bottom: 1px dotted #ccc; 28 | } 29 | 30 | #intro { 31 | max-width: var(--col-width); 32 | margin: 2rem auto; 33 | } 34 | 35 | /* Used within components */ 36 | button { 37 | background-color: #2a85d2; 38 | border-radius: 5px; 39 | border: none; 40 | color: white; 41 | font-size: 3rem; 42 | text-align: center; 43 | padding: 0 1rem; 44 | cursor: pointer; 45 | } 46 | 47 | button.reset { 48 | font-size: 1rem; 49 | margin-left: 5rem; 50 | } 51 | 52 | span { 53 | display: inline-block; 54 | font-size: 2rem; 55 | padding: 2rem; 56 | width: 50px; 57 | text-align: right; 58 | } 59 | 60 | progress { 61 | display: block; 62 | width: 100%; 63 | } 64 | 65 | .description { 66 | color: #888; 67 | font-style: italic; 68 | } 69 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | Demo of WebComponents built with Brick 3 | 4 | 5 | 6 |

WebComponents made with Brick

7 | 8 |
9 |

10 | Viewing the source of this webpage will show you the whole source code of the components. 11 | Brick does not require any webpack or other compiling library. 12 | You can just use it right away. 13 |

14 | 15 |

16 | A few demo example are available below: each box is an HTML with a description and the component usage below the line. 17 |

18 |
19 | 20 |
21 |
22 |

Minimalist Component Display

23 |

Below is a dummy component that just displays a text. 24 | The text is self-contained in the component and a different text is passed as attribute. 25 | The value can also be changed from the outside. Click on the second component to change the value.

26 | 27 | 28 |
29 | 30 |
31 |

Button

32 |

A button can contain all its actions within the component itself and react.

33 | 34 |
35 | 36 |
37 |

Counter

38 |

Counters demo are the new black. 39 | You can't make a component without a demo with a counter in 2020.

40 | 41 |
42 |
43 | 44 | 47 | 48 | 111 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | function t(t,e,r,n,i,o,s){for(var u=i,l=-1,a=o-i+1;r<=n;){if(s(t[r],e[u])){if(l<0&&(l=r),++u>o)return l}else{if(r+a>n)return-1;l=-1,u=i}r++}return-1}function e(t){for(var e=0;e50)return;for(c=u-1,d=u>0?g[u-1]:[0,0],h=g[u]=[],l=-u;l<=u;l+=2){for(f=l===-u||l!==u&&d[c+l-1]=0;u--){for(;f>0&&a>0&&r(e[o+f-1],t[n+a-1]);)b[w--]=2,f--,a--;if(!u)break;c=u-1,d=u?g[u-1]:[0,0],(l=f-a)===-u||l!==u&&d[c+l-1]=0&&(m[a]=f,_[a]={newi:b,oldi:f,prev:_[a-1]});a=m.length-1;for(;m[a]>s;)a--;var w=_[a],k=Array(y+p-a),S=n,A=s,O=k.length-1;for(;w;){const{newi:t,oldi:e}=w;for(;S>t;)k[O--]=4,S--;for(;A>e;)k[O--]=8,A--;k[O--]=2,S--,A--,w=w.prev}for(;S>=r;)k[O--]=4,S--;for(;A>=o;)k[O--]=8,A--;return{diff:k,deleteMap:h}}(t,e,n,o,s,u)}function i(t,e){for(var r=1,n=t.length-1;r<=n;){var i=Math.ceil((r+n)/2);ev&&p>b)return;if(c<=v&&p>b)return void h(e,r,c,v,_[p],o);if(p<=b&&c>v)return void y(e,_,i,p,b);var w=b-p+1,k=v-c+1,S=-1;if(w=0){h(e,r,c,S-1,_[p],o);var A=S+w;for(c=S;ck&&(S=t(i,r,p,b,c,v,m))>=0){for(y(e,_,i,p,S-1),A=S+k,p=S;p0)return i}function p(t,e,r,n){if(!0===r)t.setAttribute(e,"");else if(!1===r)t.removeAttribute(e);else{var i=n?u[e]:void 0;void 0!==i?t.setAttributeNS(i,e,r):t.setAttribute(e,r)}}function y(t,e,r,n=0,i=r.length-1){var o;for(e.length===i-n+1&&(t.textContent="",o=!0);n<=i;){var s=r[n],u=e[n];o||t.removeChild(u),n++,d(s,u)}}function g(t,e,r,n,i,o){var s=n[i],u=f(t,e,s,o);return u!=s&&(n[i]=u,r.replaceChild(u,s),d(e,s,o)),u}function m(t,e){return(null!=t&&null!=t.key?t.key:null)===(null!=e&&null!=e.key?e.key:null)}function b(t,e,r,n){if(r!==n)return!0;for(var i in t)if(t[i]!==e[i])return!0;return!1}const _=Symbol("@petit-dom/component");function w(t){var e=t[_];return null==e&&(e=t[_]=function({render:t,shouldUpdate:e=b}){return{mount(e,r,n){var i=t(e);return r.vnode=i,a(i,n)},patch(r,n,i,o,s){if(!e(r,n))return o;var u=t(r),l=i.vnode;return i.vnode=u,f(u,l,o,s)},unmount(t,e,r){t.wasUnmounted=!0,d(t.vnode,e,r)}}}({render:t,shouldUpdate:t.shouldUpdate})),e}const k={},S=t=>null===t,A=t=>"string"==typeof t||"number"==typeof t,O=t=>null!=t&&128===t.vtype,N=t=>null!=t&&256===t.vtype;function C(t,r=k,...n){const i=null!=r?r.key:null;if("string"==typeof t)return{vtype:128,type:t,key:i,props:r,children:e(n)};if(null!=(o=t)&&null!=o.mount&&null!=o.patch&&null!=o.unmount)return{vtype:256,type:t,key:i,props:Object.assign({},r,{children:n}),_state:null};if("function"==typeof t)return{vtype:256,type:w(t),key:i,props:Object.assign({},r,{children:n}),_state:null};var o;throw new Error("h: Invalid type!")}function j(t,e){var r,n=e.$$petitDomState$$;null==n?(r=a(t),e.appendChild(r)):(r=f(t,n.vnode,n.domNode))!==n.domNode&&(e.replaceChild(r,n.domNode),d(n.vnode,n.domNode)),e.$$petitDomState$$={vnode:t,domNode:r}}class M extends HTMLElement{constructor(){super(),this.useShadowDOM=!0,this.__state={},this.init&&this.init(),this.watchProps=Object.keys(this.__state),this.__attributesToState(),this.document=this.useShadowDOM?this.attachShadow({mode:"open"}):this}__attributesToState(){Object.assign(this.state,Array.from(this.attributes).reduce((t,e)=>Object.assign(t,{[e.name]:e.value}),{}))}get vdom(){return({state:t})=>""}get vstyle(){return({state:t})=>""}setAttribute(t,e){super.setAttribute(t,e),this.watchProps.includes(t)&&(this.state[t]=e)}removeAttribute(t){super.removeAttribute(t),this.watchProps.includes(t)&&t in this.state&&delete this.state[t]}connectedCallback(){this.connected&&this.connected(),this.render()}disconnectedCallback(){this.disconnected&&this.disconnected()}setState(t={}){return Object.assign(this.__state,t)}set state(t){Object.assign(this.__state,t)}get state(){return this.__state}render(t){return this.setState(t),j(C("root",{},[this.vdom({state:this.__state}),this.vstyle({state:this.__state})]),this.document)}}export{M as Component,C as h,j as render}; 2 | -------------------------------------------------------------------------------- /dist/index.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polight/brick/1f50ce58913ecd701c8c2a2cc7ba5c512b967b98/dist/index.js.gz -------------------------------------------------------------------------------- /lib/Component.js: -------------------------------------------------------------------------------- 1 | import { h, render } from './index.js' 2 | 3 | 4 | export default class extends HTMLElement { 5 | constructor() { 6 | super() 7 | this.useShadowDOM = true 8 | this.__state = {} 9 | if(this.init) this.init() 10 | this.watchProps = Object.keys(this.__state) 11 | this.__attributesToState() 12 | this.document = this.useShadowDOM ? this.attachShadow({mode: 'open'}) : this 13 | } 14 | 15 | __attributesToState() { 16 | Object.assign(this.state, Array.from(this.attributes).reduce((obj, attr) => Object.assign(obj, {[attr.name]: attr.value}), {})) 17 | } 18 | 19 | get vdom() { return ({ state }) => '' } 20 | 21 | get vstyle() { return ({ state }) => '' } 22 | 23 | setAttribute(name, value) { 24 | super.setAttribute(name, value) 25 | if(this.watchProps.includes(name)) this.state[name] = value 26 | } 27 | 28 | removeAttribute(name) { 29 | super.removeAttribute(name) 30 | if(this.watchProps.includes(name) && name in this.state) delete this.state[name] 31 | } 32 | 33 | connectedCallback() { 34 | if(this.connected) this.connected() 35 | this.render() 36 | } 37 | 38 | disconnectedCallback() { 39 | if(this.disconnected) this.disconnected() 40 | } 41 | 42 | setState(props = {}) { 43 | return Object.assign(this.__state, props) 44 | } 45 | 46 | set state(value) { 47 | Object.assign(this.__state, value) 48 | } 49 | 50 | get state() { 51 | return this.__state 52 | } 53 | 54 | render(state) { 55 | this.setState(state) 56 | return render(h('root', {}, [ 57 | this.vdom({ state: this.__state }), 58 | this.vstyle({ state: this.__state }), 59 | ]), this.document) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import { h, render } from 'petit-dom' 2 | import Component from './Component.js' 3 | 4 | export { h, render, Component } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@polight/brick", 3 | "version": "1.2.0", 4 | "description": "The minimalistic and fast Web-Component library", 5 | "main": "dist/index.js", 6 | "devDependencies": { 7 | "petit-dom": "^0.3.4", 8 | "rollup": "^2.12.0", 9 | "@rollup/plugin-node-resolve": "^8.0.0", 10 | "rollup-plugin-gzip": "^2.5.0", 11 | "rollup-plugin-terser": "^6.1.0" 12 | }, 13 | "scripts": { 14 | "build": "rollup -c" 15 | }, 16 | "keywords": [ 17 | "webcomponents", 18 | "vue", 19 | "riot", 20 | "react", 21 | "vdom", 22 | "hyperdom", 23 | "virtualdom" 24 | ], 25 | "author": "Vincent Agnano", 26 | "license": "MIT", 27 | "directories": { 28 | "lib": "lib" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/polight/brick.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/polight/brick/issues" 36 | }, 37 | "homepage": "https://github.com/polight/brick#readme" 38 | } 39 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Brick 2 | 3 | > The minimalist reactive web-component 4 | 5 | Brick is a thin layer to constructs [native webcomponents](https://developer.mozilla.org/en-US/docs/Web/Web_Components) and make them [reactive](https://en.wikipedia.org/wiki/Reactive_programming). 6 | 7 | Brick is: 8 | - 👙 minimalist: 74 lines of readable code (non-optimised, uncompressed, no cheating) 9 | - 🌱 low dependency: its single third-party is the minimalist [Petit-Dom](https://github.com/yelouafi/petit-dom) which itself has no dependency 10 | - ♻️ reactive: updating the state recalculate the Virtual Dom when needed 11 | - 🚀 fast: using virtual dom through a thin layer makes it close to bare-metal 12 | - 💧 simple: that's [Vanilla](http://vanilla-js.com/), there isn't much to know, it's a raw class to extend; no magic ✨ 13 | 14 | View the [demo](https://polight.github.io/brick/demo/) and [their source](https://github.com/Polight/lego/tree/master/demo) 🧪. 15 | 16 | If you're looking for a higher level Web-Components library, checkout [Lego](https://github.com/polight/lego) that builds Bricks out of HTML web-component files. 17 | 18 | 19 | ### Getting started 20 | 21 | Here's a fully working example with no install. 22 | 23 | Copy-paste the following in an HMTL file and run it in a browser: 24 | 25 | ```html 26 | 27 | 28 | 46 | ``` 47 | 48 | There isn't much to explain, but let's detail a little: 49 | 50 | - `` is a web-component. That's native. `name="earth"` will be sent to the component as `{ state: 'earth' }` 51 | - `init() { this.state = { name: 'world' } }` declares the `state` with it's default values. Also, anything that is declared in the state—and only what is declared here—will be reactive 52 | - `toggleName` is a custom method that will be called on click 53 | - `get vdom()` is the property that should return a function reprenting your HTML. That function is itself called passing the `state` argument. It should returns a virtual-dom. If you know [virtual-dom](https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060), [React](https://reactjs.org/) or [elm](https://elm-lang.org/), this writing will be familiar 54 | - `customElements.define('hello-world', HelloWorld)` that's the native way in HTML to declare the web-component. 55 | 56 | ### Documentation 57 | 58 | The best documentation is [reading the code](./lib/Component.js). 59 | 60 | Because it constructs native webcomponents, all the native [web-components documentation](https://developer.mozilla.org/en-US/docs/Web/Web_Components) applies. 61 | You can therefore use [slots](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/slot), disable or enable [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM), the [`is` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#attr-is) and other web-component or HTML capabilities. 62 | 63 | In addition, a couple of extra tools are brought by Brick: 64 | 65 | #### state 66 | 67 | this object is made available to the virtual-dom (and CSS styles). 68 | It can be reactive (when declared in the init) and can be updated by the app. 69 | 70 | State is fed the following way: 71 | 1. declared in the `init()` with the default values 72 | 2. attributes of the element in the HTML will overwrite the defaults 73 | 3. changing values in the object or on the component attributes will update the state values (without re-rendering) 74 | 4. call `this.render(state)` to update the state **and** update the interface. 75 | 76 | #### init() 77 | 78 | You can initialize whatever you need here. 79 | That's a convenient instance to declare your reactive `state` object. 80 | When declaring your `state` here, it's properties will be made reactive. 81 | 82 | #### vdom() 83 | 84 | Must return a function that returns a string, an array or a `h()` instance. 85 | The structure of `h()` takes 3 arguments: 86 | `h(, , )`. 87 | 88 | #### vstyle() 89 | 90 | Same as `vdom()`, but returning a CSS style node. 91 | 92 | ```js 93 | get vstyle() { 94 | return ({ state }) => h('style', {}, 'p{ color: red }') 95 | } 96 | ``` 97 | 98 | #### connected() 99 | 100 | A convenient method to call when the component is attached to the dom. 101 | 102 | #### disconnected() 103 | 104 | A convenient method to call when the component is removed from the dom. 105 | 106 | 107 | ## Development 108 | 109 | ### Working on sources 110 | 111 | When participating to Brick you can simply tune the _lib/index.js_ and import it. 112 | _petit-dom_ is the only dependency. You may need to adapt its path. 113 | 114 | ### Compilation 115 | 116 | Compiling is convenient but not _future-proof_. 117 | Compilation is done with [Rollup](http://rollupjs.org/) and will break in the future. 118 | Therefore it is not a high dependency and should never be. 119 | 120 | That said, plugins for bundling, minifying and gziping are set up. 121 | 122 | You may install them all with `npm i --env=dev`. 123 | 124 | #### Compiling task 125 | 126 | `npm run build` to bundle/minimify/gzip _dist/index.js_. 127 | That will be bandwidth-friendlier. 128 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import { terser } from 'rollup-plugin-terser' 3 | import gzipPlugin from 'rollup-plugin-gzip' 4 | 5 | 6 | export default { 7 | input: 'lib/index.js', 8 | output: { 9 | file: 'dist/index.js', 10 | format: 'es' 11 | }, 12 | plugins: [resolve(), terser(), gzipPlugin()] 13 | } 14 | --------------------------------------------------------------------------------