Building SPA web app with Rust
I wanted to build a SPA (Single Page Application), that is using rust files only, no JavaScript
or HTML
files.
I got the ide after noting that WASM
code should be called from JS
using fetch
, so though may using XHR
or Fetch
can be used to call required functionality from Rust
and forms/pages UI can be called as html
responce as well.
I'd 2 issues, that are:
- The need of submitting forms data as json, so I created global function for this to avoid multiple codingof the same lines.
- Loading the first page, as nthing can be done before
window.loaded
so even the first page had been loaded by recievig XHR responce
SO, I coded the below for:
- Creating the global function
toJSONString(form)
, - Calling the
'/first'
UI and display it
#[get("/")]
fn index() -> content::Html<&'static str> { content::Html(r#" <script> (function (root, factory) { if ( typeof define === 'function' && define.amd ) { define([], factory(root)); } else if ( typeof exports === 'object' ) { module.exports = factory(root); } else { root.oryxPlugin = factory(root); } })(typeof global !== "undefined" ? global : this.window || this.global, function (root) {
; var oryxPlugin = {}; oryxPlugin.toJSONString = function(form){ var obj = {}; var elements = form.querySelectorAll( "input, select, textarea" ); for( var i = 0; i < elements.length; ++i ) { var element = elements[i]; var name = element.name; var value = element.value; if( name ) { obj[name] = value; } } return JSON.stringify( obj ); }; return oryxPlugin; }); var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { var s = document.createElement('script'); s.type = 'text/javascript'; if (this.readyState == 4 && this.status == 200) { s.appendChild(document.createTextNode(this.responseText)); document.body.appendChild(s); } }; xhr.open("GET", '/first'); xhr.send(); </script> "#)
}
And coded the below as '/first'
page UI loading, in which I splitted the page into 2 parts:
- header, to be used for menu and so
- context, to be
empty
andrefilled
with each new page content
fn first() -> (content::Html<&'static str>) { content::Html(r#" var hdr = document.createElement("div"); hdr.id = 'header' hdr.innerHTML= ` <h1>Welcome to my app</h1><br> <button id = 'btn'>load second screen</button> `; hdr.querySelector(' var context = document.querySelector('#context') var xhr = new XMLHttpRequest(); xhr.open("GET", '/second'); xhr.onreadystatechange = function() { var s = document.createElement('script'); s.type = 'text/javascript'; if (this.readyState == 4 && this.status == 200) { s.appendChild(document.createTextNode(this.responseText)); document.body.appendChild(s); } }; xhr.send(); } document.body.appendChild(hdr); var context = document.createElement("div"); context.id = 'context' var form = document.createElement("form"); var button = document.createElement("button"); form.innerHTML =` <input type="text" name="fname" /> <input type="text" name="lname" /> ` button.onclick = function(){ var dataContainer = oryxPlugin.toJSONString(form); console.log(dataContainer); var xhr = new XMLHttpRequest(); xhr.open("POST", '/call', true); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var obj = JSON.parse(this.responseText); console.log('Returned string is: ' + obj.fname + ', ' + obj.lname); } }; xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); xhr.send(dataContainer); }; button.innerHTML ='Click HERE' context.appendChild(<h5>This is the first screen</h5><br>`) context.appendChild(form) context.appendChild(button) document.body.appendChild(context); "#)
}
Each page after the first
page, can be coded similar to the below, where the contents of the context
elemt are keep updating:
#[get("/second")]
fn second() -> (content::Html<&'static str>) { content::Html(r#" var div = document.createElement("div"); div.innerHTML= ` <h5>This is the second screen</h5><br> `; while (context.hasChildNodes()) { context.removeChild(context.lastChild); } context.appendChild(div); "#)
}
To handle json
recieved/sending, below code is for doing both steps:
struct Name { fname: String, lname: String,
} fn call(name: Json<Name>) -> Json<Name> { let user = &name; println!("Form field is: {:?} ", user); dbg!(user); println!("Fist name: {0}, Family name: {1}", user.fname, user.lname); let x = Name{ fname : name.fname.to_owned(), lname : name.lname.to_owned() }; Json(x)
}
This had been done using rocket.rs
, so the main.rs
header should be:
use rocket_contrib::json::Json; use rocket::response::content;
And the routes to be defined as:
fn main() { rocket::ignite() .mount("/", routes![index, call, first, second]) .launch();
}
Not to forget the Cargo.toml
is:
[package]
name = "workshop"
version = "0.1.0"
authors = ["Hasan Yousef"]
edition = "2018"
[dependencies]
rocket = "0.4.0"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
[dependencies.rocket_contrib]
version = "0.4.0"
default-features = false
features = ["json"]
Nightly
rust is required for rocket.rs
apps till now.