app: Create a component for modals

This commit is contained in:
traxys 2023-05-29 16:47:18 +02:00
parent 1eb815ada9
commit a90b87e849
2 changed files with 240 additions and 80 deletions

205
app/src/bootstrap.rs Normal file
View file

@ -0,0 +1,205 @@
use yew::prelude::*;
pub mod bs {
use wasm_bindgen::prelude::*;
#[wasm_bindgen(js_namespace = bootstrap)]
extern "C" {
pub type Modal;
#[wasm_bindgen(static_method_of = Modal, js_name = "getInstance")]
pub fn get_instance(selector: &str) -> Modal;
#[wasm_bindgen(method)]
pub fn hide(this: &Modal);
}
}
#[derive(Properties, PartialEq)]
pub struct ModalProps {
pub id: AttrValue,
#[prop_or(true)]
pub fade: bool,
#[prop_or_default]
pub centered: bool,
#[prop_or_default]
pub labeled_by: Option<AttrValue>,
pub children: Children,
}
#[function_component]
pub fn Modal(props: &ModalProps) -> Html {
let mut class = classes!("modal");
if props.fade {
class.push("fade");
}
let mut dialog_class = classes!("modal-dialog");
if props.centered {
dialog_class.push("modal-dialog-centered");
}
html! {
<div
{class}
id={props.id.clone()}
tabindex="-1"
aria-labelledby={props.labeled_by.clone()}
aria-hidden="true"
>
<div class={dialog_class}>
<div class="modal-content">
{ for props.children.iter() }
</div>
</div>
</div>
}
}
#[derive(Properties, PartialEq)]
pub struct TitledModalProps {
pub id: AttrValue,
#[prop_or(true)]
pub fade: bool,
#[prop_or_default]
pub centered: bool,
pub title: AttrValue,
pub children: Children,
}
#[function_component]
pub fn TitledModal(
TitledModalProps {
id,
fade,
centered,
children,
title,
}: &TitledModalProps,
) -> Html {
let label = format!("{id}Label");
html! {
<Modal {id} {fade} {centered} labeled_by={label.clone()}>
<ModalHeader>
<h1 class={classes!("modal-title", "fs-5")} id={label}>{title}</h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
>
</button>
</ModalHeader>
{ for children.iter() }
</Modal>
}
}
#[derive(PartialEq, Properties)]
pub struct FormModalProps {
pub id: AttrValue,
#[prop_or(true)]
pub fade: bool,
#[prop_or_default]
pub centered: bool,
#[prop_or("Submit".into())]
pub submit_label: AttrValue,
pub on_submit: Callback<()>,
pub title: AttrValue,
pub children: Children,
}
#[function_component]
pub fn FormModal(
FormModalProps {
id,
fade,
centered,
submit_label,
title,
on_submit,
children,
}: &FormModalProps,
) -> Html {
let form_id = format!("{id}Form");
let on_submit = on_submit.clone();
let onsubmit = Callback::from(move |e: SubmitEvent| {
e.prevent_default();
on_submit.emit(());
});
html! {
<TitledModal {id} {fade} {centered} {title}>
<ModalBody>
<form id={form_id.clone()} {onsubmit}>
{ for children.iter() }
</form>
</ModalBody>
<ModalFooter>
<button type="button" class={classes!("btn", "btn-danger")} data-bs-dismiss="modal">
{"Cancel"}
</button>
<button type="submit" class={classes!("btn", "btn-primary")} form={form_id}>
{submit_label}
</button>
</ModalFooter>
</TitledModal>
}
}
#[derive(Properties, PartialEq)]
pub struct ModalToggleProps {
#[prop_or(classes!("btn", "btn-primary"))]
pub classes: Classes,
pub modal_id: AttrValue,
pub children: Children,
}
#[function_component]
pub fn ModalToggleButton(props: &ModalToggleProps) -> Html {
html! {
<button
class={props.classes.clone()}
data-bs-toggle="modal"
data-bs-target={format!("#{}", props.modal_id)}
>
{ for props.children.iter() }
</button>
}
}
#[derive(Properties, PartialEq)]
pub struct ModalContentProps {
pub children: Children,
}
#[function_component]
pub fn ModalHeader(props: &ModalContentProps) -> Html {
html! {
<div class="modal-header">
{ for props.children.iter() }
</div>
}
}
#[function_component]
pub fn ModalBody(props: &ModalContentProps) -> Html {
html! {
<div class="modal-body">
{ for props.children.iter() }
</div>
}
}
#[function_component]
pub fn ModalFooter(props: &ModalContentProps) -> Html {
html! {
<div class="modal-footer">
{ for props.children.iter() }
</div>
}
}

