README
Ng-Harmony-Component
======================
Concept
A (sample) ES7-proposal (babel-feature) annotation collection mimicking the upcoming Angular 2.0 code-style. Write your own, contribute, and feel like a hero ... it's easy!
Use:
- literate-programming to write markdown-flavored literate JS, HTML and CSS
- jspm for a nice solution to handle npm-modules with ES6-Module-Format-Loading ...
Files
This serves as literate-programming compiler-directive
dist/raw/ng_harmony_component.js
Compilation
To Compile this package please run npm run all
from a proper shell (on Windows there's gitbash!);
Dependencies/Base-Classes
import { Srvc, Ctrl } from "ng-harmony";
import { PowerCtrl } from "ng-harmony-powercontroller";
The DataService Class is a starting point for API-consuming Services, that provide linkable Data-Objects
export class DataService extends Srvc {
constructor(...args) { super(...args); }
Initiate and trigger
_db (api, { name, oneshot, interval }) {
this.name = name;
this.oneshot = oneshot === true || !(interval !== undefined && interval !== null);
this.interval = interval || null;
this.q = this.$q.defer();
if (this.db === undefined || this.db === null) {
this.db = {
busy: false,
ready: false,
handle: (api !== undefined && api !== null) ? this.$resource(api) : null,
store: []
}
}
if (this.db.busy === true) {
this.$timeout(() => {
(this.oneshot === true) ?
this.q.reject() :
this.q.notify(false);
}, 0);
}
if (this.oneshot === false) {
this.q.promise.then(
() => { true; },
(notification) => { this.$timeout(this._api, this.interval); },
() => { false; }
)
}
this._api();
this.q.promise();
}
AJAX Mechanism
_api () {
if (this.db.busy === true) { return null; }
this.db.busy = true;
this.db.handle.get().$promise
.then((data) => {
console.info(`${(new Date()).toLocaleTimeString('en-US')}: API/${this.name}: success`);
this._store(data[this.name] || data);
this.db.busy = false;
if (this.oneshot !== false) {
this.q && this.q.resolve();
this.q = this.$q.defer();
} else { this.q.notify(true); }
}).catch((err) => {
console.warn(`${(new Date()).toLocaleTimeString('en-US')}: API/${this.name}: ${err.toString()}`);
if (this.oneshot !== false) {
this.q && this.q.reject();
this.q = this.$q.defer();
} else {
this.q.notify(false);
}
});
}
Storage Mechanism
_store (data) {
Usually the API returns an Array of n datasets Should your API return an Object instead, it is "arrayified"
let _data = Object.prototype.toString.call(data) === "[object Array]" ? data : [data];
Before we update the existing in-memory database to the newly returned API values we want to clean up all outdated datasets
for (let [i, o] of this.db.store.entries()) {
o.deleted = true;
}
Iterating over the new datasets we
- invalidate the deleted prop for the now newly existing props
- via this mech will not loose special ng-hash-vals and possibly some of your personal special data
for (let [i, o] of this.db.store.entries()) {
let current = null;
if (current = _data.filter((el, i, arr) => { return (el.id === o.id); })[0]) {
for (let [k, v] of this.constructor.iterate(current)) { this.db.store[i][k] = v; }
this.db.store[i].deleted = false;
}
}
for (let [i, o] of _data.entries()) {
let current = null;
if (this.db.store.filter((el, i, arr) => { return el.id === o.id; }).length === 0) {
this.db.store.push(o);
this.db.store[this.db.store.length - 1].deleted = false;
}
}
}
}
DataService.$inject = ["$resource", "$interval", "$q", "$timeout"];
The DynamicDataService provides some convenience mechanisms for cooperation of DataServices and Controllers
- Event Subscription
- Auto Notification on Ajax-Data
- Pre-Configuration of Ajax-Data
- Getter and Setter to the in-memory db
export class DynamicDataService extends DataService {
constructor(...args) { super(...args); }
subscribe (callback, oneshot = false) {
if (this.subscribers === undefined || this.subscribers === null) { this.subscribers = []; }
if (this.once_subscribers === undefined || this.once_subscribers === null) { this.once_subscribers = []; }
if (onehost === true) { this.once_subscribers.push(callback); }
else { this.subscribers.push(callback); }
}
aspects (injection, oneshot = false) {
if (this.aspects === undefined || this.aspects === null) { this.aspects = []; }
if (this.once_aspects === undefined || this.once_aspects === null) { this.once_aspects = []; }
if (onehost === true) { this.once_aspects.push(injection); }
else { this.aspects.push(injection); }
}
getData (matcher) {
return this.db.store.filter((el, i, arr) => {
for (let [k, v] of this.constructor.iterate(matcher)) {
if (!(typeof v === "function" && v(el[k]) || (el[k] === v))) { return false; }
}
return true;
})
}
setData (opts) {
if (!~opts.i) {
for (let [doc, i] of this.db.store.entries()) {
doc[opts.prop] = typeof opts.val === "function" ? opts.val(this.db, doc.id) : opts.val;
}
} else {
let foo = this.db.store[typeof opts.i === "function" ? opts.i(this.db) : opts.i];
if (foo !== undefined && foo !== null) {
foo[opts.prop] = typeof opts.val === "function" ?
opts.val(this.db, this.db.store[
typeof opts.i === "function" ?
opts.i(this.db) :
opts.i
]) :
opts.val
}
}
}
digest () {
if (this.db.ready === false) { return null; }
if (this.db.resolved === undefined || this.db.resolved === null) { this.db.resolved = false; }
for (let [i, once_aspect] of this.once_aspects.entries()) {
typeof once_aspect === "function" && once_aspect(this.db);
this.once_aspects[i] = null;
}
this.once_aspects = [];
for (let [i, aspect] of this.aspects.entries()) { aspect(this.db); }
for (let [i, d] of this.db.store.entries()) {
if (d.deleted === true) { d.selected = false; }
else if (d.selected === true) { this.db.current = d; }
else if (d.selected === undefined || d.selected === null) { d.selected = false; }
}
this.db.resolved = true;
for (let [i, once_cb] of this.once_subscribers.entries()) {
typeof once_cb === "function" && once_cb(this.db);
this.once_subscribers[i] = null;
}
this.once_subscribers = [];
for (let [i, cb] of this.subscribers.entries()) { cb(this.db); }
return true;
}
}
The Component class is building on top of the Controller and taking advantage of the DynamicDataService ... it
- automatically hooks up to all injected DataServices
- provides for a default data-transformation
- provides a default css-driving UI/UX-state mechansim
export class Component extends Ctrl {
constructor (...args) {
super(...args);
Aspect Oriented Feature here. You can actually initialize a db-store (api-result-set) with a value or set values each api-cycle Syntax is:
this.$scope.model = {};
this.transform = [{
descriptor: "Name", //of DataService without the DataService-suffix
init: [
// i is the id ... 0 - length-1 ... or -1 for all datasets
//{ i: 0, prop: "loading", val: false },
//{ i: -1, prop: "selected", val: (db, id) => { return id is 0; } }
],
digest: [
//{ i: -1, prop: "selected", val: (db, id) -> (db.store.find((el, i, arr) => { return el.id is id; }).special is this.$scope.someConditional }
]
}];
The state var is a CSS-state-descriptor/helper
this.$scope.state = {
loading: true,
selected: null,
busy: null,
error: null
};
Injecting the aspects defined in this.transform --- default actions for data transformation
for (let [i, dataset] of this.transform.entries()) {
let Service = this[`${dataset.descriptor[0].toUpperCase()}${dataset.descriptor.substr(1)}DataService`];
for (let [i, rule] of dataset.init.entries()) {
Service.aspects(() => { Service.set(rule) }, true);
}
for (let [i, rule] of dataset.digest.entries()) {
Service.aspects(() => { Service.set(rule) });
}
}
Hooking up the injected DataServices
- the transform member method is automatically called
- it has 2 params(descriptor = Name DataService, db = NameDataService.db.store)
- always call super(descriptor, db) first and return if it returns false -> that's when something didn't work out with the AJAX call and there's nothing to be processed
- it takes care of a special obj-var "current" which reflects the currently "selected = true" dataset
for (let [key, Service] of this.constructor.iterate(this)) {
if (!/DataService/.test(key)) { continue; }
descriptor = key.remove("DataService").toLowerCase();
Service.subscribe(this._transform.bind(this, descriptor));
if (Service.db && Service.db.ready === true) { Service.digest(); }
}
Taking care of the state watchers
- Applying the state descriptors/names as CSS-classes to the container
- Emitting an event that bubbles up to the Routing-Controller and allows for global State-Handling
for (let [k, v] of this.constructor.iterate(this.$scope.state)) {
let className = this.$element.className.split(/\s+/);
let hasClass = !!~className.indexOf(k);
if (v === true && !hasClass) { this.$element.addClass(k); }
((_k, _v, _hasClass, _className) => {
this.$scope.$watch(`state.${_k}`, (after, before) => {
if (after === true && !_hasClass) { this.$element.className += ` ${_k}`; }
else if (_hasClass) { this.$element.className = _className.filter((el, i, arr) => { return el !== _k }).join(" "); }
if (before === after || ((before === undefined || before === null) && (after === undefined || after === null))) { return null; }
this.$scope.$emit(`state.${_k}`, {
obj: this.toString(),
val: after
});
})
})(k, v, hasClass, className);
}
}
The actual default transform function - as described at the hook-up-section
_transform (descriptor, db) {
if (this.$scope.model[descriptor] === undefined || this.$scope.model[descriptor] === null) {
this.$scope.model[descriptor] = this[descriptor[1].toUpperCase() + descriptor.substring(1) + "DataService"].db.store;
}
if (this.$scope.model.current === undefined || this.$scope.model.current === null) {
this.$scope.model.current = {};
}
this.$scope.model.current[descriptor] = db.current || null;
if (this.$scope.model[descriptor] === undefined || this.$scope.model[descriptor] === null || this.$scope.model[descriptor].length === 0) {
console.warn(`${this.toString()}::_transform: the dataset of ${descriptor} was empty`)
return false;
} else { return true; }
}
}
Component.$inject = "$element";
An enhanced Component-Variation, enabling the PowerControllers Eventing in the already powerful Ctrl
export class PowerComponent extends Component.mixin(PowerCtrl) {}
CHANGELOG
0.1.1: PowerComponent: ComponentCtrl with Eventing-Capabilities of the PowerCtrl