DEV Community

loading...
Cover image for Frontend development with Rust part 2

Frontend development with Rust part 2

swisschili profile image Cheese Cake Aficionado ・3 min read

In the last post of this series I covered installing the dependencies needed to compile Rust for the web, and demonstrated how to print to the console and call rust functions from JavaScript. In this post I will explain in more detail how to interface between Rust and JS, use event listeners to wait for user interactions, and modify DOM elements.

Calling JavaScript code from Rust

The stdweb crate provides a useful js macro which makes it trivial to call JavaScript code from rust. You can easily use Rust code in this macro too using the @{} syntax. Here's a very basic example of using Rust variables in the js macro.

#[macro_use]
extern crate stdweb;

use stdweb::{
    js,
    console,
    web::*
};

fn main() {
    let name = "richard stallman";
    js! {
        alert("Hello, " + @{name});
    };
    console!(log, "Hello, " + name);
}

Alright! Now it's possible to call Rust code from JS and JS code from Rust. Neat!

DOM Manipulation

For some strange reason stdweb document elements do not have a method to set their inner HTML, but fortunately this is easy to implement using the power of the js macro.

Here's a small example of setting the content of an element with JS:

let some_element = document().query_selector("#some_element").unwrap().unwrap();
js! {
    @{some_element}.innerHTML = "<h1>Some Element</h1>";
};

Make sure you have an element with id some_element or this won't work (obviously).

Event Listeners

Event listeners are a simple way to listen for a certain event to take place on an object. They are an integral part of web development, and fortunately are very easy to use in Rust.

In this example I will demonstrate how to create a counter that displays the number of times a button was clicked.

The first thing that is needed is the HTML:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8"> 
        <title>Example</title>
    </head>
    <body>
        <div id="app">
            <button id="counter">Clicked 0 times</button>
        </div>
        <script src="project_name.js">
        </script>
    </body>
</html>

We will listen for click events on the button#counter and update it's content accordingly.

First, we need to get the element using document().query_selector() as seen earlier. Then we'll need to add an event listener, and in that event listener update it's inner HTML.

// Needed for the js! macro
#![feature(proc_macro_hygiene)]

#[macro_use]
extern crate stdweb;

use stdweb::{
    web::*,
    web::event::*
};


fn main() {
    stdweb::initialize();

    let button = document().query_selector("#click").unwrap().unwrap();

    let mut counter = 0;
    button.clone().add_event_listener(move |_: ClickEvent| {
        counter += 1;
        js! {
            @{&button}.innerHTML = @{format!("Clicked {} times", counter)};
        };
    });

    stdweb::event_loop();
}

Alright! Reload your browser (or run cargo web start if you haven't started the server yet) and click the button! If all goes well you should see it change at every click!

One thing to note about the above code is that you will not be able to use the button after the closure. This is because the lambda is called on a clone of the button, while the actual button is moved to the context of the lambda (the move statement). If you would like to use the button later on in your code, create a clone of it beforehand and manipulate that clone inside the closure.

In the next installment of this series I will cover writing a simple router that will allow for modular single-page applications and transferring the UI code to Rust to make it more easily extendable.

I hope you found this useful, if there is anything you think I missed please let me know.

Discussion

pic
Editor guide
Collapse
therossinator profile image
Ross Kuehl

Great series! I'm very new to Rust but I find the possibilities for front end development one of the coolest things about it.

Would you mind showing what it would look like to create a clone of the button beforehand and manipulate it inside the closure? Thanks

Collapse
swisschili profile image
Cheese Cake Aficionado Author

Sure thing!

Notice the move keyword before the closure, this tells the compiler to move the values accessed inside the closure. I'm using this to tick the counter.

Here's one way of doing this that seems like it should work:

button.add_event_listener(move |_: ClickEvent| {
    // Code that accesses button
});

Although this looks perfectly fine, this won't compile. This is because when calling the method on button it must live until the end of the method:

});<--- here

But, it is moved inside the closure, making its lifetime end here:

here--->});

This causes a paradox and won't compile. To fix this in my example im calling the method on a clone of the button. This way, the clone's lifetime will extend past the end of the closure but the actual button will die at the end of it. This means I will not be able to access the button variable because it is essentially out of scope.

To fix this and be able to use it later, simply do the following:

button = btn.clone();
btn.add_event_listener( ... 

And use button in the closure. This will leave btn still in scope for me to use later.

Collapse
therossinator profile image
Ross Kuehl

That's awesome, thanks. That way the actual button will live on and we can simply just clone it whenever we want to use it again. Sweet!

Thread Thread
swisschili profile image
Cheese Cake Aficionado Author

Except for event listeners or other times the value is moved you don't even have to clone it.

Collapse
bbrosdev profile image
Begley Brothers (Development)

Thank for sharing your experiences and tips.

In the next installment of this series I will cover writing a simple router that will allow for modular single-page applications and transferring the UI code to Rust to make it more easily extendable.

Did this get published elsewhere?