Created
November 8, 2015 21:00
-
-
Save imposibrus/2572942bc0d263907de3 to your computer and use it in GitHub Desktop.
JS ActiveRecord
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Dispatcher, {EventObject} from "Dispatcher"; | |
/** | |
* Items collection | |
*/ | |
export default class Collection { | |
static E_ADD = 'add'; | |
static E_REMOVE = 'remove'; | |
static E_CHANGE = 'change'; | |
/** | |
* @param elements | |
* @param dispatcher | |
*/ | |
constructor(elements = [], dispatcher:Dispatcher = null) { | |
dispatcher = dispatcher || new Dispatcher(); | |
Object.defineProperty(this, 'events', { get: () => dispatcher }); | |
Object.defineProperty(this, 'elements', { get: () => elements }); | |
this.events.listen(this.constructor.E_ADD, () => { | |
this.events.fire(this.constructor.E_CHANGE, this.elements); | |
}); | |
this.events.listen(this.constructor.E_REMOVE, () => { | |
this.events.fire(this.constructor.E_CHANGE, this.elements); | |
}); | |
for (var i = 0; i < elements.length; i++) { | |
this.push(elements[i]); | |
} | |
} | |
/** | |
* @param event | |
* @param callback | |
* @returns {EventObject} | |
*/ | |
on(event, callback:Function) { | |
return this.events.listen(event, callback); | |
} | |
/** | |
* @param event | |
* @returns {Dispatcher} | |
*/ | |
off(event:EventObject) { | |
return event.remove(); | |
} | |
/** | |
* @param event | |
* @param callback | |
* @returns {Event} | |
*/ | |
once(event, callback:Function) { | |
return this.on(event, callback).once(); | |
} | |
/** | |
* @param item | |
* @returns {Collection} | |
*/ | |
push(item) { | |
this.elements.push(item); | |
this.events.fire(this.constructor.E_ADD, item); | |
return this; | |
} | |
/** | |
* @param item | |
* @returns {Collection} | |
*/ | |
unshift(item) { | |
this.elements.unshift(item); | |
this.events.fire(this.constructor.E_ADD, item); | |
return this; | |
} | |
/** | |
* @returns {Collection} | |
*/ | |
pop() { | |
var item = this.elements.pop(); | |
this.events.fire(this.constructor.E_REMOVE, item); | |
return item; | |
} | |
/** | |
* @returns {Collection} | |
*/ | |
shift() { | |
var item = this.elements.shift(); | |
this.events.fire(this.constructor.E_REMOVE, item); | |
return item; | |
} | |
/** | |
* @param callback | |
* @returns {Collection} | |
*/ | |
remove(callback:Function) { | |
var items = []; | |
for (var i = 0; i < this.elements.length; i++) { | |
if (!callback(this.elements[i])) { | |
items.push(this.elements[i]); | |
} | |
} | |
return new this.constructor(items); | |
} | |
/** | |
* @param callback | |
* @returns {Collection} | |
*/ | |
find(callback:Function) { | |
var items = []; | |
for (var i = 0; i < this.elements.length; i++) { | |
if (callback(this.elements[i])) { | |
items.push(this.elements[i]); | |
} | |
} | |
return new this.constructor(items); | |
} | |
/** | |
* @param key | |
* @param op | |
* @param value | |
* @returns {Collection} | |
*/ | |
where(key, op, value) { | |
if (typeof op === 'undefined') { | |
op = '='; | |
value = true; | |
} else if (typeof value === 'undefined') { | |
value = op; | |
op = '='; | |
} | |
return this.find(item => { | |
switch (op) { | |
case '>': return item[key] > value; | |
case '<': return item[key] < value; | |
case '>=': return item[key] >= value; | |
case '<=': return item[key] <= value; | |
case '<>': | |
case '!=': return item[key] != value; | |
default: return item[key] == value; | |
} | |
}); | |
} | |
/** | |
* @param callback | |
* @param order | |
* @return {Collection} | |
*/ | |
sort(callback:Function, order = 1) { | |
switch (order.toString().toLowerCase()) { | |
case 'asc': | |
order = 1; | |
break; | |
case 'desc': | |
order = -1; | |
break; | |
} | |
order = order > 0 ? 1 : -1; | |
return new this.constructor(this.elements.sort((a, b) => { | |
a = callback(a); | |
b = callback(b); | |
if (a === b) { | |
return 0; | |
} | |
return a > b ? order : -order; | |
})); | |
} | |
/** | |
* @param callback | |
* @returns {Collection} | |
*/ | |
each(callback:Function) { | |
for (var i = 0; i < this.elements.length; i++) { | |
callback(this.elements[i]); | |
} | |
return this; | |
} | |
/** | |
* @param callback | |
* @returns {Collection} | |
*/ | |
map(callback:Function) { | |
return new this.constructor(this.elements.map(item => callback(item))); | |
} | |
/** | |
* @param delimiter | |
* @param property | |
* @returns {*} | |
*/ | |
join(delimiter = ', ', property = null) { | |
if (this.length > 0) { | |
var items = this.elements.map(item => { | |
if (property) { | |
return item[property] ? item[property].toString() : ''; | |
} | |
return item.toString(); | |
}); | |
return items.join(delimiter); | |
} | |
return ''; | |
} | |
/** | |
* @param count | |
* @returns {Collection} | |
*/ | |
take(count = 1) { | |
return new this.constructor(this.elements.slice(0, count)); | |
} | |
/** | |
* @param field | |
* @param order | |
* @returns {Collection} | |
*/ | |
orderBy(field, order = 1) { | |
return this.sort(item => item[field], order); | |
} | |
/** | |
* @param shift | |
* @returns {*} | |
*/ | |
first(shift = 0) { | |
if (this.elements.length > 0) { | |
return this.elements.slice(shift, 1)[0]; | |
} | |
return null; | |
} | |
/** | |
* @returns {Collection} | |
*/ | |
clear() { | |
this.elements = []; | |
return this; | |
} | |
/** | |
* @returns {Collection} | |
*/ | |
clone() { | |
return new this.constructor(this.all()); | |
} | |
/** | |
* @returns {Array} | |
*/ | |
all() { | |
return this.elements.slice(0); | |
} | |
/** | |
* @param target | |
* @returns {Array} | |
*/ | |
toArray(target = null) { | |
var result = this.all(); | |
if (target instanceof Function) { | |
target(result); | |
} | |
return result; | |
} | |
/** | |
* @returns {Number} | |
*/ | |
get length() { | |
return this.elements.length; | |
} | |
/** | |
* @returns {Generator} | |
*/ | |
*[Symbol.iterator]() { | |
for (var i = 0; i < this.elements.length; i++) { | |
yield this.elements[i]; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Event | |
*/ | |
export class EventObject { | |
/** | |
* @type {number} | |
*/ | |
static id = 0; | |
/** | |
* @type {number} | |
*/ | |
$id = 0; | |
/** | |
* @type {Dispatcher} | |
*/ | |
$dispatcher = null; | |
/** | |
* @type {String} | |
*/ | |
$name = null; | |
/** | |
* @type {Function} | |
*/ | |
$callback = null; | |
/** | |
* @type {boolean} | |
*/ | |
$once = false; | |
/** | |
* @param {Dispatcher} dispatcher | |
* @param {String} name | |
* @param {Function} callback | |
*/ | |
constructor(dispatcher:Dispatcher, name:String, callback:Function) { | |
this.$id = this.constructor.id++; | |
this.$name = name; | |
this.$dispatcher = dispatcher; | |
this.$callback = callback; | |
} | |
/** | |
* @returns {number} | |
*/ | |
get id() { | |
return this.$id; | |
} | |
/** | |
* @returns {String} | |
*/ | |
get name() { | |
return this.$name; | |
} | |
/** | |
* @param once | |
* @returns {Event} | |
*/ | |
once(once = true) { | |
this.$once = once; | |
return this; | |
} | |
/** | |
* @param args | |
* @returns {*} | |
*/ | |
fire(...args) { | |
var result = this.$callback(...args); | |
if (this.$once) { | |
this.remove(); | |
} | |
return result; | |
} | |
/** | |
* @returns {Dispatcher} | |
*/ | |
remove() { | |
var handlers = this.$dispatcher.getHandlers(this.$name); | |
for (var i = 0; i < handlers.length; i++) { | |
if (handlers[i].$id === this.$id) { | |
handlers.splice(i, 1); | |
break; | |
} | |
} | |
return this.$dispatcher; | |
} | |
} | |
/** | |
* Event Dispatcher | |
*/ | |
export default class Dispatcher { | |
/** | |
* @constructor | |
*/ | |
constructor() { | |
this.events = {}; | |
} | |
/** | |
* @param name | |
* @param callback | |
* @returns {EventObject} | |
*/ | |
listen(name:String, callback:Function) { | |
var event = new EventObject(this, name, callback); | |
this.getHandlers(name).push(event); | |
return event; | |
} | |
/** | |
* @param name | |
* @param callback | |
* @returns {Event} | |
*/ | |
once(name:String, callback:Function) { | |
return this.listen(name, callback).once(); | |
} | |
/** | |
* @param name | |
* @param args | |
* @returns {Array} | |
*/ | |
fire(name:String, ...args) { | |
var result = []; | |
var handlers = this.getHandlers(name); | |
for (var i = 0; i < handlers.length; i++) { | |
result.push(handlers[i].fire(...args)); | |
} | |
return result; | |
} | |
/** | |
* @param name | |
* @returns {*} | |
*/ | |
getHandlers(name) { | |
if (!this.events[name]) { | |
this.events[name] = []; | |
} | |
return this.events[name]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Dispatcher from "Dispatcher"; | |
import Collection from "Collection"; | |
/** | |
* Model | |
*/ | |
export default class Model { | |
/** | |
* @type {WeakMap} | |
*/ | |
static $collections = new WeakMap(); | |
/** | |
* @returns {Collection} | |
*/ | |
static get collection() { | |
if (!this.$collections.has(this)) { | |
this.$collections.set(this, new Collection([])); | |
// Import collection methods | |
for (let method in this.$collections.get(this)) { | |
if (method instanceof Function && typeof this[method] == 'undefined') { | |
Object.defineProperty(this, method, { | |
enumerable: true, | |
get: () => this.collection[method] | |
}) | |
} | |
} | |
} | |
return this.$collections.get(this); | |
} | |
/** | |
* @type {WeakMap} | |
*/ | |
static $dispatchers = new WeakMap(); | |
/** | |
* @returns {Dispatcher} | |
*/ | |
static get dispatcher() { | |
if (!this.$dispatchers.has(this)) { | |
this.$dispatchers.set(this, new Dispatcher); | |
} | |
return this.$dispatchers.get(this); | |
} | |
/** | |
* @param event | |
* @param callback | |
* @returns {EventObject} | |
*/ | |
static on(event, callback: Function) { | |
return this.dispatcher.listen(event, callback); | |
} | |
/** | |
* @type {WeakMap} | |
*/ | |
static $booted = new WeakMap(); | |
/** | |
* @returns {boolean} | |
*/ | |
static get booted() { | |
if (!this.$booted.has(this)) { | |
this.$booted.set(this, false); | |
} | |
return this.$booted.get(this); | |
} | |
/** | |
* @param {boolean} value | |
*/ | |
static set booted(value) { | |
this.$booted.set(this, value); | |
} | |
/** | |
* @returns {Model} | |
*/ | |
static bootIfNotBooted() { | |
if (!this.booted) { | |
this.constructor(); | |
} | |
return this; | |
} | |
/** | |
* @return void | |
*/ | |
static constructor() { | |
// Do nothing | |
} | |
/** | |
* @param attributes | |
* @returns {Model} | |
*/ | |
static create(attributes = {}) { | |
var model = new this(attributes); | |
this.dispatcher.fire('creating', model); | |
this.collection.push(model); | |
this.dispatcher.fire('created', model); | |
return this; | |
} | |
// ================ INSTANCE ================ // | |
/** | |
* @type {{}} | |
*/ | |
original = {}; | |
/** | |
* @type {{}} | |
*/ | |
attributes = {}; | |
/** | |
* @type {{}} | |
*/ | |
updated = {}; | |
/** | |
* @param attributes | |
*/ | |
constructor(attributes = {}) { | |
this.constructor.bootIfNotBooted(); | |
this.fill(attributes); | |
} | |
/** | |
* @param attributes | |
*/ | |
fill(attributes = {}) { | |
this.original = this.attributes = attributes; | |
for (let attribute in attributes) { | |
if (typeof this[attribute] == 'undefined') { | |
Object.defineProperty(this, attribute, { | |
enumerable: true, | |
get: () => this.get(attribute), | |
set: value => this.set(attribute, value) | |
}) | |
} | |
} | |
} | |
/** | |
* @returns {Model} | |
*/ | |
reset() { | |
this.attributes = this.original; | |
return this; | |
} | |
/** | |
* @returns {{}} | |
*/ | |
save() { | |
this.constructor.dispatcher.fire('saving', this); | |
this.original = this.attributes; | |
var fields = this.updated; | |
this.updated = {}; | |
if (!this.saved()) { | |
this.constructor.collection.push(this); | |
} | |
this.constructor.dispatcher.fire('saved', this); | |
return fields; | |
} | |
/** | |
* @returns {boolean} | |
*/ | |
saved() { | |
return this.constructor.collection | |
.find(item => this === item) | |
.length > 0; | |
} | |
/** | |
* @returns {Collection} | |
*/ | |
remove() { | |
this.constructor.dispatcher.fire('deleting', this); | |
var result = this.constructor.collection | |
.remove(item => item === this); | |
this.constructor.dispatcher.fire('deleted', this); | |
return result; | |
} | |
/** | |
* @returns {boolean} | |
*/ | |
dirty() { | |
return Object.keys(this.updated).length > 0; | |
} | |
/** | |
* @param attribute | |
* @returns {*} | |
*/ | |
get(attribute) { | |
if (this.has(attribute)) { | |
return this.attributes[attribute]; | |
} | |
return null; | |
} | |
/** | |
* @param attribute | |
* @param value | |
* @returns {Model} | |
*/ | |
set(attribute, value) { | |
if (this.has(attribute)) { | |
this.attributes[attribute] = value; | |
this.updated[attribute] = value; | |
} | |
return this; | |
} | |
/** | |
* @param attribute | |
* @returns {boolean} | |
*/ | |
has(attribute) { | |
return !!this.attributes[attribute]; | |
} | |
/** | |
* @returns {string} | |
*/ | |
toJson() { | |
return JSON.stringify(this.attributes); | |
} | |
/** | |
* @returns {Generator} | |
*/ | |
*[Symbol.iterator]() { | |
yield this.constructor.collection[Symbol.iterator]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment