Forest Logo
search
package_2

itemdisplayrenderer

By @biotoxin495

Roblox

Mirrored from Wally

ItemDisplayRenderer — A reusable UI rendering registry for Roblox

A dependency-free, centralized item and reward visualization system for Roblox UI.

ItemDisplayRenderer lets a game register the visual behavior for a content type once, then reuse that renderer across reward screens, inventories, shops, quests, collectibles, purchase results, and other interfaces.

The module combines two common rendering workflows under one lifecycle:

  • Create() — the renderer creates/owns its display frame.
  • RenderInto() — the caller already owns the target frame and the renderer decorates it.

Both return the same render handle, support the same options, and clean up through the same lifecycle.

Quick example

Register a renderer once, then create its UI anywhere that needs to display that content type.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ItemDisplayRenderer = require(ReplicatedStorage.Packages.ItemDisplayRenderer)

local Renderer = ItemDisplayRenderer.new()

Renderer:Register("Coins", {
    Create = function()
        return CoinTemplate:Clone()
    end,

    Render = function(context)
        context.Frame.Amount.Text = tostring(context.Data.Amount)
    end,
})

local handle = Renderer:Create("Coins", RewardsContainer, {
    Amount = 500,
})

🚀 Features

One renderer, two workflows

Use Create() when the renderer should create and own its display frame. Use RenderInto() when another system already owns the target frame. Both workflows return the same handle and use the same update and cleanup behavior.

Central renderer registry

Register visual behavior once and reuse it across reward screens, inventories, shops, quests, collectibles, and purchase results.

Deterministic lifecycle

Every render has a handle that can be inspected, updated, or destroyed. Cleanup runs when a render is updated, replaced, destroyed, or when its target GuiObject is destroyed.

Lists, variants, and templates

Render ordered lists with duplicate renderer IDs, select presentation variants through display options, and create frames with functions or reusable templates.

Optional integrations

Popup adapters, asynchronous work, and animated ViewportFrame renderers remain opt-in and dependency-free.

Fully typed, no dependencies

ItemDisplayRenderer uses strict Luau and does not require Kernel, Maid, Promise, Signal, or another framework.

📖 Basic usage

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ItemDisplayRenderer = require(
    ReplicatedStorage.Packages.ItemDisplayRenderer
)

local Renderer = ItemDisplayRenderer.new()

Register a renderer

Every renderer must define Render(context).

If it should also be usable through Create(), give it either a Create(context) function, a Template, or a Templates table.

Renderer:Register("Coins", {
    Create = function(context)
        local frame = CoinTemplate:Clone()
        return frame
    end,

    Render = function(context)
        context.Frame.Amount.Text = tostring(context.Data.Amount)

        return function()
            -- Optional cleanup.
        end
    end,
})

The cleanup function returned by Render() runs when the render is updated, replaced, destroyed, or when its target frame is destroyed.

Renderer:Create(rendererId, parent, data, options?)

Use Create() when the renderer should create its own frame.

local handle = Renderer:Create("Coins", RewardsContainer, {
    Amount = 5000,
})

The resulting frame can be obtained from the handle:

local frame = handle:GetFrame()

By default, a frame created through Create() is destroyed when its handle is destroyed.

Renderer:RenderInto(rendererId, frame, data, options?)

Use RenderInto() when the surrounding system already owns the frame.

local handle = Renderer:RenderInto("Coins", ExistingFrame, {
    Amount = 5000,
})

Destroying this handle runs renderer cleanup but keeps ExistingFrame, because the renderer does not own it.

This is useful for Studio-authored inventory slots, shop cards, reward entries, or any UI where the surrounding feature owns the layout.

Render handle API

Every successful render returns a handle.

local handle = Renderer:Create(...)

Check its state

if handle:IsActive() then
    print("Still active")
end

Read its frame

local frame = handle:GetFrame()

Update it

handle:Update({
    Amount = 10000,
})

Update() retains the target GuiObject, cleans up the previous rendering lifecycle, then runs the renderer again with the new data.

This means every renderer automatically supports updates without needing a separate update callback.

Destroy it

handle:Destroy()

This runs:

  1. popup cleanup,
  2. renderer cleanup,
  3. automatic lifecycle disconnection,
  4. frame destruction if the renderer owns the frame.

Render replacement

Only one ItemDisplayRenderer render owns a target frame at a time.

Calling RenderInto() on a frame that already has an active renderer automatically cleans up the old render before attaching the new one.

