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) {
'use strict';
var oryxPlugin = {}; // Object for public APIs
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
#[get("/first")]
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('#btn').onclick = function(){
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:
#[derive(Serialize, Deserialize, Debug)]
struct Name {
fname: String,
lname: String,
}
#[post("/call", format = "application/json", data = "<name>")]
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:
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket;
#[macro_use] extern crate serde_derive;
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.