Skip to content

Commit

Permalink
add basic layouting tutorial & better ways to style elements (#200)
Browse files Browse the repository at this point in the history
* add basic layouting tutorial

* fix slider example

* mostly finish tutorial

* switch to styles everywhere

* improve docs

* improve documentation

* clean up docs

* some tweaks
  • Loading branch information
SimonDanisch authored Dec 15, 2023
1 parent badb798 commit 7de60a9
Show file tree
Hide file tree
Showing 29 changed files with 2,091 additions and 260 deletions.
1 change: 1 addition & 0 deletions .JuliaFormatter.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
style = "blue"
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ WidgetsBase = "eead4739-05f7-45a1-878c-cee36b57321c"
CodecZlib = "0.7"
Colors = "0.12"
HTTP = "1.5"
Hyperscript = "0.0.3, 0.0.4"
Hyperscript = "0.0.3, 0.0.4, 0.0.5"
MsgPack = "1.2"
Observables = "0.5.1"
RelocatableFolders = "0.1, 0.2, 1"
Expand Down
20 changes: 14 additions & 6 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ using Deno_jll, JSServe
using Documenter

ci = get(ENV, "CI", "false") == "true"

makedocs(
modules=[JSServe],
sitename="JSServe",
Expand All @@ -11,12 +10,21 @@ makedocs(
authors="Simon Danisch and other contributors",
pages=[
"Home" => "index.md",
"Plotting" => "plotting.md",
"Widgets" => "widgets.md",
"Animation" => "animation.md",
"Components" => [
"Styling" => "styling.md",
"Components" => "components.md",
"Layouting" => "layouting.md",
"Widgets" => "widgets.md",
"Interactions" => "interactions.md",
],
"Examples" => [
"Plotting" => "plotting.md",
"Wrapping JS libraries" => "javascript-libraries.md",
"Assets" => "assets.md",
"Extending" => "extending.md",
],
"Deployment" => "deployment.md",
"Assets" => "assets.md",
"Extending" => "extending.md",
"Static Sites" => "static.md",
"Api" => "api.md",
]
)
Expand Down
1 change: 1 addition & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Public Functions


```@autodocs
Modules = [JSServe]
Order = [:module, :constant, :type, :function, :macro]
Expand Down
2 changes: 1 addition & 1 deletion docs/src/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ DOM.img(src=some_file)
# This will also resolve to a valid URL and load jsmodule as an es6 module
DOM.sript(src=jsmodule, type="module")

# Assets also work with online sources, which is great for online dependencies!
# Assets also work with online sources.
# Usage is exactly the same as when using local files
THREE = ES6Module("https://unpkg.com/three@0.136.0/build/three.js")
# Also offer an easy way to use packages from a CDN (currently esm.sh):
Expand Down
58 changes: 58 additions & 0 deletions docs/src/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Components

Components in JSServe are meant to be re-usable, easily shareable types and functions to create complex JSServe Apps.
We invite everyone to share their Components by turning them into a Julia library.

There are two ways of defining components in JSServe:

1. Write a function which returns `DOM` objects
2. Overload `jsrender` for a type

The first is a very lightweight form of defining reusable components, which should be preferred if possible.

But, for e.g. widgets the second form is unavoidable, since you will want to return a type that the user can register interactions with.
Also, the second form is great for integrating existing types into JSServe like plot objects.
How to do the latter for Plotly is described in [Plotting](@ref).

Let's start with the simple function based components that reuses existing JSServe components:

```@setup 1
using JSServe
JSServe.Page()
```

```@example 1
using Dates
function CurrentMonth(date=now(); style=Styles(), div_attributes...)
current_day = Dates.day(date)
month = Dates.monthname(date)
ndays = Dates.daysinmonth(date)
current_day_style = Styles(style, "background-color" => "gray", "color" => "white")
days = map(1:ndays) do day
if day == current_day
return Card(Centered(day); style=current_day_style)
else
return Card(Centered(day); style=style)
end
end
grid = Grid(days...; columns="repeat(7, 1fr)")
return DOM.div(DOM.h2(month), grid; style=Styles("width" => "400px", "margin" => "5px"))
end
App(()-> CurrentMonth())
```

Now, we could define the same kind of rendering via overloading `jsrender`, if a date is spliced into a DOM:

```@example 1
function JSServe.jsrender(session::Session, date::DateTime)
return JSServe.jsrender(session, CurrentMonth(date))
end
App() do
DOM.div(now())
end
```

Please note, that `jsrender` is not applied recursively on its own, so one needs to apply it manually on the return value.
It's not needed for simple divs and other Hyperscript elements, but e.g. `Styles` requires a pass through `jsrender` to do the deduplication etc.
33 changes: 29 additions & 4 deletions docs/src/deployment.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Deployment

Apps can be deployed in a wide variety of scenarios.

Lets start with a very simple example app and show how to deploy that App:

```@example 1
using JSServe
example_app = App(DOM.div("hello world"), title="hello world")
Expand Down Expand Up @@ -55,7 +59,9 @@ url_to_visit = online_url(server, "/my/nested/page")
```

### nginx
If you need to re-route JSServe (e.g. to host in parallel to PlutoSliderServer, you want a reverse-proxy like `nginx`. We did some testing with nginx and the following configuration worked for us:

If you need to re-route JSServe e.g. to host in parallel to PlutoSliderServer, you want a reverse-proxy like `nginx`. We did some testing with nginx and the following configuration worked for us:

```nginx
server {
listen 8080;
Expand All @@ -68,6 +74,7 @@ server {
}
}
```

and the JSServer with:
```julia
server = Server("127.0.0.1", 8081;proxy_url="https://www.abc.org/jsserve/")
Expand All @@ -79,8 +86,8 @@ This would re-route `www.abc.org:8080/jsserve/` to your local JSServe-Server.
If you get errors in your browser console relating to "GET", "MIME-TYPE"

1. First make sure that the URL of the assets is "correct", that is, there is no `//` somewhere in the domain, and in principle the client tries to find the correct target (`Server(...,verbose=1)` might help to see if requests arrive).
2. if the app shows up fine, but you get these errors (typically with wss:// in the front, indicating some websocket issue), double check that all the slashes `/` in your configuration are set correct. That is all these 4 paths should have `/`'s at the end: `location /subfolder/`, `proxy_pass =.../` `Server(...,proxy_url=".../")` and `route!(...,'/'=>app)`
3. If it still doesnt work, you might need to look into websocket forwarding - or you might have an intermediate reverse-proxy that blocks the websocket.
2. if the app shows up fine, but you get these errors (typically with `wss://` in the front, indicating some WebSocket issue), double check that all the slashes `/` in your configuration are set correct. That is all these 4 paths should have `/`'s at the end: `location /subfolder/`, `proxy_pass =.../` `Server(...,proxy_url=".../")` and `route!(...,'/'=>app)`
3. If it still doesn't work, you might need to look into WebSocket forwarding - or you might have an intermediate reverse-proxy that blocks the WebSocket.

### Heroku

Expand Down Expand Up @@ -109,6 +116,7 @@ my_app_name = get(ENV, "HEROKU_APP_NAME", "example-app")
url = "https://$(my_app_name).herokuapp.com/"
wait(JSServe.Server(my_app, "0.0.0.0", port, proxy_url=url))
```

`Procfile`:
```
web: julia --project=. app.jl
Expand All @@ -130,8 +138,9 @@ You can see the full example here:
https://github.com/SimonDanisch/JSServe-heroku

## Terminal

If no HTML display is found in the Julia display stack, JSServe calls `JSServe.enable_browser_display()` in the `__init__` function.
This adds a display, that opens a browser window to display the app
This adds a display, that opens a browser window to display the app.
The loading of the `BrowserDisplay` happen in any kind of environment without html display, so this should also work in any kind of terminal or when evaluating a script.

```julia
Expand All @@ -144,6 +153,10 @@ The loading of the `BrowserDisplay` happen in any kind of environment without ht
VScode with enabled `Plot Pane` will display any `JSServe.App` in the HTML plotpane:
![](vscode.png)

If VSCode is used in a remote setting, VSCode may automatically forward the port so the plot pane can work out of the box.
If this doesn't happen for some reason (it has been reported to not always work), you can manually forward it via the command menu (ctr+shift+p) and `forward a port`, or just select the PORTS tab in the terminal view.


## Notebooks

Most common notebook systems should work out of the box.
Expand Down Expand Up @@ -194,6 +207,18 @@ One can use the JSServe documentation source to see an example.

## Static export

JSServe works also to create static sites, by using `Routes` and `export_static`.
```julia
routes = Routes(
"/" => App(index_func, title="Makie"),
"/team" => App(team_func, title="Team"),
"/contact" => App(contact_func, title="Contact"),
"/support" => App(support_func, title="Support")
)
JSServe.export_static("html/folder", routes)
```

Please visit [Static Sites](@ref) for more details.

## Anything else

Expand Down
14 changes: 7 additions & 7 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
`JSServe.jl` is a pretty simple package allowing to render HTML and serve it from within Julia and build up a communication bridge with the Browser. This allows to combine any of your Julia applications with libraries like [WGLMakie](https://docs.makie.org/dev/documentation/backends/wglmakie/index.html#export) and create interactive Dashboards like this:
`JSServe.jl` is a pretty simple package allowing you to render HTML and serve it from within Julia and build up a communication bridge with the Browser. This allows to combine any of your Julia applications with libraries like [WGLMakie](https://docs.makie.org/dev/documentation/backends/wglmakie/index.html#export) and create interactive Dashboards like this:

![dashboard](https://user-images.githubusercontent.com/1010467/214651671-2f8174b6-48ab-4627-b15f-e19c35042faf.gif)

JSServe is tightly integrated with WGLMakie, which makes them a great pair for high performance, interactive visualizations.
If performance is not a high priority, many other plotting/visualization libraries which overload the Julia display system should work with `JSServe.jl` as well.

`JSServe.jl` itself tries to stay out of major choices like the HTML/CSS/Javascript framework to use for creating UIs and Dashboards.
`JSServe.jl` itself tries to stay out of major choices like the HTML/CSS/Javascript framework to use for creating UIs and Dashboards. Instead, it allows to create modular components and makes it easy to use any CSS/Javascript library.

It uses plain HTML widgets for UI elements where it can, and only to give some base convenience, there is `JSServe.TailwindDashboard` which gives the basic JSServe widgets some nicer look and make it a bit easier to construct complex layouts.
It uses plain HTML widgets for UI elements where it can, and there are a few styleable components like `Card`, `Grid`, `Row`, `Col` to make it easy to create some more complex dashboards out of the box.

As you can see in `JSServe/src/tailwind-dashboard.jl`, it's just a thin wrapper around the basic `JSServe.jl` widgets, which gives them some class(es) to style them via [TailwindCSS](https://tailwindcss.com/).
Anyone can do this with their own CSS or HTML/Javascript framework, which should help to create a rich ecosystem of extensions around `JSServe.jl`.
If you look at the source of those components, one will see that they're very simple and easy to create, which should help to create a rich ecosystem of extensions around `JSServe.jl`.
Read more about it in [Components](@ref).

## Quickstart

Expand Down Expand Up @@ -39,7 +39,7 @@ App(() -> DOM.div(...))
App(DOM.div(...))
```

The app will be displayed by e.g. the VSCode plotpane, Jupyter/Pluto or any other framework that overloads the Julia display system for HTML display.
The app will be displayed by e.g. the VSCode plot pane, Jupyter/Pluto or any other framework that overloads the Julia display system for HTML display.
In the REPL or an environment without an HTML ready display, a browser should open to display it (enabled explicitly via `JSServe.browser_display()`), but one can also serve the App very easily:

```julia
Expand Down Expand Up @@ -73,7 +73,7 @@ App() do
end
```

Read more about wrapping libraries in [Tutorial](@ref).
Read more about wrapping libraries in [Javascript](@ref).

## Deploying

Expand Down
14 changes: 7 additions & 7 deletions docs/src/animation.md → docs/src/interactions.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Animating things
# Interactions

Animations in JSServe are done via Observables.jl, much like it's the case for Makie.jl, so the same docs apply:

https://docs.makie.org/stable/documentation/nodes/index.html
https://docs.makie.org/stable/documentation/animation/index.html
* [observables](https://docs.makie.org/stable/documentation/nodes/index.html)
* [animation](https://docs.makie.org/stable/documentation/animation/index.html)

But lets quickly get started with a JSServe specific example:

Expand All @@ -18,13 +18,14 @@ App() do session
value = map(s.value) do x
return x ^ 2
end
# Record states is an experimental feature to record all states generated in the Julia session and allow the slider to stay interactive in the statically hosted docs!
return JSServe.record_states(session, DOM.div(s, value))
end
```

The `s.value` is an `Observable` which can be `mapp'ed` to take on new values, and one can insert observables as an input to `DOM.tag` or as any attribute.
The value of the `observable` will be renedered via `jssrender(session, observable[])`, and then updated whenever the value changes.
So anything that supports being inserted into the `DOM` can be inside an observable, and the fallback is to use the display system (so plots etc work as well).
The value of the `observable` will be rendered via `jssrender(session, observable[])`, and then updated whenever the value changes.
So anything that supports being inserted into the `DOM` can be inside an observable, and the fallback is to use the display system (so plots etc. work as well).
This way, one can also return `DOM` elements as the result of an observable:

```@example 1
Expand Down Expand Up @@ -64,7 +65,6 @@ App() do session
end
```


Likes this one create interactive examples like this:

```@example 1
Expand Down Expand Up @@ -95,7 +95,7 @@ app = App() do session
end
```

As you notice, when exporting this example to the docs which get statically hosted, all interactions requiring Julia ceise to exist.
As you notice, when exporting this example to the docs which get statically hosted, all interactions requiring Julia cease to exist.
One way to create interactive examples that stay active is to move the parts that need Julia to Javascript:

```@example 1
Expand Down
8 changes: 5 additions & 3 deletions docs/src/tutorial.md → docs/src/javascript-libraries.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# Tutorial
# Javascript

## Wrapping Javascript Libraries

```@setup 1
using JSServe
JSServe.Page()
```

```@example 1
leafletjs = JSServe.ES6Module("https://esm.sh/v111/leaflet@1.9.3/es2022/leaflet.js")
leafletcss = JSServe.Asset("https://unpkg.com/leaflet@1.9.3/dist/leaflet.css")
leafletjs = JSServe.ES6Module("https://esm.sh/v133/leaflet@1.9.4/es2022/leaflet.mjs")
leafletcss = JSServe.Asset("https://unpkg.com/leaflet@1.9.4/dist/leaflet.css")
struct LeafletMap
position::NTuple{2,Float64}
zoom::Int
Expand Down
Loading

0 comments on commit 7de60a9

Please sign in to comment.