Binder
Binds a constructor to Roblox instances for their whole lifetime. A Binder watches the game (or a set of ancestors) for instances that match its filters, calls your constructor for each one with a dedicated Trove, and cleans that Trove up again when the instance is removed, loses its tag, or is unbound manually.
Filters are combined: an instance has to carry one of the Tags, be one of the ClassNames,
sit under one of the Ancestors and pass the Predicate. Any filter you leave out is
skipped. Values returned by the predicate (after the boolean) are forwarded to your constructor
after the Trove, which is how the Predicates factories hand you the
attribute, child or player they found.
local Binder = require(path.to.Binder)
local Door = {}
Door.__index = Door
function Door.new(instance: Model, trove, openTime: number)
local self = setmetatable({ Instance = instance, OpenTime = openTime }, Door)
trove:Connect(instance.PrimaryPart.Touched, function()
self:Open()
end)
return self
end
function Door:Open()
print(`Opening {self.Instance.Name} for {self.OpenTime}s`)
end
function Door:Destroy()
-- called automatically when the door is unbound
end
local doors = Binder.new(Door, {
Tags = { "Door" },
ClassNames = { "Model" },
Ancestors = { workspace },
Predicate = Binder.Predicates.Attribute("OpenTime", "number", "DoorBinder"),
AutoStart = true,
})
doors.InstanceBound:Connect(function(instance, trove, door)
print("Bound", instance:GetFullName())
end)
The module also exposes the whole Predicates module through its metatable,
so Binder.Predicates.Attribute(...), Binder.doPredicateWarning and Binder.T are the same
values you would get from requiring Predicates directly.
Credits: Trove and Signal are by sleitnick (sleitnick's RbxUtil); the predicate filters come from my own Predicates package. Wally installs all three.
Installation and guide: Binder package page.
Types
Constructable
type Constructable = {new: (instance: any,trove: Trove,...any) → A} | (instance: any,trove: Trove,...any) → A
What a Binder constructs for each bound instance. Either a table with a .new function (a
typical class module) or a plain function. It is called with the instance, a Trove that is
cleaned up when the instance is unbound, and any extra values the predicate returned. It must
return a non-nil value; if the value has a Destroy method it is added to the Trove so it is
destroyed automatically on unbind. If you have nothing to return, return the Trove.
Predicate
type Predicate = (any) → (boolean,...any)Re-export of Predicates.Predicate. Receives the candidate instance and returns whether it may be bound, followed by any extra values to pass to the constructor.
BinderConfig
interface BinderConfig {Tags: {string}?--
CollectionService tags. An instance needs at least one of them to bind, and Bind adds all of them to the instance. Also drives automatic binding: tagging/untagging an instance binds/unbinds it.
ClassNames: {string}?--
Class names the instance must match (checked with IsA, so superclasses such as "BasePart" work).
Ancestors: {Instance}?--
Only descendants of one of these instances may bind, and only these are watched for DescendantAdded/DescendantRemoving. When omitted the whole game is watched and the instance must be a descendant of game.
Predicate: Predicate?--
Final check run on every candidate. Must return true or false; extra return values are passed to the constructor after the Trove. Errors inside it are reported and count as false.
Priority: number?--
When set, automatic binds/unbinds are queued and flushed on the next Heartbeat, ordered so that lower numbers run first across all Binders. When omitted they run immediately.
AutoStart: boolean?--
Call Start for you right after construction (in a new thread). Cannot be combined with ManualBindingOnly.
ManualBindingOnly: boolean?--
Disable automatic binding entirely: Start errors, Bind skips the tag/class/ancestor checks (the predicate still runs) and errors instead of returning nil when the predicate fails.
RemoveTagsOnCleanup: boolean?--
Remove every Tags entry from the instance when it is unbound. Defaults to false.
}Configuration table accepted by Binder.new. Every field is optional; table fields are frozen once passed in and a field with the wrong type raises an error.
Properties
Trove
This item is read only and cannot be modified. Read OnlyBinder.Trove: TroveThe Binder's root Trove. Every per-instance Trove is an extension of it, so cleaning it (which is what Binder:Destroy does) unbinds every instance and disconnects the signals. You may add your own cleanup tasks to it.
InstanceBound
This item is read only and cannot be modified. Read OnlyFires after an instance has been bound, whether automatically or through Binder:Bind. Receives the instance, the Trove created for it and the value your constructor returned.
InstanceUnbound
This item is read only and cannot be modified. Read OnlyFires after an instance has been unbound and its Trove cleaned, whether because it was removed, lost its tag, Binder:Unbind was called or the Binder was destroyed. Receives the instance.
Functions
new
Binder.new(constructable: Constructable<A>,--
Class table with a .new function, or a plain function, called for each bound instance.
) → Binder<A>--
The new Binder.
Creates a Binder that constructs constructable for every instance matching config.
What happens next depends on the config:
-
No
config, orManualBindingOnly = true: nothing is watched. Use Binder:Bind and Binder:Unbind yourself. -
AutoStart = true: Binder:Start is called for you in a new thread, so existing instances are bound shortly after this returns. - Otherwise the filters are stored but nothing happens until you call Binder:Start.
Errors
| Type | Description |
|---|---|
| "Invalid config" | `config` is not a table. |
| "Invalid type passed, expected <type>" | A config field has the wrong type. |
| "Binder cannot have both AutoStart and ManualBindingOnly set to true" | Both flags were set. |
CanBind
Binder:CanBind() → (boolean,--
Whether the instance passes every configured filter.
{any}?--
Packed extra values from the predicate, or nil when there is no predicate.
)
Runs the filters against instance without binding it. The checks are, in order: Tags
(any one), ClassNames (any one, via IsA), Ancestors (any one, or "is a descendant of
game" when no ancestors are configured) and finally Predicate. When the Binder is
ManualBindingOnly the first three checks are skipped and only the predicate runs.
If a predicate is configured and passes, the second return value is a packed table of the extra
values the predicate returned; these are what Binder:Bind unpacks into the constructor.
A predicate that errors or does not return a boolean first is reported through error in a
separate thread and treated as false.
GetTrove
Binder:GetTrove(binding: A--
The object returned by the constructor for some bound instance.
) → Trove?--
The object's Trove, or nil if it is not (or no longer) bound.
Returns the Trove that was created for a bound object. Note that the key is the value your constructor returned, not the instance; use Binder:GetBinding first if you only have the instance. Cleaning this Trove unbinds the object.
GetBinding
Binder:GetBinding() → A?--
The bound object, or nil if the instance is not bound by this Binder.
Returns the object the constructor produced for instance.
GetInstances
Returns a new array of every instance currently bound by this Binder. The order is not defined.
Bind
Binder:Bind(...: any--
Reserved. Currently ignored; the constructor receives the predicate's extra values instead.
) → A?--
The constructed object (or the existing one if already bound), or nil if the filters rejected the instance.
Binds instance right now. This is what automatic binding calls internally, and what you call
yourself on a ManualBindingOnly Binder.
Steps, in order:
- If the instance is already bound, a warning is printed and the existing object is returned.
-
Binder:CanBind is run. On failure this returns
nil, or errors when the Binder isManualBindingOnly. - Every configured tag is added to the instance.
-
A Trove is extended from Binder.Trove and the constructable is called with
(instance, trove, ...predicateValues). If the result has aDestroymethod it is added to the Trove. Returningnilfrom the constructable is an error. - Binder.InstanceBound fires with
(instance, trove, object).
When the Trove is later cleaned (by Binder:Unbind, removal, or
Binder:Destroy) the tags are removed if RemoveTagsOnCleanup is set and
Binder.InstanceUnbound fires.
Errors
| Type | Description |
|---|---|
| "Unable to bind instance" | The Binder is `ManualBindingOnly` and the predicate rejected the instance. |
| "Nothing was returned by the constructable..." | The constructable returned nil. |
| "Add a .new constructor method to your constructor" | The constructable is a table without a `new` field. |
Unbind
Unbinds instance by cleaning the Trove that was created for it. This destroys the bound object
(if it has a Destroy method), disconnects anything you connected through that Trove, removes
the tags when RemoveTagsOnCleanup is set and fires Binder.InstanceUnbound.
Does nothing if the instance is not bound. The instance itself is not destroyed.
Start
Binder:Start() → ()
Starts automatic binding. Any previous listeners are stopped first, so calling this twice is
safe. Not needed when the Binder was created with AutoStart = true.
Listeners connected:
-
For each
Tagsentry:CollectionService:GetInstanceAddedSignalbinds andGetInstanceRemovedSignalunbinds. -
For each
Ancestorsentry:DescendantAddedbinds andDescendantRemovingunbinds, and every existing descendant is bound in its own thread. -
With no
Ancestors, every existing descendant ofgameis tried once (in its own thread); afterwards only tag changes trigger binds, so give the BinderTagsorAncestorsif you want to react to new instances.
Every candidate still has to pass Binder:CanBind, so tag and ancestor listeners can be combined freely.
Priority queue. Without a Priority, binds and unbinds happen as soon as the event fires.
With a Priority, each bind/unbind is appended to a queue shared by all Binders in the same
VM; on the next Heartbeat the queue is sorted ascending by priority and flushed, so a Binder
with priority 1 binds before one with priority 2 even when their instances appear in the
same frame. The first Binder that needs the queue owns its Heartbeat connection until that
Binder is stopped or destroyed, after which the next queued bind claims it again.
Errors
| Type | Description |
|---|---|
| "Binder is manual only because ManualBindingOnly is set to true..." | The Binder was created with `ManualBindingOnly`. |
Stop
Binder:Stop() → ()Disconnects the listeners created by Binder:Start so no new instances are bound or unbound automatically. Instances that are already bound stay bound; use Binder:Destroy to clean those up as well. Safe to call on a Binder that was never started.
Destroy
Binder:Destroy() → ()
Destroys the Binder by cleaning Binder.Trove. This stops automatic binding, unbinds
every bound instance (firing Binder.InstanceUnbound for each) and destroys
the InstanceBound/InstanceUnbound signals. The Binder should not be used afterwards.