Initial commit

This commit is contained in:
Thomas Gideon 2020-05-28 14:09:25 -04:00
commit 96b6152d7b
20 changed files with 914 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target
Cargo.lock

20
Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "bootstrap-rs"
version = "0.1.0"
authors = ["Thomas Gideon <cmdln@thecommandline.net>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
yew = "~0.16.2"
log = "~0.4.8"
serde_json = "^1.0.40"
serde = "^1.0.98"
anyhow = "^1.0.28"
wasm-bindgen = "~0.2.61"
web-sys = "~0.3.38"
wasm-logger = "~0.2.0"
yew-router = "~0.13.0"
yew-components = "~0.1.2"

46
src/breadcrumb/item.rs Normal file
View file

@ -0,0 +1,46 @@
use crate::prelude::*;
use yew::{html::Children, prelude::*};
pub struct BreadcrumbItem {
props: Props,
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
pub active: bool,
#[prop_or_default]
pub children: Children,
}
impl Component for BreadcrumbItem {
type Properties = Props;
type Message = ();
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
if self.props.active {
html! {
<li class="breadcrumb-item active" aria-current="page">
{ self.props.children.render() }
</li>
}
} else {
html! {
<li class="breadcrumb-item">
{ self.props.children.render() }
</li>
}
}
}
}

77
src/breadcrumb/mod.rs Normal file
View file

@ -0,0 +1,77 @@
mod item;
use crate::prelude::*;
pub use item::BreadcrumbItem;
use yew::{html::Children, prelude::*};
pub struct Breadcrumb {
props: Props,
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
#[prop_or_default]
pub border: Option<Border>,
#[prop_or_default]
pub borders: Vec<Border>,
#[prop_or_default]
pub border_color: Option<BorderColor>,
#[prop_or_default]
pub border_colors: Vec<BorderColor>,
#[prop_or_default]
pub margin: Option<Margin>,
#[prop_or_default]
pub margins: Vec<Margin>,
#[prop_or_default]
pub class: String,
#[prop_or_default]
pub style: String,
#[prop_or_default]
pub children: Children,
}
impl Component for Breadcrumb {
type Properties = Props;
type Message = ();
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
html! {
<nav
class=self.props.class.clone()
aria-label="breadcrumb"
style=self.props.style.clone()
>
<ol
class="breadcrumb"
>
{ self.props.children.render() }
</ol>
</nav>
}
}
}
impl<'a> From<&'a Props> for BootstrapProps<'a> {
fn from(props: &Props) -> BootstrapProps {
let borders = collect_bs(&props.border, &props.borders);
let border_colors = collect_bs(&props.border_color, &props.border_colors);
let margins = collect_bs(&props.margin, &props.margins);
BootstrapProps {
borders,
border_colors,
margins,
}
}
}

67
src/button/mod.rs Normal file
View file

@ -0,0 +1,67 @@
use crate::prelude::*;
use yew::{html::Children, prelude::*};
pub struct ButtonGroup {
props: Props,
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
#[prop_or_default]
pub class: String,
#[prop_or_default]
pub style: String,
#[prop_or_default]
pub role: String,
#[prop_or_default]
pub aria_label: String,
#[prop_or_default]
pub children: Children,
}
impl Component for ButtonGroup {
type Properties = Props;
type Message = ();
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
html! {
<div class=self.class()
style=self.style()
role=self.props.role.clone()
aria-label=self.props.aria_label.clone()
>
{ self.props.children.render() }
</div>
}
}
}
impl ButtonGroup {
fn class(&self) -> String {
if self.props.class.is_empty() {
"btn-grp".into()
} else {
format!("btn-group {}", self.props.class)
}
}
fn style(&self) -> &str {
if self.props.style.is_empty() {
""
} else {
&self.props.style
}
}
}

59
src/card/body.rs Normal file
View file