Renderer:RenderInto("Coins", Slot, coinData)
Renderer:RenderInto("Pet", Slot, petData)

The second call safely replaces the first render while preserving Slot itself.

Set:

{
    ReplaceExisting = false,
}

if replacement should be rejected instead.

Ordered render lists

Lists use ordered request descriptors rather than renderer IDs as table keys.

local handles = Renderer:RenderList(RewardsContainer, {
    {
        Renderer = "Coins",
        Data = { Amount = 500 },
    },
    {
        Renderer = "Coins",
        Data = { Amount = 2500 },
    },
    {
        Renderer = "Pet",
        Data = { Name = "Golden Cat" },
    },
})

Because this is an array, duplicate renderer types are supported and ordering is deterministic.

Display variants

Presentation-specific state belongs in display options rather than domain data.

Renderer:Create("Pet", Container, {
    Name = "Golden Cat",
}, {
    Variant = "Expanded",
})

Inside the renderer:

Render = function(context)
    if context.Variant == "Expanded" then
        -- Expanded presentation.
    else
        -- Default presentation.
    end
end

This replaces patterns such as _showExpanded mixed into reward/item data.

Templates

Instead of implementing Create(), a renderer can use a template:

Renderer:Register("Coins", {
    Template = CoinTemplate,

    Render = function(context)
        context.Frame.Amount.Text = tostring(context.Data.Amount)
    end,
})

Or variant-specific templates:

Renderer:Register("Pet", {
    Templates = {
        Default = CompactPetTemplate,
        Expanded = ExpandedPetTemplate,
    },

    Render = function(context)
        -- Populate whichever template was selected.
    end,
})

The module does not require UI to be built through code. Templates may be authored in Studio or created at runtime.

Popup and tooltip adapters

Popups are optional and deliberately kept outside the core module.

Construct the renderer with an adapter:

local Renderer = ItemDisplayRenderer.new({
    PopupAdapter = MyPopupAdapter,
})

A renderer can then return a popup descriptor:

Popup = function(context)
    return {
        Type = "CurrencyPopup",
        Data = {
            CurrencyType = "Coins",
            Amount = context.Data.Amount,
        },
    }
end

Enable it per render:

Renderer:Create("Coins", Container, data, {
    Popup = true,
})

The adapter contract is:

function MyPopupAdapter:Register(frame, descriptor, context)
    -- Attach hover/click behavior.

    return function()
        -- Remove it again.
    end
end

Async-safe renderers

Some renderers may need asynchronous work, such as avatar thumbnails or remote/config lookups.

Use:

context:IsActive()

before applying delayed results.

task.spawn(function()
    local result = getSomethingAsync()

    if context:IsActive() then
        context.Frame.Icon.Image = result
    end
end)

This prevents an old asynchronous operation from mutating a frame after that render has been replaced or destroyed.

ViewportFrame and animated renderers

A renderer can create connections or run a 3D preview and return the matching cleanup function.

Render = function(context)
    local connection = RunService.RenderStepped:Connect(function(dt)
        -- Animate the preview.
    end)

    return function()
        connection:Disconnect()
        -- Clear the ViewportFrame.
    end
end

Updating or destroying the render disconnects its RenderStepped connection through the returned cleanup function.

Renderer packs with RegisterMany

Renderer packs can be grouped into separate modules and registered together.

Renderer:RegisterMany({
    Coins = CoinRenderer,
    Gems = GemRenderer,
    Pet = PetRenderer,
})

This lets a larger project organize definitions into packages such as:

Renderers/
├── CurrencyRenderers.lua
├── PetRenderers.lua
├── CollectibleRenderers.lua
└── PowerupRenderers.lua

⚙️ API

ItemDisplayRenderer.new(config?)

Creates an independent renderer registry. Configuration may provide a popup adapter, enable strict errors, or change the default variant name.

Registry methods

MethodDescription
Register(rendererId, renderer, options?)Registers one renderer and returns whether registration succeeded
RegisterMany(renderers, options?)Registers a dictionary of renderer definitions
Unregister(rendererId)Removes a renderer registration
GetRenderer(rendererId)Returns the registered definition, if present
HasRenderer(rendererId)Reports whether an ID is registered

Pass { Override = true } to Register or RegisterMany to deliberately replace an existing registration.

Rendering methods

