├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── counter.html ├── logo.png ├── package.json ├── q.js ├── q.min.js ├── test.js └── todo.html /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 2018 13 | }, 14 | "rules": { 15 | } 16 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Serge Zaitsev 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Q! 2 | 3 | [![Build Status](https://travis-ci.org/zserge/q.svg?branch=master)](https://travis-ci.org/zserge/q) 4 | [![npm](https://img.shields.io/npm/v/@zserge/q.svg)](http://npm.im/@zserge/q) 5 | [![gzip size](http://img.badgesize.io/https://unpkg.com/@zserge/q/q.min.js?compression=gzip)](https://unpkg.com/@zserge/q/q.min.js) 6 | 7 |
8 | Q! 9 |
10 |

11 | 12 | A really small (well under 1KB minified!) library to explain how VueJS-like frameworks work. Never meant to be used in any of the serious projects. But, hey, despite the tiny code size it supports some smart reactivity and comes with a few of the most common directives, isn't it cool? It was really an exercise in minimalism, and nothing more. 13 | 14 | If you liked it - there is a similar [toy ReactJS library](https://github.com/zserge/o). 15 | 16 | ## Example to whet your appetite 17 | 18 | ```html 19 |

20 | 21 | 22 |

23 |
24 | 27 | ``` 28 | 29 | Try ["counter" example](https://raw.githack.com/zserge/q/master/counter.html) or try ["todo" example](https://raw.githack.com/zserge/q/master/todo.html). 30 | 31 | ## API Reference 32 | 33 | Hey, it might be quicker to read the sources than this text. Anyway, the following directives are supported: 34 | 35 | * `q-text` - updates element innerText. 36 | * `q-html` - updates element innerHTML (use with care). 37 | * `q-if` - toggles "hidden" property if the expression is true. 38 | * `q-on:` - adds an event listener to the element. 39 | * `q-bind:` - binds element attribute to the expression value. 40 | * `q-model` - binds element (normally, ``) value to the variable. 41 | * `q-each` - renders child elements for each item of the array. Child elements have separate scope, with two special variables - `$it` which is an array element and `$parent` which is a parent data scope. 42 | 43 | To initialize the Q app pass the root element and the data model: `Q(el, {name: 'John', age: 42})`. 44 | 45 | ## What a weird name for a project 46 | 47 | The library is called "Q!". A "cue" means a signal to a performer to begin a speech or action, so it's very much related to the concepts of reactivity/observers/watchers etc. Also, "Q" rhymes with "Vue". Moreover, "Q" resembles zero, which is a metaphor for both, library footprint and usefulness. Finally, there is [O!](https://github.com/zserge/o) library which is a similar experiment for React and "Q!" seems like a good companion name. 48 | 49 | ## License 50 | 51 | Code is distributed under MIT license, feel free to use it in your proprietary 52 | projects as well, but I don't advise to do so - better use a proper framework instead. 53 | 54 | However, pull requests, issue reports and bug fixes are welcome! 55 | -------------------------------------------------------------------------------- /counter.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |
8 | 9 | 10 |

11 |
    12 |
  • 13 |
14 |
15 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zserge/q/6206173e2753168c258ec6044caafdd4b0d0e9f0/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zserge/q", 3 | "version": "0.0.2", 4 | "description": "The worst possible VueJS clone", 5 | "scripts": { 6 | "test": "eslint q.js && cat q.js test.js | node", 7 | "minify": "terser --compress=drop_console,ecma=6,passes=2 --mangle -- q.js > q.min.js && sed -i 's/const/let/g' q.min.js && ls -l q.min.js && gzip -cf9 q.min.js > q.min.js.gz && ls -l q.min.js.gz && rm -f q.min.js.gz" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/zserge/q.git" 12 | }, 13 | "author": "Serge Zaitsev ", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "eslint": "^7.12.1", 17 | "jsdom": "^16.4.0", 18 | "terser": "^5.3.8" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /q.js: -------------------------------------------------------------------------------- 1 | const call = (expr, ctx) => 2 | new Function(`with(this){${`return ${expr}`}}`).bind(ctx)(); 3 | 4 | const directives = { 5 | html: (el, _, val, ctx) => (el.innerHTML = call(val, ctx)), 6 | text: (el, _, val, ctx) => (el.innerText = call(val, ctx)), 7 | if: (el, _, val, ctx) => (el.hidden = !call(val, ctx)), 8 | on: (el, name, val, ctx) => (el[`on${name}`] = () => call(val, ctx)), 9 | model: (el, name, val, ctx) => { 10 | el.value = ctx[val]; 11 | el.oninput = () => (ctx[val] = el.value); 12 | }, 13 | bind: (el, name, value, ctx) => { 14 | const v = call(value, ctx); 15 | if (name === 'style') { 16 | el.removeAttribute('style'); 17 | for (const k in v) { 18 | el.style[k] = v[k]; 19 | } 20 | } else if (name === 'class') { 21 | el.setAttribute('class', [].concat(v).join(' ')); 22 | } else { 23 | v ? el.setAttribute(name, v) : el.removeAttribute(name); 24 | } 25 | }, 26 | each: (el, name, val, ctx) => { 27 | const items = call(val, ctx); 28 | if (!el.$each) { 29 | el.$each = el.children[0]; 30 | } 31 | el.innerText = ''; 32 | for (let it of items) { 33 | const childNode = document.importNode(el.$each); 34 | const childCtx = {$parent: ctx, $it: it}; 35 | childNode.$q = childCtx; 36 | Q(childNode, childCtx); 37 | el.appendChild(childNode); 38 | } 39 | }, 40 | }; 41 | 42 | let $dep; 43 | 44 | const walk = (node, q) => { 45 | for (const {name, value} of node.attributes) { 46 | if (name.startsWith('q-')) { 47 | const [directive, event] = name.substring(2).split(':'); 48 | const d = directives[directive]; 49 | $dep = () => d(node, event, value, q); 50 | $dep(); 51 | $dep = undefined; 52 | } 53 | } 54 | for (const child of node.children) { 55 | if (!child.$q) { 56 | walk(child, q); 57 | } 58 | } 59 | }; 60 | 61 | const proxy = q => { 62 | const deps = {}; 63 | for (const name in q) { 64 | deps[name] = []; 65 | let prop = q[name]; 66 | Object.defineProperty(q, name, { 67 | get() { 68 | if ($dep) { 69 | deps[name].push($dep); 70 | } 71 | return prop; 72 | }, 73 | set(value) { 74 | prop = value; 75 | if (!name.startsWith('$')) { 76 | for (const dep of deps[name]) { 77 | dep(value); 78 | } 79 | } 80 | }, 81 | }); 82 | } 83 | return q; 84 | }; 85 | 86 | const Q = (el, q) => walk(el, proxy(q)); 87 | -------------------------------------------------------------------------------- /q.min.js: -------------------------------------------------------------------------------- 1 | let call=(t,e)=>new Function(`with(this){${"return "+t}}`).bind(e)(),directives={html:(t,e,n,o)=>t.innerHTML=call(n,o),text:(t,e,n,o)=>t.innerText=call(n,o),if:(t,e,n,o)=>t.hidden=!call(n,o),on:(t,e,n,o)=>t["on"+e]=()=>call(n,o),model:(t,e,n,o)=>{t.value=o[n],t.oninput=()=>o[n]=t.value},bind:(t,e,n,o)=>{let i=call(n,o);if("style"===e){t.removeAttribute("style");for(let e in i)t.style[e]=i[e]}else"class"===e?t.setAttribute("class",[].concat(i).join(" ")):i?t.setAttribute(e,i):t.removeAttribute(e)},each:(t,e,n,o)=>{let i=call(n,o);t.$each||(t.$each=t.children[0]),t.innerText="";for(let e of i){let n=document.importNode(t.$each),i={$parent:o,$it:e};n.$q=i,Q(n,i),t.appendChild(n)}}};let $dep;let walk=(t,e)=>{for(let{name:n,value:o}of t.attributes)if(n.startsWith("q-")){let[i,l]=n.substring(2).split(":"),s=directives[i];$dep=()=>s(t,l,o,e),$dep(),$dep=void 0}for(let n of t.children)n.$q||walk(n,e)},proxy=t=>{let e={};for(let n in t){e[n]=[];let o=t[n];Object.defineProperty(t,n,{get:()=>($dep&&e[n].push($dep),o),set(t){if(o=t,!n.startsWith("$"))for(let o of e[n])o(t)}})}return t},Q=(t,e)=>walk(t,proxy(e)); 2 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const jsdom = require('jsdom'); 3 | const fs = require('fs'); 4 | 5 | // Global map of tests 6 | const $ = {}; 7 | 8 | $['call'] = () => { 9 | assert.equal(call('2+3', null), 5); 10 | assert.equal(call('a', {a:42}), 42); 11 | assert.equal(call('a+b', {a:1, b: 2}), 3); 12 | assert.equal(call('Math.pow(2, a)', {a: 3}), 8); 13 | global.FOO = 'hello' 14 | assert.equal(call('FOO', {}), 'hello'); 15 | }; 16 | 17 | $['proxy:simple'] = () => { 18 | const a = proxy({x: 1, y: 2}); 19 | let called = 0; 20 | let testCallback = () => called++; 21 | $dep = testCallback; 22 | a.x; // subscribe to "x" 23 | assert.equal(called, 0); 24 | a.x = 1; 25 | assert.equal(called, 1); 26 | a.x = 'foo'; 27 | assert.equal(called, 2); 28 | a.y = 'first'; 29 | assert.equal(called, 2); 30 | a.y; // subscribe to "y" 31 | a.y = 'second'; 32 | assert.equal(called, 3); 33 | }; 34 | 35 | $['Q:simple'] = () => { 36 | const model = {name: 'John'}; 37 | const view = ` 38 |
39 |

40 |
41 | `; 42 | const el = new jsdom.JSDOM(view).window.document.querySelector('main'); 43 | Q(el, model); 44 | assert.equal(el.querySelector('p').innerText, 'John'); 45 | model.name = 'Jane'; 46 | assert.equal(el.querySelector('p').innerText, 'Jane'); 47 | }; 48 | 49 | // --------------------------------------------------------------------------- 50 | // --------------------------------------------------------------------------- 51 | // --------------------------------------------------------------------------- 52 | 53 | // Try running "+" tests only, otherwise run all tests, skipping "-" tests. 54 | if ( 55 | Object.keys($) 56 | .filter(t => t.startsWith('+')) 57 | .map(t => $[t]()).length == 0 58 | ) { 59 | for (let t in $) { 60 | if (t.startsWith('-')) { 61 | console.log('SKIP:', t); 62 | } else { 63 | console.log('TEST:', t); 64 | $[t](); 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /todo.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |
8 | 9 | 10 | 11 | 12 |
    13 |
  • 18 |
19 |
20 | 21 | 22 | 36 | 37 | --------------------------------------------------------------------------------