├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .netlify ├── config.json └── state.json ├── .vscode └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets ├── logo-inkscape.svg ├── logo.png └── logo.svg ├── benches └── core.rs ├── book ├── book.toml └── src │ ├── 00-intro.md │ ├── 01-targets │ ├── 00-index.md │ ├── 01-web.md │ └── 99-new.md │ ├── 01-values.md │ ├── 99-dev │ ├── 00-index.md │ └── 01-releases.md │ └── SUMMARY.md ├── clippy.toml ├── dom ├── CHANGELOG.md ├── Cargo.toml ├── augdom │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ ├── event.rs │ │ ├── lib.rs │ │ ├── rsdom.rs │ │ ├── testing.rs │ │ └── webdom.rs ├── examples │ ├── counter_fn │ │ ├── Cargo.toml │ │ ├── index.html │ │ └── src │ │ │ └── lib.rs │ ├── dom_builder │ │ ├── Cargo.toml │ │ ├── index.html │ │ └── src │ │ │ └── lib.rs │ ├── drivertest │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── hacking │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── index.html │ │ └── src │ │ │ └── lib.rs │ ├── ssr │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ │ └── main.rs │ └── todo │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── e2e │ │ ├── cypress.json │ │ ├── cypress │ │ │ ├── fixtures │ │ │ │ └── example.json │ │ │ ├── integration │ │ │ │ └── app_spec.js │ │ │ └── support │ │ │ │ ├── commands.js │ │ │ │ └── index.js │ │ ├── package-lock.json │ │ └── package.json │ │ ├── index.html │ │ └── src │ │ ├── filter.rs │ │ ├── footer.rs │ │ ├── header.rs │ │ ├── input.rs │ │ ├── integration_tests.rs │ │ ├── item.rs │ │ ├── lib.rs │ │ └── main_section.rs ├── local-wasm-pack │ ├── Cargo.lock │ ├── Cargo.toml │ └── wasm-pack.rs ├── prettiest │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── raf │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── src │ ├── cached_node.rs │ ├── elements.rs │ ├── elements │ │ ├── embedding.rs │ │ ├── forms.rs │ │ ├── interactive.rs │ │ ├── media.rs │ │ ├── metadata.rs │ │ ├── scripting.rs │ │ ├── sectioning.rs │ │ ├── table.rs │ │ ├── text_content.rs │ │ └── text_semantics.rs │ ├── embed.rs │ ├── interfaces │ │ ├── content_categories.rs │ │ ├── element.rs │ │ ├── event_target.rs │ │ ├── global_events.rs │ │ ├── html_element.rs │ │ ├── mod.rs │ │ ├── node.rs │ │ └── security.rs │ ├── lib.rs │ ├── macros.rs │ └── text.rs └── tests │ ├── custom_component.rs │ └── dom_builder.rs ├── dyn-cache ├── CHANGELOG.md ├── Cargo.toml └── src │ ├── cache_cell.rs │ ├── definition.rs │ ├── dep_node.rs │ ├── lib.rs │ └── namespace.rs ├── favicon.ico ├── illicit ├── CHANGELOG.md ├── Cargo.toml ├── benches │ └── basic_env.rs ├── macro │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── src │ ├── anon_rc.rs │ ├── lib.rs │ └── snapshots │ ├── illicit__tests__failure_error.snap │ └── illicit__tests__layer_debug_impl.snap ├── index.css ├── index.html ├── mox ├── CHANGELOG.md ├── Cargo.toml ├── src │ └── lib.rs └── tests │ ├── dashes.rs │ ├── derive_builder.rs │ └── simple_builder.rs ├── netlify.toml ├── ofl ├── Cargo.lock ├── Cargo.toml ├── reloadOnChanges.js └── src │ ├── coverage.rs │ ├── format.rs │ ├── main.rs │ ├── published.rs │ ├── server.rs │ ├── server │ ├── inject.rs │ ├── run.rs │ └── session.rs │ ├── versions.rs │ ├── website.rs │ └── workspace.rs ├── rustfmt.toml ├── src ├── lib.rs ├── runtime.rs ├── runtime │ ├── context.rs │ ├── runloop.rs │ └── var.rs └── testing.rs ├── tests └── issue_238.rs └── topo ├── CHANGELOG.md ├── Cargo.toml ├── benches └── simple_calls.rs ├── macro ├── Cargo.toml └── src │ └── lib.rs ├── src ├── lib.rs └── slot.rs └── tests └── simple.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | #################################################################################################### 3 | # core crates 4 | 5 | core-flow = """ 6 | watch --clear 7 | -w benches 8 | -w dyn-cache 9 | -w illicit 10 | -w mox 11 | -w src 12 | -w tests 13 | -w topo 14 | -w Cargo.toml 15 | -x clippy-core 16 | -x test-core 17 | -x test-core-doc 18 | """ 19 | 20 | clippy-core = """clippy 21 | --package dyn-cache 22 | --package illicit 23 | --package illicit-macro 24 | --package mox 25 | --package moxie 26 | --package topo 27 | --package topo-macro 28 | """ 29 | test-core = """test --all-targets 30 | --package dyn-cache 31 | --package illicit 32 | --package illicit-macro 33 | --package mox 34 | --package moxie 35 | --package topo 36 | --package topo-macro 37 | """ 38 | test-core-doc = """test --doc 39 | --package dyn-cache 40 | --package illicit 41 | --package illicit-macro 42 | --package mox 43 | --package moxie 44 | --package topo 45 | --package topo-macro 46 | """ 47 | 48 | docs-all = "doc --workspace --no-deps --all-features" 49 | 50 | #################################################################################################### 51 | # dom crates and examples 52 | 53 | dom-flow = """ 54 | watch --clear 55 | -x build-dom-counter-fn 56 | -x build-dom-dom-builder 57 | -x build-dom-todo 58 | -x build-dom-hacking 59 | -x test-prettiest 60 | -x test-dom 61 | -x test-dom-doc 62 | -x test-augdom 63 | -x test-dom-counter-fn 64 | -x test-dom-dom-builder 65 | -x test-dom-todo 66 | -x test-dom-lib-browser 67 | -x test-dom-drivertest 68 | -x test-dom-hacking 69 | -x clippy-dom 70 | """ 71 | 72 | wa-pack = "run --manifest-path dom/local-wasm-pack/Cargo.toml --" 73 | wa-pack-build = "wa-pack build --target web --out-name index" 74 | wa-test = "wa-pack test --chrome --headless" 75 | 76 | build-dom-lib = "wa-pack-build dom" 77 | build-dom-counter-fn = "wa-pack-build dom/examples/counter_fn" 78 | build-dom-dom-builder = "wa-pack-build dom/examples/dom_builder" 79 | build-dom-hacking = "wa-pack-build dom/examples/hacking" 80 | build-dom-todo = "wa-pack-build dom/examples/todo" 81 | 82 | # browser tests 83 | test-augdom = "wa-test dom/augdom" 84 | test-prettiest = "wa-test dom/prettiest" 85 | test-dom-counter-fn = "wa-test dom/examples/counter_fn" 86 | test-dom-dom-builder = "wa-test dom/examples/dom_builder" 87 | test-dom-lib-browser = "wa-test dom" 88 | test-dom-drivertest = "wa-test dom/examples/drivertest" 89 | test-dom-hacking = "wa-test dom/examples/hacking" 90 | test-dom-todo = "wa-test dom/examples/todo" 91 | test-dom-todo-e2e = """ 92 | ofl serve-then-run 93 | --cargo-before build-dom-todo 94 | --cwd dom/examples/todo/e2e -- 95 | npx cypress run 96 | """ 97 | 98 | # standalones 99 | test-dom = "test --package moxie-dom --package ssr-poc --all-targets" 100 | test-dom-doc = "test --package moxie-dom --package ssr-poc --doc" 101 | 102 | # dom utilities 103 | clippy-dom = """clippy 104 | --all-targets 105 | --all-features 106 | --package moxie-dom 107 | --package dom-hacking 108 | --package todomvc-moxie 109 | """ 110 | 111 | #################################################################################################### 112 | # ofl 113 | 114 | ofl = "run --manifest-path ofl/Cargo.toml --release --" 115 | server = "watch -w ofl/ -x ofl" 116 | 117 | ofl-flow = """ 118 | watch --clear -w ofl 119 | -x clippy-ofl 120 | -x test-ofl 121 | -x docs-ofl 122 | """ 123 | site-flow = "watch --clear -x ofl-build-website" 124 | 125 | 126 | ofl-build-website = "ofl website build target/website" 127 | ofl-fmt-project = "ofl fmt" 128 | clippy-ofl = "clippy --manifest-path ofl/Cargo.toml --workspace" 129 | test-ofl = "test --manifest-path ofl/Cargo.toml --workspace" 130 | docs-ofl = "doc --manifest-path ofl/Cargo.toml --workspace --no-deps" 131 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: quick-xml 11 | versions: 12 | - 0.21.0 13 | - dependency-name: hashbrown 14 | versions: 15 | - 0.10.0 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | target/ 3 | **/*.rs.bk 4 | /Cargo.lock 5 | *.log 6 | *.new 7 | node_modules/ 8 | .idea/ 9 | pkg/ 10 | cargo-timing*.html 11 | **/cypress/screenshots 12 | **/cypress/videos -------------------------------------------------------------------------------- /.netlify/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "telemetryDisabled": true 3 | } -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "3ad3f9c1-495b-4558-987c-ab0363f47651" 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "core crates", 8 | "type": "shell", 9 | "presentation": { 10 | "group": "main", 11 | "panel": "dedicated" 12 | }, 13 | "runOptions": { 14 | "runOn": "folderOpen" 15 | }, 16 | "isBackground": true, 17 | "command": "cargo", 18 | "args": [ 19 | "core-flow" 20 | ], 21 | "problemMatcher": [ 22 | "$rustc-watch" 23 | ] 24 | }, 25 | { 26 | "label": "dom crates", 27 | "type": "shell", 28 | "presentation": { 29 | "group": "main", 30 | "panel": "dedicated" 31 | }, 32 | "runOptions": { 33 | "runOn": "folderOpen" 34 | }, 35 | "isBackground": true, 36 | "command": "cargo", 37 | "args": [ 38 | "dom-flow" 39 | ], 40 | "problemMatcher": [ 41 | "$rustc-watch" 42 | ] 43 | }, 44 | { 45 | "label": "project website", 46 | "type": "shell", 47 | "presentation": { 48 | "group": "tools", 49 | "panel": "dedicated" 50 | }, 51 | "runOptions": { 52 | "runOn": "folderOpen" 53 | }, 54 | "isBackground": true, 55 | "command": "cargo", 56 | "args": [ 57 | "site-flow" 58 | ], 59 | "problemMatcher": [ 60 | "$rustc-watch" 61 | ] 62 | }, 63 | { 64 | "label": "project server", 65 | "type": "shell", 66 | "presentation": { 67 | "group": "tools", 68 | "panel": "dedicated" 69 | }, 70 | "runOptions": { 71 | "runOn": "folderOpen" 72 | }, 73 | "isBackground": true, 74 | "command": "cargo", 75 | "args": [ 76 | "server" 77 | ], 78 | "problemMatcher": [] 79 | }, 80 | { 81 | "label": "ofl crates", 82 | "type": "shell", 83 | "presentation": { 84 | "group": "tools", 85 | "panel": "dedicated" 86 | }, 87 | "runOptions": { 88 | "runOn": "folderOpen" 89 | }, 90 | "isBackground": true, 91 | "command": "cargo", 92 | "args": [ 93 | "ofl-flow" 94 | ], 95 | "problemMatcher": [ 96 | "$rustc-watch" 97 | ] 98 | }, 99 | { 100 | "label": "docs/fmt", 101 | "type": "shell", 102 | "presentation": { 103 | "group": "tools", 104 | "panel": "dedicated" 105 | }, 106 | "runOptions": { 107 | "runOn": "folderOpen" 108 | }, 109 | "isBackground": true, 110 | "command": "cargo", 111 | "args": [ 112 | "watch", 113 | "-x", 114 | "ofl-fmt-project", 115 | "-x", 116 | "docs-all", 117 | ], 118 | "problemMatcher": [ 119 | "$rustc-watch" 120 | ] 121 | }, 122 | { 123 | "label": "cypress", 124 | "type": "shell", 125 | "presentation": { 126 | "group": "tools", 127 | "panel": "dedicated" 128 | }, 129 | "runOptions": { 130 | "runOn": "folderOpen" 131 | }, 132 | "isBackground": true, 133 | "command": "npx", 134 | "args": [ 135 | "cypress", 136 | "open" 137 | ], 138 | "options": { 139 | "cwd": "${workspaceFolder}/dom/examples/todo/e2e" 140 | }, 141 | "problemMatcher": [] 142 | } 143 | ] 144 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # moxie 2 | 3 | moxie supports incremental "declarative" Rust code for interactive systems. 4 | It comes with a lightweight event loop runtime that supports granular 5 | reuse of arbitrary work, state change notifications, and async loaders. 6 | 7 | 8 | 9 | ## [0.7.1] - 2021-05-05 10 | 11 | ### Added 12 | 13 | - `Key::mutate` allows naive clone-update-compare access to a state variable. 14 | - `#[moxie::updater(...)]` attribute macro supports creating a `Key` wrapper with shorthand for 15 | mutating methods. 16 | - `wasm-bindgen` cargo feature which enables correct usage of parking_lot on wasm32 targets. 17 | 18 | ### Fixed 19 | 20 | - Some new clippy lints. 21 | 22 | ### Changed 23 | 24 | - No longer requires a nightly cargo to build. 25 | 26 | ## [0.7.0] - 2020-09-27 27 | 28 | ### Added 29 | 30 | - Support borrowed arguments to cache functions to avoid cloning on every revision. 31 | - Common trait implementations for `Key`. 32 | - Updated crate & module docs. 33 | - Testing utilities in `testing` module. 34 | 35 | ### Removed 36 | 37 | - Futures loading is no longer feature flagged. 38 | - moxie's cache (previously MemoStorage) is moved to dyn-cache, a new dependency. 39 | - Built-in executor which was only used for testing. 40 | - No longer depends on nightly Rust for `#[track_caller]` -- it's stabilized. 41 | - `mox!` macro is now published separately. 42 | 43 | ### Changed 44 | 45 | - "Memoization" renamed to "caching" in all APIs. 46 | - `Runtime::run_once` allows passing an argument to the root function. 47 | - `Runtime` no longer owns the root function. 48 | - `embed` module renamed to `runtime`. 49 | - State functions return a tuple `(Commit, Key)` instead of just a `Key`. 50 | 51 | ## [0.2.3] - 2019-12-27 52 | 53 | ### Fixed 54 | 55 | - Incorrect version numbers which prevented 0.2.2 from working from crates.io. 56 | 57 | ## [0.2.2] - 2019-12-27 58 | 59 | ### Added 60 | 61 | - Depends on nightly Rust for `#[track_caller]` feature. 62 | 63 | ### Changed 64 | 65 | - Update to topo version that produces functions instead of macros from `#[topo::nested]`. No more 66 | macros! Makes use of `#[track_caller]`. 67 | 68 | ## [0.2.1] - 2019-11-22 69 | 70 | ### Added 71 | 72 | - Async executor integration w/ futures loading (`load`, `load_once`, ...). Under feature flag. 73 | - `#![forbid(unsafe_code)]` 74 | - `Runtime::run_once` returns the root closure's return value. 75 | - `memo_with`, `once_with` functions that allows non-`Clone` types in storage 76 | - `Key` tracks its callsite. 77 | - `mox!` re-exported. 78 | 79 | ### Removed 80 | 81 | - Attempts at memoizing all components. 82 | - Unnecessary revision bookkeeping for state variables. 83 | 84 | ### Fixed 85 | 86 | - Passing illicit env values to a root function. 87 | 88 | ## [0.1.1-alpha.0] - 2019-08-17 89 | 90 | Initial release in support of moxie-dom. 91 | 92 | ## [0.1.0] - 2018-11-10 93 | 94 | Initial name reservation. 95 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at adam.n.perry+conduct@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to moxie 2 | 3 | Hello! The project is still very early but we're so excited to see you here! 4 | 5 | The core bits have only recently stabilized enough to invite contribution, and we're still working 6 | on a body of starter issues and docs that can enable more participation. If this doesn't scare you 7 | away, then read on. 8 | 9 | The project currently uses a [Discord server](https://discord.gg/vTAzk3d) for chat and we 10 | recommend joining if you're interested in contributing at this phase. If you would be interested in 11 | contributing but prefer other communications media, please let us know! It's certainly not 12 | required to contribute, but GitHub issues are a bit constraining for the level of ambiguity in the 13 | project today. 14 | 15 | ## Code of Conduct 16 | 17 | See the [project's Code of Conduct](./CODE_OF_CONDUCT.md) for more details. 18 | 19 | ## Continuous Integration 20 | 21 | CI is run via [GitHub Actions](https://github.com/anp/moxie/actions), and 22 | [configured in-tree](.github/workflows/main.yml). 23 | 24 | ### Landing PRs 25 | 26 | GitHub now offers an option to require that a branch is up-to-date before it is merged in a PR, which is enabled for the repository to aid in implementing [The Not Rocket Science Rule of Software Engineering](https://graydon.livejournal.com/186550.html): 27 | 28 | > automatically maintain a repository of code that always passes all the tests 29 | 30 | ## Development environment 31 | 32 | ### Requirements 33 | 34 | * [rustup](https://rustup.rs) 35 | * `rustup component add clippy rustfmt` 36 | * [cargo-watch](https://crates.io/crates/cargo-watch) 37 | 38 | ### Workflows 39 | 40 | #### Core libraries 41 | 42 | From the project root, this command will run the default development loop: 43 | 44 | ```shell 45 | $ cargo core-flow 46 | ``` 47 | 48 | See [its definition](./.cargo/config) for details. 49 | 50 | #### moxie-dom 51 | 52 | The main workflow for the dom library: 53 | 54 | ```shell 55 | $ cargo dom-flow 56 | ``` 57 | 58 | To view examples, in a separate terminal: 59 | 60 | ```shell 61 | $ cargo server 62 | ``` 63 | 64 | This will start a local HTTP server providing access to the project directory. It also watches the 65 | filesystem for changes to files it has served, delivering notifications when any of them 66 | change. The server injects the necessary JavaScript into each HTML page to open a websocket 67 | connection to listen for changes, reloading when changes occur. 68 | 69 | ##### End-to-end tests 70 | 71 | The TodoMVC example app has some e2e tests which use [cypress.io] and thus require a recent Node/npm 72 | installation. 73 | 74 | ``` 75 | $ cd dom/examples/todo/e2e 76 | $ npx cypress open 77 | ``` 78 | 79 | Alternatively, there is a project-local VSCode task configured which will open cypress when the 80 | workspace is opened (assuming one enables auto-tasks for this workspace). 81 | 82 | #### Releases 83 | 84 | During development all non-tool crate versions should be suffixed with `-pre` indicating a 85 | pre-release of some kind. To release a version of a crate, publish a commit to `origin/main/HEAD` 86 | without the pre-release suffix, making sure to update CHANGELOGs appropriately. The project's 87 | continuous integration ensures that any "release" versions (without `-pre`) have been published to 88 | crates.io. 89 | 90 | After a release, all version numbers should be incremented and have `-pre` re-appended. PRs are 91 | expected to bump the version number of the crate they're modifying behind the `-pre` suffix as well 92 | as updating the relevant CHANGELOGs. 93 | 94 | Changing the version of a crate in the repository should be done by running `cargo ofl versions`. 95 | 96 | #### New crates 97 | 98 | Things to update: 99 | 100 | * `Cargo.toml` 101 | * `.cargo/config` 102 | * `.github/workflows/main.yml` 103 | * `index.html` 104 | 105 | (Dependabot discovers the workspace members from the root manifest.) 106 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moxie" 3 | version = "0.7.1" 4 | description = "Incremental runtime for interactive software." 5 | categories = ["asynchronous", "caching", "concurrency", "gui", "rust-patterns"] 6 | keywords = ["incremental", "memoize", "intern", "reactive"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [features] 17 | default = [] 18 | wasm-bindgen = [ "dyn-cache/wasm-bindgen", "parking_lot/wasm-bindgen", "topo/wasm-bindgen" ] 19 | 20 | [dependencies] 21 | dyn-cache = { path = "dyn-cache", version = "0.12.2"} 22 | futures = "0.3.5" 23 | illicit = { path = "illicit", version = "1.1.2"} 24 | parking_lot = "0.11" 25 | scopeguard = "1" 26 | topo = { path = "topo", version = "0.13.2"} 27 | tracing = "^0.1" 28 | 29 | [dev-dependencies] 30 | criterion = "0.3" 31 | tracing-subscriber = { version = "0.3.1", features = [ "env-filter" ] } 32 | 33 | [workspace] 34 | members = [ 35 | "dom", 36 | "dom/augdom", 37 | "dom/examples/counter_fn", 38 | "dom/examples/dom_builder", 39 | "dom/examples/drivertest", 40 | "dom/examples/hacking", 41 | "dom/examples/ssr", 42 | "dom/examples/todo", 43 | "dom/prettiest", 44 | "dom/raf", 45 | "dyn-cache", 46 | "illicit", 47 | "illicit/macro", 48 | "mox", 49 | "topo", 50 | "topo/macro", 51 | ] 52 | exclude = [ 53 | "ofl", 54 | ] 55 | 56 | [[bench]] 57 | name = "core" 58 | harness = false 59 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Adam Perry 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | moxie logo 2 | 3 | # moxie 4 | 5 | ![crates.io](https://img.shields.io/crates/v/moxie) 6 | ![License](https://img.shields.io/crates/l/moxie.svg) 7 | [![codecov](https://codecov.io/gh/anp/moxie/branch/main/graph/badge.svg)](https://codecov.io/gh/anp/moxie) 8 | 9 | ## More Information 10 | 11 | For more information about the moxie project, see the [website](https://moxie.rs). 12 | 13 | ## Contributing and Code of Conduct 14 | 15 | See [CONTRIBUTING.md](CONTRIBUTING.md) for overall contributing info and [CONDUCT.md](CODE_OF_CONDUCT.md) 16 | for the project's Code of Conduct. The project is still early in its lifecycle but we welcome 17 | anyone interested in getting involved. 18 | 19 | ## License 20 | 21 | Licensed under either of 22 | 23 | * [Apache License, Version 2.0](LICENSE-APACHE) 24 | * [MIT license](LICENSE-MIT) 25 | 26 | at your option. 27 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp/moxie/4f71b6f28340b2db263f07d0883394fa0b233de0/assets/logo.png -------------------------------------------------------------------------------- /benches/core.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | 4 | use criterion::{BenchmarkId, Criterion}; 5 | use moxie::{ 6 | once, 7 | runtime::{Revision, RunLoop}, 8 | }; 9 | use std::rc::Rc; 10 | 11 | criterion::criterion_group!(runtime, once_from_store, run_empty, run_repeated); 12 | criterion::criterion_main!(runtime); 13 | 14 | fn once_from_store(c: &mut Criterion) { 15 | let mut rt = RunLoop::new(|| once(|| Rc::new(vec![0; 1_000_000]))); 16 | rt.run_once(); 17 | c.bench_function("1mb vec cached", |b| b.iter(|| rt.run_once())); 18 | } 19 | 20 | fn run_empty(c: &mut Criterion) { 21 | let mut rt = RunLoop::new(Revision::current); 22 | c.bench_function("run_empty", |b| b.iter(|| rt.run_once())); 23 | } 24 | 25 | fn run_n_times_empty(b: &mut criterion::Bencher, n: &usize) { 26 | let mut rt = RunLoop::new(Revision::current); 27 | b.iter(|| { 28 | for _ in 0..*n { 29 | rt.run_once(); 30 | } 31 | }); 32 | } 33 | 34 | fn run_repeated(c: &mut Criterion) { 35 | let mut group = c.benchmark_group("run_repeated"); 36 | for input in &[2, 7, 23] { 37 | group.bench_with_input(BenchmarkId::from_parameter(input), input, run_n_times_empty); 38 | } 39 | group.finish(); 40 | } 41 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "the moxie project" 3 | author = "the moxie developers" 4 | description = "prose related to the moxie project, for publication to the website" 5 | 6 | [build] 7 | build-dir = "pkg" 8 | create-missing = false 9 | 10 | [preprocessor.index] 11 | 12 | [preprocessor.links] 13 | -------------------------------------------------------------------------------- /book/src/00-intro.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | `moxie` (/ˈmäksē/) is a lightweight platform-agnostic UI runtime written in Rust, powering a strongly-typed declarative programming style with minimal interaction latency. 4 | 5 | `moxie` is principally inspired by [React][react] and more specifically the recent [Hooks API][hooks]. It aims to smoothly bridge the gap between stateless tools like [dear imgui][dear] and "traditional" UI paradigms with manually managed graphs of stateful, mutable objects. There are many interesting parallels in our design to those of recently announced UI frameworks [Jetpack Compose][compose] and [SwiftUI][swiftui], although a more in-depth comparison hasn't yet been made. Also, in the course of looking for prior art (_ahem_ googling "memoized imgui"), I found a very interesting [thread on LtU](http://lambda-the-ultimate.org/node/4561) discussing various commenters' efforts and curiosities -- it's a fun read. 6 | 7 | [react]: https://reactjs.org 8 | [hooks]: https://reactjs.org/docs/hooks-intro.html 9 | [dear]: https://github.com/ocornut/imgui 10 | [swiftui]: https://developer.apple.com/xcode/swiftui/ 11 | [compose]: https://developer.android.com/jetpack/compose 12 | 13 | ## Hands On 14 | 15 | Want to try things out? Check out 16 | [the development requirements](CONTRIBUTING.md#development-environment), and run 17 | these commands in two terminals: 18 | 19 | ``` 20 | $ cargo dom-flow 21 | ``` 22 | 23 | This will build all of the web examples and watch for local changes. 24 | 25 | ``` 26 | $ cargo server 27 | ``` 28 | 29 | This will start an HTTP server for the static files in the repo, and opens the repo directory in a 30 | browser. 31 | 32 | Take a look at [`.cargo/config`](.cargo/config) for other subcommand aliases used in the project. 33 | 34 | --- 35 | 36 | random: 37 | 38 | # Declarative style 39 | 40 | TODO "imperative but idempotent" 41 | 42 | "describe the UI *right now*" 43 | 44 | partition the render space using function calls 45 | 46 | while managing persistent stateful elements 47 | 48 | with minimal incremental updates 49 | 50 | in order to achieve minimal latency and consistent responsiveness 51 | -------------------------------------------------------------------------------- /book/src/01-targets/00-index.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /book/src/01-targets/01-web.md: -------------------------------------------------------------------------------- 1 | 2 | * moxie-dom 3 | * getting started, examples 4 | * creating elements 5 | * mounting elements 6 | * element attributes 7 | * element event handling 8 | * use of call slots 9 | -------------------------------------------------------------------------------- /book/src/01-targets/99-new.md: -------------------------------------------------------------------------------- 1 | # Adding new targets 2 | 3 | Expressing an interactive system "in terms of moxie's tools" requires making some decisions. 4 | 5 | ## A core choice in moxie: Builders 6 | 7 | Existing UI systems tend to come with complex objects that require nuanced initialization, often 8 | with many parameters, some of which are optional and some which are not. Rust has one main tool 9 | for describing those initializations: the [builder pattern]. It is possible to describe complex 10 | "mixed-optionality" initialization *without* the builder pattern in Rust, but it's so prevalent 11 | in Rust that it's [officially recommended][builder pattern]. 12 | 13 | The [`mox!`][mox] macro ("**M**ockery **O**f **X**ML") is essentially an XML syntax for Rust 14 | builders. See [its documentation][mox] in the moxie crate for information about exactly how it 15 | expands. 16 | 17 | ## Finding Event Loops 18 | 19 | TODO 20 | 21 | ## Memoization 22 | 23 | TODO 24 | 25 | ## Persistence 26 | 27 | TODO 28 | 29 | ## Parent/child relationships 30 | 31 | TODO 32 | 33 | [mox]: https://docs.rs/moxie/latest/moxie/macro.mox.html 34 | [builder pattern]: https://rust-lang.github.io/api-guidelines/type-safety.html#builders-enable-construction-of-complex-values-c-builder -------------------------------------------------------------------------------- /book/src/01-values.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | * project values 4 | * empathy 5 | * respect 6 | * sharing 7 | * dialogue 8 | -------------------------------------------------------------------------------- /book/src/99-dev/00-index.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /book/src/99-dev/01-releases.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | [Introduction](00-intro.md) 2 | [Project Values](01-values.md) 3 | 4 | * [Targets](01-targets/00-index.md) 5 | * [The web](01-targets/01-web.md) 6 | * [Adding new targets](01-targets/99-new.md) 7 | * [Contributing](99-dev/00-index.md) 8 | * [Releases](99-dev/01-releases.md) 9 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp/moxie/4f71b6f28340b2db263f07d0883394fa0b233de0/clippy.toml -------------------------------------------------------------------------------- /dom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # moxie-dom releases 2 | 3 | TODO -------------------------------------------------------------------------------- /dom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moxie-dom" 3 | version = "0.3.0-pre" 4 | description = "Incrementally interactive HTML applications." 5 | categories = ["asynchronous", "concurrency", "gui", "wasm", "web-programming"] 6 | keywords = ["dom", "web", "incremental", "interactive"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [package.metadata.docs.rs] 17 | default-target = "wasm32-unknown-unknown" 18 | all-features = true 19 | 20 | [lib] 21 | crate-type = [ "cdylib", "rlib", ] 22 | 23 | [features] 24 | default = ["webdom"] 25 | rsdom = ["augdom/rsdom"] 26 | webdom = [ 27 | "augdom/webdom", 28 | "moxie/wasm-bindgen", 29 | "raf", 30 | "topo/wasm-bindgen", 31 | "wasm-bindgen", 32 | "wasm-bindgen-futures", 33 | ] 34 | 35 | [dependencies] 36 | augdom = { path = "augdom", version = "0.2.0-pre", default-features = false } 37 | futures = "0.3.5" 38 | illicit = { path = "../illicit", version = "1.1.2"} 39 | moxie = { path = "../", version = "0.7.1-pre"} 40 | paste = "1.0.0" 41 | scopeguard = "1" 42 | topo = { path = "../topo", version = "0.13.2"} 43 | 44 | # web-only 45 | raf = { path = "raf", version = "0.2.0-pre", optional = true } 46 | wasm-bindgen = { version = "0.2.68", optional = true } 47 | wasm-bindgen-futures = { version = "0.4.13", optional = true } 48 | 49 | [dev-dependencies] 50 | mox = { path = "../mox", version = "0.12.0"} 51 | pretty_assertions = "1.0" 52 | wasm-bindgen-test = "0.3" 53 | -------------------------------------------------------------------------------- /dom/augdom/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # augdom releases 2 | 3 | TODO -------------------------------------------------------------------------------- /dom/augdom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "augdom" 3 | version = "0.2.0-pre" 4 | description = "DOM API usable both inside of a browser (web-sys) and outside (emulation)." 5 | categories = ["api-bindings", "emulators", "gui", "wasm", "web-programming"] 6 | keywords = ["dom", "incremental"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [package.metadata.docs.rs] 17 | default-target = "wasm32-unknown-unknown" 18 | all-features = true 19 | 20 | [features] 21 | default = ["webdom"] 22 | rsdom = ["illicit"] 23 | webdom = ["gloo-timers", "js-sys", "prettiest", "wasm-bindgen", "web-sys"] 24 | 25 | [dependencies] 26 | futures = "0.3.5" 27 | gloo-timers = { version = "0.2.1", features = ["futures"], optional = true } 28 | illicit = { version = "1.1.2", path = "../../illicit", optional = true } 29 | paste = "1" 30 | quick-xml = "0.22.0" 31 | static_assertions = "1" 32 | tracing = "0.1" 33 | 34 | # webdom dependencies: 35 | js-sys = { version = "0.3.25", optional = true } 36 | prettiest = { version = "0.2.0", path = "../prettiest", optional = true } 37 | wasm-bindgen = { version = "0.2.48", optional = true } 38 | 39 | [dependencies.web-sys] 40 | version = "0.3.28" 41 | optional = true 42 | features = [ 43 | # dom types 44 | "Attr", 45 | "CharacterData", 46 | "Document", 47 | "Element", 48 | "EventTarget", 49 | "HtmlElement", 50 | "HtmlHeadElement", 51 | "NamedNodeMap", 52 | "Node", 53 | "NodeList", 54 | "Text", 55 | "Window", 56 | 57 | # event types 58 | "AnimationEvent", 59 | "AnimationEventInit", 60 | "BlobEvent", 61 | "BlobEventInit", 62 | "CloseEvent", 63 | "CloseEventInit", 64 | "CompositionEvent", 65 | "CompositionEventInit", 66 | "DeviceMotionEvent", 67 | "DeviceMotionEventInit", 68 | "DeviceOrientationEvent", 69 | "DeviceOrientationEventInit", 70 | "DragEvent", 71 | "DragEventInit", 72 | "ErrorEvent", 73 | "ErrorEventInit", 74 | "Event", 75 | "EventInit", 76 | "FetchEvent", 77 | "FetchEventInit", 78 | "FocusEvent", 79 | "FocusEventInit", 80 | "GamepadEvent", 81 | "GamepadEventInit", 82 | "HashChangeEvent", 83 | "HashChangeEventInit", 84 | "IdbVersionChangeEvent", 85 | "IdbVersionChangeEventInit", 86 | "KeyboardEvent", 87 | "KeyboardEventInit", 88 | "MessageEvent", 89 | "MessageEventInit", 90 | "MouseEvent", 91 | "MouseEventInit", 92 | "NotificationEvent", 93 | "NotificationEventInit", 94 | "OfflineAudioCompletionEvent", 95 | "OfflineAudioCompletionEventInit", 96 | "PageTransitionEvent", 97 | "PageTransitionEventInit", 98 | "PointerEvent", 99 | "PointerEventInit", 100 | "PopStateEvent", 101 | "PopStateEventInit", 102 | "ProgressEvent", 103 | "ProgressEventInit", 104 | "PushEvent", 105 | "PushEventInit", 106 | "SpeechRecognitionEvent", 107 | "SpeechRecognitionEventInit", 108 | "SpeechSynthesisEvent", 109 | "SpeechSynthesisEventInit", 110 | "SpeechSynthesisErrorEvent", 111 | "SpeechSynthesisErrorEventInit", 112 | "StorageEvent", 113 | "StorageEventInit", 114 | "TouchEvent", 115 | "TouchEventInit", 116 | "TransitionEvent", 117 | "TransitionEventInit", 118 | "UiEvent", 119 | "UiEventInit", 120 | "UserProximityEvent", 121 | "UserProximityEventInit", 122 | "WheelEvent", 123 | "WheelEventInit", 124 | 125 | # testing types, 126 | "MutationObserver", 127 | "MutationObserverInit", 128 | "MutationRecord", 129 | 130 | # TODO these are for examples only, move them there 131 | "HtmlInputElement", 132 | ] 133 | 134 | [dev-dependencies] 135 | wasm-bindgen-test = "0.3" 136 | -------------------------------------------------------------------------------- /dom/examples/counter_fn/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter-moxie-dom-fn" 3 | version = "0.1.0" 4 | publish = false 5 | description = "an example counter for moxie-dom" 6 | edition = "2018" 7 | license-file = "../../../../LICENSE-MIT" 8 | repository = "https://github.com/anp/moxie.git" 9 | 10 | [package.metadata.wasm-pack.profile.release] 11 | wasm-opt = false 12 | 13 | [lib] 14 | crate-type = [ "cdylib" ] 15 | 16 | [dependencies] 17 | mox = { path = "../../../mox" } 18 | moxie = { path = "../../.." } 19 | moxie-dom = { path = "../../" } 20 | wasm-bindgen = "0.2" 21 | -------------------------------------------------------------------------------- /dom/examples/counter_fn/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | moxie-dom • counter 10 | 11 | 12 | 13 |
14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /dom/examples/counter_fn/src/lib.rs: -------------------------------------------------------------------------------- 1 | use mox::mox; 2 | use moxie_dom::{elements::html::*, prelude::*}; 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | pub fn boot(root: moxie_dom::raw::sys::Node) { 7 | moxie_dom::boot(root, || { 8 | let (count, incrementer) = state(|| 0); 9 | let decrementer = incrementer.clone(); 10 | mox! { 11 |
12 | 13 | { count } 14 | 15 |
16 | } 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /dom/examples/dom_builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dom-builder-moxie-dom-fn" 3 | version = "0.1.0" 4 | publish = false 5 | description = "an example counter for moxie-dom using the DOM builder API" 6 | edition = "2018" 7 | license-file = "../../../../LICENSE-MIT" 8 | repository = "https://github.com/anp/moxie.git" 9 | 10 | [package.metadata.wasm-pack.profile.release] 11 | wasm-opt = false 12 | 13 | [lib] 14 | crate-type = [ "cdylib" ] 15 | 16 | [dependencies] 17 | moxie = { path = "../../.." } 18 | moxie-dom = { path = "../../" } 19 | wasm-bindgen = "0.2" 20 | -------------------------------------------------------------------------------- /dom/examples/dom_builder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | moxie-dom • builder 10 | 11 | 12 | 13 |
14 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /dom/examples/dom_builder/src/lib.rs: -------------------------------------------------------------------------------- 1 | use moxie_dom::{elements::html::*, prelude::*}; 2 | use wasm_bindgen::prelude::*; 3 | 4 | /// The counter_fn example, but using the DOM builder API. 5 | #[wasm_bindgen] 6 | pub fn boot(root: moxie_dom::raw::sys::Node) { 7 | moxie_dom::boot(root, || { 8 | let (count, incrementer) = state(|| 0); 9 | let decrementer = incrementer.clone(); 10 | 11 | div() 12 | .child(button().onclick(move |_| decrementer.mutate(|count| *count -= 1)).child("-")) 13 | .child(count) 14 | .child(button().onclick(move |_| incrementer.mutate(|count| *count += 1)).child("+")) 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /dom/examples/drivertest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "drivertest" 3 | publish = false 4 | version = "0.1.0" 5 | authors = ["Adam Perry "] 6 | edition = "2018" 7 | 8 | [package.metadata.wasm-pack.profile.release] 9 | wasm-opt = false 10 | 11 | [dependencies] 12 | augdom = { path = "../../augdom", features = ["rsdom"] } 13 | mox = { path = "../../../mox" } 14 | moxie = { path = "../../../" } 15 | moxie-dom = { path = "../../", features = ["rsdom"] } 16 | topo = { path = "../../../topo" } 17 | wasm-bindgen = "0.2.51" 18 | wasm-bindgen-test = "0.3" 19 | 20 | [dependencies.web-sys] 21 | version = "0.3.28" 22 | features = [ 23 | "Element", 24 | "Node", 25 | "HtmlElement", 26 | ] 27 | -------------------------------------------------------------------------------- /dom/examples/drivertest/src/lib.rs: -------------------------------------------------------------------------------- 1 | use mox::mox; 2 | use moxie_dom::{ 3 | elements::html::{button, li, ul}, 4 | embed::DomLoop, 5 | prelude::*, 6 | }; 7 | use wasm_bindgen::JsCast; 8 | use wasm_bindgen_test::*; 9 | use web_sys as sys; 10 | wasm_bindgen_test_configure!(run_in_browser); 11 | 12 | #[wasm_bindgen_test] 13 | fn mini_list() { 14 | let list = || { 15 | mox! { 16 |
    17 |
  • "first"
  • 18 |
  • "second"
  • 19 |
  • "third"
  • 20 |
21 | } 22 | }; 23 | 24 | let web_div = augdom::document().create_element("div"); 25 | let mut web_tester = DomLoop::new(web_div.clone(), list); 26 | let rsdom_root = augdom::create_virtual_element("div"); 27 | let mut virtual_tester = DomLoop::new_virtual(rsdom_root.clone(), list); 28 | 29 | web_tester.run_once(); 30 | virtual_tester.run_once(); 31 | 32 | let expected_html = r#"
  • first
  • second
  • third
"#; 33 | 34 | assert_eq!( 35 | expected_html, 36 | web_div.outer_html(), 37 | "our outer_html implementation must match the expected HTML", 38 | ); 39 | 40 | assert_eq!( 41 | expected_html, 42 | rsdom_root.outer_html(), 43 | "HTML produced by virtual nodes must match expected", 44 | ); 45 | 46 | let expected_pretty_html = r#" 47 |
48 |
    49 |
  • first
  • 50 |
  • second
  • 51 |
  • third
  • 52 |
53 |
"#; 54 | 55 | assert_eq!( 56 | expected_pretty_html, 57 | &(String::from("\n") + &web_div.pretty_outer_html(2)), 58 | "pretty HTML produced from DOM nodes must match expected", 59 | ); 60 | 61 | assert_eq!( 62 | expected_pretty_html, 63 | &(String::from("\n") + &rsdom_root.pretty_outer_html(2)), 64 | "pretty HTML produced from virtual nodes must match expected", 65 | ); 66 | } 67 | 68 | #[wasm_bindgen_test] 69 | fn mutiple_event_listeners() { 70 | // Create a button with two click event listeners 71 | let web_div = augdom::document().create_element("div"); 72 | let mut web_tester = DomLoop::new(web_div.clone(), move || { 73 | // Each event listener increments a counter 74 | let (counter1_val, counter1) = moxie::state(|| 0u8); 75 | let (counter2_val, counter2) = moxie::state(|| 0u8); 76 | 77 | let increment = |n: &u8| Some(n + 1); 78 | 79 | mox! { 80 | 87 | } 88 | }); 89 | 90 | web_tester.run_once(); // Initial rendering 91 | 92 | // Retreive the HtmlElement of to the 33 | }); 34 | 35 | for t in &["first", "second", "third"] { 36 | root = root.child(mox! {
{ t }
}); 37 | } 38 | 39 | root.build() 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use super::*; 45 | use augdom::testing::{Query, TargetExt}; 46 | use wasm_bindgen_test::*; 47 | wasm_bindgen_test_configure!(run_in_browser); 48 | 49 | #[wasm_bindgen_test] 50 | pub async fn hello_browser() { 51 | let test_root = document().create_element("div"); 52 | moxie_dom::boot(test_root.clone(), root); 53 | 54 | let button = test_root.find().by_text("increment").until().one().await.unwrap(); 55 | assert_eq!( 56 | test_root.first_child().unwrap().to_string(), 57 | r#"
58 |
hello world from moxie! (0)
59 | 60 |
first
61 |
second
62 |
third
63 |
"# 64 | ); 65 | 66 | button.click(); 67 | test_root.find().by_text("hello world from moxie! (1)").until().one().await.unwrap(); 68 | button.click(); 69 | test_root.find().by_text("hello world from moxie! (2)").until().one().await.unwrap(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /dom/examples/ssr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ssr-poc" 3 | version = "0.1.0" 4 | publish = false 5 | authors = ["Adam Perry "] 6 | edition = "2018" 7 | description = "proof of concept for server-side rendered HTML with moxie-dom" 8 | 9 | [dependencies] 10 | augdom = { path = "../../augdom" } 11 | gotham = "0.6.0" 12 | gotham_derive = "0.6.0" 13 | hyper = "0.14" 14 | mox = { path = "../../../mox" } 15 | moxie = { path = "../../../" } 16 | serde = "1" 17 | serde_derive = "1" 18 | topo = { path = "../../../topo" } 19 | 20 | [dependencies.moxie-dom] 21 | path = "../../" 22 | default-features = false 23 | features = [ "rsdom" ] -------------------------------------------------------------------------------- /dom/examples/ssr/README.md: -------------------------------------------------------------------------------- 1 | # moxie server-side rendering example 2 | 3 | A proof-of-concept implementation of rendering HTML in gotham using moxie-dom without any browser 4 | dependencies. 5 | 6 | `cargo run` starts a server that listens on `127.0.0.1:7878`, serving HTML based on the URL after 7 | `/paths/*`. 8 | 9 | `cargo test` uses gotham's (very nice) test server tool to verify the behavior matches what we 10 | expect. 11 | -------------------------------------------------------------------------------- /dom/examples/ssr/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate gotham_derive; 3 | #[macro_use] 4 | extern crate serde_derive; 5 | 6 | use augdom::Dom; 7 | use gotham::{ 8 | router::{builder::*, Router}, 9 | state::{FromState, State}, 10 | }; 11 | use mox::mox; 12 | use moxie_dom::{ 13 | elements::text_content::{li, ul, Ul}, 14 | embed::DomLoop, 15 | prelude::*, 16 | }; 17 | 18 | fn main() { 19 | let addr = "127.0.0.1:7878"; 20 | println!("Listening for requests at http://{}", addr); 21 | gotham::start(addr, router()) 22 | } 23 | 24 | #[derive(Deserialize, StateData, StaticResponseExtender)] 25 | struct PathExtractor { 26 | #[serde(rename = "*")] 27 | parts: Vec, 28 | } 29 | 30 | #[topo::nested] 31 | fn simple_list(items: &[String]) -> Ul { 32 | let mut list = ul(); 33 | for item in items { 34 | list = list.child(mox!(
  • { item }
  • )); 35 | } 36 | list.build() 37 | } 38 | 39 | fn parts_handler(state: State) -> (State, String) { 40 | let parts = { 41 | let path = PathExtractor::borrow_from(&state); 42 | path.parts.to_owned() 43 | }; 44 | let web_div = augdom::create_virtual_element("div"); 45 | let mut renderer = DomLoop::new_virtual(web_div.clone(), move || simple_list(&parts)); 46 | renderer.run_once(); 47 | (state, web_div.pretty_outer_html(2)) 48 | } 49 | 50 | fn router() -> Router { 51 | build_simple_router(|route| { 52 | route.get("/parts/*").with_path_extractor::().to(parts_handler); 53 | }) 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::*; 59 | use gotham::test::TestServer; 60 | use hyper::StatusCode; 61 | 62 | #[test] 63 | fn extracts_one_component() { 64 | let test_server = TestServer::new(router()).unwrap(); 65 | let response = test_server.client().get("http://localhost/parts/head").perform().unwrap(); 66 | 67 | assert_eq!(response.status(), StatusCode::OK); 68 | 69 | let body = String::from_utf8(response.read_body().unwrap()).unwrap(); 70 | assert_eq!( 71 | &body, 72 | r#"
    73 |
      74 |
    • head
    • 75 |
    76 |
    "#, 77 | ); 78 | } 79 | 80 | #[test] 81 | fn extracts_multiple_components() { 82 | let test_server = TestServer::new(router()).unwrap(); 83 | let response = test_server 84 | .client() 85 | .get("http://localhost/parts/head/shoulders/knees/toes") 86 | .perform() 87 | .unwrap(); 88 | 89 | assert_eq!(response.status(), StatusCode::OK); 90 | 91 | let body = String::from_utf8(response.read_body().unwrap()).unwrap(); 92 | assert_eq!( 93 | &body, 94 | &r#"
    95 |
      96 |
    • head
    • 97 |
    • shoulders
    • 98 |
    • knees
    • 99 |
    • toes
    • 100 |
    101 |
    "#, 102 | ); 103 | } 104 | 105 | #[test] 106 | fn basic_list_prerender() { 107 | let root = augdom::create_virtual_element("div"); 108 | let mut tester = DomLoop::new_virtual(root.clone(), move || { 109 | mox! { 110 |
      111 |
    • "first"
    • 112 |
    • "second"
    • 113 |
    • "third"
    • 114 |
    115 | } 116 | }); 117 | 118 | tester.run_once(); 119 | 120 | assert_eq!( 121 | &root.outer_html(), 122 | r#"
    • first
    • second
    • third
    "#, 123 | "concisely-rendered string output must have no newlines or indentation" 124 | ); 125 | 126 | assert_eq!( 127 | // this newline lets the below string output seem legible 128 | format!("\n{:#?}", &root), 129 | r#" 130 |
    131 |
      132 |
    • first
    • 133 |
    • second
    • 134 |
    • third
    • 135 |
    136 |
    "#, 137 | "pretty debug output must be 4-space-indented" 138 | ); 139 | 140 | assert_eq!( 141 | // this newline lets the below string output seem legible 142 | format!("\n{}", &root), 143 | r#" 144 |
    145 |
      146 |
    • first
    • 147 |
    • second
    • 148 |
    • third
    • 149 |
    150 |
    "#, 151 | "Display output must be 2-space-indented" 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /dom/examples/todo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todomvc-moxie" 3 | description = "TodoMVC clone with moxie-dom" 4 | version = "0.1.0" 5 | publish = false 6 | edition = "2018" 7 | license-file = "../../../../LICENSE-MIT" 8 | repository = "https://github.com/anp/moxie.git" 9 | 10 | [package.metadata.wasm-pack.profile.release] 11 | wasm-opt = false 12 | 13 | [lib] 14 | crate-type = [ "cdylib" ] 15 | 16 | [dependencies] 17 | console_error_panic_hook = "0.1.6" 18 | illicit = { path = "../../../illicit" } 19 | mox = { path = "../../../mox" } 20 | moxie-dom = { path = "../../" } 21 | topo = { path = "../../../topo" } 22 | tracing = { version = "^0.1", features = [ "log" ] } 23 | tracing-wasm = "0.2.0" 24 | wasm-bindgen = "0.2" 25 | 26 | [dev-dependencies] 27 | pretty_assertions = "1.0" 28 | wasm-bindgen-test = "0.3" 29 | -------------------------------------------------------------------------------- /dom/examples/todo/README.md: -------------------------------------------------------------------------------- 1 | # TodoMVC Example 2 | 3 | Commands all assume the working directory is the repository root. 4 | 5 | ## Serving 6 | 7 | Build the example and start the project's local HTTP server: 8 | 9 | ``` 10 | $ cargo build-dom-todo # for live-watching rebuilds use `cargo dom-flow` 11 | $ cargo ofl serve 12 | ``` 13 | 14 | In VSCode the same can be accomplished by running the `dom crates` and `project server` tasks. 15 | 16 | ## Using 17 | 18 | To use the example locally, follow the directions for [serving](#serving) the project and 19 | navigate to `http://[::1]:8000/dom/examples/todo/index.html` in your browser. 20 | 21 | ## Tests 22 | 23 | Unit & integration tests can be run with `cargo test-dom-todo`. 24 | 25 | ### End-to-end 26 | 27 | End-to-end tests are run with [Cypress](https://cypress.io) which requires 28 | [Node.js](https://nodejs.org) to run. 29 | 30 | If you've already followed the [serving](#serving) instructions the e2e tests can be run from the 31 | Cypress UI directly. Start the test runner with the `cypress` VSCode task or run the following: 32 | 33 | ``` 34 | $ cd dom/examples/todo/e2e; npx cypress run 35 | ``` 36 | 37 | #### One-off 38 | 39 | The tests require a running HTTP server and a current build. The `test-dom-todo-e2e` cargo command 40 | runs a build, and starts an HTTP server for the test before running it: 41 | 42 | ``` 43 | $ cargo test-dom-todo-e2e 44 | ``` 45 | -------------------------------------------------------------------------------- /dom/examples/todo/e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "9vrkgj", 3 | "baseUrl": "http://localhost:8000/dom/examples/todo", 4 | "fixturesFolder": false, 5 | "pluginsFile": false 6 | } -------------------------------------------------------------------------------- /dom/examples/todo/e2e/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "example": "fixture" 3 | } -------------------------------------------------------------------------------- /dom/examples/todo/e2e/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create the custom commands: 'createDefaultTodos' 4 | // and 'createTodo'. 5 | // 6 | // The commands.js file is a great place to 7 | // modify existing commands and create custom 8 | // commands for use throughout your tests. 9 | // 10 | // You can read more about custom commands here: 11 | // https://on.cypress.io/commands 12 | // *********************************************** 13 | 14 | Cypress.Commands.add('createDefaultTodos', function () { 15 | 16 | let TODO_ITEM_ONE = 'buy some cheese' 17 | let TODO_ITEM_TWO = 'feed the cat' 18 | let TODO_ITEM_THREE = 'book a doctors appointment' 19 | 20 | // begin the command here, which by will display 21 | // as a 'spinning blue state' in the UI to indicate 22 | // the command is running 23 | let cmd = Cypress.log({ 24 | name: 'create default todos', 25 | message: [], 26 | consoleProps () { 27 | // we're creating our own custom message here 28 | // which will print out to our browsers console 29 | // whenever we click on this command 30 | return { 31 | 'Inserted Todos': [TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE], 32 | } 33 | }, 34 | }) 35 | 36 | // additionally we pass {log: false} to all of our 37 | // sub-commands so none of them will output to 38 | // our command log 39 | 40 | cy.get('.new-todo', { log: false }) 41 | .type(`${TODO_ITEM_ONE}{enter}`, { log: false }) 42 | .type(`${TODO_ITEM_TWO}{enter}`, { log: false }) 43 | .type(`${TODO_ITEM_THREE}{enter}`, { log: false }) 44 | 45 | cy.get('.todo-list li', { log: false }) 46 | .then(function ($listItems) { 47 | // once we're done inserting each of the todos 48 | // above we want to return the .todo-list li's 49 | // to allow for further chaining and then 50 | // we want to snapshot the state of the DOM 51 | // and end the command so it goes from that 52 | // 'spinning blue state' to the 'finished state' 53 | cmd.set({ $el: $listItems }).snapshot().end() 54 | }) 55 | }) 56 | 57 | Cypress.Commands.add('createTodo', function (todo) { 58 | 59 | let cmd = Cypress.log({ 60 | name: 'create todo', 61 | message: todo, 62 | consoleProps () { 63 | return { 64 | 'Inserted Todo': todo, 65 | } 66 | }, 67 | }) 68 | 69 | // create the todo 70 | cy.get('.new-todo', { log: false }).type(`${todo}{enter}`, { log: false }) 71 | 72 | // now go find the actual todo 73 | // in the todo list so we can 74 | // easily alias this in our tests 75 | // and set the $el so its highlighted 76 | cy.get('.todo-list', { log: false }) 77 | .contains('li', todo.trim(), { log: false }) 78 | .then(function ($li) { 79 | // set the $el for the command so 80 | // it highlights when we hover over 81 | // our command 82 | cmd.set({ $el: $li }).snapshot().end() 83 | }) 84 | }) 85 | 86 | Cypress.Commands.add('addAxeCode', () => { 87 | cy.window({ log: false }).then((win) => { 88 | return new Promise((resolve) => { 89 | const script = win.document.createElement('script') 90 | 91 | script.src = '/node_modules/axe-core/axe.min.js' 92 | script.addEventListener('load', resolve) 93 | 94 | win.document.head.appendChild(script) 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /dom/examples/todo/e2e/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your other test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/guides/configuration#section-global 10 | // *********************************************************** 11 | 12 | require('./commands') 13 | require('cypress-axe') 14 | -------------------------------------------------------------------------------- /dom/examples/todo/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "cypress-example-todomvc-e2e-tests", 4 | "version": "0.0.0-development", 5 | "devDependencies": { 6 | "axe-core": "4.0.2", 7 | "cypress": "^5.2.0", 8 | "cypress-axe": "0.8.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /dom/examples/todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | moxie-dom • TodoMVC 10 | 11 | 12 | 13 | 14 | 15 |
    16 | 17 |
    18 |

    Double-click to edit a todo

    19 | 20 |

    Created by anp

    21 |

    Part of moxie-dom

    22 |
    23 | 24 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /dom/examples/todo/src/filter.rs: -------------------------------------------------------------------------------- 1 | use crate::Todo; 2 | use mox::mox; 3 | use moxie_dom::{ 4 | elements::{ 5 | html::*, 6 | text_content::{Li, Ul}, 7 | }, 8 | prelude::*, 9 | }; 10 | use Visibility::{Active, All, Completed}; 11 | 12 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 13 | pub enum Visibility { 14 | All, 15 | Active, 16 | Completed, 17 | } 18 | 19 | impl Default for Visibility { 20 | fn default() -> Self { 21 | All 22 | } 23 | } 24 | 25 | impl std::fmt::Display for Visibility { 26 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 27 | f.write_str(match self { 28 | All => "All", 29 | Active => "Active", 30 | Completed => "Completed", 31 | }) 32 | } 33 | } 34 | 35 | impl Visibility { 36 | pub fn should_show(self, todo: &Todo) -> bool { 37 | match self { 38 | All => true, 39 | Active => !todo.completed, 40 | Completed => todo.completed, 41 | } 42 | } 43 | } 44 | 45 | #[topo::nested] 46 | #[illicit::from_env(visibility: &Key)] 47 | pub fn filter_link(to_set: Visibility) -> Li { 48 | let visibility = visibility.clone(); 49 | mox! { 50 |
  • 51 | 54 | { to_set } 55 | 56 |
  • 57 | } 58 | } 59 | 60 | #[topo::nested] 61 | pub fn filter() -> Ul { 62 | let mut list = ul(); 63 | list = list.class("filters"); 64 | for &to_set in &[All, Active, Completed] { 65 | list = list.child(filter_link(to_set)); 66 | } 67 | 68 | list.build() 69 | } 70 | -------------------------------------------------------------------------------- /dom/examples/todo/src/footer.rs: -------------------------------------------------------------------------------- 1 | use crate::{filter::filter, Todo}; 2 | use mox::mox; 3 | use moxie_dom::{ 4 | elements::{forms::Button, html::*, sectioning::Footer, text_semantics::Span}, 5 | prelude::*, 6 | }; 7 | 8 | #[topo::nested] 9 | pub fn items_remaining(num_active: usize) -> Span { 10 | let bolded = if num_active == 0 { text("No") } else { text(num_active.to_string()) }; 11 | mox! { 12 | 13 | {bolded} 14 | {% " {} left", if num_active == 1 { "item" } else { "items" } } 15 | 16 | } 17 | } 18 | 19 | #[topo::nested] 20 | #[illicit::from_env(todos: &Key>)] 21 | pub fn clear_completed_button(num_complete: usize) -> Button { 22 | let todos = todos.to_owned(); 23 | let remove_completed = 24 | move |_| todos.update(|t| Some(t.iter().filter(|t| !t.completed).cloned().collect())); 25 | mox! { 26 | 31 | } 32 | } 33 | 34 | #[topo::nested] 35 | pub fn filter_footer(num_complete: usize, num_active: usize) -> Footer { 36 | let mut footer = footer().class("footer").child(items_remaining(num_active)).child(filter()); 37 | 38 | if num_complete > 0 { 39 | footer = footer.child(clear_completed_button(num_complete)); 40 | } 41 | 42 | footer.build() 43 | } 44 | -------------------------------------------------------------------------------- /dom/examples/todo/src/header.rs: -------------------------------------------------------------------------------- 1 | use crate::{input::text_input, Todo}; 2 | use mox::mox; 3 | use moxie_dom::{ 4 | elements::sectioning::{h1, header, Header}, 5 | prelude::*, 6 | }; 7 | use tracing::info; 8 | 9 | #[topo::nested] 10 | #[illicit::from_env(todos: &Key>)] 11 | pub fn input_header() -> Header { 12 | let todos = todos.clone(); 13 | mox! { 14 |
    15 |

    "todos"

    16 | { text_input( 17 | "What needs to be done?", 18 | false, 19 | move |value: String| { 20 | todos.update(|prev| { 21 | let mut todos: Vec = prev.to_vec(); 22 | todos.push(Todo::new(value)); 23 | info!({ ?todos }, "added new todo"); 24 | Some(todos) 25 | }); 26 | }, 27 | )} 28 |
    29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dom/examples/todo/src/input.rs: -------------------------------------------------------------------------------- 1 | use mox::mox; 2 | use moxie_dom::{ 3 | elements::forms::{input, Input}, 4 | prelude::*, 5 | }; 6 | use wasm_bindgen::JsCast; 7 | 8 | #[topo::nested] 9 | pub fn text_input( 10 | placeholder: &str, 11 | editing: bool, 12 | mut on_save: impl FnMut(String) + 'static, 13 | ) -> Input { 14 | let (text, set_text) = state(|| if editing { placeholder.to_string() } else { String::new() }); 15 | let clear_text = set_text.clone(); 16 | 17 | fn input_value(ev: impl AsRef) -> String { 18 | let event: &sys::Event = ev.as_ref(); 19 | let target = event.target().unwrap(); 20 | let input: sys::HtmlInputElement = target.dyn_into().unwrap(); 21 | let val = input.value(); 22 | input.set_value(""); // it's a little weird to clear the text every time, TODO clean up 23 | val 24 | } 25 | 26 | mox! { 27 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /dom/examples/todo/src/integration_tests.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for TodoMVC. 2 | //! 3 | //! A module within the application rather than a "proper" integration test 4 | //! because cargo and wasm-pack are conspiring to make that not build somehow. 5 | //! The workaround for now is to make this a module of the app itself, so we 6 | //! have to be on our best behavior and only use public API. 7 | 8 | use moxie_dom::{ 9 | prelude::*, 10 | raw::{ 11 | testing::{Query, TargetExt}, 12 | Node, 13 | }, 14 | }; 15 | use std::{fmt::Debug, ops::Deref}; 16 | use tracing::*; 17 | use wasm_bindgen_test::wasm_bindgen_test; 18 | 19 | #[wasm_bindgen_test] 20 | pub async fn add_2_todos() { 21 | let test = Test::new(); 22 | test.add_todo("learn testing").await; 23 | test.add_todo("be cool").await; 24 | } 25 | 26 | #[wasm_bindgen_test] 27 | pub async fn add_default_todos() { 28 | Test::new().add_default_todos().await; 29 | } 30 | 31 | #[allow(unused)] // TODO add back wasm_bindgen_test when we have a full page load to test autofocus 32 | async fn initial_open_focuses_input() { 33 | let _test = Test::new(); 34 | let focused = document().active_element().unwrap(); 35 | assert_eq!(focused.get_attribute("className").unwrap(), "new-todo"); 36 | } 37 | 38 | struct Test { 39 | root: Node, 40 | } 41 | 42 | impl Deref for Test { 43 | type Target = Node; 44 | 45 | fn deref(&self) -> &Self::Target { 46 | &self.root 47 | } 48 | } 49 | 50 | impl Test { 51 | fn new() -> Self { 52 | // Please only use public functions from the crate, see module docs for 53 | // explanation. 54 | use super::{boot, setup_tracing}; 55 | 56 | setup_tracing(); 57 | let root = document().create_element("div"); 58 | document().body().append_child(&root); 59 | boot(root.expect_concrete().clone()); 60 | Test { root } 61 | } 62 | 63 | fn todos(&self) -> Vec { 64 | self.query_selector_all(".todo-list li") 65 | .iter() 66 | .map(|t| t.get_inner_text()) 67 | .collect::>() 68 | } 69 | 70 | #[track_caller] 71 | fn assert_todos(&self, expected: &[Expected]) 72 | where 73 | String: PartialEq, 74 | Expected: Debug, 75 | { 76 | assert_eq!(self.todos(), expected); 77 | } 78 | 79 | /// Add a new todo to the list, asserting that the list only grows by the 80 | /// one item. 81 | async fn add_todo(&self, todo: &str) { 82 | let mut expected = self.todos(); 83 | expected.push(todo.to_owned()); 84 | 85 | // actually input the new todo 86 | self.input().keyboardln(todo); 87 | // wait for it to show up 88 | self.find().by_text(todo).until().many().await.unwrap(); 89 | self.assert_todos(&expected[..]); 90 | } 91 | 92 | async fn add_default_todos(&self) { 93 | info!("adding default TODOs"); 94 | let expected = &[TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE]; 95 | for todo in expected { 96 | self.add_todo(todo).await; 97 | } 98 | } 99 | 100 | fn input(&self) -> Node { 101 | self.find().by_placeholder_text(INPUT_PLACEHOLDER).one().unwrap() 102 | } 103 | } 104 | 105 | impl Drop for Test { 106 | fn drop(&mut self) { 107 | document().body().remove_child(&self.root); 108 | // TODO blur active element just to be safe 109 | // TODO stop app and block until cleaned up 110 | // TODO clear local storage 111 | } 112 | } 113 | 114 | const INPUT_PLACEHOLDER: &str = "What needs to be done?"; 115 | const TODO_ITEM_ONE: &str = "buy some cheese"; 116 | const TODO_ITEM_TWO: &str = "feed the cat"; 117 | const TODO_ITEM_THREE: &str = "book a doctors appointment"; 118 | -------------------------------------------------------------------------------- /dom/examples/todo/src/item.rs: -------------------------------------------------------------------------------- 1 | use crate::{input::text_input, Todo}; 2 | use mox::mox; 3 | use moxie_dom::{ 4 | elements::{ 5 | forms::Input, 6 | html::*, 7 | text_content::{Div, Li}, 8 | }, 9 | prelude::*, 10 | }; 11 | 12 | #[illicit::from_env(todos: &Key>)] 13 | fn item_edit_input(todo: Todo, editing: Key) -> Input { 14 | let todos = todos.clone(); 15 | let text = todo.title.clone(); 16 | text_input(&text, true, move |value: String| { 17 | editing.set(false); 18 | todos.update(|todos| { 19 | let mut todos = todos.to_vec(); 20 | if let Some(mut todo) = todos.iter_mut().find(|t| t.id == todo.id) { 21 | todo.title = value; 22 | } 23 | Some(todos) 24 | }); 25 | }) 26 | } 27 | 28 | #[illicit::from_env(todos: &Key>)] 29 | fn item_with_buttons(todo: Todo, editing: Key) -> Div { 30 | let todos = todos.clone(); 31 | let id = todo.id; 32 | let toggle_todos = todos.clone(); 33 | 34 | let toggle_completion = move |_| { 35 | toggle_todos.update(|t| { 36 | Some( 37 | t.iter() 38 | .cloned() 39 | .map(move |mut t| { 40 | if t.id == id { 41 | t.completed = !t.completed; 42 | t 43 | } else { 44 | t 45 | } 46 | }) 47 | .collect(), 48 | ) 49 | }) 50 | }; 51 | 52 | mox! { 53 |
    54 | 55 | 56 | 59 | 60 |
    64 | } 65 | } 66 | 67 | #[topo::nested(slot = "&todo.id")] 68 | pub fn todo_item(todo: &Todo) -> Li { 69 | let (editing, set_editing) = state(|| false); 70 | 71 | let mut classes = String::new(); 72 | if todo.completed { 73 | classes.push_str("completed "); 74 | } 75 | if *editing { 76 | classes.push_str("editing"); 77 | } 78 | 79 | let mut item = li(); 80 | item = item.class(classes); 81 | 82 | if *editing { 83 | item = item.child(item_edit_input(todo.clone(), set_editing)); 84 | } else { 85 | item = item.child(item_with_buttons(todo.clone(), set_editing)); 86 | } 87 | 88 | item.build() 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::*; 94 | use pretty_assertions::assert_eq; 95 | 96 | #[wasm_bindgen_test::wasm_bindgen_test] 97 | pub async fn single_item() { 98 | let root = document().create_element("div"); 99 | crate::App::boot_fn(&[Todo::new("weeeee")], root.clone(), || { 100 | let todo = &illicit::expect::>>()[0]; 101 | todo_item(todo) 102 | }); 103 | 104 | assert_eq!( 105 | root.pretty_outer_html(2), 106 | r#"
    107 |
  • 108 |
    109 | 110 | 111 | 112 | 114 |
    115 |
  • 116 |
    "# 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /dom/examples/todo/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use filter::Visibility; 4 | use header::input_header; 5 | use main_section::main_section; 6 | 7 | use illicit::AsContext; 8 | use mox::mox; 9 | use moxie_dom::{ 10 | elements::sectioning::{section, Section}, 11 | interfaces::element::Element, 12 | prelude::*, 13 | }; 14 | use std::sync::atomic::{AtomicU32, Ordering}; 15 | use tracing::*; 16 | use wasm_bindgen::prelude::*; 17 | 18 | pub mod filter; 19 | pub mod footer; 20 | pub mod header; 21 | pub mod input; 22 | pub mod item; 23 | pub mod main_section; 24 | 25 | #[topo::nested] 26 | fn todo_app() -> Section { 27 | mox! { 28 |
    29 | { input_header() } 30 | { main_section() } 31 |
    32 | } 33 | } 34 | 35 | pub(crate) struct App { 36 | pub todos: Key>, 37 | pub visibility: Key, 38 | } 39 | 40 | impl App { 41 | #[topo::nested] 42 | pub fn current() -> Self { 43 | let (_, visibility) = state(Visibility::default); 44 | let (_, todos) = 45 | // we allow the default empty to be overridden for testing 46 | // TODO support localStorage 47 | state(|| illicit::get::>().map(|d| d.clone()).unwrap_or_default()); 48 | 49 | Self { todos, visibility } 50 | } 51 | 52 | pub fn enter(self, f: impl FnMut() -> T) -> T { 53 | illicit::Layer::new().offer(self.todos).offer(self.visibility).enter(f) 54 | } 55 | 56 | pub fn boot(node: impl Into) { 57 | Self::boot_fn(&[], node, todo_app) 58 | } 59 | 60 | fn boot_fn( 61 | default_todos: &[Todo], 62 | node: impl Into, 63 | mut root: impl FnMut() -> Root + 'static, 64 | ) { 65 | let defaults = default_todos.to_vec(); 66 | moxie_dom::boot(node, move || defaults.clone().offer(|| App::current().enter(&mut root))); 67 | info!("running"); 68 | } 69 | } 70 | 71 | #[derive(Clone, Debug)] 72 | pub struct Todo { 73 | id: u32, 74 | title: String, 75 | completed: bool, 76 | } 77 | 78 | impl Todo { 79 | fn new(s: impl Into) -> Self { 80 | static NEXT_ID: AtomicU32 = AtomicU32::new(0); 81 | Self { id: NEXT_ID.fetch_add(1, Ordering::SeqCst), title: s.into(), completed: false } 82 | } 83 | } 84 | 85 | #[wasm_bindgen(start)] 86 | pub fn setup_tracing() { 87 | static SETUP: std::sync::Once = std::sync::Once::new(); 88 | SETUP.call_once(|| { 89 | let config = tracing_wasm::WASMLayerConfigBuilder::new() 90 | .set_console_config(tracing_wasm::ConsoleConfig::ReportWithoutConsoleColor) 91 | .build(); 92 | tracing_wasm::set_as_global_default_with_config(config); 93 | std::panic::set_hook(Box::new(|info| { 94 | error!(?info, "crashed"); 95 | })); 96 | info!("tracing initialized"); 97 | }); 98 | console_error_panic_hook::set_once(); 99 | } 100 | 101 | #[wasm_bindgen] 102 | pub fn boot(root: moxie_dom::raw::sys::Node) { 103 | App::boot(root); 104 | } 105 | 106 | /// Included as a module within the crate rather than a separate file because 107 | /// cargo is grumpy about resolving the crate-under-test. 108 | #[cfg(test)] 109 | mod integration_tests; 110 | 111 | #[cfg(test)] 112 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 113 | -------------------------------------------------------------------------------- /dom/examples/todo/src/main_section.rs: -------------------------------------------------------------------------------- 1 | use crate::{filter::*, footer::*, item::todo_item, Todo}; 2 | use mox::mox; 3 | use moxie_dom::{ 4 | elements::{html::*, sectioning::Section, text_content::Ul, text_semantics::Span}, 5 | prelude::*, 6 | }; 7 | 8 | #[topo::nested] 9 | #[illicit::from_env(todos: &Key>)] 10 | pub fn toggle(default_checked: bool) -> Span { 11 | let todos = todos.clone(); 12 | let onclick = move |_| { 13 | todos.update(|t| { 14 | Some( 15 | t.iter() 16 | .map(|t| { 17 | let mut new = t.clone(); 18 | new.completed = !default_checked; 19 | new 20 | }) 21 | .collect(), 22 | ) 23 | }) 24 | }; 25 | 26 | mox! { 27 | 28 | 29 | 31 | } 32 | } 33 | 34 | #[topo::nested] 35 | #[illicit::from_env(todos: &Key>, visibility: &Key)] 36 | pub fn todo_list() -> Ul { 37 | let mut list = ul().class("todo-list"); 38 | for todo in todos.iter() { 39 | if visibility.should_show(todo) { 40 | list = list.child(todo_item(todo)); 41 | } 42 | } 43 | list.build() 44 | } 45 | 46 | #[topo::nested] 47 | #[illicit::from_env(todos: &Key>)] 48 | pub fn main_section() -> Section { 49 | let num_complete = todos.iter().filter(|t| t.completed).count(); 50 | 51 | let mut section = section().class("main"); 52 | 53 | if !todos.is_empty() { 54 | section = section.child(toggle(num_complete == todos.len())); 55 | } 56 | section = section.child(todo_list()); 57 | 58 | if !todos.is_empty() { 59 | section = section.child(filter_footer(num_complete, todos.len() - num_complete)); 60 | } 61 | 62 | section.build() 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use super::*; 68 | use pretty_assertions::assert_eq; 69 | 70 | #[wasm_bindgen_test::wasm_bindgen_test] 71 | pub async fn list_filtering() { 72 | let root = document().create_element("div"); 73 | crate::App::boot_fn( 74 | &[Todo::new("first"), Todo::new("second"), Todo::new("third")], 75 | root.clone(), 76 | main_section, 77 | ); 78 | 79 | assert_eq!( 80 | root.pretty_outer_html(2), 81 | r#"
    82 |
    83 | 84 | 85 | 86 | 88 | 89 |
      90 |
    • 91 |
      92 | 93 | 94 | 95 | 97 |
      98 |
    • 99 |
    • 100 |
      101 | 102 | 103 | 104 | 106 |
      107 |
    • 108 |
    • 109 |
      110 | 111 | 112 | 113 | 115 |
      116 |
    • 117 |
    118 |
    119 | 120 | 3 items left 121 | 132 |
    133 |
    134 |
    "# 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /dom/local-wasm-pack/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "local-wasm-pack" 3 | version = "0.1.0" 4 | authors = ["Adam Perry "] 5 | edition = "2018" 6 | publish = false 7 | description = "uses wasm-pack as a library to have cargo install it for us" 8 | 9 | [[bin]] 10 | path = "wasm-pack.rs" 11 | name = "wasm-pack" 12 | 13 | [dependencies] 14 | pretty_env_logger = "0.3" 15 | structopt = "0.2" 16 | 17 | [dependencies.wasm-pack] 18 | version = "0.9.1" 19 | # TODO https://github.com/rustwasm/wasm-pack/issues/954 go back to upstream 20 | git = "https://github.com/anp/wasm-pack.git" 21 | branch = "apple-silicon-rosetta" 22 | 23 | [workspace] 24 | -------------------------------------------------------------------------------- /dom/local-wasm-pack/wasm-pack.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | fn main() { 4 | pretty_env_logger::formatted_timed_builder().init(); 5 | wasm_pack::command::run_wasm_pack(wasm_pack::Cli::from_args().cmd).unwrap(); 6 | } 7 | -------------------------------------------------------------------------------- /dom/prettiest/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # prettiest 2 | 3 | [prettiest](https://docs.rs/prettiest) provides pretty-printing `Debug` and `Display` impls 4 | for Javascript values in the [wasm-bindgen](https://docs.rs/wasm-bindgen) crate. 5 | 6 | 7 | 8 | ## [0.2.1] - 2020-10-18 9 | 10 | ### 11 | 12 | - Output should be (more) stable across browser versions. 13 | 14 | ### Changed 15 | 16 | - `Prettified` sorts object properties for each prototype before printing. 17 | - `Prettified` prints functions for each prototype after the properties from that prototype and 18 | before the properties of the preceding prototype. 19 | 20 | ## [0.2.0] - 2020-08-22 21 | 22 | ### Added 23 | 24 | - `Pretty` trait offers a `.pretty()` method to anything `AsRef`. 25 | - `Prettified` implements `Display`. 26 | - `Prettified::skip_property` allows deleting properties that aren't useful to print, like 27 | `timeStamp`. 28 | 29 | ### Fixed 30 | 31 | - Cycles in objects are broken correctly. 32 | - Null and undefined values are handled correctly. 33 | - Values not explicitly handled are represented by `Pretty::Unknown`. 34 | - Objects print properties from their prototype chain. 35 | 36 | ### Changed 37 | 38 | - `Pretty` enum renamed to `Prettified` to allow trait to be named `Pretty`. 39 | - Objects print non-function properties before function properties. 40 | - Objects print their properites in prototype-order. 41 | - HTML elements, window and document all have abbreviated output. 42 | 43 | ## [0.1.0] - 2020-08-20 44 | 45 | Initial release. Only sort of works -- not recommended for use. 46 | -------------------------------------------------------------------------------- /dom/prettiest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prettiest" 3 | version = "0.2.1" 4 | description = "Pretty-printer for JS values from wasm-bindgen." 5 | categories = ["web"] 6 | keywords = ["debug", "pretty", "javascript"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [dependencies] 17 | js-sys = "0.3.25" 18 | scopeguard = "1.1.0" 19 | wasm-bindgen = "0.2.48" 20 | 21 | [dependencies.web-sys] 22 | version = "0.3.28" 23 | features = [ 24 | "Document", 25 | "Element", 26 | "Event", 27 | "EventTarget", 28 | "HtmlElement", 29 | "KeyboardEvent", 30 | "KeyboardEventInit", 31 | "Window", 32 | ] 33 | 34 | [dev-dependencies] 35 | futures = "0.3.5" 36 | wasm-bindgen-test = "0.3" 37 | -------------------------------------------------------------------------------- /dom/raf/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # raf releases -------------------------------------------------------------------------------- /dom/raf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "raf" 3 | version = "0.2.0-pre" 4 | description = "browser event loop scheduler using requestAnimationFrame" 5 | categories = ["asynchronous", "concurrency", "gui", "wasm", "web-programming"] 6 | keywords = ["scheduler", "events", "requestAnimationFrame"] 7 | readme = "CHANGELOG.md" 8 | 9 | # update here, update everywhere! 10 | license = "MIT/Apache-2.0" 11 | homepage = "https://moxie.rs" 12 | repository = "https://github.com/anp/moxie.git" 13 | authors = ["Adam Perry "] 14 | edition = "2018" 15 | 16 | [dependencies] 17 | futures = "0.3.5" 18 | wasm-bindgen = "0.2.48" 19 | web-sys = { version = "0.3.28", features = ["Window"] } 20 | -------------------------------------------------------------------------------- /dom/raf/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Provides a scheduled loop in the browser via `requestAnimationFrame`. 2 | 3 | #![deny(missing_docs)] 4 | 5 | use futures::task::{waker, ArcWake}; 6 | use std::{ 7 | cell::{Cell, RefCell}, 8 | rc::Rc, 9 | sync::Arc, 10 | task::Waker, 11 | }; 12 | use wasm_bindgen::{prelude::*, JsCast}; 13 | use web_sys::window; 14 | 15 | /// A value which can be mutably called by the scheduler. 16 | pub trait Tick: 'static { 17 | /// Tick this value, indicating a new frame request is being fulfilled. 18 | fn tick(&mut self); 19 | } 20 | 21 | /// A value which can receive a waker from the scheduler that will request a new 22 | /// frame when woken. 23 | pub trait Waking { 24 | /// Receive a waker from the scheduler that calls `requestAnimationFrame` 25 | /// when woken. 26 | fn set_waker(&mut self, wk: Waker); 27 | } 28 | 29 | /// Owns a `WebRuntime` and schedules its execution using 30 | /// `requestAnimationFrame`. 31 | #[must_use] 32 | pub struct AnimationFrameScheduler(Rc>); 33 | 34 | struct AnimationFrameState { 35 | ticker: RefCell, 36 | handle: Cell>, 37 | } 38 | 39 | impl ArcWake for AnimationFrameScheduler { 40 | fn wake_by_ref(arc_self: &Arc>) { 41 | arc_self.ensure_scheduled(false); 42 | } 43 | } 44 | 45 | impl AnimationFrameScheduler { 46 | /// Construct a new scheduler with the provided callback. `ticker.tick()` 47 | /// will be called once per fulfilled animation frame request. 48 | pub fn new(ticker: T) -> Self { 49 | AnimationFrameScheduler(Rc::new(AnimationFrameState { 50 | ticker: RefCell::new(ticker), 51 | handle: Cell::new(None), 52 | })) 53 | } 54 | 55 | fn ensure_scheduled(&self, immediately_again: bool) { 56 | let existing = self.0.handle.replace(None); 57 | let handle = existing.unwrap_or_else(|| { 58 | let self2 = AnimationFrameScheduler(Rc::clone(&self.0)); 59 | let callback = Closure::once(Box::new(move || { 60 | self2.0.handle.set(None); 61 | 62 | self2.0.ticker.borrow_mut().tick(); 63 | 64 | if immediately_again { 65 | self2.ensure_scheduled(true); 66 | } 67 | })); 68 | 69 | AnimationFrameHandle::request(callback) 70 | }); 71 | self.0.handle.set(Some(handle)); 72 | } 73 | 74 | /// Consumes the scheduler to initiate a `requestAnimationFrame` callback 75 | /// loop where new animation frames are requested immmediately after the 76 | /// last `moxie::Revision` is completed. `WebRuntime::run_once` is 77 | /// called once per requested animation frame. 78 | pub fn run_on_every_frame(self) { 79 | self.ensure_scheduled(true); 80 | } 81 | } 82 | 83 | impl AnimationFrameScheduler { 84 | /// Consumes the scheduler to initiate a `requestAnimationFrame` callback 85 | /// loop where new animation frames are requested whenever the waker 86 | /// passed to the provided closure is woken. 87 | pub fn run_on_wake(self) { 88 | let state = Rc::clone(&self.0); 89 | let waker = waker(Arc::new(self)); 90 | state.ticker.borrow_mut().set_waker(waker); 91 | state.ticker.borrow_mut().tick(); 92 | } 93 | } 94 | 95 | // don't send these to workers until have a fix :P 96 | unsafe impl Send for AnimationFrameScheduler {} 97 | unsafe impl Sync for AnimationFrameScheduler {} 98 | 99 | struct AnimationFrameHandle { 100 | raw: i32, 101 | /// Prefixed with an underscore because it is only read by JS, otherwise 102 | /// we'll get a warning. 103 | _callback: Closure, 104 | } 105 | 106 | impl AnimationFrameHandle { 107 | fn request(callback: Closure) -> Self { 108 | let raw = 109 | window().unwrap().request_animation_frame(callback.as_ref().unchecked_ref()).unwrap(); 110 | 111 | Self { raw, _callback: callback } 112 | } 113 | } 114 | 115 | impl Drop for AnimationFrameHandle { 116 | fn drop(&mut self) { 117 | window().unwrap().cancel_animation_frame(self.raw).ok(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /dom/src/cached_node.rs: -------------------------------------------------------------------------------- 1 | //! Nodes which cache mutations. 2 | 3 | use augdom::{Dom, Node}; 4 | use moxie::cache_with; 5 | use std::{ 6 | cell::Cell, 7 | fmt::{Debug, Formatter, Result as FmtResult}, 8 | }; 9 | 10 | /// A topologically-nested "incremental smart pointer" for an HTML element. 11 | /// 12 | /// Created during execution of the (element) macro and the element-specific 13 | /// wrappers. Offers a "stringly-typed" API for mutating the contained DOM 14 | /// nodes, adhering fairly closely to the upstream web specs. 15 | pub struct CachedNode { 16 | id: topo::CallId, 17 | last_child: Cell>, 18 | node: Node, 19 | } 20 | 21 | impl CachedNode { 22 | #[topo::nested] 23 | pub(crate) fn new(node: Node) -> Self { 24 | Self { node, last_child: Cell::new(None), id: topo::CallId::current() } 25 | } 26 | 27 | pub(crate) fn raw_node(&self) -> &Node { 28 | &self.node 29 | } 30 | 31 | // TODO accept PartialEq+ToString implementors 32 | #[topo::nested(slot = "&(self.id, name)")] 33 | pub(crate) fn set_attribute(&self, name: &'static str, value: &str) { 34 | let mut should_set = false; 35 | cache_with( 36 | value, 37 | |_| { 38 | // when this isn't the first time the attribute is being set for this element, 39 | // this closure executes while the previous attribute's guard is still live. 40 | // if we actually set the attribute here, it will be removed when this closure exits 41 | // which we definitely don't want. easiest fix is to set the attribute after our 42 | // hypothetical cleanup has completed 43 | should_set = true; 44 | let name = name.to_owned(); 45 | // TODO find a way to reuse the guard if we're replacing a previous value 46 | scopeguard::guard(self.node.clone(), move |node| { 47 | node.remove_attribute(&name); 48 | }) 49 | }, 50 | |_| {}, 51 | ); 52 | 53 | if should_set { 54 | self.node.set_attribute(name, value); 55 | } 56 | } 57 | 58 | pub(crate) fn ensure_child_attached(&self, new_child: &Node) { 59 | let prev_sibling = self.last_child.replace(Some(new_child.clone())); 60 | 61 | let existing = if prev_sibling.is_none() { 62 | self.node.first_child() 63 | } else { 64 | prev_sibling.and_then(|p| p.next_sibling()) 65 | }; 66 | 67 | if let Some(ref existing) = existing { 68 | if existing != new_child { 69 | self.node.replace_child(new_child, existing); 70 | } 71 | } else { 72 | self.node.append_child(new_child); 73 | } 74 | } 75 | 76 | pub(crate) fn remove_trailing_children(&self) { 77 | let last_desired_child = self.last_child.replace(None); 78 | 79 | // if there weren't any children declared this revision, we need to 80 | // make sure we clean up any from the last revision 81 | let mut next_to_remove = if let Some(c) = last_desired_child { 82 | // put back the last node we found this revision so this can be called multiple 83 | // times 84 | self.last_child.set(Some(c.clone())); 85 | c.next_sibling() 86 | } else { 87 | self.node.first_child() 88 | }; 89 | 90 | while let Some(to_remove) = next_to_remove { 91 | next_to_remove = to_remove.next_sibling(); 92 | self.node.remove_child(&to_remove).unwrap(); 93 | } 94 | } 95 | } 96 | 97 | impl Debug for CachedNode { 98 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 99 | f.debug_struct("CachedNode").field("node", &self.node).finish() 100 | } 101 | } 102 | 103 | #[cfg(test)] 104 | mod tests { 105 | use crate::{elements::just_all_of_it_ok::div, prelude::*}; 106 | use mox::mox; 107 | use moxie::{runtime::RunLoop, state}; 108 | 109 | #[wasm_bindgen_test::wasm_bindgen_test] 110 | pub fn attributes_change() { 111 | let mut rt = RunLoop::new(|| { 112 | let (value, key) = state(|| String::from("boo")); 113 | (key, mox!(
    )) 114 | }); 115 | let (key, node) = rt.run_once(); 116 | assert_eq!( 117 | node.raw_node_that_has_sharp_edges_please_be_careful().to_string(), 118 | "
    \n
    " 119 | ); 120 | 121 | key.set(String::from("aha")); 122 | let (_, node) = rt.run_once(); 123 | assert_eq!( 124 | node.raw_node_that_has_sharp_edges_please_be_careful().to_string(), 125 | "
    \n
    " 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /dom/src/elements.rs: -------------------------------------------------------------------------------- 1 | //! Element definitions generated from the listing on [MDN]. 2 | //! 3 | //! [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element 4 | 5 | /// A module for glob-importing all element creation functions, similar to the 6 | /// global HTML namespace. 7 | pub mod html { 8 | pub use super::{ 9 | body, 10 | embedding::{embed, iframe, object, param, picture, source}, 11 | forms::{ 12 | button, datalist, fieldset, form, input, label, legend, meter, optgroup, option, 13 | output, progress, select, textarea, 14 | }, 15 | html, 16 | interactive::{details, dialog, menu, summary}, 17 | media::{area, audio, img, map, track, video}, 18 | metadata::{base, head, link, meta, style, title}, 19 | scripting::{canvas, noscript, script}, 20 | sectioning::{ 21 | address, article, aside, footer, h1, h2, h3, h4, h5, h6, header, hgroup, main, nav, 22 | section, 23 | }, 24 | table::{caption, col, colgroup, table, tbody, td, tfoot, th, thead, tr}, 25 | text_content::{blockquote, dd, div, dl, dt, figcaption, figure, hr, li, ol, p, pre, ul}, 26 | text_semantics::{ 27 | a, abbr, b, bdi, bdo, br, cite, code, data, del, dfn, em, i, ins, kbd, mark, q, rb, rp, 28 | rt, rtc, ruby, s, samp, small, span, strong, sub, sup, time, u, var, wbr, 29 | }, 30 | }; 31 | } 32 | 33 | pub(crate) mod just_all_of_it_ok { 34 | pub use super::{ 35 | embedding::*, forms::*, interactive::*, media::*, metadata::*, scripting::*, sectioning::*, 36 | table::*, text_content::*, text_semantics::*, *, 37 | }; 38 | } 39 | 40 | pub mod embedding; 41 | pub mod forms; 42 | pub mod interactive; 43 | pub mod media; 44 | pub mod metadata; 45 | pub mod scripting; 46 | pub mod sectioning; 47 | pub mod table; 48 | pub mod text_content; 49 | pub mod text_semantics; 50 | 51 | html_element! { 52 | /// The [`` element][mdn] represents the root (top-level element) of an HTML document, 53 | /// so it is also referred to as the *root element*. All other elements must be descendants of 54 | /// this element. 55 | /// 56 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html 57 | 58 | 59 | children { 60 | tags { 61 | , 62 | } 63 | } 64 | 65 | attributes { 66 | /// Specifies the XML Namespace of the document. Default value is 67 | /// `http://www.w3.org/1999/xhtml`. This is required in documents parsed with XML parsers, 68 | /// and optional in text/html documents. 69 | xmlns 70 | } 71 | } 72 | 73 | html_element! { 74 | /// The [HTML `` element][mdn] represents the content of an HTML document. There can be 75 | /// only one `` element in a document. 76 | /// 77 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body 78 | 79 | 80 | categories { 81 | Sectioning 82 | } 83 | children { 84 | categories { 85 | Flow 86 | } 87 | } 88 | } 89 | 90 | macro_rules! body_events { 91 | ($($property:ident <- $event:ident,)+) => { 92 | $( 93 | impl crate::interfaces::event_target::EventTarget for BodyBuilder 94 | {} 95 | )+ 96 | 97 | impl BodyBuilder {$( 98 | /// Set an event handler. 99 | pub fn $property( 100 | self, 101 | callback: impl FnMut(augdom::event::$event) + 'static, 102 | ) -> Self { 103 | use crate::interfaces::event_target::EventTarget; 104 | self.on(callback) 105 | } 106 | )+} 107 | }; 108 | } 109 | 110 | body_events! { 111 | onafterprint <- AfterPrint, 112 | onbeforeprint <- BeforePrint, 113 | onhashchange <- HashChange, 114 | onmessage <- WebsocketMessage, 115 | onoffline <- Offline, 116 | ononline <- Online, 117 | onstorage <- Storage, 118 | onunload <- Unload, 119 | } 120 | 121 | html_element! { 122 | /// The [HTML `` element][mdn]—part of the [Web Components][wc] technology suite—is a 123 | /// placeholder inside a web component that you can fill with your own markup, which lets you 124 | /// create separate DOM trees and present them together. 125 | /// 126 | /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot 127 | /// [wc]: https://developer.mozilla.org/en-US/docs/Web/Web_Components 128 | 129 | 130 | categories { 131 | Flow, Phrasing 132 | } 133 | 134 | attributes { 135 | /// The slot's name. 136 | name 137 | } 138 | } 139 | 140 | html_element! { 141 | /// The [HTML Content Template (`