Last active
August 29, 2015 14:21
-
-
Save erikpukinskis/f997d5cfe2ab2ee6d37c to your computer and use it in GitHub Desktop.
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
// Component | |
// Most people when they want to make something happen with software, need to | |
// pull together a couple different kinds of things. Maybe you want a button on | |
// your thing that when people hit it they can sign up for some text messages | |
// that are going to get sent to them according to some pattern. That's a couple | |
// different kinds of things: a widget people can interact with, some sort of | |
// off-line activity that springs to life and sends messages at particular | |
// times. A thing that remembers the phone numbers, etc. | |
// There's potentially some complexity behind each of those things, so beyond | |
// the simplest cases you want to have separate modules that take care of the | |
// specifics. But you need something that describes how those different modules | |
// fit together.[1] | |
// Here's an example that ties together a few different things: a text message | |
// function, a button, and a server booting function. | |
// We'll need an sms module to exist, but we're not going to actually use it, so | |
// let's just make an empty one. | |
define("sms", function() { | |
return { | |
send: function() { | |
throw new Error("We don't actually want to be here!") | |
} | |
} | |
}) | |
define( | |
"important-person-texting-component", | |
["sms", "nrtv-element", "component", "bridge-tie", "server-tie", "element-tie"], | |
function(sms, element, component) { | |
var Texter = component() | |
var server = Texter.server() | |
var bridge = Texter.bridge(server) | |
var successMessage = Texter.element(".success.hidden", "Success! You da real MVP.") | |
var showSuccess = successMessage.showOnClient(bridge) | |
var textSomeone = server.route( | |
"post", | |
"/text", | |
function(x, response) { | |
sms.send( | |
"812-320-1877", | |
"Erik, do something!" | |
) | |
respond.json(showSuccess) | |
}) | |
) | |
var body = Texter.element([ | |
element( | |
"Press the button!" | |
), | |
successMessage, | |
element( | |
"button.do-it", | |
{ | |
onclick: textSomeone | |
.makeRequest(bridge) | |
// could give data here! | |
}, | |
"Do iiiiit" | |
) | |
]) | |
server.route( | |
"get", | |
"/", | |
bridge.sendPage(body) | |
) | |
return Texter | |
} | |
) | |
// That's a component. There's some complex stuff going on behind the scenes, | |
// with the sms library and the template/element stuff. And there's a bunch of | |
// complexity hidden about how, in general, those things know how to work | |
// together. But everything you need to know about how this specific template, | |
// route, and function work together is represented in the component. | |
// Let's test to see if it works. | |
define( | |
"important-person-test", | |
[ | |
"important-person-texting-component" | |
, "chai", "sms", "sinon", "zombie", "sinon-chai", "html" | |
], | |
function(Texter, chai, sms, sinon, Browser, sinonChai, html) { | |
var expect = chai.expect | |
chai.use(sinonChai); | |
sinon.stub(sms, 'send') | |
var instance = new Texter() | |
instance.start(3090) | |
Browser.localhost("localhost", 3090); | |
var browser = new Browser() | |
browser.on("error", function(e) { | |
throw(e) | |
}) | |
browser.visit("/", function() { | |
browser.assert.hasClass('.success', 'hidden') | |
console.log(html.prettyPrint(browser.html())) | |
console.log('zoops') | |
browser.pressButton( | |
".do-it", | |
function() { | |
console.log(html.prettyPrint(browser.html())) | |
runChecks() | |
} | |
) | |
}) | |
function runChecks() { | |
// instance.stop() | |
browser.dump() | |
// setTimeout(null,10000) | |
// console.log("\n==================\nand now\n\n") | |
// console.log(html.prettyPrint(browser.html())) | |
browser.assert.hasNoClass('.success', 'hidden') | |
console.log("message is visible!") | |
// And then we just expect that someone tried to sms me. This is a handy | |
// side-effect of having stubbed sms.send before. Now we can check to see | |
// if it was called. You can't do that unless you stub it. | |
expect(sms.send).to.have | |
.been.calledWith( | |
"812-320-1877", | |
"Erik, do something!" | |
) | |
console.log("sms got called!") | |
} | |
} | |
) | |
// OK, so we described a simple component and we wrote some code to test that it | |
// works, but why would it work? We haven't actually implemented any of that | |
// stuff yet. | |
// So let's make it work. | |
define( | |
"component", | |
function() { | |
var sharedStatics = {} | |
function component(name) { | |
var componentConstructor = | |
function() { /* */ } | |
componentConstructor.name = name | |
for (key in sharedStatics) { | |
componentConstructor[key] = sharedStatics[key] | |
} | |
return componentConstructor | |
} | |
// Component ties are different than modules. Modules don't know about the | |
// other modules that use them... the "button" module and the "sms" module | |
// know nothing about each other. As far as each is concerned, the other is | |
// speaking gibberish. Modules only know about the specific handful of modules | |
// they need to do their jbos | |
// But component ties know about each other. The template tie knows about the | |
// route tie. More distant ties might not be able to coordinate... the | |
// database tie doesn't know anything about buttons. But other ties do. | |
// Which means ties all sort of form little neighborhoods. Around the button | |
// tie is a neighborhood of ties that know how to do stuff with buttons. | |
// So. We'll need a way to tell our component about the types of ties | |
// available to it. | |
component.addTypeOfTie = | |
function(name, tieConstructor, exports) { | |
// Each type of tie gets a function on the component: | |
sharedStatics[name] = | |
function() { | |
// We grab the arguments for constructing the tie | |
var args = [null].concat( | |
Array.prototype.slice.call( | |
arguments, 0 | |
) | |
) | |
// We bind the constructor to those arguments | |
var boundConstructor = | |
tieConstructor.bind.apply( | |
tieConstructor, | |
args | |
) | |
// And then instantiate the tie | |
var tie = new boundConstructor() | |
// Some ties want to make some of their methods publically available on the component: | |
for (i=0; i<(exports && exports.length); i++) { | |
var key = exports[i] | |
this.prototype[key] = tie[key].bind(tie) | |
} | |
return tie | |
} | |
} | |
return component | |
}) | |
// Server Tie | |
define( | |
"server-tie", | |
["nrtv-server", "component"], | |
function(Server, component) { | |
function ServerTie() { | |
this.instance = new Server() | |
} | |
ServerTie.prototype.start = | |
function(port) { | |
this.instance.start(port) | |
this.url = "http://localhost:"+port | |
} | |
ServerTie.prototype.stop = | |
function() { | |
this.instance.stop() | |
} | |
ServerTie.prototype.route = | |
function(verb, pattern, func) { | |
this.instance[verb](pattern, func) | |
return { | |
// Takes a bridge and returns an onclick that would call the route | |
makeRequest: | |
function(bridge) { | |
var ajax = bridge | |
.defineOnClient(hitRoute) | |
return ajax( | |
verb, | |
pattern | |
).evalable() | |
} | |
} | |
} | |
function hitRoute(verb, path) {} | |
$.ajax({ | |
method: verb, | |
url: path, | |
dataType: "json", | |
success: this.respond, | |
error: function(a,b,c) { | |
document.write(JSON.stringify([a,b,c],null,0)) | |
} | |
} | |
component.addTypeOfTie( | |
"server", ServerTie, ["start", "stop"] | |
) | |
} | |
) | |
// Bridge Tie | |
define( | |
"bridge-tie", | |
["component", "nrtv-element", "object-hash"], | |
function(component, element, hash) { | |
function BridgeTie(instance) { | |
this.instance = instance | |
this.clientBindings = {} | |
} | |
BridgeTie.prototype.sendPage = | |
function(body) { | |
var jquery = element("script", {src: "https://code.jquery.com/jquery-2.1.4.min.js"}) | |
var bindings = element( | |
"script", | |
this.script() | |
) | |
var styles = element("style", " .hidden { display: none }") | |
return element("html", [ | |
element("head", [ | |
jquery, | |
bindings, | |
styles | |
]), | |
element("body", body.html()) | |
]).html() | |
} | |
BridgeTie.prototype.script = | |
function() { | |
var funcsJson = JSON.stringify(this.funcs,null,2) | |
var clientSource = client.toString().replace("%funcs%", funcsJson) | |
return "var bridge\n" | |
+ "var funcs\n" | |
+ "(\n" | |
+ clientSource | |
+ "\n)()" | |
} | |
BridgeTie.prototype.defineOnClient = | |
function(func) { | |
var key = | |
(func.name || "function") | |
+ "/" | |
+ hash(func) | |
if (!clientBindings[key]) { | |
// We keep the functions so when someone asks for a page we can send | |
// them down with the HTML | |
clientBindings[key] = func | |
} | |
return function() { | |
var args = Array.prototype.slice.call(arguments) | |
new BoundFunc(key, args) | |
} | |
} | |
function BoundFunc(key, args) { | |
this.binding = { | |
key: key, | |
args: args | |
} | |
} | |
// gives you a string that when evaled on the client, would cause the | |
// function to be called with the args | |
BoundFunc.prototype.evalable = | |
function() { | |
return "funcs[" | |
+ this.binding.key | |
+ "].apply(bridge," | |
+ JSON.stringify(this.binding.args) | |
+ "))" | |
} | |
// gives you a JSON object that, if sent to the client, causes the function | |
// to be called with the args | |
BoundFunc.prototype.evalResponse = | |
function() { | |
return this.binding | |
} | |
// And here is the client we run in the browser to facilitate those two | |
// things. The funcs are swapped in when we write the HTML page. | |
function client() { | |
funcs = "%funcs%" | |
bridge = { | |
handle: handleResponse | |
} | |
function handleResponse(binding) { | |
funcs[binding.key].apply(bridge, binding.args) | |
} | |
} | |
component.addTypeOfTie("bridge", BridgeTie) | |
} | |
) | |
// Element Tie | |
define( | |
"element-tie", | |
["component", "nrtv-element"], | |
function(component, element) { | |
function ElementTie() { | |
this.el = element.apply(null, arguments) | |
this.id = this.el.assignId() | |
} | |
ElementTie.prototype.html = | |
function() { | |
return this.el.html() | |
} | |
ElementTie.prototype.addChildren = | |
function(children) { | |
this.el.children = children | |
} | |
function showElement(id) { | |
document.write("curveball!") | |
var el = $("#"+id) | |
el.removeClass("hidden") | |
} | |
ElementTie.prototype.showOnClient = | |
function(bridge) { | |
var show = bridge | |
.defineOnClient(showElement) | |
return show(this.el.id) | |
.evalResponse() | |
} | |
component.addTypeOfTie( | |
"element", ElementTie | |
) | |
} | |
) | |
// Let's check that that worked! | |
define( | |
"super-simple-example", | |
["component", "chai", "supertest", "nrtv-server"], | |
function(component, chai, request, Server) { | |
var expect = chai.expect | |
// We need a super simple tie type that just starts up a server that prints | |
// out some text: | |
function TextServer(text) { | |
this.instance = new Server | |
this.instance.get( | |
"/", | |
function(request, response) { | |
response.send(text) | |
} | |
) | |
} | |
TextServer.prototype.start = | |
function() { | |
this.instance.start(5511) | |
} | |
TextServer.prototype.stop = | |
function() { | |
this.instance.stop() | |
} | |
// That's basically as simple as it gets. Usually there's at least two ties. | |
// But this is the simplest test. | |
// So let's add that the text server tie type: | |
component.addTypeOfTie( | |
"text", | |
TextServer, | |
["stop", "start"] | |
) | |
// And then we can call that boot function: | |
var Hello = component() | |
Hello.text("Hello worldkins!") | |
var instance = new Hello() | |
instance.start() | |
// Now we should see some magic! | |
request( | |
"http://localhost:5511" | |
) | |
.get("/") | |
.end(function(x, response) { | |
console.log(response.text) | |
expect(response.text).to.match( | |
/worldkins/ | |
) | |
instance.stop() | |
}) | |
} | |
) | |
time('parsing') | |
requirejs( | |
['super-simple-example'], | |
function(example) { | |
time('super simple example') | |
requirejs( | |
['important-person-test'], | |
function(test) { | |
time('important person test') | |
} | |
) | |
} | |
) | |
// So that's the basic of how a component works. In order for our original | |
// example to work, we have to load the real TemplateTie and RouteTie ties, | |
// which are a little more involved. Those are next up! | |
// [1] Often people let a "framework" handle that logic, and their modules work | |
// [together purely by dint of being in the same folder together. Or they have | |
// [several "glue" files that make a whole bunch of one specific type of | |
// [connection. Like gluing all the routes to their controllers. But then | |
// [there's a whole other place that glues the controllers to their database | |
// [representations. And whole other place that glues the routes to actions in | |
// [the user interface. Without a single thing whose job is to glue together a | |
// [concern, when you want to understand what, say, hitting a button *does*, you | |
// [have to hold in your head the entire logic of the framework. Building | |
// [components is making those relationships all specific and creating a place | |
// [where they can all live together. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment