0.0.1 • Published 5 years ago

so-so-easy v0.0.1

Weekly downloads
2
License
MIT
Repository
github
Last release
5 years ago

SoSoEasy.js

An utility script for web development.

WARNING: This library is currently unstable, there may be bugs in it. Further test is required. Do NOT use it in production!

Introduction

Yet Another JS Library, Why?

Nowadays, Web Development is domainated by Webpack and Babel, which becomes overkill when developing small projects like Demo Programs and Online Utilities. So I created the small library. It has some utility functions and some DOM operation functions. It is possible to use only one of those two parts.

Overview

  • Assertion   JavaScript has a lot of bad design, including opinionated implicit type conversion. This library provides functions for expression assertion and type assertion.
/* Expression Assertion */
__.assert(1+1 != 2)  // Error: Asserstion Failed
__.assert(1+1 == 2)  // true

/* Type Assertion */
__('1234').require('Array')  // Error: Asserstion Failed
__([1,2,3,4]).require('Array') // true
__(123).require('String')  // Error: Asserstion Failed
__('123').require('String') // true

/* Type Check */
__([]).is('Array')  // true
__('123').is('Number')  // false
__(123).is('Number')  // true
  • Iterator Operation   ES6 added map(), filter() and reduce() on Array.prototype, it is good, but for iterators (returned by generators), those methods are not available. This library provides generator version of those methods.
__.range(0,10)
// Handle { operand: Generator }

__.range(0,10).collect()
// Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

__.range(0,10).map(x => 2**x).filter(y => y > 20).collect()
// Array(5) [ 32, 64, 128, 256, 512 ]

__([1,2,3,4,5]).every(n => n > 0)
// true

__([{x:1,y:2},{x:3,y:4}]).find(p => p.x == 3)
// Object { x: 3, y: 4 }

__([{x:1,y:2},{x:3,y:4}]).find(p => p.x == 5)
// null
  • Hash Table Operation   JavaScript lacks functional-style methods for pure objects (aka hash tables). This library provides methods like map_key(), map_value(), map_entry() and filter_entry() for pure objects.
__({x:1,y:2,z:3,w:4}).map_value(x => x*100)
// Object { x: 100, y: 200, z: 300, w: 400 }

_({x:1,y:2,z:3,w:4}).filter_entry((key, value) => key != 'x' && value != 4)
// Object { y: 2, z: 3 }

__({x:1,y:2,z:3,w:4}).map_entry((key, value) => `${key}=${value}`)
// Handle { operand: Generator }

__({x:1,y:2,z:3,w:4}).map_entry((key, value) => `${key}=${value}`).join(', ')
// "x=1, y=2, z=3, w=4"
  • Shallow Copy and Shallow Equal   This library provides a quick way to do shallow copy and shallow equality test for arrays and pure objects.
let p = {x:1,y:2,z:3,w:4}
let q = __(p).copy()
q.x = 100000
q  // Object { x: 100000, y: 2, z: 3, w: 4 }
p  // Object { x: 1, y: 2, z: 3, w: 4 }
__(p).equals(q)  // false
__(p).equals(p)  // true
  • Reactive Data Binding on Actual DOM   Unlike libraries designed for big apps, which tend to use virtual DOM, this library provides a way to bind data on actual DOM, and update DOM content reactively. Unlike other libraries, this library does not use templates. It provides a simple way to describe DOM structure and data binding by vanilla JS, so it does not contain a template parser, which makes it very lightweight.
let Data = { count: 0 }
let Counter = ['div', {}, [
    ['button', { text: '-', on: { click: ev => { Data.count-- } } }],
    ['span', { text: count => ` count: ${count} ` }],
    ['button', { text: '+', on: { click: ev => { Data.count++ } } }]
]]
document.body.appendChild(__.bind(Data, Counter))

For more examples, this repository contains a sample folder, there is a sample todo list app in it.

Build

$ yarn build    # output: 'lib/so-so-easy.js'

Note that Babel is not used, so the compiled code is still ES6 and does NOT include any polyfill.

