jQuery without jQuery and some more helpful functions
Highlights:
- Supports even IE9!
$()
and$$()
element selectors- Convinient methods:
on
,off
,once
, andtrigger
$.getListeners(element)
- Event delegation for vanilla Javascript
$.extend
and$.merge
for different use cases- Bulk functions
$.properties
,$.attributes
, and$.style
$.ready
and$.fetch
Heavily inspired by:
- Bliss.js by Lea Verou
- min.js by Remy Sharp
- bling.js by Paul Irish
Works in all browsers and IE >= 9, but only if WeakMap
, Promise
, and URL
are polyfilled.
https://polyfill.io/v3/polyfill.min.js?features=Promise%2CURL%2CWeakMap
Promise
and URL
are only used in $.ready
, $.fetch
, $.getScript
, and $.defer
. If you don't use them, you don't need those polyfills.
Without any polyfill, it works in all evergreen browers and not in IE.
If IE >= 11 is enough, then only Promise
and URL
need polyfills.
Polyfills for Element.matches
, Element.closest
, NodeList.forEach
, CustomEvent
, and DOMException
are included if they don't exist.
Install with yarn: yarn add opusonline-subtle.js
$$('#section a').on('click', function(event) {
event.preventDefault();
});
$.style($('#section > h1'), {
color: 'red',
fontSize: '24pt',
backgroundColor: 'black'
});
$('#section').find('p').forEach(…);
var test = {foo: {bar: 1}}; // runs smoothly even if test is undefined or empty or whatever
if ($.deep(test, 'foo.bar') === 1) {
…
}
var testClone = $.clone(test);
testClone.foo.bar = 2;
log(test.foo.bar); // 1
var deferred = $.defer();
setTimeout(function() {
deferred.resolve(true);
}, 5000);
deferred.then(…);
$.getScript('/scripts/polyfill.js', navigator.userAgent.indexOf('MSIE') > -1).then(function() {
haveFun();
});
Returns DOM element if exists, else null
.
Returns a NodeList that is iteratable with forEach
and has a length
property. It really only retrieves child elements from the given element (See https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll#User_notes). :scope
is automatically injected if not used already.
Good to know: if find
returns an empty list you can still call forEach
.
$('#selector').find('> p').forEach(…);
Returns a NodeList that is iteratable with forEach
and has a length
property.
element.on(event, callback, [options]), element.off([event], [callback]), element.trigger(event), element.once(event, callback, [options]) => element
Event assigning on element(s). Classnames are supported.
Good to know: You can still use addEventListener
and removeEventListener
.
Goody: on
, off
, etc. are added to EventTarget
which is the base not only for elements, but also for objects like XMLHttpRequest
, FileReader
, and others.
var elements = $$('#selector a');
elements.on('click.test', doStuff);
elements.trigger('click');
elements.off('.test');
elements.on({'focusin': onFocus, 'focusout': onBlur});
elements.on('canHazData', function(event) {
log(event.detail); // {'foo': 'bar'}
});
elements.trigger('canHazData', {'foo': 'bar'});
window.on('scroll', nonBlockingFunction, {'passive': true}); // silently ignored if not supported (IE)
$.on(element, event, callback, [options]), $ .off(element, [event], [callback]), $.trigger(element, event), $ .once(element, event, callback, [options]) => element
Same as element.on()
but another syntax.
element
can be an array of elements.
Returns a list of event listeners for given element.
Good to know: Events added with addEventListener
and removeEventListener
are also considered.
$('#test').on('click.test', function(){});
$.getListeners($('#test'));
// {
// 'click': [
// {
// 'type': 'click',
// 'listener': function(){},
// 'handler': null,
// 'useCapture': false,
// 'classNames': ['test'],
// 'once': false
// }
// ]
// }
Convenient Pub/Sub event pattern on global scope.
$.subscribe('foo', function(event) {
log(event.detail); // "bar"
});
$.publish('foo', 'bar'); // triggers CustomEvent with detail = 'bar'
$.unsubscribe('foo');
This is my idea of using event delegation. That means, event handlers can already be added to a parent element like document
, before the actual element is even added to the DOM.
$('ul').on('click', $.delegated('li > a', function(event) {
event.preventDefault();
doStuff(this); // this = a / event.delegateTarget = ul
}));
Safely use multi-level objects without screwing up your app.
var test = {'foo': {'bar': 'baz'}};
$.deep(undef, 'foo.bar'); // undefined
$.deep(test, 'meh'); // undefined
$.deep(test, 'foo'); // {'bar': 'baz'}
$.deep(test, 'foo.bar'); // 'baz'
$.deep(test, 'foo.meh'); // undefined
This is both sugar and confusing at the same time. Bear with me!
Extend
simply takes a value from the source and adds it to the target. If it aleady exists, it gets replaced!
Good to know: getter and setter are not invoked in contrast to Object.assign
!
var target = {a: {b: 1, c: 2}};
var source = {a: {d: 2, c: 3}};
$.extend(target, source); // {a: {d: 2, c: 3}} - target.a is replaced with source.a!!!
Now $.merge
comes into play.
var target = {a: {b: 1, c: 2}};
var source = {a: {d: 2, c: 3}};
$.merge(target, source); // {a: {b: 1, c: 3, d: 2}} - it merges source.a to target.a
Have you noticed the 3rd parameter deep
? This makes sense for multi-level objects. If deep
is true
, a real clone is created.
var test = {'foo': {'bar': 'baz'}};
var extended = $.extend({}, test); // {'foo': {'bar': 'baz'}}
var extendedDeep = $.extend({}, test, true); // {'foo': {'bar': 'baz'}}
test.foo.bar = 'changed!';
log(extended.foo.bar); // 'changed!'
log(extendedDeep.foo.bar); // still 'baz'
Guess what? This makes a deep copy of the original object including class prototypes. It can be even used on special objects like Date
s or RegExp
.
Good to know: clone is a deep extend.
var test = {'foo': {'bar': 'baz'}};
var clone = $.clone(test);
test.foo.bar = 'changed!';
log(clone.foo.bar); // still 'baz'
function MyClass(message) {
this.number = 1;
}
var a = new MyClass();
var b = $.clone(a);
a.number = 5;
log(b.number); // still 1 !!!
Noop stands for no operation. This is simply an empty function.
Returns the base type name of the given object.
$.type('hello world!'); // String
$.type(1); // Number
$.type(true); // Boolean
$.type(function() {}); // Function
$.type(new Date()); // Date
$.type(/abc/); // RegEx
$.type(null); // Null
$.type(undefined); // Undefined
$.type(document); // HTMLDocument
$.type(new Uint8Array(1)); // Uint8Array
Set properties to an existing element or object. Works basically the same way as $.extend
does, but has some nice extra features.
Similar jQuery method: jQuery.fn.prop
$.properties($('button.next'), {
'textContent': 'Next Step',
'disabled': false,
'onclick': function() { MyApp.next() }
});
$.properties($('#new').style, $('#old').style, ['color', 'backgroundColor', 'fontSize']);
$.properties($('#new').style, $('#old').style, /color/i);
$.properties($('#new').style, $('#old').style, function(property) {
if (property.indexOf('color') > -1) {
return true;
}
return false;
});
Works the same way as $.properties
BUT attributes are added with Element.setAttribute
method.
Similar jQuery method: jQuery.fn.attr
Works the same way as $.properties
and $.attributes
.
Similar jQuery method: jQuery.fn.css
$.style($('#h1'), {
'color': '#abc',
'fontSize': '24pt',
'textTransform': 'uppercase'
});
$.style($('#new'), $('#old').style, ['color', 'backgroundColor', 'fontSize']);
You might already know it from jQuery.each
BUT there's one main difference! The callback parameter order is changed in the same way as Array.forEach
! The first callback parameter is the current value. This allows to easily walk through object entries.
Good to know: The loop can be stopped by returning false.
var test = {'a': {'count': 1}, 'b': {'count': 2}, 'c': {'count': 3}, 'd': {'count': 4}, 'e': {'count': 5}};
var sum = 0;
$.each(test, function(entry) {
sum += entry.count;
});
var search = -1;
$.each([1, 2, 3, 4, 5], function(value, index) {
if (value === 4) {
search = index;
return false;
}
});
A convenient method to remove all item ocurrencies from a given array in place.
var A = {'letter': 'a'};
var B = {'letter': 'b'};
var C = {'letter': 'c'};
var lookup = [A, B, C];
// instead of
lookup.splice(lookup.indexOf(B), 1);
// you can do
$.spliceInPlace(lookup, B);
$.isPrimitive(100); // true
$.isPrimitive(new Number(100)); // false
var str = 'string';
var strObject = new String('string');
$.type(str); // String
$.type(strObject); // String
$.isPrimitive(str); // true
$.isPrimitive(strObject); // false!!!
Promise that resolves whenever DOMContentLoaded
is true. The callback gets fired, but you can also use it the Promise way.
$.ready(function init() {
// page is fully loaded, start your app
});
Promise.all([
$.ready(),
fetch('/analytics.php')
]).then(function() {
// work done, start playing
});
Serializing an object. Needed for $.fetch
.
$.params({'foo':'a b c'}); // foo=a%20b%20c
$.params({'data': [1, 2, 3, {'foo': 'bar'}]}); // data%5B0%5D=1&data%5B1%5D=2&data%5B2%5D=3&data%5B3%5D%5Bfoo%5D=bar
$.fetch
is the equivalent of modern javascript fetch
but since it's using XMLHttpRequest under the hood, some goodies are provided.
options Object:
method
(String): GET, POST, PUT, DELETE, OPTIONS, HEAD, whatever is possible; defaults toGET
params
(Object|URLSearchParams): serialized to URL queryheaders
(Object): key value pairs that will getbe set as request headersuser
andpassword
(String): add user and password to host in URL for authenticationcache
(Boolean): enforce a fresh response; appends _=<timestamp>; defaults tofalse
body
(String|Blob|BufferSource|FormData|URLSearchParams|ReadableStream|HTMLFormElement): payload sent as POST or PUTtype
(String):json
; only 'json' is supported;body
can be an Object or JSON String.signal
(AbortSignal): Abort pattern used infetch
; when$.fetch
is aborted it throws aDOMException
with nameAbortError
; Attention: add polyfill for older browsers (e.g. https://polyfill.io/v3/polyfill.min.js?features=AbortController)- all XHR settings like
onload
,onreadystatechange
,onerror
,onabort
,ontimeout
,overrideMimeType
,upload
Object,timeout
,withCredentials
, etc.
resolve holds the xhr
object with xhr.response
as the desired response content.
reject is an Error
object that has status = xhr.status, message = xhr.statusText and the xhr
object set or it is a DOMException
for Network, Timeout, and Abort Errors with the xhr
object.
$.fetch('/data')
.then(function(xhr) {
log(xhr.responseURL); // full absolute URL like https://example.com/data
log(xhr.status, xhr.statusText); // hopefully it's 200 OK
log(xhr.response); // the actual response
log(xhr.getAllResponseHeaders());
log(xhr.getResponseHeader('content-type'));
})
.catch(function(error) {
log(error.status); // same as xhr.status, e.g. 404
log(error.message); // same as xhr.statusText, e.g. Not Found
log(error.xhr); // the full xhr object
log(error.xhr.responseURL);
});
var postData = {foo: 'bar'};
$.fetch('/data', {'method': 'POST', 'body': postData}).then(…).catch(…); // postData object is serialized with $.params()
$.fetch('/data', {'method': 'PUT', 'body': postData}).then(…).catch(…);
$.fetch('/data', {'method': 'POST', 'type': 'json', 'body': postData}).then(…).catch(…);
$.fetch('/data', {'method': 'POST', 'headers': {'content-type': 'application/json'}, 'body': JSON.parse(postData)}).then(…).catch(…); // same as above
var queryParams = {'q': 'dev', 'page': 2};
$.fetch('/search', {'params': queryParams, 'cache': false}).then(…).catch(…); // queryParams object is serialized with $.params() and appended to url, e.g. https://myserver/sarch?q=dev&page=2&_=1586807957258
$.fetch('https://myserver.com/private/data', {'user': 'u', 'password': 'pass'}).then(…).catch(…); // https://u:[email protected]/private/data
$.fetch('/data', {'onload': function(event) { // use xhr.onload as success callback
var response = event.target.response;
}}).then(…).catch(…);
$.fetch('/data', {'onprogress': function(event) { // xhr.onprogress event
var progress = event.loaded / event.total;
log(progress);
}}).then(…).catch(…);
var data = new FormData();
var file = new Blob(['Demo'], {'type': 'text/plain'});
data.append('file', file, 'demo.txt');
$.fetch('/upload', {'method': 'POST', 'body': data, 'upload': {'onprogress': function(event) { // xhr.upload.onprogress event
var uploadProgress = event.loaded / event.total;
log(uploadProgress);
}}});
$('myForm').on('submit', function(event) {
event.preventDefault();
var form = this;
$.fetch('/settings', {'method': 'POST', 'body': form}).then(…).catch(…); // HTMLFormElement will be sent as FormData
});
var controller = new AbortController();
$.fetch('/bigData', {'signal': controller.signal}).then(…).catch(function(error) {
if (error.name === 'AbortError') {
…
}
});
controller.abort();
This is $.fetch
, but the loaded serialized JSON String is parsed. Throws an error if JSON is malformed. options
Object is exactly the same as for $.fetch
.
$.getJSON('/data.json')
.then(function(data) {
log(data.foo);
})
.catch(function(error) {
if (error.name === 'SyntaxError') {
log(error.message); // Unexpected token… or whatever
}
});
// To send and retrieve JSON
var data = {'id': 123, 'username': 'test'};
$.getJSON('/endpoint', {'method': 'POST', 'body': data, 'type': 'json'}).then(…).catch(…);
Loads a script and resolves when script can be used. Optional condition reads: if <condition> then load else skip.
var url = "https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.3.1/es5-sham.min.js";
$.getScript(url, !Array.prototype.forEach).then(…).catch(…);
Gorgious Lea Verou worked this out. See http://lea.verou.me/2016/12/resolve-promises-externally-with-this-one-weird-trick/
var deferred = $.defer();
//
deferred.then(function(done) {
// party!
});
deferred.resolve(true); // parameter is optional
My optimized version of debounced function. Read here why you need this!
Immediate is false
by default. If true
, fn
is called at the first trigger instead of the last.
window.on('resize', $.debounced(updateChart, 250));