View file

@ -1,18 +1,23 @@
use api::{
CreateHouseholdRequest, CreateHouseholdResponse, Household, LoginRequest, LoginResponse,
};
use gloo_storage::{errors::StorageError, LocalStorage, Storage};
use itertools::Itertools;
use log::Level;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::{prelude::*, suspense::use_future};
use itertools::Itertools;
use yew_router::prelude::*;
use crate::sidebar::RegaladeSidebar;
use api::{
CreateHouseholdRequest, CreateHouseholdResponse, Household, LoginRequest, LoginResponse,
};
use crate::{
bootstrap::{bs, FormModal, ModalToggleButton},
sidebar::RegaladeSidebar,
};
mod bootstrap;
mod sidebar;
const API_ROUTE: &str = match option_env!("REGALADE_API_SERVER_BASE") {
@ -145,17 +150,6 @@ async fn do_new_household(token: String, name: String) -> anyhow::Result<Uuid> {
Ok(rsp.id)
}
#[wasm_bindgen(js_namespace = bootstrap)]
extern "C" {
type Modal;
#[wasm_bindgen(static_method_of = Modal, js_name = "getInstance")]
fn get_instance(selector: &str) -> Modal;
#[wasm_bindgen(method)]
fn hide(this: &Modal);
}
async fn fetch_households(token: String) -> anyhow::Result<api::Households> {
let rsp = gloo_net::http::Request::get(api!("household"))
.header("Authorization", &format!("Bearer {token}"))
@ -214,7 +208,7 @@ fn HouseholdListSelect() -> HtmlResult {
.clone()
.into_iter()
.sorted_by_key(|(_,i)| i.name.clone())
.map(mk_household)
.map(mk_household)
}
},
Err(e) => {
@ -238,9 +232,7 @@ fn HouseholdSelection() -> Html {
let err = error.clone();
let tok = token.clone();
let onsubmit = Callback::from(move |e: SubmitEvent| {
e.prevent_default();
let on_submit = Callback::from(move |()| {
let document = gloo_utils::document();
let token = tok.as_ref().unwrap().to_owned();
@ -262,7 +254,7 @@ fn HouseholdSelection() -> Html {
log::error!("Could not switch to new household: {e:?}")
}
let modal = Modal::get_instance("#newHsModal");
let modal = bs::Modal::get_instance("#newHsModal");
modal.hide();
navigator.push(&Route::Index);
@ -290,70 +282,33 @@ fn HouseholdSelection() -> Html {
<HouseholdListSelect />
</Suspense>
<hr />
<button
class={classes!("btn", "btn-lg", "btn-primary")}
data-bs-toggle="modal"
data-bs-target="#newHsModal"
<ModalToggleButton
classes={classes!("btn", "btn-lg", "btn-primary")}
modal_id="newHsModal"
>
{"New household"}
</button>
<div
class={classes!("modal", "fade")}
</ModalToggleButton>
<FormModal
id="newHsModal"
tabindex="-1"
aria-labelledby="newHsModalLabel"
aria-hidden="true"
centered={true}
submit_label={"Create"}
title="Create a Household"
{on_submit}
>
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class={classes!("modal-title", "fs-5")} id="newHsModalLabel">
{"Create a household"}
</h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
>
</button>
</div>
<div class="modal-body">
<form id="newHsForm" {onsubmit}>
if let Some(err) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{err}
</div>
}
<div class="form-floating">
<input
id="newHsName"
class={classes!("form-control")}
placeholder="Household name"
/>
<label for="newHsName">{"Household name"}</label>
</div>
</form>
</div>
<div class="modal-footer">
<button
type="button"
class={classes!("btn", "btn-secondary")}
data-bs-dismiss="modal"
>
{"Cancel"}
</button>
<button
type="submit"
class={classes!("btn", "btn-primary")}
form="newHsForm"
>
{"Create"}
</button>
</div>
if let Some(err) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{err}
</div>
}
<div class="form-floating">
<input
id="newHsName"
class={classes!("form-control")}
placeholder="Household name"
/>
<label for="newHsName">{"Household name"}</label>
</div>
</div>
</FormModal>
</div>
</>},
}