But module field in package.json has pointed to the original code file, so it is possible to import this library as a ES6 module in order to make it adopted by the Babel config of your own project.

License

MIT

API Reference

__<T> (x: T) -> Handle<T>

Returns a handle for operand x. (Invoke new Handle(x))

__(12345)
// Handle { operand: 12345 }

Handle :: unwrap()

Returns the wrapped operand of the handle.

__(12345).unwrap()
// 12345

Handle :: is (type_name: String) -> Boolean

Checks if the operand of the handle is of the type indicated by type_name.

__([1,2,3]).is('Array')  // true
__({}).is('HashTable')  // true
__(t=>t+1).is('Function') // true
__([1]).is('Iterable')  // true
__((function*() { yield 1 })()).is('Iterable')  // true
__('1234').is('Number')  // false
__([1,2,3]).is('Object')  // true
__([1,2,3]).is('HashTable')  // false
__(null).is('Object')  // false
Type NameDefinition
Booleantypeof operand == 'boolean'
Stringtypeof operand == 'string'
Numbertypeof operand == 'number'
Symboltypeof operand == 'symbol'
KeyString or Symbol
Objecttypeof operand == 'object' && operand !== null
HashTableObject with [[Prototype]] === Object.prototype
Arrayoperand instanceof Array
IterableObject with [Symbol.iterator] is Function
Functiontypeof operand == 'function'
Nulloperand === null
Emptytypeof operand == 'undefined'
NotEmptytypeof operand != 'undefined'

Handle :: require (type_name: String) -> Boolean

Similar to is(), but throws an error if the type check failed.

__('1').require('Number')
// Error: Assertion Failed
__(1).require('Number')
// true

Handle<HashTable> :: has (key: String) -> Boolean

Invokes Object.prototype.hasOwnProperty to check if key is an own-property of the operand.

__({a:1,b:2}).has('a')  // true
__({a:1,b:2}).has('42')  // false
__([0,1]).has('1')  // Error: Assertion Failed
__(Object.create({})).has('1')  // Error: Assertion Failed

Handle<HashTable> :: merge (hash: HashTable) -> HashTable

Invokes Object.assign(operand, hash) to merge hash into the operand and returns the operand itself.

let h = { a: 1, b: 2 }
__(h).merge({ c: 3 })  // Object { a: 1, b: 2, c: 3 }
h  // Object { a: 1, b: 2, c: 3 }

Handle<HashTable> :: merged (hash: HashTable) -> HashTable

Invokes Object.assign({}, operand, hash) to merge hash and the operand into a new object and returns the new object.

let h1 = { a: 1, b: 2 }
let h2 = __(h1).merged({ c: 3 })
h1  // Object { a: 1, b: 2 }
h2 // Object { a: 1, b: 2, c: 3 }

Handle<HashTable> :: keys() -> Array

Equivalent to Object.keys(operand).

__({ a: 1, b: 2 }).keys()
// Array [ "a", "b" ]

Handle<HashTable> :: values() -> Array

Equivalent to Object.keys(operand).map(key => operand[key])

__({ a: 1, b: 2 }).values()
// Array [ 1, 2 ]

Handle<HashTable> :: entries() -> Array

Equivalent to Object.keys(operand).map(key => ({ key, value: operand[key]}) )

__({ a: 1, b: 2 }).entries()
// Array [ Object { key: "a", value: 1 }, Object { key: "b", value: 2 } ]

Handle<HashTable> :: for_each_entry (f: Function) -> Null

Calls f(key, value) for each entry of the operand.

__({ a: 1, b: 2 }).for_each_entry((key, value) => { console.log(`${key} = ${value}`) })
// a = 1
// b = 2

Handle<HashTable> :: map_entry (f: Function) -> Handle<Iterable>

Creates an iterator that yields f(key, value) for each entry of the operand, and returns a handle of the iterator.

__({ a: 1, b: 2 }).map_entry((key, value) => `${key} = ${value}`)
// Handle { operand: Generator }
__({ a: 1, b: 2 }).map_entry((key, value) => `${key} = ${value}`).collect()
// Array [ "a = 1", "b = 2" ]

