GeppettoJS / backbone.geppetto

Bring your Backbone applications to life with an event-driven Command framework.
http://geppettojs.github.com/backbone.geppetto/
MIT License
203 stars 28 forks source link

v0.8 #87

Open creynders opened 9 years ago

creynders commented 9 years ago

So I started on the next minor/major version. I'd been thinking about 0.7.2 and what needed to be done: updating the tests, documentation and a number of patches here and there to get things better, until I suddenly realised I should be focusing on getting the next major version out instead of trying to modify 0.7 towards what I'd really want.

So, here's what's happening: I'm trying to

loginContext.wire(15)
    .as.value("timeout duration");

loginContext.wire(ServiceRequest)
    .as.producer("serviceRequest");

loginContext.wire(AuthService)
    .as.singleton("authService")
    .using.wiring({
        timeout : "timeout duration",
        base : "serviceRequest"
    });

loginContext.wire(LoginCommand)
    .to.event("loginview:authentication:requested")
    .using.wiring("authService");

mainContext.wire(LoginView)
    .as.factory("loginPanel")
    .using.parameters({
            model : new AuthModel()
        })
        .and.handlers({
            "authservice:authentication:completed": "close",
            "authservice:authentication:failed": function(){
                this.show("ERROR!");
            }
        })
        .and.context(loginContext);

mainContext.wire(MainView)
    .using.wiring({
        loginRegion: "loginView"
    });
mainContext.trigger("maincontext:startup:requested");

Forgive me the convoluted example, but I tried to cover as much ground as possible. I think most of it is pretty self-explanatory if you're familiar with the current version. Obviously everything that's done using the context API here, can be configured inside your components as well: wiring, handlers, et cetera. As you can see views are no longer wired as "view", but as factory. And wireClass is replaced with as.producer which is a better fitting term, IMO. There's a new concept as well, "providers":


function QueueProvider(){
    this.queue = [];
};

QueueProvider.prototype.provide = function(ItemClass){
    var item = new ItemClass()
    this.queue.push(item);
    return item;
}

var requesterQueue = new QueueProvider();

context.provide("queue")
    .using(requesterQueue.provide);

context.wire(Requester)
    .as.queue("requester");

context.wire(SomeService)
    .using.wiring({
        requester: "requester"
    });

Now, what's this all about? ATM Geppetto contexts allow for registering objects as singletons, values, ... but I wanted to allow developers to create their own types. E.g. the above example would allow maintaining a list of all Requester instances. The provide method receives an ItemClass, which in this case is Requester. It creates a new instance and stores it in a queue. The SomeService class is totally oblivious to whether it's receiving a singleton instance, or a new instance, or (in this case) a new instance which is stored in a queue. As you can see the Context#provide method accepts a string which is used to expand the context API. I passed 'queue' to it, which adds a queue member to wire.as. This allows for extremely powerful and versatile concepts and patterns. E.g. I can imagine use cases for an "object pool" or "cyclic" provider for instance.

Well, that's about it FTM. As always, @geekdave please pull the reigns if I'm off into cuckoo-land.

creynders commented 9 years ago

I'm so excited about the progress I've made in branch next

In a nut shell: the wiring API's up and running. Not set in stone obviously, but pretty definite already. So, there's a number of changes and new concepts. (Dropped factory for constructor)

// v0.7
context.wireView("SomeView", SomeView);
context.wireClass("someClass", SomeClass);
context.wireValue("someValue", "some value");
context.wireSingleton("someSingleton", SomeSingleton);
context.getObject("someSingleton");
context.hasWiring("someSingleton");

//v0.8
context.wire(SomeView).as.constructor("SomeView"); //will resolve to a constructor function
context.wire(SomeClass).as.producer("someClass"); //will resolve to an instance of SomeClass
context.wire("some value").as.value("someValue"); //will resolve to "some value"
context.wire(SomeSingleton).as.singleton("someSingleton"); //will resolve to "some value"
context.get("someSingleton"); //will return the SomeSingleton instance
context.has("someSingleton"); //will output `true`

//however, then things start happening.
//ALL `.as.<provider>` (and some other) methods accept a "string", an object, an array, 
//parameters or any mix of those

context.wire(SomeSingleton).as.singleton("SingletonA", "SingletonB", "SingletonC"); //equals:
context.wire(SomeSingleton).as.singleton(["SingletonA", "SingletonB", "SingletonC"]); //equals:
context.wire(SomeSingleton).as.singleton({ a: "SingletonA", b: "SingletonB", c: "SingletonC"} ); //equals:
context.wire(SomeSingleton).as.singleton([["SingletonA"]], "SingletonB", {c: "SingletonC"});

//now what does this mean?
//They all share the same singleton instance
context.get("SingletonA") ===  context.get("SingletonB") === context.get("SingletonC");

