I’ve got some events code I wrote awhile back that’s based on the excellent Backbone.Events object in javascript (http://backbonejs.org/#Events). I’ve been meaning to share it but I’ve never got around to separating it from a bunch of other personal library code that I use. Since it’s come up, I went ahead and did that today so I could share it. Here it is:
-- Events
local tinsert = table.insert
local tremove = table.remove
local eventsKey = setmetatable({}, {__tostring=function() return "[events]" end})
local sendersKey = setmetatable({}, {__tostring=function() return "[senders]" end})
-- subscriber will not keep sender alive
local sendersMT = {__mode="k"}
local function _senders(self)
local senders = self[sendersKey]
if not senders then
senders = setmetatable({}, sendersMT)
self[sendersKey] = senders
end
return senders
end
local function _event(self, name)
local events = self[eventsKey]
if not events then
events = {}
self[eventsKey] = events
end
local callbacks = events[name]
if not callbacks then
callbacks = {}
events[name] = callbacks
end
return callbacks
end
local function _callback(context, callback)
if type(callback) == "string" then
if type(context) == "table" then
callback = context[callback] or
error("table does not contain method '"..callback.."'")
else
error("context must be a table")
end
end
return callback
end
local function _remove(callbacks, callback, context, predicate)
for i = #callbacks, 1, -1 do
if predicate(callbacks[i], callback, context) then
tremove(callbacks, i)
end
end
end
local function _on(self, once, name, callback, context)
assert(name ~= nil, "you must specify an event name.")
callback = _callback(context, callback)
assert(callback ~= nil, "you must specify a valid callback.")
tinsert(_event(self, name), {
context = context,
callback = callback,
once = (once == true) and once or nil
})
return self
end
local function on(self, ...)
return _on(self, false, ...)
end
local function once(self, ...)
return _on(self, true, ...)
end
local _offPred = {
eq = function(v, cb, ctx) return ctx == v.context and cb == v.callback end,
cbEq = function(v, cb, ctx) return cb == v.callback end,
ctxEq = function(v, cb, ctx) return ctx == v.context end,
eqNoCtx = function(v, cb, ctx) return (not v.context) and cb == v.callback end
}
local function off(self, name, callback, context)
local events = self[eventsKey]
if not events then return self end
callback = _callback(context, callback)
if not (callback or context) then
if name then
events[name] = nil
else
self[eventsKey] = nil
end
return self
end
if name then events = {name = events[name]} end
local pred
if callback and context then
pred = (context == "all") and _offPred.cbEq or _offPred.eq
else
pred = callback and _offPred.eqNoCtx or _offPred.ctxEq
end
for _, callbacks in pairs(events) do
_remove(callbacks, callback, context, pred)
end
return self
end
local function _trigger(_handlers, name, all, ...)
if not _handlers then return end
local nhandlers = #_handlers
if nhandlers == 0 then return end
local handlers = {unpack(_handlers)}
for i = 1, nhandlers do
local handler = handlers[i]
if handler.once then
for ci = 1, #_handlers do
if handler == _handlers[ci] then
tremove(_handlers, ci)
break
end
end
end
local context = handler.context
if context then
if all then
handler.callback(context, name, ...)
else
handler.callback(context, ...)
end
else
if all then
handler.callback(name, ...)
else
handler.callback(...)
end
end
end
end
local function trigger(self, name, ...)
assert(name ~= nil, "you must specify an event name")
local events = self[eventsKey]
if not events then return self end
_trigger(events[name], name, false, ...)
_trigger(events["all"], name, true, ...)
return self
end
local function _subscribe(self, once, sender, name, method)
assert(type(sender) == "table", "sender table expected")
assert(name ~= nil, "event name expected")
assert(method ~= nil, "callback method expected");
_senders(self)[sender] = true
_on(sender, once, name, method, self)
return self
end
local function subscribe(self, ...) return _subscribe(self, false, ...) end
local function subscribeOnce(self, ...) return _subscribe(self, true, ...) end
local function unsubscribe(self, sender, name, method)
if sender ~= nil and type(sender) ~= "table" then
error("sender table expected")
end
local senders = self[sendersKey]
if not senders then return self end
if sender then
senders = {[sender] = senders[sender] and true or nil}
end
for sender in pairs(senders) do
sender:off(name, method, self)
if not (name or method) then
self[sendersKey][sender] = nil
end
end
return self
end
-- Events mixin
Events = {
on = on,
once = once,
off = off,
subscribe = subscribe,
subscribeOnce = subscribeOnce,
unsubscribe = unsubscribe,
trigger = trigger
}
To use it, you need a function like extend:
-- extend target table with properties of other table(s).
-- left-most table in param lest takes precedence
function extend(target, ...)
for i = 1, select("#", ...) do
for k, v in pairs(select(i, ...)) do
target[k] = v
end
end
return target
end
There’s quite a few features here, so it’s a bit much to explain all at once, but the API is almost identical to that of Backbone.Events, except that I’ve renamed listenTo() and listenToOnce() to subscribe() and subscribeOnce().
You can extend any object to send events (or be an event subscriber) pretty easily using extend(). One way is to extend an object directly:
myObject = extend({}, Events)
function eventHandler()
print("someEvent fired!")
end
myObject:on("someEvent", eventHandler)
myObject:trigger("someEvent")
Alternative, you can extend a class, and then all of it’s instantiated objects will be able to send events:
MyClass = extend(class(), Events)
function MyClass:doSomething()
self:trigger("someEvent")
end
function setup()
local obj = MyClass()
function eventHandler()
print("someEvent fired!")
end
obj:on("someEvent", eventHandler)
obj:doSomething()
end
A couple of other notes:
- “all” is a special event; any handler listening for it will receive all broadcasted events, with the event name as the first argument, followed by any additional arguments passed to the event.
- subscribe() can be used instead of on() so that objects can easily unsubscribe from many/all events/senders at once. Additionally, when using subscribe, since the handler must be a member function, you can specify the member function by name, like:
self:subscribe(someObject, "someEvent", "myEventHandler")
is equivalent to:
self:subscribe(someObject, "someEvent", self.myEventHandler)
so long as self actually has a method named “myEventHandler”.
Anyway, sorry for the lack of explanation/documentation. The Backbone.Events reference might be helpful, since this code is pretty much based on it. Also, I’ll be happy to answer any questions about it. For what it’s worth, I pretty much use this code in all of my projects now. It’s especially helpful when implementing UI code (like Buttons, etc).