Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/oxide-byte/todo-tauri

Simple Leptos Todo App embedded in Tauri for Desktop application
https://github.com/oxide-byte/todo-tauri

leptos poc rust tauri tauri-app todo-app

Last synced: 11 days ago
JSON representation

Simple Leptos Todo App embedded in Tauri for Desktop application

Awesome Lists containing this project

README

        

# Todo - Leptos - Tauri

## Introduction

This is a simple POC of a [Leptos (0.7)](https://leptos.dev/) Todo Web Application embedded in the [Tauri 2](https://v2.tauri.app/) Framework for Desktop and/or Mobile Applications.

## Preparation

* cargo install trunk
* rustup target add wasm32-unknown-unknown

## Creation

cargo new todo-tauri

## Todo Leptos

We start doing a CSR (client-side rendering) Todo Lepos application in a couple of steps.
This part could easily replaced by an Angular, React or other Web Framework.

Adding the dependency to cargo.toml

```yaml
[dependencies]
leptos = { version = "0.7.1", features = ["csr"] }
```

We validate the current environment by running the default Hello World

```shell
cargo build
cargo run
```

Let add a simple container index.html file:

```html

Todo-Tauri

```

And modify the current main.rs

```rust
use leptos::prelude::*;

fn main() {
mount_to_body(|| view! {

Hello World

});
}
```

As we use Leptos and WebAssembly we now render a code that is shown in a browser.
Trunk offers us a Hot Reload option for developing the Frontend part.

we add a general configuration file trunk.toml
```toml
[build]
target = "index.html"
dist = "dist"
```

start the service

```shell
trunk serve
```

and open in the browser the default url http://127.0.0.1:8080/

Next apply some style to the page in configuring Tailwind

Add a file tailwind.css under /style

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Add the link on the header of the index.html

```html
...

...
```
and add in the root a config file /tailwind.config.js

```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./src/**/*.rs"],
transform: {
rs: (content) => content.replace(/(?:^|\s)class:/g, ' '),
},
},
theme: {
extend: {},
},
plugins: [],
}
```

Finally we add a Tailwind operator to the code:

```rust
mount_to_body(|| view! {

Hello World with Tailwind

});
```

With this we have closed the preparation for creating a client-side web page. The current
produced artifacts are in the folder /dist and could deployed to a server, on Github as Github Page or on AWS in a S3Bucket.

## Todo App

First step, we add some dependencies to the cargo file:
```yaml
[dependencies]
leptos = { version = "0.7.1", features = ["csr"] }
uuid = { version = "1.11.0", features = ["v4", "js"] }
instant = { version = "0.1.13", features = [ "wasm-bindgen", "inaccurate" ] }
```

Now we add a couple of files in our source:

in src/entities

todo.rs
```rust
use instant::Instant;
use uuid::Uuid;

#[derive(Clone, Debug)]
pub struct Todo {
pub id: String,
pub title: String,
pub description: String,
pub created: Instant,
}

impl Todo {
pub fn new(title: String, description: String) -> Self {
let id = Uuid::new_v4().to_string();
let created = Instant::now();
Todo { id, title, description, created }
}

pub(crate) fn new_empty() -> Todo {
Self::new("".to_string(), "".to_string())
}
}
```

and include it in the mod.rs
```rust
mod todo;

pub use todo::Todo;
```

Next the UI components in src/components:

todo_item.rs
```rust
use crate::entities::Todo;
use leptos::html::*;
use leptos::prelude::*;

#[component]
pub fn TodoItem(todo: Todo, edit: E, delete: D,
) -> impl IntoView
where
D: Fn(Todo) + 'static,
E: Fn(Todo) + 'static,
{
let button_mod_class = "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-full text-sm p-2.5 text-center inline-flex items-center mr-2";
let button_del_class = "text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-full text-sm p-2.5 text-center inline-flex items-center mr-2";
let todo_item: RwSignal = RwSignal::new(todo);

let on_edit = move |_| {
edit(todo_item.get());
};

let on_delete = move |_| {
delete(todo_item.get());
};

view! {



{todo_item.get().title}


{todo_item.get().description}












}
}
```

todo_modal.rs
```rust
use crate::entities::Todo;
use leptos::html::*;
use leptos::prelude::*;

#[component]
pub fn TodoModal(todo: RwSignal, on_close_modal: F) -> impl IntoView
where
F: Fn(Option) + 'static + Copy,
{
let input_field_class = "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline";

let (title, _set_title) = signal(todo.get().title);
let title_node: NodeRef = NodeRef::new();

let (description, _set_description) = signal(todo.get().description);
let description_node: NodeRef = NodeRef::new();

let submit = move |_| {
let title = title_node
.get()
.expect(" should be mounted")
.value();

let description = description_node
.get()
.expect(" should be mounted")
.value();

let mut mod_todo = todo.get().clone();
mod_todo.title = title;
mod_todo.description = description;
on_close_modal(Some(mod_todo));
};

let cancel = move |_| {
on_close_modal(None);
};

view! {


Create new Todo



Title




Description

{
description
}



Save


Cancel




}
}
```

app.rs
```rust
use crate::components::*;
use crate::entities::*;
use leptos::prelude::*;

#[component]
pub fn App() -> impl IntoView {
let show_modal: RwSignal = RwSignal::new(false);
let edit_todo_item: RwSignal = RwSignal::new(Todo::new_empty());

let button_new_class = "rounded-full pl-5 pr-5 bg-blue-700 text-white rounded hover:bg-blue-800";

let todos: RwSignal> = RwSignal::new(Vec::new());

let add_new_todo = move |x: Todo| {
edit_todo_item.set(x);
show_modal.set(true);
};

let edit_todo = move |todo: Todo| {
edit_todo_item.set(todo);
show_modal.set(true);
};

let delete_todo = move |todo: Todo| {
todos.update(|old| {
old.retain(|x| x.id != todo.id);
});
};

let close_modal_todo = move |x: Option| {
if let Some(todo) = x {
todos.update(|old| {
old.retain(|x| x.id != todo.id);
old.push(todo);
old.sort_by(|a, b| a.created.cmp(&b.created));
});
}
show_modal.set(false);
};

view! {


Todo List








Currently no Todos






}
}
```

and include all files in mod.rs
```rust
mod app;
mod todo_item;
mod todo_modal;

pub use app::App;
pub use todo_item::TodoItem;
pub use todo_modal::TodoModal;
```

attaching the mod files in the main.rs
```rust
mod components;
mod entities;

use crate::components::App;
use leptos::prelude::*;

fn main() {
mount_to_body(App)
}
```

start the service

```shell
trunk serve
```

We have a Todo web application running.

## Tauri

We could apply the command [cargo create-tauri-app] on a new application or [cargo tauri init] on an existing.

```shell
cargo tauri init
```

that creates a new module in our project under the folder: src-tauri.

In the main module we add to Cargo.toml
```toml
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"
console_error_panic_hook = "0.1.7"

[workspace]
members = ["src-tauri"]
```

and we add to trunk.toml
```toml
[watch]
ignore = ["./src-tauri"]

[serve]
port = 1420
open = false
```
The change of port is necessary for Tauri.

and delete now the tauri-app folder

Start the application with:

```shell
cargo tauri dev
```

We have the first run of the application on the Desktop.

## Separate Backend / Frontend

The Frontend has limited capabilities, for this we transfer some parts of our application to the Backend, for example to include a communication to a Database.

We add some dependencies to the main cargo.toml

```toml
[dependencies]
leptos = { version = "0.7.1", features = ["csr"] }
uuid = { version = "1.11.0", features = ["v4", "js"] }
chrono = { version = "0.4.39", features = ["serde", "wasm-bindgen"] }
wasm-bindgen = { version = "0.2.99", features = ["serde"] }
wasm-bindgen-futures = "0.4.49"
web-sys = "0.3.76"
js-sys = "0.3.76"
serde = { version = "1.0.216", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.5"}
gloo-utils = { version = "0.2.0", features = ["serde"] }
```

and to the cargo.toml in src-tauri:
```toml
[dependencies]
tauri = { version = "2.1.1", features = [] }
tauri-plugin-opener = "2.2.2"
serde = { version = "1.0.216", features = ["derive"] }
serde_json = { version = "1.0.133" }
chrono = { version = "0.4.39", features = ["serde"] }
```

As you can I switch for the timestamps to the chrono library as it had less problems for the serialization.

In the main.rs file from the Frontend we add:

```rust
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]
async fn invoke_without_args(cmd: &str) -> JsValue;

#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}
```

Two binding methods, to communicate from the Frontend to the Backend.

The Todo Entity get modified:

```rust
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Todo {
pub id: String,
pub title: String,
pub description: String,
pub created: DateTime,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct TodoJsValue {
pub todo: Todo
}

impl Todo {
pub fn new(title: String, description: String) -> Self {
let id = Uuid::new_v4().to_string();
let created = Utc::now();
Todo { id, title, description, created }
}

pub fn new_empty() -> Todo {
Self::new("".to_string(), "".to_string())
}
pub fn js_value(&self) -> JsValue {
let container = TodoJsValue { todo: self.clone() };
JsValue::from_serde(&container).unwrap()
}
}
```

The TodoJsValue is a container/wrapper to sent the Todo Data to the Backend by JsValue.

We can now modify the app.rs:

```rust
#[derive(Debug, PartialEq)]
enum Mode {
ADD,
EDIT
}

async fn load_data(_trigger: DateTime, signal: WriteSignal>) -> Vec {
let rtn = invoke_without_args("get_todo_list").await;
let todos = rtn.into_serde::>().unwrap();
signal.set(todos.clone());
todos
}

#[component]
pub fn App() -> impl IntoView {
let show_modal: RwSignal = RwSignal::new(false);
let show_modal_mode: RwSignal = RwSignal::new(ADD);
let edit_todo_item: RwSignal = RwSignal::new(Todo::new_empty());

let button_new_class = "rounded-full pl-5 pr-5 bg-blue-700 text-white rounded hover:bg-blue-800";
let todos: RwSignal> = RwSignal::new(Vec::new());

let refresh :RwSignal> = RwSignal::new(Utc::now());
let _fetch_todos = LocalResource::new(move || load_data(refresh.get(), todos.write_only()));

let add_new_todo = move |x: Todo| {
edit_todo_item.set(x);
show_modal_mode.set(ADD);
show_modal.set(true);
};

let edit_todo = move |todo: Todo| {
edit_todo_item.set(todo);
show_modal_mode.set(EDIT);
show_modal.set(true);
};

let delete_todo = move |todo: Todo| {
spawn_local(async move {
let data = todo.js_value();
invoke("delete_todo", data).await;
refresh.set(Utc::now());
});
};

let close_modal_todo = move |x: Option| {
spawn_local(async move {
if show_modal_mode.read() == ADD {
let data = x.unwrap().js_value();
invoke("add_todo", data).await;
} else {
let data = x.unwrap().js_value();
invoke("edit_todo", data).await;
}
refresh.set(Utc::now());
});
show_modal.set(false);
};

view! {


Todo List








Currently no Todos






}
}
```

The call to the Backend are asynchronous, for this we use spawn_local and call the WASM invoke methods.

Finally in the src-tauri backend part we include the business logic in the lib.rs

```rust
#[tauri::command]
fn get_todo_list(storage: State) -> Vec {
println!("Backend:get_todo_list");
let s = storage.store.lock().unwrap();
s.to_vec()
}

#[tauri::command]
fn add_todo(todo: Todo, storage: State) {
println!("Backend:add_todo: {:?}", todo);
let mut s = storage.store.lock().unwrap();
s.push(todo);
}

#[tauri::command]
fn edit_todo(todo: Todo, storage: State) {
println!("Backend:edit_todo");
let mut s = storage.store.lock().unwrap();
s.retain(|x| x.id != todo.id);
s.push(todo);
}

#[tauri::command]
fn delete_todo(todo: Todo, storage: State) {
println!("Backend:delete_todo");
let mut s = storage.store.lock().unwrap();
s.retain(|x| x.id != todo.id);
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Todo {
pub id: String,
pub title: String,
pub description: String,
pub created: DateTime
}

#[derive(Debug)]
struct Storage {
store: Mutex>,
}
// To evaluate in the future
// #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.manage(Storage { store: Default::default() })
.invoke_handler(tauri::generate_handler![
get_todo_list,
add_todo,
edit_todo,
delete_todo,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
and declare the different commands.

## Final Build

We can now do the final build:

```shell
cargo tauri build
```

and run for your machine the correspondent file: target/release/bundle