//sometimes it's more beneficial to have separate singletons though, i.e. a "multiton"
context.wire(SomeSingleton).as.multiton("SingletonA", "SingletonB", "SingletonC");
//will create three separate singletons

//both of the above open up a ton of possibilities, where classes use the same dependencies, 
//however they're named differently which boosts reuse, flexibility et cetera.

//but there's more: sometimes you want to create an instance (to configure it for example)
//at wiring-time, but you want its dependencies to be resolved lazily, that's possible now too.
var foo = new FooClass(); //it has its dependencies declared as a `wiring` object inside the prototype
context.wire(foo).as.unresolved("foo");
context.get("foo"); //will resolve its dependencies

//oh, and `get` and `has` are just as flexible as the `.as.<provider>` methods:
var result = context.get("SingletonA", ["someClass"], {view:"SomeView"});
console.log(result);
/*
output:
{
    SingletonA: <SomeSingleton instance>,
    someClass: <SomeClass instance>,
    view: <SomeView instance>
}
*/

//but if you `get` a single key, it returns a single value
context.get("SingletonA");

//the same goes for `has`
var result = context.has("SingletonA", ["someClass"], {view:"SomeView"}, "notfound");
console.log(result);
/*
output:
{
    SingletonA: true,
    someClass: true,
    view: true,
    notfound: false
}
*/

//And if you simply want to be sure all keys have been wired:
var result = context.has.each("SingletonA", ["someClass"], {view:"SomeView"}, "notfound");
console.log(result);//outputs: false (since "notfound" is not wired)

//the same flexibility applies to `release`
context.release.wires("SingletonA")
context.release.wires("SingletonA", ["someClass"], {view:"SomeView"});
context.release.all();

//Oh, and wirings get merged.
function Foo(){
    this.wiring = "a";
}

//either:
context.wire(Foo).as.singleton("foo").using.wiring("b");
var foo = context.get("foo"); //instance with "a" and "b" resolved

//or:
var foo = new Foo();
context.resolve(foo, "b"); //foo has its dependencies "a" and "b" resolved

//ah yes, wiring declarations are just as flexible:
function Foo(){
    this.wiring = "a"; //resolves "a" into member "a"
    this.wiring = ["a", "b"]; //resolves "a" and "b" into members "a" and "b"
    this.wiring = { foo: "a", baz : "b"};//resolves "a" and "b" into members "foo" and "baz"
    this.wiring=["a", {baz:"b"}];//resolves "a" and "b" into members "a" and "baz"
}

Now on to tackle the rest. I rewrote all tests, they're inside specs.

mmikeyy commented 9 years ago

wow! You're on fire these days! I'm getting excited too!! :smiley:

I'm anxious to see a working version that I can test! I have several projects that I want to convert to Geppetto, some quite big. Any idea how long you're giving yourself to 'tackle the rest'?

creynders commented 9 years ago

@mmikeyy hard to say, since I can only work on this very irregularly. Also, there's still a ton to do.

The two major jobs are

  1. documentation: will need to be rewritten almost from scratch and it will be a LOOOT of work
  2. messaging: I've been thinking about this a lot and its hard. ATM I'm swaying towards not dropping backbone and expand on the Events system it provides, something like this:
//we want to be able to let components react to context events
wire(SomeView).as.producer("someView")
    .and.on("some:context:event").execute("render")

//or if you want to setup a bunch at once:
wire(SomeView).as.producer("someView")
    .and.on({ "some:context:event" : "render" });

//but we also want the reverse, i.e. components should easily be able to communicate to the context
wire(SomeView).as.producer("someView")
    .and.relay("some:view:event") //sends "some:view:event" through the context
//but we want event translation as well
wire(SomeView).as.producer("someView")
    .and.relay("some:view:event").as("another:context:event"); //when view triggers "some:view:event" the context translates it to "another:context:event"
//again with a map
wire(SomeView).as.producer("someView")
    .and.relays({ "some:view:event":"another:context:event" })
//or an array
wire(SomeView).as.producer("someView")
    .and.relays([ "some:view:event" ])

//then the context needs to be able to do stuff with context events
//relay them to other contexts
context.relay("some:view:event").to.all(); // this is to every other context
//or to its parent
context.relay("some:view:event").to.parent(); // this is to its parent context

//but we also want to be able to translate events
context.relay("some:view:event").as("another:context:event")
//to other contexts if necessary
context.relay("some:view:event").as("another:context:event").to.all()
context.relay("some:view:event").as("another:context:event").to.parent()

//all components have their `trigger` method which dispatches to direct listeners, 
//(or context listeners if configured that way) but they also have 
//a `relay` method which functions as described above