@ -0,0 +1,59 @@
use crate::prelude::*;
use yew::{html::Children, prelude::*};
pub struct CardBody {
props: Props,
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
#[prop_or_default]
pub class: String,
#[prop_or_default]
pub style: String,
#[prop_or_default]
pub children: Children,
}
impl Component for CardBody {
type Properties = Props;
type Message = ();
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
html! {
<div class=self.class() style=self.style()>
{ self.props.children.render() }
</div>
}
}
}
impl CardBody {
fn class(&self) -> String {
if self.props.class.is_empty() {
"card-body".into()
} else {
format!("card-body {}", self.props.class)
}
}
fn style(&self) -> &str {
if self.props.style.is_empty() {
""
} else {
&self.props.style
}
}
}

59
src/card/header.rs Normal file
View file

@ -0,0 +1,59 @@
use crate::prelude::*;
use yew::{html::Children, prelude::*};
pub struct CardHeader {
props: Props,
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
#[prop_or_default]
pub class: String,
#[prop_or_default]
pub style: String,
#[prop_or_default]
pub children: Children,
}
impl Component for CardHeader {
type Properties = Props;
type Message = ();
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
html! {
<p class=self.class() style=self.style()>
{ self.props.children.render() }
</p>
}
}
}
impl CardHeader {
fn class(&self) -> String {
if self.props.class.is_empty() {
"card-header".into()
} else {
format!("card-header {}", self.props.class)
}
}
fn style(&self) -> &str {
if self.props.style.is_empty() {
""
} else {
&self.props.style
}
}
}

93
src/card/mod.rs Normal file
View file

@ -0,0 +1,93 @@
mod body;
mod header;
mod text;
pub use self::{body::CardBody, header::CardHeader, text::CardText};
use crate::prelude::*;
use yew::{html::Children, prelude::*};
pub struct Card {
props: Props,
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
#[prop_or_default]
pub margin: Option<Margin>,
#[prop_or_default]
pub margins: Vec<Margin>,
#[prop_or_default]
pub border: Option<Border>,
#[prop_or_default]
pub borders: Vec<Border>,
#[prop_or_default]
pub border_color: Option<BorderColor>,
#[prop_or_default]
pub border_colors: Vec<BorderColor>,
#[prop_or_default]
pub class: String,
#[prop_or_default]
pub style: String,
#[prop_or_default]
pub children: Children,
}
impl Component for Card {
type Properties = Props;
type Message = ();
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
html! {
<div class=self.class() style=self.style()>
{ self.props.children.render() }
</div>
}
}
}
impl Card {
fn class(&self) -> String {
if self.props.class.is_empty() {
format!("card {}", calculate_classes((&self.props).into()))
} else {
format!(
"card {} {}",
self.props.class,
calculate_classes((&self.props).into())
)
}
}
fn style(&self) -> &str {
if self.props.style.is_empty() {
""
} else {
&self.props.style
}
}
}
impl<'a> From<&'a Props> for BootstrapProps<'a> {
fn from(props: &'a Props) -> Self {
let borders = collect_bs(&props.border, &props.borders);
let border_colors = collect_bs(&props.border_color, &props.border_colors);
let margins = collect_bs(&props.margin, &props.margins);
Self {
borders,
border_colors,
margins,
}
}
}

59
src/card/text.rs Normal file
View file

@ -0,0 +1,59 @@
use crate::prelude::*;
use yew::{html::Children, prelude::*};
pub struct CardText {
props: Props,
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
#[prop_or_default]
pub class: String,
#[prop_or_default]
pub style: String,
#[prop_or_default]
pub children: Children,
}
impl Component for CardText {
type Properties = Props;
type Message = ();
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
html! {
<p class=self.class() style=self.style()>
{ self.props.children.render() }
</p>
}
}
}
impl CardText {
fn class(&self) -> String {
if self.props.class.is_empty() {
"card-text".into()
} else {
format!("card-text {}", self.props.class)
}
}
fn style(&self) -> &str {
if self.props.style.is_empty() {
""
} else {
&self.props.style
}
}
}

37
src/container.rs Normal file
View file

@ -0,0 +1,37 @@
use crate::prelude::*;
use yew::{html::Children, prelude::*};
pub struct Container {
props: Props,
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
#[prop_or_default]
pub children: Children,
}
impl Component for Container {
type Properties = Props;
type Message = ();
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
html! {
<div class="container">
{ self.props.children.render() }
</div>
}
}
}

57
src/input/group.rs Normal file
View file

