/
hmvc.js
288 lines (264 loc) · 9.07 KB
/
hmvc.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
hmvc = (function(){
/**
* An object keeping track of all the components
* registered within the HMVC object.
*
* @type {Object}
*/
var components = {};
/**
* A list of services available to your
* components.
*
* When declaring a component, you can
* inject additional services by specifying
* them in the callback declaration and in
* the "dependencies" array, for example.
*
* hmvc.component('example', function(element, MyService){
* ...
* ...
* ...
* }, ["MyService"]);
*
* Note that you will need to specify the name
* of the dependency twice (kinda ugly-ish, but for now
* it's more than enough).
*
* @type {Object}
*/
var services = {};
/**
* Wrapper for the built-in templating engine of
* lo-dash.
*
* By default, we will use curly brackets rather
* than the opinionated <%= ;-)
*
* This hack is intended to prevent using _.template(...)
* which screams ugliness, and to use a much more
* meaningful variant, templating.render(...).
*/
var templating = (function(){
engine = _;
engine.templateSettings = {
'interpolate': /{{([\s\S]+?)}}/g
};
return {
render: function(content, variables){
return engine.template(content, variables);
}
}
})();
var getDependenciesForComponent = function(component, element) {
var dependencies = [createElement(element)];
components[component].dependencies.forEach(function(name){
dependency = services[name];
if (dependency) {
dependencies.push(dependency)
} else {
throw new Error("HMVC could not find the dependency '{d}' of component '{c}' Are you sure you defined it? Got typo?".replace('{c}', component).replace('{d}', name));
}
});
return dependencies;
}
/**
* Runs the {component} on the specified element,
* by running the component's function and using
* the returned object to manipulate the DOM and/or
* simply executing the run() function.
*
* If the component returns a template in form of a
* string, the templating system will render that
* string and replace the original element with
* it.
*
* @param {String} component
* @param {FancyElement} element
*/
var runComponent = function(component, element) {
try {
result = components[component].callback.apply(this, getDependenciesForComponent(component, element));
} catch (err) {
console.error(err.message)
console.info("Aborting the execution of the component {c}, have fun!".replace('{c}', component))
return;
}
var viewData = result.run() || {};
Q.when(viewData).then(function(data) {
if (result.template && typeof result.template === 'string') {
element.outerHTML = templating.render(result.template, data);
}
result.terminate && result.terminate();
});
}
/**
* Usual DOM manipulation stuff that comes with JavaScript.
*
* In order not to bundle either Sizzle / jQuery / Zepto with
* this library, we implemented some basic utilities that
* resemble angular.element().
*
* @credits http://youmightnotneedjquery.com/
* @param element
* @returns {Object} {{original: original, text: text, html: html, attr: attr, hasClass: hasClass, addClass: addClass, removeClass: removeClass, parent: parent}}
*/
var createElement = function(element) {
return {
original: function() {
return element;
},
text: function() {
return element.innerHTML;
},
html: function(content) {
element.innerHTML = content;
},
attr: function(name, value) {
if (typeof value !== 'undefined') {
element.setAttribute(name, value);
}
return element.getAttribute(name);
},
hasClass: function(name) {
if (element.classList) {
return element.classList.contains(name);
}
else {
return new RegExp('(^| )' + name + '( |$)', 'gi').test(element.className);
}
},
addClass: function(name) {
if (element.classList)
element.classList.add(name);
else {
element.className += ' ' + name;
}
},
removeClass: function(name) {
if (element.classList) {
element.classList.remove(name);
}
else {
element.className = el.className.replace(new RegExp('(^|\\b)' + name.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
}
},
parent: function() {
return createElement(element.parentNode);
}
};
};
/**
* parseUri 1.2.2
* (c) Steven Levithan <stevenlevithan.com>
* MIT License
*
* @see http://blog.stevenlevithan.com/archives/parseuri
* @param str
* @returns {{}}
*/
function parseUri (str) {
var o = parseUri.options,
m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
uri = {},
i = 14;
while (i--) uri[o.key[i]] = m[i] || "";
uri[o.q.name] = {};
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
if ($1) uri[o.q.name][$1] = $2;
});
return uri;
};
parseUri.options = {
strictMode: false,
key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
q: {
name: "queryKey",
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
},
parser: {
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
}
};
services.uri = parseUri(window.location);
/**
* Here starts the fun!
*/
return {
/**
* Runs HMVC: call this function everytime you want
* HMVC to parse your dom and run your components.
*/
run: function() {
for (component in components) {
var selectors = [component, "[" + component + "]", "[data-" + component + "]"];
var elements = document.querySelectorAll(selectors);
var i, length = elements.length;
for (i = 0; i < length; i++) {
runComponent(component, elements[i])
}
}
},
/**
* Declares a new component.
*
* Each component needs to specify it's own
* unique name and a callback function which
* has to return an object made of the following
* properties:
*
* - run, a function that HMVC will run
* everytime it parses the component,
* and should ideally return an object
* that will be passed to the view
*
* - template, a string representing
* the template (view), that will
* replace the original dom of
* your component (ie. <demo>...</demo>)
*
* @see runComponent
* @param {String} name
* @param {Function} callback
* @param {Array} dependencies
*/
component: function(name, callback, dependencies) {
components[name] = {
callback: callback,
dependencies: dependencies || []
};
},
/**
* Takes an HTML element and transforms it into a
* jQuery-style object, but much more lightweight.
*
* This is more of a utility / convenient method.
*
* @see createElement
* @param element
* @returns {Object}
*/
createElement: function(element) {
return createElement(element);
},
/**
* Declares a new service within this hmvc instance.
*
* Services can then be injected to your
* components, by name.
*
* Example:
*
* hmvc.component('abc', function(element, myServiceName){
* console.log(myServiceName) // object
* })
*
* @param {String} name
* @param {Object} object
*/
service: function(name, object) {
services[name] = object;
}
};
})();