vide/docs/tut/crash-course/11-control-flow.md
2024-07-03 15:02:52 +01:00

5.3 KiB

Control Flow

Eventually you may need a way to dynamically create and destroy UI elements resulting from source updates. Vide provides functions to help you do this, known as control flow functions.

These functions return new sources, which hold the instances to be displayed.

Control flow functions run their components in a new stable scope, which can be destroyed independently of the stable scope that called the control flow function. This means parts of your app can be independently created and destroyed.

switch()

switch() condtionally displays one instance at a time. It uses a table to map a source value to a component.

local source = vide.source
local switch = vide.switch

local function Button(props: {
    Text: string,
    Activated: () -> ()
})
    local hovered = source(false)

    return create "TextButton" {
        Text = props.Text,
        Activated = props.Activated,

        TextColor3 = function()
            return hovered() and Color3.new(1, 1, 1) or Color3.new(.7, .7, .7)
        end,

        MouseEnter = function() hovered(true) end,
        MouseLeave = function() hovered(false) end
    }
end

local function JoinMenu()
    local joined = source(false)

    local function JoinButton()
        return Button {
            Text = "Join",
            Activated = function() joined(true) end
        }
    end

    local function LeaveButton()
        return Button {
            Text = "Leave"
            Activated = function() joined(false) end
        }
    end

    return create "Frame" {
        switch(joined) {
            [true] = LeaveButton,
            [false] = JoinButton
        }
    }
end

The reactive graph for the above example:

%%{init: {
    "theme": "base",
    "themeVariables": {
        "primaryColor": "#1B1B1F",
        "primaryTextColor": "#fff",
        "primaryBorderColor": "#1B1B1F",
        "lineColor": "#79B8FF",
        "tertiaryColor": "#161618",
        "tertiaryBorderColor": "#1C1C1F"
    }
}}%%

graph

subgraph root["root scope"]
    direction LR
    joined --> switch -.- subroot

    subgraph subroot["switch scope"]
        direction LR
        effect["TextColor3 effect"]
    end
end

A switch() call creates a new effect and a new stable scope as seen in the above graph. Whenever menu updates, it causes the switch effect to run, which will destroy and recreate the switch scope with the new component.

This will also destroy the internal effect that the button uses to highlight itself when it is hovered, each time the switch is rerun.

indexes()

Often, you will have a table of values with each value displayed in a similar manner. Rather than manually looping over each value to generate a corresponding UI element, indexes() allows you to create elements each corresponding to a table index, to display the value at that index.

local todoList = source {
    "finish the crash course",
    "star vide's GitHub"
}

local function TodoList(props: { list: () -> Array<string> })
    return create "Frame" {
        create "UIListLayout" {},

        indexes(todoList, function(todo, i)
            return create "TextLabel" {
                Text = function()
                    return i .. ": " .. todo()
                end,

                LayoutOrder = i
            }
        end)
    }
end

TodoList { list = todoList }

For each index in the given source table, the given function will be called with:

  1. a source containing the value of the index
  2. the index itself

When the value at an index is changed, the function is not reran. Instead, the given source for that index is updated.

Any time the input source table is updated, the given function will be ran for any newly added indexes, while any removed indexes (indexes now with a nil value), will have its corresponding reactive scope destroyed to clean up that element.

indexes() is said to map each table index to a new UI element that can update to display the current value at that index. Each table index is given a single corresponding UI element.

The reactive graph for the above example:

%%{init: {
    "theme": "base",
    "themeVariables": {
        "primaryColor": "#1B1B1F",
        "primaryTextColor": "#fff",
        "primaryBorderColor": "#1B1B1F",
        "lineColor": "#79B8FF",
        "tertiaryColor": "#161618",
        "tertiaryBorderColor": "#1C1C1F"
    }
}}%%

graph

subgraph root ["root scope"]
    direction LR
    todoList --> indexes -.- subroot1 & subroot2

    subgraph subroot1 ["indexes scope 1"]
        direction LR
        value1[todo] --> prop1["prop binding"]
    end

    subgraph subroot2 ["indexes scope 2"]
        direction LR
        value2[todo] --> prop2[prop binding]
    end
end

One thing to note regarding table sources, is that when you edit a table in a source, you must set that table again to actually update the source.

local src = source { 1, 2 }
local data = src()
table.insert(data, 3) -- no effects will run
src(data) -- effects will run

Together, these control flow functions cover the majority of cases where you need to dynamically create and destroy parts of your UI.

If you need to do something that these control flow functions cannot, you can always use mount() within an effect to dynamically create and destroy components on your own terms. Just remember to use cleanup() to unmount when the effect reruns.