MethodDescription
Create(rendererId, parent, data, options?)Creates a frame, renders it, and returns its handle
RenderInto(rendererId, frame, data, options?)Renders into a caller-owned frame and returns its handle
RenderList(parent, requests, options?)Creates an ordered list and returns its successful handles
EndRendering(frame)Destroys the active handle associated with a frame
GetActiveHandle(frame)Returns the active handle associated with a frame

Manager lifecycle methods

MethodDescription
SetPopupAdapter(adapter)Replaces or clears the popup adapter
Destroy()Destroys active handles, clears registrations, and releases the adapter

Handle methods

MethodDescription
IsActive()Reports whether the handle is still active
GetFrame()Returns the rendered GuiObject
GetRendererId()Returns the registered renderer ID
GetData()Returns the handle's current data
GetOptions()Returns the resolved display options
Update(data, options?)Cleans up the current render and renders new state into the same frame
Destroy()Runs cleanup and destroys the frame when the handle owns it

Complete options reference

You normally only need to provide the values that differ from the defaults.

Constructor configuration

PropertyTypeDescription
PopupAdapterPopupAdapter?Adapter used to attach popup or tooltip behavior
Strictboolean?Raises errors instead of warnings for runtime rendering failures
DefaultVariantstring?Fallback variant name; defaults to "Default"

Display options

PropertyTypeDescription
Variantstring?Presentation variant exposed as context.Variant
Popupboolean?Enables the renderer's popup descriptor for this render
ReplaceExistingboolean?Allows replacement of an active render on the same frame; defaults to true
DestroyFrameOnCleanupboolean?Controls whether a frame created by Create() is destroyed with its handle
Metadata{ [any]: any }?Caller-defined presentation metadata available through context.Options

Renderer definition

MemberTypeDescription
Render(context) -> cleanupFunction?Required function that applies data to the target frame
Create(context) -> GuiObject?Optional factory used by Create()
TemplateGuiObject?Optional default template cloned by Create()
Templates{ [string]: GuiObject }?Optional templates indexed by variant name
Popup(context) -> PopupDescriptor?Optional popup descriptor factory

Display context

MemberDescription
RendererIdID of the active renderer
FrameTarget GuiObject; nil only while a custom Create function is running
ParentRequested or current parent instance
DataDomain data supplied by the caller
OptionsResolved display options
VariantSelected variant or configured default
ManagerItemDisplayRenderer instance managing the render
HandleActive render handle; nil while creating the frame
IsActive()Reports whether delayed work still belongs to an active render

Architecture boundary

ItemDisplayRenderer is a presentation package.

It should not grant rewards or authoritatively mutate player data.

A typical flow is:

Server reward / inventory logic
            │
            ▼
    item/reward descriptor
            │
            ▼
          Client
            │
            ▼
   ItemDisplayRenderer
            │
            ▼
       Roblox UI

Your reward system determines that a player receives 500 Coins. ItemDisplayRenderer determines how those 500 Coins look in a given interface.

Migrating from the old two-manager design

The old MainDisplayManager use case maps to RenderInto():

local handle = Renderer:RenderInto("Pet", existingFrame, data, {
    Popup = true,
})

The old RewardVisualizerOnFrameManager use case maps to Create():

local handle = Renderer:Create("Pet", rewardsContainer, data, {
    Popup = true,
})

Both now share the same registry, options, handle type, popup integration, update behavior, and cleanup lifecycle.

Behavior

Register how something should appear once, then reuse that visual behavior anywhere.

The consuming feature owns its game logic and surrounding UI. ItemDisplayRenderer owns the visualization lifecycle.

📝 Notes

  • A renderer must provide Render(context).
  • Create() additionally requires Create(context), Template, or Templates.
  • Only one active ItemDisplayRenderer render may own a target frame at a time.
  • Use context:IsActive() before applying delayed asynchronous results.
  • ItemDisplayRenderer is a presentation package; authoritative inventory and reward logic belongs elsewhere.
  • Call Renderer:Destroy() when the rendering context is no longer needed.

🛠️ Installation

Create a ModuleScript named ItemDisplayRenderer, copy the contents of init.luau into it, and place it at:

ReplicatedStorage
└── Packages
    └── ItemDisplayRenderer

Require the module from a client script wherever your packages are mapped.

License

This project is released under the MIT License.

See LICENSE for details.

made with ❤️ by biotoxin495

Package Details

Install command (Click to copy)


Version

1.0.0

License

MIT

check_circle

Safe for commercial use

Automated license review — not legal advice.