diff --git a/docs/examples/managing_state/all_possible_states.py b/docs/examples/managing_state/all_possible_states.py new file mode 100644 index 000000000..a71f9b54a --- /dev/null +++ b/docs/examples/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/managing_state/alt_stateful_picture_component.py b/docs/examples/managing_state/alt_stateful_picture_component.py new file mode 100644 index 000000000..9cdfb18c2 --- /dev/null +++ b/docs/examples/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( + { + "className": "background", + "onClick": lambda event: set_is_active(False), + }, + html.img( + { + "onClick": event(stop_propagation=True), + "className": "picture picture--active", + "alt": "Rainbow houses in Kampung Pelangi, Indonesia", + "src": "https://i.imgur.com/5qwVYb1.jpeg", + } + ), + ) + else: + return html.div( + {"className": "background background--active"}, + html.img( + { + "onClick": event( + lambda event: set_is_active(True), stop_propagation=True + ), + "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 new file mode 100644 index 000000000..ecf5c88f3 --- /dev/null +++ b/docs/examples/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!") + + 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.css b/docs/examples/managing_state/conditional_form_component.css new file mode 100644 index 000000000..2cc8b7afe --- /dev/null +++ b/docs/examples/managing_state/conditional_form_component.css @@ -0,0 +1,3 @@ +.Error { + color: red; +} diff --git a/docs/examples/managing_state/conditional_form_component.py b/docs/examples/managing_state/conditional_form_component.py new file mode 100644 index 000000000..65307a829 --- /dev/null +++ b/docs/examples/managing_state/conditional_form_component.py @@ -0,0 +1,35 @@ +from reactpy import component, html + + +# start +@component +def error(status): + if status == "error": + return html.p( + {"className": "error"}, "Good guess but a wrong answer. Try again!" + ) + + return "" + + +@component +def form(status="empty"): + # Try "submitting", "error", "success": + if status == "success": + return html.h1("That's right!") + + 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.css b/docs/examples/managing_state/multiple_form_components.css new file mode 100644 index 000000000..b24e106e8 --- /dev/null +++ b/docs/examples/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/managing_state/multiple_form_components.py b/docs/examples/managing_state/multiple_form_components.py new file mode 100644 index 000000000..48d6c3ca2 --- /dev/null +++ b/docs/examples/managing_state/multiple_form_components.py @@ -0,0 +1,15 @@ +# start +from conditional_form_component import form + +from reactpy import component, html + + +@component +def item(status): + return html.section(html.h4("Form", status, ":"), form(status)) + + +@component +def app(): + statuses = ["empty", "typing", "submitting", "success", "error"] + return html.fragment([item(status) for status in statuses]) diff --git a/docs/examples/managing_state/necessary_states.py b/docs/examples/managing_state/necessary_states.py new file mode 100644 index 000000000..ee70c5686 --- /dev/null +++ b/docs/examples/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/managing_state/picture_component.css b/docs/examples/managing_state/picture_component.css new file mode 100644 index 000000000..85827067c --- /dev/null +++ b/docs/examples/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/managing_state/picture_component.py b/docs/examples/managing_state/picture_component.py new file mode 100644 index 000000000..9efc57b17 --- /dev/null +++ b/docs/examples/managing_state/picture_component.py @@ -0,0 +1,16 @@ +from reactpy import component, html + + +# start +@component +def picture(): + return html.div( + {"className": "background background--active"}, + html.img( + { + "className": "picture", + "alt": "Rainbow houses in Kampung Pelangi, Indonesia", + "src": "https://i.imgur.com/5qwVYb1.jpeg", + } + ), + ) diff --git a/docs/examples/managing_state/refactored_states.py b/docs/examples/managing_state/refactored_states.py new file mode 100644 index 000000000..3d080c92c --- /dev/null +++ b/docs/examples/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/managing_state/stateful_form_component.py b/docs/examples/managing_state/stateful_form_component.py new file mode 100644 index 000000000..d2ef5580d --- /dev/null +++ b/docs/examples/managing_state/stateful_form_component.py @@ -0,0 +1,69 @@ +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( + {"className": "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.fragment( + html.h2("City quiz"), + html.p( + "In which city is there a billboard that turns air into drinkable water?" + ), + html.form( + {"onSubmit": handle_submit}, + html.textarea( + { + "value": answer, + "onChange": 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/managing_state/stateful_picture_component.py b/docs/examples/managing_state/stateful_picture_component.py new file mode 100644 index 000000000..e3df7991c --- /dev/null +++ b/docs/examples/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_className = "background" + picture_className = "picture" + + if is_active: + picture_className += " picture--active" + else: + background_className += " background--active" + + @event(stop_propagation=True) + def handle_click(event): + set_is_active(True) + + return html.div( + {"className": background_className, "onClick": set_is_active(False)}, + html.img( + { + "onClick": handle_click, + "className": picture_className, + "alt": "Rainbow houses in Kampung Pelangi, Indonesia", + "src": "https://i.imgur.com/5qwVYb1.jpeg", + } + ), + ) diff --git a/docs/examples/tutorial_tic_tac_toe/tic_tac_toe.py b/docs/examples/tutorial_tic_tac_toe/tic_tac_toe.py index 5eed504d1..1362f422c 100644 --- a/docs/examples/tutorial_tic_tac_toe/tic_tac_toe.py +++ b/docs/examples/tutorial_tic_tac_toe/tic_tac_toe.py @@ -34,7 +34,7 @@ def inner(event): f"Winner: {winner}" if winner else "Next player: " + ("X" if x_is_next else "O") ) - return html._( + return html.fragment( html.div({"className": "status"}, status), html.div( {"className": "board-row"}, diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 77af61224..2594e705e 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -11,8 +11,7 @@ nav: - Add React to an Existing Project: learn/add-react-to-an-existing-project.md - Setup: - Editor Setup: learn/editor-setup.md - # - ReactPy Developer Tools 🚫: learn/react-developer-tools.md - - Tools, Modules, and Packages 🚧: learn/extra-tools-and-packages.md + - Tools, Libraries, and Packages: learn/tools-and-packages.md # - More Tutorials: # - "Tutorial: React Bootstrap 🚫": learn/tutorial-react-bootstrap.md # - "Tutorial: Material UI 🚫": learn/tutorial-material-ui.md @@ -35,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 @@ -85,8 +84,8 @@ nav: - Components and Hooks must be pure 🚧: reference/components-and-hooks-must-be-pure.md - React calls Components and Hooks 🚧: reference/react-calls-components-and-hooks.md - Rules of Hooks 🚧: reference/rules-of-hooks.md - # - Template Tags: - # - Jinja 🚧: reference/jinja.md + - Template Tags: + - Jinja 🚧: reference/jinja.md - Protocol Structure 🚧: reference/protocol-structure.md - Client API 🚧: reference/client-api.md - About: diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 77446c7c8..e639183c8 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -124,7 +124,7 @@

Add interactivity wherever you need it

Go full-stack with a framework

ReactPy is a library. It lets you put components together, but it doesn't prescribe how to do routing and - data fetching. To build an entire app with ReactPy, we recommend a backend framework like + data fetching. To build an entire app with ReactPy, you can leverage a backend framework like Django or Starlette.

diff --git a/docs/overrides/homepage_examples/add_interactivity.py b/docs/overrides/homepage_examples/add_interactivity.py index 9a7bf76f1..45d479504 100644 --- a/docs/overrides/homepage_examples/add_interactivity.py +++ b/docs/overrides/homepage_examples/add_interactivity.py @@ -17,7 +17,7 @@ def searchable_video_list(videos): search_text, set_search_text = use_state("") found_videos = filter_videos(videos, search_text) - return html._( + return html.fragment( search_input( {"onChange": lambda event: set_search_text(event["target"]["value"])}, value=search_text, 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/add-react-to-an-existing-project.md b/docs/src/learn/add-react-to-an-existing-project.md index d201c1b9e..db907d697 100644 --- a/docs/src/learn/add-react-to-an-existing-project.md +++ b/docs/src/learn/add-react-to-an-existing-project.md @@ -46,16 +46,16 @@ First, install ReactPy, Starlette, and your preferred ASGI webserver. Next, configure your ASGI framework to use ReactPy's Jinja2 template tag. The method for doing this will vary depending on the ASGI framework you are using. Below is an example that follow's [Starlette's documentation](https://www.starlette.io/templates/): +!!! abstract "Note" + + The `ReactPyJinja` extension enables a handful of [template tags](../reference/jinja.md) that allow you to render ReactPy components in your templates. The `component` tag is used to render a ReactPy SSR component, while the `pyscript_setup` and `pyscript_component` tags can be used together to render CSR components. + ```python linenums="0" hl_lines="6 11 17" {% include "../../examples/add_react_to_an_existing_project/asgi_configure_jinja.py" %} ``` Now you will need to wrap your existing ASGI application with ReactPy's middleware, define the dotted path to your root components, and render your components in your existing HTML templates. -!!! abstract "Note" - - The `ReactPyJinja` extension enables a handful of [template tags](/reference/templatetags/) that allow you to render ReactPy components in your templates. The `component` tag is used to render a ReactPy SSR component, while the `pyscript_setup` and `pyscript_component` tags can be used together to render CSR components. - === "main.py" ```python hl_lines="6 22" diff --git a/docs/src/learn/creating-a-react-app.md b/docs/src/learn/creating-a-react-app.md index 5669d055c..b6c7ad945 100644 --- a/docs/src/learn/creating-a-react-app.md +++ b/docs/src/learn/creating-a-react-app.md @@ -20,7 +20,7 @@ These standalone executors are the easiest way to get started with ReactPy, as t In order to serve the initial HTML page, you will need to run a server. The ASGI examples below use [Uvicorn](https://www.uvicorn.org/), but you can use [any ASGI server](https://github.com/florimondmanca/awesome-asgi#servers). - Executors on this page can either support client-side rendering ([CSR](https://developer.mozilla.org/en-US/docs/Glossary/CSR)) or server-side rendering ([SSR](https://developer.mozilla.org/en-US/docs/Glossary/SSR)) + Executors on this page can either support client-side rendering ([CSR](https://developer.mozilla.org/en-US/docs/Glossary/CSR)) or server-side rendering ([SSR](https://developer.mozilla.org/en-US/docs/Glossary/SSR)). ### Running via ASGI SSR @@ -93,3 +93,7 @@ Support for WSGI executors is coming in a [future version](https://github.com/re ### Running via WSGI CSR Support for WSGI executors is coming in a [future version](https://github.com/reactive-python/reactpy/issues/1260). + +### Running as a native app + +Support for native apps (iO, Android, Windows, etc.) is coming in a [future version](https://github.com/reactive-python/reactpy/issues/570). diff --git a/docs/src/learn/editor-setup.md b/docs/src/learn/editor-setup.md index 052f16663..7590f069b 100644 --- a/docs/src/learn/editor-setup.md +++ b/docs/src/learn/editor-setup.md @@ -17,7 +17,6 @@ A properly configured editor can make code clearer to read and faster to write. Other popular text editors used in the React community include: -- [WebStorm](https://www.jetbrains.com/webstorm/) is an integrated development environment designed specifically for JavaScript. - [Sublime Text](https://www.sublimetext.com/) has support for [syntax highlighting](https://stackoverflow.com/a/70960574/458193) and autocomplete built in. - [Vim](https://www.vim.org/) is a highly configurable text editor built to make creating and changing any kind of text very efficient. It is included as "vi" with most UNIX systems and with Apple OS X. @@ -25,46 +24,32 @@ Other popular text editors used in the React community include: Some editors come with these features built in, but others might require adding an extension. Check to see what support your editor of choice provides to be sure! -### Python Linting +### Linting -Linting is the process of running a program that will analyse code for potential errors. [Flake8](https://flake8.pycqa.org/en/latest/) is a popular, open source linter for Python. +Linting is the process of running a program that will analyse code for potential errors. [Ruff](https://docs.astral.sh/ruff/) is Reactive Python's linter of choice due to its speed and configurability. Additionally, we recommend using [Pyright](https://microsoft.github.io/pyright/) as a performant type checker for Python. -- [Install Flake8](https://flake8.pycqa.org/en/latest/#installation) (be sure you have [Python installed!](https://www.python.org/downloads/)) -- [Integrate Flake8 in VSCode with the official extension](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8) -- [Install Reactpy-Flake8](https://pypi.org/project/reactpy-flake8/) to lint your ReactPy code +Be sure you have [Python installed](https://www.python.org/downloads/) before you proceed! -### JavaScript Linting - -You typically won't use much JavaScript alongside ReactPy, but there are still some cases where you might. For example, you might want to use JavaScript to fetch data from an API or to add some interactivity to your app. - -In these cases, it's helpful to have a linter that can catch common mistakes in your code as you write it. [ESLint](https://eslint.org/) is a popular, open source linter for JavaScript. - -- [Install ESLint with the recommended configuration for React](https://www.npmjs.com/package/eslint-config-react-app) (be sure you have [Node installed!](https://nodejs.org/en/download/current/)) -- [Integrate ESLint in VSCode with the official extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - -**Make sure that you've enabled all the [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) rules for your project.** They are essential and catch the most severe bugs early. The recommended [`eslint-config-react-app`](https://www.npmjs.com/package/eslint-config-react-app) preset already includes them. +- [Install Ruff using the VSCode extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) or the [standalone package](https://docs.astral.sh/ruff/installation/). +- [Install Pyright using the VSCode extension](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) or the [standalone package](https://microsoft.github.io/pyright/#/installation). ### Formatting -The last thing you want to do when sharing your code with another contributor is get into an discussion about [tabs vs spaces](https://www.google.com/search?q=tabs+vs+spaces)! Fortunately, [Prettier](https://prettier.io/) will clean up your code by reformatting it to conform to preset, configurable rules. Run Prettier, and all your tabs will be converted to spaces—and your indentation, quotes, etc will also all be changed to conform to the configuration. In the ideal setup, Prettier will run when you save your file, quickly making these edits for you. +The last thing you want to do when sharing your code with another contributor is get into an discussion about [tabs vs spaces](https://www.google.com/search?q=tabs+vs+spaces)! Fortunately, [Ruff](https://docs.astral.sh/ruff/) will clean up your code by reformatting it to conform to preset, configurable rules. Run Ruff, and all your tabs will be converted to spaces—and your indentation, quotes, etc will also all be changed to conform to the configuration. In the ideal setup, Ruff will run when you save your file, quickly making these edits for you. -You can install the [Prettier extension in VSCode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) by following these steps: +You can install the [Ruff extension in VSCode](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) by following these steps: 1. Launch VS Code 2. Use Quick Open, press ++ctrl+p++ -3. Paste in `ext install esbenp.prettier-vscode` +3. Paste in `ext install charliermarsh.ruff` 4. Press Enter -#### Formatting on save +**Formatting on save** Ideally, you should format your code on every save. VS Code has settings for this! 1. In VS Code, press ++ctrl+shift+p++ -2. Type "settings" +2. Type "workspace settings" 3. Hit Enter -4. In the search bar, type "format on save" -5. Be sure the "format on save" option is ticked! - -!!! abstract "Note" - - If your ESLint preset has formatting rules, they may conflict with Prettier. We recommend disabling all formatting rules in your ESLint preset using [`eslint-config-prettier`](https://github.com/prettier/eslint-config-prettier) so that ESLint is _only_ used for catching logical mistakes. If you want to enforce that files are formatted before a pull request is merged, use [`prettier --check`](https://prettier.io/docs/en/cli.html#--check) for your continuous integration. +4. In the search bar, type "Format on save" +5. Be sure the "Format on save" option is ticked! diff --git a/docs/src/learn/extra-tools-and-packages.md b/docs/src/learn/extra-tools-and-packages.md deleted file mode 100644 index 17a619944..000000000 --- a/docs/src/learn/extra-tools-and-packages.md +++ /dev/null @@ -1,2 +0,0 @@ -- ReactPy Router -- ReactPy Flake8 diff --git a/docs/src/learn/reacting-to-input-with-state.md b/docs/src/learn/reacting-to-input-with-state.md index 4247a88d1..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; -``` - -```js -{ - "hardReloadOnChange": true -} -``` - -```html -
-

City quiz

-

What city is located on two continents?

- -
- - - -
-

That's right!

- - -``` +=== "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; + ``` + +=== "index.html" + + ```html +
+

City quiz

+

What city is located on two continents?

+ +
+ + + +
+

That's right!

+ + + ``` + +=== ":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). @@ -141,7 +145,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,136 +163,71 @@ 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

That's right!

; - } - return ( - <> -

City quiz

-

- In which city is there a billboard that turns air into drinkable - water? -

-
-