Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/oxide-byte/todo-tauri
- Owner: oxide-byte
- License: mit
- Created: 2024-12-23T16:15:15.000Z (about 1 month ago)
- Default Branch: master
- Last Pushed: 2025-01-10T19:11:11.000Z (12 days ago)
- Last Synced: 2025-01-10T20:22:49.901Z (12 days ago)
- Topics: leptos, poc, rust, tauri, tauri-app, todo-app
- Language: Rust
- Homepage:
- Size: 246 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.md
- License: LICENSE
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