@quenty/binder 14.45.0 → 14.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +2 -3
- package/src/Shared/Binder.lua +6 -0
- package/src/Shared/Collection/BoundChildCollection.lua +26 -21
- package/src/Shared/Collection/BoundChildCollection.spec.lua +90 -0
- package/src/Shared/Trackers/BoundAncestorTracker.lua +0 -83
- package/src/Shared/Trackers/BoundAncestorTracker.spec.lua +0 -179
- package/src/Shared/Trackers/BoundParentTracker.lua +0 -64
- package/src/Shared/Trackers/BoundParentTracker.spec.lua +0 -171
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [14.47.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/binder@14.46.0...@quenty/binder@14.47.0) (2026-09-23)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- Binder cleans up during construct/deconstruct moment ([404160e](https://github.com/Quenty/NevermoreEngine/commit/404160efddacbdf95eba0ede5e15066c5d71ed64))
|
|
11
|
+
|
|
12
|
+
# [14.46.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/binder@14.45.0...@quenty/binder@14.46.0) (2026-09-02)
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- Add RxBinderUtils.observeBoundAncestor<T>() and make sure everything is using binder:ObserveInstance() for performance ([9236c34](https://github.com/Quenty/NevermoreEngine/commit/9236c343fb438d75d29bcfcbaae71025fd344b0e))
|
|
17
|
+
|
|
6
18
|
# [14.45.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/binder@14.44.0...@quenty/binder@14.45.0) (2026-08-28)
|
|
7
19
|
|
|
8
20
|
**Note:** Version bump only for package @quenty/binder
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/binder",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.47.0",
|
|
4
4
|
"description": "Utility object to Bind a class to Roblox object, and associated helper methods",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -42,11 +42,10 @@
|
|
|
42
42
|
"@quenty/servicebag": "11.21.0",
|
|
43
43
|
"@quenty/signal": "7.13.2",
|
|
44
44
|
"@quenty/table": "3.9.2",
|
|
45
|
-
"@quenty/valueobject": "13.38.0",
|
|
46
45
|
"@quentystudios/jest-lua": "3.10.0-quenty.2"
|
|
47
46
|
},
|
|
48
47
|
"publishConfig": {
|
|
49
48
|
"access": "public"
|
|
50
49
|
},
|
|
51
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "31423d2314075bdd1f14ebbbc3eb637c5e0cb6ce"
|
|
52
51
|
}
|
package/src/Shared/Binder.lua
CHANGED
|
@@ -698,6 +698,12 @@ function Binder._add<T>(self: Binder<T>, inst: Instance)
|
|
|
698
698
|
tostring(type(constructor) == "table" and constructor.ClassName or constructor)
|
|
699
699
|
)
|
|
700
700
|
)
|
|
701
|
+
if MaidTaskUtils.isValidTask(class) then
|
|
702
|
+
task.spawn(function()
|
|
703
|
+
MaidTaskUtils.doTask(class)
|
|
704
|
+
end)
|
|
705
|
+
end
|
|
706
|
+
|
|
701
707
|
return
|
|
702
708
|
end
|
|
703
709
|
|
|
@@ -21,6 +21,7 @@ export type BoundChildCollection<T> =
|
|
|
21
21
|
_binder: Binder.Binder<T>,
|
|
22
22
|
_parent: Instance,
|
|
23
23
|
_classes: Set.Set<T>,
|
|
24
|
+
_childToClass: { [Instance]: T },
|
|
24
25
|
ClassAdded: Signal.Signal<T>,
|
|
25
26
|
ClassRemoved: Signal.Signal<T>,
|
|
26
27
|
_size: number,
|
|
@@ -56,15 +57,9 @@ function BoundChildCollection.new<T>(binder: Binder.Binder<T>, parent: Instance)
|
|
|
56
57
|
self.ClassRemoved = self._maid:Add(Signal.new() :: any) -- :Fire(class)
|
|
57
58
|
|
|
58
59
|
self._classes = {} -- [class] = true
|
|
60
|
+
self._childToClass = {}
|
|
59
61
|
self._size = 0
|
|
60
62
|
|
|
61
|
-
self._maid:GiveTask(self._binder:GetClassAddedSignal():Connect(function(...)
|
|
62
|
-
self:_handleNewClassBound(...)
|
|
63
|
-
end))
|
|
64
|
-
self._maid:GiveTask(self._binder:GetClassRemovingSignal():Connect(function(class)
|
|
65
|
-
self:_removeClass(class)
|
|
66
|
-
end))
|
|
67
|
-
|
|
68
63
|
self:_startTracking()
|
|
69
64
|
|
|
70
65
|
return self
|
|
@@ -128,29 +123,39 @@ function BoundChildCollection._startTracking<T>(self: BoundChildCollection<T>)
|
|
|
128
123
|
end
|
|
129
124
|
|
|
130
125
|
function BoundChildCollection._addChild<T>(self: BoundChildCollection<T>, inst: Instance, doNotFire: boolean?): ()
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
end
|
|
126
|
+
self._maid[inst] = self._binder:ObserveInstance(inst, function(class)
|
|
127
|
+
self:_setChildClass(inst, class)
|
|
128
|
+
end)
|
|
135
129
|
|
|
136
|
-
self:
|
|
130
|
+
self:_setChildClass(inst, self._binder:Get(inst), doNotFire)
|
|
137
131
|
end
|
|
138
132
|
|
|
139
|
-
function BoundChildCollection.
|
|
140
|
-
|
|
141
|
-
return
|
|
142
|
-
end
|
|
133
|
+
function BoundChildCollection._removeChild<T>(self: BoundChildCollection<T>, inst: Instance): ()
|
|
134
|
+
self._maid[inst] = nil
|
|
143
135
|
|
|
144
|
-
self:
|
|
136
|
+
self:_setChildClass(inst, nil)
|
|
145
137
|
end
|
|
146
138
|
|
|
147
|
-
function BoundChildCollection.
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
function BoundChildCollection._setChildClass<T>(
|
|
140
|
+
self: BoundChildCollection<T>,
|
|
141
|
+
inst: Instance,
|
|
142
|
+
class: T?,
|
|
143
|
+
doNotFire: boolean?
|
|
144
|
+
): ()
|
|
145
|
+
local existing = self._childToClass[inst]
|
|
146
|
+
if existing == class then
|
|
150
147
|
return
|
|
151
148
|
end
|
|
152
149
|
|
|
153
|
-
|
|
150
|
+
if existing ~= nil then
|
|
151
|
+
self._childToClass[inst] = nil
|
|
152
|
+
self:_removeClass(existing)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
if class ~= nil then
|
|
156
|
+
self._childToClass[inst] = class
|
|
157
|
+
self:_addClass(class, doNotFire)
|
|
158
|
+
end
|
|
154
159
|
end
|
|
155
160
|
|
|
156
161
|
function BoundChildCollection._addClass<T>(self: BoundChildCollection<T>, class: T, doNotFire: boolean?): ()
|
|
@@ -161,6 +161,96 @@ describe("BoundChildCollection dynamic updates", function()
|
|
|
161
161
|
controller:Destroy()
|
|
162
162
|
end)
|
|
163
163
|
|
|
164
|
+
it("fires ClassAdded when an existing child binds after construction", function()
|
|
165
|
+
local controller = setup()
|
|
166
|
+
|
|
167
|
+
local parent = controller.newInstance()
|
|
168
|
+
local child = controller.newInstance(parent)
|
|
169
|
+
controller.boot()
|
|
170
|
+
|
|
171
|
+
local collection = controller.track(BoundChildCollection.new(controller.binder, parent))
|
|
172
|
+
expect(collection:GetSize()).toEqual(0)
|
|
173
|
+
|
|
174
|
+
local added = {}
|
|
175
|
+
collection.ClassAdded:Connect(function(class)
|
|
176
|
+
table.insert(added, class)
|
|
177
|
+
end)
|
|
178
|
+
|
|
179
|
+
controller.binder:Tag(child)
|
|
180
|
+
|
|
181
|
+
expect(collection:GetSize()).toEqual(1)
|
|
182
|
+
expect(#added).toEqual(1)
|
|
183
|
+
expect(added[1]).toEqual(controller.binder:Get(child))
|
|
184
|
+
|
|
185
|
+
controller:Destroy()
|
|
186
|
+
end)
|
|
187
|
+
|
|
188
|
+
it("ignores a bind on an instance that is not a child of the parent", function()
|
|
189
|
+
local controller = setup()
|
|
190
|
+
|
|
191
|
+
local parent = controller.newInstance()
|
|
192
|
+
local outsider = controller.newInstance()
|
|
193
|
+
local grandchild = controller.newInstance(controller.newInstance(parent))
|
|
194
|
+
controller.boot()
|
|
195
|
+
|
|
196
|
+
local collection = controller.track(BoundChildCollection.new(controller.binder, parent))
|
|
197
|
+
|
|
198
|
+
controller.binder:Tag(outsider)
|
|
199
|
+
controller.binder:Tag(grandchild)
|
|
200
|
+
|
|
201
|
+
expect(collection:GetSize()).toEqual(0)
|
|
202
|
+
|
|
203
|
+
controller:Destroy()
|
|
204
|
+
end)
|
|
205
|
+
|
|
206
|
+
it("stops following a child that was reparented out", function()
|
|
207
|
+
local controller = setup()
|
|
208
|
+
|
|
209
|
+
local parent = controller.newInstance()
|
|
210
|
+
local child = controller.newInstance(parent)
|
|
211
|
+
controller.binder:Tag(child)
|
|
212
|
+
controller.boot()
|
|
213
|
+
|
|
214
|
+
local collection = controller.track(BoundChildCollection.new(controller.binder, parent))
|
|
215
|
+
expect(collection:GetSize()).toEqual(1)
|
|
216
|
+
|
|
217
|
+
child.Parent = controller.container
|
|
218
|
+
expect(collection:GetSize()).toEqual(0)
|
|
219
|
+
|
|
220
|
+
local removed = 0
|
|
221
|
+
collection.ClassRemoved:Connect(function()
|
|
222
|
+
removed += 1
|
|
223
|
+
end)
|
|
224
|
+
|
|
225
|
+
controller.binder:Untag(child)
|
|
226
|
+
|
|
227
|
+
expect(collection:GetSize()).toEqual(0)
|
|
228
|
+
expect(removed).toEqual(0)
|
|
229
|
+
|
|
230
|
+
controller:Destroy()
|
|
231
|
+
end)
|
|
232
|
+
|
|
233
|
+
it("re-adds a child that leaves and comes back", function()
|
|
234
|
+
local controller = setup()
|
|
235
|
+
|
|
236
|
+
local parent = controller.newInstance()
|
|
237
|
+
local child = controller.newInstance(parent)
|
|
238
|
+
controller.binder:Tag(child)
|
|
239
|
+
controller.boot()
|
|
240
|
+
|
|
241
|
+
local collection = controller.track(BoundChildCollection.new(controller.binder, parent))
|
|
242
|
+
expect(collection:GetSize()).toEqual(1)
|
|
243
|
+
|
|
244
|
+
child.Parent = controller.container
|
|
245
|
+
expect(collection:GetSize()).toEqual(0)
|
|
246
|
+
|
|
247
|
+
child.Parent = parent
|
|
248
|
+
expect(collection:GetSize()).toEqual(1)
|
|
249
|
+
expect(collection:HasClass(controller.binder:Get(child))).toEqual(true)
|
|
250
|
+
|
|
251
|
+
controller:Destroy()
|
|
252
|
+
end)
|
|
253
|
+
|
|
164
254
|
it("fires ClassRemoved when a tracked child is reparented out", function()
|
|
165
255
|
local controller = setup()
|
|
166
256
|
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[=[
|
|
3
|
-
Tracks a parent bound to a specific binder
|
|
4
|
-
@class BoundAncestorTracker
|
|
5
|
-
]=]
|
|
6
|
-
|
|
7
|
-
local require = require(script.Parent.loader).load(script)
|
|
8
|
-
|
|
9
|
-
local BaseObject = require("BaseObject")
|
|
10
|
-
local Binder = require("Binder")
|
|
11
|
-
local BinderUtils = require("BinderUtils")
|
|
12
|
-
local ValueObject = require("ValueObject")
|
|
13
|
-
|
|
14
|
-
local BoundAncestorTracker = setmetatable({}, BaseObject)
|
|
15
|
-
BoundAncestorTracker.ClassName = "BoundAncestorTracker"
|
|
16
|
-
BoundAncestorTracker.__index = BoundAncestorTracker
|
|
17
|
-
|
|
18
|
-
export type BoundAncestorTracker<T> =
|
|
19
|
-
typeof(setmetatable(
|
|
20
|
-
{} :: {
|
|
21
|
-
_child: Instance,
|
|
22
|
-
_binder: Binder.Binder<T>,
|
|
23
|
-
Class: ValueObject.ValueObject<T?>,
|
|
24
|
-
},
|
|
25
|
-
{} :: typeof({ __index = BoundAncestorTracker })
|
|
26
|
-
))
|
|
27
|
-
& BaseObject.BaseObject
|
|
28
|
-
|
|
29
|
-
--[=[
|
|
30
|
-
Constructs a new BoundAncestorTracker
|
|
31
|
-
|
|
32
|
-
@param binder Binder<T>
|
|
33
|
-
@param child Instance
|
|
34
|
-
@return BoundAncestorTracker
|
|
35
|
-
]=]
|
|
36
|
-
function BoundAncestorTracker.new<T>(binder: Binder.Binder<T>, child: Instance): BoundAncestorTracker<T>
|
|
37
|
-
local self: BoundAncestorTracker<T> = setmetatable(BaseObject.new() :: any, BoundAncestorTracker)
|
|
38
|
-
|
|
39
|
-
self._child = child or error("No child")
|
|
40
|
-
self._binder = binder or error("No binder")
|
|
41
|
-
|
|
42
|
-
--[=[
|
|
43
|
-
@prop Class ValueObject<T>
|
|
44
|
-
@readonly
|
|
45
|
-
@within BoundAncestorTracker
|
|
46
|
-
Bound value
|
|
47
|
-
]=]
|
|
48
|
-
self.Class = ValueObject.new() :: ValueObject.ValueObject<T?>
|
|
49
|
-
self._maid:GiveTask(self.Class)
|
|
50
|
-
|
|
51
|
-
-- Handle instance removing
|
|
52
|
-
self._maid:GiveTask(self._binder:GetClassRemovingSignal():Connect(function(class)
|
|
53
|
-
if class == self.Class.Value then
|
|
54
|
-
self.Class.Value = nil
|
|
55
|
-
end
|
|
56
|
-
end))
|
|
57
|
-
|
|
58
|
-
self._maid:GiveTask(self._binder:GetClassAddedSignal():Connect(function(_, instance)
|
|
59
|
-
if self._child:IsDescendantOf(instance) then
|
|
60
|
-
self:_update()
|
|
61
|
-
end
|
|
62
|
-
end))
|
|
63
|
-
|
|
64
|
-
-- Perform update
|
|
65
|
-
self._maid:GiveTask(self._child.AncestryChanged:Connect(function()
|
|
66
|
-
self:_update()
|
|
67
|
-
end))
|
|
68
|
-
self:_update()
|
|
69
|
-
|
|
70
|
-
return self
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
function BoundAncestorTracker._update<T>(self: BoundAncestorTracker<T>): ()
|
|
74
|
-
local parent = self._child.Parent
|
|
75
|
-
if not parent then
|
|
76
|
-
self.Class.Value = nil
|
|
77
|
-
return
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
self.Class.Value = BinderUtils.findFirstAncestor(self._binder, parent)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
return BoundAncestorTracker
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
@class BoundAncestorTracker.spec.lua
|
|
4
|
-
]]
|
|
5
|
-
|
|
6
|
-
local require = require(script.Parent.loader).load(script)
|
|
7
|
-
|
|
8
|
-
local Binder = require("Binder")
|
|
9
|
-
local BinderProvider = require("BinderProvider")
|
|
10
|
-
local BoundAncestorTracker = require("BoundAncestorTracker")
|
|
11
|
-
local Jest = require("Jest")
|
|
12
|
-
local JestUtils = require("JestUtils")
|
|
13
|
-
local Maid = require("Maid")
|
|
14
|
-
local ServiceBag = require("ServiceBag")
|
|
15
|
-
|
|
16
|
-
local describe = Jest.Globals.describe
|
|
17
|
-
local expect = Jest.Globals.expect
|
|
18
|
-
local it = Jest.Globals.it
|
|
19
|
-
|
|
20
|
-
local specCounter = 0
|
|
21
|
-
|
|
22
|
-
local function makeClass()
|
|
23
|
-
local Class = {}
|
|
24
|
-
Class.__index = Class
|
|
25
|
-
Class.ClassName = "BoundAncestorTrackerSpecClass"
|
|
26
|
-
function Class.new(inst)
|
|
27
|
-
return setmetatable({ instance = inst }, Class)
|
|
28
|
-
end
|
|
29
|
-
function Class:Destroy() end
|
|
30
|
-
return Class
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
local function setup()
|
|
34
|
-
local maid = Maid.new()
|
|
35
|
-
|
|
36
|
-
specCounter += 1
|
|
37
|
-
local suffix = specCounter
|
|
38
|
-
|
|
39
|
-
local serviceBag = ServiceBag.new()
|
|
40
|
-
local container = Instance.new("Folder")
|
|
41
|
-
container.Name = "BoundAncestorTrackerSpecContainer"
|
|
42
|
-
container.Parent = workspace
|
|
43
|
-
|
|
44
|
-
local instances: { Instance } = {}
|
|
45
|
-
local cleanups: { any } = {}
|
|
46
|
-
local booted = false
|
|
47
|
-
|
|
48
|
-
local binder = Binder.new(string.format("BoundAncestorTrackerSpecTag_%d", suffix), makeClass() :: any)
|
|
49
|
-
|
|
50
|
-
local function newInstance(parent: Instance?): Instance
|
|
51
|
-
local inst = Instance.new("Folder")
|
|
52
|
-
inst.Parent = parent or container
|
|
53
|
-
table.insert(instances, inst)
|
|
54
|
-
return inst
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
local function track(item: any): any
|
|
58
|
-
table.insert(cleanups, item)
|
|
59
|
-
return item
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
local function boot()
|
|
63
|
-
assert(not booted, "Already booted")
|
|
64
|
-
booted = true
|
|
65
|
-
|
|
66
|
-
local provider = BinderProvider.new(string.format("BoundAncestorTrackerSpecProvider_%d", suffix), function(self)
|
|
67
|
-
self:Add(binder)
|
|
68
|
-
end)
|
|
69
|
-
serviceBag:GetService(provider)
|
|
70
|
-
serviceBag:Init()
|
|
71
|
-
serviceBag:Start()
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
local function awaitChange(tracker, previous)
|
|
75
|
-
if tracker.Class.Value == previous then
|
|
76
|
-
tracker.Class.Changed:Wait()
|
|
77
|
-
end
|
|
78
|
-
return tracker.Class.Value
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
maid:GiveTask(function()
|
|
82
|
-
for _, item in cleanups do
|
|
83
|
-
pcall(function()
|
|
84
|
-
item:Destroy()
|
|
85
|
-
end)
|
|
86
|
-
end
|
|
87
|
-
serviceBag:Destroy()
|
|
88
|
-
for _, inst in instances do
|
|
89
|
-
pcall(function()
|
|
90
|
-
inst:Destroy()
|
|
91
|
-
end)
|
|
92
|
-
end
|
|
93
|
-
container:Destroy()
|
|
94
|
-
end)
|
|
95
|
-
|
|
96
|
-
local controller = {
|
|
97
|
-
container = container,
|
|
98
|
-
binder = binder,
|
|
99
|
-
newInstance = newInstance,
|
|
100
|
-
track = track,
|
|
101
|
-
boot = boot,
|
|
102
|
-
awaitChange = awaitChange,
|
|
103
|
-
Destroy = function(_self)
|
|
104
|
-
maid:DoCleaning()
|
|
105
|
-
end,
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
maid:GiveTask(JestUtils.afterThis(controller))
|
|
109
|
-
|
|
110
|
-
return controller
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
describe("BoundAncestorTracker tracking", function()
|
|
114
|
-
it("exposes the nearest bound ancestor's class", function()
|
|
115
|
-
local controller = setup()
|
|
116
|
-
|
|
117
|
-
local grandparent = controller.newInstance()
|
|
118
|
-
local parent = controller.newInstance(grandparent)
|
|
119
|
-
local child = controller.newInstance(parent)
|
|
120
|
-
controller.binder:Tag(grandparent)
|
|
121
|
-
controller.boot()
|
|
122
|
-
|
|
123
|
-
local tracker = controller.track(BoundAncestorTracker.new(controller.binder, child))
|
|
124
|
-
expect(tracker.Class.Value).toEqual(controller.binder:Get(grandparent))
|
|
125
|
-
|
|
126
|
-
controller:Destroy()
|
|
127
|
-
end)
|
|
128
|
-
|
|
129
|
-
it("has no value when no ancestor is bound", function()
|
|
130
|
-
local controller = setup()
|
|
131
|
-
|
|
132
|
-
local parent = controller.newInstance()
|
|
133
|
-
local child = controller.newInstance(parent)
|
|
134
|
-
controller.boot()
|
|
135
|
-
|
|
136
|
-
local tracker = controller.track(BoundAncestorTracker.new(controller.binder, child))
|
|
137
|
-
expect(tracker.Class.Value).toBeNil()
|
|
138
|
-
|
|
139
|
-
controller:Destroy()
|
|
140
|
-
end)
|
|
141
|
-
|
|
142
|
-
it("updates when an ancestor becomes bound", function()
|
|
143
|
-
local controller = setup()
|
|
144
|
-
|
|
145
|
-
-- The tracker resolves ancestors above the child's direct parent, so bind the grandparent.
|
|
146
|
-
local ancestor = controller.newInstance()
|
|
147
|
-
local parent = controller.newInstance(ancestor)
|
|
148
|
-
local child = controller.newInstance(parent)
|
|
149
|
-
controller.boot()
|
|
150
|
-
|
|
151
|
-
local tracker = controller.track(BoundAncestorTracker.new(controller.binder, child))
|
|
152
|
-
expect(tracker.Class.Value).toBeNil()
|
|
153
|
-
|
|
154
|
-
controller.binder:Tag(ancestor)
|
|
155
|
-
local value = controller.awaitChange(tracker, nil)
|
|
156
|
-
expect(value).toEqual(controller.binder:Get(ancestor))
|
|
157
|
-
|
|
158
|
-
controller:Destroy()
|
|
159
|
-
end)
|
|
160
|
-
|
|
161
|
-
it("clears the value when the child leaves the bound ancestry", function()
|
|
162
|
-
local controller = setup()
|
|
163
|
-
|
|
164
|
-
local ancestor = controller.newInstance()
|
|
165
|
-
local parent = controller.newInstance(ancestor)
|
|
166
|
-
local child = controller.newInstance(parent)
|
|
167
|
-
controller.binder:Tag(ancestor)
|
|
168
|
-
controller.boot()
|
|
169
|
-
|
|
170
|
-
local tracker = controller.track(BoundAncestorTracker.new(controller.binder, child))
|
|
171
|
-
local class = controller.binder:Get(ancestor)
|
|
172
|
-
expect(tracker.Class.Value).toEqual(class)
|
|
173
|
-
|
|
174
|
-
child.Parent = controller.container
|
|
175
|
-
expect(controller.awaitChange(tracker, class)).toBeNil()
|
|
176
|
-
|
|
177
|
-
controller:Destroy()
|
|
178
|
-
end)
|
|
179
|
-
end)
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[=[
|
|
3
|
-
Tracks a parent bound to a specific binder
|
|
4
|
-
@class BoundParentTracker
|
|
5
|
-
]=]
|
|
6
|
-
|
|
7
|
-
local require = require(script.Parent.loader).load(script)
|
|
8
|
-
|
|
9
|
-
local BaseObject = require("BaseObject")
|
|
10
|
-
local Binder = require("Binder")
|
|
11
|
-
local ValueObject = require("ValueObject")
|
|
12
|
-
|
|
13
|
-
local BoundParentTracker = setmetatable({}, BaseObject)
|
|
14
|
-
BoundParentTracker.ClassName = "BoundParentTracker"
|
|
15
|
-
BoundParentTracker.__index = BoundParentTracker
|
|
16
|
-
|
|
17
|
-
export type BoundParentTracker<T> =
|
|
18
|
-
typeof(setmetatable(
|
|
19
|
-
{} :: {
|
|
20
|
-
_child: Instance,
|
|
21
|
-
_binder: Binder.Binder<T>,
|
|
22
|
-
Class: ValueObject.ValueObject<T?>,
|
|
23
|
-
},
|
|
24
|
-
{} :: typeof({ __index = BoundParentTracker })
|
|
25
|
-
))
|
|
26
|
-
& BaseObject.BaseObject
|
|
27
|
-
|
|
28
|
-
function BoundParentTracker.new<T>(binder: Binder.Binder<T>, child: Instance): BoundParentTracker<T>
|
|
29
|
-
local self: BoundParentTracker<T> = setmetatable(BaseObject.new() :: any, BoundParentTracker)
|
|
30
|
-
|
|
31
|
-
self._child = child or error("No child")
|
|
32
|
-
self._binder = binder or error("No binder")
|
|
33
|
-
|
|
34
|
-
-- Bound value
|
|
35
|
-
self.Class = ValueObject.new(nil :: T?)
|
|
36
|
-
self._maid:GiveTask(self.Class)
|
|
37
|
-
|
|
38
|
-
-- Handle instance removing
|
|
39
|
-
self._maid:GiveTask(self._binder:GetClassRemovingSignal():Connect(function(class)
|
|
40
|
-
if class == self.Class.Value then
|
|
41
|
-
self.Class.Value = nil
|
|
42
|
-
end
|
|
43
|
-
end))
|
|
44
|
-
|
|
45
|
-
-- Perform update
|
|
46
|
-
self._maid:GiveTask(self._child:GetPropertyChangedSignal("Parent"):Connect(function()
|
|
47
|
-
self:_update()
|
|
48
|
-
end))
|
|
49
|
-
self:_update()
|
|
50
|
-
|
|
51
|
-
return self
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
function BoundParentTracker._update<T>(self: BoundParentTracker<T>): ()
|
|
55
|
-
local parent = self._child.Parent
|
|
56
|
-
if not parent then
|
|
57
|
-
self.Class.Value = nil
|
|
58
|
-
return
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
self.Class.Value = self._binder:Get(parent)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
return BoundParentTracker
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
@class BoundParentTracker.spec.lua
|
|
4
|
-
]]
|
|
5
|
-
|
|
6
|
-
local require = require(script.Parent.loader).load(script)
|
|
7
|
-
|
|
8
|
-
local Binder = require("Binder")
|
|
9
|
-
local BinderProvider = require("BinderProvider")
|
|
10
|
-
local BoundParentTracker = require("BoundParentTracker")
|
|
11
|
-
local Jest = require("Jest")
|
|
12
|
-
local JestUtils = require("JestUtils")
|
|
13
|
-
local Maid = require("Maid")
|
|
14
|
-
local ServiceBag = require("ServiceBag")
|
|
15
|
-
|
|
16
|
-
local describe = Jest.Globals.describe
|
|
17
|
-
local expect = Jest.Globals.expect
|
|
18
|
-
local it = Jest.Globals.it
|
|
19
|
-
|
|
20
|
-
local specCounter = 0
|
|
21
|
-
|
|
22
|
-
local function makeClass()
|
|
23
|
-
local Class = {}
|
|
24
|
-
Class.__index = Class
|
|
25
|
-
Class.ClassName = "BoundParentTrackerSpecClass"
|
|
26
|
-
function Class.new(inst)
|
|
27
|
-
return setmetatable({ instance = inst }, Class)
|
|
28
|
-
end
|
|
29
|
-
function Class:Destroy() end
|
|
30
|
-
return Class
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
local function setup()
|
|
34
|
-
local maid = Maid.new()
|
|
35
|
-
|
|
36
|
-
specCounter += 1
|
|
37
|
-
local suffix = specCounter
|
|
38
|
-
|
|
39
|
-
local serviceBag = ServiceBag.new()
|
|
40
|
-
local container = Instance.new("Folder")
|
|
41
|
-
container.Name = "BoundParentTrackerSpecContainer"
|
|
42
|
-
container.Parent = workspace
|
|
43
|
-
|
|
44
|
-
local instances: { Instance } = {}
|
|
45
|
-
local cleanups: { any } = {}
|
|
46
|
-
local booted = false
|
|
47
|
-
|
|
48
|
-
local binder = Binder.new(string.format("BoundParentTrackerSpecTag_%d", suffix), makeClass() :: any)
|
|
49
|
-
|
|
50
|
-
local function newInstance(parent: Instance?): Instance
|
|
51
|
-
local inst = Instance.new("Folder")
|
|
52
|
-
inst.Parent = parent or container
|
|
53
|
-
table.insert(instances, inst)
|
|
54
|
-
return inst
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
local function track(item: any): any
|
|
58
|
-
table.insert(cleanups, item)
|
|
59
|
-
return item
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
local function boot()
|
|
63
|
-
assert(not booted, "Already booted")
|
|
64
|
-
booted = true
|
|
65
|
-
|
|
66
|
-
local provider = BinderProvider.new(string.format("BoundParentTrackerSpecProvider_%d", suffix), function(self)
|
|
67
|
-
self:Add(binder)
|
|
68
|
-
end)
|
|
69
|
-
serviceBag:GetService(provider)
|
|
70
|
-
serviceBag:Init()
|
|
71
|
-
serviceBag:Start()
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
local function awaitChange(tracker, previous)
|
|
75
|
-
if tracker.Class.Value == previous then
|
|
76
|
-
tracker.Class.Changed:Wait()
|
|
77
|
-
end
|
|
78
|
-
return tracker.Class.Value
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
maid:GiveTask(function()
|
|
82
|
-
for _, item in cleanups do
|
|
83
|
-
pcall(function()
|
|
84
|
-
item:Destroy()
|
|
85
|
-
end)
|
|
86
|
-
end
|
|
87
|
-
serviceBag:Destroy()
|
|
88
|
-
for _, inst in instances do
|
|
89
|
-
pcall(function()
|
|
90
|
-
inst:Destroy()
|
|
91
|
-
end)
|
|
92
|
-
end
|
|
93
|
-
container:Destroy()
|
|
94
|
-
end)
|
|
95
|
-
|
|
96
|
-
local controller = {
|
|
97
|
-
container = container,
|
|
98
|
-
binder = binder,
|
|
99
|
-
newInstance = newInstance,
|
|
100
|
-
track = track,
|
|
101
|
-
boot = boot,
|
|
102
|
-
awaitChange = awaitChange,
|
|
103
|
-
Destroy = function(_self)
|
|
104
|
-
maid:DoCleaning()
|
|
105
|
-
end,
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
maid:GiveTask(JestUtils.afterThis(controller))
|
|
109
|
-
|
|
110
|
-
return controller
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
describe("BoundParentTracker.new()", function()
|
|
114
|
-
it("throws without a binder or child", function()
|
|
115
|
-
expect(function()
|
|
116
|
-
BoundParentTracker.new(nil :: any, Instance.new("Folder"))
|
|
117
|
-
end).toThrow()
|
|
118
|
-
end)
|
|
119
|
-
end)
|
|
120
|
-
|
|
121
|
-
describe("BoundParentTracker tracking", function()
|
|
122
|
-
it("exposes the bound class of the direct parent", function()
|
|
123
|
-
local controller = setup()
|
|
124
|
-
|
|
125
|
-
local parent = controller.newInstance()
|
|
126
|
-
local child = controller.newInstance(parent)
|
|
127
|
-
controller.binder:Tag(parent)
|
|
128
|
-
controller.boot()
|
|
129
|
-
|
|
130
|
-
local tracker = controller.track(BoundParentTracker.new(controller.binder, child))
|
|
131
|
-
expect(tracker.Class.Value).toEqual(controller.binder:Get(parent))
|
|
132
|
-
|
|
133
|
-
controller:Destroy()
|
|
134
|
-
end)
|
|
135
|
-
|
|
136
|
-
it("clears the value when the child is reparented off the bound parent", function()
|
|
137
|
-
local controller = setup()
|
|
138
|
-
|
|
139
|
-
local parent = controller.newInstance()
|
|
140
|
-
local child = controller.newInstance(parent)
|
|
141
|
-
controller.binder:Tag(parent)
|
|
142
|
-
controller.boot()
|
|
143
|
-
|
|
144
|
-
local tracker = controller.track(BoundParentTracker.new(controller.binder, child))
|
|
145
|
-
local class = controller.binder:Get(parent)
|
|
146
|
-
expect(tracker.Class.Value).toEqual(class)
|
|
147
|
-
|
|
148
|
-
child.Parent = controller.container
|
|
149
|
-
expect(controller.awaitChange(tracker, class)).toBeNil()
|
|
150
|
-
|
|
151
|
-
controller:Destroy()
|
|
152
|
-
end)
|
|
153
|
-
|
|
154
|
-
it("clears the value when the parent's class is unbound", function()
|
|
155
|
-
local controller = setup()
|
|
156
|
-
|
|
157
|
-
local parent = controller.newInstance()
|
|
158
|
-
local child = controller.newInstance(parent)
|
|
159
|
-
controller.binder:Tag(parent)
|
|
160
|
-
controller.boot()
|
|
161
|
-
|
|
162
|
-
local tracker = controller.track(BoundParentTracker.new(controller.binder, child))
|
|
163
|
-
local class = controller.binder:Get(parent)
|
|
164
|
-
expect(tracker.Class.Value).toEqual(class)
|
|
165
|
-
|
|
166
|
-
controller.binder:Untag(parent)
|
|
167
|
-
expect(controller.awaitChange(tracker, class)).toBeNil()
|
|
168
|
-
|
|
169
|
-
controller:Destroy()
|
|
170
|
-
end)
|
|
171
|
-
end)
|