Components
You can use functions to create self-contained, reusable blocks of code. In the world of UI, you may think of them as components - though they can be used for much more than just UI.
For example, consider this function, which generates a button based on some
props
the user passes in:
type UsedAs<T> = Fusion.UsedAs<T>
local function Button(
scope: Fusion.Scope,
props: {
Position: UsedAs<UDim2>?,
AnchorPoint: UsedAs<Vector2>?,
Size: UsedAs<UDim2>?,
LayoutOrder: UsedAs<number>?,
ButtonText: UsedAs<string>
}
)
return scope:New "TextButton" {
BackgroundColor3 = Color3.new(0, 0.25, 1),
Position = props.Position,
AnchorPoint = props.AnchorPoint,
Size = props.Size,
LayoutOrder = props.LayoutOrder,
Text = props.ButtonText,
TextSize = 28,
TextColor3 = Color3.new(1, 1, 1),
[Children] = UICorner { CornerRadius = UDim2.new(0, 8) }
}
end
You can call this function later to generate as many buttons as you need.
local helloBtn = Button(scope, {
ButtonText = "Hello",
Size = UDim2.fromOffset(200, 50)
})
helloBtn.Parent = Players.LocalPlayer.PlayerGui.ScreenGui
Since the scope
is the first parameter, it can even be used with scoped()
syntax.
local scope = scoped(Fusion, {
Button = Button
})
local helloBtn = scope:Button {
ButtonText = "Hello",
Size = UDim2.fromOffset(200, 50)
}
helloBtn.Parent = Players.LocalPlayer.PlayerGui.ScreenGui
This is the primary way of writing components in Fusion. You create functions
that accept scope
and props
, then return some content from them.
Properties¶
If you don't say what props
should contain, it might be hard to figure
out how to use it.
You can specify your list of properties by adding a type to props
, which gives
you useful autocomplete and type checking.
local function Cake(
-- ... some stuff here ...
props: {
Size: Vector3,
Colour: Color3,
IsTasty: boolean
}
)
-- ... some other stuff here ...
end
Note that the above code only accepts constant values, not state objects. If you
want to accept either a constant or a state object, you can use the
UsedAs
type.
type UsedAs<T> = Fusion.UsedAs<T>
local function Cake(
-- ... some stuff here ...
props: {
Size: UsedAs<Vector3>,
Colour: UsedAs<Color3>,
IsTasty: UsedAs<boolean>
}
)
-- ... some other stuff here ...
end
This is usually what you want, because it means the user can easily switch
a property to dynamically change over time, while still writing properties
normally when they don't change over time. You can mostly treat UsedAs
properties like they're state objects, because functions like peek()
and
use()
automatically choose the right behaviour for you.
You can use the rest of Luau's type checking features to do more complex things, like making certain properties optional, or restricting that values are valid for a given property. Go wild!
Be mindful of the angle brackets
Remember that, when working with UsedAs
, you should be mindful of whether
you're putting things inside the angled brackets, or outside of them.
Putting some things inside of the angle brackets can change their meaning,
compared to putting them outside of the angle brackets.
Consider these two type definitions carefully:
-- A Vector3, or a state object storing Vector3, or nil.
UsedAs<Vector3>?
-- A Vector3?, or a state object storing Vector3?
UsedAs<Vector3?>
The first type is best for optional properties, where you provide a default value if it isn't specified by the user. If the user does specify it, they're forced to always give a valid value for it.
The second type is best if the property understands nil
as a valid value.
This means the user can set it to nil
at any time.
Scopes¶
In addition to props
, you should also ask for a scope
. The scope
parameter should come first, so that your users can use scoped()
syntax to
create it.
-- barebones syntax
local thing = Component(scope, {
-- ... some properties here ...
})
-- scoped() syntax
local thing = scope:Component {
-- ... some properties here ...
}
It's a good idea to provide a type for scope
. This lets you specify what
methods you need the scope to have.
scope: Fusion.Scope<YourMethodsHere>
If you don't know what methods to ask for, consider these two strategies.
-
If you use common methods (like Fusion's constructors) then it's a safe assumption that the user will also have those methods. You can ask for a scope with those methods pre-defined.
local function Component( scope: Fusion.Scope, props: {} ) return scope:New "Thing" { -- ... rest of code here ... } end
-
If you need more specific or niche things that the user likely won't have (for example, components you use internally), then you should not ask for those. Instead, create a new inner scope with the methods you need.
local function Component( scope: Fusion.Scope, props: {} ) local scope = scope:innerScope { SpecialThing1 = require(script.SpecialThing1), SpecialThing2 = require(script.SpecialThing2), } return scope:SpecialThing1 { -- ... rest of code here ... } end
If you're not sure which strategy to pick, the second is always a safe fallback, because it assumes less about your users and helps hide implementation details.
Modules¶
It's common to save different components inside of different files. There's a number of advantages to this:
- it's easier to find the source code for a specific component
- it keep each file shorter and simpler
- it makes sure components are properly independent, and can't interfere
- it encourages reusing components everywhere, not just in one file
Here's an example of how you could split up some components into modules:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|