Skip to main content

Storyteller

Storyteller is a library for the discovery and rendering of UI stories and powers our storybook plugin Flipbook.

The API for this package focuses around the following areas:

  1. Validation for the Story format and Storybook format
  2. Discvoery of valid ModuleScripts with .story and .storybook extensions
  3. Loading of Stories and Storybooks into a sandbox with cacheless module requiring
  4. Rendering stories into a container with lifecycle callbacks for updating and unmounting

There also exist React hooks for ease of integration into storybook apps.

Functions

loadStoryModule

StoryModule Loading
Storyteller.loadStoryModule(
storyModuleModuleScript,
storybookLoadedStorybook
) → LoadedStory<T>

Loads the source of a Story module.

A ModuleLoader instance is required for handling the requiring of the module.

This function will throw if the return value of storyModule does not conform to the Story format, or if the source has a syntax error that require would normally fail for.

For legacy compatibility this function also loads Hoarcekat stories. Instead of a usual table-based Story definition, it takes the returned function and wraps it in a Story, making the story field the function body.

isStorybookModule

ValidationStorybook
since 1.0.0
</>
Storyteller.isStorybookModule(instanceInstance) → boolean

Validates a given Instance is a Storybook module.

No validation is performed on the internals of the module. Only name and class are checked.

See Storyteller.loadStorybookModule for validation of the module source.

isStoryModule

ValidationStory
since 0.1.0
</>
Storyteller.isStoryModule(instanceInstance) → boolean

Validates a given Instance is a Story.

No validation is performed on the internals of the module. Only name and class are checked.

See Storyteller.loadStoryModule for validation of the module source.

loadStorybookModule

StorybookModule Loading
Storyteller.loadStorybookModule(
moduleModuleScript,
providedLoaderModuleLoader?
) → LoadedStorybook

Loads the source of a Storybook module.

A ModuleLoader instance is required for handling the requiring of the module.

This function will throw if the return value of storybookModule does not conform to Storybook format, or if the source has a syntax error that require would normally fail for.

findStoryModulesForStorybook

StorybookStoryDiscovery
since 0.1.0
</>
Storyteller.findStoryModulesForStorybook(storybooktypes.LoadedStorybook) → {ModuleScript}

Discovers all Story modules that are managed by the given Storybook.

render

Rendering
Storyteller.render(
containerInstance,
storyLoadedStory<T>
) → RenderLifecycle

Render a Story to an Instance in the DataModel.

After discovering, validating, and loading Story modules, rendering them is the final step to getting stories visually presented to the user.

This function handles the lifecycle of mounting, updating, and unmounting the Story. On each update controls can be passed down to the story for live-reloading from user interaction.

Usage:

local ModuleLoader = require("@pkg/ModuleLoader")
local Storyteller = require("@pkg/Storyteller")


