@flamework-experimental/core 2.0.0-alpha.3 → 2.0.0-alpha.4
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/README.md +11 -6
- package/docs/README.md +63 -0
- package/docs/guide/01-getting-started.md +334 -0
- package/docs/guide/02-modules.md +254 -0
- package/docs/guide/03-providers.md +423 -0
- package/docs/guide/04-lifecycle-events.md +423 -0
- package/docs/guide/05-components.md +793 -0
- package/docs/guide/06-networking.md +614 -0
- package/docs/guide/07-macros.md +332 -0
- package/docs/guide/08-plugins.md +203 -0
- package/docs/guide/09-project-structure.md +392 -0
- package/docs/guide/10-migrating-from-v1.md +573 -0
- package/docs/guide/11-scopes.md +165 -0
- package/docs/guide/12-testing.md +342 -0
- package/flamework.build +1 -1
- package/out/index.d.ts +1 -0
- package/out/init.luau +1 -0
- package/out/module/module.luau +1 -1
- package/out/module/moduleBuilder.luau +1 -1
- package/out/utility/getClassesInPath.d.ts +27 -1
- package/out/utility/getClassesInPath.luau +101 -19
- package/out/utility/pathRoot.d.ts +20 -1
- package/out/utility/pathRoot.luau +80 -4
- package/package.json +14 -7
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
# 5. Components
|
|
2
|
+
|
|
3
|
+
A **component** is a class attached to an Instance, usually through a CollectionService tag.
|
|
4
|
+
Flamework constructs one for each tagged instance, checks its attributes and its instance tree, and
|
|
5
|
+
destroys it when the tag is removed.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @flamework-experimental/components
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Map it in your Rojo project next to `core` ([Getting started › Rojo](01-getting-started.md#rojo)).
|
|
12
|
+
|
|
13
|
+
## Your first component
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// src/shared/components/vehicle.ts
|
|
17
|
+
import { BaseComponent, Component } from "@flamework-experimental/components";
|
|
18
|
+
import { OnStart } from "@flamework-experimental/core";
|
|
19
|
+
|
|
20
|
+
interface Attributes {
|
|
21
|
+
speed: number;
|
|
22
|
+
label?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Component({ tag: "Vehicle" })
|
|
26
|
+
export class Vehicle extends BaseComponent<Attributes, Model> implements OnStart {
|
|
27
|
+
public onStart() {
|
|
28
|
+
print(this.instance.Name, this.attributes.speed);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`BaseComponent<A, I>` gives you `this.instance`, typed as `I`, and `this.attributes`, typed as
|
|
34
|
+
`Readonly<A>`. The transformer reads both type parameters and generates *guards* from them: checks
|
|
35
|
+
that run at runtime.
|
|
36
|
+
|
|
37
|
+
Register the components and include the plugin:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { ComponentPlugin } from "@flamework-experimental/components";
|
|
41
|
+
|
|
42
|
+
Flamework.createModule()
|
|
43
|
+
.includePlugin(ComponentPlugin.fromPath("src/shared/components"))
|
|
44
|
+
.registerProviders("src/server/services")
|
|
45
|
+
.ignite();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Tag a `Model` with `Vehicle` in Studio, set a `speed` attribute, and the component is constructed.
|
|
49
|
+
|
|
50
|
+
### Shorthand vs full form
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// Shorthand: register a folder and build the plugin in one call
|
|
54
|
+
ComponentPlugin.fromPath("src/shared/components");
|
|
55
|
+
|
|
56
|
+
// Full form: the same thing, with room to add more
|
|
57
|
+
ComponentPlugin.createPlugin()
|
|
58
|
+
.registerComponents("src/shared/components")
|
|
59
|
+
.registerComponent(SpecialCase)
|
|
60
|
+
.build();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Lifecycle
|
|
64
|
+
|
|
65
|
+
Components are constructed through the module that includes `ComponentPlugin`. So they get
|
|
66
|
+
`onTick`, `onPhysics` and `onRender` from **that module's** lifecycle plugin, which every module
|
|
67
|
+
starts with. `onInit` and `onStart` are the exceptions: `Components` calls both itself, so they work
|
|
68
|
+
even with `disableDefaultLifecycle()`. In order:
|
|
69
|
+
|
|
70
|
+
| Step | When |
|
|
71
|
+
|---|---|
|
|
72
|
+
| constructor | Dependencies are injected. `this.instance`, `this.attributes` and every link are already resolved. |
|
|
73
|
+
| `onInit()` | Runs synchronously, right after construction, **before the component can be seen**: `getComponent` has not handed it back yet, no other component holds it in `childComponents` or `attributeComponents`, and no `onComponentAdded` listener has heard of it. A Promise it returns is not awaited. If it raises, the component is **invalid** (see below). |
|
|
74
|
+
| attached | `getComponent` answers, links resolve to it, added listeners fire. |
|
|
75
|
+
| `onStart()` | Runs on its own thread, after the component is attached, and not before ignition has finished. So a component built from a provider's `onInit` starts once every provider has started. A component that removes itself here is announced as added and then as removed, and no `waitForComponent` is resolved with it. |
|
|
76
|
+
| per-frame events | From the module's lifecycle plugin. |
|
|
77
|
+
| `destroy()` | When the component is removed. `BaseComponent`'s own releases only the `onAttributeChanged` handlers; see [Cleaning up](#cleaning-up). |
|
|
78
|
+
|
|
79
|
+
Put the setup that anything else may rely on in `onInit`. Another component that links to this one
|
|
80
|
+
(see [Links](#links)) receives it already initialised, whichever of the two was tagged first.
|
|
81
|
+
|
|
82
|
+
**Tagged instances get their components once the module has ignited**, after every provider's
|
|
83
|
+
`onStart` has been called and has run up to its first yield: the plugin starts watching tags in its
|
|
84
|
+
`onIgnited` hook, after the lifecycle plugin has started the providers. So a provider's `onStart`
|
|
85
|
+
finds none of them with `getAllComponents<T>()` or `getComponents<T>(instance)`, and
|
|
86
|
+
`onComponentAdded<T>(cb)` connected there hears about each one as it is built. That listener never
|
|
87
|
+
replays components that already exist, so a listener connected later (after a yield, from a lazy
|
|
88
|
+
provider, from an event handler) reads `getAllComponents<T>()` first. `getComponent` is the
|
|
89
|
+
exception: it builds a qualifying component on demand, at any time. See
|
|
90
|
+
[Lifecycle events › Components](04-lifecycle-events.md#components).
|
|
91
|
+
|
|
92
|
+
### Cleaning up
|
|
93
|
+
|
|
94
|
+
`BaseComponent.destroy()` releases only what Flamework connected for the component, the handlers
|
|
95
|
+
behind `onAttributeChanged`. Removing the tag, or `removeComponent`, leaves the instance where it
|
|
96
|
+
is, so a connection of your own keeps firing into a component that has gone. Disconnect it in
|
|
97
|
+
`destroy`, and call `super.destroy()`:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
@Component({ tag: "Coin" })
|
|
101
|
+
export class Coin extends BaseComponent<{}, BasePart> implements OnStart {
|
|
102
|
+
private touched?: RBXScriptConnection;
|
|
103
|
+
|
|
104
|
+
public onStart() {
|
|
105
|
+
this.touched = this.instance.Touched.Connect((part) => this.collect(part));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public override destroy() {
|
|
109
|
+
this.touched?.Disconnect();
|
|
110
|
+
super.destroy();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private collect(part: BasePart) {}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`destroy` also runs for every component when the module extinguishes. A `destroy` that raises does
|
|
118
|
+
not hold up the teardown (see [Caveats](#caveats)).
|
|
119
|
+
|
|
120
|
+
**Removing the component from its own constructor or `onInit`** undoes the construction as it
|
|
121
|
+
finishes. This covers `removeComponent`, and taking its tag away where the place delivers signals
|
|
122
|
+
immediately. The component is destroyed, and never attached, started or announced.
|
|
123
|
+
|
|
124
|
+
**An `onInit` that raises** does not take the component away, and does not build another one. The
|
|
125
|
+
component stays where it is, marked invalid:
|
|
126
|
+
|
|
127
|
+
- it gets no `onStart` and no per-frame events;
|
|
128
|
+
- it is absent from `getComponent`, `getComponents`, `getAllComponents` and `waitForComponent`;
|
|
129
|
+
- no added listener hears of it;
|
|
130
|
+
- a component that links to it keeps waiting. The warning says
|
|
131
|
+
`carries an invalid '...', whose onInit raised: ...`.
|
|
132
|
+
|
|
133
|
+
It still holds its place. Nothing is built on top of it until the *tracker* takes it down for a
|
|
134
|
+
reason of its own: the tag goes, the tree breaks, a link is lost. (The tracker is the part of
|
|
135
|
+
Flamework that watches instances and builds and removes their components.) The component built once
|
|
136
|
+
that reason has passed is a fresh one, with its own `onInit`.
|
|
137
|
+
|
|
138
|
+
The failure is reported in two ways. For a tagged instance, Flamework warns
|
|
139
|
+
(`Failed to instantiate ...`). `addComponent` by hand raises
|
|
140
|
+
`component '...' failed to initialise for ...`, and raises again, with `waiting to be removed`,
|
|
141
|
+
while the invalid component is there. `removeComponent` clears it.
|
|
142
|
+
|
|
143
|
+
Register by glob when the components are spread across feature folders:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
ComponentPlugin.fromGlob("src/**/components");
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A module may include several component plugins, in any mix: a `fromPath` per folder, a `fromGlob`,
|
|
150
|
+
one built by hand. They share **one** `Components` for that module:
|
|
151
|
+
|
|
152
|
+
- every class any of them registers ends up in it;
|
|
153
|
+
- a component in one can link to a component in another;
|
|
154
|
+
- `Dependency<Components>()` and constructor injection get that one `Components`.
|
|
155
|
+
|
|
156
|
+
A class that two plugins register is registered once, and kept when either registration's scope
|
|
157
|
+
holds. A module that imports another keeps its own `Components` if it includes a component plugin of
|
|
158
|
+
its own. If it includes none, it resolves the import's `Components`.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
Flamework.createModule()
|
|
162
|
+
.includePlugin(ComponentPlugin.fromPath("src/shared/components"))
|
|
163
|
+
.includePlugin(ComponentPlugin.fromPath("src/server/components"))
|
|
164
|
+
.ignite();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Path and glob registration find every component defined in the files there, exported or not, the
|
|
168
|
+
way `registerProviders` finds providers ([Providers](03-providers.md#how-it-actually-works)).
|
|
169
|
+
|
|
170
|
+
As with providers, only classes decorated with `@Component()` **themselves** are registered. An
|
|
171
|
+
exported but undecorated subclass is skipped, and `registerComponent` raises an error for one.
|
|
172
|
+
|
|
173
|
+
## Attributes
|
|
174
|
+
|
|
175
|
+
Attribute guards are generated from the first type parameter. An instance whose attributes do not
|
|
176
|
+
match is rejected: the component is not created, and `addComponent` throws
|
|
177
|
+
`... has invalid attribute 'speed' for '...'`.
|
|
178
|
+
|
|
179
|
+
Optional properties really are optional: `label?: string` accepts a missing attribute.
|
|
180
|
+
|
|
181
|
+
### Defaults
|
|
182
|
+
|
|
183
|
+
Instead of rejecting the instance, you can write a default value back to it:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
@Component({ tag: "Vehicle", defaults: { speed: 16 } })
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
A missing or invalid `speed` becomes `16`, and the attribute is set on the instance.
|
|
190
|
+
|
|
191
|
+
### Reacting to changes
|
|
192
|
+
|
|
193
|
+
Attributes are tracked by default, so `this.attributes` stays current:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
this.onAttributeChanged("speed", (newValue, oldValue) => {
|
|
197
|
+
print(`${oldValue} -> ${newValue}`);
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
A value that fails the guard is never applied:
|
|
202
|
+
|
|
203
|
+
- With no `defaults` entry for the attribute, the component is removed. Once the attribute is valid,
|
|
204
|
+
the component is built again, reading the attributes afresh.
|
|
205
|
+
- With a `defaults` entry, the component keeps its last good value.
|
|
206
|
+
|
|
207
|
+
Turn tracking off with `refreshAttributes: false`, which also disables `onAttributeChanged`. The
|
|
208
|
+
validity of the attributes is watched either way. Like the instance tree, it is a *criterion*: a
|
|
209
|
+
condition the component needs in order to exist.
|
|
210
|
+
|
|
211
|
+
### Writing an attribute
|
|
212
|
+
|
|
213
|
+
Assigning to `this.attributes` writes the value back to the instance:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
this.attributes.speed = 32;
|
|
217
|
+
this.attributes.speed += 8;
|
|
218
|
+
this.attributes.speed++;
|
|
219
|
+
delete this.attributes.label;
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The write has to be spelled `<component>.attributes.<name>`, because that is the shape the
|
|
223
|
+
transformer rewrites. The component does not have to be `this`: a component reached through
|
|
224
|
+
`getComponent` is written the same way. A write through a local
|
|
225
|
+
(`const attributes = this.attributes; attributes.speed = 32`) is an ordinary table write, and the
|
|
226
|
+
instance never hears about it. A read-modify-write (`+=`, `++`, `--`) evaluates the component
|
|
227
|
+
expression a second time for the value it computes, so keep side effects out of that expression.
|
|
228
|
+
|
|
229
|
+
Every write is checked against the same guard the attribute was accepted with, and raises an error
|
|
230
|
+
if it fails. This catches a write that a cast let through:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// Raises: 'fast' is not a valid value for attribute 'speed' of '...'
|
|
234
|
+
this.attributes.speed = someString as unknown as number;
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Without the check, the component would hold a value that its own declared type says is impossible.
|
|
238
|
+
The instance would carry it too, and reject the component the next time one is built. Writing
|
|
239
|
+
`undefined` to a required attribute raises for the same reason. An optional attribute accepts
|
|
240
|
+
`undefined`, and the attribute is cleared.
|
|
241
|
+
|
|
242
|
+
### Overriding a guard
|
|
243
|
+
|
|
244
|
+
To use your own guard for an attribute instead of the generated one, pass it in `attributes`:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
@Component({
|
|
248
|
+
tag: "Vehicle",
|
|
249
|
+
attributes: { speed: t.numberPositive },
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Instance guards
|
|
254
|
+
|
|
255
|
+
The second type parameter is the instance tree. `BaseComponent<{}, Part>` will not attach to a
|
|
256
|
+
Folder. To require children, intersect it with an object type, as deep as you like:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// Requires a Humanoid child, and a Head with a Face, before the component is created
|
|
260
|
+
@Component({ tag: "Character" })
|
|
261
|
+
export class Character extends BaseComponent<{}, Model & { Humanoid: Humanoid; Head: BasePart & { Face: Decal } }> {}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The transformer writes the tree down as data: the classes each instance may be, and the children it
|
|
265
|
+
must have, by name. Flamework reads the tree the way your code reads `this.instance.Head`: with
|
|
266
|
+
`FindFirstChild`, which returns the first child of that name. So a second child with the same name
|
|
267
|
+
is not an error, and it is not the one that is checked. It can come and go without the component
|
|
268
|
+
noticing.
|
|
269
|
+
|
|
270
|
+
A child may be a union of classes (`Texture | Decal`). A union of whole trees, such as
|
|
271
|
+
`(Model & { Root: Part }) | (Folder & { Core: Folder })`, cannot be written down this way. It gets a
|
|
272
|
+
`t` guard instead, which can only be re-run whole.
|
|
273
|
+
|
|
274
|
+
A mismatch is named. `addComponent` raises with `child 'Head.Face' is missing (expected Decal)` or
|
|
275
|
+
`child 'Head' is a Folder, expected BasePart`, and the warning for a tagged instance that never
|
|
276
|
+
qualifies says the same (see [Streaming](#streaming)).
|
|
277
|
+
|
|
278
|
+
A child cannot be optional, and Flamework rejects one when you build:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
// Rejected: `this.instance.Head` would error whenever the child is missing
|
|
282
|
+
export class Character extends BaseComponent<{}, Model & { Head?: BasePart }> {}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`this.instance.Head` indexes the instance itself, and Roblox raises an error for a child that is not
|
|
286
|
+
there, rather than returning nothing. Even the `if (this.instance.Head)` you would write to check
|
|
287
|
+
for it raises. So the optional type would promise a read that cannot be made. Either require the
|
|
288
|
+
child, or leave it out of the tree and get it with `FindFirstChild`. A child that names a
|
|
289
|
+
**component** is no exception: name a component that may or may not be there with an optional link
|
|
290
|
+
attribute, or look it up with `getComponent`. See [links](#links).
|
|
291
|
+
|
|
292
|
+
Attributes work differently and stay optional: a missing one reads back as `undefined`, so
|
|
293
|
+
`label?: string` is fine.
|
|
294
|
+
|
|
295
|
+
If the generated guard is not what you want, replace it entirely with `instanceGuard`. A guard
|
|
296
|
+
written by hand can only say that it failed, and it can only be re-run whole when the tree changes.
|
|
297
|
+
|
|
298
|
+
## Links
|
|
299
|
+
|
|
300
|
+
A **link** is an attribute or a child that names another instance. Flamework waits for the named
|
|
301
|
+
instance, keeps the link resolved, and takes the component down again if the instance goes away. A
|
|
302
|
+
link can name an instance, or a component on an instance.
|
|
303
|
+
|
|
304
|
+
### Instance attributes
|
|
305
|
+
|
|
306
|
+
An attribute typed as an Instance is stored on the instance as an `InstanceHandle`, which is what
|
|
307
|
+
Roblox's own instance-valued attributes are:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
interface Attributes {
|
|
311
|
+
Target: BasePart;
|
|
312
|
+
Spare?: BasePart;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@Component({ tag: "Turret" })
|
|
316
|
+
export class Turret extends BaseComponent<Attributes, Model> {
|
|
317
|
+
public onStart() {
|
|
318
|
+
// The handle is resolved for you; this is the part itself.
|
|
319
|
+
print(this.attributes.Target.Position);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The component is not constructed until the handle resolves. A handle is empty until the instance it
|
|
325
|
+
names has streamed in at least once. So under StreamingEnabled, a far-away target keeps the
|
|
326
|
+
component waiting. Once the target has streamed in, the handle stays resolved, even if the target
|
|
327
|
+
streams back out.
|
|
328
|
+
|
|
329
|
+
Assigning to the attribute writes a fresh handle, after checking the instance the same way the link
|
|
330
|
+
was resolved:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
this.attributes.Target = otherPart;
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The check is the whole guard, structure included. A link to a component that declares
|
|
337
|
+
`Model & { Root: BasePart }` only accepts a model that has that child. Assigning an instance that
|
|
338
|
+
could never be right raises an error.
|
|
339
|
+
|
|
340
|
+
An attribute typed `InstanceHandle` is left alone: you get the handle, and nothing waits. Use this
|
|
341
|
+
when you want to do the resolving yourself.
|
|
342
|
+
|
|
343
|
+
`defaults` works here as it does elsewhere. Give an instance as the default, and an attribute that
|
|
344
|
+
was never written is filled in with a handle for it, instead of keeping the component waiting. That
|
|
345
|
+
holds for an optional link too, whose guard would accept the attribute being missing. The default is
|
|
346
|
+
written to the instance either way, so the component and the instance never disagree about what the
|
|
347
|
+
link names. The default stands in for an attribute the component was **built** without, not for one
|
|
348
|
+
the component has since cleared. Clearing an optional link clears it, and the default is
|
|
349
|
+
applied again the next time a component is built.
|
|
350
|
+
|
|
351
|
+
### Naming a component
|
|
352
|
+
|
|
353
|
+
Type an attribute or a child as a **component** rather than an Instance, and the instance it names
|
|
354
|
+
has to carry that component:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
interface Tree extends Model {
|
|
358
|
+
EffectHandler: EffectHandlerComponent;
|
|
359
|
+
Barrel: BasePart;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@Component({ tag: "Turret" })
|
|
363
|
+
export class Turret extends BaseComponent<{ Owner: PlayerComponent }, Tree> {
|
|
364
|
+
public onStart() {
|
|
365
|
+
// `instance` holds instances, and the components sit beside it.
|
|
366
|
+
const part: BasePart = this.instance.EffectHandler;
|
|
367
|
+
|
|
368
|
+
this.childComponents.EffectHandler.playEffect();
|
|
369
|
+
this.attributeComponents.Owner.credit();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`this.instance` still holds instances: `this.instance.EffectHandler` is the part the component is
|
|
375
|
+
attached to, and that is what the generated instance guard checks. The components themselves live in
|
|
376
|
+
`childComponents` and `attributeComponents`. Their fields are readonly: Flamework owns them, and
|
|
377
|
+
reassigning one would only put it out of step with the instance.
|
|
378
|
+
|
|
379
|
+
The tree *under* a child that names a component is that component's business, not the owner's. The
|
|
380
|
+
owner's shape stops at the child's class. Whether the rest is there is decided by the linked
|
|
381
|
+
component's own tracker, under its own streaming mode. (A component type cannot be intersected with
|
|
382
|
+
a tree of its own, since `Handler & { Root: Part }` is not a type, so there is nothing the owner
|
|
383
|
+
could add.)
|
|
384
|
+
|
|
385
|
+
`Turret` is not constructed until `EffectHandler` exists **and** carries its component, in either
|
|
386
|
+
tag order. It is removed again if that component goes away. This uses the same criteria mechanism
|
|
387
|
+
as component dependencies and streaming, so the warning that lists what a component is waiting for
|
|
388
|
+
names the link.
|
|
389
|
+
|
|
390
|
+
A link resolves the class it names and **nothing else**. A subclass does not stand in for its
|
|
391
|
+
parent. An instance carrying several components hands back the one the link names, not whichever
|
|
392
|
+
came first, so there is no ambiguity to resolve. This works in both directions: another component
|
|
393
|
+
leaving the linked instance changes nothing, even a subclass of the one the link names. The guard
|
|
394
|
+
is the whole shape too: a link to a component declaring `Model & { Root: BasePart }` only accepts a
|
|
395
|
+
model that has that child.
|
|
396
|
+
|
|
397
|
+
A link waits for what `getComponent` would hand back, and on top of that it respects the ancestor
|
|
398
|
+
lists (see [Where components may attach](#where-components-may-attach)). If a `predicate` refuses
|
|
399
|
+
the linked component, or its instance sits under a blocked ancestor, the link stays unmet and the
|
|
400
|
+
component is not built. A link never reports itself met and then fails to build the component. It
|
|
401
|
+
also never builds the linked component there itself: pointing a link attribute at a tagged instance
|
|
402
|
+
under a blocked ancestor leaves the link unmet, rather than constructing the component the ancestor
|
|
403
|
+
lists refused. The ancestor lists gate *construction*, not the link. So a component that is already
|
|
404
|
+
attached to a blocked instance (added by hand, or built by a `getComponent` of your own) does
|
|
405
|
+
satisfy the link.
|
|
406
|
+
|
|
407
|
+
"What `getComponent` would hand back" is the whole rule. A link is met by an instance that already
|
|
408
|
+
carries the component, **or** by one that Flamework would build it on. The answer is the same
|
|
409
|
+
whether or not anything is tracking that instance yet. So a spawner can tag a whole tree and ask for
|
|
410
|
+
its component straight away. Tag announcements arrive a resumption later, and `getComponent` builds
|
|
411
|
+
the link's component on the way to building yours, rather than refusing because the announcement
|
|
412
|
+
has not arrived yet.
|
|
413
|
+
|
|
414
|
+
A link that names its own component on its own instance is unmet for the same reason. It is the one
|
|
415
|
+
link that can never be met on the way in, because the component would already have to exist to be
|
|
416
|
+
built. So the component is not built, and the link reports this instead of raising an error out of
|
|
417
|
+
the tag that asked for it. Point the attribute somewhere else (or, if it is optional, clear it) and
|
|
418
|
+
the component is built. Pointing it back at its own instance afterwards resolves to the component
|
|
419
|
+
that is now there. A ring of links works the same way, however many instances it goes round: none of
|
|
420
|
+
it can be built from nothing, so the ring stays unmet until something in it exists for another
|
|
421
|
+
reason.
|
|
422
|
+
|
|
423
|
+
That promise covers a rebuild as well. Roblox delivers the tree's signals a resumption late, so a
|
|
424
|
+
change that takes a component down and a change that should keep it down can arrive one after the
|
|
425
|
+
other. That is why every link is read from the instance again on the way in, rather than trusted to
|
|
426
|
+
still be what it last reported. A component is built only when the tree agrees.
|
|
427
|
+
|
|
428
|
+
The guard is also kept current, not read once when the attribute is written. A link to a component
|
|
429
|
+
declaring `Model & { Root: BasePart }` is unmet while the model it names has no `Root`, and becomes
|
|
430
|
+
met when one is parented in. So an attribute may be written before the instance it names is
|
|
431
|
+
finished, and the component is built once that instance is finished.
|
|
432
|
+
|
|
433
|
+
A component can only be named as a **direct** member of the tree. One further down is a compile
|
|
434
|
+
error, because `this.instance` would have nowhere to put it. Declare it on the component attached to
|
|
435
|
+
that child instead, or look it up with `getComponent`.
|
|
436
|
+
|
|
437
|
+
A child naming a component cannot be optional, just as a plain child cannot: `this.instance.Core`
|
|
438
|
+
still indexes the instance, and raises while the child is missing. Name a component that may or may
|
|
439
|
+
not be there through an [attribute](#instance-attributes) instead, which can be optional, or look it
|
|
440
|
+
up with `getComponent` when you need it.
|
|
441
|
+
|
|
442
|
+
#### Writing one
|
|
443
|
+
|
|
444
|
+
Sometimes you assign an instance that is the right shape but does not carry the component **yet**.
|
|
445
|
+
That is a matter of timing, not a bad value, so it does not raise. But writing it would make the
|
|
446
|
+
component doing the writing stop qualifying, and destroy it in the middle of a method. So instead,
|
|
447
|
+
the write is refused and Flamework warns. Wait for the component first:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
// Components has to be injected for this; ComponentMetadata comes first.
|
|
451
|
+
const [ok] = this.components.waitForComponent<Rig>(target).timeout(5).await();
|
|
452
|
+
if (!ok) return warn("that instance never got its component");
|
|
453
|
+
|
|
454
|
+
this.attributes.Rigged = target;
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
The resolved attribute type already asks for the linked component's instance type, so most mistakes
|
|
458
|
+
here are compile errors. The guard catches the ones a cast let through.
|
|
459
|
+
|
|
460
|
+
Writing an instance under a blocked ancestor is handled the same way, because a link never builds a
|
|
461
|
+
component where the ancestor lists keep one out. The write is refused with a warning, whether or not
|
|
462
|
+
that instance is tagged.
|
|
463
|
+
|
|
464
|
+
### What takes a component down again
|
|
465
|
+
|
|
466
|
+
| Change | Effect |
|
|
467
|
+
|---|---|
|
|
468
|
+
| The tag is removed | Removed. |
|
|
469
|
+
| The instance leaves the DataModel (unparented, destroyed, or an **ancestor** of it unparented) | Removed. CollectionService announces the tag as gone for the whole subtree that left, and announces it again when it is parented back in, so the components come back with it. Moving an instance *within* the DataModel announces nothing and changes nothing. |
|
|
470
|
+
| The component a link names is destroyed | Removed, whatever the streaming mode: that is a lifecycle event, not the tree moving. |
|
|
471
|
+
| Some **other** component on a linked instance is destroyed | **Kept**, including a subclass of the one the link names. |
|
|
472
|
+
| A link attribute is re-pointed at something that fails its guard | Removed, and built again if it is pointed back at something valid. |
|
|
473
|
+
| The instance a **plain** link attribute names stops passing its guard | Removed, and built again once it passes. The guard carries the whole shape, so a linked model losing the child the link asked for counts, whatever the streaming mode: the target's tree is not this component's tree. |
|
|
474
|
+
| The tree under a linked **component** breaks | Depends on that component's own streaming mode, since its tree is its business. Under `Watching` it goes and takes this component with it. Under `Disabled` it stays, and so does this one. |
|
|
475
|
+
| A required link attribute is cleared from outside | Removed. |
|
|
476
|
+
| A plain attribute is changed to a value its guard rejects | Removed, and built again once it is valid. The warning names it: `invalid attribute 'speed' ("fast")`. With a `defaults` entry for it, **kept** with its last good value, and `onAttributeChanged` does not fire. |
|
|
477
|
+
| A child a link names is replaced by another instance of the same name | Removed and built again around the new one, so it never holds a child that has left. |
|
|
478
|
+
| The instance tree stops matching (a child goes, including one a link names) | Follows `streamingMode` (below). |
|
|
479
|
+
|
|
480
|
+
Swaps need a note, because signals are deferred. When a child is parented out and its replacement is
|
|
481
|
+
parented in within one resumption, they arrive as a single change: the child of that name is now a
|
|
482
|
+
different instance. There is never a moment with no child at all. It is still a different tree, so
|
|
483
|
+
the component is still rebuilt.
|
|
484
|
+
|
|
485
|
+
This holds even when the swap happens before Flamework starts watching. `getComponent` builds a
|
|
486
|
+
component the moment you ask for it, but the tag that makes Flamework follow its tree is announced a
|
|
487
|
+
resumption later. If the tree changes in between, Flamework compares it with the child the component
|
|
488
|
+
was actually built with, not with whatever the tree holds when Flamework first looks.
|
|
489
|
+
|
|
490
|
+
The streaming mode only affects the last row, because that is the only row about the tree rather
|
|
491
|
+
than about another object's lifetime:
|
|
492
|
+
|
|
493
|
+
| `ComponentStreamingMode` | A child, or a child link, going away |
|
|
494
|
+
|---|---|
|
|
495
|
+
| `Contextual` (default) | Re-checked on a client, ignored on a server. |
|
|
496
|
+
| `Watching` | Re-checked, so the component goes and comes back with its tree. |
|
|
497
|
+
| `Disabled` | Read once and kept. The component stays, still holding the child it resolved to. |
|
|
498
|
+
|
|
499
|
+
`Disabled` reads the *tree* once, not the components in it. A child that moves elsewhere in the
|
|
500
|
+
DataModel keeps its tag and its components, and that is the case where `Disabled` keeps the
|
|
501
|
+
component. A child that is unparented or destroyed loses its own components on the way out. So a
|
|
502
|
+
link naming one of them falls under the "component a link names is destroyed" row of the previous
|
|
503
|
+
table, not under streaming at all: the owner goes with it whatever the mode.
|
|
504
|
+
|
|
505
|
+
`Disabled` applies to the component that was built, not to the next one. Something else can take
|
|
506
|
+
that component down, say a link attribute re-pointed at an instance its guard refuses. The build
|
|
507
|
+
that follows reads the tree as it is then. So if the tree no longer holds the child a child link
|
|
508
|
+
names, the component stays down until it does. While the component is down, a child link looks its
|
|
509
|
+
child up again whenever a child of that name arrives, and whenever the child it holds loses its
|
|
510
|
+
component. So if a linked child is destroyed and replaced by another that carries the component, the
|
|
511
|
+
component comes back around the new one. This happens under `Disabled`, and under `Contextual` on a
|
|
512
|
+
server.
|
|
513
|
+
|
|
514
|
+
### Waiting and warnings
|
|
515
|
+
|
|
516
|
+
| Option | Effect |
|
|
517
|
+
|---|---|
|
|
518
|
+
| `warningTimeout` | Seconds before Flamework says what a component is still waiting for, links included. |
|
|
519
|
+
| `attributeWarningTimeout` | Seconds before it says an attribute's instance has not streamed in. Defaults to `warningTimeout`. |
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
@Component({ tag: "Turret", attributeWarningTimeout: 10 })
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Both default to 5, and `0` disables them. Keep instances that an attribute names somewhere that is
|
|
526
|
+
always loaded, such as ReplicatedStorage or inside the same model, and the wait never happens.
|
|
527
|
+
|
|
528
|
+
The warning explains why, criterion by criterion, and follows a link into the linked component's own
|
|
529
|
+
reasons:
|
|
530
|
+
|
|
531
|
+
```
|
|
532
|
+
Waiting for component 'Turret'
|
|
533
|
+
Waiting for the following criteria: instance guard (child 'Barrel' is missing (expected BasePart)),
|
|
534
|
+
child 'EffectHandler' with component 'EffectHandlerComponent' (Workspace.Turret.EffectHandler is
|
|
535
|
+
waiting for: CollectionService tag), attribute 'Owner' with component 'PlayerComponent' (the
|
|
536
|
+
attribute names nothing that has streamed in)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Writing a link attribute reports problems the same way. An instance that could never carry the
|
|
540
|
+
component it names raises an error with that component's reason:
|
|
541
|
+
`did not pass the guard for attribute 'Rig' of 'Turret': instance guard (child 'Root' is missing (expected BasePart))`.
|
|
542
|
+
An instance of the right shape that just has no component yet is refused with a warning, and the
|
|
543
|
+
attribute is left alone.
|
|
544
|
+
|
|
545
|
+
A warning is only ever about something that is **waiting**. A link watches the instance it names
|
|
546
|
+
without waiting for it, so it neither starts a warning nor keeps one going. If you untag an
|
|
547
|
+
instance, the warning goes with the tag, however many links are still watching, and tagging it
|
|
548
|
+
again starts the wait over. The same goes down the chain, in both directions. The components a
|
|
549
|
+
watched component depends on are watched too, and report nothing until something asks for the
|
|
550
|
+
component itself. When the tag that was asking goes, their warnings go with it.
|
|
551
|
+
|
|
552
|
+
The warning also comes again after every loss. A component that goes down (its tree broke, a link
|
|
553
|
+
was lost, an attribute went bad) and stays down for `warningTimeout` seconds warns just like one that
|
|
554
|
+
never came up, with the reason.
|
|
555
|
+
|
|
556
|
+
## Where components may attach
|
|
557
|
+
|
|
558
|
+
| Option | Effect |
|
|
559
|
+
|---|---|
|
|
560
|
+
| `predicate` | Rejects an instance outright, before anything else runs. |
|
|
561
|
+
| `ancestorWhitelist` | Only construct under these ancestors. Takes priority over the blocklist. |
|
|
562
|
+
| `ancestorBlacklist` | Never construct under these. Defaults to ServerStorage, ReplicatedStorage, StarterPack, StarterGui and StarterPlayer. |
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
@Component({ tag: "Prop", predicate: (instance) => instance.Name !== "Template" })
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
The default blocklist is why a tagged template in ReplicatedStorage gets no component, while a
|
|
569
|
+
tagged instance cloned into Workspace does.
|
|
570
|
+
|
|
571
|
+
The ancestor lists only gate construction that Flamework drives: a tag, or a link to the component.
|
|
572
|
+
So you can still attach a component by hand to something in ReplicatedStorage. The `predicate` also
|
|
573
|
+
gates the eager path in `getComponent`: an instance it rejects never gets a component unless you call
|
|
574
|
+
`addComponent` yourself. `addComponent` ignores all three options.
|
|
575
|
+
|
|
576
|
+
A component can also be tied to the build's scopes with `activeIn` and `inactiveIn`, on the
|
|
577
|
+
decorator or on the registration (`registerComponent(Class, { ... })`, `fromPath(path, { ... })`).
|
|
578
|
+
A component left out by scope is not registered in the plugin at all. It is never attached, and
|
|
579
|
+
`getComponent` on it raises an error with the reason. See [Scopes](11-scopes.md).
|
|
580
|
+
|
|
581
|
+
When the condition on `fromPath`, `fromGlob`, `registerComponents` or `registerComponentsGlob` does
|
|
582
|
+
not hold, the folder is not loaded at all, so it can be missing from the place. The error from
|
|
583
|
+
`getComponent` then names that registration. A condition on `includePlugin` does not stop
|
|
584
|
+
`fromPath` from loading its folder: `fromPath` looks the folder up when you call it.
|
|
585
|
+
|
|
586
|
+
## Streaming
|
|
587
|
+
|
|
588
|
+
With StreamingEnabled, an instance can arrive before its descendants. So an instance tree that
|
|
589
|
+
requires children may be incomplete at first, and complete a moment later.
|
|
590
|
+
|
|
591
|
+
| `ComponentStreamingMode` | Behaviour |
|
|
592
|
+
|---|---|
|
|
593
|
+
| `Contextual` (default) | Watches on the client; never on the server; skips atomic models, which replicate whole. |
|
|
594
|
+
| `Watching` | Always follows the tree as it changes. |
|
|
595
|
+
| `Disabled` | Reads the tree once. |
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
@Component({ tag: "Character", streamingMode: ComponentStreamingMode.Watching })
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Watching uses one watcher per required child. It does not re-check the whole tree. For
|
|
602
|
+
`Model & { Root: Part & { Texture: Texture } }`, it listens for children arriving and leaving on the
|
|
603
|
+
model and, once `Root` has resolved, on `Root`. With `watchRenames` on (below), it also follows the
|
|
604
|
+
`Name` of each resolved child. A `Texture` arriving three levels down re-resolves only that one
|
|
605
|
+
slot. A child that is not in the tree, or a second child with a required name, changes nothing
|
|
606
|
+
however often it moves.
|
|
607
|
+
|
|
608
|
+
Renames are not followed unless the component asks for it with `watchRenames: true` (or
|
|
609
|
+
`components.watchRenames` in `flamework.config.json`). A child is rarely renamed, and only the
|
|
610
|
+
renamed child announces a rename. So following names costs a connection on each resolved child.
|
|
611
|
+
While a required child is missing, it also costs one on every other child, since that is the only
|
|
612
|
+
way to hear a sibling being renamed to the required name. Once the required child resolves, the
|
|
613
|
+
children ahead of it in child order stay followed: `FindFirstChild` reads the first child of a name,
|
|
614
|
+
so one of them renamed to that name becomes the one read.
|
|
615
|
+
|
|
616
|
+
With `watchRenames` left off, a rename is noticed only the next time that slot is read. That covers
|
|
617
|
+
a child renamed away, and a sibling renamed to a required name. The slot is read when a child of
|
|
618
|
+
that name arrives, when the child it resolved to leaves, or when the tag arrives.
|
|
619
|
+
|
|
620
|
+
A guard written by hand with `instanceGuard` has no such structure. It is re-run whole on every
|
|
621
|
+
descendant change, and hears no rename at all.
|
|
622
|
+
|
|
623
|
+
When a watched component's tree breaks apart again, the component is removed. That holds however the
|
|
624
|
+
component came to qualify. For example, a tag arriving at an instance that another component's link
|
|
625
|
+
was already watching re-reads the tree, and what it reads is what is watched from then on. So the
|
|
626
|
+
component still goes and comes back with its tree afterwards.
|
|
627
|
+
|
|
628
|
+
If an instance never qualifies, Flamework warns after `warningTimeout` seconds (default 5, `0`
|
|
629
|
+
disables) and lists the criteria it is still waiting on, such as
|
|
630
|
+
`instance guard (child 'Root.Texture' is missing (expected Texture))`. This is usually the fastest
|
|
631
|
+
way to find a typo in a tag or a missing child.
|
|
632
|
+
|
|
633
|
+
## Component dependencies
|
|
634
|
+
|
|
635
|
+
A component can depend on another component **on the same instance**. Declare it as a constructor
|
|
636
|
+
parameter. `ComponentMetadata` has to come first, because `BaseComponent` takes it:
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
import { BaseComponent, Component, ComponentMetadata } from "@flamework-experimental/components";
|
|
640
|
+
|
|
641
|
+
@Component({ tag: "Car" })
|
|
642
|
+
export class Car extends BaseComponent<{}, Model> {
|
|
643
|
+
constructor(
|
|
644
|
+
metadata: ComponentMetadata,
|
|
645
|
+
private engine: Engine,
|
|
646
|
+
) {
|
|
647
|
+
super(metadata);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
`Car` is not constructed until `Engine` exists on the same instance, in either tag order. This is
|
|
653
|
+
the same criteria mechanism that streaming uses. Some `Engine`s Flamework never builds by itself: one
|
|
654
|
+
with no tag, or one its predicate refuses on this instance. Such an `Engine` counts once you add it
|
|
655
|
+
with `addComponent`. Removing an `Engine` by hand takes `Car` down with it, tagged or not. `Car`
|
|
656
|
+
comes back with the next `Engine`: one you add, or one that `getComponent` builds on the
|
|
657
|
+
still-tagged instance.
|
|
658
|
+
|
|
659
|
+
## Working with components
|
|
660
|
+
|
|
661
|
+
`Components` is a provider exported by the plugin, so inject it:
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
@Provider()
|
|
665
|
+
export class VehicleService {
|
|
666
|
+
constructor(private components: Components) {}
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
| Method | Notes |
|
|
671
|
+
|---|---|
|
|
672
|
+
| `getComponent<T>(instance)` | Exact class only. Constructs eagerly if the instance qualifies. |
|
|
673
|
+
| `getComponents<T>(instance)` | Every component on the instance matching a class **or interface**. |
|
|
674
|
+
| `getAllComponents<T>()` | The same, across every instance. |
|
|
675
|
+
| `addComponent<T>(instance)` | Attaches by hand. Throws if the guards fail. |
|
|
676
|
+
| `removeComponent<T>(instance)` | Detaches and destroys. |
|
|
677
|
+
| `waitForComponent<T>(instance)` | Promise; resolves immediately if it already exists. A waiter whose handler removes the component leaves the others waiting for the next one. |
|
|
678
|
+
| `onComponentAdded<T>(cb)` | Fires for every future component of that type. |
|
|
679
|
+
| `onComponentRemoved<T>(cb)` | Fires after the component has left the lookups: before `destroy` where the place delivers signals immediately, after it where they are deferred. |
|
|
680
|
+
|
|
681
|
+
`getComponent` needs the exact class. The polymorphic ones (`getComponents`, `getAllComponents`,
|
|
682
|
+
and both listeners) accept an interface the component implements, or a superclass that is itself
|
|
683
|
+
decorated with `@Component()` (an abstract base with no `tag`, say). The ids a component answers to
|
|
684
|
+
are read from Flamework's metadata, which only a decorated class carries. So a base class without
|
|
685
|
+
its own decorator is not looked up: asking for it finds nothing, and so does asking for an
|
|
686
|
+
interface only such a base class declares. The same rule decides which `implements` clauses count
|
|
687
|
+
for lifecycle events ([Lifecycle events](04-lifecycle-events.md#the-events)).
|
|
688
|
+
|
|
689
|
+
```ts
|
|
690
|
+
// every component on this instance that implements OnTick
|
|
691
|
+
for (const component of this.components.getComponents<OnTick>(instance)) {
|
|
692
|
+
component.onTick(dt);
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## Patterns
|
|
697
|
+
|
|
698
|
+
**Server and client components with the same tag.** Register different classes from the two entry
|
|
699
|
+
points. They attach to the same instances and never see each other.
|
|
700
|
+
|
|
701
|
+
**An interface for shared behaviour.** Declare `interface Damageable { takeDamage(n): void }`,
|
|
702
|
+
implement it on several components, and use `getComponents<Damageable>(instance)` to reach whichever
|
|
703
|
+
are present.
|
|
704
|
+
|
|
705
|
+
**`waitForComponent` at boundaries.** When a provider needs a component that may not exist yet,
|
|
706
|
+
`await this.components.waitForComponent<Vehicle>(instance)` is better than polling.
|
|
707
|
+
|
|
708
|
+
**Composition over inheritance.** Two components on one instance with a dependency between them are
|
|
709
|
+
usually clearer than a deep component hierarchy. It is also the case Flamework's tracker is built
|
|
710
|
+
for.
|
|
711
|
+
|
|
712
|
+
## Caveats
|
|
713
|
+
|
|
714
|
+
- **`getComponent` constructs.** It is not a pure lookup. If the instance is in the DataModel,
|
|
715
|
+
tagged, passes the predicate and qualifies, `getComponent` builds the component then and there,
|
|
716
|
+
ignoring the ancestor lists. The instance must be in the DataModel because that is what announces
|
|
717
|
+
the tag: an instance sitting in a pool, or a template being assembled, gets nothing until it is
|
|
718
|
+
parented in. Whether some other component's link happens to be watching that instance makes no
|
|
719
|
+
difference to the answer. That includes a link that watched it while its instance guard was still
|
|
720
|
+
failing. It also includes an instance under a blocked ancestor, where `getComponent` is the only
|
|
721
|
+
way in and a link watching it is not allowed to build the component there. This stays true as the
|
|
722
|
+
tree goes on moving: a watched component still comes and goes with its tree on an instance a link
|
|
723
|
+
found first. Use `getAllComponents` when you want to *observe* rather than ensure.
|
|
724
|
+
- **`getComponent` returns nothing for a component that is still constructing.** So a constructor
|
|
725
|
+
asking for its own component gets `undefined`. So does an attribute-changed handler that runs
|
|
726
|
+
inside the write of one of the component's `defaults`, where the place delivers signals
|
|
727
|
+
immediately. Forcing the construction with `addComponent` from inside the constructor raises
|
|
728
|
+
`component '...' is cyclic`.
|
|
729
|
+
- **Extinguishing the module destroys every component** and stops watching the tags. `addComponent`
|
|
730
|
+
on the extinguished module raises, and tagging an instance afterwards does nothing. `getComponent`
|
|
731
|
+
builds nothing from the moment the extinguish begins: it answers `undefined` for a still-tagged
|
|
732
|
+
instance.
|
|
733
|
+
- **A `destroy` that raises does not hold up the teardown.** Whatever Flamework attached for the
|
|
734
|
+
component (the attribute-changed connections behind `onAttributeChanged`) is released either way,
|
|
735
|
+
so nothing keeps firing into a component that has gone.
|
|
736
|
+
- On extinguish, the failure is warned about and the remaining components still come down, so one
|
|
737
|
+
component cannot leave a module half-extinguished.
|
|
738
|
+
- A removal Flamework makes on its own (a tree, a link, an attribute or a dependency lost) warns
|
|
739
|
+
the same way, and the change that caused it still finishes. For example, a dependent whose
|
|
740
|
+
`destroy` raises does not keep its dependency attached after the dependency's tag has gone.
|
|
741
|
+
- A `removeComponent` you call by hand still re-raises the error, since you asked for the removal.
|
|
742
|
+
- **Per-frame events come from the module's lifecycle plugin.** `disableDefaultLifecycle()` on the
|
|
743
|
+
module that includes `ComponentPlugin` stops components ticking. `onStart` still runs.
|
|
744
|
+
- **An invalid attribute throws** unless a default is configured.
|
|
745
|
+
- **`onComponentRemoved` runs when the engine delivers it.** The announcement is a BindableEvent, so
|
|
746
|
+
when the callback runs depends on how the place delivers signals:
|
|
747
|
+
- **Immediately.** The callback runs inside the removal, before `destroy`, so the component is
|
|
748
|
+
still usable inside it. But it has already left `getComponent` and `getComponents` by then, and
|
|
749
|
+
nothing builds a replacement while the removal is running. So the value the callback is handed
|
|
750
|
+
is the only way to reach it. An `addComponent` of your own from the callback still attaches a
|
|
751
|
+
new one, and the dependents holding the old one are rebuilt around it once the removal is over.
|
|
752
|
+
- **Deferred.** The callback runs once the thread yields. The removal has finished and `destroy`
|
|
753
|
+
has already run, and asking a still-tagged instance for the component there builds a new one.
|
|
754
|
+
|
|
755
|
+
A removal by hand leaves the tag alone. So asking a still-tagged instance for the component
|
|
756
|
+
*after* `removeComponent` has returned builds a new one, as it always has. That is also what keeps
|
|
757
|
+
two components whose links name each other from removing one another twice: taking one down takes
|
|
758
|
+
the other with it, once each.
|
|
759
|
+
|
|
760
|
+
The announcement is delivered a resumption late, so a link compares it with the instance as it
|
|
761
|
+
stands when it arrives. Sometimes the instance has since replaced the component, because that same
|
|
762
|
+
resumption asked for it again. Then the removal leaves the link alone, and updates
|
|
763
|
+
`childComponents` and `attributeComponents` to the component that is there now. Otherwise a cycle
|
|
764
|
+
would remove and rebuild itself for as long as the place is running.
|
|
765
|
+
- **Attribute tracking is on by default.** `refreshAttributes: false` disables `onAttributeChanged`
|
|
766
|
+
as well as the tracking. It does not turn off the watch on the attributes' validity: an attribute
|
|
767
|
+
changed to a value its guard rejects still takes the component down.
|
|
768
|
+
- **A component with no `tag` can only be added by hand.**
|
|
769
|
+
- **`@Component` classes are not providers.** They are not picked up by `registerProviders`, and
|
|
770
|
+
`registerComponents` will not pick up providers. `Dependency<T>()` and a provider's constructor
|
|
771
|
+
cannot take one either: the transformer refuses both. Get it from `Components` instead.
|
|
772
|
+
- **Component dependencies are same-instance only.** There is no cross-instance dependency. A link
|
|
773
|
+
is how you reach another instance.
|
|
774
|
+
- **A linked component must be registered in the same module**, by any of its component plugins.
|
|
775
|
+
Igniting raises if it is not.
|
|
776
|
+
- **`addComponent` will not wait.** By hand, a link that has not resolved raises instead of
|
|
777
|
+
yielding. Through a tag, the component is not created until the link has resolved.
|
|
778
|
+
- **A link is only kept current for a tag-driven component.** One added by hand is resolved once,
|
|
779
|
+
like its instance guard.
|
|
780
|
+
- **`refreshAttributes: false` freezes link attributes too.** Re-pointing one stops updating
|
|
781
|
+
`this.attributes` and `attributeComponents`, and `onAttributeChanged` does not fire for them
|
|
782
|
+
either. Only the component's *view* is frozen, not the criterion behind it: a re-point that the
|
|
783
|
+
guard refuses still takes the component down, as it would with tracking on. The component's own
|
|
784
|
+
writes still land, in both `this.attributes` and `attributeComponents`, and, as for a plain
|
|
785
|
+
attribute, they announce nothing.
|
|
786
|
+
- **Clearing a required link raises.** Only an optional one can be set back to `undefined`.
|
|
787
|
+
- **A write that fails its guard raises**, so an attribute never holds a value its type forbids.
|
|
788
|
+
- **A link write to an instance without the component warns and is refused**, rather than raising or
|
|
789
|
+
destroying the component that wrote it. Await the component first.
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
Previous: [Lifecycle events](04-lifecycle-events.md) · Next: [Networking](06-networking.md)
|