22 changed files with 996 additions and 37 deletions
@ -1,2 +1,4 @@ |
|||
//= link_tree ../images
|
|||
//= link_directory ../stylesheets .css
|
|||
//= link_tree ../../javascript .js
|
|||
//= link_tree ../../../vendor/javascript .js
|
|||
@ -1,9 +1,16 @@ |
|||
|
|||
// app/javascript/application.js
|
|||
|
|||
import "@hotwired/turbo-rails" |
|||
import * as ActiveStorage from "@rails/activestorage" |
|||
import "channels" |
|||
|
|||
import "jquery" |
|||
import "jquery_ujs" |
|||
import "./jquery_ui" |
|||
import { Application } from "@hotwired/stimulus" |
|||
import { definitionsFromContext } from "@hotwired/stimulus-loading" |
|||
|
|||
const application = Application.start() |
|||
const context = require.context("controllers", true, /\.js$/) |
|||
application.load(definitionsFromContext(context)) |
|||
|
|||
import $ from "jquery" |
|||
window.$ = $ |
|||
window.jQuery = $ |
|||
|
|||
console.log("🚀 application.js geladen") |
|||
@ -1,8 +1,10 @@ |
|||
|
|||
import "@hotwired/turbo-rails" |
|||
import * as ActiveStorage from "@rails/activestorage" |
|||
import "channels" |
|||
import { application } from "controllers/application" |
|||
|
|||
import $ from "jquery" |
|||
import "bootstrap" |
|||
|
|||
window.$ = $ |
|||
window.jQuery = $ |
|||
|
|||
import "jquery" |
|||
import "jquery_ujs" |
|||
import "./jquery_ui" |
|||
console.log("🚀 application.js geladen") |
|||
@ -0,0 +1,8 @@ |
|||
import { Application } from "@hotwired/stimulus" |
|||
import { definitionsFromContext } from "@hotwired/stimulus-loading" |
|||
|
|||
const application = Application.start() |
|||
const context = require.context("./", true, /\.js$/) |
|||
application.load(definitionsFromContext(context)) |
|||
|
|||
export { application } |
|||
@ -0,0 +1,7 @@ |
|||
import { Controller } from "@hotwired/stimulus" |
|||
|
|||
export default class extends Controller { |
|||
connect() { |
|||
this.element.textContent = "Hello World!" |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
// Import and register all your controllers from the importmap via controllers/**/*_controller
|
|||
import { application } from "controllers/application" |
|||
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" |
|||
eagerLoadControllersFrom("controllers", application) |
|||
@ -0,0 +1,4 @@ |
|||
#!/usr/bin/env ruby |
|||
|
|||
require_relative "../config/application" |
|||
require "importmap/commands" |
|||
@ -1,3 +1,19 @@ |
|||
pin "jquery", to: "jquery.min.js", preload: true |
|||
pin "jquery_ujs", to: "jquery_ujs.js", preload: true |
|||
pin "jquery-ui", to: "jquery-ui.min.js", preload: true |
|||
pin "application" |
|||
|
|||
# Hotwire: Turbo + Stimulus |
|||
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true |
|||
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true |
|||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true |
|||
|
|||
# Stimulus-Controller automatisch laden |
|||
pin_all_from "app/javascript/controllers", under: "controllers" |
|||
|
|||
# jQuery (über JSPM) |
|||
pin "jquery", to: "https://ga.jspm.io/npm:jquery@3.7.1/dist/jquery.js" |
|||
|
|||
# Bootstrap 5 ESM-Version (inkl. Popper) |
|||
pin "bootstrap", to: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.esm.min.js" |
|||
|
|||
# Chart.js + interne Abhängigkeit |
|||
pin "chart.js", to: "https://ga.jspm.io/npm:chart.js@4.4.1/dist/chart.js" |
|||
pin "@kurkle/color", to: "https://ga.jspm.io/npm:@kurkle/color@0.3.2/dist/color.esm.js" |
|||
@ -0,0 +1,16 @@ |
|||
{ |
|||
"name": "praktikum", |
|||
"version": "1.0.0", |
|||
"lockfileVersion": 3, |
|||
"requires": true, |
|||
"packages": { |
|||
"node_modules/controllers": { |
|||
"version": "0.0.2", |
|||
"resolved": "https://registry.npmjs.org/controllers/-/controllers-0.0.2.tgz", |
|||
"integrity": "sha512-Xhe4m8rr1reyM9jHIxaJQMR/P7ItoXvYhqFATht/5XMWcuONaFbYUNnV7/6WxR58uc+d4OHU9rLuKYK+5KUopw==", |
|||
"engines": { |
|||
"node": "*" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
node_modules/* |
|||
@ -0,0 +1,21 @@ |
|||
fs = require 'fs' |
|||
{print} = require 'util' |
|||
{spawn, exec} = require 'child_process' |
|||
|
|||
build = (watch, callback) -> |
|||
if typeof watch is 'function' |
|||
callback = watch |
|||
watch = false |
|||
options = ['-c', '-o', 'lib', 'src'] |
|||
options.unshift '-w' if watch |
|||
|
|||
coffee = spawn 'coffee', options |
|||
coffee.stdout.on 'data', (data) -> print data.toString() |
|||
coffee.stderr.on 'data', (data) -> print data.toString() |
|||
coffee.on 'exit', (status) -> callback?() if status is 0 |
|||
|
|||
task 'build', 'Compile CoffeeScript source files', -> |
|||
build() |
|||
|
|||
task 'watch', 'Recompile CoffeeScript source files when modified', -> |
|||
build true |
|||
@ -0,0 +1,127 @@ |
|||
# Controllers |
|||
|
|||
A simple mvc framework and route extender for Express. |
|||
|
|||
### Installation |
|||
|
|||
```bash |
|||
$ npm install controllers |
|||
``` |
|||
|
|||
### Usage |
|||
|
|||
After setting all your middleware in Express, call the controllers method to initialise. |
|||
|
|||
``` |
|||
express = require 'express' |
|||
controllers = require 'controllers' |
|||
|
|||
app = express.createServer() |
|||
app.use(express.static(__dirname + '/public')); |
|||
|
|||
# Make sure all your app.use statements have been called |
|||
controllers app, options |
|||
``` |
|||
|
|||
Your folder system should now look like the following: |
|||
|
|||
``` |
|||
site |
|||
|-> controllers |
|||
| |-> home.js (or coffee, if you have overwritten the require calls) |
|||
| |-> blog.js |
|||
|-> views |
|||
| -> home |
|||
| |-> index.jade (these are your views, use whatever renderer you want) |
|||
| |-> welcome.jade |
|||
| -> blog |
|||
| | -> index.jade |
|||
| -> shared |
|||
| -> layout.jade |
|||
``` |
|||
|
|||
Controllers are called depending on your routing, and the render call is overwritten to access the folder with the same name as the controller, falling back to the shared folder if needed. |
|||
|
|||
### Routing |
|||
|
|||
When routing a controller and action must be defined, controllers extends the routing in Express to allow for default values |
|||
|
|||
``` |
|||
# app.get 'route', defaults, middleware... |
|||
app.get '/blogPage', { controller: 'blog', action: 'index' }, middleware |
|||
app.get '/:controller?/:action?/:id?', { controller: 'home', action: 'index' }, middleware |
|||
``` |
|||
|
|||
The above routing will route the following paths: |
|||
|
|||
``` |
|||
'/' -> Routes to the controller 'home' and runs the method 'index' |
|||
'/blogPage' -> Routes to the controller 'blog' and runs the method 'index' |
|||
'/home/welcome/1' -> Routes to the controller 'home' and runs the method 'welcome', with the 'id' param set to 1 |
|||
``` |
|||
### What does a controller look like? |
|||
|
|||
The controller actions follow the normal convention of Express, taking the request, response and next arguments: |
|||
|
|||
``` |
|||
module.exports.index = (req, res, next) -> |
|||
res.render() |
|||
|
|||
module.exports.welcome = (req, res, next) -> |
|||
id = req.param.id ?? 0 |
|||
res.partial { id: id } |
|||
``` |
|||
|
|||
The render does not take an argument as the view for this action is automatically searched for in at 'views/home/index' and if that fails falls back to 'views/shared/index'. |
|||
|
|||
### Helpers |
|||
|
|||
There are a number of useful calls available in the controllers and views. |
|||
|
|||
Controllers: |
|||
|
|||
``` |
|||
req.controller # stores current controller |
|||
req.action # stores current action |
|||
req.executeController 'controller', 'action', cb # Executes another controller and overwrites the next function with the cb |
|||
``` |
|||
|
|||
Views: |
|||
|
|||
``` |
|||
controller # stores current controller |
|||
action # stores current action |
|||
getUrl 'controller', 'action', defaultParams, queryParams # returns a url corresponding to the controller/action specified |
|||
getUrl 'action', defaultParams, queryParams # same as above but using the current controller |
|||
``` |
|||
|
|||
### Options |
|||
|
|||
The default options are: |
|||
|
|||
``` |
|||
# If the controller/action is not defined do we throw an exception? |
|||
strict: true |
|||
|
|||
# Overwrite the render/partial calls to use the 'controller/action' breakdown of the views |
|||
overwriteRender: true |
|||
|
|||
# Log when the controllers are loaded and called |
|||
log: false |
|||
|
|||
# Set the root folder for the controllers |
|||
root: app.set('controllers') || process.cwd() + '/controllers' |
|||
|
|||
# Set the share folder in the views, all render/partial calls will fall back to this folder |
|||
sharedFolder: 'shared' |
|||
``` |
|||
|
|||
### License |
|||
|
|||
©2012 Felix Jorkowski and available under the [MIT license](http://www.opensource.org/licenses/mit-license.php): |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
@ -0,0 +1,343 @@ |
|||
(function() { |
|||
var Controllers, fs, path; |
|||
var __slice = Array.prototype.slice; |
|||
|
|||
fs = require('fs'); |
|||
|
|||
path = require('path'); |
|||
|
|||
module.exports = function(app, options) { |
|||
var _ref, _ref2, _ref3, _ref4, _ref5; |
|||
if (options == null) options = {}; |
|||
if ((_ref = options.strict) == null) options.strict = true; |
|||
if ((_ref2 = options.overwriteRender) == null) options.overwriteRender = true; |
|||
if ((_ref3 = options.log) == null) options.log = false; |
|||
if ((_ref4 = options.root) == null) { |
|||
options.root = app.set('controllers') || process.cwd() + '/controllers'; |
|||
} |
|||
if ((_ref5 = options.sharedFolder) == null) options.sharedFolder = 'shared'; |
|||
return new Controllers(app, options); |
|||
}; |
|||
|
|||
Controllers = (function() { |
|||
|
|||
function Controllers(app, options) { |
|||
var originalRoute, self; |
|||
this.options = options; |
|||
self = this; |
|||
this._controllers = {}; |
|||
this.executeOnDirectory(this.options.root, function(file) { |
|||
var controller, ext, reduced; |
|||
ext = path.extname(file); |
|||
if (ext === '.js' || ext === '.coffee') { |
|||
reduced = file.replace(ext, ''); |
|||
controller = path.basename(reduced); |
|||
self._controllers[controller] = require(reduced); |
|||
if (self.options.log) { |
|||
return console.log("Controller '" + controller + "' has been loaded"); |
|||
} |
|||
} |
|||
}); |
|||
originalRoute = app.routes._route; |
|||
app.routes._route = function() { |
|||
var c, callbacks, defaults, defkey, defvalue, holder, key, method, newCallbacks, newRoute, path, result, _i, _j, _len, _len2, _ref; |
|||
method = arguments[0], path = arguments[1], defaults = arguments[2], callbacks = 4 <= arguments.length ? __slice.call(arguments, 3) : []; |
|||
if ('function' === typeof defaults) { |
|||
callbacks.push(defaults); |
|||
defaults = null; |
|||
} |
|||
if (callbacks.length === 0) callbacks.push(function(req, res) {}); |
|||
if (defaults == null) defaults = {}; |
|||
holder = {}; |
|||
for (_i = 0, _len = callbacks.length; _i < _len; _i++) { |
|||
c = callbacks[_i]; |
|||
newCallbacks = self.overwriteCallback(c, holder); |
|||
} |
|||
result = originalRoute.call(app.routes, method, path, newCallbacks); |
|||
holder.route = newRoute = result.routes[method][result.routes[method].length - 1]; |
|||
for (defkey in defaults) { |
|||
defvalue = defaults[defkey]; |
|||
key = self.getKeyInRoute(defkey, newRoute); |
|||
if (key != null) { |
|||
key["default"] = defvalue; |
|||
} else { |
|||
if (defkey === 'controller' || defkey === 'action') { |
|||
newRoute[defkey] = defvalue; |
|||
} |
|||
} |
|||
} |
|||
_ref = newRoute.keys; |
|||
for (_j = 0, _len2 = _ref.length; _j < _len2; _j++) { |
|||
key = _ref[_j]; |
|||
if (key.name === 'controller' || key.name === 'action') { |
|||
newRoute[key.name] = '*'; |
|||
} |
|||
} |
|||
return result; |
|||
}; |
|||
this.addHelpers(app); |
|||
} |
|||
|
|||
Controllers.prototype.addReqHelpers = function(req, res) { |
|||
var self; |
|||
self = this; |
|||
return req.executeController = function(controller, action, next) { |
|||
var currentA, currentC, nextFunc; |
|||
if (!(controller != null) || !(action != null)) { |
|||
throw new Error("executeController needs the controller and action specified"); |
|||
} |
|||
if (next != null) { |
|||
currentC = req.controller; |
|||
currentA = req.action; |
|||
nextFunc = next; |
|||
next = function() { |
|||
req.controller = currentC; |
|||
req.action = currentA; |
|||
return nextFunc.apply(this, arguments); |
|||
}; |
|||
} |
|||
req.controller = controller; |
|||
req.action = action; |
|||
return self._controllers[controller][action](req, res, next); |
|||
}; |
|||
}; |
|||
|
|||
Controllers.prototype.addHelpers = function(app) { |
|||
var self; |
|||
self = this; |
|||
return app.dynamicHelpers({ |
|||
controller: function(req, res) { |
|||
return req.controller; |
|||
}, |
|||
action: function(req, res) { |
|||
return req.action; |
|||
}, |
|||
getUrl: function(req, res) { |
|||
return function(controller, action, other, query) { |
|||
var def, first, hasReplaced, i, key, regExp, replacement, result, route, value, _i, _len, _ref, _ref2, _ref3, _ref4; |
|||
if (!(action != null) || 'object' === typeof action) { |
|||
query = other; |
|||
other = action; |
|||
action = controller; |
|||
controller = null; |
|||
} |
|||
if (controller == null) controller = req.controller; |
|||
if (other == null) other = {}; |
|||
other.controller = controller; |
|||
other.action = action; |
|||
if (query == null) query = {}; |
|||
if (!(action != null) || !(controller != null)) { |
|||
throw new Error("getUrl needs at minimum an action defined, but also takes a controller"); |
|||
} |
|||
_ref = app.routes.routes.get; |
|||
for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
|||
route = _ref[_i]; |
|||
if (self.isMatchingPath(other, route)) { |
|||
hasReplaced = false; |
|||
result = route.path; |
|||
for (i = _ref2 = route.keys.length - 1; _ref2 <= 0 ? i <= 0 : i >= 0; _ref2 <= 0 ? i++ : i--) { |
|||
key = route.keys[i]; |
|||
def = (_ref3 = key["default"]) != null ? _ref3 : ''; |
|||
replacement = (_ref4 = other[key.name]) != null ? _ref4 : def; |
|||
if (hasReplaced && replacement === '') { |
|||
throw new Error("The optional parameter '" + key.name + "' is required for this getUrl call as an parameter further down the path has been specified"); |
|||
} else { |
|||
if (!hasReplaced) { |
|||
if ((!key.optional || replacement !== def) && (hasReplaced = true)) {} else { |
|||
replacement = ''; |
|||
} |
|||
} |
|||
} |
|||
regExp = new RegExp(":" + key.name + "(\\?)?"); |
|||
result = result.replace(regExp, replacement); |
|||
} |
|||
result = result.replace(/\/+/g, '/'); |
|||
if (result !== '/') result = result.replace(/\/+$/, ''); |
|||
first = true; |
|||
for (key in query) { |
|||
value = query[key]; |
|||
if (first) { |
|||
first = false; |
|||
result = result + '?' + key; |
|||
if ((value != null) && value !== '') { |
|||
result = result + '=' + value; |
|||
} |
|||
} else { |
|||
result = result + '&' + key; |
|||
if ((value != null) && value !== '') { |
|||
result = result + '=' + value; |
|||
} |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
} |
|||
throw new Error("Route could not be found that matches getUrl parameters, make sure to specify a valid controller, action and required parameters"); |
|||
}; |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
Controllers.prototype.getKeyInRoute = function(name, route) { |
|||
var key, _i, _len, _ref; |
|||
_ref = route.keys; |
|||
for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
|||
key = _ref[_i]; |
|||
if (key.name === name) return key; |
|||
} |
|||
return null; |
|||
}; |
|||
|
|||
Controllers.prototype.isMatchingPath = function(object, route) { |
|||
var key, value, _i, _len, _ref; |
|||
if (route.controller !== '*' && route.controller !== object.controller) { |
|||
return false; |
|||
} |
|||
if (route.action !== '*' && route.action !== object.action) return false; |
|||
for (key in object) { |
|||
value = object[key]; |
|||
if (key !== 'controller' && key !== 'action') { |
|||
if (!((this.getKeyInRoute(key, route)) != null)) return false; |
|||
} |
|||
} |
|||
_ref = route.keys; |
|||
for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
|||
key = _ref[_i]; |
|||
if (key.name !== 'controller' && key.name !== 'action') { |
|||
if (!key.optional && !(object[key] != null)) return false; |
|||
} |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
Controllers.prototype.overwriteCallback = function(callback, routeHolder) { |
|||
var options, self; |
|||
self = this; |
|||
options = this.options; |
|||
return function(req, resp, next) { |
|||
var action, controller, key, route, _i, _len, _ref, _ref2, _ref3; |
|||
callback(req, resp, next); |
|||
self.addReqHelpers(req, resp); |
|||
route = routeHolder.route; |
|||
_ref = route.keys; |
|||
for (_i = 0, _len = _ref.length; _i < _len; _i++) { |
|||
key = _ref[_i]; |
|||
if (!(req.params[key.name] != null) && (key["default"] != null)) { |
|||
req.params[key.name] = key["default"]; |
|||
} |
|||
} |
|||
req.controller = (_ref2 = req.params.controller) != null ? _ref2 : route.controller; |
|||
req.action = (_ref3 = req.params.action) != null ? _ref3 : route.action; |
|||
if (options.log) { |
|||
console.log('Controller: ' + req.controller); |
|||
console.log('Action: ' + req.action); |
|||
} |
|||
if (options.strict) { |
|||
if (!(req.controller != null)) { |
|||
throw new Error("Is in strict mode and no controller specified"); |
|||
} |
|||
if (!(req.action != null)) { |
|||
throw new Error("Is in strict mode and no action specified"); |
|||
} |
|||
} |
|||
if ((req.controller != null) && (req.action != null)) { |
|||
if (options.overwriteRender) self.overwriteRender(req, resp); |
|||
controller = self._controllers[req.controller]; |
|||
if (!(controller != null)) { |
|||
if (options.log) { |
|||
console.log("Controller '" + req.controller + "' could not be found"); |
|||
} |
|||
next('route'); |
|||
return; |
|||
} |
|||
action = controller[req.action]; |
|||
if (!(action != null)) { |
|||
if (options.log) { |
|||
console.log("Action '" + req.action + "' could not be found on controller '" + req.controller + "' "); |
|||
} |
|||
next('route'); |
|||
return; |
|||
} |
|||
return action(req, resp, next); |
|||
} else { |
|||
if (options.log) { |
|||
return console.log('Controller or action was not specified, no action was called'); |
|||
} |
|||
} |
|||
}; |
|||
}; |
|||
|
|||
Controllers.prototype.overwriteRender = function(req, resp) { |
|||
var original, root, self; |
|||
self = this; |
|||
original = resp.render; |
|||
root = resp.app.set('views') || process.cwd() + '/views'; |
|||
return resp.render = function(view, opts, fn, parent, sub) { |
|||
var finalPass, hasHints, reset, result, secondRender, secondResult; |
|||
if ('object' === typeof view || 'function' === typeof view) { |
|||
sub = parent; |
|||
parent = fn; |
|||
fn = opts; |
|||
opts = view; |
|||
view = null; |
|||
} |
|||
if (view == null) view = req.action; |
|||
hasHints = resp.app.enabled('hints'); |
|||
resp.app.disable('hints'); |
|||
result = null; |
|||
secondResult = null; |
|||
reset = function() { |
|||
if (hasHints) return resp.app.enable('hints'); |
|||
}; |
|||
finalPass = function(err, err2, str) { |
|||
reset(); |
|||
if (err != null) err = err + '\r\n\r\n' + err2; |
|||
if (fn != null) { |
|||
return fn(err, str); |
|||
} else { |
|||
if (err != null) { |
|||
return req.next(err); |
|||
} else { |
|||
return resp.send(str); |
|||
} |
|||
} |
|||
}; |
|||
secondRender = function(err, str) { |
|||
if (err != null) { |
|||
return secondResult = original.call(resp, self.options.sharedFolder + '/' + view, opts, (function(err2, str2) { |
|||
return finalPass(err2, err, str2); |
|||
}), parent, sub); |
|||
} else { |
|||
reset(); |
|||
if (fn != null) { |
|||
return fn(err, str); |
|||
} else { |
|||
return resp.send(str); |
|||
} |
|||
} |
|||
}; |
|||
result = original.call(resp, req.controller + '/' + view, opts, secondRender, parent, sub); |
|||
if (secondResult != null) result = secondResult; |
|||
reset(); |
|||
return result; |
|||
}; |
|||
}; |
|||
|
|||
Controllers.prototype.executeOnDirectory = function(dir, action) { |
|||
return fs.readdirSync(dir).forEach(function(file) { |
|||
var localpath, stat; |
|||
localpath = dir + '/' + file; |
|||
stat = fs.statSync(localpath); |
|||
if (stat && stat.isDirectory()) { |
|||
return self.executeOnDirectory(localpath, action); |
|||
} else { |
|||
return action(localpath); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
return Controllers; |
|||
|
|||
})(); |
|||
|
|||
}).call(this); |
|||
@ -0,0 +1,16 @@ |
|||
{ |
|||
"author": "Felix Jorkowski (http://jorkowski.com)", |
|||
"name": "controllers", |
|||
"description": "A simple mvc framework and route extender for Express", |
|||
"version": "0.0.2", |
|||
"homepage": "https://github.com/ajorkowski/controllers", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git://github.com/ajorkowski/controllers.git" |
|||
}, |
|||
"main": "lib/controllers.js", |
|||
"dependencies": { |
|||
}, |
|||
"devDependencies": { |
|||
} |
|||
} |
|||
@ -0,0 +1,324 @@ |
|||
fs = require('fs') |
|||
path = require('path') |
|||
|
|||
module.exports = (app, options = {}) -> |
|||
options.strict ?= true |
|||
options.overwriteRender ?= true |
|||
options.log ?= false |
|||
options.root ?= app.set('controllers') || process.cwd() + '/controllers' |
|||
options.sharedFolder ?= 'shared' |
|||
|
|||
new Controllers app, options |
|||
|
|||
class Controllers |
|||
constructor: (app, @options) -> |
|||
self = this |
|||
@_controllers = {} |
|||
|
|||
# Pre-load all the controllers... one time hit so done sync |
|||
this.executeOnDirectory @options.root, (file) -> |
|||
ext = path.extname file |
|||
if ext == '.js' || ext == '.coffee' |
|||
reduced = file.replace ext, '' |
|||
controller = path.basename reduced |
|||
self._controllers[controller] = require reduced |
|||
if self.options.log |
|||
console.log "Controller '#{controller}' has been loaded" |
|||
|
|||
# We are off to hijack the req.app.routes._route |
|||
# which is the point of contact of all our get/post/pull/etc methods. |
|||
# We will let the usual chain occur till the very last |
|||
# callback, and then we will make sure the controller and action |
|||
# are both defined, and then load up that controller/action. |
|||
# We have already cached the controllers to reduce require calls |
|||
originalRoute = app.routes._route |
|||
app.routes._route = (method, path, defaults, callbacks...) -> |
|||
# We might not have defaults |
|||
if 'function' == typeof defaults |
|||
callbacks.push defaults |
|||
defaults = null |
|||
|
|||
if callbacks.length == 0 |
|||
callbacks.push (req, res) -> |
|||
|
|||
defaults ?= { } |
|||
holder = { } |
|||
|
|||
# overwrite the callbacks to use this info |
|||
newCallbacks = (self.overwriteCallback c, holder) for c in callbacks |
|||
result = originalRoute.call app.routes, method, path, newCallbacks |
|||
|
|||
# Extend the routing by adding defaults |
|||
holder.route = newRoute = result.routes[method][result.routes[method].length - 1] |
|||
for defkey, defvalue of defaults |
|||
key = self.getKeyInRoute defkey, newRoute |
|||
if key? |
|||
key.default = defvalue |
|||
else |
|||
# controller/action is a special case and we need to save it |
|||
if defkey == 'controller' or defkey == 'action' |
|||
newRoute[defkey] = defvalue |
|||
|
|||
# If we have a key for controller/action that means they could be anything |
|||
for key in newRoute.keys when key.name == 'controller' or key.name == 'action' |
|||
newRoute[key.name] = '*' |
|||
|
|||
return result |
|||
|
|||
# Add all the corresponding helpers |
|||
this.addHelpers app |
|||
|
|||
addReqHelpers: (req, res) -> |
|||
self = this |
|||
req.executeController = (controller, action, next) -> |
|||
if not controller? or not action? |
|||
throw new Error("executeController needs the controller and action specified") |
|||
|
|||
# If we pass a next switch the controller/action back to our current one |
|||
if next? |
|||
currentC = req.controller |
|||
currentA = req.action |
|||
nextFunc = next |
|||
next = -> |
|||
req.controller = currentC |
|||
req.action = currentA |
|||
nextFunc.apply this, arguments |
|||
|
|||
req.controller = controller |
|||
req.action = action |
|||
self._controllers[controller][action] req, res, next |
|||
|
|||
addHelpers: (app) -> |
|||
self = this |
|||
|
|||
app.dynamicHelpers { |
|||
controller: (req, res) -> |
|||
req.controller |
|||
|
|||
action: (req, res) -> |
|||
req.action |
|||
|
|||
getUrl: (req, res) -> |
|||
(controller, action, other, query) -> |
|||
if not action? or 'object' == typeof action |
|||
query = other |
|||
other = action |
|||
action = controller |
|||
controller = null |
|||
|
|||
controller ?= req.controller |
|||
other ?= {} |
|||
other.controller = controller |
|||
other.action = action |
|||
query ?= {} |
|||
|
|||
if not action? or not controller? |
|||
throw new Error("getUrl needs at minimum an action defined, but also takes a controller") |
|||
|
|||
for route in app.routes.routes.get |
|||
if self.isMatchingPath other, route |
|||
# We have found a route that matches |
|||
# We are stepping through the keys backwards so that if |
|||
# any keys are found the rest MUST be displayed |
|||
# (that is... optional keys cannot be blank) |
|||
hasReplaced = false |
|||
result = route.path |
|||
for i in [route.keys.length-1..0] |
|||
key = route.keys[i] |
|||
def = key.default ? '' |
|||
replacement = other[key.name] ? def |
|||
if hasReplaced and replacement == '' |
|||
throw new Error("The optional parameter '#{key.name}' is required for this getUrl call as an parameter further down the path has been specified") |
|||
else |
|||
if not hasReplaced |
|||
if (not key.optional or replacement != def) and |
|||
hasReplaced = true |
|||
else |
|||
replacement = '' |
|||
|
|||
# Do the replacement |
|||
regExp = new RegExp ":#{key.name}(\\?)?" |
|||
result = result.replace regExp, replacement |
|||
|
|||
# Remove multiple slashes |
|||
result = result.replace /\/+/g, '/' |
|||
|
|||
# Remove trailing slash... unless we are at root |
|||
if result != '/' |
|||
result = result.replace /\/+$/, '' |
|||
|
|||
# Add in query strings |
|||
first = true |
|||
for key, value of query |
|||
if first |
|||
first = false |
|||
result = result + '?' + key |
|||
if value? and value != '' |
|||
result = result + '=' + value |
|||
else |
|||
result = result + '&' + key |
|||
if value? and value != '' |
|||
result = result + '=' + value |
|||
|
|||
return result |
|||
|
|||
throw new Error("Route could not be found that matches getUrl parameters, make sure to specify a valid controller, action and required parameters") |
|||
} |
|||
|
|||
getKeyInRoute: (name, route) -> |
|||
for key in route.keys when key.name == name |
|||
return key |
|||
return null |
|||
|
|||
isMatchingPath: (object, route) -> |
|||
# First check the controller and action |
|||
if route.controller != '*' and route.controller != object.controller |
|||
return false |
|||
|
|||
if route.action != '*' and route.action != object.action |
|||
return false |
|||
|
|||
# This is checking that all items in the object match with a key |
|||
for key, value of object when key != 'controller' and key != 'action' |
|||
if not (@getKeyInRoute key, route)? |
|||
return false |
|||
|
|||
# This is checking all (required) keys have an object value |
|||
for key in route.keys when key.name != 'controller' and key.name != 'action' |
|||
if not key.optional and not object[key]? |
|||
return false |
|||
|
|||
return true |
|||
|
|||
overwriteCallback: (callback, routeHolder) -> |
|||
self = this |
|||
options = @options |
|||
(req, resp, next) -> |
|||
# Call the normal callback |
|||
callback req, resp, next |
|||
|
|||
# Add helpers |
|||
self.addReqHelpers req, resp |
|||
|
|||
# set the current route |
|||
route = routeHolder.route |
|||
|
|||
# Go through our keys and if they have a default and the param value |
|||
# is not set make sure it is |
|||
for key in route.keys when not req.params[key.name]? and key.default? |
|||
req.params[key.name] = key.default |
|||
|
|||
# Grab our current controller/action either from the route or use defaults |
|||
req.controller = req.params.controller ? route.controller |
|||
req.action = req.params.action ? route.action |
|||
|
|||
if options.log |
|||
console.log 'Controller: ' + req.controller |
|||
console.log 'Action: ' + req.action |
|||
|
|||
if options.strict |
|||
if not req.controller? |
|||
throw new Error("Is in strict mode and no controller specified") |
|||
if not req.action? |
|||
throw new Error("Is in strict mode and no action specified") |
|||
|
|||
if req.controller? and req.action? |
|||
# We have a controller and an action - lets overwrite the res.render |
|||
# command so that we do not have to specify view names |
|||
if options.overwriteRender |
|||
self.overwriteRender req, resp |
|||
|
|||
# Find the controller |
|||
controller = self._controllers[req.controller] |
|||
if not controller? |
|||
if options.log |
|||
console.log "Controller '#{req.controller}' could not be found" |
|||
next 'route' |
|||
return |
|||
|
|||
# Execute the action |
|||
action = controller[req.action] |
|||
if not action? |
|||
if options.log |
|||
console.log "Action '#{req.action}' could not be found on controller '#{req.controller}' " |
|||
next 'route' |
|||
return |
|||
|
|||
# Execute the controller with a nothing followup action |
|||
action req, resp, next |
|||
else |
|||
if options.log |
|||
console.log 'Controller or action was not specified, no action was called' |
|||
|
|||
overwriteRender: (req, resp) -> |
|||
self = this |
|||
original = resp.render |
|||
# This is the root dir the render method uses |
|||
root = resp.app.set('views') || process.cwd() + '/views' |
|||
|
|||
resp.render = (view, opts, fn, parent, sub) -> |
|||
# Allow for view to be empty |
|||
if 'object' == typeof view || 'function' == typeof view |
|||
sub = parent |
|||
parent = fn |
|||
fn = opts |
|||
opts = view |
|||
view = null |
|||
|
|||
# The view defaults to the action |
|||
view ?= req.action |
|||
|
|||
# Set the root directory as the controller directory |
|||
# if that doesnt work, try the shared directory |
|||
# disable hints because it comes up funny like |
|||
hasHints = resp.app.enabled 'hints' |
|||
resp.app.disable 'hints' |
|||
|
|||
result = null |
|||
secondResult = null |
|||
|
|||
reset = -> |
|||
if hasHints |
|||
resp.app.enable 'hints' |
|||
|
|||
finalPass = (err, err2, str) -> |
|||
reset() |
|||
|
|||
if err? |
|||
err = err + '\r\n\r\n' + err2 |
|||
|
|||
if fn? |
|||
fn err, str |
|||
else |
|||
if err? |
|||
req.next err |
|||
else |
|||
resp.send str |
|||
|
|||
secondRender = (err, str) -> |
|||
if err? |
|||
# If the first render failed failed try getting view from 'shared' |
|||
secondResult = original.call resp, self.options.sharedFolder + '/' + view, opts, ((err2, str2) -> finalPass(err2, err, str2)), parent, sub |
|||
else |
|||
reset() |
|||
|
|||
if fn? |
|||
fn err, str |
|||
else |
|||
resp.send str |
|||
|
|||
result = original.call resp, req.controller + '/' + view, opts, secondRender, parent, sub |
|||
if secondResult? |
|||
result = secondResult |
|||
|
|||
reset() |
|||
return result |
|||
|
|||
executeOnDirectory: (dir, action) -> |
|||
fs.readdirSync(dir).forEach (file) -> |
|||
localpath = dir + '/' + file |
|||
stat = fs.statSync localpath |
|||
if stat and stat.isDirectory() |
|||
self.executeOnDirectory localpath, action |
|||
else |
|||
action localpath |
|||
@ -0,0 +1,23 @@ |
|||
{ |
|||
"name": "praktikum", |
|||
"version": "1.0.0", |
|||
"lockfileVersion": 3, |
|||
"requires": true, |
|||
"packages": { |
|||
"": { |
|||
"name": "praktikum", |
|||
"version": "1.0.0", |
|||
"dependencies": { |
|||
"controllers": "^0.0.2" |
|||
} |
|||
}, |
|||
"node_modules/controllers": { |
|||
"version": "0.0.2", |
|||
"resolved": "https://registry.npmjs.org/controllers/-/controllers-0.0.2.tgz", |
|||
"integrity": "sha512-Xhe4m8rr1reyM9jHIxaJQMR/P7ItoXvYhqFATht/5XMWcuONaFbYUNnV7/6WxR58uc+d4OHU9rLuKYK+5KUopw==", |
|||
"engines": { |
|||
"node": "*" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
{ |
|||
"name": "praktikum", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"test": "echo \"Error: no test specified\" && exit 1" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "https://gitea.marzell.net/DerMaz/praktikum.git" |
|||
}, |
|||
"private": true, |
|||
"dependencies": { |
|||
"controllers": "^0.0.2" |
|||
} |
|||
} |
|||
Loading…
Reference in new issue