-- At least one `.storybook` module must be present
local storybookModules = Storyteller.findStorybookModules(game)
assert(#storybookModules > 0, "no Storybook modules found")

local storybook
pcall(function()
	storybook = Storyteller.loadStorybookModule(storybookModules[1])
end)

if storybook then
	-- At least one `.story` module must be a descendant of the Instances in
	-- a Storybook's `storyRoots` array
	local storyModules = Storyteller.findStoryModulesForStorybook(storybook)
	assert(#storyModules > 0, "no Story modules found")

	local story
	pcall(function()
		story = Storyteller.loadStoryModule(storyModules[1], storybook)
	end)

	if story then
		-- Finally, render the story to a container of your choosing
		local lifecycle = Storyteller.render(container, story)

		print(container:GetChildren())

		lifecycle.unmount()

		print(container:GetChildren())
	end
end

useStory

ReactStory
Storyteller.useStory(
moduleModuleScript,
storybooktypes.LoadedStorybook
) → (
types.LoadedStory<unknown>?,
string?
)

This hook triggers a rerender when the Story module or any of its required modules change. For example, updating the story property or updating a React component’s source will trigger useStory to rerender with the new content.

info

In the future version hooks may be migrated to a new package to remove the React dependency from Storyteller.

Usage:

local React = require("@pkg/React")
local Storyteller = require("@pkg/Storyteller")

local useEffect = React.useEffect
local useRef = React.useRef
local e = React.createElement

local function StoryView(props: {
	parent: Instance,
	storyModule: ModuleScript,
	storybook: Storybook,
})
	local ref = useRef(nil :: Frame?)

	local story = Storyteller.useStory(props.storyModule, props.storybook)

	useEffect(function()
		if ref.current then
			local renderer = Storyteller.createRendererForStory(story)
			Storyteller.render(renderer, ref.current, story)
		end
	end, { story })

	return e("Frame", {
		Size = UDim2.fromScale(1, 1),
		BackgroundTransparency = 1,
		ref = ref,
	})
end

return StoryView

useStorybooks

ReactStorybook
Storyteller.useStorybooks(parentInstance) → {
available{LoadedStorybook},
unavailable{UnavailableStorybook},
}

Performs all the discovery and loading of Storybook modules that would normally be done via individual API members.

This hook makes it possible to conveniently load (and reload) Storybooks for use in React UI.

info

In the future version hooks may be migrated to a new package to remove the React dependency from Storyteller.

Usage:

local React = require("@pkg/React")
local Storyteller = require("@pkg/Storyteller")

local e = React.createElement

local function StorybookList(props: {
	parent: Instance,
})
	local storybooks = Storyteller.useStorybooks(props.parent)

	local children = {}
	for index, storybook in storybooks do
		children[storybook.name] = e("TextLabel", {
			Text = storybook.name,
			LayoutOrder = index,
		}),
	end

	return e("Frame", {
		Size = UDim2.fromScale(1, 1),
		BackgroundTransparency = 1,
	}, {
		Layout = e("UIListLayout", {
			SortOrder = Enum.SortOrder.LayoutOrder
		}),
	}, children)
end

return StorybookList

This hook triggers a rerender when a Storybook module changes. For example, updating the storyRoots of a Storybook will trigger a rerender, and when paired with useStory you can get live updates to which Stories a Storybook manages.

findStorybookModules

StorybookDiscovery
Storyteller.findStorybookModules(parentInstance) → {ModuleScript}

Discovers all Storybook modules that are descendants of parent.

This is the first step in the discovery of Stories. Once you load a Storybook, you can then use its storyRoots array to discover all the Stories it manages.

Show raw api
{
    "functions": [
        {
            "name": "loadStoryModule",
            "desc": "Loads the source of a Story module.\n\nA [ModuleLoader](https://github.com/flipbook-labs/module-loader) instance\nis required for handling the requiring of the module.\n\nThis function will throw if the return value of `storyModule` does not\nconform to the [Story format](/docs/story-format#story), or if the source\nhas a syntax error that `require` would normally fail for.\n\nFor legacy compatibility this function also loads [Hoarcekat](https://github.com/Kampfkarren/hoarcekat)\nstories. Instead of a usual table-based Story definition, it takes the\nreturned function and  wraps it in a Story, making the `story` field the\nfunction body.",
            "params": [
                {
                    "name": "storyModule",
                    "desc": "",
                    "lua_type": "ModuleScript"
                },
                {
                    "name": "storybook",
                    "desc": "",
                    "lua_type": "LoadedStorybook"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "LoadedStory<T>\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "Story",
                "Module Loading"
            ],
            "source": {
                "line": 30,
                "path": "/home/runner/work/storyteller/storyteller/src/loadStoryModule.luau"
            }
        },
        {
            "name": "isStorybookModule",
            "desc": "Validates a given [Instance] is a Storybook module.\n\nNo validation is performed on the internals of the module. Only name and class are checked.\n\nSee [Storyteller.loadStorybookModule] for validation of the module source.",
            "params": [
                {
                    "name": "instance",
                    "desc": "",
                    "lua_type": "Instance"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "boolean\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "Validation",
                "Storybook"
            ],
            "since": "1.0.0",
            "source": {
                "line": 16,
                "path": "/home/runner/work/storyteller/storyteller/src/isStorybookModule.luau"
            }
        },
        {
            "name": "isStoryModule",
            "desc": "Validates a given [Instance] is a Story.\n\nNo validation is performed on the internals of the module. Only name and class are checked.\n\nSee [Storyteller.loadStoryModule] for validation of the module source.",
            "params": [
                {
                    "name": "instance",
                    "desc": "",
                    "lua_type": "Instance"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "boolean\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "Validation",
                "Story"
            ],
            "since": "0.1.0",
            "source": {
                "line": 16,
                "path": "/home/runner/work/storyteller/storyteller/src/isStoryModule.luau"
            }
        },
        {
            "name": "loadStorybookModule",
            "desc": "Loads the source of a Storybook module.\n\nA [ModuleLoader](https://github.com/flipbook-labs/module-loader) instance\nis required for handling the requiring of the module.\n\nThis function will throw if the return value of `storybookModule` does not\nconform to [Storybook format](/docs/story-format#storybook), or if the\nsource has a syntax error that `require` would normally fail for.",
            "params": [
                {
                    "name": "module",
                    "desc": "",
                    "lua_type": "ModuleScript"
                },
                {
                    "name": "providedLoader",
                    "desc": "",
                    "lua_type": "ModuleLoader?"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "LoadedStorybook\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "Storybook",
                "Module Loading"
            ],
            "source": {
                "line": 24,
                "path": "/home/runner/work/storyteller/storyteller/src/loadStorybookModule.luau"
            }
        },
        {
            "name": "findStoryModulesForStorybook",
            "desc": "Discovers all Story modules that are managed by the given Storybook.",
            "params": [
                {
                    "name": "storybook",
                    "desc": "",
                    "lua_type": "types.LoadedStorybook"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ ModuleScript }\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "Storybook",
                "Story",
                "Discovery"
            ],
            "since": "0.1.0",
            "source": {
                "line": 16,
                "path": "/home/runner/work/storyteller/storyteller/src/findStoryModulesForStorybook.luau"
            }
        },
        {
            "name": "render",
            "desc": "Render a Story to an [Instance] in the [DataModel].\n\nAfter discovering, validating, and loading Story modules, rendering them is\nthe final step to getting stories visually presented to the user.\n\nThis function handles the lifecycle of mounting, updating, and unmounting\nthe Story. On each update controls can be passed down to the story for\nlive-reloading from user interaction.\n\n**Usage:**\n\n```lua\nlocal ModuleLoader = require(\"@pkg/ModuleLoader\")\nlocal Storyteller = require(\"@pkg/Storyteller\")\n\n\n-- At least one `.storybook` module must be present\nlocal storybookModules = Storyteller.findStorybookModules(game)\nassert(#storybookModules > 0, \"no Storybook modules found\")\n\nlocal storybook\npcall(function()\n\tstorybook = Storyteller.loadStorybookModule(storybookModules[1])\nend)\n\nif storybook then\n\t-- At least one `.story` module must be a descendant of the Instances in\n\t-- a Storybook's `storyRoots` array\n\tlocal storyModules = Storyteller.findStoryModulesForStorybook(storybook)\n\tassert(#storyModules > 0, \"no Story modules found\")\n\n\tlocal story\n\tpcall(function()\n\t\tstory = Storyteller.loadStoryModule(storyModules[1], storybook)\n\tend)\n\n\tif story then\n\t\t-- Finally, render the story to a container of your choosing\n\t\tlocal lifecycle = Storyteller.render(container, story)\n\n\t\tprint(container:GetChildren())\n\n\t\tlifecycle.unmount()\n\n\t\tprint(container:GetChildren())\n\tend\nend\n```",
            "params": [
                {
                    "name": "container",
                    "desc": "",
                    "lua_type": "Instance"
                },
                {
                    "name": "story",
                    "desc": "",
                    "lua_type": "LoadedStory<T>"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "RenderLifecycle\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "Rendering"
            ],
            "source": {
                "line": 77,
                "path": "/home/runner/work/storyteller/storyteller/src/render.luau"
            }
        },
        {
            "name": "useStory",
            "desc": "This hook triggers a rerender when the Story module or any of its required\nmodules change. For example, updating the `story` property or updating a\nReact component’s source will trigger useStory to rerender with the new\ncontent.\n\n:::info\nIn the future version hooks may be migrated to a new package to remove the React dependency from Storyteller.\n:::\n\nUsage:\n\n```lua\nlocal React = require(\"@pkg/React\")\nlocal Storyteller = require(\"@pkg/Storyteller\")\n\nlocal useEffect = React.useEffect\nlocal useRef = React.useRef\nlocal e = React.createElement\n\nlocal function StoryView(props: {\n\tparent: Instance,\n\tstoryModule: ModuleScript,\n\tstorybook: Storybook,\n})\n\tlocal ref = useRef(nil :: Frame?)\n\n\tlocal story = Storyteller.useStory(props.storyModule, props.storybook)\n\n\tuseEffect(function()\n\t\tif ref.current then\n\t\t\tlocal renderer = Storyteller.createRendererForStory(story)\n\t\t\tStoryteller.render(renderer, ref.current, story)\n\t\tend\n\tend, { story })\n\n\treturn e(\"Frame\", {\n\t\tSize = UDim2.fromScale(1, 1),\n\t\tBackgroundTransparency = 1,\n\t\tref = ref,\n\t})\nend\n\nreturn StoryView\n```",
            "params": [
                {
                    "name": "module",
                    "desc": "",
                    "lua_type": "ModuleScript"
                },
                {
                    "name": "storybook",
                    "desc": "",
                    "lua_type": "types.LoadedStorybook"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "types.LoadedStory<unknown>?"
                },
                {
                    "desc": "",
                    "lua_type": "string?"
                }
            ],
            "function_type": "static",
            "tags": [
                "React",
                "Story"
            ],
            "source": {
                "line": 56,
                "path": "/home/runner/work/storyteller/storyteller/src/hooks/useStory.luau"
            }
        },
        {
            "name": "useStorybooks",
            "desc": "Performs all the discovery and loading of Storybook modules that would\nnormally be done via individual API members.\n\nThis hook makes it possible to conveniently load (and reload) Storybooks for\nuse in React UI.\n\n:::info\nIn the future version hooks may be migrated to a new package to remove the React dependency from Storyteller.\n:::\n\nUsage:\n\n```lua\nlocal React = require(\"@pkg/React\")\nlocal Storyteller = require(\"@pkg/Storyteller\")\n\nlocal e = React.createElement\n\nlocal function StorybookList(props: {\n\tparent: Instance,\n})\n\tlocal storybooks = Storyteller.useStorybooks(props.parent)\n\n\tlocal children = {}\n\tfor index, storybook in storybooks do\n\t\tchildren[storybook.name] = e(\"TextLabel\", {\n\t\t\tText = storybook.name,\n\t\t\tLayoutOrder = index,\n\t\t}),\n\tend\n\n\treturn e(\"Frame\", {\n\t\tSize = UDim2.fromScale(1, 1),\n\t\tBackgroundTransparency = 1,\n\t}, {\n\t\tLayout = e(\"UIListLayout\", {\n\t\t\tSortOrder = Enum.SortOrder.LayoutOrder\n\t\t}),\n\t}, children)\nend\n\nreturn StorybookList\n```\n\nThis hook triggers a rerender when a Storybook module changes. For example,\nupdating the `storyRoots` of a Storybook will trigger a rerender, and when\npaired with `useStory` you can get live updates to which Stories a Storybook\nmanages.",
            "params": [
                {
                    "name": "parent",
                    "desc": "",
                    "lua_type": "Instance"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{\n\tavailable: { LoadedStorybook },\n\tunavailable: { UnavailableStorybook },\n}\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "React",
                "Storybook"
            ],
            "source": {
                "line": 73,
                "path": "/home/runner/work/storyteller/storyteller/src/hooks/useStorybooks.luau"
            }
        },
        {
            "name": "findStorybookModules",
            "desc": "Discovers all Storybook modules that are descendants of `parent`.\n\nThis is the first step in the discovery of Stories. Once you load a\nStorybook, you can then use its `storyRoots` array to discover all the\nStories it manages.",
            "params": [
                {
                    "name": "parent",
                    "desc": "",
                    "lua_type": "Instance"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "{ ModuleScript }\n"
                }
            ],
            "function_type": "static",
            "tags": [
                "Storybook",
                "Discovery"
            ],
            "source": {
                "line": 15,
                "path": "/home/runner/work/storyteller/storyteller/src/findStorybookModules.luau"
            }
        }
    ],
    "properties": [],
    "types": [],
    "name": "Storyteller",
    "desc": "Storyteller is a library for the discovery and rendering of UI stories and\npowers our storybook plugin [Flipbook](https://github.com/flipbook-labs/flipbook).\n\nThe API for this package focuses around the following areas:\n1. Validation for the Story format and Storybook format\n2. Discvoery of valid ModuleScripts with `.story` and `.storybook` extensions\n3. Loading of Stories and Storybooks into a sandbox with cacheless module requiring\n4. Rendering stories into a container with lifecycle callbacks for updating and unmounting\n\nThere also exist React hooks for ease of integration into storybook apps.",
    "source": {
        "line": 17,
        "path": "/home/runner/work/storyteller/storyteller/src/init.luau"
    }
}