mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Initial commit
This commit is contained in:
commit
e76234feb5
52 changed files with 5235 additions and 0 deletions
56
docs/api/animation.md
Normal file
56
docs/api/animation.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Animation API
|
||||
|
||||
<br/>
|
||||
|
||||
## spring()
|
||||
|
||||
Returns a new state with an animated value of the original.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function spring<T>(state: State<T>, period: number, dampingRatio: number = 1): State<T>
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The output state's value is updated every frame based on the current input state's value.
|
||||
|
||||
The change is physically simulated according to a [spring](https://en.wikipedia.org/wiki/Simple_harmonic_motion).
|
||||
|
||||
`period` is the amount of time in seconds it takes for the spring to complete one full cycle
|
||||
|
||||
`dampingRatio` is relared to the amount of resistant force applied to the spring.
|
||||
|
||||
- \>1 = Overdamped (Not currently supported)
|
||||
- 1 = Critically damped
|
||||
- <1 = Underdamped
|
||||
- 0 = Undamped
|
||||
|
||||
Velocity is conserved between input state updates for smooth animation.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local state = wrap(1)
|
||||
|
||||
local springed = spring(state, 1, 1)
|
||||
```
|
||||
|
||||
<details><summary>Example of an animated counter</summary>
|
||||
|
||||
```lua
|
||||
local count = wrap(1000)
|
||||
|
||||
local function Counter(props)
|
||||
local tweenedCount = spring(count, 0.5, 1)
|
||||
|
||||
return create("TextLabel") {
|
||||
Text = "Count: " .. tweenedCount
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
-------------------------------------------------------------------
|
||||
325
docs/api/creation.md
Normal file
325
docs/api/creation.md
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
# Element Creation API
|
||||
|
||||
<br/>
|
||||
|
||||
## create()
|
||||
|
||||
Creates a new UI element, applying any given properties.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function create(classNameOrInstance: string | Instance): (properties: Map<string, any>) -> Instance
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The function can take either a `string` or an `Instance` as its first argument.
|
||||
|
||||
- If given a `string`, a new instance with the string class name will be created with default properties already applied.
|
||||
- If given an `Instance`, a new instance that is a clone of the given instance will be created.
|
||||
|
||||
This returns another function that is used to apply any properties to the new instance.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local frame = create("Frame") {
|
||||
Name = "NewFrame",
|
||||
Position = UDim2.fromScale(1, 0)
|
||||
}
|
||||
|
||||
-- creates a clone of `frame` with new properties applied.
|
||||
local frame2 = create(frame) {
|
||||
Size = UDim2.fromOffset(50, 100)
|
||||
}
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## apply()
|
||||
|
||||
Applies any given properties to a given instance.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function apply(instance: Instance): (properties: Map<string, any>) -> Instance
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
Applies properties in the same manner as `create` for already existing existances.
|
||||
|
||||
Can use symbols and bind state just like `create`.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local frame = Instance.new("Frame")
|
||||
|
||||
apply(frame) {
|
||||
Position = UDim2.fromScale(1, 0)
|
||||
}
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## Layout
|
||||
|
||||
Symbol used to pass layout properties to elements.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
type Layout = Symbol
|
||||
|
||||
type LayoutProps = {
|
||||
[Symbol] = {
|
||||
-- These are all properties considered to be "layout properties"
|
||||
AnchorPoint: Prop<Vector2>?;
|
||||
LayoutOrder: Prop<number>?;
|
||||
Position: Prop<UDim2>?;
|
||||
Rotation: Prop<number>?;
|
||||
Size: Prop<UDim2>?;
|
||||
SizeConstraint: Prop<Enum.SizeConstraint>?;
|
||||
Visible: Prop<boolean>?;
|
||||
ZIndex: Prop<number>?;
|
||||
}
|
||||
}
|
||||
|
||||
type Prop<T> = T | State<T>
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The primary purpose of this symbol is to enable easy passthrough of layout properties
|
||||
through user-defined component hierarchies.
|
||||
|
||||
It is recommended to only set layout properties using the `Layout` symbol when using
|
||||
your own components.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local function BlackFrame(props)
|
||||
return create("Frame") {
|
||||
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||
[Layout] = props[Layout]
|
||||
}
|
||||
end
|
||||
|
||||
BlackFrame {
|
||||
[Layout] = {
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
Size = UDim2.fromOffset(100, 50)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## Children
|
||||
|
||||
Symbol used to pass child instances to elements.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
type Children = Symbol
|
||||
|
||||
type ChildrenProps = {
|
||||
[Symbol] = ChildrenProp
|
||||
}
|
||||
|
||||
type ChildrenProp = Prop<Instance> | Array<ChildrenProp>
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
This symbol is flexible in the way that children can be passed in the form of nested arrays.
|
||||
|
||||
Children can also be assigned using a state binding.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
create("Frame") {
|
||||
-- all of the below are valid methods of assigning children
|
||||
[Children] = create("TextLabel") {},
|
||||
|
||||
[Children] = {
|
||||
create("TextLabel") {},
|
||||
create("TextLabel") {},
|
||||
},
|
||||
|
||||
[Children] = {
|
||||
create("TextLabel") {},
|
||||
{
|
||||
create("TextLabel") {},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## Event
|
||||
|
||||
Symbol used to connect callbacks to instance events.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
type Event = Map<string, Symbol>
|
||||
|
||||
type EventProps = {
|
||||
[Symbol] = Prop<(...unknown) -> ()>
|
||||
}
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The `Event` symbol can be indexed to get symbols to connect to specific events.
|
||||
|
||||
Event parameters are passed into the callback.
|
||||
|
||||
When callbacks are connected by binding to a state,
|
||||
connections are automatically disconnected when the state changes.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
create("TextButton") {
|
||||
[Event.Activated] = function()
|
||||
print("Clicked")
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## Changed
|
||||
|
||||
Symbol used to connect callbacks to instance property changed events.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
type Changed = Map<string, Symbol>
|
||||
|
||||
type ChangedProps = {
|
||||
[Symbol] = Prop<(...unknown) -> ()>
|
||||
}
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The `Changed` symbol can be indexed to get symbols to connect to specific events just like `Event`.
|
||||
|
||||
Event parameters are passed into the callback.
|
||||
|
||||
When callbacks are connected by binding to a state,
|
||||
connections are automatically disconnected when the state changes.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
create("TextBox") {
|
||||
[Changed.Text] = function()
|
||||
print("New text entered")
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## Bind
|
||||
|
||||
Symbol used to bind states to instance properties.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
type Bind = Map<string, Symbol>
|
||||
|
||||
type BindProps = {
|
||||
[Symbol] = State<any>
|
||||
}
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The `Bind` symbol can be indexed to bind specific properties just like `Event`.
|
||||
|
||||
Sets the given state value to the instance property value immediately after instance creation.
|
||||
|
||||
When an instance property is changed, the value of the given state will automatically
|
||||
be set to the new property. Effectively a shorthand for connecting a property changed event
|
||||
to set state values.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local text = wrap()
|
||||
|
||||
local box = create("TextBox") {
|
||||
[Bind.Text] = text
|
||||
}
|
||||
|
||||
box.Text = "New text"
|
||||
print(text.Value) -- "New text"
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## Created
|
||||
|
||||
Symbol used to run a callback once immediately after instance creation.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
type Created = Symbol
|
||||
|
||||
type CreatedProps = {
|
||||
[Symbol] = (Instance) -> ()
|
||||
}
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The instance being defined with the `Created` symbol is passed as the first
|
||||
argument to the callback.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local frame
|
||||
|
||||
create("Frame") {
|
||||
Name = "Background",
|
||||
[Created] = function(instance)
|
||||
frame = instance
|
||||
end
|
||||
}
|
||||
|
||||
print(frame.Name) -- "Background"
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
249
docs/api/reactivity-core.md
Normal file
249
docs/api/reactivity-core.md
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
# Reactivity API: Core
|
||||
|
||||
<br/>
|
||||
|
||||
## wrap()
|
||||
|
||||
Wraps and returns any given values with reactive state objects.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function wrap<T>(value: T): State<T>
|
||||
function wrap(value: ...unknown): ...State<any>
|
||||
|
||||
type State<T> = {
|
||||
Value: T,
|
||||
value: T
|
||||
}
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The state object has a single mutable field `.Value`.
|
||||
|
||||
Read operations to `.Value` are tracked and write operations can trigger
|
||||
dependency updates and watchers.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local count = wrap(0)
|
||||
|
||||
print(count.Value) -- 0
|
||||
|
||||
count.Value += 1
|
||||
|
||||
print(count.Value) -- 1
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## derive()
|
||||
|
||||
Derives a new reactive state object from an existing state object.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function derive<T>(
|
||||
(from) -> T,
|
||||
cleanup: (value: T) -> ()?
|
||||
): State<T>
|
||||
|
||||
function from<T>(T | State<T>): T
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The derived state will have its value recalculated when any state it derives from is updated.
|
||||
|
||||
Takes a callback that is immediately run to determine what states are being referenced. Only states referenced in the immediate function scope can trigger updates.
|
||||
|
||||
The state object returned by this function is readonly.
|
||||
|
||||
Has an optional cleanup parameter which takes a function that is called with the old value any
|
||||
time the derived state recalculates a value.
|
||||
|
||||
An optional utility function is passed as the first argument to the callback, if given a state, the value of the state will be returned (changes to this state still triggers updates unlike `unwrap`), if given a value the value is returned.
|
||||
|
||||
> ⚠️ The callback cannot yield.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local count = wrap(0)
|
||||
local text = derive(function() return "Count: "..count.Value end)
|
||||
|
||||
print(text.Value) -- "Count: 0"
|
||||
|
||||
count.Value += 1
|
||||
|
||||
print(text.Value) -- "Count: 1"
|
||||
```
|
||||
|
||||
```lua
|
||||
local count = wrap(0)
|
||||
local text = derive(function(from)
|
||||
return "Count: "..from(count)
|
||||
end)
|
||||
```
|
||||
|
||||
A shorthand method for deriving states also exists, following example is equivalent to the above:
|
||||
|
||||
```lua
|
||||
local count = wrap(0)
|
||||
local text = "Count: "..count -- all binary operators are supported
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## foreach()
|
||||
|
||||
Derives a new state object from an existing state object.
|
||||
Designed to work specifically with table states.
|
||||
|
||||
Also works with non state tables.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function foreach<KO, VO>( -- number as first arg
|
||||
i: number,
|
||||
transform: (key: number) -> (KO, VO),
|
||||
cleanup: (KO, VO) -> ()?
|
||||
): Map<KO, VO>
|
||||
|
||||
function foreach<KI, KO, VI, VO>( -- table as first arg
|
||||
table: Map<KI, VI>,
|
||||
transform: (key: KI, value: VI) -> (KO, VO),
|
||||
cleanup: (KO, VO) -> ()?
|
||||
): Map<KO, VO>
|
||||
|
||||
function foreach<KI, KO, VI, VO>( -- state as first arg
|
||||
state: State<Map<KI, VI>>,
|
||||
transform: (key: KI, value: VI) -> (KO, VO),
|
||||
cleanup: (KO, VO) -> ()?
|
||||
): State<Map<KO, VO>>
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
When the state being derived from is updated, the derived state will
|
||||
recompute by applying its transform function to each key-value pair.
|
||||
|
||||
Will only be recomputed if the corresponding key differs between updates.
|
||||
|
||||
Has an optional cleanup function to cleanup the old key and value.
|
||||
|
||||
> ⚠️ The transform function cannot yield.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local numbers = wrap { 1, 2, 3 }
|
||||
local plusOne = foreach(numbers, function(i, v)
|
||||
return i, v + 1
|
||||
end)
|
||||
|
||||
print(plusOne.Value) -- { [1]: 2, [2]: 3, [3]: 4 }
|
||||
|
||||
-- note that assignment must take place to trigger reactive updates.
|
||||
-- modifying the value without assignment `numbers.Value[2] = 5` will not trigger updates.
|
||||
numbers.Value = { 1, 5, 3 }
|
||||
|
||||
print(plusOne.Value) -- { [1]: 2, [2]: 6, [3]: 4 }
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## match()
|
||||
|
||||
Derives a new state object from an existing state object.
|
||||
Similar to switch statements in other languages.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function match<K, V>(value: K): (transform: Map<K, V>) -> V
|
||||
function Match<K, V>(state: State<K>): (transform: Map<K, V>) -> State<V>
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
When the state being derived from is updated, the derived state will
|
||||
recompute by using the input value as a key to map to an output value.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local state = wrap(true)
|
||||
local matched = match(state) {
|
||||
[true] = 1,
|
||||
[false] = 0
|
||||
}
|
||||
|
||||
print(matched.Value) -- 1
|
||||
|
||||
state.Value = false
|
||||
|
||||
print(matched.Value) -- 0
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## watch()
|
||||
|
||||
Runs a callback on state change.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function watch(callback: () -> Cleanup?): Unwatch
|
||||
|
||||
type Cleanup = () -> ()
|
||||
type Unwatch = () -> ()
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The callback is ran immediately to determine what states to watch.
|
||||
|
||||
Any time a state read in the watch callback is changed, the watcher callback will be deferred
|
||||
to the end of the resumption cycle and ran.
|
||||
|
||||
Only states in the immediate function scope can trigger the watch callback.
|
||||
|
||||
Watchers are run *before* UI properties are updated.
|
||||
|
||||
The callback can return an optional cleanup function that is run each time the watcher is rerun.
|
||||
|
||||
Also returns a function that when called, stops the watcher immediately (also runs cleanup if any was given).
|
||||
|
||||
> ⚠️ The callback cannot yield.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local state = wrap(1)
|
||||
|
||||
watch(function()
|
||||
print(state.Value)
|
||||
end)
|
||||
|
||||
-- prints 1
|
||||
|
||||
state.Value += 1
|
||||
|
||||
-- prints 2
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
152
docs/api/reactivity-utility.md
Normal file
152
docs/api/reactivity-utility.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Reactivity API: Utility
|
||||
|
||||
<br/>
|
||||
|
||||
## isState()
|
||||
|
||||
Determines if a given value is a state object or not.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function isState(value: unknown): boolean
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local value = wrap()
|
||||
|
||||
print(isState(value)) -- true
|
||||
|
||||
value = 0
|
||||
|
||||
print(isState(value)) -- false
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## unwrap()
|
||||
|
||||
Unwraps a state and returns its stored value.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function unwrap<T>(value: T | State<T>): T
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
If given a state, the state's stored value will be returned.
|
||||
|
||||
Unwrapping a state within a derived callback will not trigger updates.
|
||||
|
||||
Can be given a non-state value, in which case the same value will just be returned.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local state = wrap(1)
|
||||
|
||||
print(unwrap(state)) -- 1
|
||||
|
||||
print(unwrap(1)) -- 1
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## readonly()
|
||||
|
||||
Creates a new derived state with the same value as the state being derived from.
|
||||
|
||||
Used to create readonly states.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function readonly<T>(state: State<T>): State<T>
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local count = wrap(1)
|
||||
local read = readonly(count)
|
||||
|
||||
print(read.Value) -- 1
|
||||
|
||||
count.Value += 1
|
||||
|
||||
print(read.Value) -- 2
|
||||
|
||||
read.Value += 1 -- error
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## mutate()
|
||||
|
||||
Mutates a given state's value and updated any derived states.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
function mutate<T>(value: T | State<T>): T
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
Since states only update derived states if a new value is set (tables are compared by reference),
|
||||
this function serves as a way to trigger derived state updates if a state's value is not changed but
|
||||
instead mutated.
|
||||
|
||||
Can also take non-state as an argument.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local state = wrap { Count = 1 }
|
||||
|
||||
local derived = derive(function()
|
||||
return state.Value.Count
|
||||
end)
|
||||
|
||||
mutate(state, function(value)
|
||||
value.Count += 1
|
||||
end)
|
||||
|
||||
print(derived.Value) -- 2
|
||||
```
|
||||
|
||||
<details><summary>Motivation for this function</summary>
|
||||
|
||||
```lua
|
||||
local state = wrap { Count = 1 }
|
||||
|
||||
local derived = derive(function()
|
||||
return state.Value.Count
|
||||
end)
|
||||
|
||||
state.Value.Count += 1
|
||||
|
||||
print(derived.Value) -- still 1 because `state.Value` was never set with a new value so change wasn't detected
|
||||
|
||||
local value = state.Value
|
||||
value.Count += 1
|
||||
state.Value = value
|
||||
|
||||
print(derived.Value) -- still 1 because although `state.Value` was set, when the new value set was compared,
|
||||
-- it was still the same as the previous (tables are compared by reference not their contents)
|
||||
-- and so no update was made
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
-------------------------------------------------------------------
|
||||
57
docs/api/strict.md
Normal file
57
docs/api/strict.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Strict Mode
|
||||
|
||||
<br/>
|
||||
|
||||
## strict
|
||||
|
||||
A flag that users can set to enable or disable strict mode (disabled by default).
|
||||
|
||||
### Type
|
||||
|
||||
```ts
|
||||
boolean strict = false
|
||||
```
|
||||
|
||||
### Details
|
||||
|
||||
The purpose of strict mode is to help ensure stateful code is *pure* (deterministic and free from side effects).
|
||||
|
||||
Setting this flag is global for all scripts requiring the same instance of the Vide module.
|
||||
|
||||
What strict mode will do:
|
||||
|
||||
1. Run derived callbacks twice when calculating state value.
|
||||
2. Run watcher callbacks twice each time state changes.
|
||||
3. Throw an error if a derived callback yields.
|
||||
4. Throw an error if a watcher callback yields.
|
||||
|
||||
It is recommend to develop UI with strict mode set to `true`
|
||||
and to set it back to false when pushing to production.
|
||||
|
||||
Using strict mode will help identify potential non-deterministic code and side-effects by running code
|
||||
multiple times in places where it would only run once.
|
||||
|
||||
Strict mode will also ensure that watcher side effects are self contained in the sense that they clean themselves up
|
||||
properly when ran multiple times in quick succession, in case any asynchronous operation is performed.
|
||||
|
||||
Yielding within derived or watcher callbacks can cause undefined behavior as the reactive graph is not designed
|
||||
to work with asynchronous code. Strict mode can identify and throw an error when asynchronous code is detected.
|
||||
This isn't done during runtime as these checks are computationally expensive.
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
vide.strict = true -- this only needs to be done once, preferably in the first module to require Vide
|
||||
|
||||
local state = wrap()
|
||||
|
||||
watch(function()
|
||||
local cleanup = doAsyncOperation(state.Value)
|
||||
|
||||
return function()
|
||||
cleanup()
|
||||
end
|
||||
end)
|
||||
|
||||
state.Value = 1 -- this will cause the watcher to be ran twice, identifying if cleanup occurs properly
|
||||
```
|
||||
55
docs/api/types.md
Normal file
55
docs/api/types.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Types API
|
||||
|
||||
<br/>
|
||||
|
||||
## State\<T>
|
||||
|
||||
A type representing a Vide state object.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
type State<T> = {
|
||||
Value: T,
|
||||
value: T
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local state: State<number> = wrap(1)
|
||||
|
||||
local derived: State<string> = "Count: " .. state
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
<br/>
|
||||
|
||||
## Prop\<T>
|
||||
|
||||
A utility type representing a union of a value and a state.
|
||||
|
||||
### Type
|
||||
|
||||
```lua
|
||||
type Prop<T> = T | State<T>
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
type BackgroundProps = {
|
||||
Position: Prop<UDim2>,
|
||||
Size: Prop<UDim2>
|
||||
}
|
||||
local function Background(props: { Position: Prop<UDim2> })
|
||||
return create("Frame") {
|
||||
Position = props.Position
|
||||
Size = props.Size
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
-------------------------------------------------------------------
|
||||
337
docs/tut/crash-course.md
Normal file
337
docs/tut/crash-course.md
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
# Vide Crash Course
|
||||
|
||||
Hello! This is a brief tutorial designed to give you a quick runthrough of the usage of Vide.
|
||||
|
||||
Vide is inspired by the popular libraries Vue and Fusion.
|
||||
|
||||
- Note that this tutorial assumes that you are familiar with Luau and the Roblox UI system.
|
||||
|
||||
<br>
|
||||
|
||||
## Creating UI Instances
|
||||
|
||||
In Vide, it is intended to create all UI instances through code.
|
||||
|
||||
Instances are created using [`vide.create`](../api/creation#create).
|
||||
|
||||
```lua
|
||||
local vide = require(...)
|
||||
local create = vide.create
|
||||
```
|
||||
|
||||
```lua
|
||||
local frame = create("Frame") {
|
||||
Name = "Background",
|
||||
Position = UDim2.fromScale(0.5, 0.5)
|
||||
}
|
||||
```
|
||||
|
||||
The function returns a constructor for a given class which then takes a table of properties to assign to create a new instance for that class.
|
||||
|
||||
Sometimes you want to do more than setting properties, such as setting children or connecting to events.
|
||||
Vide uses special keys called *symbols* which provide unique functionality like the above mentioned.
|
||||
|
||||
Children can be assigned to instances using the `Children` symbol.
|
||||
|
||||
```lua
|
||||
local Children = vide.Children
|
||||
```
|
||||
|
||||
```lua
|
||||
local screenGui = create("ScreenGui") {
|
||||
Parent = game.StarterGui,
|
||||
|
||||
[Children] = create("Frame") {
|
||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||
Position = UDim2.fromScale(0.5, 0.5),
|
||||
Size = UDim2.fromScale(0.4, 0.7),
|
||||
|
||||
[Children] = {
|
||||
create("TextLabel") {
|
||||
Text = "Hi"
|
||||
},
|
||||
|
||||
create("TextLabel") {
|
||||
Text = "Bye"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, we import the symbol [`vide.Children`](../api/creation#Children).
|
||||
|
||||
This symbol can accept an instance, an array of instances and nested arrays of instances.
|
||||
All given instances will be parented to the instance the symbol was used on.
|
||||
|
||||
<br>
|
||||
|
||||
## Connecting To Events
|
||||
|
||||
Built-in instance events and property changed events can be connected to using two other symbols, [`vide.Event`](../api/creation#Event) and [`vide.Changed`](../api/creation#Changed).
|
||||
|
||||
```lua
|
||||
local Event = vide.Event
|
||||
local Changed = vide.Changed
|
||||
```
|
||||
|
||||
```lua
|
||||
local textBox = create("TextBox") {
|
||||
PlaceholderText = "Enter text",
|
||||
|
||||
[Event.Focused] = function(...)
|
||||
print("User is focusing on text box")
|
||||
end,
|
||||
|
||||
[Changed.Text] = function(newText)
|
||||
print("New text: " .. newText)
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
Both of these symbols can be indexed into to get a specific symbol for an event to connect to.
|
||||
The callback function for `Event` receives any event-specific arguments and the callback function for
|
||||
`Changed` receives the new property value as the only argument (unlike `Instance:GetPropertyChangedSignal()`).
|
||||
|
||||
<br>
|
||||
|
||||
## State
|
||||
|
||||
*State* is the condition something is in at a specific time. The state of a program is simply the data it contains at some timepoint.
|
||||
|
||||
The purpose of all UI is to take some state and reflect that state visually.
|
||||
|
||||
In Vide, UI state is represented using special objects simply called *state*.
|
||||
|
||||
A state object in Vide can be created using [`vide.wrap`](../api/reactivity-core#wrap).
|
||||
|
||||
```lua
|
||||
local wrap = vide.wrap
|
||||
```
|
||||
|
||||
```lua
|
||||
local isVisible = wrap(false)
|
||||
|
||||
local image = create("ImageLabel") {
|
||||
Image = "rbxassetid://xxx",
|
||||
Visible = isVisible
|
||||
}
|
||||
|
||||
while true do
|
||||
wait(1)
|
||||
isVisible.Value = not isVisible.Value
|
||||
end
|
||||
```
|
||||
|
||||
The function `wrap` will *wrap* any given value with a state object of type `State<T>` which can be read from/wrote to through its `.value` property.
|
||||
|
||||
In the above code, the `ImageLabel.Visible` property is assigned a state. Now any time that state's value is assigned to, `ImageLabel.Visible` will also update with the new value assigned, without you having to explicitly set the property. The above code gives the effect of the image label toggling visibility at a 1 second interval forever.
|
||||
|
||||
|
||||
There are a few reasons why we use state objects instead of plain variables:
|
||||
|
||||
1. Vide detects when you assign a state object as a property value. This is known as *binding* and doing so will cause the property to *automatically* update whenever that state object's value is changed.
|
||||
2. We can create new state objects that derive from other state objects, which again, *automatically* update when the derived state objects change.
|
||||
|
||||
The reason why this is useful, is that you as the programmer do not have to worry about manually updating variables or UI instances, you can just focus on defining how the data maps to UI and everything will automatically update when changes occur.
|
||||
|
||||
<br>
|
||||
|
||||
## Derived State
|
||||
|
||||
You can create new state from other states. This is known as *deriving state*.
|
||||
|
||||
```lua
|
||||
local derive = vide.derive
|
||||
```
|
||||
|
||||
```lua
|
||||
local count = wrap(0)
|
||||
|
||||
local text = derive(function(from)
|
||||
return "Count: " .. from(count)
|
||||
end)
|
||||
|
||||
print(text.value) -- "Count: 0"
|
||||
|
||||
count.value += 1
|
||||
|
||||
print(text.value) -- "Count: 1"
|
||||
```
|
||||
|
||||
Here we use [`vide.derive`](../api/reactivity-core#derive) to *derive* a new state `text` which depends on `count`.
|
||||
|
||||
A function is used to transform the value of `count`, where the value returned becomes the new value of `text`. The function receives an argument named `from` which is used to *capture* dependent states. This is used to link `count` to `text`, so that whenever `count` is updated, `text` will be too.
|
||||
|
||||
Whenever `count`'s value is changed, `text` will recompute its value and update anything dependent on `text`, such as UI.
|
||||
|
||||
States can be derived in a more concise manner when doing single operations such as concatenation:
|
||||
|
||||
```lua
|
||||
local text = "Count: " .. count
|
||||
```
|
||||
|
||||
You can derive new states using any Luau operator in this manner.
|
||||
|
||||
<br>
|
||||
|
||||
## Components
|
||||
|
||||
*Components* in UI are just custom-made reusable pieces of UI made from other pieces of UI.
|
||||
|
||||
The recommended way to create components is to use functions that take a table of properties as an argument and return the new UI instance.
|
||||
|
||||
```lua
|
||||
local function Background(args)
|
||||
return create("Frame") {
|
||||
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||
Position = args.Position,
|
||||
Size = args.Size
|
||||
}
|
||||
end
|
||||
|
||||
local background = Background {
|
||||
Position = UDim2.new(),
|
||||
Size = UDim2.new()
|
||||
}
|
||||
```
|
||||
|
||||
Above is a simple example of a frame component with its background color set to black.
|
||||
|
||||
A single parameter `args` is used to pass properties to the component.
|
||||
|
||||
Components allow you to *encapsulate* behavior. You can only modify the component in ways that are defined in the component.
|
||||
Looking at the above example, the only properties you are allowed to modify is `Position` and `Size`.
|
||||
This is a good approach to use for organised code.
|
||||
|
||||
However, properties concering layout (positional and size properties) aren't usually intrinsinc to the component. In most cases the user would want to be able to pass these properties without having to manually pass each one in the component.
|
||||
|
||||
For these cases, the [`vide.Layout`](../api/creation#Layout) symbol can be used.
|
||||
|
||||
```lua
|
||||
local Layout = vide.Layout
|
||||
```
|
||||
|
||||
```lua
|
||||
local function Background(props)
|
||||
return create("Frame") {
|
||||
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||
[Layout] = props[Layout],
|
||||
[Children] = props[Children]
|
||||
}
|
||||
end
|
||||
|
||||
local background = Background {
|
||||
[Layout] = {
|
||||
AnchorPoint = Vector2.new(),
|
||||
Position = UDim2.new(),
|
||||
Size = UDim2.new(),
|
||||
},
|
||||
|
||||
[Children] = {
|
||||
create("TextLabel") {},
|
||||
create("ImageLabel") {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, the `Layout` symbol automatically assigns those layout-specific properties without having to explicitly assign each one in the component definition. This is a very common case and for this reason it is recommended to only assign layout properties using the `Layout` symbol for consistency when dealing with components.
|
||||
|
||||
This allows you to pass through layout properties without breaking encapsulation.
|
||||
|
||||
Additionally, the above example shows how children can be passed to components in a similar manner.
|
||||
|
||||
<br>
|
||||
|
||||
## Stateful components
|
||||
|
||||
Often, you need components that maintain their own internal state, such as a toggle button or a counter.
|
||||
|
||||
Below you can see how a simple counter component can be implemented.
|
||||
|
||||
```lua
|
||||
local function Counter(args)
|
||||
-- create internal state unique to each component instance
|
||||
local count = wrap(0)
|
||||
|
||||
return create("TextButton") {
|
||||
Name = "Counter",
|
||||
|
||||
Text = "Count: " .. count
|
||||
|
||||
[Event.Activated] = function()
|
||||
count.value += 1
|
||||
end
|
||||
|
||||
[Layout] = args[Layout]
|
||||
}
|
||||
end
|
||||
|
||||
create "ScreenGui" {
|
||||
Parent = game.StarterGui,
|
||||
|
||||
[Children] = {
|
||||
Counter {
|
||||
[Layout] = {
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
Position = UDim2.fromScale(0.5, 0),
|
||||
Size = UDim2.fromScale(0.3, 0.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here a reusable counter component is created, that when clicked on will increase its count and display it independent from other counter instances.
|
||||
|
||||
## Tables of data
|
||||
|
||||
basic inventory
|
||||
|
||||
```lua
|
||||
type Item = {
|
||||
Name: string,
|
||||
Icon: number
|
||||
}
|
||||
|
||||
local items = wrap({} :: Array<Item>)
|
||||
|
||||
local function ItemSlot(args)
|
||||
return create("Frame") {
|
||||
[Layout] = args[Layout]
|
||||
|
||||
[Children] = {
|
||||
create("TextLabel") {
|
||||
Name = args.Name,
|
||||
[Layout] = ...
|
||||
},
|
||||
|
||||
create("ImageLabel") {
|
||||
Image = "rbxassetid://" .. args.Icon,
|
||||
[Layout] = ...
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
local function Inventory(args)
|
||||
return create("Frame") {
|
||||
[Layout] = args[Layout],
|
||||
|
||||
[Children] = {
|
||||
create("UIListLayout") {},
|
||||
|
||||
map(items, function(i, item)
|
||||
return ItemSlot {
|
||||
Name = item.Name,
|
||||
Icon = item.Icon,
|
||||
[Layout] = { LayoutOrder = i, ... }
|
||||
}
|
||||
end)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
|
||||
More comprehensive tutorials are in the works. To find out more refer to the [`API documentation`](../../README#API).
|
||||
99
docs/tut/reactive-graph.md
Normal file
99
docs/tut/reactive-graph.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# Vide Reactive Graph
|
||||
|
||||
Details on how Vide's reactive graph works.
|
||||
|
||||
## Nodes
|
||||
|
||||
A "node" refers to a point on the reactive graph.
|
||||
|
||||
- Nodes can have parents and children.
|
||||
- Updating a node will mark all descendant nodes for update.
|
||||
- Each Vide state object acts as a node on the reactive graph.
|
||||
|
||||
Vide's reactive graph uses a *lazy evaluation* model, meaning that
|
||||
if a node with children is updated, the new value for the child node
|
||||
is not recalculated immediately. Only when something attempts to access
|
||||
the child's value is it recalculated.
|
||||
|
||||
## Example
|
||||
|
||||
Below is an (*overengineered*) example to demonstrate how the reactive graph functions.
|
||||
States are used here to model the various transforms done on two inputs, `health` and `maxHealth`
|
||||
to represent player health for UI.
|
||||
|
||||
```lua
|
||||
local health = wrap(90)
|
||||
local maxHealth = wrap(100)
|
||||
|
||||
local healthTweened = spring(health, 0.5)
|
||||
local text = "Health: " .. healthTweened
|
||||
|
||||
local ratio = health / maxHealth
|
||||
|
||||
local barSize = derive(function(from)
|
||||
return UDim2.fromScale(from(ratio), 1)
|
||||
end
|
||||
```
|
||||
|
||||
Below is a graphical representation of the reactive graph formed by the above code.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A(( ))
|
||||
B(( ))
|
||||
A --> health
|
||||
B --> maxHealth
|
||||
|
||||
health --> healthTweened
|
||||
healthTweened --> text
|
||||
|
||||
health --> ratio
|
||||
maxHealth --> ratio
|
||||
ratio --> barSize
|
||||
```
|
||||
|
||||
When states are initially derived, all values are known.
|
||||
|
||||
Say if the player is damaged, and the `health` node changes value.
|
||||
All descendant nodes from `health` will be marked as updated.
|
||||
The nodes marked as updated are represented by the broken lines below.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A(( ))
|
||||
B(( ))
|
||||
A --> health
|
||||
B --> maxHealth
|
||||
|
||||
health .-x healthTweened
|
||||
healthTweened .-x text
|
||||
|
||||
health .-x ratio
|
||||
maxHealth --> ratio
|
||||
ratio .-x barSize
|
||||
```
|
||||
|
||||
When something tries to read the value of the node `text`, a recalculation occurs.
|
||||
While `text` is being recalculated, `healthTweened` will be read from, causing it to be recalculated as well.
|
||||
This results in a chain that propogates up the reactive graph until all ancestors are up to date.
|
||||
|
||||
Below is what the graph will look like after `text` has been recalculated.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A(( ))
|
||||
B(( ))
|
||||
A --> health
|
||||
B --> maxHealth
|
||||
|
||||
health --> healthTweened
|
||||
healthTweened --> text
|
||||
|
||||
health .-x ratio
|
||||
maxHealth --> ratio
|
||||
ratio .-x barSize
|
||||
```
|
||||
|
||||
Lazy evaluation is a useful model as it saves unecessary calculation, only calculating when needed.
|
||||
|
||||
Looking at stateful code as a reactive graph is a good way to mentally picture how your data maps to UI.
|
||||
20
docs/tut/tmp.md
Normal file
20
docs/tut/tmp.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
```lua
|
||||
local function Text(args)
|
||||
return create("TextLabel") {
|
||||
[Layout] = {
|
||||
Size = scale(1),
|
||||
args[Layout]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
Text {
|
||||
[Layout] = {
|
||||
Position = scale(0.5, 0.1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```lua
|
||||
a
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue