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..2cc8b7afe --- /dev/null +++ b/docs/examples/css/managing_state/conditional_form_component.css @@ -0,0 +1,3 @@ +.Error { + color: red; +} 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..b24e106e8 --- /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; +} 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..a71f9b54a --- /dev/null +++ b/docs/examples/python/managing_state/all_possible_states.py @@ -0,0 +1,8 @@ +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..1fdd9c1ad --- /dev/null +++ b/docs/examples/python/managing_state/alt_stateful_picture_component.py @@ -0,0 +1,37 @@ +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", + } + ), + ) 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..07328bff9 --- /dev/null +++ b/docs/examples/python/managing_state/basic_form_component.py @@ -0,0 +1,16 @@ +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")), + ) 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..44f4f565d --- /dev/null +++ b/docs/examples/python/managing_state/conditional_form_component.py @@ -0,0 +1,42 @@ +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 in ["empty", "submitting"] else "False" + ) + }, + "Submit", + ), + error(status), + ), + ) 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..4d50bd643 --- /dev/null +++ b/docs/examples/python/managing_state/multiple_form_components.py @@ -0,0 +1,16 @@ +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)) + + +@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..ee70c5686 --- /dev/null +++ b/docs/examples/python/managing_state/necessary_states.py @@ -0,0 +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 new file mode 100644 index 000000000..bc60a8143 --- /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", + } + ), + ) 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..3d080c92c --- /dev/null +++ b/docs/examples/python/managing_state/refactored_states.py @@ -0,0 +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' 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..c9d8ffa64 --- /dev/null +++ b/docs/examples/python/managing_state/stateful_form_component.py @@ -0,0 +1,70 @@ +import asyncio + +from reactpy import component, event, hooks, html + + +async def submit_form(*args): + 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 in ["empty", "submitting"] else "False" + ) + }, + "Submit", + ), + 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 new file mode 100644 index 000000000..38e338cbf --- /dev/null +++ b/docs/examples/python/managing_state/stateful_picture_component.py @@ -0,0 +1,30 @@ +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
- In which city is there a billboard that turns air into drinkable - water? -
- - > - ); -} -``` +=== "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- In which city is there a billboard that turns air into drinkable - water? -
- - > - ); -} -``` +=== "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 + ```- In which city is there a billboard that turns air into drinkable - water? -
- - > - ); -} - -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 ( -