@ -0,0 +1,57 @@
use crate::prelude::*;
use yew::{html::Children, prelude::*};
pub struct InputGroup {
props: Props,
}
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
#[prop_or_default]
pub margin: Option<Margin>,
#[prop_or_default]
pub margins: Vec<Margin>,
#[prop_or_default]
pub class: String,
#[prop_or_default]
pub children: Children,
}
impl<'a> From<&'a Props> for BootstrapProps<'a> {
fn from(props: &'a Props) -> BootstrapProps<'a> {
let borders = Vec::new();
let border_colors = Vec::new();
let margins = collect_bs(&props.margin, &props.margins);
Self {
borders,
border_colors,
margins,
}
}
}
impl Component for InputGroup {
type Message = ();
type Properties = Props;
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
let classes = calculate_classes((&self.props).into());
html! {
<div class=format!("input-group {} {}", classes, self.props.class)>
{ self.props.children.render() }
</div>
}
}
}

88
src/input/mod.rs Normal file
View file

@ -0,0 +1,88 @@
mod group;
mod textarea;
pub use self::{group::InputGroup, textarea::TextArea};
use crate::prelude::*;
use yew::prelude::*;
#[derive(Clone, PartialEq)]
pub enum InputType {
Text,
}
impl InputType {
fn as_str(&self) -> &str {
match self {
Self::Text => "text",
}
}
}
pub struct Input {
link: ComponentLink<Self>,
state: String,
props: Props,
}
#[derive(Debug)]
pub struct InputChange(ChangeData);
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
#[prop_or_default]
pub name: String,
#[prop_or_default]
pub id: String,
pub on_signal: Callback<String>,
pub input_type: InputType,
#[prop_or_default]
pub readonly: bool,
#[prop_or_default]
pub value: String,
}
impl Component for Input {
type Message = InputChange;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let state = props.value.clone();
Self { props, state, link }
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
if let InputChange(ChangeData::Value(value)) = msg {
self.state = value.clone();
self.props.on_signal.emit(value);
true
} else {
false
}
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
if self.props.value != props.value {
self.state = props.value.clone();
}
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
let class = if self.props.readonly {
"form-control-plaintext pl-3"
} else {
"form-control"
};
html! {
<input
name=&self.props.name
id=&self.props.id
type=self.props.input_type.as_str()
class=class
value=&self.state
readonly=self.props.readonly
onchange=self.link.callback(|evt| InputChange(evt))
/>
}
}
}

57
src/input/textarea.rs Normal file
View file

@ -0,0 +1,57 @@
use crate::prelude::*;
use yew::prelude::*;
pub struct TextArea {
link: ComponentLink<Self>,
state: String,
props: Props,
}
#[derive(Debug)]
pub struct InputChange(ChangeData);
#[derive(Properties, Clone, PartialEq)]
pub struct Props {
#[prop_or_default]
pub name: String,
#[prop_or_default]
pub id: String,
pub on_signal: Callback<String>,
}
impl Component for TextArea {
type Message = InputChange;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let state = String::default();
Self { props, state, link }
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
if let InputChange(ChangeData::Value(value)) = msg {
self.state = value.clone();
self.props.on_signal.emit(value);
true
} else {
false
}
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
render_on_change(&mut self.props, props)
}
fn view(&self) -> Html {
html! {
<textarea
name=&self.props.name
id=&self.props.id
class="form-control"
onchange=self.link.callback(|evt| InputChange(evt))
>
{ &self.state }
</textarea>
}
}
}

43
src/jumbotron.rs Normal file
View file

@ -0,0 +1,43 @@
use crate::prelude::*;
use yew::{html::Children, prelude::*};
pub struct Jumbotron {
props: Props,
}
#[derive(Properties, Clone)]
pub struct Props {
#[prop_or_default]
pub margin: Option<Margin>,
#[prop_or_default]
pub margins: Vec<Margin>,
#[prop_or_default]
pub class: String,
#[prop_or_default]
pub children: Children,
}
impl Component for Jumbotron {
type Properties = Props;
type Message = ();
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<div class=format!("jumbotron {}", self.props.class)>
{ self.props.children.render() }
</div>
}
}
}

