The Tiny Function Library
Introduction
This small library is meant to provide some handy functions which are missing in DOM manipulating frameworks like jQuery or others.
And it doesn't want to be another armed-to-teeth full featured framework (might also be hard-to-learn).
Its goal is providing a minimalist building foundation for modern web pages.
import()
By default, Tiny will not inject global functions and object methods used in below examples into global
namespace
(the window
object).
To use those short function names, pleas call tiny.import()
ahead of all calls.
// register global function names for tiny methods
tiny.import();
A typical initialization code with Tiny sequence looks like this:
tiny.import(); // import functions & methods
tiny.output('error'); // output errors only in production environment
_storage.keyPrefix = 'my_app'; // set storage key prefix
var lang = _storage('lang'); // get user language setting
_lang.strings = LANG_STRINGS; // bind translation data
_lang.set(lang); // load correspond language strings
app.start(); // start your app script
help()
Show a structural overview of tiny
namespace. This function will also show a list of injected
global
functions and prototype methods.
// show tiny definitions in console
tiny.help();
Core Functions
_type()
Get detailed object type. This function is slow on Objects, don't use it in performance critical operations.
EXAMPLES
ASSERT('undefined', _type() == 'undefined');
ASSERT('null', _type(null) == 'null');
ASSERT('number', _type(123) == 'number');
ASSERT('string', _type('test') == 'string');
ASSERT('function', _type(function(){}) == 'function');
ASSERT('object', _type({test: true}) == 'object');
ASSERT('RegExp', _type(/\s\t\n/) == 'RegExp');
ASSERT('Date', _type(new Date()) == 'Date');
ASSERT('Array', _type([1,2,3]) == 'Array');
ASSERT('Arguments', _type(arguments) == 'Arguments');
ASSERT('Window', _type(window) == 'Window');
ASSERT('HTMLCollection', _type(document.forms) == 'HTMLCollection');
ASSERT('NodeList', _type(document.querySelectorAll('.run-code')) == 'NodeList');
_each()
A handy replacement to the for()
and for...in
loop.
The syntax looks like Array.forEach() method, but you can also use it on Array
, Object
,
Function
, String
, Number
and some Array-like objects:
Arguments
- thearguments
object inside a functionStorage
- thewindow.localStorage
objectHTMLCollection
- likedocument.forms
NodeList
- result ofdocument.querySelectorAll
jQuery
- result of$()
function
And you can continue or break with a return value.
The number of steps was determined by the array length before the loop starts. It won't change no matter you add items to or remove items from the array.
_each(object, [start,] callback [,this_arg]) : any
// object : Array, Object, String or Number to loop
// start : you can set the start index of the loop (Array-like only)
// callback : callback function for the loop
// this_arg : set the value of 'this' inside the scope
// extended method for Array and String
Array._each([start,] callback [,this_arg]) : any
String._each([start,] callback [,this_arg]) : any
function callback(value, index, array) {
return; // continue to next item in array_or_object
return any_value; // break out, and _each() will return any_value
}
NOTES
Always use for()
on Arrays in performance critical operations.
Below is a test result on an Array:
for() 109.114ms // best performance
_each() 792.093ms // implement with for() loop
Array.forEach() 2052.318ms
for...of 1501.836ms
for...in 9032.005ms
For looping through Object, Map or Set, if you can use for..of
feature of ECMAScript 6 in your
project,
the performance can be 50 times faster. And this feature allows you using continue
and break
to control code flow.
Below is a test result on an Object:
Object.keys() + for() 1064.989ms // not sure why this is slow
Object.keys().forEach() 145.356ms // fast but no flow control
_each() 822.088ms // implement with for...in loop
for...in 802.236ms
for...of 213.247ms // best choice - just a little slower than forEach()
EXAMPLES
var arr = [];
// ==> Number
// value: 1 -> 6, index: 0 -> 5
_each(6, function (value, index) {
arr[index] = value;
});
ASSERT('_each() on number', arr.join() === '1,2,3,4,5,6');
// ==> String
// value: a -> c, index: 1 -> 2
'abc'._each(1, function (value) {
arr.push(value);
});
ASSERT('_each() on string', arr.join() === '1,2,3,4,5,6,b,c');
// ==> Array
_each(arr, function (value, index, array) {
if (value < 4) return; // continue
if (value > 5) return true; // break
array[index] = value * 10;
});
ASSERT('_each() on array', arr.join() === '1,2,3,40,50,6,b,c' );
// return a value
var chr = arr._each(function (value, index, array) {
if (typeof value !== 'number')
return value; // return first non-number value
});
ASSERT('Array._each() & return value', chr === 'b' );
// ==> Object
var list = { action: 'test', animal: 'duck' };
var my_string = '';
_each(list, 'action', function (value, label) {
my_string += 'This is a ' + value + '. ';
});
ASSERT('_each() on object', my_string === 'This is a test. This is a duck. ');
// ==> Elements
var elems = document.querySelectorAll('h3');
var count = 0;
_each(elems, function (elem, index) {
if(elem.tagName != 'H3') count += index;
});
ASSERT('_each() on NodeList', count === 0 );
// ==> Arguments
// this code block is called as func(elem, 'this', 'is', 'a', 'test');
count = 0;
_each(arguments, function (arg, index) {
if(arg == 'test') count = index;
});
ASSERT('_each() on arguments', count === 4 );
ERROR TESTS
// A TypeError will be thrown on invalid parameter types
// Detailed information can be found in console
try{
_each([1, 2, 3], 'error test 2');
FAIL('_each() paramter 2 error');
}catch(err){
ASSERT('_each() paramter 2 error', err instanceof TypeError );
_info(err.message)
}
_extend()
Extend an object with new properties. This function does not do deep extension.
And Javscript preserved keywords can not be used or overwritten.
_extend(target, extensions[, overwrite]) : object
// target : the target Object or Function you want to extend
// extensions : an Object contains extensions that will be applied to target
// overwrite : whether overwrite existing properties, default is true
EXAMPLES
var point = { x: 1, y: 2 };
var point_3d = _extend(point, {z: 3});
ASSERT('_extend()', FLAT(point_3d) === '{"x":1,"y":2,"z":3}' );
// x will be overwrite
var point_4d = _extend(point_3d, {x: 10, t: 4});
ASSERT('_extend() overwrite', FLAT(point_4d) === '{"x":10,"y":2,"z":3,"t":4}' );
// x & y will not be overwrite
point_4d = _extend(point_3d, {x: 100, y: 100}, false);
ASSERT('_extend() no overwrite', FLAT(point_4d) === '{"x":10,"y":2,"z":3,"t":4}' );
ERROR TESTS
// A TypeError will be thrown on invalid parameter types
// Detailed information can be found in console
try{
_extend('error test 1');
FAIL('_extend() parameter 1 error');
}catch(err){
ASSERT('_extend() parameter 1 error', err instanceof TypeError );
}
try{
_extend({}, 'error test 2');
FAIL('_extend() parameter 2 error');
}catch(err){
ASSERT('_extend() parameter 2 error', err instanceof TypeError );
_info(err.message)
}
hash()
A fast hash function for generating 32-bit hash number from a string.
Currently it's using the Murmur2 algorithm. This algorithm is designed for uniqueness not security. Don't use it for password hashing.
EXAMPLES
ASSERT('1', tiny.hash('This is a test') === 466082811);
// you can apply an integer seed
ASSERT('1', tiny.hash('This is a test', 5330) === 3689166302);
// kown Murmur2 collisions
ASSERT('collision 1', tiny.hash('costarring') !== tiny.hash('liquid'));
Console Functions
_log() ... _error()
Tiny provides several shorthands to console methods: _log()
, _group()
, _timer()
,
_info()
,
_warn()
and _error()
.
You might use these functions just like the original methods of console
. These are not wrapper
functions,
and will not show the wrapper function name & location in the console output.
By default, output of _log()
& _dir()
is turned off. Don't bother end users
with those
logs.
You can easily turn the console log on or off by calling tiny.output('all')
in develop or
production
environments.
EXAMPLES
// enable _log() output when you need them
tiny.output('all');
// use them as you would like the console.xxx equivalents
_log('Test for _log()', '----------------------------');
_info('Test for _info()', null );
_warn('Test for _warn()', 'Nothing will happen');
_error('Test for _error()', '----------------------------');
_inspect()
The _inspect
function is a helper function for debug purpose.
It will return a formatted JSON string version of given object for inspection.
Functions and special objects might lose in this process.
_inspect(obj [, key_filter] [, log]) : string
EXAMPLES
var obj = {
id: 123,
view: { loaded: true,
date: new Date(1467190047725) },
method: function(){}
};
var result = _inspect( obj, ['method'], false );
var expected_result = JSON.stringify({
"id": 123,
"view": {
"loaded": true,
"date": "2016-06-29T08:47:27.725Z"
}
}, null, 4);
// the result should looks like expected_result
ASSERT('_inspect()', result == expected_result);
_group()
Wrapper for console.group()/groupCollapsed()/groupEnd().
EXAMPLES
// call with a name to start a group
_group('test group');
// log something
_info('test', 123)
// call again to end a group
_group();
// collapsed group - first parameter is false
_group(false, 'test collapsed group');
_info('test', 456)
_group();
_timer()
Timing function using window.performance.now(). It returns a value on end
EXAMPLES
// call with an id to record start time
_timer('test');
// call again to output to console - last timer name can be ommitted
_timer();
// start another
_timer('test delayed');
setTimeout(function(){
// call with false as second parameter to get the value
ASSERT('time', _timer('test delayed') > 199);
}, 200);
Message System
This tiny library includes a basic pseudo message system. Messages are more like remote function calls.
In order to distinguish from the DOM Event system, it is named _message.
Though it's not real asynchronize message delivery, it should be enough for most use cases on browser client.
_message.register()
Register messages.
All messages must be registered before using. Duplicate registration will result an error.
_message.register([namespace,] array_of_message_name_strings) : _message
_message.listen()
listen to a message.
There is no unlisten() method, since you can easily drop a message in your message handler by checking internal flag (and you definitely will have an internal flag in such case).
_message.listen(message_name, handler) : _message
function handler(msg_name, param_1, param_2 ...){ ... }
_message.post()
Post a message to listeners.
_message.post(msg_name, param_1, param_2 ...) : _message
_message.postDelayed()
Post a message to listeners after a specified delay in milliseconds.
If the same message is triggered again, the delay time will be reset. This behavior is suit for implement Type-n-Search like features.
_message.postDelayed(delay_time, msg_name, param_1, param_2 ...) : _message
All these methods will return the _message
object. You can chain methods if required.
EXAMPLES
var list = [];
// define the message handler
function put_in_list(){
var args = Array.prototype.slice.call(arguments);
list = list.concat(args);
}
// register messages
_message.register('global-msg')
_message.register('list::add')
_message.register(
'list', [
'remove',
'error' ]
)
// listen to the message
_message
.listen('list::add', put_in_list)
.listen('list::error', function(){ FAIL('_message.post()') });
// post a message
_message.post('list::add', 'me', 123);
// check result
ASSERT( '_message.post()', FLAT(list) == '["me",123]' );
// post a delayed message
_message.postDelayed(50, 'list::add', 'you', 456);
// check result after 100 milliseconds
setTimeout(function(){
ASSERT( '_message.postDelayed()', FLAT(list) == '["me",123,"you",456]' );
}, 100);
ERROR TESTS
// A TypeError will be thrown on invalid parameter types
// Detailed information can be found in console
try{
_message.register(1, []);
FAIL('_message.register() parameter 1 error');
}catch(err){
ASSERT('_message.register() parameter 1 error', err instanceof TypeError );
_info(err.message)
}
try{
_message.register('ns', 2);
FAIL('_message.register() parameter 2 error');
}catch(err){
ASSERT('_message.register() parameter 2 error', err instanceof TypeError );
_info(err.message)
}
try{
_message.listen(['error test', 1]);
FAIL('_message.listen() parameter 1 error');
}catch(err){
ASSERT('_message.listen() parameter 1 error', err instanceof TypeError );
_info(err.message)
}
try{
_message.listen('my_msg', 'error test');
FAIL('_message.listen() parameter 2 error');
}catch(err){
ASSERT('_message.listen() parameter 2 error', err instanceof TypeError );
_info(err.message)
}
try{
_message.post(['error test'], null );
FAIL('_message.post() parameter 1 error');
}catch(err){
ASSERT('_message.post() parameter 1 error', err instanceof TypeError );
_info(err.message)
}
try{
_message.postDelayed(['error test 1'], null , null );
FAIL('_message.postDelayed() parameter 1 error');
}catch(err){
ASSERT('_message.postDelayed() parameter 1 error', err instanceof TypeError );
_info(err.message)
}
try{
_message.postDelayed(100, ['error test'], null );
FAIL('_message.postDelayed() parameter 2 error');
}catch(err){
ASSERT('_message.postDelayed() parameter 2 error', err instanceof TypeError );
_info(err.message)
}
Router System
Routes are used to indicate a stateful view by a persistent URL.
With them you can access or share content directly with an URL, go back & forth with the browser buttons.
They should not be used for temporary view like a popup dialog or represent an action to execute.
This is why the router system of Tiny is this simple.
And for simplicity and compatibility, this router system uses window.location.hash
(#string)
for routing,
instead of the fancy History API introduced in HTML5.
And Tiny provides a predefined View Manager for fast implement of views.
_route.watch()
Watch a certain route.
There is no unwatch() method, since you can easily drop a call in your route handler by checking internal flag (and you definitely will have an internal flag in such case).
The route
parameter can be a string or a RegExp object.
_route.watch(route, handler) : _route
function handler(current_route, param_object){ ... }
This method will return _route
object, you can chain methods if needed.
The route
parameter can be:
'/'
- match''
or'/'
route only. This is used to implement index view.'/module'
- match any route which starts with a '/module' section.'module'
- match any route which contains a 'module' section. A section must start with '/' or start of line, and end with '/' or end of line.'/search/{keyword}/p{page}'
- will match '/search/me/p3' and call handler with {keyword: 'me', page: '3'}/item=(.*?),(.*?)/i
- use RegExp to match, matched parameters will be returned in an Array.
The handler
function will be called when:
- Current route is a match.
- The route was matched last time, but no match this time.
This behavior can make view switching easier.
The current_route
parameter of the callback function will be current hash string without
starting '#'.
And the param_object
will be true
or an Object
contains paramters
mateched
in URL. Like:
// route match
-> handler('current/route', true);
// route match and has parameters
-> handler('current/route/noname/123', {name: 'noname', id: '123'});
If watched route was matched last time but not this time, the param_object
will be false
.
// route not match
-> handler('current/route', false);
_route.check()
Check a route string or window.location.hash for route matching. Returns true if match found.
If you want Tiny to check automatically on window.location.hash changes, please use _route.on()
.
_route.check([route_string]) : boolean
// route_string : if not given, window.location.hash will be checked
EXAMPLES
var data = [];
function save_data(route, params){
data.push(params);
}
_route
.watch('/', function(route, param){ data.push(param ? 1 : 0) })
.watch('/list', save_data)
.watch('list/id:{id}', save_data);
// invoke root rule only
_route.check('');
ASSERT('_route.check() root', FLAT(data) === '[1]');
// '/' no match, a false parameter will be sent
// and match '/list' without parameter
var result = _route.check('/list/a');
ASSERT('_route.check() true', result === true);
ASSERT('data', FLAT(data) === '[1,0,true]');
// no match, but '/list' matched previously, a false parameter will be sent
result = _route.check('/id:123');
ASSERT('_route.check() false', result === false);
ASSERT('data add false', FLAT(data) === '[1,0,true,false]');
// no match again, nothing should happen
result = _route.check('lis');
ASSERT('_route.check() false again', result === false);
ASSERT('data no change', FLAT(data) === '[1,0,true,false]');
// match '/list/id:{id}' and get parameters
result = _route.check('word/list/id:123');
ASSERT('_route.check() true again', result === true);
ASSERT('data change', FLAT(data) === '[1,0,true,false,{"id":"123"}]');
data = [];
// match '/list' and '/list/id:{id}'
result = _route.check('/list/id:456');
ASSERT('_route.check() must true again', result === true);
ASSERT('data change again', FLAT(data) === '[true,{"id":"456"}]');
data = [];
// no match becuase the section string is 'my_list', not 'list'
// and previously matched routes will receive false
result = _route.check('my_list/id:789');
ASSERT('_route.check() shoule be false', result === false);
ASSERT('data add 2 false', FLAT(data) === '[false,false]');
_route.on/off()
Start or stop monitoring window.onhashchange event and check current window.location.hash immediately.
_route.on() : _route
_route.off() : _route
_route.append()
Append sections to route string.
_route.append(string_or_array[, trigger]) : _route
// string_or_array : a string or an array of items to append
// trigger : whether trigger route change event, default is true
_route.remove()
Remove route string from first occurance of given string.
Slashes matters:
'key'
- /my_key_chain/car_key/key:1' -> '/my_'.'/key'
-'/my_key_chain/car_key/key:1' -> '/my_key_chain/car_key'.'key/'
-'/my_key_chain/car_key/key:1' -> '/my_key_chain/car_'.'/key/'
-'/my_key_chain/key/id:1' -> '/my_key_chain'.
_route.remove(section[, trigger]) : _route
// section : from which the route string will be removed
EXAMPLES
var data = [];
function save_data(route, params){
data.push(params);
}
_route
.watch('/', function(){ data.push('/') })
.watch('/books', save_data)
.watch('books/id:{id}', save_data)
.watch('books/{id}/{page}', save_data);
// set a test route - no event triggered
_route.set('/books', false);
ASSERT('_route.set() no match', FLAT(data) === '[]');
// start monitoring window.location.hash changes
_route.on();
// got a match immediately
ASSERT('_route.on() match', FLAT(data) === '[true]');
// add something to current route
_route.append('id:123');
ASSERT('_route.get()', _route.get() === '/books/id:123');
// match '/books' and '/books/id:{id}' with the parameter
ASSERT('_route.append()', FLAT(data) === '[true,true,{"id":"123"}]');
// remove the 'id:123' section without trigger the event
_route.remove('/id:', false);
ASSERT('_route.get() after cut', _route.get() === '/books');
// no change to the data
ASSERT('_route.remove()', FLAT(data) === '[true,true,{"id":"123"}]');
data = [];
// send an array to _route.add()
_route.append([123, 456]);
ASSERT('_route.append() array', _route.get() === '/books/123/456');
// '/books' match, 'books/id:{id}' no match, 'books/{id}/{page}' match
ASSERT('_route.append() result', FLAT(data) === '[true,false,{"id":"123","page":"456"}]');
// turn off event monitoring
_route.off();
// set a new route
_route.set('my/books/id:789');
ASSERT('_route.set() result', _route.get() === 'my/books/id:789');
// no change to the data
ASSERT('_route.set() array result', FLAT(data) === '[true,false,{"id":"123","page":"456"}]');
data = [];
// check window.location.hash
_route.check();
// '/books' no match, 'books/id:{id}' match, 'books/{id}/{page}' no match
ASSERT('_route.on() again', FLAT(data) === '[false,{"id":"789"},false]');
data = [];
// move to root
_route.set('/');
_route.check();
// '/' match, 'books/id:{id}' no match
ASSERT('_route.set() to root', FLAT(data) === '["/",false]');
ERROR TESTS
// A TypeError will be thrown on invalid parameter types
// Detailed information can be found in console
try{
_route.watch(null);
FAIL('_route.watch() paramter 1 error');
}catch(err){
ASSERT('_route.watch() paramter 1 error', err instanceof TypeError );
}
try{
_route.watch('/test', null);
FAIL('_route.watch() paramter 2 error');
}catch(err){
ASSERT('_route.watch() paramter 2 error', err instanceof TypeError );
}
try{
_route.check(123);
FAIL('_route.check() paramter 1 error');
}catch(err){
ASSERT('_route.check() paramter 1 error', err instanceof TypeError );
}
try{
_route.append(null);
FAIL('_route.append() paramter 1 error');
}catch(err){
ASSERT('_route.append() paramter 1 error', err instanceof TypeError );
}
try{
_route.remove(null);
FAIL('_route.remove() paramter 1 error');
}catch(err){
ASSERT('_route.remove() paramter 1 error', err instanceof TypeError );
}
DOM Manipulation
_q() and _q1()
A tiny jQuery like function solely relies on document.querySelector()
. It just provide some
basic functions in order to keep lean. And we shouldn't reinvent the wheel when there are plenty good ones.
Note: It might have problems under IE8 since many CSS3 selectors are not supported and IE8 has some namespace problems on selectors.
Local Storage
_storage()
A handy function for accessing window.localStorage
.
Integer, Boolean, Array, Date & Object types will be automatically converted on access.
Note: All local pages might share a same localStorage store in some browsers. You can use keyPrefix
to avoid collisions.
// filter keys by prefix, default is ''
tiny.storage.keyPrefix = 'your_prefix';
_storage() // return all values as an Object
_storage(key) // get value by key
_storage(key, value) // set value by key
_storage(key, null) // delete item of given key
_storage(null, null) // delete all contents
// or send an object for batch operations
_storage({
key1: value1,
key2: null,
...
})
EXAMPLES
// clean up for test
_storage('tiny_lib_test', null);
// write a value
_storage('test', false);
ASSERT('_storage() no prefix', _storage('test') === false);
// set a prefix
tiny.storage.keyPrefix = 'tiny_lib';
// should not be able to read from previous key
ASSERT('_storage() with prefix', _storage('test') === undefined);
// write value - with prefix
_storage('test', true);
_storage('num', 123);
_storage('array', [4, 5, 6]);
_storage('date', new Date(1467190047725));
_storage('object', {x:1, y:2});
// read - with prefix
ASSERT('_storage() 1', _storage('test') === true);
ASSERT('_storage() 2', _storage('num') === 123);
ASSERT('_storage() 3', FLAT(_storage('array')) === '[4,5,6]');
ASSERT('_storage() 4', FLAT(_storage('date')) === '"2016-06-29T08:47:27.725Z"');
ASSERT('_storage() 5', FLAT(_storage('object')) === '{"x":1,"y":2}');
// read all - with prefix
var data = _storage();
ASSERT('_storage() all 1', data.test === true);
ASSERT('_storage() all 2', data.num === 123);
ASSERT('_storage() all 3', FLAT(data.array) === '[4,5,6]');
ASSERT('_storage() all 4', FLAT(data.date) === '"2016-06-29T08:47:27.725Z"');
ASSERT('_storage() all 5', FLAT(data.object) === '{"x":1,"y":2}');
// delete - with prefix
_storage('date', null);
ASSERT('_storage() delete', _storage('date') === undefined);
// batch operation
_storage({
bool: false, // add
num: 789, // modify
array: null // delete
});
ASSERT('_storage() batch 1', _storage('bool') === false);
ASSERT('_storage() batch 2', _storage('num') === 789);
ASSERT('_storage() batch 3', _storage('array') === undefined);
// delete all - with prefix
_storage(null, null);
ASSERT('_storage() delete all', FLAT(_storage()) === '{}');
tiny.storage.keyPrefix = '';
// previous value with no prefix should still there
ASSERT('_storage() no prefix value', _storage('test') === false);
ERROR TESTS
// A TypeError will be thrown on invalid parameter types
// Detailed information can be found in console
try{
_storage(123);
FAIL('_storage() paramter 1 error');
}catch(err){
ASSERT('_storage() paramter 1 error', err instanceof TypeError );
}
Internationalize
Tiny provides a set of simple functions to make i18n easier.
_lang()
Get a string from language string set.
Undefined strings will be collected in a list _lang.missing
in runtime, this will make finding
missing
language strings easier.
_lang(str [,lang_code])
// str : the string to lookup in current language string set
// lang_code : the language code
The language strings should be assign to _lang.strings
. You can modify it whatever you want.
But the default section set in _lang.code
must exist.
// current languague code - default is 'en'
// please use
_lang.code = 'en';
// assign set object
_lang.strings = {
// default = 'en'
"en" : {
'_name' : 'English',
"hello" : "Hello!"
},
// Simplified Chinese - 'zh-cn'
"zh-cn" : {
'_name' : '简体中文',
'language' : '=> _name', // refer to '_name', must starts with '=> '
// these options will be set to format functions on _lang.set() call if exist
'_decimalDelimiter' : '.',
'_thousandsDelimiter' : ' ',
'_currencyFormat' : '¥ [.00]',
'_dateFormat' : 'datetime',
'_dateNames' : {
day: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
dayAbbr: ['日', '一', '二', '三', '四', '五', '六'],
month: ['一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'],
monthAbbr: ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'],
ap: ['上午', '下午']
},
// here goes your language strings
'hello' : "你好!"
}
};
_lang.set()
Get a string from language string set.
_lang.set([lang_code])
// lang_code : the language code
Format Template
_format()
Simply yet powerful enough format function with template support.
This function support both normal html templates and the shorthand templates.
Templates are parsed in a one-time loop over the string. In favor of performance, no RegExp is used.
Expanded shorthand templates will be cached in memory or their original <script> tags for performance.
_format(template, data_object);
// template : the template string
// data_object : an object contains data you want to fill into the template
String._format(data_object);
// format html template string
_format('<span class="date-tag">Today is {date}</span>', {date: new Date()});
// format shorthand template string (starts with '...')
_format('span.date-tag :Today is {date}', new Date());
// get template for HTML tag with id 'date-tag-template'
'#date-tag-template'._format(obj_date);
/*
>> Template Token format for value fill-in
{index} : refer to data_object[index] item by index
{key} : refer to data_object[key], the token will be kept if undefined
{*key} : optional token, fill with '' if undefined
{key|format} : set format string for data_object[key]
{} : fill with whole object, format with default style
{|format} : fill with whole object with given 'format' string
{[any text]} : contents inside brackets will be output directly
If a token's value is undefined, the token will be kept untouched.
>> Special Formats
{key|!html} : don't encode special html chars
{$key} : refer to a language string by calling _t(key)
{key|5} : output first 5 chars from beginning
{key|5.} : output first 5 chars and add ... at end
{key|-5} : output last 5 chars from ending
{key|-5.} : output last 5 chars and add ... at beginning
>> Conditional Block
{?key} : Show block if data_object[key] is not empty or false,
and if it is an Array, loop throught it to build a list
{subkey} : Tokens inside block will be filled with data_object[key][subkey]
{/?key}
{!key} : Show block if data_object[key] is empty or false
{other_key} : Tokens inside block will be filled with data_object[other_key]
{/!key}
>> Reference to template
{#template-id} : refer to a template block inside html file with id
>> Shorthand Template Syntax
... : always starts with three dots mark
tag : start a tag with tag name, DIV can be ommitted
#id : mark start of an ID
.classname : mark start of a class
[attr=val] : mark an attribute
:text : all string between this mark to next newline char will be treated as tag content
> : indicate an inline nested indent (won't work after ':' mark)
*/
EXAMPLES - SIMPLE FORMAT
// use whole data object and format with 'DD'
var result = _format('Today is {|DD}.', new Date(1118102950753));
ASSERT('date', result === 'Today is Tuesday.');
// cut long string
var str = 'This is a very very loooooooong string';
result = 'It says: "{|14.}"'._format(str);
ASSERT('cut end', result === 'It says: "This is a very..."');
result = 'It says: "{|-14.}"'._format(str);
ASSERT('cut start', result === 'It says: "...ooooong string"');
// output text and keep { } inside
result = '{[Output {token} text\n with new line]}'._format({token: 123});
ASSERT('{[output text]}', result === 'Output {token} text\n with new line');
var my_items = {
book: { price: 1299.99,
name: "my book" }
};
// refer to a sub key of data object
result = 'The price is {book.price|,}'._format(my_items);
ASSERT('sub key', result === 'The price is 1,299.99');
var arr = [10, 20, 30];
// array index - starts at 0
result = 'Refer to {1}'._format(arr);
ASSERT('array index', result === 'Refer to 20');
// special chars will be escaped
result = 'Click {}'._format('<a href="#">HERE</a>');
ASSERT('escape html', result === 'Click <a href="#">HERE</a>');
// don't escape
result = 'Click {|!html}'._format('<a href="#">HERE</a>');
ASSERT('keep html', result === 'Click <a href="#">HERE</a>');
EXAMPLES - SHORTHAND TEMPLATE
var data = {
id: 123456,
title: 'Plan A & B',
tasks: 7,
people: 9
};
// shorthand template is short and clear
var template = '... li #{id} .item > .title :{title}';
var result = template._format(data);
ASSERT('inline shorthand 1',
result ===
'<li id="123456" class="item">\n' +
' <div class="title">Plan A & B</div>\n' +
'</li>\n');
template = '...' +
'li #{id} .item :{[\n' +
' Output {token} text\n' +
' with new line]}\n' +
' b :Though this might mess up';
result = template._format(data);
ASSERT('inline shorthand 2',
result ===
'<li id="123456" class="item">\n' +
' Output {token} text\n' +
' with new line\n' +
' <b>Though this might mess up</b>\n' +
'</li>\n');
// complex shorthand template
// indents('\t' or ' ') and end-of-lines('\n') are important
// you can choose to use tabs or spaces, mixing them might result chaos
template = '...' +
'li.item[data={*data}][ref={url}]\n'+
' .title :{title}\n' +
' .tags :{tasks} TASKS <i>/</i> {people} PEOPLE\n';
// format without data - returns template
result = template._format();
ASSERT('shorthand -> html template',
result ===
'<li class="item" data="{*data}" ref="{url}">\n' +
' <div class="title">{title}</div>\n' +
' <div class="tags">{tasks} TASKS <i>/</i> {people} PEOPLE</div>\n' +
'</li>\n');
// fill in data
// undefined value's token will be kept untouched unless it's optional (start with *)
result = result._format(data);
ASSERT('html template -> output',
result ===
'<li class="item" data="" ref="{url}">\n' +
' <div class="title">Plan A & B</div>\n' +
' <div class="tags">7 TASKS <i>/</i> 9 PEOPLE</div>\n' +
'</li>\n');
EXAMPLES - TEMPLATES INSIDE HTML FILE
<!--
****** TEMPLATES IN HTML FILE ******
Templates must be placed inside <script> tags to keep indents, tags and invisible.
And it's better to place them after the body tag to avoid being translated by
the _lang.translate() function before usage.
-->
<!-- Normal HTML Template -->
<script type="text/x-template" id="tpl-circular-ref">
<div class="content">
<h2>
<a href="#/project/{id}">
<img src="img/project_{logo}.png"/>
Plan List of {project}
</a>
</h2>
<ul class="plan-list">
{?plans}
{#tpl-circular-ref} <!-- Circular Reference to self => ReferenceError -->
{/?plans}
</ul>
</div>
</script>
<!-- Shorthand Equilavent of above -->
<!--
Indents are important in shorthand templates
a tab is count as one indent, and don't mix tabs and spaces
and conditional {} tokens should start with : to output as text
-->
<script type="text/x-template" id="tpl-sh-block">
...
.content
h2
a [href=#/project/{id}]
img [src=img/project_{logo}.png]
:Plan List of {project}
:{!done}Project in Progress{/!done}
ul .plan-list
:{?plans}
:{#tpl-sh-li}
:{/?plans}
</script>
<!-- List item template -->
<script type="text/x-template" id="tpl-sh-li">
...
li .item [id={id}]
.title :{title}
.tags :{tasks} TASKS <i>/</i> {people} PEOPLE
</script>
/****** Javascript File ******/
// returns expanded HTML template string if no data object is given
var result = _format('#tpl-sh-block');
ASSERT('sh -> html',
result ===
'<div class="content">\n'+
' <h2>\n'+
' <a href="#/project/{id}">\n'+
' <img src="img/project_{logo}.png"/>\n'+
' Plan List of {project}\n'+
' </a>\n'+
' {!done}Project in Progress{/!done}\n'+
' </h2>\n'+
' <ul class="plan-list">\n'+
' {?plans}\n'+
' {#tpl-sh-li}\n'+
' {/?plans}\n'+
' </ul>\n'+
'</div>\n');
// and the expanded template should be cached inside the original tag
var cache = document.getElementById('tpl-sh-block').innerHTML;
ASSERT('cached', cache == result);
// prepare data object
var data = {
id: 789,
project: 'Tiny Project',
logo: 'panda',
plans: [
{id: 1, title: 'Sprint 1', tasks: 2, people: 5},
{id: 2, title: 'Sprint 2', tasks: 15, people: 3},
]
};
// fill data into the template
result = result._format(data);
ASSERT('output',
result ==
'<div class="content">\n' +
' <h2>\n' +
' <a href="#/project/789">\n' +
' <img src="img/project_panda.png"/>\n' +
' Plan List of Tiny Project\n' +
' </a>\n' +
'\n'+ // {!done}
'Project in Progress\n' + // {/!done}
' </h2>\n' +
' <ul class="plan-list">\n' +
'\n'+ // {?plans}
'<li class="item" id="1">\n' +
' <div class="title">Sprint 1</div>\n' +
' <div class="tags">2 TASKS <i>/</i> 5 PEOPLE</div>\n' +
'</li>\n' +
'<li class="item" id="2">\n' +
' <div class="title">Sprint 2</div>\n' +
' <div class="tags">15 TASKS <i>/</i> 3 PEOPLE</div>\n' +
'</li>\n' +
'\n' + // {/?plans}
' </ul>\n' +
'</div>\n');
ERROR TESTS
<!-- HTML CONTENT FOR ERROR TESTS -->
<!-- Missing close '}' => SyntaxError -->
<script type="text/x-template" id="tpl-missing-mustache">
<div class="{state">{title} </div>
</script>
<!-- Missing close ']}' => SyntaxError -->
<script type="text/x-template" id="tpl-missing-bracket">
<div class="{state}">{[Use {token} to
replace} </div>
</script>
<!-- Missing close '{/?token}' => SyntaxError -->
<script type="text/x-template" id="tpl-missing-close-token">
{?has_state}
<div class="{state}">
{?has_state}
{[Use {token} to replace]}
{/?has_state}
</div>
{/?has_token} <!-- wrong close token -->
</script>
// Detailed error information can be found in console
// TypeError : invalid parameter types
try{
_format({obj: "obj"});
FAIL('_format() paramter error');
}catch(err){
ASSERT('_format() paramter error', err instanceof TypeError );
}
// TypeError : data object is not an object
try{
_format('{key}', 'test');
FAIL('_format() invalid data object');
}catch(err){
ASSERT('_format() invalid data object', err instanceof TypeError );
}
// ReferenceError : element of given id is not found
try{
_format('#test-id-here');
FAIL('_format() id not found');
}catch(err){
ASSERT('_format() id not found', err instanceof ReferenceError );
}
// ReferenceError : circular reference is detected
try{
_format('#tpl-circular-ref');
FAIL('_format() circular reference');
}catch(err){
ASSERT('_format() circular reference', err instanceof ReferenceError );
}
// SyntaxError : '\n' inside token
try{
'{test\nnew_line}'._format({});
FAIL('_format() \\n inside');
}catch(err){
ASSERT('_format() \\n inside', err instanceof SyntaxError );
}
// SyntaxError : a close '}' is missing
try{
_format('#tpl-missing-mustache', {});
FAIL('_format() missing }');
}catch(err){
ASSERT('_format() missing }', err instanceof SyntaxError );
}
// SyntaxError : a close ']}' is missing
try{
_format('#tpl-missing-bracket', {});
FAIL('_format() missing ]}');
}catch(err){
ASSERT('_format() missing ]}', err instanceof SyntaxError );
}
// SyntaxError : a close '{/?token}' is missing
try{
_format('#tpl-missing-close-token', {});
FAIL('_format() missing clsoe {/?token}');
}catch(err){
ASSERT('_format() missing close {/?token}}', err instanceof SyntaxError );
}
_htmlSafe()
Make a string HTML-safe.
_htmlSafe(str [, keep_spaces]);
String._htmlSafe([keep_spaces]);
EXAMPLES
var txt = 'task:\n >> done';
ASSERT('1', _htmlSafe(txt) === 'task:<br/> >> done');
ASSERT('2', txt._htmlSafe() === 'task:<br/> >> done');
// keep white spaces in html
ASSERT('3', _htmlSafe(txt, true) === 'task:<br/> >> done');
ASSERT('4', txt._htmlSafe(true) === 'task:<br/> >> done');
ERROR TESTS
// A TypeError will be thrown on invalid parameter types
// Detailed information can be found in console
try{
_htmlSafe(123);
FAIL('_htmlSafe() paramter 1 error');
}catch(err){
ASSERT('_htmlSafe() paramter 1 error', err instanceof TypeError );
}
_formatNumber()
Number format function.
_formatNumber(num[, format]);
Number._format([format]);
// you can set the default curreny format
_format.currencyFormat = '$[,.00]';
// and default delimiter characters
_format.decimalDelimiter = '.';
_format.thousandsDelimiter = ',';
// FORMAT STRING CODES:
// '[]' = indicate format token, '$ [.].00' => '$ 99.00'
// ',' = add thousand delimiter, hex will add ' ' every 4 digits
// '0000' = pad number to given width with '0'
// '.00' = set decimal place by number of '0'
// '$' = use default currency format
// '%' = convert to percent, decimal only
// 'x' = lowercase hex
// 'X' = uppercase HEX
EXAMPLES
var num = 123456.789;
ASSERT('1', num._format() == '123456.789');
ASSERT('2', num._format('Round to [.]') == 'Round to 123457');
ASSERT('3', num._format('.00') == '123456.79');
ASSERT('4', num._format(',') == '123,456.789');
ASSERT('5', num._format('[,.00]') == '123,456.79');
ASSERT('6', num._format('[,.00%] * 100') == '12,345,678.90% * 100');
ASSERT('7', num._format('$') == '$123,456.79');
// pad zero
num = 12345.678;
ASSERT('pad 1', num._format('00000000') == '00012345.678');
ASSERT('pad 2', num._format('00000000.') == '00012346');
ASSERT('pad 3', num._format('Padded: [,00000000.00]') == 'Padded: 00,012,345.68');
// change delimiters & currency format
_format.decimalDelimiter = ',';
_format.thousandsDelimiter = ' ';
_format.currencyFormat = '[,.00] €';
ASSERT('currency', num._format('$') == '12 345,68 €');
// hex - won't affect by above settings
num = 123456.789;
ASSERT('hex 1', num._format('x') == '1e240.c9fbe76c9');
ASSERT('hex 2', num._format('X.') == '1E240');
ASSERT('hex 3', num._format('X.00') == '1E240.C9');
ASSERT('hex 4', num._format('X0000000000,.') == '00 0001 E240');
ERROR TESTS
// A TypeError will be thrown on invalid parameter types
// Detailed information can be found in console
try{
_formatNumber('123');
FAIL('_formatNumber() paramter 1 error');
}catch(err){
ASSERT('_formatNumber() paramter 1 error', err instanceof TypeError );
}
try{
_formatNumber(123, 456);
FAIL('_formatNumber() paramter 2 error');
}catch(err){
ASSERT('_formatNumber() paramter 2 error', err instanceof TypeError );
}
_formatDate()
Date format function.
// you can use localized date names by replacing this data object
_format.dateNames = {
day: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
dayAbbr: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
month: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December'],
monthAbbr: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
ap: ['AM', 'PM']
};
// and set default format
_format.defaultDateFormat = 'datetime';
// 'datetime' => '2005-06-07 08:09:10'
// 'date' => '2005-06-07' // yyyy-MM-dd
// 'time' => '08:09:10' // HH:mm:ss
// 'iso' => '2005-06-07T00:09:10.753Z' // ISO 8601
// call the function
_formatDate(date[, format]);
// or use extended method
Date._format([format]);
// FORMAT STRING CODES:
// '[]' = indicate format token, 'yyyy[-M-d]' => 'yyyy-6-7'
// 'yyyy' = 2009, 'yy' = 09, 'y' = 9 // Year
// 'M' = 6, 'MM' = 06 // Numeric month
// 'MMM' = Jun, 'MMMM' = June // Month name
// 'd' = 7, 'dd' = 07 // Day of the month
// 'D' = Tue, 'DD' = Tuesday // Day of the week
// 'h' = 8, 'hh' = 08 // 12 Hour clock
// 'H' = 8, 'HH' = 08 // 24 Hour clock
// 'm' = 9, 'mm' = 09 // Minutes
// 's' = 10, 'ss' = 10, 'sss' = 753 // Seconds & Milliseconds
// 'z' = +08, 'zz' = +0800, 'ZZ' = +08:00 // Timezone
// 't' = AM, // AM / PM
EXAMPLES
var d = new Date(1118102950753);
ASSERT('0', d._format() === '2005-06-07 08:09:10');
ASSERT('1', d._format('datetime') === '2005-06-07 08:09:10');
ASSERT('2', d._format('date') == '2005-06-07');
ASSERT('3', d._format('time') == '08:09:10');
ASSERT('4', d._format('iso') == '2005-06-07T00:09:10.753Z');
ASSERT('5', d._format('D, d MMM yyyy H:m:s') == 'Tue, 7 Jun 2005 8:9:10');
// also accept date number from Date.getTime()
// text inside [] will be kept
ASSERT('[] token', _formatDate(1118102950753, 'Today is [DD]') == 'Today is Tuesday');
// mix token and text
ASSERT('mix', d._format('[D, d MMM yyyy H:m:s] GMT[zz]') == 'Tue, 7 Jun 2005 8:9:10 GMT+0800');
ERROR TESTS
// A TypeError will be thrown on invalid parameter types
// Detailed information can be found in console
try{
_formatDate('123');
FAIL('_formatDate() paramter 1 error');
}catch(err){
ASSERT('_formatDate() paramter 1 error', err instanceof TypeError );
}
try{
_formatDate(new Date(), 456);
FAIL('_formatDate() paramter 2 error');
}catch(err){
ASSERT('_formatDate() paramter 2 error', err instanceof TypeError );
}