{"id":17649035,"url":"https://github.com/daymxn/story","last_synced_at":"2026-05-02T13:32:02.125Z","repository":{"id":233609287,"uuid":"787546973","full_name":"daymxn/story","owner":"daymxn","description":"Simple UI binding library for Roblox.","archived":false,"fork":false,"pushed_at":"2024-04-16T22:18:12.000Z","size":28,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-03-10T10:31:17.842Z","etag":null,"topics":["binding","lua","luau","roblox","state-management","story","ui"],"latest_commit_sha":null,"homepage":"","language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/daymxn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-16T18:23:38.000Z","updated_at":"2024-04-16T18:45:04.000Z","dependencies_parsed_at":"2024-05-01T17:47:48.154Z","dependency_job_id":null,"html_url":"https://github.com/daymxn/story","commit_stats":null,"previous_names":["daymxn/story"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/daymxn/story","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daymxn%2Fstory","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daymxn%2Fstory/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daymxn%2Fstory/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daymxn%2Fstory/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/daymxn","download_url":"https://codeload.github.com/daymxn/story/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/daymxn%2Fstory/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32536576,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-02T12:25:33.646Z","status":"ssl_error","status_checked_at":"2026-05-02T12:24:51.733Z","response_time":132,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["binding","lua","luau","roblox","state-management","story","ui"],"created_at":"2024-10-23T11:23:41.163Z","updated_at":"2026-05-02T13:32:02.068Z","avatar_url":"https://github.com/daymxn.png","language":"Lua","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003e\n\t\u003cbr\u003e\n\t\u003cbr\u003e\n\u003cimg width=\"320\" src=\"./logo.svg\"  alt=\"story logo\"/\u003e\n\t\u003cbr\u003e\n\t\u003cbr\u003e\n\t\u003cbr\u003e\n\u003c/h1\u003e\n\n\u003e Simple UI binding library for Roblox.\n\n![GitHub release (latest by date)](https://img.shields.io/github/v/release/daymxn/story?style=flat-square)\n![GitHub last commit (branch)](https://img.shields.io/github/last-commit/daymxn/story/master?style=flat-square)\n![GitHub issues](https://img.shields.io/github/issues/daymxn/story?style=flat-square)\n![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/daymxn/story?style=flat-square)\n![GitHub](https://img.shields.io/github/license/daymxn/story?style=flat-square)\n\n---\n\n\u003cbr\u003e\n\n## Installation\n\nYou can install Story automatically with [wally](https://wally.run/package/daymxn/story):\n\n```toml\nstory = \"daymxn/story@1.0.0\"\n```\n\nAlternatively, you can manually install Story by downloading [the latest release](https://github.com/daymxn/story/releases)\nand manually inserting it in your project.\n\n## Overview\n\nStory came about as a simple solution for adding logic to an existing UI, while ensuring\nproper clean-up procedures were made when instances were destroyed. Especially when it\ncame to deeply nested UI structures, and external (non rbx) listeners created on individual\nUI elements.\n\nStory allows you to easily (and explicitly) define the listeners that should be cleaned up,\nas well as other nested UI elements. It also allows you to redraw instances under certain\nconditions (such as state updates).\n\n## Alternatives\n\n`What makes this better than React/Roact?`\n\nReact is my go-to for new projects, and I highly reccomend it for new projects!\nBut react falls short when it comes to binding to an already created UI- it's more-so\napplicable to creating the UI from code entirely, instead of binding to it externally.\n\n`What about hydration in Fusion?`\n\nFusion is a great alternative, especially if you're already familiar with it!\nBut Fusion comes with a lot of behind the scenes magic to make its hydration work,\nwhich is a big part of why they're still not officially released. Story is very\nexplicit and straightforward with its approach, which makes it easy to not only\ndiagnose edge-case issues- but also makes it very extensible.\n\n## Features\n\n- Bind UI `Instance`(s) to their respective logic\n- Automatically disconnect listners when an `Instance` is destroyed\n- Nest UI elements within one another- creating a dependency tree for lifecycle events\n- Redraw UI elements on state updates\n- Avoid memory leaks when an `Instance` is already destroyed before logic binding\n\n## Usage\n\nInstead of just talking about it, let's show you how Story works in practice.\n\n### Basic Usage\n\nThe expected workflow for Story is to perform your bindings from a top-down approach:\n\n```lua\nreturn function Main(main: MainUI)\n    return Story.wrap(main, function(story)\n        story:AddStory(Pages(main.Pages))\n        story:AddStory(Sidebar(main.Sidebar))\n    end)\nend\n```\n\nAnd then binding your `Main` story to your character:\n\n```lua\nPlayers.LocalPlayer.CharacterAdded:Connect(function(_)\n    local ui = MainUI:Clone()\n\n    ui.Parent = game.Players.LocalPlayer.PlayerGui\n\n    Main(ui)\nend)\n```\n\nFrom this, Story will automatically perform the cleanup steps necessary whenever\nthe player respawns and has their UI destroyed.\n\nYou may have noticed that you get a `story` variable when wrapping an instance.\nThis is utilized to add nested Story elements, or attach listeners to specific stories.\n\nFor example, lets say we have a vehicle spawning panel. We could define a common button\nstory for individual vehicle elements, and use `:AddListener` to bind the story with the\n`MouseButton1Click` event:\n\n```lua\nfunction Vehicle(button: ImageButton)\n    return Story.wrap(button, function(story)\n        story:AddListener(button.MouseButton1Click:Connect(function()\n            SpawnVehicle:FireServer(button.Name)\n        end))\n    end)\nend\n```\n\nWith that, we can iterate over all the vehicle buttons and attach this story:\n\n```lua\nfunction VehiclesPage(page: MainUI.Pages.Vehicles)\n    return Story.wrap(page, function(story)\n        for _, vehicle in page.vehicles:GetChildren() do\n            -- Skip layout elements\n            if not vehicle:IsA(\"ImageButton\") then continue end\n            \n            story:AddStory(Vehicle(vehicle))\n        end\n    end)\nend\n```\n\nWe've attached the individual `Vehicle` story elements to the\n`VehiclesPage`'s story with `:AddStory`, so now whenever `VehiclesPage`\nis destroyed- the `Vehicle` buttons will be as well.\n\nAlthough, the story heiarchy is not only useful for cleanup. You can\nalso force redraws from a top down approach.\n\nFor example, what if our vehicles should have an unlocked symbol depending on\nif they're actually unlocked?\n\n```lua\nfunction Vehicle(button: ImageButton)\n    return Story.wrap(button, function(story)\n        local name = button.Name\n        local unlocked = table.find(State.UnlockedVehicles, name) ~= nil\n\n        button.Unlocked.Visible = unlocked\n\n        if unlocked then\n            story:AddListener(button.MouseButton1Click:Connect(function()\n                SpawnVehicle:FireServer(name)\n            end))\n        end\n    end)\nend\n```\n\nThe problem here is that if the vehicle becomes unlocked, since the UI was already\ndrawn- the `Unlocked` symbol won't be updated, and the `SpawnVehicle` won't be able\nto be called.\n\nTo solve this, Story provides the `:Redraw` method:\n\n```lua\nfunction VehiclesPage(page: MainUI.Pages.Vehicles)\n    return Story.wrap(page, function(story)\n        for _, vehicle in page.vehicles:GetChildren() do\n            -- Skip layout elements\n            if not vehicle:IsA(\"ImageButton\") then continue end\n            \n            story:AddStory(Vehicle(vehicle))\n        end\n\n        -- Add a listener for whenever `State.UnlockedVehicles` is updated\n        story:AddListener(onVehiclesUpdated:connect(function()\n            story:Redraw()\n        end))\n    end)\nend\n```\n\nThis will force another \"draw\" for not only the story itself, but all child stories\nadded via `:AddStory`.\n\nA \"draw\" is defined by your call to `wrap`. Specifically, the callback function you provide\nis used as the \"draw\" method. When a story wants to redraw, it will \"destroy\" itself and nested stories-\neffectively wiping the slate clean of listeners and such. Then, it will call the defined \"draw\"\nmethod to re-define all the listeners and nested stories. From here, the individual `Vehicle` stories\nwill have the most up-to-date State.\n\n### Advanced Usage\n\nWhile the standard work-flow will cover 9/10 use cases, there are other scenarios where other\nbehaviors may be desired. Especially when defining an intermediate API.\n\n#### Manually creating stories\n\nYou can also create `Story` instances directly with `new`, and manually bind to the instance\nwith `:BindToInstance`:\n\n\u003e [!WARNING]\n\u003e Instances created with `new` do not have a bound \"draw\" method, and so can effectively not be\n\u003e redrawn by calling `:Redraw`.\n\n```lua\nlocal vehiclesPage = Story.new()\nvehiclesPage:BindToInstance(pages.Vehicles)\n```\n\n#### Binding to multiple instances\n\n`:BindToInstance` is not limited to an individual instance. You can bind your stories\nto _multiple_ instances:\n\n```lua\nlocal vehiclesPage = Story.new()\nvehiclesPage:BindToInstance(pages.Vehicles)\nvehiclesPage:BindToInstance(game.Players.LocalPlayer.Character)\nvehiclesPage:BindToInstance(game:FindFirstChild(\"map\"))\n```\n\nAnd whenever _any_ of the bound instances are destroyed, the `Story` instance will destory itself.\n\n\u003e [!NOTE]\n\u003e If an instance is already destroyed whenever you try to initilize it, the `:Destory` method on the story\n\u003e will be called immediately. This avoids any potentional memory leaks from listeners created on destroyed\n\u003e elements.\n\n#### Manually destroying stories\n\nIf, for whatever reason, you want to destory a `Story` instance yourself- you can explicitly call the `:Destroy`\nmethod:\n\n```lua\nvehiclesPage:Destroy()\n```\n\n#### Custom listeners\n\nListeners added by `:AddListener` are not limited to `RBXScriptSignal`- a listener only needs to have\na `:Disconnect` method:\n\n```lua\nfunction CustomListener.new(): CustomListener\n    local self = {}\n    setmetatable(self, CustomListener)\n\n    return self\nend\n\nfunction CustomListener:Disconnect()\n  -- do stuff\nend\n\n\nvehiclesPage:AddListener(CustomListener.new())\n```\n\n#### Method chaining\n\nAll story methods return themselves- which allows for easy method chaining:\n\n```lua\npages:AddStory(vehiclesPage)\n     :AddStory(characterPage)\n     :AddStory(settingsPage)\n```\n\n## Roadmap\n\n- CI testing\n- Unit tests\n- TypeScript integration\n- Webpage for API docs\n- Add names to story elements for debugging facilities\n  - Add logging to edge-case scenarios with the story name as a point of reference\n\n## License\n\n[Apache 2.0](/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdaymxn%2Fstory","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdaymxn%2Fstory","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdaymxn%2Fstory/lists"}