local function Init() local metadata = { ---@param self ReactiveValue ---@param other ReactiveValue ---@return ReactiveValue|nil __add = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value + other._value end if otherType == "string" and self._type == otherType then return self._value .. other end if otherType == "number" and self._type == otherType then return self._value + other end return nil end, ---@param self ReactiveValue ---@param other ReactiveValue ---@return ReactiveValue|nil __mul = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value * other._value end if otherType == "number" and self._type == otherType then return self._value * other end return nil end, ---@param self ReactiveValue ---@param other ReactiveValue ---@return ReactiveValue|nil __sub = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value - other._value end if otherType == "number" and self._type == otherType then return self._value - other end return nil end, ---@param self ReactiveValue ---@param other ReactiveValue ---@return ReactiveValue|nil __div = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value / other._value end if otherType == "number" and self._type == otherType then return self._value / other end return nil end, ---@param self ReactiveValue ---@param other ReactiveValue ---@return ReactiveValue|nil __mod = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value % other._value end if otherType == "number" and self._type == otherType then return self._value % other end return nil end, ---@param self ReactiveValue ---@param other ReactiveValue ---@return ReactiveValue|nil __pow = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value ^ other._value end if otherType == "number" and self._type == otherType then return self._value ^ other end return nil end, ---@param self ReactiveValue ---@param other ReactiveValue ---@return boolean __eq = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value == other._value end return self._value == other end, ---@param self ReactiveValue ---@param other ReactiveValue ---@return boolean __lt = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value < other._value end return self._value < other end, ---@param self ReactiveValue ---@param other ReactiveValue ---@return boolean __le = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value <= other._value end return self._value <= other end, ---@param self ReactiveValue ---@param other ReactiveValue ---@return boolean __gt = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value > other._value end return self._value > other end, ---@param self ReactiveValue ---@param other ReactiveValue ---@return boolean __ge = function(self, other) local otherType = type(other) if otherType == "table" and other._type and other._type == self._type and other._value then return self._value >= other._value end return self._value >= other end, ---@param self ReactiveValue ---@return number __len = function(self) if self._type == "table" then return #self._value end if self._type == "string" then return string.len(self._value) end return 0 end, ---@param self ReactiveValue ---@return string __tostring = function(self) return tostring(self._value) end, ---@param self ReactiveValue ---@param key string ---@param value any ---@return nil __newindex = function(self, key, value) local setupComplete = rawget(self, "_setupComplete") if setupComplete == nil or setupComplete == false then rawset(self, key, value) return end if self._type ~= "table" then rawset(self, key, value) return end self._value[key] = value local ChangedKey = { key } -- If the value being assigned is a ReactiveValue -- Then listen to changes on it as well -- And propagate those changes upwards if self._recursive and getmetatable(value) == getmetatable(self) then self:_setupListeners(key, value) end self:_notify() self:_notifyFieldChanged(ChangedKey) self:_notifyAnyFieldChanged(ChangedKey) end, ---@param self ReactiveValue ---@param key string ---@return any|nil __index = function(self, key) local value = rawget(self, key) if value ~= nil then return value end if rawget(self, "_type") ~= "table" then return nil end local innerTable = rawget(self, "_value") if innerTable ~= nil then return rawget(innerTable, key) end return nil end -- __index = ReactiveValue } --- Sadly I could not get @generic to play nice with this class --- I think it's not ready yet, there are issues on github describing similar problems and it is marked as WIP... --- Guess I'll have to live without it for now and specify type of a RV in #type ---## A type safe value that can be listened to for changes ---### **Always use RV:set() for setting primitive values** --- Supports primitive values and tables
--- Tables can be listened to for changes on any field or a specific field
---### Example usage (value):
--- ```lua --- local test = ReactiveValue.new(1) --- test:onChange(function(value) --- print("test changed to " .. value) --- end) --- test:set(2) --- test:set(test + 3) --- ``` ---### Example usage (table):
--- ```lua --- local test = ReactiveValue.new({1, 2, 3}) --- test:onAnyFieldChange(function(field, value) --- print(string.format("test.%s changed to %s", table.concat(field, "."), value)) --- end) --- test[1] = 4 -- test.1 changed to 4 --- test[4] = {1, 2, 3} -- test.4 changed to --- test[4][1] = 14 -- No log(!!) because test[4] is a table and not a ReactiveValue --- ``` ---### To trigger a callback for `test[4][1]` in the previous example do:
--- ```lua --- local test = ReactiveValue.new({1, 2, 3}, true) --- test:onAnyFieldChange(function(field, value) --- print(string.format("test.%s changed to %s", table.concat(field, "."), value)) --- end) --- test[1] = 4 -- test.1 changed to 4 --- test[4] = {1, 2, 3} -- test.4 changed to
--- test[4][1] = 14 -- test.4.1 changed to 14 --- ``` ---### To listen to a specific field of a table do:
--- ```lua --- local test = ReactiveValue.new({1, 2, 3}, true) --- test:onFieldChange("1", function(value) --- print("test.1 changed to " .. value) --- end) --- test[1] = 4 -- test.1 changed to 4 --- test[4] = {1, 2, 3} -- Does not trigger callback -- ``` ---@class ReactiveValue ---@field _listeners table ---@field _fieldListeners table> ---@field _anyFieldListeners table> ---@field _oneTimeListeners table ---@field _value any ---@field _type string ---@field _recursive boolean? ReactiveValue = { ---#### Get the underlying value of a ReactiveValue ---@param self ReactiveValue ---@return any get = function(self) end, ---### Set the underlying value of a ReactiveValue triggering listener callbacks ---@param self ReactiveValue ---@param newValue any set = function(self, newValue) end, ---## EVENT ---### Register a listener that is triggered whenever the underlying value changes --- Returns a function that can be called to undo the callback ---@param self ReactiveValue ---@param callback fun(value: any, type: string) ---@return fun(): nil onChange = function(self, callback) end, ---## EVENT ---### Register a listener that is triggered whenever a specific field of a table changes --- Returns a function that can be called to undo the callback ---@param self ReactiveValue ---@param field string ---@param callback fun(field: string[], value: any, type: string) ---@return fun(): nil onFieldChange = function(self, field, callback) end, ---## EVENT ---### Register a listener that is triggered whenever any field of a table changes --- Returns a function that can be called to undo the callback ---@param self ReactiveValue ---@param callback fun(field: string[], value: any, type: string) ---@param depth number? How deep to listen for changes ---@return fun(): nil onAnyFieldChange = function(self, callback, depth) end, ---## EVENT ---### Register a listener that is triggered ONCE whenever the underlying value changes --- Returns a function that can be called to undo the callback ---@param self ReactiveValue ---@param callback fun(value: any, type: string) ---@return fun(): nil once = function(self, callback) end, ---### Setup listeners for all fields of a table recursively --- This is used to ensure that listeners are notified recursively ---@param self ReactiveValue _setupAllListenersRecursively = function(self) end, ---### Setup listeners for a specific field of a table recursively --- This is used to ensure that listeners are notified recursively ---@param self ReactiveValue _setupListeners = function(self, key, value, recursive) end, ---### Notify listeners that the underlying value has changed ---@param self ReactiveValue ---@return nil ---#### Event contains: --- 2. value: any - The new value of the changed field --- 3. type: string - The type of the new value of the changed field _notify = function(self) end, ---### Notify listeners that a specific field of the underlying value has changed ---#### Event contains: --- 1. field: table - A list of keys that lead to the changed field --- 2. value: any - The new value of the changed field --- 3. type: string - The type of the new value of the changed field ---@param self ReactiveValue _notifyFieldChanged = function(self, field) end, ---### Notify listeners that any field of the underlying value has changed ---#### Event contains: --- 1. field: table - A list of keys that lead to the changed field --- 2. value: any - The new value of the changed field --- 3. type: string - The type of the new value of the changed field _notifyAnyFieldChanged = function(self, field) end, } ---### Constructor ---@param initialValue any ---@param recursive boolean? ---@return ReactiveValue ReactiveValue.new = function(initialValue, recursive) local self = setmetatable({}, metadata) self._listeners = {} self._fieldListeners = {} self._anyFieldListeners = {} self._oneTimeListeners = {} self._value = initialValue self._type = type(initialValue) self._recursive = recursive or false ---@return any self.get = function(self) return self._value end ---@param newValue any self.set = function(self, newValue) if self._value == newValue then return end if type(newValue) ~= self._type then error("Expected " .. self._type .. ", got " .. type(newValue)) return end self._value = newValue self:_notify() end self.onChange = function(self, callback) if type(callback) ~= "function" then error("Expected function, got " .. type(callback)) return function() end end self._listeners[callback] = true return function() self._listeners[callback] = nil end end self.onFieldChange = function(self, field, callback) if type(callback) ~= "function" then error("Expected function, got " .. type(callback)) return function() end end if self._fieldListeners[field] == nil then self._fieldListeners[field] = {} end self._fieldListeners[field][callback] = true return function() self._fieldListeners[field][callback] = nil end end self.onAnyFieldChange = function(self, callback, depth) depth = depth or 99999 if type(callback) ~= "function" then error("Expected function, got " .. type(callback)) return function() end end if self._anyFieldListeners[depth] == nil then self._anyFieldListeners[depth] = {} end self._anyFieldListeners[depth][callback] = true return function() self._anyFieldListeners[depth][callback] = nil end end self.once = function(self, callback) if type(callback) ~= "function" then error("Expected function, got " .. type(callback)) return function() end end self._oneTimeListeners[callback] = true return function() self._oneTimeListeners[callback] = nil end end self._setupAllListenersRecursively = function(self) if self._type ~= "table" then return end for key, value in pairs(self._value) do self:_setupListeners(key, value, true) end end ---@param key string ---@param value any ---@param recursive boolean? self._setupListeners = function(self, key, value, recursive) recursive = recursive or false if self._type ~= "table" then return end if getmetatable(value) ~= getmetatable(self) then return end value._recursive = true if value._type == "table" then value:onAnyFieldChange(function(key2) ChangedKey = { key, table.unpack(key2) } self:_notifyFieldChanged(ChangedKey) self:_notifyAnyFieldChanged(ChangedKey) end) else value:onChange(function(newVal) ChangedKey = { key } self:_notifyFieldChanged(ChangedKey) self:_notifyAnyFieldChanged(ChangedKey) end) end if recursive then value:_setupAllListenersRecursively() end end if recursive then self:_setupAllListenersRecursively() end self._notify = function(self) for listener, _ in pairs(self._oneTimeListeners) do -- task.spawn(listener, self._value, self._type) listener(self._value, self._type) self._oneTimeListeners[listener] = nil end for listener, _ in pairs(self._listeners) do -- task.spawn(listener, self._value, self._type) listener(self._value, self._type) end end -- TODO: Maybe implement some sort of regex here or something... -- Such as listening to *.field1 or something -- But this (having to loop over listeners and evaluate some condition) would tank performance -- Compared to a simple lookup -- So I'm not going to do anything about it for now, until I figure out a better way ---@param field table A list of keys that lead to the changed field ---@return nil self._notifyFieldChanged = function(self, field) local value = self._value for _, key in ipairs(field) do value = value[key] end local strfield = table.concat(field, ".") if self._fieldListeners[strfield] == nil then return end for listener, _ in pairs(self._fieldListeners[strfield]) do -- task.spawn(listener, value, type(value)) listener(value, type(value)) end end ---@param self ReactiveValue ---@param field table A list of keys that lead to the changed field ---@return nil self._notifyAnyFieldChanged = function(self, field) local value = self._value for _, key in ipairs(field) do value = value[key] end local keyDepth = #field for listenerDepth, listeners in pairs(self._anyFieldListeners) do if listenerDepth >= keyDepth then for listener, _ in pairs(listeners) do -- The reason this also returns type(value) is so that clients don't have to compute type(value) -- I assume some of them might want to do it so computing it once is probably better than having every client compute it for themselves -- task.spawn(listener, field, value, type(value)) listener(field, value, type(value)) end end end end self._setupComplete = true return self end -- S -- begintest -- S local invocations = 0 -- S -- Integer example -- S local test = ReactiveValue.new(1) -- S test:onChange(function(value) -- S invocations = invocations + 1 -- S print("test changed to " .. value) -- S end) -- S test:set(2) -- S assert(invocations == 1) -- S -- S invocations = 0 -- String example -- S test = ReactiveValue.new("test") -- S test:onChange(function(value) -- S invocations = invocations + 1 -- S print("test changed to " .. value) -- S end) -- S test:set("test2") -- S assert(invocations == 1) -- S -- S -- Type safety example -- S local res, err = pcall(test.set, test, 1) -- S assert(res == false) -- S assert(err:find("Expected string, got number")) -- S -- S -- Table example -- S invocations = 0 -- S test = ReactiveValue.new({1, 2, 3}) -- S local clbk = test:onChange(function(value) -- S invocations = invocations + 1 -- S print("test changed to") -- S dumpTable(value, 0) -- S end) -- S test:set({1, 2, 3, 4}) -- S assert(invocations == 1) -- S -- S -- Callback removal example -- S clbk() -- S -- S invocations = 0 -- S -- Any field change example -- S clbk = test:onAnyFieldChange(function(field, value) -- S invocations = invocations + 1 -- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value)) -- S end) -- S test.Pero = 1 -- S test.Pero = nil -- S assert(invocations == 2) -- S clbk() -- S -- S invocations = 0 -- S -- Field change example -- S test:onFieldChange("Pero", function(value) -- S invocations = invocations + 1 -- S print("test.Pero changed to " .. value) -- S end) -- S test.Pero = 2 -- S assert(invocations == 1) -- S -- S invocations = 0 -- S -- One time listener example -- S test:once(function(value) -- S invocations = invocations + 1 -- S print("test changed to") -- S dumpTable(value, 0) -- S end) -- S test:set({3, 2, 1}) -- S assert(invocations == 1) -- S -- S invocations = 0 -- S -- Table push example -- S test = ReactiveValue.new({}) -- S test:onChange(function(value) -- S invocations = invocations + 1 -- S print("test changed to") -- S dumpTable(value, 0) -- S end) -- S test:onAnyFieldChange(function(field, value) -- S invocations = invocations + 1 -- S print("test." .. table.concat(field, ".") .. " changed to " .. value) -- S end) -- S test[#test + 1] = 4 -- S assert(invocations == 2) -- S -- S invocations = 0 -- S test = ReactiveValue.new({ -- S name = "pero", -- S coins = ReactiveValue.new(1) -- S }) -- S test.coins:onChange(function(value) -- S invocations = invocations + 1 -- S print("test.coins changed to " .. value) -- S end) -- S test.coins:set(2) -- S assert(invocations == 1) -- S -- S invocations = 0 -- S test = ReactiveValue.new({ -- S name = "pero", -- S coins = ReactiveValue.new(1) -- S }, true) -- S test:onAnyFieldChange(function(field, value) -- S invocations = invocations + 1 -- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value)) -- S end) -- S test.coins:set(2) -- S test.pero2 = ReactiveValue.new({}) -- S test.pero2.coins = ReactiveValue.new(1) -- S test.pero2.coins:set(2) -- S assert(invocations == 4) -- S -- S invocations = 0 -- S test = ReactiveValue.new({ -- S name = "pero", -- S coins = ReactiveValue.new({ -- S value = ReactiveValue.new(1) -- S }) -- S }, true) -- S test:onAnyFieldChange(function(field, value) -- S invocations = invocations + 1 -- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value)) -- S end) -- S test.coins.value:set(2) -- S assert(invocations == 1) -- S -- S invocations = 0 -- S test = ReactiveValue.new({}, true) -- S test.coins = ReactiveValue.new({}) -- S test.coins.value = ReactiveValue.new(1) -- S test:onAnyFieldChange(function(field, value) -- S invocations = invocations + 1 -- S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value)) -- S end) -- S test.coins.value:set(3) -- S assert(invocations == 1) --S --S invocations = 0 --S test = ReactiveValue.new({}, true) --S test:onAnyFieldChange(function(field, value) --S invocations = invocations + 1 --S print("test." .. table.concat(field, ".") .. " changed to " .. tostring(value)) --S end, 1) --S test.test2 = ReactiveValue.new({}, true) --S test.test2.test3 = ReactiveValue.new(1) --S assert(invocations == 1) --S -- S -- endtest end local frame = CreateFrame("Frame") frame:RegisterEvent("PLAYER_LOGIN") frame:RegisterEvent("PLAYER_ENTERING_WORLD") frame:RegisterEvent("GUILD_ROSTER_UPDATE") frame:SetScript("OnEvent", function(self, event, ...) Init() end) Init()