R
roblox-ts•2mo ago
kv_

Create components at runtime

I have a function external to flamework which components/services/controllers can call to create components at runtime without having to set the instance up with attributes or having to call some setup method so that components can always be expected to work solely on attributes. This makes it a lot easier to create components that depend on each other or need to construct each other without implementing special handling for components created outside of those cases.
// runtime_create.ts
export const createComponent = (component: BaseComponent, attributes?: { [key: string]: AttributeValue }, instance?: Instance) => {
const components = Dependency<Components>();
if (!instance) {
instance = new Instance('Part', Workspace);
}

if (attributes) {
for (const [idx, v] of pairs(attributes)) {
instance.SetAttribute(idx as string, v); // attributes keys cannot be numeric
}
}

return components.addComponent<typeof component>(instance);
};
// runtime_create.ts
export const createComponent = (component: BaseComponent, attributes?: { [key: string]: AttributeValue }, instance?: Instance) => {
const components = Dependency<Components>();
if (!instance) {
instance = new Instance('Part', Workspace);
}

if (attributes) {
for (const [idx, v] of pairs(attributes)) {
instance.SetAttribute(idx as string, v); // attributes keys cannot be numeric
}
}

return components.addComponent<typeof component>(instance);
};
I am sure this is not a proper way of doing this but seems to be the only one I can find. Currently it is producing an error that I am not sure how to fix:
ReplicatedStorage.rbxts_include.node_modules.@flamework.components.out.components:565: Could not find component from specifier: $c:baseComponent@BaseComponent
ReplicatedStorage.rbxts_include.node_modules.@flamework.components.out.components:565: Could not find component from specifier: $c:baseComponent@BaseComponent
I also have a gripe with it; creating the components dependency every time the function is called sounds like a bad idea, but I can't create it outside of the function since the script will call it before flamework is done loading:
[Flamework] Attempting to load dependency '$c:components@Components' during preloading.
This is prone to race conditions and is not guaranteed to succeed.
[Flamework] Attempting to load dependency '$c:components@Components' during preloading.
This is prone to race conditions and is not guaranteed to succeed.
Any help is appreciated 🙂
Solution:
Solution was to pass the component specifier to addComponent instead of the class: ```ts /** @metadata macro */ export function makeComponent<T>(attributes?: { [key: string]: AttributeValue }, instance?: Instance, id?: Modding.Generic<T, 'id'>) { const components = Dependency<Components>();...
Jump to solution
2 Replies
kv_
kv_OP•2mo ago
Here is an example of how it is intended to be used:
// printer.ts
// example testing class
@Component()
export class printer extends BaseComponent<{ doIPrint: boolean }> implements OnStart {
constructor() {
super();
}

onStart() {
if (this.attributes.doIPrint) {
print('HELLO!');
}
}
}
// printer.ts
// example testing class
@Component()
export class printer extends BaseComponent<{ doIPrint: boolean }> implements OnStart {
constructor() {
super();
}

onStart() {
if (this.attributes.doIPrint) {
print('HELLO!');
}
}
}
// test.ts
// example testing service
@Service()
export class test {
constructor(private printer: printer) {
createComponent(this.printer, { doIPrint: true };
}
}
// test.ts
// example testing service
@Service()
export class test {
constructor(private printer: printer) {
createComponent(this.printer, { doIPrint: true };
}
}
This should just print "HELLO" to the console. Ok, I think I have figured some of it out. I am passing the type of BaseComponent to addComponent which compiles to the specifier for the base component, which does not exist. I should be passing the specifier of component but I am not sure how to get it. I created a macro and take the component as a type parameter now however the macro appears to not be inserted into the caller
// macro
/** @metadata macro */
export function makeComponent<T>(attributes?: { [key: string]: AttributeValue }, instance?: Instance) {
const components = Dependency<Components>();

if (!instance) {
instance = new Instance('Part', Workspace);
}

for (const [idx, v] of pairs(attributes || {})) {
instance.SetAttribute(idx as string, v);
}

return components.addComponent<T>(instance);
}
// macro
/** @metadata macro */
export function makeComponent<T>(attributes?: { [key: string]: AttributeValue }, instance?: Instance) {
const components = Dependency<Components>();

if (!instance) {
instance = new Instance('Part', Workspace);
}

for (const [idx, v] of pairs(attributes || {})) {
instance.SetAttribute(idx as string, v);
}

return components.addComponent<T>(instance);
}
// caller
@Service()
export class RTCS_test implements OnStart {
onStart() {
makeComponent<printer>({ doIPrint: true });
}
}
// caller
@Service()
export class RTCS_test implements OnStart {
onStart() {
makeComponent<printer>({ doIPrint: true });
}
}
-- compiled
function RTCS_test:onStart()
makeComponent({ -- macro is not expanded
doIPrint = true,
})
end
-- compiled
function RTCS_test:onStart()
makeComponent({ -- macro is not expanded
doIPrint = true,
})
end
I expected something like this:
-- compiled
function RTCS_test:onStart()
local components = Flamework.resolveDependency("$c:components@Components")
if not instance then
instance = Instance.new("Part", Workspace)
end
for idx, v in pairs(attributes or {}) do
instance:SetAttribute(idx, v)
end
components:addComponent(instance, "proper-component-specifier-here"
end
-- compiled
function RTCS_test:onStart()
local components = Flamework.resolveDependency("$c:components@Components")
if not instance then
instance = Instance.new("Part", Workspace)
end
for idx, v in pairs(attributes or {}) do
instance:SetAttribute(idx, v)
end
components:addComponent(instance, "proper-component-specifier-here"
end
Solution
kv_
kv_•2mo ago
Solution was to pass the component specifier to addComponent instead of the class:
/** @metadata macro */
export function makeComponent<T>(attributes?: { [key: string]: AttributeValue }, instance?: Instance, id?: Modding.Generic<T, 'id'>) {
const components = Dependency<Components>();

if (!instance) {
instance = new Instance('Part', Workspace);
}

for (const [idx, v] of pairs(attributes || {})) {
instance.SetAttribute(idx as string, v);
}

return components.addComponent(instance, id);
}
/** @metadata macro */
export function makeComponent<T>(attributes?: { [key: string]: AttributeValue }, instance?: Instance, id?: Modding.Generic<T, 'id'>) {
const components = Dependency<Components>();

if (!instance) {
instance = new Instance('Part', Workspace);
}

for (const [idx, v] of pairs(attributes || {})) {
instance.SetAttribute(idx as string, v);
}

return components.addComponent(instance, id);
}

Did you find this page helpful?