Introduction
Have you ever wanted to detect variable changes inside of tables or mutations in your deep tables as a whole? Then look no further because EventModule is exactly what you need!
Often times I stumble across posts of people asking how they can detect table changes in their scripts similar to how Roblox’s Object Values does it, but this is no simple task. I have seen suggestions about using infinite while loops or setValue functions but all of which just works against you by either making the code less efficient or the development process more tedious.
That’s where EventModule comes in! It mirrors the way Roblox handles it’s Object and makes for an intuitive and familiar workplace for Roblox developers. On top of that, everything is run with metamethods, and events are handled securely so performance hit and memory leaks are kept at a minimum.
There have been similar modules created in the past like @Voidage’s PropertyModule which is a big inspiration for the EventModule. But all of those had some missing pieces with the biggest one being the lack of support for nested and deep tables. This and the other missing pieces is what EventModule is here to solve.
How to use?
1. Creating a new EventModule object
local rs = game:GetService("ReplicatedStorage")
local EventModule = require(rs.EventModule ) -- require the module
-- Creates a new EventModule object with these properties, if no properties are passed the object will be constructed around an empty table.
local Table = EventModule.new({
a = "hello",
nestedTable = {1,2,3}
})
-- Objects behave just like normal tables (with some exceptions) so it's still manipulated in the same way
print(Table.a) -- > "hello"
Table.b = "Never Gonna Give You Up"
print(Table.b) -- > "Never Gonna Give You Up"
-- Because Table.nestedTable is passed as a table, it too is converted to an Object
Table.nestedTable == EventModule.new({1,2,3})
-- Any new nested **tables** added into an object will also be converted to its own object
Table.nestedTable2 = {4,5,6}
Table.nestedTable2 == EventModule.new({4,5,6})
NOTE: The keys: “Parent”, “events”, “properties” and “mutated” are reserved for internal properties and can therefore not be used in the table.
2. The .mutated event
Here comes the juicy stuff. The .mutated event is the EventModule object’s version of the .changed event. It is available in all EventModule objects even in once nested in other objects. It was named .mutation instead of .changed because it is called on tables and not values which makes it more accurate to call it a table mutation and not a value change.
-- Code carries over from last step î
-- Table.mutated returns a "Signal" object which we can connect a function too just like so
Table.mutated:Connect(function(oldTable, newTable)
-- oldTable = *raw table* copy of the object before mutation.
-- newTable = Table Object after mutation
end)
-- mutation event is fired if the Table has any of its values, created, changed, or deleted
Table.d = "Never gonna let you down" -- > Fires Event
Table.d = "Never gonna run around and desert you" -- > Fires Event
Table.d = "Never gonna run around and desert you" -- > Setting the same value does not Fire the Event
Table.d = nil -- > Fires Event
-- same goes for tables
Table.nestedTable = nil -- > Fires Event
Table.nestedTable = {1,2,3} -- > Fires Event
-- mutation event even fires on any changes within nested tables
Table.nestedTable[4] = 4 -- > Fires Event
In summary, the object.mutation returns a signal that’s fired if the object is mutated in any way, or any of it’s descending deep tables are mutated.
3. GetPropertyChangedSignal()
object:GetPropertyChangedSignal(key) does exactly what it implies, returns a signal that is fired when the property of the corresponding key is created, changed, or deleted.
-- Signal connected to the same way as before
Table:GetPropertyChangedSignal("a"):Connect(function(oldVal, newVal)
-- oldVal = property's old value
-- newVal = property's new value
end)
Table.a = nil -- Fires Event
Table.a = "Goodbye" -- FiresEvent
Table.a = "Goodbye" -- Setting the same value does not Fire the Event
Table.b = "skrrrt" -- Table.b is not the property listned for so the event does not fire
4. Parent
Normally a table can only reference nested tables downstream but with the EventModule object you can reference the parent object of any nested table.
local Table = EventModule.new({
nestedTable = {}
})
nestedTable.Parent == Table
WARNING: This parent structure creates a circular dependency and can cause issues in some cases when using recursion. Use the injected functions and properties documented below to avoid stack overflows.
5. Connection Cleanup
Loose connections and signals no longer in use contribute a lot to lag and memory leaks so EventModule tries to make this cleaning process as easy as possible.
Disconnect:
Disconnects all of the objects events does not alter children’s or parent’s events
self:Disconnect() -- All events previoustly created for self are no longer active
DisconnectDescendants:
Disconnects all events of the object’s Descendants
self:DisconnectDescendants() -- all of self's events are still intact but all objects below self in the table hierarchy have had its events Disconnected.
DisconnectAllParents:
self has all their parents’ Events Disconnected
self:DisconnectAllParents() -- all of self's events are still intact but all objects above self in the table hierarchy have had their events Disconnected.
Destroy:
deletes object and all its events/connections returns: a raw table copy of the object’s properties
local event = self.mutated:Connect(function)
local properties = self:Destroy()
print(event)
-- > nil
print(self)
-- > nil
print(properties)
-- > {a copy of self's properties in raw table form}
Automatically Destroys Objects whos no longer indexed
nested objects who have their index removed gets automatically cleaned up and Destroy()'ed
local Table = EventModule.new({
nestedTable = {}
})
Table.nestedTable.mutated:Connect(function()
-- DO SOMETHING WHEN Table.nestedTable MUTATES
end)
Table.nestedTable[1] = 1 -- > Fires Event
Table.nestedTable = 1 -- > previous nestedTable object can no longer be indexed, the object will therefore be destroyed and cleaned up.
This covers most of the basics of the EventModule. However for more advanced features check out the documentation page below
Documentation
Documentation can also be found commented out at the top of the script.
Summary
--[[
Injected Properties:
self = EventModule.new(tbl)
self.Parent
self.mutated
self:GetPropertyChangedSignal(propertyName)
self:GetProperties()
self:pairs()
self:ipairs()
self:len()
self:insert(value, pos)
self:remove(pos)
self:find(value, init)
self:Disconnect()
self:DisconnectDescendants()
self:DisconnectAllParents()
self:Destroy()
Details:
Parent:
Table that contains self's parent object
local tbl = EventModule.new({a = {}})
a.Parent == tbl
mutated:
returns a signal thats fired when the table or any of it's descending nested tables are mutated/changed.
self.mutated:Connect(function(oldTable, newTable)
oldTable = tbl, copy of the table before mutation
newTable = self Object after change
end)
GetPropertyChangedSignal:
returns a signal thats fired when a value of that table with the specifiedKey is,
created, changed or deleted.
self:GetPropertyChangedSignal(propertyName):Connect(function(oldVal, newVal)
oldVal = property's old value
newVal = property's new value
end)
GetProperties:
Returns: a raw table copy of the object's properties
local properties = self:GetProperties()
print(properties)
-- > {a copy of self's properties in raw table form}
pairs:
Using roblox's pairs() function on the object will error so self:pairs() is a replacement for that,
it returns what you would expect from pairs(self).
self:pairs() == pairs(self)
ipairs:
Using roblox's ipairs() function on the object will error so self:ipairs() is a replacement for that,
it returns what you would expect from ipairs(self).
self:ipairs() == ipairs(self)
len:
Using roblox's # length function on the object will error so self:len() is a replacement for that,
it returns what you would expect from #self.
self:len() -- > table length
insert:
Roblox's table.insert() function uses rawset and therefore does not trigger the mutation and property events,
self:insert(value, pos) is an identical replacement that adresses this issue.
value = value that's inserted
pos = optional, number position in table where value will be inserted, defaults to #t+1
self:insert("LastValue")
print(self[self:len()]) -- > "LastValue"
remove:
Roblox's table.remove() function uses rawset and therefore does not trigger the mutation and property events,
self:remove(pos) is an identical replacement that adresses this issue.
pos = Removes from at position pos,
returning the value of the removed element.
When pos is an integer between 1 and #t,
it shifts down the elements t[pos+1], t[pos+2], …, t[#t] and erases element t[#t].
The index pos can also be 0 when #t is 0 or #t+1;
in those cases, the function erases the element t[pos].
self = {1,2,3}
self:remove(2)
print(self) -- > {1,3}
find:
Roblox's table.find() function does not work on self,
self:find(value, init) is an identical replacement that adresses this issue.
value = linear search performed, returns first index that matches "value"
init = number pos where linear search starts
self = {1,2,3,2,1}
print(self:find(2)) -- > 2
print(self:find(2, 4)) -- > 4
Disconnect:
Disconnects all of the objects events
does not alter children's or parent's events
self:Disconnect() -- All events previoustly created for self are no longer active
DisconnectDescendants:
Disconnects all events of the object's Descendants
self:DisconnectDescendants()
-- all of self's events are still intact but all objects below self in the table hiarchy have had it's events Disconnected.
DisconnectAllParents
self has all their parents Events Disconnected
self:DisconnectAllParents()
-- all of self's events are still intact but all objects above self in the table hiarchy have had it's events Disconnected.
Destroy:
deletes object and all its events/connections
returns: a raw table copy of the object's properties
local event = self.mutated:Connect(function)
local properties = self:Destroy()
print(event)
-- > nil
print(self)
-- > nil
print(properties)
-- > {a copy of self's properties in raw table form}
--]]
Want to contribute?
EventModule is an open source project so everyone is welcome to contribute. Here is the git-hub repository feel free to submit pull requests and issues .
Get it here!
You can get the Event module either by syncing the git-hub repository directly into your game with rojo or get this assets:
https://www.roblox.com/library/6144158003/EventModule
Support me! Follow me here:
https://twitter.com/Legenderox