//the context also dispatches with `trigger` which is exaclty like in Backbone
context.trigger("event", ...params);
//except it's enhanced, allowing you to dispatch to other contexts
context.trigger("event", ...params).to.all(); //dispatches within 'context' but also to all other contexts
context.trigger("event", ...params).to.parent(); //dispatches within 'context' but also to parent context

Just to be clear, I'm totally focusing on how everything works at wiring-time, but everything you can configure outside the components will be available inside the components as well. I.e. it will cater to both styles.

creynders commented 9 years ago

Then, on to commands and listening directly:

//if you want run a default handler:
wire(SomeView).as.producer("someView")
    .and.on("some:context:event").execute(); //calls the instance's `execute` method

//which leads us to commands, changed my mind about not having them in the API.
context.wire(SomeCommand).as.command().on("some:context:event"); //which is (almost) equivalent to:
context.wire(SomeCommand).as.producer("someCommand")
    .and.on("some:context:event").execute();
//you could provide a name to the "normal" commands as well:
context.wire(SomeCommand).as.command("someCommand").on("some:context:event");

//the above means you can do:
context.wire(SomeCommand).as.singleton("someCommand")
    .and.on("some:context:event").execute(); //creates a singleton command, i.e. it's not dropped after execution

//then, listening directly on a context, the BB way
context.on("some:context:event", function(){
    console.log("I rock!");
});

//or fluently
context.on("some:context:event").execute( function(){
    console.log("And roll!");
});

As you can see I aim for a use-it-as-you-wish API, which allows for being very strict, but also creating exceptions when necessary. Again, it will be the documentation's task to show the "best" way. And then for "advanced" usage explain what possibilities and exceptions are possible.

mmikeyy commented 9 years ago

OK. I can see that the new Geppetto won't be available any time soon. No problem: we already have something very good...

I think it's good to keep the Backbone event system. Why reinvent the wheel? But then... one could retort that this comment is not surprising coming from a Backbone user, and not everyone uses Backbone. Anyway, I don't remember seeing any complaints about this...

Concerning events, I've been wondering why we always send events to self, to parent and/or to parents. Why not have children too as an option? If I'm not mistaken, the only way to reach children ATM is to dispatch to all.

Anxious to test a functional version!

creynders commented 9 years ago

So, time for an update. I just pushed into the next branch a LAAARGE part of Geppetto.Events. Again, aiming for readability and versatility. Most of what I wrote above is still correct.

Usage:

var dispatcher = new Geppetto.Events();
//equals
var dispatcher = Geppetto.Events();
//equals
var dispatcher = _.extend({}, Geppetto.Events); //As you would do with Backbone.Events

//dummy handler
var handler = function(){
  console.log(arguments);
}

//fully Backbone.Events compatible:
dispatcher.on("event", handler, someObject);
dispatcher.off("event");
someObject.listenTo(dispatcher, "event", handler);
//et cetera

//But it's enhanced:

dispatcher.on("event").have(someObject).execute(handler); // in scope of `someObject`
//you can attach functions directly to events as well
dispatcher.on("event").execute(handler);

//if your listener has a method "foo" for instance you can use these too:
dispatcher.on("event").have(someObject).execute("foo");

//when hanging on a geppetto context you can use keys for lazy creation:
context.on("event").have("loginService").execute("signin"); // loginService will only be created when event is first dispatched

//"execute" is the default handler, which leads to the new way of wiring commands (Yes, changed my mind again
context.on("event").have(MyCommand).execute();

//BTW: you can pass multiple functions to `execute`:
//as parameters
context.on("event").execute(f1, f2, f3)
//as an array
context.on("event").execute([f1, f2, f3])
//as an object
context.on("event").execute({ a: f1, b : f2, c: f3 });

//`trigger` works as in Backbone
context.trigger("event", a, b, c); //handling function will receive `a,b,c`
//and you can use `dispatch` to have the old Geppetto style
context.dispatch("event", { a: a, b: b, c: c});
//handling function receives following object
var received = {
  eventName : "event",
  eventData : {
    a: a,
    b: b,
    c: c
  }
}

//`listenTo` works as in Backbone:
context.listenTo(someObj, "event", handler);
//but is also enhanced:
context.listenTo(someObj).on("event").execute(handler);
//and aliased to `allow`
context.allow(someObj).on("event").execute(handler); 

//`once` and `listenToOnce` work as in BB AND are enhanced as well
context.once("event").execute(f1, f2, f3)

//And if you prefer a really explicit style, you can do this:
var when = Geppetto.Events;
when(context).dispatches("event").execute(handler);
when(context).dispatches("event").have(someObject).execute(handler);
//completely equal to:
context.on("event").execute(handler);
context.on("event").have(someObject).execute(handler);

I'm not entirely satisfied with have and allow, but really can't come up with anything better.

creynders commented 9 years ago

488 tests already!