feat: create reactive traits (AsReactive and ReactiveCapture)
This commit is contained in:
1 parent
d01b568f15
commit
ec26fb6fce
9 files changed
+201
-20
No files matched your search
Generated
+9
@@ -2159,6 +2159,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
"traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2960,6 +2961,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_dynamo",
|
||||
"tokio",
|
||||
"traits",
|
||||
"url",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
@@ -3645,6 +3647,13 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "traits"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"leptos",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "scholarships-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -24,6 +24,7 @@ leptos_oidc = "0.9.0"
|
||||
url = "2.5.4"
|
||||
|
||||
macros = { path = "./macros" }
|
||||
traits = { path = "./traits" }
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
|
||||
@@ -9,3 +9,5 @@ proc-macro = true
|
||||
[dependencies]
|
||||
syn = "2.0"
|
||||
quote = "1.0"
|
||||
|
||||
traits = { path = "../traits" }
|
||||
+81
-10
@@ -1,7 +1,72 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{Data, Ident, parse_macro_input};
|
||||
use syn::{parse_macro_input, Data, Ident};
|
||||
|
||||
/// Derives the `AsReactive` and `ReactiveCapture` traits for a struct, while also
|
||||
/// providing a new reactive struct with the same field names, but they are wrapped
|
||||
/// in the Leptos `RwSignal`.
|
||||
///
|
||||
/// Fields that are custom types are not automatically converted into reactive types. They
|
||||
/// will be wrapped in an `RwSignal` struct, but that is where this process stops. Making
|
||||
/// other information reactive must be implemented manually.
|
||||
///
|
||||
/// For example, when the `Reactive` macro is expanded for the struct `User`, it will look
|
||||
/// like this:
|
||||
///
|
||||
/// ```
|
||||
/// #[derive(Reactive)]
|
||||
/// struct User {
|
||||
/// id: i32,
|
||||
/// name: String,
|
||||
/// custom: CustomType
|
||||
/// }
|
||||
///
|
||||
/// // This code will be added:
|
||||
/// struct UserReactive {
|
||||
/// id: RwSignal<i32>,
|
||||
/// name: RwSignal<String>,
|
||||
/// custom: RwSignal<CustomType>
|
||||
/// }
|
||||
///
|
||||
/// impl AsReactive for User {
|
||||
/// type ReactiveType = UserReactive;
|
||||
/// // Other methods - refer to AsReactive documentation.
|
||||
/// }
|
||||
///
|
||||
/// impl ReactiveCapture for UserReactive {
|
||||
/// type CaptureType = User;
|
||||
/// // Other methods - refer to ReactiveCapture documentation.
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The base type (`User`) can then be converted into the reactive type:
|
||||
/// ```
|
||||
/// let user = User::new();
|
||||
/// let user_reactive = user.as_reactive();
|
||||
/// ```
|
||||
/// And captured back into a non-reactive type:
|
||||
/// ```
|
||||
/// let captured_user = user_reactive.capture()
|
||||
/// ```
|
||||
///
|
||||
/// # Important Considerations
|
||||
///
|
||||
/// A reminder that custom types, in this case `CustomType`, are just wrapped in an `RwSignal`.
|
||||
/// They are not automatically converted to their own `Reactive` type in the case that they
|
||||
/// derive this macro. As a result, custom reactive types *must* be updated manually by utilizing
|
||||
/// the `RwSignal::update` and `RwSignal::set` methods.
|
||||
///
|
||||
/// For example, the `UserReactive` struct from earlier does not convert the `CustomType` into a
|
||||
/// `CustomTypeReactive`. We have to do that ourselves:
|
||||
/// ```
|
||||
/// let user: User = User::new();
|
||||
/// let user_reactive: UserReactive = user.as_reactive();
|
||||
/// let custom_reactive: CustomTypeReactive = user_reactive.custom.as_reactive();
|
||||
/// // And then convert back...
|
||||
/// user_reactive.custom.set(custom_reactive.capture());
|
||||
/// ```
|
||||
/// If a shorthand for the `capture` and `as_reactive` functions are absolutely *required*, then this
|
||||
/// macro is probably not the way to go - as of now, this functionality should be impmlemented manually.
|
||||
#[proc_macro_derive(Reactive)]
|
||||
pub fn make_reactive(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as syn::DeriveInput);
|
||||
@@ -32,7 +97,7 @@ pub fn make_reactive(input: TokenStream) -> TokenStream {
|
||||
|
||||
// Create new function arguments/inits
|
||||
let new_fn_arg = quote! {
|
||||
#field_name: _RwSignal::new(data.#field_name),
|
||||
#field_name: _RwSignal::new(self.#field_name),
|
||||
};
|
||||
|
||||
let from_fn_arg = quote! {
|
||||
@@ -43,30 +108,36 @@ pub fn make_reactive(input: TokenStream) -> TokenStream {
|
||||
from_fn_args.push(from_fn_arg);
|
||||
}
|
||||
|
||||
let reactive_docs = format!("Reactive version of the [`{name}`] struct. Automatically created by the `Reactive` macro.");
|
||||
let reactive_docs = format!(
|
||||
"Reactive version of the [`{name}`] struct. Automatically created by the `Reactive` macro."
|
||||
);
|
||||
|
||||
// Expand the macro. The output is a struct with new reactive fields and a new function,
|
||||
// which initializes the reactive fields based on the original struct's fields.
|
||||
let expanded = quote! {
|
||||
use leptos::prelude::RwSignal as _RwSignal; // in case the user imports it themselves
|
||||
use leptos::prelude::GetUntracked as _;
|
||||
use traits::{AsReactive, ReactiveCapture};
|
||||
|
||||
#[doc = #reactive_docs]
|
||||
pub struct #reactive_name {
|
||||
#(#new_fields)*
|
||||
}
|
||||
|
||||
impl #reactive_name {
|
||||
/// Creates a new `#reactive_name` struct from a non-reactive `#name`.
|
||||
pub fn new(data: #name) -> Self {
|
||||
Self {
|
||||
impl AsReactive for #name {
|
||||
type ReactiveType = #reactive_name;
|
||||
|
||||
fn as_reactive(self) -> Self::ReactiveType {
|
||||
#reactive_name {
|
||||
#(#new_fn_args)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Captures the information of this struct into a `#name` struct, for use
|
||||
/// in server functions.
|
||||
pub fn capture(&self) -> #name {
|
||||
impl ReactiveCapture for #reactive_name {
|
||||
type CaptureType = #name;
|
||||
|
||||
fn capture(&self) -> Self::CaptureType {
|
||||
#name {
|
||||
#(#from_fn_args)*
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::components::ActionButton;
|
||||
use leptos::prelude::*;
|
||||
use macros::Reactive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Reactive, Default)]
|
||||
pub struct MultiEntryData {
|
||||
pub id: Uuid,
|
||||
pub data: HashMap<String, ValueType>,
|
||||
@@ -130,8 +131,12 @@ pub fn MultiEntry(
|
||||
<div class="rounded-md bg-red-700">
|
||||
// This div MUST have the same spacing CSS as the main entry div
|
||||
<div class="flex bg-inherit p-3 m-1">
|
||||
<span class="flex-1 text-white font-bold">{name_member.get().display_name}</span>
|
||||
<span class="flex-1 text-white font-bold">{info_member.get().display_name}</span>
|
||||
<span class="flex-1 text-white font-bold">
|
||||
{name_member.get().display_name}
|
||||
</span>
|
||||
<span class="flex-1 text-white font-bold">
|
||||
{info_member.get().display_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
// Entry section
|
||||
@@ -139,18 +144,18 @@ pub fn MultiEntry(
|
||||
<For
|
||||
each=move || entries.get()
|
||||
key=|entry: &MultiEntryData| entry.id
|
||||
children = move |entry| view! {
|
||||
children=move |entry| {
|
||||
view! {
|
||||
<Entry
|
||||
entry_data=entry.clone()
|
||||
name_member=name_member
|
||||
info_member=info_member
|
||||
/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ActionButton on:click=create_entry>
|
||||
Add Entry
|
||||
</ActionButton>
|
||||
<ActionButton on:click=create_entry>Add Entry</ActionButton>
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue, Clien
|
||||
use serde_dynamo::{from_item, to_item};
|
||||
|
||||
use crate::app::Unauthenticated;
|
||||
use crate::common::{StudentInfo, StudentInfoReactive, UserClaims};
|
||||
use crate::common::{StudentInfo, UserClaims};
|
||||
use crate::components::{
|
||||
ActionButton, CheckboxList, Loading, OutlinedTextField, Panel, Row, Select,
|
||||
RadioList
|
||||
@@ -14,6 +14,7 @@ use crate::components::{
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
|
||||
use traits::{AsReactive, ReactiveCapture};
|
||||
|
||||
#[server(GetSubmission, endpoint = "/get-submission")]
|
||||
pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> {
|
||||
@@ -119,7 +120,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
server_resource
|
||||
.get()
|
||||
.map(|submission: StudentInfo| {
|
||||
let reactive_info = StudentInfoReactive::new(submission);
|
||||
let reactive_info = submission.as_reactive();
|
||||
let select_value = RwSignal::new(String::from("Math"));
|
||||
let chk_select = RwSignal::new(vec!["Testing 2".into()]);
|
||||
let elements_disabled = RwSignal::new(false);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "traits"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
leptos = { version = "0.8" }
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod reactive;
|
||||
|
||||
pub use reactive::*;
|
||||
@@ -0,0 +1,82 @@
|
||||
/// Trait that ensures a reactive struct can be created from a non-reactive struct.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```rust
|
||||
/// struct User {
|
||||
/// id: i32,
|
||||
/// name: String
|
||||
/// }
|
||||
///
|
||||
/// struct UserReactive {
|
||||
/// id: RwSignal<i32>,
|
||||
/// name: RwSignal<String>
|
||||
/// }
|
||||
///
|
||||
/// impl ReactiveCapture for UserReactive {
|
||||
/// // implement - required for ReactiveType.
|
||||
/// }
|
||||
///
|
||||
/// impl AsReactive for User {
|
||||
/// type ReactiveType = UserReactive;
|
||||
/// fn as_reactive(self) -> UserReactive {
|
||||
/// UserReactive {
|
||||
/// id: RwSignal::new(self.id),
|
||||
/// name: RwSignal::new(self.name)
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait AsReactive {
|
||||
/// The type of the reactive struct. It must implement `ReactiveCapture`.
|
||||
type ReactiveType: ReactiveCapture;
|
||||
|
||||
/// Wraps all fields of the struct into an `RwSignal`, returning a new
|
||||
/// "reactive" struct.
|
||||
///
|
||||
/// Note that this method consumes the original struct and returns a new
|
||||
/// reactive struct.
|
||||
fn as_reactive(self) -> Self::ReactiveType;
|
||||
}
|
||||
|
||||
/// Trait that ensures a reactive struct can be captured into a non-reactive struct.
|
||||
/// The "captured" struct does not contain *any* reactive fields, meaning that it is not
|
||||
/// wrapped in an `RwSignal`.
|
||||
///
|
||||
/// The captured struct should simply collect all the values from the reactive fields at
|
||||
/// that point in time.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```rust
|
||||
/// struct User {
|
||||
/// id: i32,
|
||||
/// name: String
|
||||
/// }
|
||||
///
|
||||
/// struct UserReactive {
|
||||
/// id: RwSignal<i32>,
|
||||
/// name: RwSignal<String>
|
||||
/// }
|
||||
///
|
||||
/// impl AsReactive for User {
|
||||
/// // implement - required for CaptureType.
|
||||
/// }
|
||||
///
|
||||
/// impl ReactiveCapture for UserReactive {
|
||||
/// type CaptureType = User;
|
||||
/// fn capture(self) -> User {
|
||||
/// User {
|
||||
/// id: self.id.get(),
|
||||
/// name: self.name.get()
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait ReactiveCapture {
|
||||
/// The type of the captured struct. It must implement `AsReactive`.
|
||||
type CaptureType: AsReactive;
|
||||
|
||||
/// Captures the current state of the reactive struct into a non-reactive struct.
|
||||
fn capture(&self) -> Self::CaptureType;
|
||||
}
|
||||
Reference in new issue
Block a user