Handle<HashTable> :: map_key (f: Function) -> HashTable

Creates a new hash table with keys mapped by f(key, value) for each entry of the operand, and returns the new hash table.

__({ a: 1, b: 2 }).map_key(k => k.toUpperCase())
// Object { A: 1, B: 2 }
__({ a: 1, b: 2 }).map_key((_, v) => v.toString())
// Object { 1: 1, 2: 2 }

Handle<HashTable> :: map_value (f: Function) -> HashTable

Creates a new hash table with values mapped by f(value, key) for each entry of the operand, and returns the new hash table.

__({ a: 1, b: 2 }).map_value(v => v*1000)
// Object { a: 1000, b: 2000 }
__({ a: 1, b: 2 }).map_value((v, k) => v*1000 + k.codePointAt(0))
// Object { a: 1097, b: 2098 }

Handle<HashTable> :: filter_entry (f: Function) -> HashTable

Creates a new hash table with entries filtered by f(key, value) for each entry of the operand, and returns the new hash table.

__({ a: 1, b: 2, c: 3 }).filter_entry((k, v) => k != 'a' && v != 3)
// Object { b: 2 }

Handle<Iterable> :: map (f: Function) -> Handle<Iterable>

Create a new iterator with elements mapped by f(element, index) for each element yielded by the operand, and returns a handle of the new iterator.

__([ 1, 2, 3, 4, 5 ]).map(n => 2**n)
// Handle { operand: Generator }
__([ 1, 2, 3, 4, 5 ]).map(n => 2**n).collect()
// Array(5) [ 2, 4, 8, 16, 32 ]
__([  'a', 'b', 'c' ]).map((_, i) => i).collect()
// Array(3) [ 0, 1, 2 ]
__({ a: 1, b: 2 }).map((_, i) => i).collect()
// Error: Assertion Failed

Handle<Iterable> :: filter (f: Function) -> Handle<Iterable>

Create a new iterator with elements filtered by f(element, index) for each element yielded by the operand, and returns a handle of the new iterator.

__([ 1, 2, 3, 4, 5 ]).filter(n => n > 3).collect()
// Array [ 4, 5 ]
__([ 'a', 'b', 'c', 'd' ]).filter((_, i) => i > 1).collect()
// Array [ "c", "d" ]

Handle<Iterable> :: reduce (initial: any, f: Function) -> any

Let v to be the initial value. For each element yielded by the operand, re-assign v to the value of f(element, v, index), and return the final value of v when all elements have been yielded.

let sum = it => __(it).reduce(0, (e, s) => s + e)
sum([ 1, 2, 3, 4 ])
// 10
sum(__.range(0, 101).unwrap())
// 5050

Handle<Iterable> :: every (f: Function) -> Boolean

Checks if for all elements yielded by the operand, f(element, index) is a truthy value.

__([ 1, 2, 3 ]).every(n => __(n).is('Number'))
// true
__([ 1, '2', 3 ]).every(n => __(n).is('Number'))
// false
__([ 6, 11, 88 ]).every((n, i) => i == 0 || n > 10)
// true

Handle<Iterable> :: some (f: Function) -> Boolean

Checks if there exists an element yielded by the operand that satisfies that f(element, index) is a truthy value.

__([ 1, '2', 3 ]).some(n => __(n).is('String'))
// true
__([ 1, 2, 3 ]).some(n => __(n).is('String'))
// false
__([ 6, 11, 88 ]).some((n, i) => i > 0 && n < 10)
// false

Handle<Iterable> :: find (f: Function) -> any

Finds the first element which satisfies that f(element) is a truthy value, in all elements yielded by the operand, and returns it. If all the elements do not satisfy the condition, null will be returned.

__([{ x: 1, y: 2 }, { x: -1, y: 3 }]).find(p => p.x < 0)
// Object {x: -1, y: 3}
__([{ x: 1, y: 2 }, { x: -1, y: 3 }]).find(p => p.y < 0)
// null

Handle<Iterable> :: for_each_item (f: Function) -> Null

