diff --git a/.vscode/settings.json b/.vscode/settings.json index 2b242420..be160690 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "luau-lsp.sourcemap.rojoProjectFile": "tests.project.json" + "luau-lsp.sourcemap.rojoProjectFile": "tests.project.json", + "luau-lsp.types.definitionFiles": [ + "testez.d.lua" + ] } \ No newline at end of file diff --git a/src/Renderers/createFusionRenderer.spec.lua b/src/Renderers/createFusionRenderer.spec.lua new file mode 100644 index 00000000..7bd3d374 --- /dev/null +++ b/src/Renderers/createFusionRenderer.spec.lua @@ -0,0 +1,68 @@ +return function() + local flipbook = script:FindFirstAncestor("flipbook") + + local Fusion = require(flipbook.Packages.Fusion) + local createFusionRenderer = require(script.Parent.createFusionRenderer) + + local New = Fusion.New + local Value = Fusion.Value + type StateObject = Fusion.StateObject + + type ButtonProps = { + isDisabled: StateObject, + } + local function Button(props) + return New("TextButton")({ + Text = if props.isDisabled:get() then "Disabled" else "Enabled", + }) + end + + it("should render a Fusion component", function() + local renderer = createFusionRenderer({ Fusion = Fusion }) + local args = { + isDisabled = Value(false), + } + + local target = Instance.new("Folder") + local gui = renderer.mount(target, Button, args) + + expect(gui).to.be.ok() + expect(gui.Text).to.equal("Enabled") + end) + + it("should unmount a Fusion component", function() + local renderer = createFusionRenderer({ Fusion = Fusion }) + local args = { + isDisabled = Value(false), + } + + local target = Instance.new("Folder") + local gui = renderer.mount(target, Button, args) + + expect(gui).to.be.ok() + expect(gui:IsDescendantOf(game)).to.equal(true) + + renderer.unmount() + + expect(gui:IsDescendantOf(game)).to.equal(false) + end) + + it("should update the component on arg changes", function() + local renderer = createFusionRenderer({ Fusion = Fusion }) + local args = { + isDisabled = Value(false), + } + + local target = Instance.new("Folder") + local gui = renderer.mount(target, Button, args) + + expect(gui).to.be.ok() + expect(gui:IsDescendantOf(game)).to.equal(true) + + renderer.unmount() + + expect(gui:IsDescendantOf(game)).to.equal(false) + end) + + it("should never re-mount on arg changes", function() end) +end diff --git a/src/Renderers/createRobloxRenderer.lua b/src/Renderers/createRobloxRenderer.lua index 130d9b80..a73cdd47 100644 --- a/src/Renderers/createRobloxRenderer.lua +++ b/src/Renderers/createRobloxRenderer.lua @@ -7,7 +7,15 @@ type Renderer = types.Renderer local function createRobloxRenderer(): Renderer local handle - local function mount(target, element) + local function shouldUpdate() + return true + end + + local function mount(target, element, args) + if typeof(element) == "function" then + element = element(args) + end + if typeof(element) == "Instance" and element:IsA("GuiObject") then element.Parent = target handle = element @@ -21,7 +29,13 @@ local function createRobloxRenderer(): Renderer end end + local function render() + unmount() + mount() + end + return { + shouldUpdate = shouldUpdate, mount = mount, unmount = unmount, } diff --git a/src/Renderers/render.lua b/src/Renderers/render.lua new file mode 100644 index 00000000..283bf094 --- /dev/null +++ b/src/Renderers/render.lua @@ -0,0 +1,37 @@ +local flipbook = script:FindFirstAncestor("flipbook") + +local types = require(flipbook.Renderers.types) + +type Args = types.Args +type Context = types.Context +type Renderer = types.Renderer + +local function render(renderer: Renderer, target: Instance, element: any, args: Args) + local handle: Instance + local context: Context = { + target = target, + element = element, + args = args, + } + local prevContext: Context? = nil + + local function renderOnce() + if not renderer.shouldUpdate or renderer.shouldUpdate(context, prevContext) then + if renderer.unmount then + renderer.unmount(context) + else + handle:Destroy() + end + + handle = renderer.mount(target, element, context) + end + end + + renderOnce() + + return function() + renderOnce() + end +end + +return render diff --git a/src/Renderers/types.lua b/src/Renderers/types.lua index 7fb2f27a..1436f87b 100644 --- a/src/Renderers/types.lua +++ b/src/Renderers/types.lua @@ -1,8 +1,17 @@ +export type Args = { + [string]: any, +} + +export type Context = { + args: Args, + target: Instance, +} + export type Renderer = { - transformArgs: ((args: { [string]: any }) -> { [string]: any })?, - shouldUpdate: (() -> boolean)?, - mount: (target: Instance, element: any) -> GuiObject | Folder, - unmount: (() -> ())?, + transformArgs: ((args: Args, context: Context) -> Args)?, + shouldUpdate: ((context: Context, prevContext: Context?) -> boolean)?, + mount: (target: Instance, element: any, context: Context) -> GuiObject | Folder, + unmount: ((context: Context) -> ())?, } return nil diff --git a/story-example.lua b/story-example.lua new file mode 100644 index 00000000..1e1f43bb --- /dev/null +++ b/story-example.lua @@ -0,0 +1,14 @@ +local React = require("@pkg/React") +local Button = require("./Button") +local FlipbookReact = require("@pkg/flipbook-react") +type StoryObj = FlipbookReact.StoryObj + +local stories: { [string]: StoryObj } = {} + +stories.Primary = React.createElement(Button, {}) + +return { + name = "Button", + component = Button, + stories = stories, +} diff --git a/wally.toml b/wally.toml index 0fbe2165..55068274 100644 --- a/wally.toml +++ b/wally.toml @@ -17,3 +17,4 @@ t = "osyrisrblx/t@3.0.0" # dev dependencies Roact = "roblox/roact@1.4.4" TestEZ = "roblox/testez@0.4.1" +Fusion = "elttob/fusion@0.2.0"