diff --git a/docs/api-reference/roblox/members/ref.md b/docs/api-reference/roblox/members/ref.md deleted file mode 100644 index 9bcf45013..000000000 --- a/docs/api-reference/roblox/members/ref.md +++ /dev/null @@ -1,30 +0,0 @@ - - -

- :octicons-workflow-24: - Ref - - : SpecialKey - -

- -```Lua -Fusion.Ref: SpecialKey -``` - -A [special key](../../types/specialkey) which outputs the instance it's being -applied to. - -When paired with a [value object](../../../state/types/value) in a -[property table](../../types/propertytable), the special key sets the value to -the instance. - ------ - -## Learn More - -- [References tutorial](../../../../tutorials/roblox/references) \ No newline at end of file diff --git a/docs/api-reference/state/types/value.md b/docs/api-reference/state/types/value.md index 65cfa665f..697456cf2 100644 --- a/docs/api-reference/state/types/value.md +++ b/docs/api-reference/state/types/value.md @@ -44,19 +44,21 @@ can be used to tell types of state object apart.

set - -> () + -> T

```Lua function Value:set( newValue: T -): () +): T ``` -Updates the value of this state object. +Updates the value of this state object. Other objects using the value are +notified of the change. -Other objects using the value are notified immediately of the change. +The `newValue` is always returned, so that `:set()` can be used to capture +values inside of expressions. ----- diff --git a/docs/tutorials/best-practices/references.md b/docs/tutorials/best-practices/references.md new file mode 100644 index 000000000..0f620c1bb --- /dev/null +++ b/docs/tutorials/best-practices/references.md @@ -0,0 +1,116 @@ +At some point, you might need to refer to another part of the UI. There are +various techniques that can let you do this. + +```Lua hl_lines="8" +local ui = scope:New "Folder" { + [Children] = { + scope:New "SelectionBox" { + -- the box should adorn to the part, but how do you reference it? + Adornee = ???, + }, + scope:New "Part" { + Name = "Selection Target", + } + } +} +``` + +----- + +## Constants + +The first technique is simple - instead of creating the UI all at once, you can +extract part of the UI that you want to reference later. + +In practice, that means you'll move some of the creation code into a new `local` +constant, so that you can refer to it later by name. + +```Lua +-- the part is now constructed first, whereas before it was constructed second +local selectionTarget = scope:New "Part" { + Name = "Selection Target", +} + +local ui = scope:New "Folder" { + [Children] = { + scope:New "SelectionBox" { + Adornee = selectionTarget + }, + selectionTarget + } +} +``` + +While this is a simple and robust technique, it has some disadvantages: + +- By moving parts of your UI code into different local variables, your UI will +be constructed in a different order based on which local variables come first +- Refactoring code in this way can be bothersome and inelegant, disrupting the +structure of the code +- You can't have two pieces of UI refer to each other cyclically + +Constants work well for trivial examples, but you should consider a more +flexible technique if those disadvantages are relevant. + +----- + +## Value Objects + +Where it's impossible or inelegant to use named constants, you can use +[value objects](../../fundamentals/values) to easily set up references. + +Because their `:set()` method returns the value that's passed in, you can use +`:set()` to reference part of your code without disrupting its structure: + +```Lua +-- `selectionTarget` will show as `nil` to all code trying to use it, until the +-- `:set()` method is called later on. +local selectionTarget = scope:Value(nil :: Part?) + +local ui = scope:New "Folder" { + [Children] = { + scope:New "SelectionBox" { + Adornee = selectionTarget + }, + selectionTarget:set( + scope:New "Part" { + Name = "Selection Target", + } + ) + } +} +``` + +It's important to note that the value object will briefly be `nil` (or whichever +default value you provide in the constructor). This is because it takes time to +reach the `:set()` call, so any in-between code will see the `nil`. + +In the above example, the `Adornee` is briefly set to `nil`, but because +`selectionTarget` is a value object, it will change to the part instance when +the `:set()` method is called. + +While dealing with the brief `nil` value can be annoying, it is also useful, +because this lets you refer to parts of your UI that haven't yet been created. +In particular, this lets you create cyclic references. + +```Lua +local aliceRef = scope:Value(nil :: Instance?) +local bobRef = scope:Value(nil :: Instance?) + +-- These two `ObjectValue` instances will refer to each other once the code has +-- finished running. +local alice = aliceRef:set( + scope:New "ObjectValue" { + Value = bobRef + } +) +local bob = bobRef:set( + scope:New "ObjectValue" { + Value = aliceRef + } +) +``` + +Value objects are generally easier to work with than named constants, so they're +often used as the primary way of referencing UI, but feel free to mix both +techniques based on what your code needs. \ No newline at end of file diff --git a/docs/tutorials/fundamentals/values.md b/docs/tutorials/fundamentals/values.md index 1b3b1323a..5bdf7e3cc 100644 --- a/docs/tutorials/fundamentals/values.md +++ b/docs/tutorials/fundamentals/values.md @@ -53,6 +53,21 @@ health:set(25) print(peek(health)) --> 25 ``` +??? tip "`:set()` returns the value you give it" + You can use `:set()` in the middle of calculations: + + ```Lua + local myNumber = scope:Value(0) + local computation = 10 + myNumber:set(2 + 2) + print(computation) --> 14 + print(peek(myNumber)) --> 4 + ``` + + This is useful when building complex expressions. On a later page, you'll + see one such use case. + + Generally though, it's better to keep your expressions simple. + Value objects are Fusion's simplest 'state object'. State objects contain a single value - their *state*, you might say - and that single value can be read out at any time using `peek()`. diff --git a/docs/tutorials/roblox/references.md b/docs/tutorials/roblox/references.md deleted file mode 100644 index 047307264..000000000 --- a/docs/tutorials/roblox/references.md +++ /dev/null @@ -1,89 +0,0 @@ -The `[Ref]` key allows you to save a reference to an instance you're hydrating -or creating. - -```Lua -local myRef = scope:Value() - -local thing = scope:New "Part" { - [Ref] = myRef -} - -print(peek(myRef)) --> Part -print(peek(myRef) == thing) --> true -``` - ------ - -## Usage - -`Ref` doesn't need a scope - import it into your code from Fusion directly. - -```Lua linenums="1" hl_lines="2" -local Fusion = require(ReplicatedStorage.Fusion) -local Ref = Fusion.Ref -``` - -When creating an instance with `New`, `[Ref]` will save that instance to a value -object. - -```Lua -local myRef = scope:Value() - -scope:New "Part" { - [Ref] = myRef -} - -print(peek(myRef)) --> Part -``` - -Among other things, this allows you to refer to instances from other instances. - -```Lua -local myPart = scope:Value() - -New "SelectionBox" { - -- the selection box should adorn to the part - Adornee = myPart -} - -New "Part" { - -- sets `myPart` to this part, which sets the adornee to this part - [Ref] = myPart -} -``` - -You can also get references to instances from deep inside function calls. - -```Lua --- this will refer to the part, once we create it -local myPart = scope:Value() - -scope:New "Folder" { - [Children] = scope:New "Folder" { - [Children] = scope:New "Part" { - -- save a reference into the value object - [Ref] = myPart - } - } -} -``` - -!!! warning "Nil hazard" - Before the part is created, the `myPart` value object will be `nil`. Be - careful not to use it before it's created. - - If you need to know about the instance ahead of time, you should create the - instance early, and parent it in later, when you create the rest of the - instances. - - ```Lua - -- build the part elsewhere, so it can be saved to a variable - local myPart = scope:New "Part" {} - - local folders = scope:New "Folder" { - [Children] = scope:New "Folder" { - -- parent the part into the folder here - [Children] = myPart - } - } - ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 5ea624260..251f3c007 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,10 +77,10 @@ nav: - Events: tutorials/roblox/events.md - Change Events: tutorials/roblox/change-events.md - Outputs: tutorials/roblox/outputs.md - - References: tutorials/roblox/references.md - Best Practices: - Components: tutorials/best-practices/components.md - Instance Handling: tutorials/best-practices/instance-handling.md + - References: tutorials/best-practices/references.md - Callbacks: tutorials/best-practices/callbacks.md - State: tutorials/best-practices/state.md - Sharing Values: tutorials/best-practices/sharing-values.md @@ -153,7 +153,6 @@ nav: - OnChange: api-reference/roblox/members/onchange.md - OnEvent: api-reference/roblox/members/onevent.md - Out: api-reference/roblox/members/out.md - - Ref: api-reference/roblox/members/ref.md - Animation: - Types: - Animatable: api-reference/animation/types/animatable.md diff --git a/src/Instances/Ref.luau b/src/Instances/Ref.luau deleted file mode 100644 index 69e1c91a8..000000000 --- a/src/Instances/Ref.luau +++ /dev/null @@ -1,43 +0,0 @@ ---!strict ---!nolint LocalUnused ---!nolint LocalShadow -local task = nil -- Disable usage of Roblox's task scheduler - ---[[ - A special key for property tables, which stores a reference to the instance - in a user-provided Value object. -]] - -local Package = script.Parent.Parent -local Types = require(Package.Types) -local External = require(Package.External) -local isState = require(Package.State.isState) -local whichLivesLonger = require(Package.Memory.whichLivesLonger) - -return { - type = "SpecialKey", - kind = "Ref", - stage = "observer", - apply = function( - self: Types.SpecialKey, - scope: Types.Scope, - value: unknown, - applyTo: Instance - ) - if not isState(value) then - External.logError("invalidRefType") - end - local value = value :: Types.StateObject - if value.kind ~= "Value" then - External.logError("invalidRefType") - end - local value = value :: Types.Value - - if value.scope == nil then - External.logError("useAfterDestroy", nil, "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) - elseif whichLivesLonger(scope, applyTo, value.scope, value.oldestTask) == "definitely-a" then - External.logWarn("possiblyOutlives", "The Value object, which [Ref] outputs to,", `the {applyTo} instance`) - end - value:set(applyTo) - end -} :: Types.SpecialKey \ No newline at end of file diff --git a/src/State/Value.luau b/src/State/Value.luau index d56ae731d..d1e724f26 100644 --- a/src/State/Value.luau +++ b/src/State/Value.luau @@ -33,13 +33,14 @@ local CLASS_METATABLE = {__index = class} function class:set( newValue: unknown, force: boolean? -) +): unknown local self = self :: InternalTypes.Value local oldValue = self._value if force or not isSimilar(oldValue, newValue) then self._value = newValue updateAll(self) end + return newValue end --[[ diff --git a/src/Types.luau b/src/Types.luau index 3b7c1a659..f8b294286 100644 --- a/src/Types.luau +++ b/src/Types.luau @@ -99,8 +99,8 @@ export type Use = (target: UsedAs) -> T -- A state object whose value can be set at any time by the user. export type Value = StateObject & { kind: "State", - set: (Value, newValue: S, force: boolean?) -> (), - ____phantom_setType: (never) -> S -- phantom data so this contains a T + set: (Value, newValue: S, force: boolean?) -> S, + ____phantom_setType: (never) -> S -- phantom data so this contains a T } export type ValueConstructor = ( scope: Scope, @@ -256,7 +256,6 @@ export type Fusion = { New: NewConstructor, Hydrate: HydrateConstructor, - Ref: SpecialKey, Child: ({Child}) -> Child, Children: SpecialKey, Out: (propertyName: string) -> SpecialKey, diff --git a/src/init.luau b/src/init.luau index 87759eb2d..841f392b4 100644 --- a/src/init.luau +++ b/src/init.luau @@ -70,7 +70,6 @@ local Fusion: Types.Fusion = table.freeze { OnChange = require(script.Instances.OnChange), OnEvent = require(script.Instances.OnEvent), Out = require(script.Instances.Out), - Ref = require(script.Instances.Ref), -- Animation Tween = require(script.Animation.Tween), diff --git a/test/Spec/Instances/Ref.spec.luau b/test/Spec/Instances/Ref.spec.luau deleted file mode 100644 index ae0319eaa..000000000 --- a/test/Spec/Instances/Ref.spec.luau +++ /dev/null @@ -1,30 +0,0 @@ ---!strict ---!nolint LocalUnused -local task = nil -- Disable usage of Roblox's task scheduler - -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local Fusion = ReplicatedStorage.Fusion - -local New = require(Fusion.Instances.New) -local Ref = require(Fusion.Instances.Ref) -local Value = require(Fusion.State.Value) -local peek = require(Fusion.State.peek) -local doCleanup = require(Fusion.Memory.doCleanup) - -return function() - local it = getfenv().it - - it("should set State objects passed as [Ref]", function() - local expect = getfenv().expect - - local scope = {} - local refValue = Value(scope, nil) - - local child = New(scope, "Folder") { - [Ref] = refValue - } - - expect(peek(refValue)).to.equal(child) - doCleanup(scope) - end) -end diff --git a/test/Spec/State/Value.spec.luau b/test/Spec/State/Value.spec.luau index f945bb425..0538b4bb6 100644 --- a/test/Spec/State/Value.spec.luau +++ b/test/Spec/State/Value.spec.luau @@ -59,4 +59,26 @@ return function() doCleanup(scope) end) + + + it("always returns the set value", function() + local expect = getfenv().expect + + local scope = {} + local value = Value(scope, 0 :: string | number) + + local foo = value:set(10) + expect(foo).to.equal(10) + + local bar = value:set(10) + expect(bar).to.equal(10) + + local baz = value:set(25) + expect(baz).to.equal(25) + + local garb = value:set(25) + expect(garb).to.equal(25) + + doCleanup(scope) + end) end