Invokes f(element, index) for each element yielded by the operand.

__.range(10, 15).for_each_item((e, i) => { console.log(`#${i}: ${e}`) })
/*
#1: 11
#2: 12
#3: 13
#4: 14
*/

Handle<Iterable> :: join (separator: String) -> String

Behaves like Array.prototype.join(separator) but operates on the iterable operand, which does not have to be an array.

__.range(0,10).map(n => 2**n).join('-')
// "1-2-4-8-16-32-64-128-256-512"

Handle<Iterable> :: collect () -> Array

Collects all elements yielded by the operand into an array, and returns the array.

__((function*() { yield 1; yield 2 })()).collect()
// Array(2) [ 1, 2 ]

Handle<Array> :: reversed () -> Handle<Iterable>

Creates a reversed iterator of the operand array, and returns a handle of the iterator.

__([ 5, 7, 9 ]).reversed()
// Handle { operand: Generator }
__([ 5, 7, 9 ]).reversed().collect()
// Array(3) [ 9, 7, 5 ]

Handle<Array> :: prepended (element: any) -> Array

Creates a copy of the operand array with element prepended, and returns the new array.

let l = [ 1, 2, 3 ]
let m = __(l).prepended(0)
l  // Array(3) [ 1, 2, 3 ]
m  // Array(4) [ 0, 1, 2, 3 ]

Handle<Array> :: appended (element: any) -> Array

Creates a copy of the operand array with element appended, and returns the new array.

let l = [ 1, 2, 3 ]
let m = __(l).appended(4)
l  // Array(3) [ 1, 2, 3 ]
m  // Array(4) [ 1, 2, 3, 4 ]

Handle<Array> :: removed (index: Number) -> Array

Creates a copy of the operand array with element at index removed, and returns the new array. Note that index should be a valid index of the operand array, otherwise the method will throw an error.

__([ 0, 1, 2, 3, 4 ]).removed(2)
// Array(4) [ 0, 1, 3, 4 ]
__([ 0, 1, 2, 3, 4]).removed(5)
// Error: Assertion Failed

Handle<Array> :: copy () -> Array

Handle<HashTable> :: copy() -> HashTable

Creates a shallow copy of the operand and returns the copy.

let a1 = [ 0, 1 ]
let a2 = __(a1).copy()
a1[0] = 777
a1  // Array(2) [ 777, 1 ]
a2  // Array(2) [ 0, 1 ]
let h1 = { a: 1, b: 2 }
let h2 = __(h1).copy()
h2.b *= 500
h1  // Object { a: 1, b: 2 }
h2  // Object { a: 1, b: 1000 }

Handle<Array> :: equals (another: Array) -> Array

Handle<HashTable> :: equals (another: HashTable) -> HashTable

Checks the shallow equality of the operand and the argument another. (each element or value is checked by === operator)

__([ 1, 2, 3 ]).equals([ 1, 2, 3 ])
// true
__([ '1', 2, 3 ]).equals([ 1, 2, 3 ])
// false
__([ 1, 2, {} ]).equals([ 1, 2, {} ])
// false
__({ a: 1, b: 2 }).equals({ a: 1, b: 2 })
// true
__({ a: 1, b: [2] }).equals({ a: 1, b: [2] })
// false

Handle<HTMLElement> :: $ (selector: String) -> HTMLElement | Null

Equivalent to operand.querySelector(selector).

__(document.head).$('meta')
// <meta charset="UTF-8">

Handle<HTMLElement> :: $$ (selector: String) -> HTMLElement[]

Invokes operand.querySelectorAll(selector), returns an array created from the result NodeList.

__(document.head).$$('script')
// Array(2) [script, script]
__(document.head).$$('vrgwegvergvqe')
// Array []

Handle<Object> :: track (key: String) -> Null

Makes operand[key] a reactive property, that is, when the value of operand[key] changes, the watchers of the key will be notified. If operand[key] is already a reactive property, does nothing. If operand[key] does not exist, throws an error.

