mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Merge 18c1d02d43 into f3bfc65607
This commit is contained in:
commit
2d1e5983c6
7 changed files with 276 additions and 0 deletions
|
|
@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## [0.4.1] - 2026-07-11
|
## [0.4.1] - 2026-07-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `tag()` action for adding CollectionService tags to instances. Supports
|
||||||
|
static strings and arrays as well as reactive sources for dynamic tag sets.
|
||||||
|
Tags are automatically removed when the surrounding scope is destroyed.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `branch()` is now allowed to be used within a reactive scope.
|
- `branch()` is now allowed to be used within a reactive scope.
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,56 @@ A wrapper for `action()` to listen for property changes.
|
||||||
|
|
||||||
Runs with an action priority of 1.
|
Runs with an action priority of 1.
|
||||||
|
|
||||||
|
## tag()
|
||||||
|
|
||||||
|
A wrapper for `action()` to add [CollectionService](https://create.roblox.com/docs/reference/engine/classes/CollectionService)
|
||||||
|
tags to an instance.
|
||||||
|
|
||||||
|
- **Type**
|
||||||
|
|
||||||
|
```luau
|
||||||
|
function tag(tag: string): Action
|
||||||
|
function tag(tags: { string }): Action
|
||||||
|
function tag(source: () -> string): Action
|
||||||
|
function tag(source: () -> { string }): Action
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Details**
|
||||||
|
|
||||||
|
Adds the given tag(s) to the instance using `Instance:AddTag()`. When the
|
||||||
|
surrounding scope is destroyed, any tags that were added are removed using
|
||||||
|
`Instance:RemoveTag()`.
|
||||||
|
|
||||||
|
A source (function) can be passed instead of a static value to reactively
|
||||||
|
update the applied tags. When the source updates, tags no longer present
|
||||||
|
are removed and new tags are added; tags present in both the previous and
|
||||||
|
new value are left untouched.
|
||||||
|
|
||||||
|
Multiple `tag()` actions can be composed on the same instance.
|
||||||
|
|
||||||
|
Runs with an action priority of 1.
|
||||||
|
|
||||||
|
- **Example**
|
||||||
|
|
||||||
|
Apply static tags:
|
||||||
|
|
||||||
|
```luau
|
||||||
|
create "Frame" {
|
||||||
|
tag("Clickable"),
|
||||||
|
tag({ "Draggable", "Highlightable" })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply a reactive tag driven by a source:
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local state = source "idle"
|
||||||
|
|
||||||
|
create "Frame" {
|
||||||
|
tag(function() return "state-" .. state() end)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## mount() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
## mount() <Badge type="info" text="STABLE"><a href="/vide/api/reactivity-core#Scopes">STABLE</a></Badge>
|
||||||
|
|
||||||
Runs a function in a new stable scope and optionally applies its result to a
|
Runs a function in a new stable scope and optionally applies its result to a
|
||||||
|
|
|
||||||
|
|
@ -53,3 +53,8 @@ instance.Text = "foo" -- "foo" will be printed by the effect
|
||||||
|
|
||||||
The source `output` will be updated with the new property value any time it is
|
The source `output` will be updated with the new property value any time it is
|
||||||
changed externally.
|
changed externally.
|
||||||
|
|
||||||
|
Vide also ships a few common actions out of the box, such as `vide.changed()`
|
||||||
|
for the pattern above and `vide.tag()` for adding
|
||||||
|
[CollectionService](https://create.roblox.com/docs/reference/engine/classes/CollectionService)
|
||||||
|
tags to an instance.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ local values = require "./values"
|
||||||
local spring, update_springs = require "./spring"()
|
local spring, update_springs = require "./spring"()
|
||||||
local action = require "./action"()
|
local action = require "./action"()
|
||||||
local changed = require "./changed"
|
local changed = require "./changed"
|
||||||
|
local tag = require "./tag"
|
||||||
local timeout, update_timeouts = require "./timeout"()
|
local timeout, update_timeouts = require "./timeout"()
|
||||||
local flags = require "./flags"
|
local flags = require "./flags"
|
||||||
|
|
||||||
|
|
@ -77,6 +78,7 @@ local vide = {
|
||||||
-- actions
|
-- actions
|
||||||
action = action,
|
action = action,
|
||||||
changed = changed,
|
changed = changed,
|
||||||
|
tag = tag,
|
||||||
|
|
||||||
-- flags
|
-- flags
|
||||||
strict = (nil :: any) :: boolean,
|
strict = (nil :: any) :: boolean,
|
||||||
|
|
|
||||||
71
src/tag.luau
Normal file
71
src/tag.luau
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
local action = require "./action"()
|
||||||
|
local cleanup = require "./cleanup"
|
||||||
|
local effect = require "./effect"
|
||||||
|
|
||||||
|
type Tags = string | { string }
|
||||||
|
|
||||||
|
local function build_set(value: Tags): { [string]: true }
|
||||||
|
local set: { [string]: true } = {}
|
||||||
|
if type(value) == "string" then
|
||||||
|
set[value] = true
|
||||||
|
else
|
||||||
|
for _, name in value do
|
||||||
|
if type(name) ~= "string" then
|
||||||
|
error("tag() expects a string or an array of strings", 0)
|
||||||
|
end
|
||||||
|
set[name] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return set
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tag(value: Tags | () -> Tags)
|
||||||
|
return action(function(instance)
|
||||||
|
if type(value) == "function" then
|
||||||
|
local current: { [string]: true } = {}
|
||||||
|
|
||||||
|
effect(function()
|
||||||
|
local new_set = build_set((value :: () -> Tags)())
|
||||||
|
|
||||||
|
for name in current do
|
||||||
|
if not new_set[name] then
|
||||||
|
instance:RemoveTag(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for name in new_set do
|
||||||
|
if not current[name] then
|
||||||
|
instance:AddTag(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
current = new_set
|
||||||
|
end)
|
||||||
|
|
||||||
|
cleanup(function()
|
||||||
|
for name in current do
|
||||||
|
instance:RemoveTag(name)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
local set = build_set(value)
|
||||||
|
|
||||||
|
for name in set do
|
||||||
|
instance:AddTag(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
cleanup(function()
|
||||||
|
for name in set do
|
||||||
|
instance:RemoveTag(name)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
type Action = typeof(action(nil :: any))
|
||||||
|
|
||||||
|
return tag :: ((tag: string) -> Action)
|
||||||
|
& ((tags: { string }) -> Action)
|
||||||
|
& ((source: () -> string) -> Action)
|
||||||
|
& ((source: () -> { string }) -> Action)
|
||||||
|
|
@ -66,6 +66,7 @@ local Instance = {} :: any do
|
||||||
children: { Data },
|
children: { Data },
|
||||||
changed: { [string]: RBXScriptSignal },
|
changed: { [string]: RBXScriptSignal },
|
||||||
properties: { [string]: unknown },
|
properties: { [string]: unknown },
|
||||||
|
tags: { [string]: true },
|
||||||
destroying: RBXScriptSignal,
|
destroying: RBXScriptSignal,
|
||||||
class: string,
|
class: string,
|
||||||
type: "Instance"
|
type: "Instance"
|
||||||
|
|
@ -158,6 +159,7 @@ local Instance = {} :: any do
|
||||||
children = {},
|
children = {},
|
||||||
changed = {},
|
changed = {},
|
||||||
properties = {},
|
properties = {},
|
||||||
|
tags = {},
|
||||||
class = class,
|
class = class,
|
||||||
destroying = Signal.new() :: any,
|
destroying = Signal.new() :: any,
|
||||||
type = "Instance" :: "Instance"
|
type = "Instance" :: "Instance"
|
||||||
|
|
@ -211,6 +213,29 @@ local Instance = {} :: any do
|
||||||
return data.changed[property]
|
return data.changed[property]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function methods.AddTag(userdata: userdata, tag: string)
|
||||||
|
if type(tag) ~= "string" then error("tag must be a string", 2) end
|
||||||
|
get_data(userdata).tags[tag] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
function methods.RemoveTag(userdata: userdata, tag: string)
|
||||||
|
if type(tag) ~= "string" then error("tag must be a string", 2) end
|
||||||
|
get_data(userdata).tags[tag] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function methods.HasTag(userdata: userdata, tag: string): boolean
|
||||||
|
if type(tag) ~= "string" then error("tag must be a string", 2) end
|
||||||
|
return get_data(userdata).tags[tag] == true
|
||||||
|
end
|
||||||
|
|
||||||
|
function methods.GetTags(userdata: userdata): { string }
|
||||||
|
local out = {}
|
||||||
|
for name in get_data(userdata).tags do
|
||||||
|
table.insert(out, name)
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
function methods.Destroy(userdata: userdata)
|
function methods.Destroy(userdata: userdata)
|
||||||
local data = get_data(userdata);
|
local data = get_data(userdata);
|
||||||
Signal.fire(data.destroying)
|
Signal.fire(data.destroying)
|
||||||
|
|
|
||||||
117
test/tests.luau
117
test/tests.luau
|
|
@ -24,6 +24,7 @@ local vide = require "../../vide"
|
||||||
local spring = vide.spring
|
local spring = vide.spring
|
||||||
local action = vide.action
|
local action = vide.action
|
||||||
local changed = vide.changed
|
local changed = vide.changed
|
||||||
|
local tag = vide.tag
|
||||||
local apply = vide.apply
|
local apply = vide.apply
|
||||||
local step = vide.step
|
local step = vide.step
|
||||||
local graph = require "../../vide/src/graph"
|
local graph = require "../../vide/src/graph"
|
||||||
|
|
@ -2340,6 +2341,122 @@ TEST("changed()", wrap_root(function()
|
||||||
end
|
end
|
||||||
end))
|
end))
|
||||||
|
|
||||||
|
TEST("tag()", wrap_root(function()
|
||||||
|
do CASE "apply single tag"
|
||||||
|
local frame = create "Frame" {
|
||||||
|
tag("MyTag")
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(frame:HasTag("MyTag"))
|
||||||
|
CHECK(#frame:GetTags() == 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "apply multiple tags"
|
||||||
|
local frame = create "Frame" {
|
||||||
|
tag({ "A", "B", "C" })
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(frame:HasTag("A"))
|
||||||
|
CHECK(frame:HasTag("B"))
|
||||||
|
CHECK(frame:HasTag("C"))
|
||||||
|
CHECK(#frame:GetTags() == 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "compose multiple tag() actions"
|
||||||
|
local frame = create "Frame" {
|
||||||
|
tag("A"),
|
||||||
|
tag("B")
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(frame:HasTag("A"))
|
||||||
|
CHECK(frame:HasTag("B"))
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "remove tags on scope destroy"
|
||||||
|
local _, frame, destroy = root(function(destroy)
|
||||||
|
return create "Frame" {
|
||||||
|
tag({ "A", "B" })
|
||||||
|
}, destroy
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(frame:HasTag("A"))
|
||||||
|
CHECK(frame:HasTag("B"))
|
||||||
|
|
||||||
|
destroy()
|
||||||
|
|
||||||
|
CHECK(not frame:HasTag("A"))
|
||||||
|
CHECK(not frame:HasTag("B"))
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "reactive single tag"
|
||||||
|
local current = source("A")
|
||||||
|
|
||||||
|
local frame = create "Frame" {
|
||||||
|
tag(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(frame:HasTag("A"))
|
||||||
|
CHECK(#frame:GetTags() == 1)
|
||||||
|
|
||||||
|
current("B")
|
||||||
|
|
||||||
|
CHECK(not frame:HasTag("A"))
|
||||||
|
CHECK(frame:HasTag("B"))
|
||||||
|
CHECK(#frame:GetTags() == 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "reactive multiple tags"
|
||||||
|
local current = source({ "A", "B" })
|
||||||
|
|
||||||
|
local frame = create "Frame" {
|
||||||
|
tag(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(frame:HasTag("A"))
|
||||||
|
CHECK(frame:HasTag("B"))
|
||||||
|
CHECK(#frame:GetTags() == 2)
|
||||||
|
|
||||||
|
-- preserve "A", remove "B", add "C"
|
||||||
|
current({ "A", "C" })
|
||||||
|
|
||||||
|
CHECK(frame:HasTag("A"))
|
||||||
|
CHECK(not frame:HasTag("B"))
|
||||||
|
CHECK(frame:HasTag("C"))
|
||||||
|
CHECK(#frame:GetTags() == 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "reactive tag removed on scope destroy"
|
||||||
|
local _, frame, destroy = root(function(destroy)
|
||||||
|
local current = source("A")
|
||||||
|
return create "Frame" {
|
||||||
|
tag(current)
|
||||||
|
}, destroy
|
||||||
|
end)
|
||||||
|
|
||||||
|
CHECK(frame:HasTag("A"))
|
||||||
|
|
||||||
|
destroy()
|
||||||
|
|
||||||
|
CHECK(not frame:HasTag("A"))
|
||||||
|
CHECK(#frame:GetTags() == 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "reactive tag derived from source"
|
||||||
|
local name = source("hello")
|
||||||
|
|
||||||
|
local frame = create "Frame" {
|
||||||
|
tag(function() return "prefix-" .. name() end)
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK(frame:HasTag("prefix-hello"))
|
||||||
|
|
||||||
|
name("world")
|
||||||
|
|
||||||
|
CHECK(not frame:HasTag("prefix-hello"))
|
||||||
|
CHECK(frame:HasTag("prefix-world"))
|
||||||
|
end
|
||||||
|
end))
|
||||||
|
|
||||||
TEST("batch()", wrap_root(function()
|
TEST("batch()", wrap_root(function()
|
||||||
do CASE "evaluation deferred"
|
do CASE "evaluation deferred"
|
||||||
local a = source(0)
|
local a = source(0)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue