Files
scholarships-rs/src/components/data_display.rs
T

254 lines
10 KiB
Rust

use crate::common::{SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, SchemaType, ValueType};
use leptos::prelude::*;
use std::collections::HashMap;
#[component]
pub fn DataDisplay(
/// The schema that this component should use to display the given data.
#[prop(into)]
schema: Signal<SchemaNode>,
/// The starting key in the data map.
#[prop(into, optional)]
data_member: Signal<String>,
/// A `HashMap` containing all the data that should be displayed.
#[prop(into)]
data_map: Signal<HashMap<String, ValueType>>,
) -> impl IntoView {
// The schema should contain information about the header type and styling, along with the
// information display. This is rendered as part of the DataDisplayWrapper, which is simply a
// wrapper around the information to be displayed.
// The children are all SchemaNodes as well, meaning that this component needs to call itself
// for each of these children. They will of course contain all the same information that we
// expect here.
// The actual displayed information is controlled through a DataDisplayView, which will accept
// settings for how the information is parsed. For example, rendering a list may be done by
// a string join operation, or it may be a list of maps, each of which needs to be displayed with
// a specific set of children. The SchemaNode must be able to handle this, and these options
// should be given to the DataDisplayView somehow.
// As the display_value here is actually an Option<ValueType>, we want to be able to handle cases
// where the display is given no values. In this case, the SchemaNode needs to specify whether
// a missing value is expected or an error.
let display_value = Memo::new(move |_| {
let member = data_member.get();
if member.is_empty() {
Some(ValueType::Map(Some(data_map.get())))
} else {
data_map.get().get(&member).cloned()
}
});
view! {
<DataDisplayWrapper
container_style=schema.get().container_style
header_style=schema.get().header_style
header_text=schema.get().header
>
<DataDisplayView display_data=display_value schema=schema />
</DataDisplayWrapper>
}
}
#[component]
fn DataDisplayWrapper(
#[prop(into)] container_style: SchemaContainerStyle,
#[prop(into)] header_style: SchemaHeaderStyle,
#[prop(into)] header_text: Signal<String>,
children: Children,
) -> impl IntoView {
// This component takes a wrapper style and displays the "chrome" of the interface. It has two
// options: Text Headers or Capsules. This will then render a DataDisplayHeader, which handles
// the styling for the text inside the header specifically.
// The capsule style is a colored header and a rounded container.
// The text header style is an invisible container.
let expanded = RwSignal::new(true);
match container_style {
SchemaContainerStyle::Header => {
// Creates a simple container that only has a header and a series of children.
// The spacing between the header and children is controlled by the header style.
// This type of container is not collapsable at the moment.
view! {
<div class="flex flex-col w-full text-left">
<div class="self-start w-full">
<DataDisplayHeader header_text=header_text header_style=header_style />
<div class="p-2 flex flex-col gap-1">{children()}</div>
</div>
</div>
}
.into_any()
}
SchemaContainerStyle::Capsule => {
// Creates a capsule container - it is collapsable, with rounded corners and a colored
// header.
view! {
<div class="self-stretch rounded-lg shadow-lg flex flex-col w-full text-center">
<div
class="p-2 px-3 rounded-t-lg cursor-pointer bg-red-800 text-white"
class=(["rounded-lg"], move || !expanded.get())
on:click=move |_| expanded.update(|b| *b = !*b)
>
<DataDisplayHeader header_text=header_text header_style=header_style />
</div>
<div
class="grid transition-all ease-in-out"
style:grid-template-rows=move || if expanded.get() { "1fr" } else { "0fr" }
>
<div class="min-h-0 overflow-hidden">
<div class="p-2 flex flex-col gap-1 flex-1">{children()}</div>
</div>
</div>
</div>
}
.into_any()
}
}
}
#[component]
fn DataDisplayHeader(
#[prop(into)] header_style: SchemaHeaderStyle,
#[prop(into)] header_text: Signal<String>,
) -> impl IntoView {
// This component will take a header style and displays the corresponding text. It has a few
// options, all of which are defined in the HeaderStyle enum.
match header_style {
SchemaHeaderStyle::MainHeader => {
view! { <div class="text-3xl font-bold my-2">{header_text}</div> }.into_any()
}
SchemaHeaderStyle::SubsectionHeader => {
view! { <div class="text-xl font-bold">{header_text}</div> }.into_any()
}
SchemaHeaderStyle::Bold => view! { <div class="font-bold">{header_text}</div> }.into_any(),
SchemaHeaderStyle::None => view! {}.into_any(),
}
}
#[component]
fn DataDisplayView(
#[prop(into)] display_data: Signal<Option<ValueType>>,
#[prop(into)] schema: Signal<SchemaNode>,
) -> impl IntoView {
// This is the part of the data display that does most of the work. It will take the values
// found in the DataDisplay component and perform the logic that actually displays the
// information. This component is only displayed if there is Some(value) in the DataDisplay.
// In the case that this is displaying a Map or List, it will recursively call the DataDisplay
// component.
// If this is displaying a PrimitiveList, it will flatten and join all the values into a single
// string, then display that string.
let data_type = schema.get().data_type;
// Matches the specific data type. Displays errors if the given display data does not match.
match data_type {
SchemaType::String => {
let value = Signal::derive(move || {
display_data
.get()
.map(|v| v.as_string().ok().flatten())
.flatten()
.unwrap_or("N/A".to_string())
});
view! { <div>{value}</div> }.into_any()
}
SchemaType::Number => {
let value = Signal::derive(move || {
display_data
.get()
.map(|v| v.as_number().ok().flatten())
.flatten()
.unwrap_or("N/A".to_string())
});
view! { <div>{value}</div> }.into_any()
}
SchemaType::PrimitiveList => {
let value = Signal::derive(move || {
display_data
.get()
.map(|v| v.as_list().ok().flatten())
.flatten()
.unwrap_or_default()
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(", ")
});
view! { <div>{value}</div> }.into_any()
}
SchemaType::MapList => {
let list = Signal::derive(move || {
display_data
.get()
.map(|v| v.as_list().ok().flatten())
.flatten()
.unwrap_or_default()
.iter()
.filter_map(|v| v.as_map().ok().flatten().clone())
.collect::<Vec<HashMap<String, ValueType>>>()
});
let primary_key = StoredValue::new(schema.get().primary_key.unwrap_or_default());
view! {
<Show
when=move || !list.get().is_empty()
fallback=|| view! { <div class="w-full text-center">"List is empty."</div> }
>
<For
each=move || list.get()
key=move |map| {
map.get(&primary_key.get_value())
.map(|v| v.to_string())
.unwrap_or_default()
}
children=move |map| {
view! {
<DataDisplay
schema=*schema.get().item_template.unwrap_or_default()
data_map=map
/>
}
}
/>
</Show>
}
.into_any()
}
SchemaType::Map => {
let map = Signal::derive(move || {
display_data
.get()
.map(|v| v.as_map().ok().flatten())
.flatten()
});
view! {
<Show
when=move || map.get().is_some()
fallback=|| view! { <div class="text-red-500">"Map was not found."</div> }
>
<For
each=move || schema.get().children
key=|(key, _)| key.clone()
children=move |(key, schema)| {
view! {
<DataDisplay
schema=schema
data_member=key
data_map=Signal::derive(move || map.get().unwrap_or_default())
/>
}
}
/>
</Show>
}
.into_any()
}
}
}