let o = { t: 0 }
__(o).track('t')
__(o).watch(t => { console.log(`The value of t changed to ${t}`) })
o.t = 1
// The value of t changed to 1
o.t = 2
// The value of t changed to 2
__(o).track('cwn2390vnwriovn3i')
// Error: Assertion Failed

Handle<Object> :: define (key: String, get: Function) -> Null

Warning: Use of this method causes you CANNOT minify, uglify or obscure your JavaScript code. That is because the names of properties depended by the computed property is determined by the parameter list of the get function, this behaviour relies on the result of Function.prototype.toString(), which will break when the code is minified. Moreover, using default value for parameters, or putting comments between parameters of the get function, is NOT allowed.

Defines key as a computed property on the operand, with parameters of the get function as its dependecies.

let o = { u: 1, v: 2 }
__(o).define('computed', (u, v) => u+v)
o.computed  // 3
o.u = -2
o.computed  // 0
__(o).track('u')
__(o).track('v')
__(o).watch(computed => { console.log(`The computed value changed to ${computed}`) })
o.u = 5
// The computed value changed to 7
o.v = 5
// The computed value changed to 10
o.v = 100
// The computed value changed to 105

Handle<Object> :: watch (callback: Function) -> Watcher

Warning: Use of this method causes you CANNOT minify, uglify or obscure your JavaScript code. That is because the names of properties depended by the watcher is determined by the parameter list of the callback function, this behaviour relies on the result of Function.prototype.toString(), which will break when the code is minified. Moreover, using default value for parameters, or putting comments between parameters of the callback function, is NOT allowed.

For each parameter of the callback function, when the value of the property having the same name as the parameter changes, the callback function will be called (only if the property is set reactive by track(), define(), or __.bind()), and returns a watcher object, which can be used when calling unwatch().

let o = { name: 'ABC' }
__(o).watch(name => { console.log(`new name: ${name}`) })
o.name = 'DEF'
// Nothing Happended
__(o).track('name')
o.name = 'Foo'
// new name: Foo
o.name = 'Bar'
// new name: Bar

Handle<Object> :: unwatch (watcher: Watcher) -> Boolean

If watcher exists on the operand object, remove it and returns true. Otherwise returns false.

let o = { x: 3.5 }
let w1 = __(o).watch(x => { console.log(`x = ${x}`) })
let w2 = __(o).watch(x => { console.log(`2x = ${2*x}`) })
__(o).track('x')
o.x = 7
// x = 7
// 2x = 14
o.x = 8
// x = 8
// 2x = 16
__(o).unwatch(w1)
// true
__(o).unwatch(w1)
// false
o.x = 9
// 2x = 18
__(o).unwatch(w2)
// true
o.x = 10
// Nothing Happended

__.assert (value: any) -> Boolean

If value is a truthy value, returns true, otherwise throws and error.

__.assert('a'.toUpperCase() == 'A')
// true
__.assert('a'.toUpperCase() == 'B')
// Error: Assertion Failed

__.concat (...args: Iterable[]) -> Handle<Iterable>

Concatenates all the arguments.

__.concat([ 1, 2, 3 ], [ 4, 5, 6 ]).collect()
// Array(6) [ 1, 2, 3, 4, 5, 6 ]
__.concat(__.range(0, 5).unwrap(), [ 4, 3, 2 ]).map(n => n/10).collect()
// Array(8) [ 0, 0.1, 0.2, 0.3, 0.4, 0.4, 0.3, 0.2 ]

__.zip (...args: Iterable[]) -> Handle<Iterable>

Converges all arguments.

__.zip([1,2,3],['1','2','3'],['one','two','three']).collect()
// Array(3) [ [ 1, "1", "one" ], [ 2, "2", "two" ], [ 3, "3", "three" ] ]

__.range (start: Number, end: Number) -> Handle<Iterable>

Similar to the range() function in Python.

__.range(0, 10).collect()
// Array(10) [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
__.range(-10, 0).collect()
// Array(10) [ -10, -9, -8, -7, -6, -5, -4, -3, -2, -1 ]

__.$ (selector: String) -> HTMLElement | Null

Invokes document.querySelector(selector).