16
src/lib.rs Normal file
View file

@ -0,0 +1,16 @@
mod breadcrumb;
mod button;
mod card;
mod container;
pub mod input;
mod jumbotron;
pub mod prelude;
pub use self::{
breadcrumb::{Breadcrumb, BreadcrumbItem},
button::ButtonGroup,
card::{Card, CardBody, CardHeader, CardText},
container::Container,
input::{Input, InputGroup, TextArea},
jumbotron::Jumbotron,
};

20
src/prelude/border.rs Normal file
View file

@ -0,0 +1,20 @@
#[derive(Clone, PartialEq)]
pub enum Border {
All,
Top,
Right,
Bottom,
Left,
}
impl super::BootstrapClass for Border {
fn as_classname(&self) -> String {
match self {
Self::All => "border".into(),
Self::Top => "border-top".into(),
Self::Right => "border-right".into(),
Self::Bottom => "border-bottom".into(),
Self::Left => "border-left".into(),
}
}
}

View file

@ -0,0 +1,22 @@
#[derive(Clone, PartialEq)]
pub enum BorderColor {
Primary,
Secondary,
Unset,
}
impl Default for BorderColor {
fn default() -> Self {
BorderColor::Unset
}
}
impl super::BootstrapClass for BorderColor {
fn as_classname(&self) -> String {
match self {
Self::Primary => "border-primary".into(),
Self::Secondary => "border-secondary".into(),
Self::Unset => "".into(),
}
}
}

8
src/prelude/margin.rs Normal file
View file

@ -0,0 +1,8 @@
#[derive(Debug, Clone, PartialEq)]
pub struct Margin(pub super::Edge, pub usize);
impl super::BootstrapClass for Margin {
fn as_classname(&self) -> String {
format!("m{}-{}", self.0, self.1)
}
}

76
src/prelude/mod.rs Normal file
View file

@ -0,0 +1,76 @@
mod border;
mod border_color;
mod margin;
pub use self::{border::Border, border_color::BorderColor, margin::Margin};
use std::fmt::{Display, Formatter, Result};
use yew::prelude::*;
#[derive(Debug, Clone, PartialEq)]
pub enum Edge {
Top,
Right,
Bottom,
Left,
}
impl Display for Edge {
fn fmt(&self, fmt: &mut Formatter) -> Result {
match self {
Self::Top => write!(fmt, "t"),
Self::Bottom => write!(fmt, "b"),
Self::Right => write!(fmt, "r"),
Self::Left => write!(fmt, "l"),
}
}
}
trait BootstrapClass {
fn as_classname(&self) -> String;
}
pub struct BootstrapProps<'a> {
pub borders: Vec<&'a Border>,
pub border_colors: Vec<&'a BorderColor>,
pub margins: Vec<&'a Margin>,
}
pub fn render_on_change<P: Properties + PartialEq>(
props_on_comp: &mut P,
props: P,
) -> ShouldRender {
if props_on_comp == &props {
false
} else {
*props_on_comp = props;
true
}
}
pub fn collect_bs<'a, T>(t: &'a Option<T>, ts: &'a [T]) -> Vec<&'a T> {
if let Some(t) = t.as_ref() {
let mut r = vec![t];
r.append(&mut ts.iter().collect());
r
} else {
ts.iter().collect()
}
}
pub fn calculate_classes(props: BootstrapProps) -> String {
let BootstrapProps {
borders,
border_colors,
margins,
} = props;
let mut classes = Vec::new();
classes.append(&mut into_classnames(borders));
classes.append(&mut into_classnames(border_colors));
classes.append(&mut into_classnames(margins));
classes.join(" ")
}
fn into_classnames<C: BootstrapClass>(c: Vec<&C>) -> Vec<String> {
c.into_iter().map(|c| c.as_classname()).collect()
}

8
src/prelude/padding.rs Normal file
View file

@ -0,0 +1,8 @@
#[derive(Debug, Clone, PartialEq)]
pub struct Padding(pub super::Edge, pub usize);
impl super::BootstrapClass for Padding {
fn as_classname(&self) -> String {
format!("m{}-{}", self.0, self.1)
}
}