Your first multipage app

This guide will show you how to create a simple app in which the user can enter a value and trigger a computation, with the result being displayed afterwards.

For this demo Genie app, we'll start with a StatisticAnalysis module with functions to generate a vector of random numbers and calculate its mean:

StatisticAnalysis.jl
module StatisticAnalysis

export gen_numbers, calc_mean

function gen_numbers(N::Int)
    return rand(N)
end

function calc_mean(x::Vector{Float64})
    return round(sum(x) / length(x); digits=4)
end
end

Then, we'll turn the above code into a web app, the app where the user will enter a value for N, and the mean will be displayed as in this preview:

Moreover, we'll add a reactive page where the analysis is triggered when the user types a value in the text field:

Running the app

Start with an empty project folder and create a lib/ folder in it. Then, place the StatisticAnalysis.jl file inside lib/. The contents of this folder will be automatically loaded into the Main module when the app is run.

Next, create a file app.jl, which will be the entry point to the app, with the following content:

module App
using GenieFramework
using Main.StatisticAnalysis
@genietools

end

The GenieFramework package includes everything you'll need to build the web app, and the @genietools macro enables features such as logging and hot code reloading.

This is what the file structure should look like so far:

.
├── app.jl
└── lib/
    └── StatisticAnalysis.jl

To launch the app, start a Julia REPL with julia --project in the project folder and run

using GenieFramework; Genie.loadapp(); up()

This will start a server on port 8000 and you can access the app at http://localhost:8000. To stop the server, execute down() in the REPL.

Now that you've set up the computational code and the app is running, it's time to add the pages to trigger the analysis and display the results.

Welcome page

Let's start with a simple index page at the root / path with a link to the number input form. You can create it with a route, which takes a as an argument an action function that returns the content of the page:

app.jl
module App
using GenieFramework
using Main.StatisticAnalysis

route("/") do
    [ h4("Welcome to the number analysis module"),
      a("Numbers form", href="/form") ]
end

end

The h4 and a functions come from Genie's low-code UI API, in which each function generates the corresponding HTML element. This allows you to implement pages in pure Julia. When you run the app, you should see the page at http://localhost:8000.

Number input form page

The second page will have a form with an input field to introduce the length of the vector of random numbers, with a submit button to send the information to the server. For this page, create a route /form the following content in its action function:

app.jl
route("/form") do
    Html.form(action = "/result", method="POST", [
        input(type="number", name="N", placeholder="Enter vector length")
        input(type="submit", value="Send")
    ])
end

The action parameter in the form element specifies the path to which the form data will be sent, and method specifies the HTTP method. We'll use POST to send the data enclosed in the body of the HTTP request. An alternative would be GET, with the data being appended to the URL as /result?N=[value].

Inside the form, the number type input will show as an input box, whereas the submit type input will appear as the button that submits the form.

Results page

When the form is submitted, a POST request will be sent to the /result path, where the analysis will be performed with the form data. For the results page, define a route implementing the processing as

app.jl
using GenieFramework.Genie.Requests: postpayload
route("/result", method=POST) do
    N = parse(Int, postpayload(:N))
    x = gen_numbers(N)
    m = calc_mean(x)
    p("The mean of $N random numbers is: $m", style="font-size:20px")
end

The postpayload call from the Requests module extracts the value of N from the header of the POST request submitted by the form. Then, the analysis is performed and a response with the result is rendered. The usual Julia string interpolation syntax is used to display the value of N and m.

Reactive form page

In some applications like dashboards, you might want to update parts of the UI when the user interacts with a component. When these updates happen without a page reload, this is known as reactivity. Genie Framework provides an elegant API to implement reactive APPs with two-way instant synchronization between the backend and the page. To introduce it, we'll reimplement the number analysis form.

Create a new module ReactiveForm.jl with the following content:

ReactiveForm.jl
module ReactiveForm
using GenieFramework
using .Main.App.StatisticAnalysis
@genietools

@app begin
    @in N = 0
    @out m = 0.0
    @onchange N begin
        m = calc_mean(gen_numbers(N))
    end
end

function ui()
    cell([
        textfield("How many numbers?", :N),
        p("The average of {{N}} random numbers is {{m}}"),
    ])
end

@page("/reactive", ui)
end

Then, include the module in app.jl by appending include("ReactiveForm.jl") to the end of the file as

module App
using GenieFramework
using Main.StatisticAnalysis

# . . .

include("Reactiveform.jl")

In the flow implemented by ReactiveForm.jl, when the user types a number in the text field a reactive variable N is updated with the new value. This update triggers a handler that recalculates the mean and stores it in a reactive variable m, whose value will be automatically sent to the UI. As you can see in the gif below, all of this happens without a page reload.

Now, let's break down the code into its parts:

  1. Imports: besides importing GenieFramework we also import the data analysis code from StatisticAnalysis. This code could also have been defined in the module itself.
  2. Reactive code: the block delimited by the @app macro implements the reactive code, which controls the app's interaction with the user.
  3. Reactive variables: the variables tagged with @in and @out monitor their own value so that when it changes, the changes are automatically propagated to the UI. The @in macro specifies a variable that can be modified from the UI, whereas @out defines a read-only variable only modifiable from the Julia code.
    @in N = 0
    @out m = 0.0
  1. Reactive handler: the @onchange macro defines a handler that is executed when the value of the tagged variable changes.
    @onchange N begin
        m = calc_mean(gen_numbers(N))
    end
  1. UI function: as seen previously, the UI is implemented in a ui function with the low-code API. The textfield is bound to the N variable, which will store the component's state. Moreover, the {{}} syntax is used to display a reactive variable's value in a page.
function ui()
    cell([
        textfield("How many numbers?", :N),
        p("The average of {{N}} random numbers is {{m}}"),
    ])
end
  1. Route: the @page macro defines a route that will render the specified ui function.
@page("/reactive", ui)

And this is it, you've implemented your first Genie app! If you'd like to go further, check out the guides on Adding dynamic pages, Adding reactive pages or Deploying Genie apps.