Skip to content

Instantly share code, notes, and snippets.

@erikpukinskis
Last active August 29, 2015 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erikpukinskis/f997d5cfe2ab2ee6d37c to your computer and use it in GitHub Desktop.
Save erikpukinskis/f997d5cfe2ab2ee6d37c to your computer and use it in GitHub Desktop.
// 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