From db98a5ac0a7a84d9642670da5a6a183fac3c5471 Mon Sep 17 00:00:00 2001 From: John Cuviello <jcuviell@uci.edu> Date: Tue, 11 Jul 2023 21:43:39 -0700 Subject: [PATCH 1/4] refactored docs for 'reacting to input with state' --- .../conditional_form_component.css | 3 + .../multiple_form_components.css | 13 + .../css/managing_state/picture_component.css | 28 + .../managing_state/all_possible_states.py | 9 + .../alt_stateful_picture_component.py | 38 ++ .../managing_state/basic_form_component.py | 18 + .../conditional_form_component.py | 45 ++ .../multiple_form_components.py | 18 + .../python/managing_state/necessary_states.py | 6 + .../managing_state/picture_component.py | 16 + .../managing_state/refactored_states.py | 7 + .../managing_state/stateful_form_component.py | 69 +++ .../stateful_picture_component.py | 33 ++ .../src/learn/reacting-to-input-with-state.md | 491 +++++------------- 14 files changed, 437 insertions(+), 357 deletions(-) create mode 100644 docs/examples/css/managing_state/conditional_form_component.css create mode 100644 docs/examples/css/managing_state/multiple_form_components.css create mode 100644 docs/examples/css/managing_state/picture_component.css create mode 100644 docs/examples/python/managing_state/all_possible_states.py create mode 100644 docs/examples/python/managing_state/alt_stateful_picture_component.py create mode 100644 docs/examples/python/managing_state/basic_form_component.py create mode 100644 docs/examples/python/managing_state/conditional_form_component.py create mode 100644 docs/examples/python/managing_state/multiple_form_components.py create mode 100644 docs/examples/python/managing_state/necessary_states.py create mode 100644 docs/examples/python/managing_state/picture_component.py create mode 100644 docs/examples/python/managing_state/refactored_states.py create mode 100644 docs/examples/python/managing_state/stateful_form_component.py create mode 100644 docs/examples/python/managing_state/stateful_picture_component.py diff --git a/docs/examples/css/managing_state/conditional_form_component.css b/docs/examples/css/managing_state/conditional_form_component.css new file mode 100644 index 000000000..98c7d8768 --- /dev/null +++ b/docs/examples/css/managing_state/conditional_form_component.css @@ -0,0 +1,3 @@ +.Error { + color: red; +} \ No newline at end of file diff --git a/docs/examples/css/managing_state/multiple_form_components.css b/docs/examples/css/managing_state/multiple_form_components.css new file mode 100644 index 000000000..db8da7178 --- /dev/null +++ b/docs/examples/css/managing_state/multiple_form_components.css @@ -0,0 +1,13 @@ +section { + border-bottom: 1px solid #aaa; + padding: 20px; +} +h4 { + color: #222; +} +body { + margin: 0; +} +.Error { + color: red; +} \ No newline at end of file diff --git a/docs/examples/css/managing_state/picture_component.css b/docs/examples/css/managing_state/picture_component.css new file mode 100644 index 000000000..85827067c --- /dev/null +++ b/docs/examples/css/managing_state/picture_component.css @@ -0,0 +1,28 @@ +body { + margin: 0; + padding: 0; + height: 250px; +} + +.background { + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background: #eee; +} + +.background--active { + background: #a6b5ff; +} + +.picture { + width: 200px; + height: 200px; + border-radius: 10px; +} + +.picture--active { + border: 5px solid #a6b5ff; +} diff --git a/docs/examples/python/managing_state/all_possible_states.py b/docs/examples/python/managing_state/all_possible_states.py new file mode 100644 index 000000000..c93e7df13 --- /dev/null +++ b/docs/examples/python/managing_state/all_possible_states.py @@ -0,0 +1,9 @@ +from reactpy import hooks + + +# start +is_empty, set_is_empty = hooks.use_state(True) +is_typing, set_is_typing = hooks.use_state(False) +is_submitting, set_is_submitting = hooks.use_state(False) +is_success, set_is_success = hooks.use_state(False) +is_error, set_is_error = hooks.use_state(False) diff --git a/docs/examples/python/managing_state/alt_stateful_picture_component.py b/docs/examples/python/managing_state/alt_stateful_picture_component.py new file mode 100644 index 000000000..dfaf8f2d2 --- /dev/null +++ b/docs/examples/python/managing_state/alt_stateful_picture_component.py @@ -0,0 +1,38 @@ +from reactpy import component, event, hooks, html + + +# start +@component +def picture(): + is_active, set_is_active = hooks.use_state(False) + + if (is_active): + return html.div( + { + "class_name": "background", + "on_click": lambda event: set_is_active(False) + }, + html.img( + { + "on_click": event(stop_propagation=True), + "class_name": "picture picture--active", + "alt": "Rainbow houses in Kampung Pelangi, Indonesia", + "src": "https://i.imgur.com/5qwVYb1.jpeg" + } + ) + ) + else: + return html.div( + { + "class_name": "background background--active" + }, + html.img( + { + "on_click": event(lambda event: set_is_active(True), + stop_propagation=True), + "class_name": "picture", + "alt": "Rainbow houses in Kampung Pelangi, Indonesia", + "src": "https://i.imgur.com/5qwVYb1.jpeg" + } + ) + ) \ No newline at end of file diff --git a/docs/examples/python/managing_state/basic_form_component.py b/docs/examples/python/managing_state/basic_form_component.py new file mode 100644 index 000000000..04e57c240 --- /dev/null +++ b/docs/examples/python/managing_state/basic_form_component.py @@ -0,0 +1,18 @@ +from reactpy import component, html + + +# start +@component +def form(status="empty"): + if status == "success": + return html.h1("That's right!") + else: + return html._( + html.h2("City quiz"), + html.p("In which city is there a billboard that turns air into drinkable water?"), + html.form( + html.textarea(), + html.br(), + html.button("Submit") + ) + ) \ No newline at end of file diff --git a/docs/examples/python/managing_state/conditional_form_component.py b/docs/examples/python/managing_state/conditional_form_component.py new file mode 100644 index 000000000..0b34d38b4 --- /dev/null +++ b/docs/examples/python/managing_state/conditional_form_component.py @@ -0,0 +1,45 @@ +from reactpy import component, html + + +# start +@component +def error(status): + if status == "error": + return html.p( + {"class_name": "error"}, + "Good guess but a wrong answer. Try again!" + ) + else: + return "" + + +@component +def form(status="empty"): + # Try status="submitting", "error", "success" + if status == "success": + return html.h1("That's right!") + else: + return html._( + html.h2("City quiz"), + html.p( + "In which city is there a billboard that turns air into \ + drinkable water?" + ), + html.form( + html.textarea( + { + "disabled": "True" if status == "submitting" + else "False" + } + ), + html.br(), + html.button( + { + "disabled": True if status == "empty" + or status == "submitting" else "False" + }, + "Submit" + ), + error(status) + ) + ) \ No newline at end of file diff --git a/docs/examples/python/managing_state/multiple_form_components.py b/docs/examples/python/managing_state/multiple_form_components.py new file mode 100644 index 000000000..0648f5dd2 --- /dev/null +++ b/docs/examples/python/managing_state/multiple_form_components.py @@ -0,0 +1,18 @@ +from reactpy import component, html +from conditional_form_component import form + + +# start +@component +def item(status): + return html.section( + html.h4("Form", status, ':'), + form(status) + ) + + +@component +def app(): + statuses = ["empty", "typing", "submitting", "success", "error"] + status_list = [item(status) for status in statuses] + return html._(status_list) diff --git a/docs/examples/python/managing_state/necessary_states.py b/docs/examples/python/managing_state/necessary_states.py new file mode 100644 index 000000000..53302a251 --- /dev/null +++ b/docs/examples/python/managing_state/necessary_states.py @@ -0,0 +1,6 @@ +from reactpy import hooks + + +# start +answer, set_answer = hooks.use_state("") +error, set_error = hooks.use_state(None) diff --git a/docs/examples/python/managing_state/picture_component.py b/docs/examples/python/managing_state/picture_component.py new file mode 100644 index 000000000..9c1c5ebf8 --- /dev/null +++ b/docs/examples/python/managing_state/picture_component.py @@ -0,0 +1,16 @@ +from reactpy import component, html + + +# start +@component +def picture(): + return html.div( + {"class_name": "background background--active"}, + html.img( + { + "class_name": "picture", + "alt": "Rainbow houses in Kampung Pelangi, Indonesia", + "src": "https://i.imgur.com/5qwVYb1.jpeg" + } + ) + ) \ No newline at end of file diff --git a/docs/examples/python/managing_state/refactored_states.py b/docs/examples/python/managing_state/refactored_states.py new file mode 100644 index 000000000..1448764d4 --- /dev/null +++ b/docs/examples/python/managing_state/refactored_states.py @@ -0,0 +1,7 @@ +from reactpy import hooks + + +# start +answer, set_answer = hooks.use_state("") +error, set_error = hooks.use_state(None) +status, set_status = hooks.use_state("typing") # 'typing', 'submitting', or 'success' \ No newline at end of file diff --git a/docs/examples/python/managing_state/stateful_form_component.py b/docs/examples/python/managing_state/stateful_form_component.py new file mode 100644 index 000000000..61d732611 --- /dev/null +++ b/docs/examples/python/managing_state/stateful_form_component.py @@ -0,0 +1,69 @@ +from reactpy import component, event, html, hooks +import asyncio + + +async def submit_form(): + await asyncio.wait(5) + + +# start +@component +def error_msg(error): + if error: + return html.p( + {"class_name": "error"}, + "Good guess but a wrong answer. Try again!" + ) + else: + return "" + + +@component +def form(status="empty"): + answer, set_answer = hooks.use_state("") + error, set_error = hooks.use_state(None) + status, set_status = hooks.use_state("typing") + + @event(prevent_default=True) + async def handle_submit(event): + set_status("submitting") + try: + await submit_form(answer) + set_status("success") + except Exception: + set_status("typing") + set_error(Exception) + + @event() + def handle_textarea_change(event): + set_answer(event["target"]["value"]) + + if status == "success": + return html.h1("That's right!") + else: + return html._( + html.h2("City quiz"), + html.p( + "In which city is there a billboard \ + that turns air into drinkable water?" + ), + html.form( + {"on_submit": handle_submit}, + html.textarea( + { + "value": answer, + "on_change": handle_textarea_change, + "disabled": True if status == "submitting" else "False" + } + ), + html.br(), + html.button( + { + "disabled": True if status == "empty" + or status == "submitting" else "False" + }, + "Submit" + ), + error_msg(error) + ) + ) \ No newline at end of file diff --git a/docs/examples/python/managing_state/stateful_picture_component.py b/docs/examples/python/managing_state/stateful_picture_component.py new file mode 100644 index 000000000..6a7879535 --- /dev/null +++ b/docs/examples/python/managing_state/stateful_picture_component.py @@ -0,0 +1,33 @@ +from reactpy import component, event, hooks, html + + +# start +@component +def picture(): + is_active, set_is_active = hooks.use_state(False) + background_class_name = "background" + picture_class_name = "picture" + + if (is_active): + picture_class_name += " picture--active" + else: + background_class_name += " background--active" + + @event(stop_propagation=True) + def handle_click(event): + set_is_active(True) + + return html.div( + { + "class_name": background_class_name, + "on_click": set_is_active(False) + }, + html.img( + { + "on_click": handle_click, + "class_name": picture_class_name, + "alt": "Rainbow houses in Kampung Pelangi, Indonesia", + "src": "https://i.imgur.com/5qwVYb1.jpeg" + } + ) + ) diff --git a/docs/src/learn/reacting-to-input-with-state.md b/docs/src/learn/reacting-to-input-with-state.md index ac04c1d98..2e81aaf8f 100644 --- a/docs/src/learn/reacting-to-input-with-state.md +++ b/docs/src/learn/reacting-to-input-with-state.md @@ -141,7 +141,7 @@ You've seen how to implement a form imperatively above. To better understand how 1. **Identify** your component's different visual states 2. **Determine** what triggers those state changes -3. **Represent** the state in memory using `useState` +3. **Represent** the state in memory using `use_state` 4. **Remove** any non-essential state variables 5. **Connect** the event handlers to set the state @@ -159,69 +159,35 @@ First, you need to visualize all the different "states" of the UI the user might Just like a designer, you'll want to "mock up" or create "mocks" for the different states before you add logic. For example, here is a mock for just the visual part of the form. This mock is controlled by a prop called `status` with a default value of `'empty'`: -```js -export default function Form({ status = "empty" }) { - if (status === "success") { - return <h1>That's right!</h1>; - } - return ( - <> - <h2>City quiz</h2> - <p> - In which city is there a billboard that turns air into drinkable - water? - </p> - <form> - <textarea /> - <br /> - <button>Submit</button> - </form> - </> - ); -} -``` +=== "app.py" + ```python + {% include "../../examples/python/managing_state/basic_form_component.py" start="# start" %} + ``` + +=== ":material-play: Run" + ```python + # TODO + ``` You could call that prop anything you like, the naming is not important. Try editing `status = 'empty'` to `status = 'success'` to see the success message appear. Mocking lets you quickly iterate on the UI before you wire up any logic. Here is a more fleshed out prototype of the same component, still "controlled" by the `status` prop: -```js -export default function Form({ - // Try 'submitting', 'error', 'success': - status = "empty", -}) { - if (status === "success") { - return <h1>That's right!</h1>; - } - return ( - <> - <h2>City quiz</h2> - <p> - In which city is there a billboard that turns air into drinkable - water? - </p> - <form> - <textarea disabled={status === "submitting"} /> - <br /> - <button - disabled={status === "empty" || status === "submitting"} - > - Submit - </button> - {status === "error" && ( - <p className="Error"> - Good guess but a wrong answer. Try again! - </p> - )} - </form> - </> - ); -} -``` +=== "app.py" -```css -.Error { - color: red; -} -``` + ```python + {% include "../../examples/python/managing_state/conditional_form_component.py" start="# start" %} + ``` + +=== "styles.css" + + ```css + {% include "../../examples/css/managing_state/conditional_form_component.css" %} + ``` + +=== ":material-play: Run" + + ```python + # TODO + ``` <DeepDive> @@ -229,62 +195,30 @@ export default function Form({ If a component has a lot of visual states, it can be convenient to show them all on one page: -```js -import Form from "./Form.js"; +=== "app.py" + + ```python + {% include "../../examples/python/managing_state/multiple_form_components.py" start="# start" %} + ``` -let statuses = ["empty", "typing", "submitting", "success", "error"]; +=== "form.py" + + ```python + {% include "../../examples/python/managing_state/conditional_form_component.py" start="# start" %} + ``` -export default function App() { - return ( - <> - {statuses.map((status) => ( - <section key={status}> - <h4>Form ({status}):</h4> - <Form status={status} /> - </section> - ))} - </> - ); -} -``` +=== "styles.css" -```js -export default function Form({ status }) { - if (status === "success") { - return <h1>That's right!</h1>; - } - return ( - <form> - <textarea disabled={status === "submitting"} /> - <br /> - <button disabled={status === "empty" || status === "submitting"}> - Submit - </button> - {status === "error" && ( - <p className="Error"> - Good guess but a wrong answer. Try again! - </p> - )} - </form> - ); -} -``` + ```css + {% include "../../examples/css/managing_state/multiple_form_components.css" %} + ``` -```css -section { - border-bottom: 1px solid #aaa; - padding: 20px; -} -h4 { - color: #222; -} -body { - margin: 0; -} -.Error { - color: red; -} -``` + +=== ":material-play: Run" + + ```python + # TODO + ``` Pages like this are often called "living styleguides" or "storybooks". @@ -321,26 +255,37 @@ To help visualize this flow, try drawing each state on paper as a labeled circle ### Step 3: Represent the state in memory with `useState` -Next you'll need to represent the visual states of your component in memory with [`useState`.](/reference/react/useState) Simplicity is key: each piece of state is a "moving piece", and **you want as few "moving pieces" as possible.** More complexity leads to more bugs! +Next you'll need to represent the visual states of your component in memory with [`use_state`.](/reference/react/useState) Simplicity is key: each piece of state is a "moving piece", and **you want as few "moving pieces" as possible.** More complexity leads to more bugs! Start with the state that _absolutely must_ be there. For example, you'll need to store the `answer` for the input, and the `error` (if it exists) to store the last error: -```js -const [answer, setAnswer] = useState(""); -const [error, setError] = useState(null); -``` +=== "app.py" + + ```python + {% include "../../examples/python/managing_state/necessary_states.py" start="# start" %} + ``` + +=== ":material-play: Run" + + ```python + # TODO + ``` Then, you'll need a state variable representing which one of the visual states that you want to display. There's usually more than a single way to represent that in memory, so you'll need to experiment with it. If you struggle to think of the best way immediately, start by adding enough state that you're _definitely_ sure that all the possible visual states are covered: -```js -const [isEmpty, setIsEmpty] = useState(true); -const [isTyping, setIsTyping] = useState(false); -const [isSubmitting, setIsSubmitting] = useState(false); -const [isSuccess, setIsSuccess] = useState(false); -const [isError, setIsError] = useState(false); -``` +=== "app.py" + + ```python + {% include "../../examples/python/managing_state/all_possible_states.py" start="# start" %} + ``` + +=== ":material-play: Run" + + ```python + # TODO + ``` Your first idea likely won't be the best, but that's ok--refactoring state is a part of the process! @@ -350,17 +295,23 @@ You want to avoid duplication in the state content so you're only tracking what Here are some questions you can ask about your state variables: -- **Does this state cause a paradox?** For example, `isTyping` and `isSubmitting` can't both be `true`. A paradox usually means that the state is not constrained enough. There are four possible combinations of two booleans, but only three correspond to valid states. To remove the "impossible" state, you can combine these into a `status` that must be one of three values: `'typing'`, `'submitting'`, or `'success'`. -- **Is the same information available in another state variable already?** Another paradox: `isEmpty` and `isTyping` can't be `true` at the same time. By making them separate state variables, you risk them going out of sync and causing bugs. Fortunately, you can remove `isEmpty` and instead check `answer.length === 0`. -- **Can you get the same information from the inverse of another state variable?** `isError` is not needed because you can check `error !== null` instead. +- **Does this state cause a paradox?** For example, `is_typing` and `is_submitting` can't both be `True`. A paradox usually means that the state is not constrained enough. There are four possible combinations of two booleans, but only three correspond to valid states. To remove the "impossible" state, you can combine these into a `status` that must be one of three values: `'typing'`, `'submitting'`, or `'success'`. +- **Is the same information available in another state variable already?** Another paradox: `is_empty` and `is_typing` can't be `true` at the same time. By making them separate state variables, you risk them going out of sync and causing bugs. Fortunately, you can remove `is_empty` and instead check `len(answer) == 0`. +- **Can you get the same information from the inverse of another state variable?** `is_error` is not needed because you can check `error != None` instead. After this clean-up, you're left with 3 (down from 7!) _essential_ state variables: -```js -const [answer, setAnswer] = useState(""); -const [error, setError] = useState(null); -const [status, setStatus] = useState("typing"); // 'typing', 'submitting', or 'success' -``` +=== "app.py" + + ```python + {% include "../../examples/python/managing_state/refactored_states.py" start="# start" %} + ``` + +=== ":material-play: Run" + + ```python + # TODO + ``` You know they are essential, because you can't remove any of them without breaking the functionality. @@ -376,79 +327,23 @@ These three variables are a good enough representation of this form's state. How Lastly, create event handlers that update the state. Below is the final form, with all event handlers wired up: -```js -import { useState } from "react"; +=== "app.py" -export default function Form() { - const [answer, setAnswer] = useState(""); - const [error, setError] = useState(null); - const [status, setStatus] = useState("typing"); + ```python + {% include "../../examples/python/managing_state/stateful_form_component.py" start="# start" %} + ``` - if (status === "success") { - return <h1>That's right!</h1>; - } +=== "styles.css" - async function handleSubmit(e) { - e.preventDefault(); - setStatus("submitting"); - try { - await submitForm(answer); - setStatus("success"); - } catch (err) { - setStatus("typing"); - setError(err); - } - } + ```css + {% include "../../examples/css/managing_state/conditional_form_component.css" %} + ``` - function handleTextareaChange(e) { - setAnswer(e.target.value); - } +=== ":material-play: Run" - return ( - <> - <h2>City quiz</h2> - <p> - In which city is there a billboard that turns air into drinkable - water? - </p> - <form onSubmit={handleSubmit}> - <textarea - value={answer} - onChange={handleTextareaChange} - disabled={status === "submitting"} - /> - <br /> - <button - disabled={answer.length === 0 || status === "submitting"} - > - Submit - </button> - {error !== null && <p className="Error">{error.message}</p>} - </form> - </> - ); -} - -function submitForm(answer) { - // Pretend it's hitting the network. - return new Promise((resolve, reject) => { - setTimeout(() => { - let shouldError = answer.toLowerCase() !== "lima"; - if (shouldError) { - reject(new Error("Good guess but a wrong answer. Try again!")); - } else { - resolve(); - } - }, 1500); - }); -} -``` - -```css -.Error { - color: red; -} -``` + ```python + # TODO + ``` Although this code is longer than the original imperative example, it is much less fragile. Expressing all interactions as state changes lets you later introduce new visual states without breaking existing ones. It also lets you change what should be displayed in each state without changing the logic of the interaction itself. @@ -458,7 +353,7 @@ Although this code is longer than the original imperative example, it is much le - When developing a component: 1. Identify all its visual states. 2. Determine the human and computer triggers for state changes. - 3. Model the state with `useState`. + 3. Model the state with `use_state`. 4. Remove non-essential state to avoid bugs and paradoxes. 5. Connect the event handlers to set state. @@ -472,50 +367,23 @@ Make it so that clicking on the picture _removes_ the `background--active` CSS c Visually, you should expect that clicking on the picture removes the purple background and highlights the picture border. Clicking outside the picture highlights the background, but removes the picture border highlight. -```js -export default function Picture() { - return ( - <div className="background background--active"> - <img - className="picture" - alt="Rainbow houses in Kampung Pelangi, Indonesia" - src="https://i.imgur.com/5qwVYb1.jpeg" - /> - </div> - ); -} -``` +=== "picture.py" -```css -body { - margin: 0; - padding: 0; - height: 250px; -} + ```python + {% include "../../examples/python/managing_state/picture_component.py" start="# start" %} + ``` -.background { - width: 100vw; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; - background: #eee; -} +=== "styles.css" -.background--active { - background: #a6b5ff; -} + ```css + {% include "../../examples/css/managing_state/picture_component.css" %} + ``` -.picture { - width: 200px; - height: 200px; - border-radius: 10px; -} +=== ":material-play: Run" -.picture--active { - border: 5px solid #a6b5ff; -} -``` + ```python + # TODO + ``` <Solution> @@ -528,136 +396,45 @@ A single boolean state variable is enough to remember whether the image is activ Verify that this version works by clicking the image and then outside of it: -```js -import { useState } from "react"; - -export default function Picture() { - const [isActive, setIsActive] = useState(false); +=== "app.py" - let backgroundClassName = "background"; - let pictureClassName = "picture"; - if (isActive) { - pictureClassName += " picture--active"; - } else { - backgroundClassName += " background--active"; - } + ```python + {% include "../../examples/python/managing_state/stateful_picture_component.py" start="# start" %} + ``` - return ( - <div - className={backgroundClassName} - on_click={() => setIsActive(false)} - > - <img - on_click={(e) => { - e.stopPropagation(); - setIsActive(true); - }} - className={pictureClassName} - alt="Rainbow houses in Kampung Pelangi, Indonesia" - src="https://i.imgur.com/5qwVYb1.jpeg" - /> - </div> - ); -} -``` +=== "styles.css" -```css -body { - margin: 0; - padding: 0; - height: 250px; -} + ```css + {% include "../../examples/css/managing_state/picture_component.css" %} + ``` -.background { - width: 100vw; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; - background: #eee; -} +=== ":material-play: Run" -.background--active { - background: #a6b5ff; -} + ```python + # TODO + ``` -.picture { - width: 200px; - height: 200px; - border-radius: 10px; - border: 5px solid transparent; -} +Alternatively, you could return two separate chunks of PSX: -.picture--active { - border: 5px solid #a6b5ff; -} -``` +=== "app.py" -Alternatively, you could return two separate chunks of JSX: + ```python + {% include "../../examples/python/managing_state/stateful_picture_component.py" start="# start" %} + ``` -```js -import { useState } from "react"; - -export default function Picture() { - const [isActive, setIsActive] = useState(false); - if (isActive) { - return ( - <div className="background" on_click={() => setIsActive(false)}> - <img - className="picture picture--active" - alt="Rainbow houses in Kampung Pelangi, Indonesia" - src="https://i.imgur.com/5qwVYb1.jpeg" - on_click={(e) => e.stopPropagation()} - /> - </div> - ); - } - return ( - <div className="background background--active"> - <img - className="picture" - alt="Rainbow houses in Kampung Pelangi, Indonesia" - src="https://i.imgur.com/5qwVYb1.jpeg" - on_click={() => setIsActive(true)} - /> - </div> - ); -} -``` - -```css -body { - margin: 0; - padding: 0; - height: 250px; -} - -.background { - width: 100vw; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; - background: #eee; -} +=== "styles.css" -.background--active { - background: #a6b5ff; -} + ```css + {% include "../../examples/css/managing_state/picture_component.css" %} + ``` -.picture { - width: 200px; - height: 200px; - border-radius: 10px; - border: 5px solid transparent; -} +=== ":material-play: Run" -.picture--active { - border: 5px solid #a6b5ff; -} -``` + ```python + # TODO + ``` -Keep in mind that if two different JSX chunks describe the same tree, their nesting (first `<div>` → first `<img>`) has to line up. Otherwise, toggling `isActive` would recreate the whole tree below and [reset its state.](/learn/preserving-and-resetting-state) This is why, if a similar JSX tree gets returned in both cases, it is better to write them as a single piece of JSX. +Keep in mind that if two different PSX chunks describe the same tree, their nesting (first `html.div` → first `html.img`) has to line up. Otherwise, toggling `is_active` would recreate the whole tree below and [reset its state.](/learn/preserving-and-resetting-state) This is why, if a similar PSX tree gets returned in both cases, it is better to write them as a single piece of PSX. </Solution> From 9d05d27b2804780d7b3199f3f7efbf80e05db7e9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 29 Oct 2024 04:49:53 -0700 Subject: [PATCH 2/4] Format examples --- .../conditional_form_component.css | 4 +- .../multiple_form_components.css | 12 +++--- .../managing_state/all_possible_states.py | 1 - .../alt_stateful_picture_component.py | 23 ++++++------ .../managing_state/basic_form_component.py | 12 +++--- .../conditional_form_component.py | 21 +++++------ .../multiple_form_components.py | 8 ++-- .../python/managing_state/necessary_states.py | 1 - .../managing_state/picture_component.py | 6 +-- .../managing_state/refactored_states.py | 3 +- .../managing_state/stateful_form_component.py | 37 ++++++++++--------- .../stateful_picture_component.py | 13 +++---- 12 files changed, 64 insertions(+), 77 deletions(-) diff --git a/docs/examples/css/managing_state/conditional_form_component.css b/docs/examples/css/managing_state/conditional_form_component.css index 98c7d8768..2cc8b7afe 100644 --- a/docs/examples/css/managing_state/conditional_form_component.css +++ b/docs/examples/css/managing_state/conditional_form_component.css @@ -1,3 +1,3 @@ .Error { - color: red; -} \ No newline at end of file + color: red; +} diff --git a/docs/examples/css/managing_state/multiple_form_components.css b/docs/examples/css/managing_state/multiple_form_components.css index db8da7178..b24e106e8 100644 --- a/docs/examples/css/managing_state/multiple_form_components.css +++ b/docs/examples/css/managing_state/multiple_form_components.css @@ -1,13 +1,13 @@ section { - border-bottom: 1px solid #aaa; - padding: 20px; + border-bottom: 1px solid #aaa; + padding: 20px; } h4 { - color: #222; + color: #222; } body { - margin: 0; + margin: 0; } .Error { - color: red; -} \ No newline at end of file + color: red; +} diff --git a/docs/examples/python/managing_state/all_possible_states.py b/docs/examples/python/managing_state/all_possible_states.py index c93e7df13..a71f9b54a 100644 --- a/docs/examples/python/managing_state/all_possible_states.py +++ b/docs/examples/python/managing_state/all_possible_states.py @@ -1,6 +1,5 @@ from reactpy import hooks - # start is_empty, set_is_empty = hooks.use_state(True) is_typing, set_is_typing = hooks.use_state(False) diff --git a/docs/examples/python/managing_state/alt_stateful_picture_component.py b/docs/examples/python/managing_state/alt_stateful_picture_component.py index dfaf8f2d2..1fdd9c1ad 100644 --- a/docs/examples/python/managing_state/alt_stateful_picture_component.py +++ b/docs/examples/python/managing_state/alt_stateful_picture_component.py @@ -6,33 +6,32 @@ def picture(): is_active, set_is_active = hooks.use_state(False) - if (is_active): + if is_active: return html.div( { "class_name": "background", - "on_click": lambda event: set_is_active(False) + "on_click": lambda event: set_is_active(False), }, html.img( { "on_click": event(stop_propagation=True), "class_name": "picture picture--active", "alt": "Rainbow houses in Kampung Pelangi, Indonesia", - "src": "https://i.imgur.com/5qwVYb1.jpeg" + "src": "https://i.imgur.com/5qwVYb1.jpeg", } - ) + ), ) else: return html.div( - { - "class_name": "background background--active" - }, + {"class_name": "background background--active"}, html.img( { - "on_click": event(lambda event: set_is_active(True), - stop_propagation=True), + "on_click": event( + lambda event: set_is_active(True), stop_propagation=True + ), "class_name": "picture", "alt": "Rainbow houses in Kampung Pelangi, Indonesia", - "src": "https://i.imgur.com/5qwVYb1.jpeg" + "src": "https://i.imgur.com/5qwVYb1.jpeg", } - ) - ) \ No newline at end of file + ), + ) diff --git a/docs/examples/python/managing_state/basic_form_component.py b/docs/examples/python/managing_state/basic_form_component.py index 04e57c240..07328bff9 100644 --- a/docs/examples/python/managing_state/basic_form_component.py +++ b/docs/examples/python/managing_state/basic_form_component.py @@ -9,10 +9,8 @@ def form(status="empty"): else: return html._( html.h2("City quiz"), - html.p("In which city is there a billboard that turns air into drinkable water?"), - html.form( - html.textarea(), - html.br(), - html.button("Submit") - ) - ) \ No newline at end of file + html.p( + "In which city is there a billboard that turns air into drinkable water?" + ), + html.form(html.textarea(), html.br(), html.button("Submit")), + ) diff --git a/docs/examples/python/managing_state/conditional_form_component.py b/docs/examples/python/managing_state/conditional_form_component.py index 0b34d38b4..44f4f565d 100644 --- a/docs/examples/python/managing_state/conditional_form_component.py +++ b/docs/examples/python/managing_state/conditional_form_component.py @@ -6,8 +6,7 @@ def error(status): if status == "error": return html.p( - {"class_name": "error"}, - "Good guess but a wrong answer. Try again!" + {"class_name": "error"}, "Good guess but a wrong answer. Try again!" ) else: return "" @@ -27,19 +26,17 @@ def form(status="empty"): ), html.form( html.textarea( - { - "disabled": "True" if status == "submitting" - else "False" - } + {"disabled": "True" if status == "submitting" else "False"} ), html.br(), html.button( { - "disabled": True if status == "empty" - or status == "submitting" else "False" + "disabled": ( + True if status in ["empty", "submitting"] else "False" + ) }, - "Submit" + "Submit", ), - error(status) - ) - ) \ No newline at end of file + error(status), + ), + ) diff --git a/docs/examples/python/managing_state/multiple_form_components.py b/docs/examples/python/managing_state/multiple_form_components.py index 0648f5dd2..4d50bd643 100644 --- a/docs/examples/python/managing_state/multiple_form_components.py +++ b/docs/examples/python/managing_state/multiple_form_components.py @@ -1,14 +1,12 @@ -from reactpy import component, html from conditional_form_component import form +from reactpy import component, html + # start @component def item(status): - return html.section( - html.h4("Form", status, ':'), - form(status) - ) + return html.section(html.h4("Form", status, ":"), form(status)) @component diff --git a/docs/examples/python/managing_state/necessary_states.py b/docs/examples/python/managing_state/necessary_states.py index 53302a251..ee70c5686 100644 --- a/docs/examples/python/managing_state/necessary_states.py +++ b/docs/examples/python/managing_state/necessary_states.py @@ -1,6 +1,5 @@ from reactpy import hooks - # start answer, set_answer = hooks.use_state("") error, set_error = hooks.use_state(None) diff --git a/docs/examples/python/managing_state/picture_component.py b/docs/examples/python/managing_state/picture_component.py index 9c1c5ebf8..bc60a8143 100644 --- a/docs/examples/python/managing_state/picture_component.py +++ b/docs/examples/python/managing_state/picture_component.py @@ -10,7 +10,7 @@ def picture(): { "class_name": "picture", "alt": "Rainbow houses in Kampung Pelangi, Indonesia", - "src": "https://i.imgur.com/5qwVYb1.jpeg" + "src": "https://i.imgur.com/5qwVYb1.jpeg", } - ) - ) \ No newline at end of file + ), + ) diff --git a/docs/examples/python/managing_state/refactored_states.py b/docs/examples/python/managing_state/refactored_states.py index 1448764d4..3d080c92c 100644 --- a/docs/examples/python/managing_state/refactored_states.py +++ b/docs/examples/python/managing_state/refactored_states.py @@ -1,7 +1,6 @@ from reactpy import hooks - # start answer, set_answer = hooks.use_state("") error, set_error = hooks.use_state(None) -status, set_status = hooks.use_state("typing") # 'typing', 'submitting', or 'success' \ No newline at end of file +status, set_status = hooks.use_state("typing") # 'typing', 'submitting', or 'success' diff --git a/docs/examples/python/managing_state/stateful_form_component.py b/docs/examples/python/managing_state/stateful_form_component.py index 61d732611..c9d8ffa64 100644 --- a/docs/examples/python/managing_state/stateful_form_component.py +++ b/docs/examples/python/managing_state/stateful_form_component.py @@ -1,8 +1,9 @@ -from reactpy import component, event, html, hooks import asyncio +from reactpy import component, event, hooks, html -async def submit_form(): + +async def submit_form(*args): await asyncio.wait(5) @@ -11,19 +12,18 @@ async def submit_form(): def error_msg(error): if error: return html.p( - {"class_name": "error"}, - "Good guess but a wrong answer. Try again!" + {"class_name": "error"}, "Good guess but a wrong answer. Try again!" ) else: return "" - + @component def form(status="empty"): answer, set_answer = hooks.use_state("") error, set_error = hooks.use_state(None) status, set_status = hooks.use_state("typing") - + @event(prevent_default=True) async def handle_submit(event): set_status("submitting") @@ -37,7 +37,7 @@ async def handle_submit(event): @event() def handle_textarea_change(event): set_answer(event["target"]["value"]) - + if status == "success": return html.h1("That's right!") else: @@ -51,19 +51,20 @@ def handle_textarea_change(event): {"on_submit": handle_submit}, html.textarea( { - "value": answer, - "on_change": handle_textarea_change, - "disabled": True if status == "submitting" else "False" + "value": answer, + "on_change": handle_textarea_change, + "disabled": (True if status == "submitting" else "False"), } ), html.br(), html.button( - { - "disabled": True if status == "empty" - or status == "submitting" else "False" - }, - "Submit" + { + "disabled": ( + True if status in ["empty", "submitting"] else "False" + ) + }, + "Submit", ), - error_msg(error) - ) - ) \ No newline at end of file + error_msg(error), + ), + ) diff --git a/docs/examples/python/managing_state/stateful_picture_component.py b/docs/examples/python/managing_state/stateful_picture_component.py index 6a7879535..38e338cbf 100644 --- a/docs/examples/python/managing_state/stateful_picture_component.py +++ b/docs/examples/python/managing_state/stateful_picture_component.py @@ -8,26 +8,23 @@ def picture(): background_class_name = "background" picture_class_name = "picture" - if (is_active): + if is_active: picture_class_name += " picture--active" else: background_class_name += " background--active" - + @event(stop_propagation=True) def handle_click(event): set_is_active(True) return html.div( - { - "class_name": background_class_name, - "on_click": set_is_active(False) - }, + {"class_name": background_class_name, "on_click": set_is_active(False)}, html.img( { "on_click": handle_click, "class_name": picture_class_name, "alt": "Rainbow houses in Kampung Pelangi, Indonesia", - "src": "https://i.imgur.com/5qwVYb1.jpeg" + "src": "https://i.imgur.com/5qwVYb1.jpeg", } - ) + ), ) From 77ff916201013dd800639db7ac4100e073eaf004 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 16 Mar 2025 16:08:22 -0700 Subject: [PATCH 3/4] change examples folder --- docs/examples/{python => }/managing_state/all_possible_states.py | 0 .../{python => }/managing_state/alt_stateful_picture_component.py | 0 docs/examples/{python => }/managing_state/basic_form_component.py | 0 .../{css => }/managing_state/conditional_form_component.css | 0 .../{python => }/managing_state/conditional_form_component.py | 0 .../{css => }/managing_state/multiple_form_components.css | 0 .../{python => }/managing_state/multiple_form_components.py | 0 docs/examples/{python => }/managing_state/necessary_states.py | 0 docs/examples/{css => }/managing_state/picture_component.css | 0 docs/examples/{python => }/managing_state/picture_component.py | 0 docs/examples/{python => }/managing_state/refactored_states.py | 0 .../{python => }/managing_state/stateful_form_component.py | 0 .../{python => }/managing_state/stateful_picture_component.py | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename docs/examples/{python => }/managing_state/all_possible_states.py (100%) rename docs/examples/{python => }/managing_state/alt_stateful_picture_component.py (100%) rename docs/examples/{python => }/managing_state/basic_form_component.py (100%) rename docs/examples/{css => }/managing_state/conditional_form_component.css (100%) rename docs/examples/{python => }/managing_state/conditional_form_component.py (100%) rename docs/examples/{css => }/managing_state/multiple_form_components.css (100%) rename docs/examples/{python => }/managing_state/multiple_form_components.py (100%) rename docs/examples/{python => }/managing_state/necessary_states.py (100%) rename docs/examples/{css => }/managing_state/picture_component.css (100%) rename docs/examples/{python => }/managing_state/picture_component.py (100%) rename docs/examples/{python => }/managing_state/refactored_states.py (100%) rename docs/examples/{python => }/managing_state/stateful_form_component.py (100%) rename docs/examples/{python => }/managing_state/stateful_picture_component.py (100%) diff --git a/docs/examples/python/managing_state/all_possible_states.py b/docs/examples/managing_state/all_possible_states.py similarity index 100% rename from docs/examples/python/managing_state/all_possible_states.py rename to docs/examples/managing_state/all_possible_states.py diff --git a/docs/examples/python/managing_state/alt_stateful_picture_component.py b/docs/examples/managing_state/alt_stateful_picture_component.py similarity index 100% rename from docs/examples/python/managing_state/alt_stateful_picture_component.py rename to docs/examples/managing_state/alt_stateful_picture_component.py diff --git a/docs/examples/python/managing_state/basic_form_component.py b/docs/examples/managing_state/basic_form_component.py similarity index 100% rename from docs/examples/python/managing_state/basic_form_component.py rename to docs/examples/managing_state/basic_form_component.py diff --git a/docs/examples/css/managing_state/conditional_form_component.css b/docs/examples/managing_state/conditional_form_component.css similarity index 100% rename from docs/examples/css/managing_state/conditional_form_component.css rename to docs/examples/managing_state/conditional_form_component.css diff --git a/docs/examples/python/managing_state/conditional_form_component.py b/docs/examples/managing_state/conditional_form_component.py similarity index 100% rename from docs/examples/python/managing_state/conditional_form_component.py rename to docs/examples/managing_state/conditional_form_component.py diff --git a/docs/examples/css/managing_state/multiple_form_components.css b/docs/examples/managing_state/multiple_form_components.css similarity index 100% rename from docs/examples/css/managing_state/multiple_form_components.css rename to docs/examples/managing_state/multiple_form_components.css diff --git a/docs/examples/python/managing_state/multiple_form_components.py b/docs/examples/managing_state/multiple_form_components.py similarity index 100% rename from docs/examples/python/managing_state/multiple_form_components.py rename to docs/examples/managing_state/multiple_form_components.py diff --git a/docs/examples/python/managing_state/necessary_states.py b/docs/examples/managing_state/necessary_states.py similarity index 100% rename from docs/examples/python/managing_state/necessary_states.py rename to docs/examples/managing_state/necessary_states.py diff --git a/docs/examples/css/managing_state/picture_component.css b/docs/examples/managing_state/picture_component.css similarity index 100% rename from docs/examples/css/managing_state/picture_component.css rename to docs/examples/managing_state/picture_component.css diff --git a/docs/examples/python/managing_state/picture_component.py b/docs/examples/managing_state/picture_component.py similarity index 100% rename from docs/examples/python/managing_state/picture_component.py rename to docs/examples/managing_state/picture_component.py diff --git a/docs/examples/python/managing_state/refactored_states.py b/docs/examples/managing_state/refactored_states.py similarity index 100% rename from docs/examples/python/managing_state/refactored_states.py rename to docs/examples/managing_state/refactored_states.py diff --git a/docs/examples/python/managing_state/stateful_form_component.py b/docs/examples/managing_state/stateful_form_component.py similarity index 100% rename from docs/examples/python/managing_state/stateful_form_component.py rename to docs/examples/managing_state/stateful_form_component.py diff --git a/docs/examples/python/managing_state/stateful_picture_component.py b/docs/examples/managing_state/stateful_picture_component.py similarity index 100% rename from docs/examples/python/managing_state/stateful_picture_component.py rename to docs/examples/managing_state/stateful_picture_component.py From 21413a1f806892d6652038a55e12524581e39a4e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 17 Mar 2025 02:04:16 -0700 Subject: [PATCH 4/4] Fix everthing besides the challenges --- .../alt_stateful_picture_component.py | 14 +- .../managing_state/basic_form_component.py | 16 +- .../conditional_form_component.py | 45 +- .../multiple_form_components.py | 5 +- .../managing_state/picture_component.py | 4 +- .../managing_state/stateful_form_component.py | 11 +- .../stateful_picture_component.py | 14 +- docs/mkdocs.yml | 2 +- docs/src/assets/css/code.css | 105 +- .../src/learn/reacting-to-input-with-state.md | 1464 ++++++++--------- 10 files changed, 831 insertions(+), 849 deletions(-) diff --git a/docs/examples/managing_state/alt_stateful_picture_component.py b/docs/examples/managing_state/alt_stateful_picture_component.py index 1fdd9c1ad..9cdfb18c2 100644 --- a/docs/examples/managing_state/alt_stateful_picture_component.py +++ b/docs/examples/managing_state/alt_stateful_picture_component.py @@ -9,13 +9,13 @@ def picture(): if is_active: return html.div( { - "class_name": "background", - "on_click": lambda event: set_is_active(False), + "className": "background", + "onClick": lambda event: set_is_active(False), }, html.img( { - "on_click": event(stop_propagation=True), - "class_name": "picture picture--active", + "onClick": event(stop_propagation=True), + "className": "picture picture--active", "alt": "Rainbow houses in Kampung Pelangi, Indonesia", "src": "https://i.imgur.com/5qwVYb1.jpeg", } @@ -23,13 +23,13 @@ def picture(): ) else: return html.div( - {"class_name": "background background--active"}, + {"className": "background background--active"}, html.img( { - "on_click": event( + "onClick": event( lambda event: set_is_active(True), stop_propagation=True ), - "class_name": "picture", + "className": "picture", "alt": "Rainbow houses in Kampung Pelangi, Indonesia", "src": "https://i.imgur.com/5qwVYb1.jpeg", } diff --git a/docs/examples/managing_state/basic_form_component.py b/docs/examples/managing_state/basic_form_component.py index 07328bff9..ecf5c88f3 100644 --- a/docs/examples/managing_state/basic_form_component.py +++ b/docs/examples/managing_state/basic_form_component.py @@ -6,11 +6,11 @@ def form(status="empty"): if status == "success": return html.h1("That's right!") - else: - return html._( - html.h2("City quiz"), - html.p( - "In which city is there a billboard that turns air into drinkable water?" - ), - html.form(html.textarea(), html.br(), html.button("Submit")), - ) + + return html.fragment( + html.h2("City quiz"), + html.p( + "In which city is there a billboard that turns air into drinkable water?" + ), + html.form(html.textarea(), html.br(), html.button("Submit")), + ) diff --git a/docs/examples/managing_state/conditional_form_component.py b/docs/examples/managing_state/conditional_form_component.py index 44f4f565d..65307a829 100644 --- a/docs/examples/managing_state/conditional_form_component.py +++ b/docs/examples/managing_state/conditional_form_component.py @@ -6,37 +6,30 @@ def error(status): if status == "error": return html.p( - {"class_name": "error"}, "Good guess but a wrong answer. Try again!" + {"className": "error"}, "Good guess but a wrong answer. Try again!" ) - else: - return "" + + return "" @component def form(status="empty"): - # Try status="submitting", "error", "success" + # Try "submitting", "error", "success": if status == "success": return html.h1("That's right!") - else: - return html._( - html.h2("City quiz"), - html.p( - "In which city is there a billboard that turns air into \ - drinkable water?" - ), - html.form( - html.textarea( - {"disabled": "True" if status == "submitting" else "False"} - ), - html.br(), - html.button( - { - "disabled": ( - True if status in ["empty", "submitting"] else "False" - ) - }, - "Submit", - ), - error(status), + + return html.fragment( + html.h2("City quiz"), + html.p( + "In which city is there a billboard that turns air into drinkable water?" + ), + html.form( + html.textarea({"disabled": "True" if status == "submitting" else "False"}), + html.br(), + html.button( + {"disabled": (True if status in ["empty", "submitting"] else "False")}, + "Submit", ), - ) + error(status), + ), + ) diff --git a/docs/examples/managing_state/multiple_form_components.py b/docs/examples/managing_state/multiple_form_components.py index 4d50bd643..48d6c3ca2 100644 --- a/docs/examples/managing_state/multiple_form_components.py +++ b/docs/examples/managing_state/multiple_form_components.py @@ -1,9 +1,9 @@ +# start from conditional_form_component import form from reactpy import component, html -# start @component def item(status): return html.section(html.h4("Form", status, ":"), form(status)) @@ -12,5 +12,4 @@ def item(status): @component def app(): statuses = ["empty", "typing", "submitting", "success", "error"] - status_list = [item(status) for status in statuses] - return html._(status_list) + return html.fragment([item(status) for status in statuses]) diff --git a/docs/examples/managing_state/picture_component.py b/docs/examples/managing_state/picture_component.py index bc60a8143..9efc57b17 100644 --- a/docs/examples/managing_state/picture_component.py +++ b/docs/examples/managing_state/picture_component.py @@ -5,10 +5,10 @@ @component def picture(): return html.div( - {"class_name": "background background--active"}, + {"className": "background background--active"}, html.img( { - "class_name": "picture", + "className": "picture", "alt": "Rainbow houses in Kampung Pelangi, Indonesia", "src": "https://i.imgur.com/5qwVYb1.jpeg", } diff --git a/docs/examples/managing_state/stateful_form_component.py b/docs/examples/managing_state/stateful_form_component.py index c9d8ffa64..d2ef5580d 100644 --- a/docs/examples/managing_state/stateful_form_component.py +++ b/docs/examples/managing_state/stateful_form_component.py @@ -12,7 +12,7 @@ async def submit_form(*args): def error_msg(error): if error: return html.p( - {"class_name": "error"}, "Good guess but a wrong answer. Try again!" + {"className": "error"}, "Good guess but a wrong answer. Try again!" ) else: return "" @@ -41,18 +41,17 @@ def handle_textarea_change(event): if status == "success": return html.h1("That's right!") else: - return html._( + return html.fragment( html.h2("City quiz"), html.p( - "In which city is there a billboard \ - that turns air into drinkable water?" + "In which city is there a billboard that turns air into drinkable water?" ), html.form( - {"on_submit": handle_submit}, + {"onSubmit": handle_submit}, html.textarea( { "value": answer, - "on_change": handle_textarea_change, + "onChange": handle_textarea_change, "disabled": (True if status == "submitting" else "False"), } ), diff --git a/docs/examples/managing_state/stateful_picture_component.py b/docs/examples/managing_state/stateful_picture_component.py index 38e338cbf..e3df7991c 100644 --- a/docs/examples/managing_state/stateful_picture_component.py +++ b/docs/examples/managing_state/stateful_picture_component.py @@ -5,24 +5,24 @@ @component def picture(): is_active, set_is_active = hooks.use_state(False) - background_class_name = "background" - picture_class_name = "picture" + background_className = "background" + picture_className = "picture" if is_active: - picture_class_name += " picture--active" + picture_className += " picture--active" else: - background_class_name += " background--active" + background_className += " background--active" @event(stop_propagation=True) def handle_click(event): set_is_active(True) return html.div( - {"class_name": background_class_name, "on_click": set_is_active(False)}, + {"className": background_className, "onClick": set_is_active(False)}, html.img( { - "on_click": handle_click, - "class_name": picture_class_name, + "onClick": handle_click, + "className": picture_className, "alt": "Rainbow houses in Kampung Pelangi, Indonesia", "src": "https://i.imgur.com/5qwVYb1.jpeg", } diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index a55cf24c2..2594e705e 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -34,7 +34,7 @@ nav: - Updating Objects in State 🚧: learn/updating-objects-in-state.md - Updating Arrays in State 🚧: learn/updating-arrays-in-state.md - Managing State: - - Reacting to Input with State 🚧: learn/reacting-to-input-with-state.md + - Reacting to Input with State: learn/reacting-to-input-with-state.md - Choosing the State Structure 🚧: learn/choosing-the-state-structure.md - Sharing State Between Components 🚧: learn/sharing-state-between-components.md - Preserving and Resetting State 🚧: learn/preserving-and-resetting-state.md diff --git a/docs/src/assets/css/code.css b/docs/src/assets/css/code.css index c54654980..f77c25e5f 100644 --- a/docs/src/assets/css/code.css +++ b/docs/src/assets/css/code.css @@ -1,111 +1,114 @@ :root { - --code-max-height: 17.25rem; - --md-code-backdrop: rgba(0, 0, 0, 0) 0px 0px 0px 0px, - rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.03) 0px 0.8px 2px 0px, - rgba(0, 0, 0, 0.047) 0px 2.7px 6.7px 0px, - rgba(0, 0, 0, 0.08) 0px 12px 30px 0px; + --code-max-height: 17.25rem; + --md-code-backdrop: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.03) 0px 0.8px 2px 0px, rgba(0, 0, 0, 0.047) 0px 2.7px 6.7px 0px, rgba(0, 0, 0, 0.08) 0px 12px 30px 0px; } [data-md-color-scheme="slate"] { - --md-code-hl-color: #ffffcf1c; - --md-code-bg-color: #16181d; - --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); - --code-tab-color: rgb(52, 58, 70); - --md-code-hl-name-color: #aadafc; - --md-code-hl-string-color: hsl(21 49% 63% / 1); - --md-code-hl-keyword-color: hsl(289.67deg 35% 60%); - --md-code-hl-constant-color: hsl(213.91deg 68% 61%); - --md-code-hl-number-color: #bfd9ab; - --func-and-decorator-color: #dcdcae; - --module-import-color: #60c4ac; + --md-code-hl-color: #ffffcf1c; + --md-code-bg-color: #16181d; + --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); + --code-tab-color: rgb(52, 58, 70); + --md-code-hl-name-color: #aadafc; + --md-code-hl-string-color: hsl(21 49% 63% / 1); + --md-code-hl-keyword-color: hsl(289.67deg 35% 60%); + --md-code-hl-constant-color: hsl(213.91deg 68% 61%); + --md-code-hl-number-color: #bfd9ab; + --func-and-decorator-color: #dcdcae; + --module-import-color: #60c4ac; } [data-md-color-scheme="default"] { - --md-code-hl-color: #ffffcf1c; - --md-code-bg-color: rgba(208, 211, 220, 0.4); - --md-code-fg-color: rgb(64, 71, 86); - --code-tab-color: #fff; - --func-and-decorator-color: var(--md-code-hl-function-color); - --module-import-color: #e153e5; + --md-code-hl-color: #ffffcf1c; + --md-code-bg-color: rgba(208, 211, 220, 0.4); + --md-code-fg-color: rgb(64, 71, 86); + --code-tab-color: #fff; + --func-and-decorator-color: var(--md-code-hl-function-color); + --module-import-color: #e153e5; } [data-md-color-scheme="default"] .md-typeset .highlight > pre > code, [data-md-color-scheme="default"] .md-typeset .highlight > table.highlighttable { - --md-code-bg-color: #fff; + --md-code-bg-color: #fff; } /* All code blocks */ .md-typeset pre > code { - max-height: var(--code-max-height); + max-height: var(--code-max-height); } /* Code blocks with no line number */ .md-typeset .highlight > pre > code { - border-radius: 16px; - max-height: var(--code-max-height); - box-shadow: var(--md-code-backdrop); + border-radius: 16px; + max-height: var(--code-max-height); + box-shadow: var(--md-code-backdrop); } /* Code blocks with line numbers */ .md-typeset .highlighttable .linenos { - max-height: var(--code-max-height); - overflow: hidden; + max-height: var(--code-max-height); + overflow: hidden; } .md-typeset .highlighttable { - box-shadow: var(--md-code-backdrop); - border-radius: 8px; - overflow: hidden; + box-shadow: var(--md-code-backdrop); + border-radius: 8px; + overflow: hidden; } /* Tabbed code blocks */ .md-typeset .tabbed-set { - box-shadow: var(--md-code-backdrop); - border-radius: 8px; - overflow: hidden; - border: 1px solid var(--md-default-fg-color--lightest); + box-shadow: var(--md-code-backdrop); + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--md-default-fg-color--lightest); } .md-typeset .tabbed-set .tabbed-block { - overflow: hidden; + overflow: hidden; } .js .md-typeset .tabbed-set .tabbed-labels { - background: var(--code-tab-color); - margin: 0; - padding-left: 0.8rem; + background: var(--code-tab-color); + margin: 0; + padding-left: 0.8rem; } .md-typeset .tabbed-set .tabbed-labels > label { - font-weight: 400; - font-size: 0.7rem; - padding-top: 0.55em; - padding-bottom: 0.35em; + font-weight: 400; + font-size: 0.7rem; + padding-top: 0.55em; + padding-bottom: 0.35em; } .md-typeset .tabbed-set .highlighttable { - border-radius: 0; + border-radius: 0; } -/* Code hightlighting colors */ +/* Code highlighting colors */ /* Module imports */ .highlight .nc, .highlight .ne, .highlight .nn, .highlight .nv { - color: var(--module-import-color); + color: var(--module-import-color); } /* Function def name and decorator */ .highlight .nd, .highlight .nf { - color: var(--func-and-decorator-color); + color: var(--func-and-decorator-color); } /* None type */ .highlight .kc { - color: var(--md-code-hl-constant-color); + color: var(--md-code-hl-constant-color); } /* Keywords such as def and return */ .highlight .k { - color: var(--md-code-hl-constant-color); + color: var(--md-code-hl-constant-color); } /* HTML tags */ .highlight .nt { - color: var(--md-code-hl-constant-color); + color: var(--md-code-hl-constant-color); +} + +/* Code blocks that are challenges */ +.challenge { + padding: 0.1rem 1rem 0.6rem 1rem; + background: var(--code-tab-color); } diff --git a/docs/src/learn/reacting-to-input-with-state.md b/docs/src/learn/reacting-to-input-with-state.md index ffd4ba6a6..2a8b9efd9 100644 --- a/docs/src/learn/reacting-to-input-with-state.md +++ b/docs/src/learn/reacting-to-input-with-state.md @@ -29,103 +29,107 @@ They don't know where you want to go, they just follow your commands. (And if yo In this example of imperative UI programming, the form is built _without_ React. It only uses the browser [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model): -```js -async function handleFormSubmit(e) { - e.preventDefault(); - disable(textarea); - disable(button); - show(loadingMessage); - hide(errorMessage); - try { - await submitForm(textarea.value); - show(successMessage); - hide(form); - } catch (err) { - show(errorMessage); - errorMessage.textContent = err.message; - } finally { - hide(loadingMessage); - enable(textarea); - enable(button); - } -} - -function handleTextareaChange() { - if (textarea.value.length === 0) { - disable(button); - } else { - enable(button); - } -} - -function hide(el) { - el.style.display = "none"; -} - -function show(el) { - el.style.display = ""; -} - -function enable(el) { - el.disabled = false; -} - -function disable(el) { - el.disabled = true; -} - -function submitForm(answer) { - // Pretend it's hitting the network. - return new Promise((resolve, reject) => { - setTimeout(() => { - if (answer.toLowerCase() == "istanbul") { - resolve(); - } else { - reject(new Error("Good guess but a wrong answer. Try again!")); - } - }, 1500); - }); -} - -let form = document.getElementById("form"); -let textarea = document.getElementById("textarea"); -let button = document.getElementById("button"); -let loadingMessage = document.getElementById("loading"); -let errorMessage = document.getElementById("error"); -let successMessage = document.getElementById("success"); -form.onsubmit = handleFormSubmit; -textarea.oninput = handleTextareaChange; -``` +=== "index.js" + + ```js + async function handleFormSubmit(e) { + e.preventDefault(); + disable(textarea); + disable(button); + show(loadingMessage); + hide(errorMessage); + try { + await submitForm(textarea.value); + show(successMessage); + hide(form); + } catch (err) { + show(errorMessage); + errorMessage.textContent = err.message; + } finally { + hide(loadingMessage); + enable(textarea); + enable(button); + } + } + + function handleTextareaChange() { + if (textarea.value.length === 0) { + disable(button); + } else { + enable(button); + } + } + + function hide(el) { + el.style.display = "none"; + } + + function show(el) { + el.style.display = ""; + } + + function enable(el) { + el.disabled = false; + } + + function disable(el) { + el.disabled = true; + } + + function submitForm(answer) { + // Pretend it's hitting the network. + return new Promise((resolve, reject) => { + setTimeout(() => { + if (answer.toLowerCase() == "istanbul") { + resolve(); + } else { + reject(new Error("Good guess but a wrong answer. Try again!")); + } + }, 1500); + }); + } + + let form = document.getElementById("form"); + let textarea = document.getElementById("textarea"); + let button = document.getElementById("button"); + let loadingMessage = document.getElementById("loading"); + let errorMessage = document.getElementById("error"); + let successMessage = document.getElementById("success"); + form.onsubmit = handleFormSubmit; + textarea.oninput = handleTextareaChange; + ``` -```js -{ - "hardReloadOnChange": true -} -``` +=== "index.html" + + ```html + <form id="form"> + <h2>City quiz</h2> + <p>What city is located on two continents?</p> + <textarea id="textarea"></textarea> + <br /> + <button id="button" disabled>Submit</button> + <p id="loading" style="display: none">Loading...</p> + <p id="error" style="display: none; color: red;"></p> + </form> + <h1 id="success" style="display: none">That's right!</h1> + + <style> + * { + box-sizing: border-box; + } + body { + font-family: sans-serif; + margin: 20px; + padding: 0; + } + </style> + ``` -```html -<form id="form"> - <h2>City quiz</h2> - <p>What city is located on two continents?</p> - <textarea id="textarea"></textarea> - <br /> - <button id="button" disabled>Submit</button> - <p id="loading" style="display: none">Loading...</p> - <p id="error" style="display: none; color: red;"></p> -</form> -<h1 id="success" style="display: none">That's right!</h1> - -<style> - * { - box-sizing: border-box; - } - body { - font-family: sans-serif; - margin: 20px; - padding: 0; - } -</style> -``` +=== ":material-play: Run" + + ```html + # TODO + ``` Manipulating the UI imperatively works well enough for isolated examples, but it gets exponentially more difficult to manage in more complex systems. Imagine updating a page full of different forms like this one. Adding a new UI element or a new interaction would require carefully checking all existing code to make sure you haven't introduced a bug (for example, forgetting to show or hide something). @@ -160,12 +164,14 @@ First, you need to visualize all the different "states" of the UI the user might Just like a designer, you'll want to "mock up" or create "mocks" for the different states before you add logic. For example, here is a mock for just the visual part of the form. This mock is controlled by a prop called `status` with a default value of `'empty'`: === "app.py" - ```python - {% include "../../examples/python/managing_state/basic_form_component.py" start="# start" %} + + ```python + {% include "../../examples/managing_state/basic_form_component.py" start="# start" %} ``` === ":material-play: Run" - ```python + + ```python # TODO ``` @@ -173,56 +179,55 @@ You could call that prop anything you like, the naming is not important. Try edi === "app.py" - ```python - {% include "../../examples/python/managing_state/conditional_form_component.py" start="# start" %} + ```python + {% include "../../examples/managing_state/conditional_form_component.py" start="# start" %} ``` === "styles.css" ```css - {% include "../../examples/css/managing_state/conditional_form_component.css" %} + {% include "../../examples/managing_state/conditional_form_component.css" %} ``` === ":material-play: Run" - ```python + ```python # TODO ``` -<DeepDive> +!!! info "Deep Dive" -#### Displaying many visual states at once + <font size="4">**Displaying many visual states at once**</font> -If a component has a lot of visual states, it can be convenient to show them all on one page: + ??? "Show Details" -=== "app.py" - - ```python - {% include "../../examples/python/managing_state/multiple_form_components.py" start="# start" %} - ``` + If a component has a lot of visual states, it can be convenient to show them all on one page: -=== "form.py" - - ```python - {% include "../../examples/python/managing_state/conditional_form_component.py" start="# start" %} - ``` + === "app.py" -=== "styles.css" + ```python + {% include "../../examples/managing_state/multiple_form_components.py" start="# start" %} + ``` - ```css - {% include "../../examples/css/managing_state/multiple_form_components.css" %} - ``` + === "form.py" + ```python + {% include "../../examples/managing_state/conditional_form_component.py" start="# start" %} + ``` -=== ":material-play: Run" - - ```python - # TODO - ``` + === "styles.css" -Pages like this are often called "living styleguides" or "storybooks". + ```css + {% include "../../examples/managing_state/multiple_form_components.css" %} + ``` -</DeepDive> + === ":material-play: Run" + + ```python + # TODO + ``` + + Pages like this are often called "living styleguides" or "storybooks". ### Step 2: Determine what triggers those state changes @@ -231,23 +236,18 @@ You can trigger state updates in response to two kinds of inputs: - **Human inputs,** like clicking a button, typing in a field, navigating a link. - **Computer inputs,** like a network response arriving, a timeout completing, an image loading. -<IllustrationBlock> - <Illustration caption="Human inputs" alt="A finger." src="/images/docs/illustrations/i_inputs1.png" /> - <Illustration caption="Computer inputs" alt="Ones and zeroes." src="/images/docs/illustrations/i_inputs2.png" /> -</IllustrationBlock> +<!-- TODO: Diagram --> -In both cases, **you must set [state variables](/learn/state-a-components-memory#anatomy-of-usestate) to update the UI.** For the form you're developing, you will need to change state in response to a few different inputs: +In both cases, **you must set [state variables](./state-a-components-memory.md#anatomy-of-usestate) to update the UI.** For the form you're developing, you will need to change state in response to a few different inputs: - **Changing the text input** (human) should switch it from the _Empty_ state to the _Typing_ state or back, depending on whether the text box is empty or not. - **Clicking the Submit button** (human) should switch it to the _Submitting_ state. - **Successful network response** (computer) should switch it to the _Success_ state. - **Failed network response** (computer) should switch it to the _Error_ state with the matching error message. -<Note> +!!! abstract "Note" -Notice that human inputs often require [event handlers](/learn/responding-to-events)! - -</Note> + Notice that human inputs often require [event handlers](./responding-to-events.md)! To help visualize this flow, try drawing each state on paper as a labeled circle, and each change between two states as an arrow. You can sketch out many flows this way and sort out bugs long before implementation. @@ -255,37 +255,21 @@ To help visualize this flow, try drawing each state on paper as a labeled circle ### Step 3: Represent the state in memory with `useState` -Next you'll need to represent the visual states of your component in memory with [`use_state`.](/reference/react/useState) Simplicity is key: each piece of state is a "moving piece", and **you want as few "moving pieces" as possible.** More complexity leads to more bugs! +Next you'll need to represent the visual states of your component in memory with [`use_state`.](../reference/use-state.md) Simplicity is key: each piece of state is a "moving piece", and **you want as few "moving pieces" as possible.** More complexity leads to more bugs! Start with the state that _absolutely must_ be there. For example, you'll need to store the `answer` for the input, and the `error` (if it exists) to store the last error: -=== "app.py" - - ```python - {% include "../../examples/python/managing_state/necessary_states.py" start="# start" %} - ``` - -=== ":material-play: Run" - - ```python - # TODO - ``` +```python linenums="0" +{% include "../../examples/managing_state/necessary_states.py" start="# start" %} +``` Then, you'll need a state variable representing which one of the visual states that you want to display. There's usually more than a single way to represent that in memory, so you'll need to experiment with it. If you struggle to think of the best way immediately, start by adding enough state that you're _definitely_ sure that all the possible visual states are covered: -=== "app.py" - - ```python - {% include "../../examples/python/managing_state/all_possible_states.py" start="# start" %} - ``` - -=== ":material-play: Run" - - ```python - # TODO - ``` +```python linenums="0" +{% include "../../examples/managing_state/all_possible_states.py" start="# start" %} +``` Your first idea likely won't be the best, but that's ok--refactoring state is a part of the process! @@ -301,27 +285,17 @@ Here are some questions you can ask about your state variables: After this clean-up, you're left with 3 (down from 7!) _essential_ state variables: -=== "app.py" - - ```python - {% include "../../examples/python/managing_state/refactored_states.py" start="# start" %} - ``` - -=== ":material-play: Run" - - ```python - # TODO - ``` +```python linenums="0" +{% include "../../examples/managing_state/refactored_states.py" start="# start" %} +``` You know they are essential, because you can't remove any of them without breaking the functionality. -<DeepDive> +!!! info "Deep Dive" -#### Eliminating “impossible” states with a reducer + <font size="4">**Eliminating “impossible” states with a reducer**</font> -These three variables are a good enough representation of this form's state. However, there are still some intermediate states that don't fully make sense. For example, a non-null `error` doesn't make sense when `status` is `'success'`. To model the state more precisely, you can [extract it into a reducer.](/learn/extracting-state-logic-into-a-reducer) Reducers let you unify multiple state variables into a single object and consolidate all the related logic! - -</DeepDive> + These three variables are a good enough representation of this form's state. However, there are still some intermediate states that don't fully make sense. For example, a non-null `error` doesn't make sense when `status` is `'success'`. To model the state more precisely, you can [extract it into a reducer.](./extracting-state-logic-into-a-reducer.md) Reducers let you unify multiple state variables into a single object and consolidate all the related logic! ### Step 5: Connect the event handlers to set state @@ -329,25 +303,25 @@ Lastly, create event handlers that update the state. Below is the final form, wi === "app.py" - ```python - {% include "../../examples/python/managing_state/stateful_form_component.py" start="# start" %} + ```python + {% include "../../examples/managing_state/stateful_form_component.py" start="# start" %} ``` === "styles.css" - ```css - {% include "../../examples/css/managing_state/conditional_form_component.css" %} + ```css + {% include "../../examples/managing_state/conditional_form_component.css" %} ``` === ":material-play: Run" - ```python + ```python # TODO ``` Although this code is longer than the original imperative example, it is much less fragile. Expressing all interactions as state changes lets you later introduce new visual states without breaking existing ones. It also lets you change what should be displayed in each state without changing the logic of the interaction itself. -<Recap> +### Recap - Declarative programming means describing the UI for each visual state rather than micromanaging the UI (imperative). - When developing a component: @@ -357,596 +331,610 @@ Although this code is longer than the original imperative example, it is much le 4. Remove non-essential state to avoid bugs and paradoxes. 5. Connect the event handlers to set state. -</Recap> - -<Challenges> - -#### Add and remove a CSS class +### Challenges -Make it so that clicking on the picture _removes_ the `background--active` CSS class from the outer `<div>`, but _adds_ the `picture--active` class to the `<img>`. Clicking the background again should restore the original CSS classes. +=== "1. Add and remove a CSS class" -Visually, you should expect that clicking on the picture removes the purple background and highlights the picture border. Clicking outside the picture highlights the background, but removes the picture border highlight. + <div class="challenge"> -=== "picture.py" + <font size="4">**Challenge 1 of 3: Add and remove a CSS class**</font> - ```python - {% include "../../examples/python/managing_state/picture_component.py" start="# start" %} - ``` - -=== "styles.css" + Make it so that clicking on the picture _removes_ the `background--active` CSS class from the outer `<div>`, but _adds_ the `picture--active` class to the `<img>`. Clicking the background again should restore the original CSS classes. - ```css - {% include "../../examples/css/managing_state/picture_component.css" %} - ``` - -=== ":material-play: Run" - - ```python - # TODO - ``` + Visually, you should expect that clicking on the picture removes the purple background and highlights the picture border. Clicking outside the picture highlights the background, but removes the picture border highlight. -<Solution> + === "picture.py" -This component has two visual states: when the image is active, and when the image is inactive: + ```python + {% include "../../examples/managing_state/picture_component.py" start="# start" %} + ``` -- When the image is active, the CSS classes are `background` and `picture picture--active`. -- When the image is inactive, the CSS classes are `background background--active` and `picture`. - -A single boolean state variable is enough to remember whether the image is active. The original task was to remove or add CSS classes. However, in React you need to _describe_ what you want to see rather than _manipulate_ the UI elements. So you need to calculate both CSS classes based on the current state. You also need to [stop the propagation](/learn/responding-to-events#stopping-propagation) so that clicking the image doesn't register as a click on the background. - -Verify that this version works by clicking the image and then outside of it: - -=== "app.py" - - ```python - {% include "../../examples/python/managing_state/stateful_picture_component.py" start="# start" %} - ``` - -=== "styles.css" - - ```css - {% include "../../examples/css/managing_state/picture_component.css" %} - ``` - -=== ":material-play: Run" + === "styles.css" - ```python - # TODO - ``` + ```css + {% include "../../examples/managing_state/picture_component.css" %} + ``` -Alternatively, you could return two separate chunks of PSX: + ??? "Show Solution" -=== "app.py" + This component has two visual states: when the image is active, and when the image is inactive: - ```python - {% include "../../examples/python/managing_state/stateful_picture_component.py" start="# start" %} - ``` + - When the image is active, the CSS classes are `background` and `picture picture--active`. + - When the image is inactive, the CSS classes are `background background--active` and `picture`. -=== "styles.css" + A single boolean state variable is enough to remember whether the image is active. The original task was to remove or add CSS classes. However, in React you need to _describe_ what you want to see rather than _manipulate_ the UI elements. So you need to calculate both CSS classes based on the current state. You also need to [stop the propagation](./responding-to-events.md#stopping-propagation) so that clicking the image doesn't register as a click on the background. - ```css - {% include "../../examples/css/managing_state/picture_component.css" %} - ``` + Verify that this version works by clicking the image and then outside of it: -=== ":material-play: Run" + === "app.py" - ```python - # TODO - ``` + ```python + {% include "../../examples/managing_state/stateful_picture_component.py" start="# start" %} + ``` -Keep in mind that if two different PSX chunks describe the same tree, their nesting (first `html.div` → first `html.img`) has to line up. Otherwise, toggling `is_active` would recreate the whole tree below and [reset its state.](/learn/preserving-and-resetting-state) This is why, if a similar PSX tree gets returned in both cases, it is better to write them as a single piece of PSX. - -</Solution> - -#### Profile editor - -Here is a small form implemented with plain JavaScript and DOM. Play with it to understand its behavior: - -```js -function handleFormSubmit(e) { - e.preventDefault(); - if (editButton.textContent === "Edit Profile") { - editButton.textContent = "Save Profile"; - hide(firstNameText); - hide(lastNameText); - show(firstNameInput); - show(lastNameInput); - } else { - editButton.textContent = "Edit Profile"; - hide(firstNameInput); - hide(lastNameInput); - show(firstNameText); - show(lastNameText); - } -} - -function handleFirstNameChange() { - firstNameText.textContent = firstNameInput.value; - helloText.textContent = - "Hello " + firstNameInput.value + " " + lastNameInput.value + "!"; -} - -function handleLastNameChange() { - lastNameText.textContent = lastNameInput.value; - helloText.textContent = - "Hello " + firstNameInput.value + " " + lastNameInput.value + "!"; -} - -function hide(el) { - el.style.display = "none"; -} - -function show(el) { - el.style.display = ""; -} - -let form = document.getElementById("form"); -let editButton = document.getElementById("editButton"); -let firstNameInput = document.getElementById("firstNameInput"); -let firstNameText = document.getElementById("firstNameText"); -let lastNameInput = document.getElementById("lastNameInput"); -let lastNameText = document.getElementById("lastNameText"); -let helloText = document.getElementById("helloText"); -form.onsubmit = handleFormSubmit; -firstNameInput.oninput = handleFirstNameChange; -lastNameInput.oninput = handleLastNameChange; -``` + === "styles.css" -```js -{ - "hardReloadOnChange": true -} -``` + ```css + {% include "../../examples/managing_state/picture_component.css" %} + ``` -```html -<form id="form"> - <label> - First name: - <b id="firstNameText">Jane</b> - <input id="firstNameInput" value="Jane" style="display: none" /> - </label> - <label> - Last name: - <b id="lastNameText">Jacobs</b> - <input id="lastNameInput" value="Jacobs" style="display: none" /> - </label> - <button type="submit" id="editButton">Edit Profile</button> - <p><i id="helloText">Hello, Jane Jacobs!</i></p> -</form> - -<style> - * { - box-sizing: border-box; - } - body { - font-family: sans-serif; - margin: 20px; - padding: 0; - } - label { - display: block; - margin-bottom: 20px; - } -</style> -``` + === ":material-play: Run" -This form switches between two modes: in the editing mode, you see the inputs, and in the viewing mode, you only see the result. The button label changes between "Edit" and "Save" depending on the mode you're in. When you change the inputs, the welcome message at the bottom updates in real time. - -Your task is to reimplement it in React in the sandbox below. For your convenience, the markup was already converted to JSX, but you'll need to make it show and hide the inputs like the original does. - -Make sure that it updates the text at the bottom, too! - -```js -export default function EditProfile() { - return ( - <form> - <label> - First name: <b>Jane</b> - <input /> - </label> - <label> - Last name: <b>Jacobs</b> - <input /> - </label> - <button type="submit">Edit Profile</button> - <p> - <i>Hello, Jane Jacobs!</i> - </p> - </form> - ); -} -``` + ```python + # TODO + ``` -```css -label { - display: block; - margin-bottom: 20px; -} -``` + Alternatively, you could return two separate chunks of HTML: -<Solution> - -You will need two state variables to hold the input values: `firstName` and `lastName`. You're also going to need an `isEditing` state variable that holds whether to display the inputs or not. You should _not_ need a `fullName` variable because the full name can always be calculated from the `firstName` and the `lastName`. - -Finally, you should use [conditional rendering](/learn/conditional-rendering) to show or hide the inputs depending on `isEditing`. - -```js -import { useState } from "react"; - -export default function EditProfile() { - const [isEditing, setIsEditing] = useState(false); - const [firstName, setFirstName] = useState("Jane"); - const [lastName, setLastName] = useState("Jacobs"); - - return ( - <form - onSubmit={(e) => { - e.preventDefault(); - setIsEditing(!isEditing); - }} - > - <label> - First name:{" "} - {isEditing ? ( - <input - value={firstName} - onChange={(e) => { - setFirstName(e.target.value); - }} - /> - ) : ( - <b>{firstName}</b> - )} - </label> - <label> - Last name:{" "} - {isEditing ? ( - <input - value={lastName} - onChange={(e) => { - setLastName(e.target.value); - }} - /> - ) : ( - <b>{lastName}</b> - )} - </label> - <button type="submit">{isEditing ? "Save" : "Edit"} Profile</button> - <p> - <i> - Hello, {firstName} {lastName}! - </i> - </p> - </form> - ); -} -``` + === "app.py" -```css -label { - display: block; - margin-bottom: 20px; -} -``` + ```python + {% include "../../examples/managing_state/stateful_picture_component.py" start="# start" %} + ``` -Compare this solution to the original imperative code. How are they different? - -</Solution> - -#### Refactor the imperative solution without React - -Here is the original sandbox from the previous challenge, written imperatively without React: - -```js -function handleFormSubmit(e) { - e.preventDefault(); - if (editButton.textContent === "Edit Profile") { - editButton.textContent = "Save Profile"; - hide(firstNameText); - hide(lastNameText); - show(firstNameInput); - show(lastNameInput); - } else { - editButton.textContent = "Edit Profile"; - hide(firstNameInput); - hide(lastNameInput); - show(firstNameText); - show(lastNameText); - } -} - -function handleFirstNameChange() { - firstNameText.textContent = firstNameInput.value; - helloText.textContent = - "Hello " + firstNameInput.value + " " + lastNameInput.value + "!"; -} - -function handleLastNameChange() { - lastNameText.textContent = lastNameInput.value; - helloText.textContent = - "Hello " + firstNameInput.value + " " + lastNameInput.value + "!"; -} - -function hide(el) { - el.style.display = "none"; -} - -function show(el) { - el.style.display = ""; -} - -let form = document.getElementById("form"); -let editButton = document.getElementById("editButton"); -let firstNameInput = document.getElementById("firstNameInput"); -let firstNameText = document.getElementById("firstNameText"); -let lastNameInput = document.getElementById("lastNameInput"); -let lastNameText = document.getElementById("lastNameText"); -let helloText = document.getElementById("helloText"); -form.onsubmit = handleFormSubmit; -firstNameInput.oninput = handleFirstNameChange; -lastNameInput.oninput = handleLastNameChange; -``` + === "styles.css" -```js -{ - "hardReloadOnChange": true -} -``` + ```css + {% include "../../examples/managing_state/picture_component.css" %} + ``` -```html -<form id="form"> - <label> - First name: - <b id="firstNameText">Jane</b> - <input id="firstNameInput" value="Jane" style="display: none" /> - </label> - <label> - Last name: - <b id="lastNameText">Jacobs</b> - <input id="lastNameInput" value="Jacobs" style="display: none" /> - </label> - <button type="submit" id="editButton">Edit Profile</button> - <p><i id="helloText">Hello, Jane Jacobs!</i></p> -</form> - -<style> - * { - box-sizing: border-box; - } - body { - font-family: sans-serif; - margin: 20px; - padding: 0; - } - label { - display: block; - margin-bottom: 20px; - } -</style> -``` + === ":material-play: Run" -Imagine React didn't exist. Can you refactor this code in a way that makes the logic less fragile and more similar to the React version? What would it look like if the state was explicit, like in React? - -If you're struggling to think where to start, the stub below already has most of the structure in place. If you start here, fill in the missing logic in the `updateDOM` function. (Refer to the original code where needed.) - -```js -let firstName = "Jane"; -let lastName = "Jacobs"; -let isEditing = false; - -function handleFormSubmit(e) { - e.preventDefault(); - setIsEditing(!isEditing); -} - -function handleFirstNameChange(e) { - setFirstName(e.target.value); -} - -function handleLastNameChange(e) { - setLastName(e.target.value); -} - -function setFirstName(value) { - firstName = value; - updateDOM(); -} - -function setLastName(value) { - lastName = value; - updateDOM(); -} - -function setIsEditing(value) { - isEditing = value; - updateDOM(); -} - -function updateDOM() { - if (isEditing) { - editButton.textContent = "Save Profile"; - // TODO: show inputs, hide content - } else { - editButton.textContent = "Edit Profile"; - // TODO: hide inputs, show content - } - // TODO: update text labels -} - -function hide(el) { - el.style.display = "none"; -} - -function show(el) { - el.style.display = ""; -} - -let form = document.getElementById("form"); -let editButton = document.getElementById("editButton"); -let firstNameInput = document.getElementById("firstNameInput"); -let firstNameText = document.getElementById("firstNameText"); -let lastNameInput = document.getElementById("lastNameInput"); -let lastNameText = document.getElementById("lastNameText"); -let helloText = document.getElementById("helloText"); -form.onsubmit = handleFormSubmit; -firstNameInput.oninput = handleFirstNameChange; -lastNameInput.oninput = handleLastNameChange; -``` + ```python + # TODO + ``` -```js -{ - "hardReloadOnChange": true -} -``` + Keep in mind that if two different HTML chunks describe the same tree, their nesting (first `html.div` → first `html.img`) has to line up. Otherwise, toggling `is_active` would recreate the whole tree below and [reset its state.](./preserving-and-resetting-state.md) This is why, if a similar HTML tree gets returned in both cases, it is better to write them as a single piece of HTML. -```html -<form id="form"> - <label> - First name: - <b id="firstNameText">Jane</b> - <input id="firstNameInput" value="Jane" style="display: none" /> - </label> - <label> - Last name: - <b id="lastNameText">Jacobs</b> - <input id="lastNameInput" value="Jacobs" style="display: none" /> - </label> - <button type="submit" id="editButton">Edit Profile</button> - <p><i id="helloText">Hello, Jane Jacobs!</i></p> -</form> - -<style> - * { - box-sizing: border-box; - } - body { - font-family: sans-serif; - margin: 20px; - padding: 0; - } - label { - display: block; - margin-bottom: 20px; - } -</style> -``` + </div> -<Solution> - -The missing logic included toggling the display of inputs and content, and updating the labels: - -```js -let firstName = "Jane"; -let lastName = "Jacobs"; -let isEditing = false; - -function handleFormSubmit(e) { - e.preventDefault(); - setIsEditing(!isEditing); -} - -function handleFirstNameChange(e) { - setFirstName(e.target.value); -} - -function handleLastNameChange(e) { - setLastName(e.target.value); -} - -function setFirstName(value) { - firstName = value; - updateDOM(); -} - -function setLastName(value) { - lastName = value; - updateDOM(); -} - -function setIsEditing(value) { - isEditing = value; - updateDOM(); -} - -function updateDOM() { - if (isEditing) { - editButton.textContent = "Save Profile"; - hide(firstNameText); - hide(lastNameText); - show(firstNameInput); - show(lastNameInput); - } else { - editButton.textContent = "Edit Profile"; - hide(firstNameInput); - hide(lastNameInput); - show(firstNameText); - show(lastNameText); - } - firstNameText.textContent = firstName; - lastNameText.textContent = lastName; - helloText.textContent = "Hello " + firstName + " " + lastName + "!"; -} - -function hide(el) { - el.style.display = "none"; -} - -function show(el) { - el.style.display = ""; -} - -let form = document.getElementById("form"); -let editButton = document.getElementById("editButton"); -let firstNameInput = document.getElementById("firstNameInput"); -let firstNameText = document.getElementById("firstNameText"); -let lastNameInput = document.getElementById("lastNameInput"); -let lastNameText = document.getElementById("lastNameText"); -let helloText = document.getElementById("helloText"); -form.onsubmit = handleFormSubmit; -firstNameInput.oninput = handleFirstNameChange; -lastNameInput.oninput = handleLastNameChange; -``` +=== "2. Profile editor" -```js -{ - "hardReloadOnChange": true -} -``` + <div class="challenge"> -```html -<form id="form"> - <label> - First name: - <b id="firstNameText">Jane</b> - <input id="firstNameInput" value="Jane" style="display: none" /> - </label> - <label> - Last name: - <b id="lastNameText">Jacobs</b> - <input id="lastNameInput" value="Jacobs" style="display: none" /> - </label> - <button type="submit" id="editButton">Edit Profile</button> - <p><i id="helloText">Hello, Jane Jacobs!</i></p> -</form> - -<style> - * { - box-sizing: border-box; - } - body { - font-family: sans-serif; - margin: 20px; - padding: 0; - } - label { - display: block; - margin-bottom: 20px; - } -</style> -``` + <font size="4">**Challenge 2 of 3: Profile editor**</font> -The `updateDOM` function you wrote shows what React does under the hood when you set the state. (However, React also avoids touching the DOM for properties that have not changed since the last time they were set.) + Here is a small form implemented with plain JavaScript and DOM. Play with it to understand its behavior: -</Solution> + === "index.js" -</Challenges> + ```js + function handleFormSubmit(e) { + e.preventDefault(); + if (editButton.textContent === "Edit Profile") { + editButton.textContent = "Save Profile"; + hide(firstNameText); + hide(lastNameText); + show(firstNameInput); + show(lastNameInput); + } else { + editButton.textContent = "Edit Profile"; + hide(firstNameInput); + hide(lastNameInput); + show(firstNameText); + show(lastNameText); + } + } + + function handleFirstNameChange() { + firstNameText.textContent = firstNameInput.value; + helloText.textContent = + "Hello " + firstNameInput.value + " " + lastNameInput.value + "!"; + } + + function handleLastNameChange() { + lastNameText.textContent = lastNameInput.value; + helloText.textContent = + "Hello " + firstNameInput.value + " " + lastNameInput.value + "!"; + } + + function hide(el) { + el.style.display = "none"; + } + + function show(el) { + el.style.display = ""; + } + + let form = document.getElementById("form"); + let editButton = document.getElementById("editButton"); + let firstNameInput = document.getElementById("firstNameInput"); + let firstNameText = document.getElementById("firstNameText"); + let lastNameInput = document.getElementById("lastNameInput"); + let lastNameText = document.getElementById("lastNameText"); + let helloText = document.getElementById("helloText"); + form.onsubmit = handleFormSubmit; + firstNameInput.oninput = handleFirstNameChange; + lastNameInput.oninput = handleLastNameChange; + ``` + + === "index.html" + + ```html + <form id="form"> + <label> + First name: + <b id="firstNameText">Jane</b> + <input id="firstNameInput" value="Jane" style="display: none" /> + </label> + <label> + Last name: + <b id="lastNameText">Jacobs</b> + <input id="lastNameInput" value="Jacobs" style="display: none" /> + </label> + <button type="submit" id="editButton">Edit Profile</button> + <p><i id="helloText">Hello, Jane Jacobs!</i></p> + </form> + + <style> + * { + box-sizing: border-box; + } + body { + font-family: sans-serif; + margin: 20px; + padding: 0; + } + label { + display: block; + margin-bottom: 20px; + } + </style> + ``` + + This form switches between two modes: in the editing mode, you see the inputs, and in the viewing mode, you only see the result. The button label changes between "Edit" and "Save" depending on the mode you're in. When you change the inputs, the welcome message at the bottom updates in real time. + + Your task is to reimplement it in React in the sandbox below. For your convenience, the markup was already converted to JSX, but you'll need to make it show and hide the inputs like the original does. + + Make sure that it updates the text at the bottom, too! + + === "app.py" + + ```js + export default function EditProfile() { + return ( + <form> + <label> + First name: <b>Jane</b> + <input /> + </label> + <label> + Last name: <b>Jacobs</b> + <input /> + </label> + <button type="submit">Edit Profile</button> + <p> + <i>Hello, Jane Jacobs!</i> + </p> + </form> + ); + } + ``` + + === "styles.css" + + ```css + label { + display: block; + margin-bottom: 20px; + } + ``` + + ??? note "Show Solution" + + You will need two state variables to hold the input values: `firstName` and `lastName`. You're also going to need an `isEditing` state variable that holds whether to display the inputs or not. You should _not_ need a `fullName` variable because the full name can always be calculated from the `firstName` and the `lastName`. + + Finally, you should use [conditional rendering](./conditional-rendering.md) to show or hide the inputs depending on `isEditing`. + + === "app.py" + + ```js + import { useState } from "react"; + + export default function EditProfile() { + const [isEditing, setIsEditing] = useState(false); + const [firstName, setFirstName] = useState("Jane"); + const [lastName, setLastName] = useState("Jacobs"); + + return ( + <form + onSubmit={(e) => { + e.preventDefault(); + setIsEditing(!isEditing); + }} + > + <label> + First name:{" "} + {isEditing ? ( + <input + value={firstName} + onChange={(e) => { + setFirstName(e.target.value); + }} + /> + ) : ( + <b>{firstName}</b> + )} + </label> + <label> + Last name:{" "} + {isEditing ? ( + <input + value={lastName} + onChange={(e) => { + setLastName(e.target.value); + }} + /> + ) : ( + <b>{lastName}</b> + )} + </label> + <button type="submit">{isEditing ? "Save" : "Edit"} Profile</button> + <p> + <i> + Hello, {firstName} {lastName}! + </i> + </p> + </form> + ); + } + ``` + + === "styles.css" + + ```css + label { + display: block; + margin-bottom: 20px; + } + ``` + + === ":material-play: Run" + + ```html + # TODO + ``` + + Compare this solution to the original imperative code. How are they different? + + </div> + +=== "3. Refactor the imperative solution without React" + + <div class="challenge"> + + <font size="4">**Challenge 3 of 3: Refactor the imperative solution without React**</font> + + Here is the original sandbox from the previous challenge, written imperatively without React: + + === "app.py" + + ```js + function handleFormSubmit(e) { + e.preventDefault(); + if (editButton.textContent === "Edit Profile") { + editButton.textContent = "Save Profile"; + hide(firstNameText); + hide(lastNameText); + show(firstNameInput); + show(lastNameInput); + } else { + editButton.textContent = "Edit Profile"; + hide(firstNameInput); + hide(lastNameInput); + show(firstNameText); + show(lastNameText); + } + } + + function handleFirstNameChange() { + firstNameText.textContent = firstNameInput.value; + helloText.textContent = + "Hello " + firstNameInput.value + " " + lastNameInput.value + "!"; + } + + function handleLastNameChange() { + lastNameText.textContent = lastNameInput.value; + helloText.textContent = + "Hello " + firstNameInput.value + " " + lastNameInput.value + "!"; + } + + function hide(el) { + el.style.display = "none"; + } + + function show(el) { + el.style.display = ""; + } + + let form = document.getElementById("form"); + let editButton = document.getElementById("editButton"); + let firstNameInput = document.getElementById("firstNameInput"); + let firstNameText = document.getElementById("firstNameText"); + let lastNameInput = document.getElementById("lastNameInput"); + let lastNameText = document.getElementById("lastNameText"); + let helloText = document.getElementById("helloText"); + form.onsubmit = handleFormSubmit; + firstNameInput.oninput = handleFirstNameChange; + lastNameInput.oninput = handleLastNameChange; + ``` + + === "index.html" + + ```html + <form id="form"> + <label> + First name: + <b id="firstNameText">Jane</b> + <input id="firstNameInput" value="Jane" style="display: none" /> + </label> + <label> + Last name: + <b id="lastNameText">Jacobs</b> + <input id="lastNameInput" value="Jacobs" style="display: none" /> + </label> + <button type="submit" id="editButton">Edit Profile</button> + <p><i id="helloText">Hello, Jane Jacobs!</i></p> + </form> + + <style> + * { + box-sizing: border-box; + } + body { + font-family: sans-serif; + margin: 20px; + padding: 0; + } + label { + display: block; + margin-bottom: 20px; + } + </style> + ``` + + Imagine React didn't exist. Can you refactor this code in a way that makes the logic less fragile and more similar to the React version? What would it look like if the state was explicit, like in React? + + If you're struggling to think where to start, the stub below already has most of the structure in place. If you start here, fill in the missing logic in the `updateDOM` function. (Refer to the original code where needed.) + + === "app.py" + + ```js + let firstName = "Jane"; + let lastName = "Jacobs"; + let isEditing = false; + + function handleFormSubmit(e) { + e.preventDefault(); + setIsEditing(!isEditing); + } + + function handleFirstNameChange(e) { + setFirstName(e.target.value); + } + + function handleLastNameChange(e) { + setLastName(e.target.value); + } + + function setFirstName(value) { + firstName = value; + updateDOM(); + } + + function setLastName(value) { + lastName = value; + updateDOM(); + } + + function setIsEditing(value) { + isEditing = value; + updateDOM(); + } + + function updateDOM() { + if (isEditing) { + editButton.textContent = "Save Profile"; + // TODO: show inputs, hide content + } else { + editButton.textContent = "Edit Profile"; + // TODO: hide inputs, show content + } + // TODO: update text labels + } + + function hide(el) { + el.style.display = "none"; + } + + function show(el) { + el.style.display = ""; + } + + let form = document.getElementById("form"); + let editButton = document.getElementById("editButton"); + let firstNameInput = document.getElementById("firstNameInput"); + let firstNameText = document.getElementById("firstNameText"); + let lastNameInput = document.getElementById("lastNameInput"); + let lastNameText = document.getElementById("lastNameText"); + let helloText = document.getElementById("helloText"); + form.onsubmit = handleFormSubmit; + firstNameInput.oninput = handleFirstNameChange; + lastNameInput.oninput = handleLastNameChange; + ``` + + === "index.html" + + ```html + <form id="form"> + <label> + First name: + <b id="firstNameText">Jane</b> + <input id="firstNameInput" value="Jane" style="display: none" /> + </label> + <label> + Last name: + <b id="lastNameText">Jacobs</b> + <input id="lastNameInput" value="Jacobs" style="display: none" /> + </label> + <button type="submit" id="editButton">Edit Profile</button> + <p><i id="helloText">Hello, Jane Jacobs!</i></p> + </form> + + <style> + * { + box-sizing: border-box; + } + body { + font-family: sans-serif; + margin: 20px; + padding: 0; + } + label { + display: block; + margin-bottom: 20px; + } + </style> + ``` + + ??? "Show solution" + + The missing logic included toggling the display of inputs and content, and updating the labels: + + === "app.py" + + ```js + let firstName = "Jane"; + let lastName = "Jacobs"; + let isEditing = false; + + function handleFormSubmit(e) { + e.preventDefault(); + setIsEditing(!isEditing); + } + + function handleFirstNameChange(e) { + setFirstName(e.target.value); + } + + function handleLastNameChange(e) { + setLastName(e.target.value); + } + + function setFirstName(value) { + firstName = value; + updateDOM(); + } + + function setLastName(value) { + lastName = value; + updateDOM(); + } + + function setIsEditing(value) { + isEditing = value; + updateDOM(); + } + + function updateDOM() { + if (isEditing) { + editButton.textContent = "Save Profile"; + hide(firstNameText); + hide(lastNameText); + show(firstNameInput); + show(lastNameInput); + } else { + editButton.textContent = "Edit Profile"; + hide(firstNameInput); + hide(lastNameInput); + show(firstNameText); + show(lastNameText); + } + firstNameText.textContent = firstName; + lastNameText.textContent = lastName; + helloText.textContent = "Hello " + firstName + " " + lastName + "!"; + } + + function hide(el) { + el.style.display = "none"; + } + + function show(el) { + el.style.display = ""; + } + + let form = document.getElementById("form"); + let editButton = document.getElementById("editButton"); + let firstNameInput = document.getElementById("firstNameInput"); + let firstNameText = document.getElementById("firstNameText"); + let lastNameInput = document.getElementById("lastNameInput"); + let lastNameText = document.getElementById("lastNameText"); + let helloText = document.getElementById("helloText"); + form.onsubmit = handleFormSubmit; + firstNameInput.oninput = handleFirstNameChange; + lastNameInput.oninput = handleLastNameChange; + ``` + + === "index.html" + + ```html + <form id="form"> + <label> + First name: + <b id="firstNameText">Jane</b> + <input id="firstNameInput" value="Jane" style="display: none" /> + </label> + <label> + Last name: + <b id="lastNameText">Jacobs</b> + <input id="lastNameInput" value="Jacobs" style="display: none" /> + </label> + <button type="submit" id="editButton">Edit Profile</button> + <p><i id="helloText">Hello, Jane Jacobs!</i></p> + </form> + + <style> + * { + box-sizing: border-box; + } + body { + font-family: sans-serif; + margin: 20px; + padding: 0; + } + label { + display: block; + margin-bottom: 20px; + } + </style> + ``` + + === ":material-play: Run" + + ```html + # TODO + ``` + + The `updateDOM` function you wrote shows what React does under the hood when you set the state. (However, React also avoids touching the DOM for properties that have not changed since the last time they were set.) + + </div>