From 58bd494368f339a669b2b13b8b3682b367b5de7b Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 27 Jan 2022 21:29:17 -0500 Subject: [PATCH] Set context globally and separate components --- web/src/main.rs | 4 ++ web/src/root.rs | 48 ++++++++++++++ web/src/service.rs | 69 +++++++++++++++++++++ web/src/web.rs | 151 +++++++++++++-------------------------------- 4 files changed, 163 insertions(+), 109 deletions(-) create mode 100644 web/src/root.rs create mode 100644 web/src/service.rs diff --git a/web/src/main.rs b/web/src/main.rs index a135ec4..3d7416e 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -12,7 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. mod typings; +mod service; mod web; +mod root; +//mod recipe; +//mod menu; use sycamore::prelude::*; use web::UI; diff --git a/web/src/root.rs b/web/src/root.rs new file mode 100644 index 0000000..a3df00c --- /dev/null +++ b/web/src/root.rs @@ -0,0 +1,48 @@ +// Copyright 2022 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::console_log; +use crate::service::AppService; + +use sycamore::{context::use_context, prelude::*}; + +#[component(Start)] +pub fn start() -> View { + view! { + div { "hello chefs!" } + RecipeList() + } +} + +/// Component to list available recipes. +#[component(RecipeList)] +pub fn recipe_list() -> View { + let app_service = use_context::(); + + let titles = create_memo(cloned!(app_service => move || { + app_service.get_recipes().get().iter().map(|(i, r)| (*i, r.title.clone())).collect::>() + })); + view! { + ul(class="recipe_list") { + Keyed(KeyedProps{ + iterable: titles, + template: |(i, title)| { + view! { li(on:click=move |_| { + console_log!("clicked item with index: {}", i) + }) { (title) } } + }, + key: |(i, title)| (*i, title.clone()), + }) + } + } +} diff --git a/web/src/service.rs b/web/src/service.rs new file mode 100644 index 0000000..0b55339 --- /dev/null +++ b/web/src/service.rs @@ -0,0 +1,69 @@ +// Copyright 2022 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::{console_debug, console_error}; + +use reqwasm::http; +use sycamore::prelude::*; + +use recipes::{parse, Recipe}; + +#[derive(Clone)] +pub struct AppService { + recipes: Signal>, +} + +impl AppService { + pub fn new() -> Self { + Self { + recipes: Signal::new(Vec::new()), + } + } + + pub async fn fetch_recipes() -> Result, String> { + let resp = match http::Request::get("/api/v1/recipes").send().await { + Ok(resp) => resp, + Err(e) => return Err(format!("Error: {}", e)), + }; + if resp.status() != 200 { + return Err(format!("Status: {}", resp.status())); + } else { + console_debug!("We got a valid response back!"); + let recipe_list = match resp.json::>().await { + Ok(recipes) => recipes, + Err(e) => return Err(format!("Eror getting recipe list as json {}", e)), + }; + let mut parsed_list = Vec::new(); + for r in recipe_list { + let recipe = match parse::as_recipe(&r) { + Ok(r) => r, + Err(e) => { + console_error!("Error parsing recipe {}", e); + break; + } + }; + console_debug!("We parsed a recipe {}", recipe.title); + parsed_list.push(recipe); + } + return Ok(parsed_list.drain(0..).enumerate().collect()); + } + } + + pub fn get_recipes(&self) -> Signal> { + self.recipes.clone() + } + + pub fn set_recipes(&mut self, recipes: Vec<(usize, Recipe)>) { + self.recipes.set(recipes); + } +} diff --git a/web/src/web.rs b/web/src/web.rs index c80aeac..f7b50d0 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -12,87 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. use crate::{console_debug, console_error, console_log}; -use reqwasm::http; -use sycamore::context::{use_context, ContextProvider, ContextProviderProps}; -use sycamore::futures::spawn_local_in_scope; -use sycamore::prelude::*; +use crate::{root, service::AppService}; + +use sycamore::{ + context::{ContextProvider, ContextProviderProps}, + futures::spawn_local_in_scope, + prelude::*, +}; use sycamore_router::{HistoryIntegration, Route, Router, RouterProps}; -use recipes::{parse, Recipe}; - -#[derive(Clone)] -struct AppService { - recipes: Signal>, -} - -impl AppService { - fn new() -> Self { - Self { - recipes: Signal::new(Vec::new()), - } - } - - async fn fetch_recipes() -> Result, String> { - let resp = match http::Request::get("/api/v1/recipes").send().await { - Ok(resp) => resp, - Err(e) => return Err(format!("Error: {}", e)), - }; - if resp.status() != 200 { - return Err(format!("Status: {}", resp.status())); - } else { - console_debug!("We got a valid response back!"); - let recipe_list = match resp.json::>().await { - Ok(recipes) => recipes, - Err(e) => return Err(format!("Eror getting recipe list as json {}", e)), - }; - let mut parsed_list = Vec::new(); - for r in recipe_list { - let recipe = match parse::as_recipe(&r) { - Ok(r) => r, - Err(e) => { - console_error!("Error parsing recipe {}", e); - break; - } - }; - console_debug!("We parsed a recipe {}", recipe.title); - parsed_list.push(recipe); - } - return Ok(parsed_list.drain(0..).enumerate().collect()); - } - } - - fn get_recipes(&self) -> Signal> { - self.recipes.clone() - } - - fn set_recipes(&mut self, recipes: Vec<(usize, Recipe)>) { - self.recipes.set(recipes); - } -} - -/// Component to list available recipes. -#[component(RecipeList)] -fn recipe_list() -> View { - let app_service = use_context::(); - - let titles = create_memo(cloned!(app_service => move || { - app_service.get_recipes().get().iter().map(|(i, r)| (*i, r.title.clone())).collect::>() - })); - view! { - ul { - Keyed(KeyedProps{ - iterable: titles, - template: |(i, title)| { - view! { li(on:click=move |_| { - console_log!("clicked item with index: {}", i) - }) { (title) } } - }, - key: |(i, title)| (*i, title.clone()), - }) - } - } -} - #[derive(Route, Debug)] enum AppRoutes { #[to("/ui")] @@ -123,36 +51,41 @@ pub fn ui() -> View { }); })); view! { - Router(RouterProps::new(HistoryIntegration::new(), move |routes: ReadSignal| { - let t = create_memo(cloned!((app_service) => move || { - console_debug!("Determining route."); - let route = routes.get(); - console_debug!("Route {:?}", route); - match route.as_ref() { - AppRoutes::Root => view! { - div { "hello chefs!" } - ContextProvider(ContextProviderProps { - value: app_service.clone(), - children: || view! { RecipeList() } - }) - }, - AppRoutes::Recipe{index:_idx} => view! { - "TODO!!" - }, - AppRoutes::Menu => view! { - "TODO!!" - }, - AppRoutes::NotFound => view! { - "NotFound" - } + // NOTE(jwall): Set the app_service in our toplevel scope. Children will be able + // to find the service as long as they are a child of this scope. + ContextProvider(ContextProviderProps { + value: app_service.clone(), + children: || view! { + Router(RouterProps::new(HistoryIntegration::new(), move |routes: ReadSignal| { + let t = create_memo(move || { + console_debug!("Determining route."); + let route = routes.get(); + console_debug!("Route {:?}", route); + match route.as_ref() { + AppRoutes::Root => view! { + root::Start() + }, + AppRoutes::Recipe{index:_idx} => view! { + "TODO!!" + }, + AppRoutes::Menu => view! { + "TODO!!" + }, + AppRoutes::NotFound => view! { + "NotFound" + } + } + }); + console_debug!("Created our route view memo."); + view! { + // NOTE(jwall): The Router component *requires* there to be exactly one node as the root of this view. + // No fragments or missing nodes allowed or it will panic at runtime. + div(class="app") { + (t.get().as_ref().clone()) + } + } + })) } - })); - console_debug!("Created our route view memo."); - view! { - div(class="app") { - (t.get().as_ref().clone()) - } - } - })) + }) } }