diff --git a/docs/docs.md b/docs/docs.md index 0af18df..64d96ee 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -7,7 +7,7 @@ Creates a new state object. Accepts an optional `InitialState` parameter, for de ## `State:Set()` ----- -Sets the value of a given key in the state, and then fires off any `Changed` signals. You should always use this when you need to change the state. Never modify state directly, unless using `RawSet`! +Sets the value of a given key in the state, and then fires off any `Changed` signals. You should always use this when you need to change the state. Use `:RawSet()` to change values without invoking change events. ### Syntax `State:Set(Key: any, Value: any): void` @@ -16,9 +16,6 @@ Sets the value of a given key in the state, and then fires off any `Changed` sig ----- Set multiple values in the state. `Changed` signals will be fired for each modified key. -!!! warning - Setting sub-tables will fully overwrite their contents in the state. This method uses shallow-merging, which only merges the values at the root of the state. Use :Get() and append/overwrite keys where required, and set the modified table when storing tables. - ### Syntax `State:SetState(StateTable: Dictionary): void` @@ -29,21 +26,29 @@ local State = BasicState.new({ Greetings = { Place = "Welcome to the Mountain!", Roblox = "Hey Roblox!", - Me = "Hi ClockworkSquirrel!" + Me = "Hi csqrl!" } }) -local function ChangeLocations(NewLocation) - local NewGreetings = State:Get("Greetings") - NewGreetings.Place = string.format("Hello %s!", NewLocation) +State:SetState({ + Location = "City", + Greetings = { + Place = "Welcome to the City!" + } +}) - State:SetState({ - Location = NewLocation, - Greetings = NewGreetings - }) -end +--[[ + The new state object will look like this: -ChangeLocations("City") + { + Location = "City", + Greetings = { + Place = "Welcome to the City!", + Roblox = "Hey Roblox!", + Me = "Hi csqrl!" + } + } +--]] ``` ## `State:Toggle()` @@ -86,7 +91,7 @@ local State = BasicState.new({ local function BuyItem(ItemName, ItemPrice) -- A cap of 0 was specified to prevent Money from going below 0 State:Decrement("Money", ItemPrice, 0) - print(("Bought %s for %d"):format(ItemName, ItemPrice)) + print(string.format("Bought %s for %d", ItemName, ItemPrice)) end BuyItem("Noodles", 12) @@ -164,6 +169,9 @@ There's a full example within the `/examples` directory on how to use BasicState ----- An [RBXScriptSignal](https://developer.roblox.com/en-us/api-reference/datatype/RBXScriptSignal) which is fired any time the state mutates. The Event fires with the following values (in order): +!!! warning + Using `:GetChangedSignal()` is the preferred method for listening to state changes. + | Name | Type | Description | |-----------------|--------------------------------|-------------------------------------------------------| | `OldState` | `Dictionary` | The entire state object prior to mutation. | diff --git a/src/init.lua b/src/init.lua index 0ed80e0..fae5c94 100644 --- a/src/init.lua +++ b/src/init.lua @@ -1,23 +1,51 @@ --[[ - BasicState by ClockworkSquirrel - Version: 0.1.0 + BasicState by csqrl (ClockworkSquirrel) + Version: 0.1.1 Documentation is at: https://clockworksquirrel.github.io/BasicState/ + + Overview of Methods: + BasicState.new([ InitialState: Dictionary = {} ]): State + + State:Set(Key: any, Value: any): void + State:SetState(StateTable: Dictionary): void + State:Toggle(Key: any): void + State:Increment(Key: any[, Amount: Number = 1][, Cap: Number = nil]): void + State:Decrement(Key: any[, Amount: Number = 1][, Cap: Number = nil]): void + State:RawSet(Key: any, Value: any): void + State:Get(Key: any[, DefaultValue: any = nil]): any + State:GetState(): Dictionary + State:GetChangedSignal(Key: any): RBXScriptSignal + State:Destroy(): void + State:Roact(Component: Roact.Component[, Keys: any[] = nil]): Roact.Component + + State.Changed: RBXScriptSignal --]] local State = {} --[[ - Helper function which creates a shallow copy of passed tables. - Child tables will not be copied, and passed ByRef, meaning - modifying them will affect the original copy + Helper function which creates a deep copy of passed tables. + + In v0.1.1, JoinDictionary now performs a deep copy of tables. This + allows nested tables within state to be modified without losing + original data. --]] local function JoinDictionary(...) local NewDictionary = {} for _, Dictionary in next, { ... } do + if (type(Dictionary) ~= "table") then + continue + end + for Key, Value in next, Dictionary do + if (type(Value) == "table") then + NewDictionary[Key] = JoinDictionary(NewDictionary[Key], Value) + continue + end + NewDictionary[Key] = Value end end @@ -66,7 +94,7 @@ function State.new(InitialState) end --[[ - Return a shallow copy of the current stored state + Return a deep copy of the current stored state --]] function State:GetState() return JoinDictionary(self.__state, {}) @@ -86,6 +114,10 @@ end function State:Set(Key, Value) local OldState = self:GetState() + if (type(Value) == "table") then + Value = JoinDictionary(OldState[Key], Value) + end + if (OldState[Key] ~= Value) then self:RawSet(Key, Value) self.__changeEvent:Fire(OldState, Key) @@ -94,13 +126,8 @@ end --[[ Like React's setState method, SetState accepts a table of key-value pairs, - which will be added to or mutated in the store. This is a shallow-merge, - and therefore sub-tables will be fully overwritten by whatever value - is specified using this method. - - Be sure to Get() a copy of the currently stored table, overwrite or append - relevant keys, and pass the modified table into this method, when setting - table values. + which will be added to or mutated in the store. This is a deep copy, so + original data will not be overwritten unless specified. --]] function State:SetState(StateTable) assert(type(StateTable) == "table")