__.$('body')
// <body>
__.$('rveirvne')
// null

__.$$ (selector: String) -> HTMLElement[]

Invokes document.querySelectorAll(selector), returns an array created from the result NodeList.

__.$$('div')
// Array(18) [ ... ]
__.$$('vgwksbvke')
// Array []

__.bind (data: Object, parameters: Array) -> HTMLElement

Warning: Use of this function causes you CANNOT minify, uglify or obscure your JavaScript code. That is because the names of properties of data that are depended by the create HTML element is determined by the parameter list of the functions defined in the Props or Children part of the array argument parameters, this behaviour relies on the result of Function.prototype.toString(), which will break when the code is minified. Moreover, using default value for function parameters, or putting comments between parameters of those functions, is NOT allowed.

Creates a HTML element by the parameters and bind data on it. All the properties of data that are depended by the created HTML element will be set reactive.

The schema of parameters is

Parameters := [TagName, Props, Children] or [TagName, Props]

TagName is a String, Props is a HashTable, and Children is an Array or a Function.

Props

Props is a HashTable, consists with some name-value pairs, for example:

__.bind({}, ['div', { text: 'Text Content', style: { 'color': 'red', 'font-size': '20px' } }])
// <div style="color: red; font-size: 20px;">Text Content</div>

The values may be functions:

{ text: name => `Your name is ${name}` }
{ class: enabled => [ enabled? 'enabled': 'disabled' ] }

If a functional value is set to a Prop, it behaves like watch(). When the dependencies defined by the parameters of the function are updated, the Prop will be updated successively.

let d = { name: 'Alice' }
let e = __.bind(d, ['div', { text: name => `Your name is ${name}` }])
e  // <div>Your name is Alice</div>
d.name = 'Bob'
e  // <div>Your name is Bob</div>

The values for Props are treated according to the following schema table:

Prop NameValue SchemaBehaviour
class'c1', 'c2', 'c3', ...element.className = 'c1 c2 c3 ...'
style{ 'name': 'value', ... }element.style = 'name: value; ...'
dataset{ 'name': 'value', ... }Mutates element.dataset to make it become the desired value
show(Boolean Value)element.style.display = value? '': 'none'
text(String Value)element.textContent = value
on{ 'event_name': handler, ... }element['on'+'event_name'] = handler; ...
{ 'enter': handler, ... }Binds handler to keyup event, emit the handler when the pressed key is Enter key.
ref(String Value)Creates a reference on the element, just like ref="..." in React and Vue. The refs object will be passed as the second argument of each event handler defined in the on Prop.
(others)valueelement[prop_name] = value

Children

Children may be an array of Parameters or HTMLElement, like this:

[['li', { text: '1' }], ['li', { text: '2' }, ['li', { text: '3' }]]
[document.createElement('div'), __.bind({}, ['div', { text: 'div2' }])]

And, it can also be a function, like this:

items => items.map(item => ['li', { text: `${item}` }])

Just like a Prop with a functional value, the dependencies defined by the parameters of the function are updated, the Children will be updated successively. All the previous child elements will be removed from DOM (if the child has a data binding, __.unbind() will be called to cancel the data binding), and the new child elements will be added.

let d = { list: [1, 2, 3]  }
let e = __.bind(d, ['ul', {}, list => list.map(i => ['li', { text: `${i}` }])])
e
// <ul>
//   <li>1</li>
//   <li>2</li>
//   <li>3</li>
// </ul>
d.list = __(d.list).appended(4)
e
// <ul>
//   <li>1</li>
//   <li>2</li>
//   <li>3</li>
//   <li>4</li>
// </ul>

__.unbind (element: HTMLElement) -> Null

Cancels the data binding on the element. If there isn't a data binding on the element, throws an error.

let d = { text: 'Text' }
let e = __.bind(d, ['p', { text: text => text }])
e.textContent  // "Text"
d.text = 'New Text 1'
e.textContent  // "New Text 1"
__.unbind(e)
d.text = 'New Text 2'
e.textContent  // "New Text 1"
__.unbind(e)  // Error: Assertion Failed