Self explanatory. How would I add an signal to detect when an value in an table is changed?
It is a very complicated setup of using proxies with metatables (with typecasting to keep intellisense) to listen to value changes and a custom signal/connection implementation to achieve APIs similar to RBXScriptSignal
s and RBXScriptConnection
s.
Here is a real example of these steps together:
Person.lua
--!strict
--[=[
An example class utilizing custom signal/connection implementations
and proxies + metatables to listen for changes.
]=]
local ScriptSignal = require(script.ScriptSignal)
local Person = {}
-- define our Person API
type Person = {
FirstName: string,
LastName: string,
Age: number,
-- custom GetPropertyChangedSignal implementation
GetPropertyChangedSignal: (self: Person, property: string) -> ScriptSignal.ScriptSignal<>
}
function Person.new(firstName: string, lastName: string, age: number): Person
return setmetatable(
-- we create a proxy table to listen for all changes
{},
{
-- custom __index metamethod to access our private class,
-- or Person if the index is not part of the indexes
__index = function(self, index)
local mt = getmetatable(self)
if mt._person[index] ~= nil then
return mt._person[index]
end
return Person[index]
end,
-- custom __newindex metamethod to listen for changes to
-- our proxy table, then redirecting it to our private class
-- to manually fire our custom signal/connection implementation
__newindex = function(self, index, newValue)
local mt = getmetatable(self)
local oldValue = mt._person[index]
if newValue ~= oldValue then
mt._person[index] = newValue
-- you can also try to save on memory by only creating
-- custom signals if GetPropertyChangedSignal was called
if mt._signals[index] ~= nil then
mt._signals[index]:Fire()
end
end
end,
_person = {
FirstName = firstName,
LastName = lastName,
Age = age
},
_signals = {}
}
) :: any
end
function Person:GetPropertyChangedSignal(property: string): ScriptSignal.ScriptSignal<>
local mt = getmetatable(self)
-- you can also error if the property does not exist
if mt._person[property] == nil then
error(`{property} is not a valid property name`, 2)
end
if mt._signals[property] == nil then
mt._signals[property] = ScriptSignal.new()
end
return mt._signals[property]
end
return Person
Example
local Person = require(script.Parent.Person)
local johnDoe = Person.new("John", "Doe", 29)
-- John Doe is 29 years old
print(johnDoe.FirstName, johnDoe.LastName, "is", johnDoe.Age, "years old")
johnDoe:GetPropertyChangedSignal("Age"):Connect(function()
print(johnDoe.FirstName, johnDoe.LastName, "is now", johnDoe.Age, "years old")
end)
-- John Doe is now 30 years old
johnDoe.Age += 1
You can grab both my custom signal/connection implementation and the example above here:
Person.rbxm (4.0 KB)
I made a guide for this
Very simple once you figure out how __newindex behaves. Metatables are pretty powerful (minus questionable performance overheads in a few places), and maybe with user-defined type funcs we might even be able to make factories that respect strict typing a lot better.
Metatables are the most robust and reusable way to do this.
A simplier (though not automatic) alternative, is a way that’s common in other languages: using getter and setter methods (specifically setter methods). With a setter method, just fire a signal inside the setter method when the property is changed.
For example:
-- Always use the SetSpeed setter method to change the _Speed
function Car:SetSpeed(newSpeed)
-- (The underscore indicates the property should only be used internally)
if self._Speed ~= newSpeed then
-- Assume _SpeedChangedSignal is something like a BindableEvent or Signal object
self._SpeedChangedSignal:Fire() -- Fires self.SpeedChanged
self._Speed = newSpeed
end
end
This can be useful when only a few changed events are needed, though if you need a lot or don’t know which properties the end user will need ones for, using metatables is probably the better approach.
I should add, using a proxy object allows you to add better object security, if you want that.
One common pattern in a lot of languages is what I like to call the private
modifier, it restricts that property to methods within the class itself, with a proxy set up like this you can sort of restrict access to it.
local Account = {}
local ProxyBindingLayer = setmetatable({}, {__mode = "k"}
Account.__index = Account
local ACCOUNT_GLOB_MT = {
__index = function(self, k)
-- Access modifiers can go here
-- Give methods higher priority than properties
-- ie: if k == "Balance" then error("property is locked") end
return Account[k] or ProxyBindingLayer[self][k]
end,
__newindex = function(self, k, v)
-- same logic can go here, ie to enforce strict tables
ProxyBindingLayer[self][k] = v
BroadcastChangeSignal(self, k, v)
end
}
function Account:Widthdraw(balance)
local realself = ProxyBindingLayer[self]
-- code to widthdraw balance
end
return function()
local o = setmetatable({}, ACCOUNT_GLOB_MT)
ProxyBindingLayer[o] = {
Balance = 100
}
return o
end
The issue with this approach is there’s a LOT of boilerplate, I can wrap it down into functions given that the metatable accesses a shared table, but the issue there is strict typing can be anoying to work with.
This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.