diff --git a/.cargo/config.toml b/.cargo/config.toml index 7b1b2f31b4d..c231417eda5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,7 @@ [target.'cfg(target_arch = "wasm32")'] runner = 'wasm-bindgen-test-runner' +# This section needs to be last. +# GitHub Actions modifies this section. [unstable] doctest-xcompile = true -# [unstable] must be the last section -- ci/write-optimisation-flags.sh relies on it diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md index b69312442e1..7acc4951a8e 100644 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -1,6 +1,6 @@ --- name: Documentation -about: Report an issue relating to this project's documetation. +about: Report an issue relating to this project's documentation. title: '' labels: documentation assignees: '' diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 619cfd96e24..3eaa44cd2ce 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,10 +5,24 @@ updates: schedule: interval: "weekly" day: "friday" - open-pull-requests-limit: 5 + open-pull-requests-limit: 2 + groups: + cargo-deps: + patterns: + - "*" - package-ecosystem: "npm" directory: "/website" schedule: interval: "monthly" target-branch: "master" + groups: + website-deps: + patterns: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + target-branch: "master" diff --git a/.github/workflows/auto-approve-maintainer-pr.yml b/.github/workflows/auto-approve-maintainer-pr.yml index c5632210dd3..b7efd1b4efd 100644 --- a/.github/workflows/auto-approve-maintainer-pr.yml +++ b/.github/workflows/auto-approve-maintainer-pr.yml @@ -1,6 +1,13 @@ name: Auto approve -on: pull_request_target +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + - ready_for_review + - review_requested jobs: auto-approve: @@ -9,14 +16,14 @@ jobs: steps: - name: Check if organization member id: is_organization_member - uses: JamesSingleton/is-organization-member@1.0.0 + uses: JamesSingleton/is-organization-member@1.0.1 with: organization: "yewstack" username: ${{ github.actor }} token: ${{ secrets.GITHUB_TOKEN }} - name: Auto approve - uses: hmarr/auto-approve-action@v2 + uses: hmarr/auto-approve-action@v3 if: ${{ steps.is_organization_member.outputs.result == 'true' || github.actor == 'dependabot[bot]' }} with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/benchmark-ssr.yml b/.github/workflows/benchmark-ssr.yml index 24b3baa4417..0d09ae695dd 100644 --- a/.github/workflows/benchmark-ssr.yml +++ b/.github/workflows/benchmark-ssr.yml @@ -6,9 +6,12 @@ on: branches: [master] paths: - .github/workflows/benchmark-ssr.yml - - "packages/yew" - - "examples/function_router" - - "tools/benchmark-ssr" + - "packages/yew/**" + - "packages/yew-macro/**" + - "packages/yew-router/**" + - "packages/yew-router-macro/**" + - "examples/function_router/**" + - "tools/benchmark-ssr/**" jobs: benchmark-ssr: @@ -19,7 +22,7 @@ jobs: - name: Checkout master uses: actions/checkout@v3 with: - repository: 'yewstack/yew' + repository: "yewstack/yew" ref: master path: yew-master @@ -29,37 +32,41 @@ jobs: path: current-pr - name: Setup toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: toolchain: stable - target: wasm32-unknown-unknown - profile: minimal + targets: wasm32-unknown-unknown - name: Restore Rust cache for master - uses: Swatinem/rust-cache@v1 + uses: Swatinem/rust-cache@v2 with: working-directory: yew-master key: master - name: Restore Rust cache for current pull request - uses: Swatinem/rust-cache@v1 + uses: Swatinem/rust-cache@v2 with: working-directory: current-pr key: pr - name: Run pull request benchmark - run: cargo run --profile=bench --bin benchmark-ssr -- --output-path ./output.json - working-directory: current-pr + run: cargo run --profile=bench -- --output-path ../output.json + working-directory: current-pr/tools/benchmark-ssr - name: Run master benchmark - run: cargo run --profile=bench --bin benchmark-ssr -- --output-path ./output.json - working-directory: yew-master + run: cargo run --profile=bench -- --output-path ../output.json + working-directory: yew-master/tools/benchmark-ssr + + - name: Write Pull Request ID + run: | + echo "${{ github.event.number }}" > .PR_NUMBER - name: Upload Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: benchmark-ssr path: | - yew-master/output.json - current-pr/output.json + .PR_NUMBER + yew-master/tools/output.json + current-pr/tools/output.json retention-days: 1 diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 9fc189e156e..970f12d8ba3 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -3,12 +3,12 @@ name: Benchmark on: push: paths-ignore: - - 'website/**' + - "website/**" branches: - master - pull_request_target: + pull_request: paths-ignore: - - 'website/**' + - "website/**" types: [labeled, synchronize, opened, reopened] permissions: @@ -18,236 +18,110 @@ permissions: contents: write jobs: - results: - runs-on: ubuntu-latest - needs: benchmark - - steps: - - uses: actions/checkout@v2 - - - run: | - touch results.json - echo '${{ needs.benchmark.outputs.results }}' >> results.json - - # gh-pages branch is updated and pushed automatically with extracted benchmark data - - name: Store benchmark result - uses: benchmark-action/github-action-benchmark@v1 - with: - name: "Yew master branch benchmarks (Lower is better)" - tool: "customSmallerIsBetter" - output-file-path: results.json - gh-pages-branch: "gh-pages" - # Access token to deploy GitHub Pages branch - github-token: ${{ secrets.GITHUB_TOKEN }} - # Push and deploy GitHub pages branch automatically - alert-threshold: "200%" - alert-comment-cc-users: "@yewstack/yew" - comment-always: ${{ github.event_name != 'pull_request_target' || contains(github.event.pull_request.labels.*.name, 'performance') }} - comment-on-alert: true - # Don't push to gh-pages if its a pull request - auto-push: ${{ github.event_name != 'pull_request_target' }} - save-data-file: ${{ github.event_name != 'pull_request_target' }} - benchmark: runs-on: ubuntu-latest - outputs: - results: ${{ steps.results.outputs.stdout }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: - path: "./yew" - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: "${{ github.head_ref }}" + path: "yew" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: repository: krausest/js-framework-benchmark - path: "./js-framework-benchmark" - ref: 678cd09a8e02b9a01bcb9b71dc9248d17a33ff82 + path: "js-framework-benchmark" - - uses: actions-rs/toolchain@v1 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: stable - target: wasm32-unknown-unknown - override: true - profile: minimal + targets: wasm32-unknown-unknown - - uses: jetli/wasm-pack-action@v0.3.0 + - uses: jetli/wasm-pack-action@v0.4.0 with: version: "latest" - name: Setup Node - uses: actions/setup-node@v1 - with: - node-version: 16 - - - uses: Swatinem/rust-cache@v1 - with: - working-directory: yew - - - uses: actions/cache@v2 + uses: actions/setup-node@v3 with: - path: ~/.npm - key: ${{ runner.os }}-benchmark-${{ hashFiles('js-framework-benchmark/package-lock.json') }}-${{ hashFiles('js-framework-benchmark/webdriver-ts/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-benchmark- - ${{ runner.os }} - - # Optimisation flags has no effect unless set at workspace level. - - name: Write optimisation flags - run: | - cat >> Cargo.toml << EOF - [profile.release] - lto = true - codegen-units = 1 - panic = "abort" - EOF - working-directory: yew - - - name: save yew-struct setup - shell: bash - run: | - mkdir yew-struct-setup - cp -r js-framework-benchmark/frameworks/keyed/yew/* yew-struct-setup/ - cd yew-struct-setup/bundled-dist - rm -rf ./* - # Will be enabled after https://github.com/krausest/js-framework-benchmark/pull/985 gets merged - # - name: save yew-hooks setup - # shell: bash - # run: | - # mkdir yew-hooks-setup - # cp -r js-framework-benchmark/frameworks/keyed/yew-hooks/* yew-hooks-setup/ - # cd yew-hooks-setup/bundled-dist - # rm -rf ./* - - - name: replace framework version in yew - shell: bash - run: | - replace=" \"frameworkVersion\": \"\"" - input=$(cat yew-struct-setup/package.json) - output=$(echo "$input" | sed -e "s@\"frameworkVersion\": .*\"@$replace@g") - if [[ "$input" == "$output" ]]; then - echo "ERROR: failed to configure framework version" - exit 1 - fi - echo "$output" > yew-struct-setup/package.json - echo "$output" - - # Will be enabled after https://github.com/krausest/js-framework-benchmark/pull/985 gets merged - # - name: replace framework version in yew-hooks - # shell: bash - # run: | - # replace=" \"frameworkVersion\": \"\"" - # input=$(cat yew-hooks-setup/package.json) - # output=$(echo "$input" | sed -e "s@\"frameworkVersion\": .*\"@$replace@g") - # if [[ "$input" == "$output" ]]; then - # echo "ERROR: failed to configure framework version" - # exit 1 - # fi - # echo "$output" > yew-hooks-setup/package.json - # echo "$output" - - - name: delete all frameworks - shell: bash - run: | - cd js-framework-benchmark/frameworks/keyed - rm -rf ./* - cd ../non-keyed - rm -rf ./* + node-version: 18 + cache: "npm" + cache-dependency-path: js-framework-benchmark/package-lock.json - - name: create framework folders - shell: bash - run: | - cd js-framework-benchmark/frameworks/keyed - mkdir yew-struct - # Will be enabled after https://github.com/krausest/js-framework-benchmark/pull/985 gets merged - # mkdir yew-hooks + - uses: Swatinem/rust-cache@v2 - - name: copy necessary framework files - shell: bash + - name: setup js-framework-benchmark + working-directory: js-framework-benchmark run: | - cp -r yew-struct-setup/* js-framework-benchmark/frameworks/keyed/yew-struct/ - # Will be enabled after https://github.com/krausest/js-framework-benchmark/pull/985 gets merged - # cp -r yew-hooks-setup/* js-framework-benchmark/frameworks/keyed/yew-hooks/ - - - name: build benchmark-struct app - shell: bash - run: | - cd yew/tools/benchmark-struct npm ci - npm run build-prod-without-tools-install + npm run install-server + npm run install-webdriver-ts - - name: build benchmark-hooks app - shell: bash + - name: setup benchmark-struct benchmark run: | - cd yew/tools/benchmark-hooks - npm ci - npm run build-prod-without-tools-install + ls -lauh + rm *.js + rm *.wasm + echo "STRUCT_BUILD_DIR=$PWD" >> $GITHUB_ENV + working-directory: js-framework-benchmark/frameworks/keyed/yew/bundled-dist/ - - name: move dist files - shell: bash + - name: build benchmark-struct app + working-directory: yew/tools/benchmark-struct run: | - mv yew/tools/benchmark-struct/bundled-dist js-framework-benchmark/frameworks/keyed/yew-struct/ - # Will be enabled after https://github.com/krausest/js-framework-benchmark/pull/985 gets merged - # mv yew/tools/benchmark-hooks/bundled-dist js-framework-benchmark/frameworks/keyed/yew-hooks/ + wasm-pack build \ + --release \ + --target web \ + --no-typescript \ + --out-name js-framework-benchmark-yew \ + --out-dir $STRUCT_BUILD_DIR - - name: js-framework-benchmark npm ci - shell: bash + - name: show built benchmark-struct benchmark files run: | - cd js-framework-benchmark - npm ci + ls -lauh js-framework-benchmark/frameworks/keyed/yew/bundled-dist/ - - name: js-framework-benchmark npm start - shell: bash + - name: setup yew-hooks benchmark run: | - cd js-framework-benchmark - npm start & + ls -lauh + rm *.js + rm *.wasm + echo "HOOKS_BUILD_DIR=$PWD" >> $GITHUB_ENV + working-directory: js-framework-benchmark/frameworks/keyed/yew-hooks/bundled-dist/ - - name: js-framework-benchmark/webdriver-ts npm ci - shell: bash + - name: build benchmark-hooks app + working-directory: yew/tools/benchmark-hooks run: | - cd js-framework-benchmark/webdriver-ts - npm ci - npm install chromedriver --chromedriver-force-download + wasm-pack build \ + --release \ + --target web \ + --no-typescript \ + --out-name js-framework-benchmark-yew-hooks \ + --out-dir $HOOKS_BUILD_DIR - - name: js-framework-benchmark/webdriver-ts npm run compile - shell: bash + - name: show built benchmark-hooks benchmark files run: | - cd js-framework-benchmark/webdriver-ts - npm run compile + ls -lauh js-framework-benchmark/frameworks/keyed/yew-hooks/bundled-dist/ - - name: js-framework-benchmark npm run build-prod - shell: bash + - name: run js-framework-benchmark server + working-directory: js-framework-benchmark run: | - cd js-framework-benchmark - npm run build-prod + npm start & + sleep 5 - name: js-framework-benchmark/webdriver-ts npm run bench - shell: bash - run: | - cd js-framework-benchmark/webdriver-ts - npm run bench -- --headless + working-directory: js-framework-benchmark/webdriver-ts + run: xvfb-run npm run bench -- --framework keyed/yew keyed/yew-hooks --runner playwright - - name: transform results into json - shell: bash - run: | - cd js-framework-benchmark/webdriver-ts/results - touch temp.txt - echo "[" >> temp.txt - for filename in *.json; do cat ${filename} >> temp.txt; echo "," >> temp.txt; done - sed -i '$ s/.$//' temp.txt #remove trailing comma - echo "]" >> temp.txt - mv temp.txt results.json - - - name: Build process-benchmark-results - shell: bash + - name: transform results to be fit for display benchmark-action/github-action-benchmark@v1 run: | - cd yew - cargo build --release -p process-benchmark-results + mkdir artifacts/ + jq -s . js-framework-benchmark/webdriver-ts/results/*.json | cargo run --manifest-path yew/Cargo.toml --release -p process-benchmark-results > artifacts/results.json + echo "$EVENT_INFO" > artifacts/PR_INFO + env: + EVENT_INFO: ${{ toJSON(github.event) }} - - name: transform results to be fit for display benchmark-action/github-action-benchmark@v1 - uses: mathiasvr/command-output@v1 - id: results + - name: Upload result artifacts + uses: actions/upload-artifact@v3 with: - run: cat js-framework-benchmark/webdriver-ts/results/results.json | ./yew/target/release/process-benchmark-results + name: results + path: artifacts/ + if-no-files-found: error diff --git a/.github/workflows/build-api-docs.yml b/.github/workflows/build-api-docs.yml index 376eb1cd0e0..188207ad1d0 100644 --- a/.github/workflows/build-api-docs.yml +++ b/.github/workflows/build-api-docs.yml @@ -19,22 +19,26 @@ jobs: env: PR_INFO_FILE: ".PR_INFO" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: nightly - override: true - profile: minimal components: rust-docs - name: Run cargo doc - uses: actions-rs/cargo@v1 env: RUSTDOCFLAGS: --cfg documenting --html-before-content ./api-docs/before-content.html --extend-css ./api-docs/styles.css -Z unstable-options --enable-index-page - with: - command: doc - args: -p yew -p yew-macro -p yew-router -p yew-router-macro -p yew-agent --no-deps --all-features + run: | + cargo doc \ + --no-deps \ + --all-features \ + -p yew \ + -p yew-macro \ + -p yew-router \ + -p yew-router-macro \ + -p yew-agent - name: Move files in correct directory run: | @@ -42,7 +46,7 @@ jobs: cp -r target/doc/* api-docs/dist/next - name: Upload build artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: api-docs path: api-docs/ @@ -55,7 +59,7 @@ jobs: - if: github.event_name == 'pull_request' name: Upload pr info - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pr-info path: "${{ env.PR_INFO_FILE }}" diff --git a/.github/workflows/build-website.yml b/.github/workflows/build-website.yml index 8f7fc91d6b6..79fb9b92049 100644 --- a/.github/workflows/build-website.yml +++ b/.github/workflows/build-website.yml @@ -20,10 +20,10 @@ jobs: env: PR_INFO_FILE: ".PR_INFO" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup node - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: "16" @@ -48,7 +48,7 @@ jobs: npm run build - name: Upload build artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: website path: website/build/ @@ -61,7 +61,7 @@ jobs: - if: github.event_name == 'pull_request' name: Upload pr info - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pr-info path: "${{ env.PR_INFO_FILE }}" diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 00000000000..57ecfa1527a --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,83 @@ +name: Clippy + +on: + pull_request: + paths: + - ".github/workflows/clippy.yml" + - "tools/**/*" + - "examples/**/*" + - "packages/**/*" + - "Cargo.toml" + - "Cargo.lock" + push: + branches: [master] + +jobs: + feature-soundness: + name: Feature Soundness + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + profile: + - dev + - release + steps: + - uses: actions/checkout@v3 + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Lint feature soundness + if: matrix.profile == 'dev' + run: bash ../../ci/feature-soundness.sh + working-directory: packages/yew + + - name: Lint feature soundness + if: matrix.profile == 'release' + run: bash ../../ci/feature-soundness-release.sh + working-directory: packages/yew + + - name: Run release clippy + if: matrix.profile == 'release' + run: | + ls packages | xargs -I {} \ + cargo clippy \ + -p {} \ + --all-targets \ + --all-features \ + --workspace \ + -- -D warnings + + clippy: + name: Clippy Workspace + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + toolchain: + - stable + + steps: + - uses: actions/checkout@v3 + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }} + components: clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Run clippy + run: | + cargo clippy \ + --all-targets \ + --all-features \ + --workspace \ + -- -D warnings diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index 2c8a8b9b495..cdc43e3ee47 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -13,17 +13,13 @@ jobs: name: cargo fmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v3 + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: nightly - override: true - profile: minimal components: rustfmt - name: Run fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - toolchain: nightly - args: --all -- --check + run: cargo +nightly fmt --all -- --check --unstable-features diff --git a/.github/workflows/inspect-next-changelogs.yml b/.github/workflows/inspect-next-changelogs.yml index fe8857e3fb1..5c1ebc1f737 100644 --- a/.github/workflows/inspect-next-changelogs.yml +++ b/.github/workflows/inspect-next-changelogs.yml @@ -12,25 +12,27 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: stable - override: true - profile: minimal - name: Build changelog generator run: cargo build --release -p changelog + working-directory: tools - name: Read yew changelog in this step run: ./target/release/changelog yew minor -t ${{ secrets.GITHUB_TOKEN }} + working-directory: tools - name: Read yew-router changelog in this step run: ./target/release/changelog yew-router minor -t ${{ secrets.GITHUB_TOKEN }} + working-directory: tools - name: Read yew-agent changelog in this step run: ./target/release/changelog yew-agent minor -t ${{ secrets.GITHUB_TOKEN }} + working-directory: tools diff --git a/.github/workflows/main-checks.yml b/.github/workflows/main-checks.yml index b5771373bd1..6e0aa009a1c 100644 --- a/.github/workflows/main-checks.yml +++ b/.github/workflows/main-checks.yml @@ -2,65 +2,21 @@ name: Main Checks on: pull_request: + paths: + - ".github/workflows/main-checks.yml" + - "ci/**" + - "packages/**/*" + - "Cargo.toml" + - "Cargo.lock" push: branches: [master] jobs: - - clippy: - name: Clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal - components: clippy - - - uses: Swatinem/rust-cache@v1 - - - name: Run clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-targets --features "csr,ssr,hydration,tokio" -- -D warnings - - - name: Lint feature soundness - run: bash ../../ci/feature-soundness.sh - working-directory: packages/yew - - - clippy-release: - name: Clippy on release profile - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal - components: clippy - - - uses: Swatinem/rust-cache@v1 - - - name: Run clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-targets --features "csr,ssr,hydration,tokio" --release -- -D warnings - - - name: Lint feature soundness - run: bash ../../ci/feature-soundness-release.sh - working-directory: packages/yew - spell_check: name: spellcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Check spelling run: | sudo apt-get install aspell @@ -70,93 +26,76 @@ jobs: name: Documentation Tests runs-on: ubuntu-latest steps: - - uses: Swatinem/rust-cache@v1 + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 - - uses: actions-rs/toolchain@v1 + # for wasm-bindgen-cli, always use stable rust + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: stable - override: true - profile: minimal - - - name: Install wasm-bindgen-test-runner - run: cargo install --git https://github.com/hamza1311/wasm-bindgen --branch respect-rustdoc-tmp-paths wasm-bindgen-cli - - uses: actions/checkout@v2 + - name: Install wasm-bindgen-cli + shell: bash + run: ./ci/install-wasm-bindgen-cli.sh - - uses: actions-rs/toolchain@v1 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: nightly - target: wasm32-unknown-unknown - override: true - profile: minimal + targets: wasm32-unknown-unknown - uses: browser-actions/setup-geckodriver@latest - - uses: nanasess/setup-chromedriver@v1 - - - name: Run doctest - uses: actions-rs/cargo@v1 - with: - command: test - args: --doc --workspace --exclude yew --exclude changelog --exclude website-test --exclude ssr_router --exclude simple_ssr --exclude benchmark-ssr --target wasm32-unknown-unknown - - - name: Run website code snippet tests - uses: actions-rs/cargo@v1 with: - command: test - args: -p website-test --target wasm32-unknown-unknown + token: ${{ secrets.GITHUB_TOKEN }} + - uses: nanasess/setup-chromedriver@v2 - - name: Run doctest - yew with features - uses: actions-rs/cargo@v1 - with: - command: test - args: -p yew --doc --all-features --target wasm32-unknown-unknown + - name: Run doctest + run: | + ls packages | xargs -I {} \ + cargo test \ + -p {} \ + --doc \ + --all-features \ + --target wasm32-unknown-unknown integration_tests: name: Integration Tests on ${{ matrix.toolchain }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: toolchain: - # anyway to dynamically grep the MSRV from Cargo.toml? - - 1.60.0 # MSRV + - 1.64.0 - stable steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - # for wasm-bindgen-cli, always use stable rust + - uses: actions/checkout@v3 + + - uses: Swatinem/rust-cache@v2 + + # for wasm-bindgen-cli, always use stable rust + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: stable - profile: minimal - - uses: actions-rs/toolchain@v1 + - name: Install wasm-bindgen-cli + shell: bash + run: ./ci/install-wasm-bindgen-cli.sh + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} - target: wasm32-unknown-unknown - override: true - profile: minimal + targets: wasm32-unknown-unknown - uses: browser-actions/setup-geckodriver@latest - - uses: nanasess/setup-chromedriver@v1 - - - uses: Swatinem/rust-cache@v1 - - - name: Install wasm-bindgen-cli - shell: bash - run: | - if [ ! -f "Cargo.lock" ]; then - cargo fetch - fi - - VERSION=$(cargo pkgid --frozen wasm-bindgen | cut -d ":" -f 3) - - # Cargo decided to change syntax after 1.61 - if [ "$VERSION" = "" ]; then - VERSION=$(cargo pkgid --frozen wasm-bindgen | cut -d "@" -f 2) - fi + with: + token: ${{ secrets.GITHUB_TOKEN }} - cargo +stable install --version $VERSION wasm-bindgen-cli + - uses: nanasess/setup-chromedriver@v2 - name: Run tests - yew run: | @@ -174,54 +113,54 @@ jobs: name: Unit Tests on ${{ matrix.toolchain }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: toolchain: - # anyway to dynamically grep the MSRV from Cargo.toml? - - 1.60.0 # MSRV + - 1.64.0 - stable - nightly steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions-rs/toolchain@v1 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.toolchain }} - override: true - profile: minimal - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 - name: Run native tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-targets --workspace --exclude yew --exclude website-test --exclude ssr_router --exclude simple_ssr + env: + # workaround for lack of ternary operator + # see https://github.com/orgs/community/discussions/25725 + RUSTFLAGS: ${{ matrix.toolchain == 'nightly' && '--cfg nightly_yew' || '' }} + run: ls packages | grep -v "^yew$" | xargs -I {} cargo test --all-targets -p {} - name: Run native tests for yew - uses: actions-rs/cargo@v1 - with: - command: test - args: -p yew --features "csr,ssr,hydration,tokio" + env: + # workaround for lack of ternary operator + # see https://github.com/orgs/community/discussions/25725 + RUSTFLAGS: ${{ matrix.toolchain == 'nightly' && '--cfg nightly_yew' || '' }} + run: cargo test -p yew --all-features test-lints: name: Test lints on nightly runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v3 + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: nightly - override: true - profile: minimal - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: -p yew-macro test_html_lints --features lints + env: + RUSTFLAGS: --cfg nightly_yew --cfg yew_lints + run: cargo test -p yew-macro test_html_lints diff --git a/.github/workflows/post-benchmark-ssr.yml b/.github/workflows/post-benchmark-ssr.yml index e564e93fb6b..c11e8f2cb4b 100644 --- a/.github/workflows/post-benchmark-ssr.yml +++ b/.github/workflows/post-benchmark-ssr.yml @@ -9,15 +9,15 @@ on: jobs: post-benchmark-ssr: + if: github.event.workflow_run.event == 'pull_request' name: Post Comment on Pull Request runs-on: ubuntu-latest steps: - name: Download Repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - - if: github.event.workflow_run.event == 'pull_request' - name: Download Artifact + - name: Download Artifact uses: Legit-Labs/action-download-artifact@v2 with: github_token: "${{ secrets.GITHUB_TOKEN }}" @@ -29,6 +29,15 @@ jobs: - name: Make pull request comment run: python3 ci/make_benchmark_ssr_cmt.py + - name: Read Pull Request ID + run: | + PR_NUMBER=$(cat "benchmark-ssr/.PR_NUMBER") + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "pr number invalid" + exit 1 + fi + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + - name: Post Comment uses: actions/github-script@v6 with: diff --git a/.github/workflows/post-benchmark.yml b/.github/workflows/post-benchmark.yml new file mode 100644 index 00000000000..85e50c4c6c8 --- /dev/null +++ b/.github/workflows/post-benchmark.yml @@ -0,0 +1,47 @@ +name: "Post benchmark results" + +on: + workflow_run: + workflows: ["Benchmark"] + types: + - completed + +jobs: + post-benchmark-results: + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + + steps: + # Checkout repo for the github-action-benchmark action + - uses: actions/checkout@v3 + + - name: Download result artifacts + uses: Legit-Labs/action-download-artifact@v2 + with: + workflow: benchmark.yml + run_id: ${{ github.event.workflow_run.id }} + name: results + path: ./artifacts + + - name: Test for PR + uses: mathiasvr/command-output@v1 + id: test-pr + with: + run: cat artifacts/PR_INFO + + # gh-pages branch is updated and pushed automatically with extracted benchmark data + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1 + with: + name: "Yew master branch benchmarks (Lower is better)" + tool: "customSmallerIsBetter" + output-file-path: artifacts/results.json + gh-pages-branch: "gh-pages" + # Access token to deploy GitHub Pages branch + github-token: ${{ secrets.GITHUB_TOKEN }} + # Push and deploy GitHub pages branch automatically + alert-threshold: "200%" + alert-comment-cc-users: "@yewstack/yew" + # Only push when this is a non-pr commit that has been benchmarked, i.e. master + auto-push: ${{ fromJSON(steps.test-pr.outputs.stdout).number == '' }} + save-data-file: ${{ fromJSON(steps.test-pr.outputs.stdout).number == '' }} diff --git a/.github/workflows/post-size-cmp.yml b/.github/workflows/post-size-cmp.yml index 3baba67b990..67ad6157b28 100644 --- a/.github/workflows/post-size-cmp.yml +++ b/.github/workflows/post-size-cmp.yml @@ -14,17 +14,27 @@ jobs: steps: - name: Download Repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - if: github.event.workflow_run.event == 'pull_request' - name: Download Artifact + name: Download Artifact (master) uses: Legit-Labs/action-download-artifact@v2 with: github_token: "${{ secrets.GITHUB_TOKEN }}" workflow: size-cmp.yml run_id: ${{ github.event.workflow_run.id }} - name: size-cmp-info - path: "size-cmp-info/" + name: size-cmp-master-info + path: "size-cmp-master-info/" + + - if: github.event.workflow_run.event == 'pull_request' + name: Download Artifact (PR) + uses: Legit-Labs/action-download-artifact@v2 + with: + github_token: "${{ secrets.GITHUB_TOKEN }}" + workflow: size-cmp.yml + run_id: ${{ github.event.workflow_run.id }} + name: size-cmp-pr-info + path: "size-cmp-pr-info/" - name: Make pull request comment run: python3 ci/make_example_size_cmt.py diff --git a/.github/workflows/publish-api-docs.yml b/.github/workflows/publish-api-docs.yml index fddf5a7cb6b..fa5f5b08d3e 100644 --- a/.github/workflows/publish-api-docs.yml +++ b/.github/workflows/publish-api-docs.yml @@ -18,7 +18,7 @@ jobs: exit 1 # need to checkout to get "firebase.json", ".firebaserc" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Download build artifact uses: dawidd6/action-download-artifact@v2 diff --git a/.github/workflows/publish-examples.yml b/.github/workflows/publish-examples.yml index 3a466bd5f56..374185c321a 100644 --- a/.github/workflows/publish-examples.yml +++ b/.github/workflows/publish-examples.yml @@ -2,9 +2,9 @@ name: Publish Examples on: push: branches: [master] - paths-ignore: - - "**/*.md" - - "website/**" + paths: + - 'ci/**' + - 'examples/**' jobs: publish: @@ -15,21 +15,18 @@ jobs: RUSTUP_TOOLCHAIN: nightly steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: actions/checkout@v3 + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: nightly - target: wasm32-unknown-unknown + targets: wasm32-unknown-unknown components: rust-src - override: true - profile: minimal - - - name: Write optimisation flags - run: ./ci/write-optimisation-flags.sh - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 - - uses: jetli/trunk-action@v0.1.0 + - uses: jetli/trunk-action@v0.4.0 with: version: 'latest' @@ -37,7 +34,7 @@ jobs: run: ./ci/build-examples.sh - name: Deploy to Firebase - uses: siku2/action-hosting-deploy@v0 + uses: siku2/action-hosting-deploy@v1 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" firebaseToken: "${{ secrets.FIREBASE_TOKEN }}" diff --git a/.github/workflows/publish-website.yml b/.github/workflows/publish-website.yml index 803ea90866d..5617c03fa24 100644 --- a/.github/workflows/publish-website.yml +++ b/.github/workflows/publish-website.yml @@ -17,7 +17,7 @@ jobs: echo "build failed" exit 1 # need to checkout to get "firebase.json", ".firebaserc" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Download build artifact uses: Legit-Labs/action-download-artifact@v2 diff --git a/.github/workflows/publish-yew-agent.yml b/.github/workflows/publish-yew-agent.yml deleted file mode 100644 index 55a94b25176..00000000000 --- a/.github/workflows/publish-yew-agent.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Publish yew-agent -permissions: - contents: write - -on: - workflow_dispatch: - inputs: - level: - description: "Version Level major|minor|patch" - required: true - type: choice - options: - - patch - - minor - - major -jobs: - publish: - name: Publish yew-agent - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v2 - with: - token: "${{ secrets.YEWTEMPBOT_TOKEN }}" - fetch-depth: 0 - - - name: Config Git - uses: oleksiyrudenko/gha-git-credentials@v2-latest - with: - token: "${{ secrets.YEWTEMPBOT_TOKEN }}" - - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal - - - name: Install cargo binary dependencies - uses: baptiste0928/cargo-install@v1 - with: - crate: cargo-release - version: 0.18.5 - - - name: Build changelog generator - run: cargo build --release -p changelog - - - name: Generate changelog - uses: mathiasvr/command-output@v1 - id: changelog - with: - run: ./target/release/changelog yew-agent ${{ github.event.inputs.level }} -t ${{ secrets.GITHUB_TOKEN }} - - - name: Commit changelog - run: | - git add CHANGELOG.md - git commit -m "update CHANGELOG.md for yew-agent release" - git push origin master - - - name: Release yew-agent - run: cargo release ${PUBLISH_LEVEL} --token ${CRATES_TOKEN} --execute --no-confirm --package yew-agent - env: - PUBLISH_LEVEL: ${{ github.event.inputs.level }} - CRATES_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - - - name: Get tag - id: gettag - uses: WyriHaximus/github-action-get-previous-tag@v1 - - - name: Create a version branch - uses: peterjgrainger/action-create-branch@v2.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - branch: ${{ steps.gettag.outputs.tag }} - - - name: Create a Release - uses: softprops/action-gh-release@v1 - with: - token: ${{ secrets.YEWTEMPBOT_TOKEN }} - tag_name: ${{ steps.gettag.outputs.tag }} - body: ${{ steps.changelog.outputs.stdout }} diff --git a/.github/workflows/publish-yew-router-only.yml b/.github/workflows/publish-yew-router-only.yml deleted file mode 100644 index 51cf052f65d..00000000000 --- a/.github/workflows/publish-yew-router-only.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Publish yew-router ONLY (WITHOUT MACRO) -permissions: - contents: write - -on: - workflow_dispatch: - inputs: - level: - description: "Version Level major|minor|patch" - required: true - type: choice - options: - - patch - - minor - - major -jobs: - publish: - name: Publish yew-router - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v2 - with: - token: "${{ secrets.YEWTEMPBOT_TOKEN }}" - fetch-depth: 0 - - - name: Config Git - uses: oleksiyrudenko/gha-git-credentials@v2-latest - with: - token: "${{ secrets.YEWTEMPBOT_TOKEN }}" - - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal - - - name: Install cargo binary dependencies - uses: baptiste0928/cargo-install@v1 - with: - crate: cargo-release - version: 0.18.5 - - - name: Build changelog generator - run: cargo build --release -p changelog - - - name: Generate changelog - uses: mathiasvr/command-output@v1 - id: changelog - with: - run: ./target/release/changelog yew-router ${{ github.event.inputs.level }} -t ${{ secrets.GITHUB_TOKEN }} - - - name: Commit changelog - run: | - git add CHANGELOG.md - git commit -m "update CHANGELOG.md for yew-router release" - git push origin master - - - name: Release yew-router - run: cargo release ${PUBLISH_LEVEL} --token ${CRATES_TOKEN} --execute --no-confirm --package yew-router - env: - PUBLISH_LEVEL: ${{ github.event.inputs.level }} - CRATES_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - - - name: Get tag - id: gettag - uses: WyriHaximus/github-action-get-previous-tag@v1 - - - name: Create a version branch - uses: peterjgrainger/action-create-branch@v2.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - branch: ${{ steps.gettag.outputs.tag }} - - - name: Create a Release - uses: softprops/action-gh-release@v1 - with: - token: ${{ secrets.YEWTEMPBOT_TOKEN }} - tag_name: ${{ steps.gettag.outputs.tag }} - body: ${{ steps.changelog.outputs.stdout }} diff --git a/.github/workflows/publish-yew-router.yml b/.github/workflows/publish-yew-router.yml deleted file mode 100644 index 3e6e88a8c59..00000000000 --- a/.github/workflows/publish-yew-router.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Publish yew-router & yew-router-macro -permissions: - contents: write - -on: - workflow_dispatch: - inputs: - level: - description: "Version Level major|minor|patch" - required: true - type: choice - options: - - patch - - minor - - major -jobs: - publish: - name: Publish yew-router - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v2 - with: - token: "${{ secrets.YEWTEMPBOT_TOKEN }}" - fetch-depth: 0 - - - name: Config Git - uses: oleksiyrudenko/gha-git-credentials@v2-latest - with: - token: "${{ secrets.YEWTEMPBOT_TOKEN }}" - - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal - - - name: Install cargo binary dependencies - uses: baptiste0928/cargo-install@v1 - with: - crate: cargo-release - version: 0.18.5 - - - name: Build changelog generator - run: cargo build --release -p changelog - - - name: Generate changelog - uses: mathiasvr/command-output@v1 - id: changelog - with: - run: ./target/release/changelog yew-router ${{ github.event.inputs.level }} -t ${{ secrets.GITHUB_TOKEN }} - - - name: Commit changelog - run: | - git add CHANGELOG.md - git commit -m "update CHANGELOG.md for yew-router release" - git push origin master - - - name: Release yew-router-macro - run: cargo release ${PUBLISH_LEVEL} --token ${CRATES_TOKEN} --execute --no-confirm --package yew-router-macro - env: - PUBLISH_LEVEL: ${{ github.event.inputs.level }} - CRATES_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - - - name: Release yew-router - run: cargo release ${PUBLISH_LEVEL} --token ${CRATES_TOKEN} --execute --no-confirm --package yew-router - env: - PUBLISH_LEVEL: ${{ github.event.inputs.level }} - CRATES_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - - - name: Get tag - id: gettag - uses: WyriHaximus/github-action-get-previous-tag@v1 - - - name: Create a version branch - uses: peterjgrainger/action-create-branch@v2.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - branch: ${{ steps.gettag.outputs.tag }} - - - name: Create a Release - uses: softprops/action-gh-release@v1 - with: - token: ${{ secrets.YEWTEMPBOT_TOKEN }} - tag_name: ${{ steps.gettag.outputs.tag }} - body: ${{ steps.changelog.outputs.stdout }} diff --git a/.github/workflows/publish-yew.yml b/.github/workflows/publish-yew.yml deleted file mode 100644 index 80ceb948634..00000000000 --- a/.github/workflows/publish-yew.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Publish yew & yew-macro -permissions: - contents: write - -on: - workflow_dispatch: - inputs: - level: - description: "Version Level major|minor|patch" - required: true - type: choice - options: - - patch - - minor - - major -jobs: - publish: - name: Publish yew - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v2 - with: - token: "${{ secrets.YEWTEMPBOT_TOKEN }}" - fetch-depth: 0 - - - name: Config Git - uses: oleksiyrudenko/gha-git-credentials@v2-latest - with: - token: "${{ secrets.YEWTEMPBOT_TOKEN }}" - - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal - - - name: Install cargo binary dependencies - uses: baptiste0928/cargo-install@v1 - with: - crate: cargo-release - version: 0.18.5 - - - name: Build changelog generator - run: cargo build --release -p changelog - - - name: Generate changelog - uses: mathiasvr/command-output@v1 - id: changelog - with: - run: ./target/release/changelog yew ${{ github.event.inputs.level }} -t ${{ secrets.GITHUB_TOKEN }} - - - name: Commit changelog - run: | - git add CHANGELOG.md - git commit -m "update CHANGELOG.md for yew release" - git push origin master - - - name: Release yew-macro - run: cargo release ${PUBLISH_LEVEL} --token ${CRATES_TOKEN} --execute --no-confirm --package yew-macro - env: - PUBLISH_LEVEL: ${{ github.event.inputs.level }} - CRATES_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - - - name: Release yew - run: cargo release ${PUBLISH_LEVEL} --token ${CRATES_TOKEN} --execute --no-confirm --package yew - env: - PUBLISH_LEVEL: ${{ github.event.inputs.level }} - CRATES_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - - - name: Get tag - id: gettag - uses: WyriHaximus/github-action-get-previous-tag@v1 - - - name: Create a version branch - uses: peterjgrainger/action-create-branch@v2.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - branch: ${{ steps.gettag.outputs.tag }} - - - name: Create a Release - uses: softprops/action-gh-release@v1 - with: - token: ${{ secrets.YEWTEMPBOT_TOKEN }} - tag_name: ${{ steps.gettag.outputs.tag }} - body: ${{ steps.changelog.outputs.stdout }} diff --git a/.github/workflows/publish-yew-only.yml b/.github/workflows/publish.yml similarity index 53% rename from .github/workflows/publish-yew-only.yml rename to .github/workflows/publish.yml index b6ba2f9b221..d45042ea425 100644 --- a/.github/workflows/publish-yew-only.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Publish yew ONLY (WITHOUT MACRO) +name: Publish yew package(s) permissions: contents: write @@ -14,62 +14,63 @@ on: - patch - minor - major + packages: + description: "List of packages to publish (space separated)" + required: true + type: string + jobs: publish: name: Publish yew runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: token: "${{ secrets.YEWTEMPBOT_TOKEN }}" fetch-depth: 0 - name: Config Git - uses: oleksiyrudenko/gha-git-credentials@v2-latest + uses: oleksiyrudenko/gha-git-credentials@v2.1.1 with: token: "${{ secrets.YEWTEMPBOT_TOKEN }}" - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master with: toolchain: stable - override: true - profile: minimal - name: Install cargo binary dependencies - uses: baptiste0928/cargo-install@v1 + uses: baptiste0928/cargo-install@v2 with: crate: cargo-release - version: 0.18.5 - - - name: Build changelog generator - run: cargo build --release -p changelog + version: =0.23.1 - - name: Generate changelog - uses: mathiasvr/command-output@v1 - id: changelog - with: - run: ./target/release/changelog yew ${{ github.event.inputs.level }} -t ${{ secrets.GITHUB_TOKEN }} + - name: Cargo login + run: cargo login ${{ secrets.CRATES_IO_TOKEN }} - - name: Commit changelog + - name: Build command + shell: bash + env: + PACKAGES: ${{ github.event.inputs.packages }} run: | - git add CHANGELOG.md - git commit -m "update CHANGELOG.md for yew release" - git push origin master + output="" + for pkg in ${{ github.event.inputs.packages }} + do + output+="--package $pkg " + done + echo "pkg=$output" >> $GITHUB_ENV - name: Release yew - run: cargo release ${PUBLISH_LEVEL} --token ${CRATES_TOKEN} --execute --no-confirm --package yew - env: - PUBLISH_LEVEL: ${{ github.event.inputs.level }} - CRATES_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} + run: cargo release ${{ github.event.inputs.level }} --execute --no-confirm ${{ env.pkg }} - name: Get tag id: gettag uses: WyriHaximus/github-action-get-previous-tag@v1 - name: Create a version branch - uses: peterjgrainger/action-create-branch@v2.0.1 + if: github.event.inputs.level != 'patch' + uses: peterjgrainger/action-create-branch@v2.4.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/size-cmp.yml b/.github/workflows/size-cmp.yml index 932208eb8d4..d734f07893b 100644 --- a/.github/workflows/size-cmp.yml +++ b/.github/workflows/size-cmp.yml @@ -13,76 +13,60 @@ on: jobs: size-cmp: - name: Compare Size between master and current Pull Request + name: Collect ${{ matrix.target }} Size runs-on: ubuntu-latest + strategy: + matrix: + target: ["master", "pr"] steps: - name: Checkout master uses: actions/checkout@v3 + if: ${{ matrix.target == 'master' }} with: - repository: 'yewstack/yew' + repository: "yewstack/yew" ref: master - path: yew-master - name: Checkout pull request uses: actions/checkout@v3 - with: - path: current-pr + if: ${{ matrix.target == 'pr' }} + + - name: Write Optimisation Flags + run: | + echo 'share-generics = true' >> .cargo/config.toml + echo 'build-std = ["std", "panic_abort"]' >> .cargo/config.toml + echo 'build-std-features = ["panic_immediate_abort"]' >> .cargo/config.toml - name: Setup toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: toolchain: nightly - target: wasm32-unknown-unknown components: rust-src - override: true - profile: minimal + targets: wasm32-unknown-unknown - name: Restore Rust cache for master - uses: Swatinem/rust-cache@v1 - with: - working-directory: yew-master - key: master - - - name: Restore Rust cache for current pull request - uses: Swatinem/rust-cache@v1 - with: - working-directory: current-pr - key: pr + uses: Swatinem/rust-cache@v2 - name: Setup Trunk - uses: jetli/trunk-action@v0.1.0 + uses: jetli/trunk-action@v0.4.0 with: - version: 'latest' - - - name: Write optimisation flags for master - run: ./ci/write-optimisation-flags.sh - working-directory: yew-master - - - name: Write optimisation flags for pull request - run: ./ci/write-optimisation-flags.sh - working-directory: current-pr - - - name: Build master examples - run: find examples/*/index.html | xargs -I '{}' trunk build --release '{}' || exit 0 - working-directory: yew-master - env: - RUSTUP_TOOLCHAIN: nightly + version: "latest" - - name: Build pull request examples - run: find examples/*/index.html | xargs -I '{}' trunk build --release '{}' || exit 0 - working-directory: current-pr + - name: Build examples + run: find ./*/index.html | xargs -I '{}' trunk build --release '{}' || exit 0 + working-directory: examples env: RUSTUP_TOOLCHAIN: nightly + RUSTFLAGS: --cfg nightly_yew - name: Collect size information - run: python3 current-pr/ci/collect_sizes.py + run: python3 ci/collect_sizes.py env: ISSUE_NUMBER: ${{ github.event.number }} - name: Upload Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: - name: size-cmp-info + name: size-cmp-${{ matrix.target }}-info path: ".SIZE_CMP_INFO" retention-days: 1 diff --git a/.github/workflows/test-website.yml b/.github/workflows/test-website.yml new file mode 100644 index 00000000000..8f1c753b359 --- /dev/null +++ b/.github/workflows/test-website.yml @@ -0,0 +1,50 @@ +--- +name: "Test Website" + +on: + pull_request: + paths: + - ".github/workflows/test-website.yml" + - "packages/**/*" + - "website/**/*" + push: + branches: [master] + +jobs: + website_tests: + name: Tests Website Snippets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + # for wasm-bindgen-cli, always use stable rust + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - name: Install wasm-bindgen-cli + shell: bash + run: ./ci/install-wasm-bindgen-cli.sh + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly + targets: wasm32-unknown-unknown + + - uses: Swatinem/rust-cache@v2 + + - uses: browser-actions/setup-geckodriver@latest + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: nanasess/setup-chromedriver@v2 + + - name: Run website code snippet tests + run: cargo test -p website-test --target wasm32-unknown-unknown diff --git a/.gitignore b/.gitignore index 80db1875443..ff4c7afbdf4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ target/ -Cargo.lock # backup files generated by rustfmt **/*.rs.bk diff --git a/CHANGELOG.md b/CHANGELOG.md index e9fcb6e8a80..0f2c0d9168f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,132 @@ # Changelog +## ✨ yew **0.20.0** *(2022-11-xx)* + +#### Changelog + +- #### 🛠 Fixes + + - Fix onsubmit event type in docs. [[@Allan](https://github.com/Allan), [#2926](https://github.com/yewstack/yew/pull/2926)] + - Fix issues with tuples in closing tag. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2886](https://github.com/yewstack/yew/pull/2886)] + - Fix checked property being reset. [[@WorldSEnder](https://github.com/WorldSEnder), [#2907](https://github.com/yewstack/yew/pull/2907)] + - Fix VList Stream in SSR. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2801](https://github.com/yewstack/yew/pull/2801)] + - Fixed `NodeRef` not being implicitly cloned with components. [[@wdcocq](https://github.com/wdcocq), [#2775](https://github.com/yewstack/yew/pull/2775)] + - Attributes: Fix apply_diff_index_maps. [[@Dietmar Maurer](https://github.com/Dietmar Maurer), [#2653](https://github.com/yewstack/yew/pull/2653)] + - Fix bubbling of events originating in shadow dom. [[@WorldSEnder](https://github.com/WorldSEnder), [#2627](https://github.com/yewstack/yew/pull/2627)] + - Fix some Hook edge cases. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2592](https://github.com/yewstack/yew/pull/2592)] + - Fix issue with node refs and hydration. [[@WorldSEnder](https://github.com/WorldSEnder), [#2597](https://github.com/yewstack/yew/pull/2597)] + - Fix macro hygiene issues. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2585](https://github.com/yewstack/yew/pull/2585)] + - Fix casing of dynamic tags. [[@WorldSEnder](https://github.com/WorldSEnder), [#2578](https://github.com/yewstack/yew/pull/2578)] + - Automatically convert closure to callback for component properties. [[@Finn Bear](https://github.com/Finn Bear), [#2554](https://github.com/yewstack/yew/pull/2554)] + - Fix a problem with NodeRefs and VTags, ref. [[@WorldSEnder](https://github.com/WorldSEnder), [#2279](https://github.com/yewstack/yew/pull/2279)] + - Fix defaulted type parameter.. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2284](https://github.com/yewstack/yew/pull/2284)] + - Use Ref::filter_map if rustc is later than 1.63. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2904](https://github.com/yewstack/yew/pull/2904)] + - Evaluate props in the order they're defined. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2887](https://github.com/yewstack/yew/pull/2887)] + - Context: Avoid storing a copy of children. [[@Dietmar Maurer](https://github.com/Dietmar Maurer), [#2885](https://github.com/yewstack/yew/pull/2885)] + - Various improvements to Classes, oriented around reducing allocations. [[@Nathan West](https://github.com/Nathan West), [#2870](https://github.com/yewstack/yew/pull/2870)] + - Resume Suspension upon unmount. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2874](https://github.com/yewstack/yew/pull/2874)] + - Make fn update() re-render the component by default. [[@Cecile Tonglet](https://github.com/Cecile Tonglet), [#2786](https://github.com/yewstack/yew/pull/2786)] + - Do not detach child elements if parent element is about to be detached. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2420](https://github.com/yewstack/yew/pull/2420)] + - remove some unsafes by using atomics. [[@WorldSEnder](https://github.com/WorldSEnder), [#2186](https://github.com/yewstack/yew/pull/2186)] + - `use_prepared_state` & `use_transitive_state`. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2650](https://github.com/yewstack/yew/pull/2650)] + - Silence some warnings from derive(Properties). [[@WorldSEnder](https://github.com/WorldSEnder), [#2266](https://github.com/yewstack/yew/pull/2266)] + - onsubmit should be a SubmitEvent. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2816](https://github.com/yewstack/yew/pull/2816)] + +- #### ⚡️ Features + + - Add VNode::from_html_unchecked. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2842](https://github.com/yewstack/yew/pull/2842)] + - Make Yew lints opt-in. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2882](https://github.com/yewstack/yew/pull/2882)] + - Allow skipping a callback when reforming. [[@Jens Reimann](https://github.com/Jens Reimann), [#2864](https://github.com/yewstack/yew/pull/2864)] + - Polled SSR Stream. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2824](https://github.com/yewstack/yew/pull/2824)] + - Add send_stream method for Scope. [[@laizy](https://github.com/laizy), [#2619](https://github.com/yewstack/yew/pull/2619)] + - Allow functions returning unit in `use_effect`. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2849](https://github.com/yewstack/yew/pull/2849)] + - Configurable Runtime. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2772](https://github.com/yewstack/yew/pull/2772)] + - Pinned Channels. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2811](https://github.com/yewstack/yew/pull/2811)] + - Bind to properties instead of attributes by default. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2819](https://github.com/yewstack/yew/pull/2819)] + - Convert nightly from a feature flag to a compiler flag. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2827](https://github.com/yewstack/yew/pull/2827)] + - Reduce SSR Buffers in VList. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2826](https://github.com/yewstack/yew/pull/2826)] + - Allow keywords after dash in element and attribute names. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2820](https://github.com/yewstack/yew/pull/2820)] + - Replace custom logging by tracing. [[@WorldSEnder](https://github.com/WorldSEnder), [#2814](https://github.com/yewstack/yew/pull/2814)] + - Implement sleep and interval for Yew Platform. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2784](https://github.com/yewstack/yew/pull/2784)] + - Remove component NodeRef. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2783](https://github.com/yewstack/yew/pull/2783)] + - Prepared States dependency should be Reference Counted. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2769](https://github.com/yewstack/yew/pull/2769)] + - Document features automatically.. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2780](https://github.com/yewstack/yew/pull/2780)] + - Streamed SSR Response. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2697](https://github.com/yewstack/yew/pull/2697)] + - Nightly features. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2743](https://github.com/yewstack/yew/pull/2743)] + - Allow VNode props to be converted to Children.. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2749](https://github.com/yewstack/yew/pull/2749)] + - Redo derive(Properties), take 2. [[@WorldSEnder](https://github.com/WorldSEnder), [#2729](https://github.com/yewstack/yew/pull/2729)] + - `Callback::reform()` should return `Callback`. [[@orzogc](https://github.com/orzogc), [#2719](https://github.com/yewstack/yew/pull/2719)] + - Span hygiene and editor UX. [[@WorldSEnder](https://github.com/WorldSEnder), [#2702](https://github.com/yewstack/yew/pull/2702)] + - Block props update during hydration. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2665](https://github.com/yewstack/yew/pull/2665)] + - Point to `callback_future` in `callback` docs. [[@Shadlock0133](https://github.com/Shadlock0133), [#2674](https://github.com/yewstack/yew/pull/2674)] + - Change access to VList children to a wrapper. [[@WorldSEnder](https://github.com/WorldSEnder), [#2673](https://github.com/yewstack/yew/pull/2673)] + - Partially undo #2673, different approach for the DerefMut impl of VList. [[@WorldSEnder](https://github.com/WorldSEnder), [#2692](https://github.com/yewstack/yew/pull/2692)] + - Rework a bunch of cfg(feature) flags to be more principled. [[@WorldSEnder](https://github.com/WorldSEnder), [#2666](https://github.com/yewstack/yew/pull/2666)] + - Delay Hydration second render until all assistive nodes have been removed. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2629](https://github.com/yewstack/yew/pull/2629)] + - Allow to consume deps in use_callback. [[@Jet Li](https://github.com/Jet Li), [#2617](https://github.com/yewstack/yew/pull/2617)] + - Add `use_future` hook to make consuming futures as suspense easier. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2609](https://github.com/yewstack/yew/pull/2609)] + - Add the ability to use non-literal string as attribute names. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2593](https://github.com/yewstack/yew/pull/2593)] + - Introduce a dedicated use_force_update hook. [[@WorldSEnder](https://github.com/WorldSEnder), [#2586](https://github.com/yewstack/yew/pull/2586)] + - Impl ImplicitClone for Rc where T: Sized. [[@Nano](https://github.com/Nano), [#2594](https://github.com/yewstack/yew/pull/2594)] + - SSR Hydration. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2552](https://github.com/yewstack/yew/pull/2552)] + - Add use_callback hook. [[@Jet Li](https://github.com/Jet Li), [#2566](https://github.com/yewstack/yew/pull/2566)] + - Introduce additional information in SSR artifact to facilitate Hydration. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2540](https://github.com/yewstack/yew/pull/2540)] + - Scoped event handlers. [[@WorldSEnder](https://github.com/WorldSEnder), [#2510](https://github.com/yewstack/yew/pull/2510)] + - An ever Increasing Component ID. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2537](https://github.com/yewstack/yew/pull/2537)] + - Prevents Fallback UI from becoming suspended. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2532](https://github.com/yewstack/yew/pull/2532)] + - `#[cfg(feature = "render")]` and `yew::Renderer`. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2498](https://github.com/yewstack/yew/pull/2498)] + - Introduce explicit internal datastructures modeling dom state. [[@WorldSEnder](https://github.com/WorldSEnder), [#2330](https://github.com/yewstack/yew/pull/2330)] + - Improve AnyScope API. [[@Aaron Erhardt](https://github.com/Aaron Erhardt), [#2445](https://github.com/yewstack/yew/pull/2445)] + - Automatic Message Batching. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2421](https://github.com/yewstack/yew/pull/2421)] + - Add Other variant to the ListenerKind. [[@Alexander Mescheryakov](https://github.com/Alexander Mescheryakov), [#2417](https://github.com/yewstack/yew/pull/2417)] + - Function Components & Hooks V2. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2401](https://github.com/yewstack/yew/pull/2401)] + - Add ContextHandle in yew::prelude. [[@Anuvrat Singh](https://github.com/Anuvrat Singh), [#2372](https://github.com/yewstack/yew/pull/2372)] + - Separate scheduler rendered call from create and render. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2374](https://github.com/yewstack/yew/pull/2374)] + - Update to edition 2021. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2354](https://github.com/yewstack/yew/pull/2354)] + - Server-side Rendering (without hydration). [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2335](https://github.com/yewstack/yew/pull/2335)] + - Make BaseComponent Sealed.. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2359](https://github.com/yewstack/yew/pull/2359)] + - Remove start_app_as_body.. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2346](https://github.com/yewstack/yew/pull/2346)] + - Bump minimal supported rust version (MSRV) to 1.56. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2334](https://github.com/yewstack/yew/pull/2334)] + - Suspense Support. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2212](https://github.com/yewstack/yew/pull/2212)] + - make layout testing code public. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2310](https://github.com/yewstack/yew/pull/2310)] + - Refactor and simplify `Callback`. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2301](https://github.com/yewstack/yew/pull/2301)] + - Add pending event listener on the VTag. [[@Alexander Mescheryakov](https://github.com/Alexander Mescheryakov), [#2300](https://github.com/yewstack/yew/pull/2300)] + - constify VList::new. [[@Alexander Mescheryakov](https://github.com/Alexander Mescheryakov), [#2293](https://github.com/yewstack/yew/pull/2293)] + - Allow `function_component` creation based on function name. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2292](https://github.com/yewstack/yew/pull/2292)] + - Implement IntoPropValue for Rc. [[@Zachary Stewart](https://github.com/Zachary Stewart), [#2285](https://github.com/yewstack/yew/pull/2285)] + - Raw field names in property structs. [[@WorldSEnder](https://github.com/WorldSEnder), [#2273](https://github.com/yewstack/yew/pull/2273)] + +## ✨ yew-router **0.17.0** *(2022-11-xx)* + +#### Changelog + +- #### 🛠 Fixes + + - Fix basename handling in router. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2297](https://github.com/yewstack/yew/pull/2297)] + +- #### ⚡️ Features + + - Simple `NodeRef` passing to `` for yew-router. [[@Athan Clark](https://github.com/Athan Clark), [#2877](https://github.com/yewstack/yew/pull/2877)] + - Make Switch to accept a closure as render function directly. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2659](https://github.com/yewstack/yew/pull/2659)] + - `#[cfg(feature = "render")]` and `yew::Renderer`. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2498](https://github.com/yewstack/yew/pull/2498)] + - Includes query parameters in rendered Link component. [[@Yuki Kodama](https://github.com/Yuki Kodama), [#2464](https://github.com/yewstack/yew/pull/2464)] + - Update to edition 2021. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2354](https://github.com/yewstack/yew/pull/2354)] + - Support named wildcards when deriving Routable.. [[@Jonathan Bailey](https://github.com/Jonathan Bailey), [#2345](https://github.com/yewstack/yew/pull/2345)] + - Add HashRouter, basename and use gloo-history. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2239](https://github.com/yewstack/yew/pull/2239)] + + +## ✨ yew-agent **0.2.0** *(2022-11-xx)* + +#### Changelog + +- #### ⚡️ Features + + - add `use_bridge` docs. [[@Shrey Sudhir](https://github.com/Shrey Sudhir), [#2722](https://github.com/yewstack/yew/pull/2722)] + - Update to edition 2021. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2354](https://github.com/yewstack/yew/pull/2354)] + - Move yew-agent to gloo. [[@Muhammad Hamza](https://github.com/Muhammad Hamza), [#2326](https://github.com/yewstack/yew/pull/2326)] + - Implement PrivateAgent. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2318](https://github.com/yewstack/yew/pull/2318)] + - Remove context & job agent. [[@Kaede Hoshikawa](https://github.com/Kaede Hoshikawa), [#2295](https://github.com/yewstack/yew/pull/2295)] + ## ✨ yew **0.19.0** *(2021-11-26)* #### Changelog diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 94fe29828bb..d7c4ebaf738 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,12 +27,12 @@ The most important tasks are outlined below. To run all tests, use the following command: ```bash -cargo make tests +cargo make test-flow ``` ### Browser tests -`cargo make tests` will automatically download Geckodriver to a temporary location if it isn't in the PATH. +`cargo make test` will automatically download Geckodriver to a temporary location if it isn't in the PATH. Because Geckodriver looks for `firefox` in the path, if you use FireFox Developer Edition, you may get an error, because Developer Editions @@ -44,18 +44,10 @@ To fix this, either install the standard version of Firefox or symlink The tests for the fetch service require a local [httpbin](https://httpbin.org/) server. If you have [Docker](https://www.docker.com/) installed, -`cargo make tests` will automatically run httpbin in a container for you. +`cargo make test` will automatically run httpbin in a container for you. Alternatively, you can set the `HTTPBIN_URL` environment variable to the URL you wish to run tests against. -### WebSocket service tests - -The tests for the web-socket service require an echo server. -If you have [Docker](https://www.docker.com/) installed, -`cargo make tests` will automatically run an [echo server](https://hub.docker.com/r/jmalloc/echo-server) in a container for you. - -Alternatively, you can set the `ECHO_SERVER_URL` environment variable to the URL you wish to run tests against. - ### Macro tests When adding or updating tests, please make sure to update the appropriate `stderr` file, which you can find [here](https://github.com/yewstack/yew/tree/master/packages/yew-macro/tests/macro) for the `html!` macro. @@ -77,22 +69,10 @@ To automatically fix formatting issues, run `cargo +nightly fmt` first. ## Benchmarks -If you wish to improve the performance of Yew, we ask you to prove the improvements of your changes through benchmarking. - -Some components of Yew have dedicated benchmarks which can be run with the following command: - -```bash -cargo make benchmarks -``` - -There's also a benchmark for the framework as a whole. +js-framework-benchmark is used as a benchmark for the framework as a whole. Simply clone [bakape/js-framework-benchmark](https://github.com/bakape/js-framework-benchmark) and follow the repository's README. -Feel free to add new benchmark tests if the current benchmark coverage is insufficient! - -> See #1453 for a discussion on how to make this easier. - ## Writing APIs When building new APIs, think about what it would be like to use them. Would this API cause confusing and hard to pin error mesages? Would this API integrate well with other APIs? Is it intuitive to use this API? diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000000..e4f7e4d4c0f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3583 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "async_clock" +version = "0.0.1" +dependencies = [ + "chrono", + "futures 0.3.28", + "gloo-net", + "yew", +] + +[[package]] +name = "atomic-polyfill" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "average" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843ec791d3f24503bbf72bbd5e49a3ab4dbb4bcd0a8ef6b0c908efa73caa27b1" +dependencies = [ + "easy-cast", + "float-ord", + "num-traits", +] + +[[package]] +name = "axum" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "basic-toml" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0de75129aa8d0cceaf750b89013f0e08804d6ec61416da787b35ad0d7cddf1" +dependencies = [ + "serde", +] + +[[package]] +name = "benchmark-ssr" +version = "0.1.0" +dependencies = [ + "average", + "clap", + "function_router", + "indicatif", + "jemallocator", + "serde", + "serde_json", + "tabled", + "tokio", + "yew", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "boids" +version = "0.1.0" +dependencies = [ + "anyhow", + "getrandom", + "gloo", + "rand", + "serde", + "web-sys", + "yew", +] + +[[package]] +name = "boolinator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "changelog" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "git2", + "once_cell", + "regex", + "reqwest", + "semver", + "serde", + "strum", +] + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clap" +version = "4.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba77a07e4489fb41bd90e8d4201c3eb246b3c2c9ea2ba0bddd6c1d1df87db7d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9b4a88bb4bc35d3d6f65a21b0f0bafe9c894fa00978de242c555ec28bea1c0" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "communication_child_to_parent" +version = "0.0.1" +dependencies = [ + "yew", +] + +[[package]] +name = "communication_grandchild_with_grandparent" +version = "0.0.1" +dependencies = [ + "yew", +] + +[[package]] +name = "communication_grandparent_to_grandchild" +version = "0.0.1" +dependencies = [ + "yew", +] + +[[package]] +name = "communication_parent_to_child" +version = "0.0.1" +dependencies = [ + "yew", +] + +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.45.0", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "contexts" +version = "0.1.0" +dependencies = [ + "serde", + "yew", + "yew-agent", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "counter" +version = "0.1.1" +dependencies = [ + "gloo", + "js-sys", + "wasm-bindgen", + "yew", +] + +[[package]] +name = "counter_functional" +version = "0.1.0" +dependencies = [ + "yew", +] + +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6548a0ad5d2549e111e1f6a11a6c2e2d00ce6a3dafe22948d67c2b443f775e52" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dyn_create_destroy_apps" +version = "0.1.0" +dependencies = [ + "gloo", + "js-sys", + "slab", + "wasm-bindgen", + "web-sys", + "yew", +] + +[[package]] +name = "easy-cast" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd102ee8c418348759919b83b81cdbdc933ffe29740b903df448b4bafaa348e" +dependencies = [ + "libm", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fake" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a44c765350db469b774425ff1c833890b16ceb9612fb5d7c4bbdf4a1b55f876" +dependencies = [ + "rand", + "unidecode", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "file_upload" +version = "0.1.0" +dependencies = [ + "base64 0.21.2", + "gloo", + "js-sys", + "web-sys", + "yew", +] + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "function_memory_game" +version = "0.1.0" +dependencies = [ + "getrandom", + "gloo", + "nanoid", + "rand", + "serde", + "strum", + "strum_macros", + "web-sys", + "yew", +] + +[[package]] +name = "function_router" +version = "0.1.0" +dependencies = [ + "getrandom", + "gloo", + "instant", + "lipsum", + "log", + "once_cell", + "rand", + "serde", + "wasm-logger", + "yew", + "yew-router", +] + +[[package]] +name = "function_todomvc" +version = "0.1.0" +dependencies = [ + "gloo", + "serde", + "strum", + "strum_macros", + "web-sys", + "yew", +] + +[[package]] +name = "futures" +version = "0.1.0" +dependencies = [ + "gloo", + "pulldown-cmark", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "game_of_life" +version = "0.1.4" +dependencies = [ + "getrandom", + "gloo", + "log", + "rand", + "wasm-logger", + "yew", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "git2" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gloo" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net", + "gloo-render", + "gloo-storage", + "gloo-timers", + "gloo-utils", + "gloo-worker", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "futures-channel", + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfd137a4b629e72b8c949ec56c71ea9bd5491cc66358a0a7787e94875feec71" +dependencies = [ + "gloo-events", + "gloo-utils", + "serde", + "serde-wasm-bindgen", + "serde_urlencoded", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3000ef231a67d5bfee6b35f2c0f6f5c8d45b3381ef5bbbea603690ec4e539762" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode", + "gloo-console", + "gloo-utils", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heapless" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "immutable" +version = "0.1.0" +dependencies = [ + "implicit-clone", + "wasm-bindgen", + "web-sys", + "yew", +] + +[[package]] +name = "implicit-clone" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73c84c395327945e71c6604eff15061c67849b9081318b4334b719bb2c11415f" +dependencies = [ + "indexmap 2.0.0", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "indicatif" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff8cc23a7393a397ed1d7f56e6365cba772aba9f9912ab968b03043c395d057" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "inner_html" +version = "0.1.0" +dependencies = [ + "gloo", + "web-sys", + "yew", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jemalloc-sys" +version = "0.5.3+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9bd5d616ea7ed58b571b2e209a65759664d7fb021a0819d7a790afc67e47ca1" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "jemallocator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c2514137880c52b0b4822b563fadd38257c1f380858addb74a400889696ea6" +dependencies = [ + "jemalloc-sys", + "libc", +] + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-framework-benchmark-yew" +version = "1.0.0" +dependencies = [ + "getrandom", + "rand", + "wasm-bindgen", + "web-sys", + "yew", +] + +[[package]] +name = "js-framework-benchmark-yew-hooks" +version = "1.0.0" +dependencies = [ + "getrandom", + "rand", + "wasm-bindgen", + "web-sys", + "yew", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "js_callback" +version = "0.1.0" +dependencies = [ + "js-sys", + "once_cell", + "wasm-bindgen", + "wasm-bindgen-futures", + "yew", +] + +[[package]] +name = "keyed_list" +version = "0.1.0" +dependencies = [ + "fake", + "getrandom", + "instant", + "log", + "rand", + "wasm-logger", + "web-sys", + "yew", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libgit2-sys" +version = "0.15.2+1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a80df2e11fb4a61f4ba2ab42dbe7f74468da143f1a75c74e11dee7c813f694fa" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "lipsum" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5e9ef2d2ad6fe67a59ace27c203c8d3a71d195532ee82e3bbe0d5f9a9ca541" +dependencies = [ + "rand", + "rand_chacha", +] + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "mount_point" +version = "0.1.0" +dependencies = [ + "gloo", + "wasm-bindgen", + "web-sys", + "yew", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nested_list" +version = "0.1.0" +dependencies = [ + "log", + "wasm-logger", + "yew", +] + +[[package]] +name = "node_refs" +version = "0.1.0" +dependencies = [ + "web-sys", + "yew", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "object" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "papergrid" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae7891b22598926e4398790c8fe6447930c72a67d36d983a49d6ce682ce83290" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.1", +] + +[[package]] +name = "password_strength" +version = "0.1.0" +dependencies = [ + "chrono", + "js-sys", + "time 0.3.22", + "wasm-bindgen", + "web-sys", + "yew", + "zxcvbn", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pinned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures 0.3.28", + "rustversion", + "thiserror", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "portable-atomic" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" + +[[package]] +name = "portals" +version = "0.1.0" +dependencies = [ + "gloo", + "wasm-bindgen", + "web-sys", + "yew", +] + +[[package]] +name = "postcard" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb" +dependencies = [ + "cobs", + "heapless", + "serde", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "prettyplease" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +dependencies = [ + "proc-macro2", + "syn 2.0.27", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "process-benchmark-results" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", +] + +[[package]] +name = "prokio" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488" +dependencies = [ + "futures 0.3.28", + "gloo", + "num_cpus", + "once_cell", + "pin-project", + "pinned", + "tokio", + "tokio-stream", + "wasm-bindgen-futures", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "router" +version = "0.1.0" +dependencies = [ + "getrandom", + "gloo", + "instant", + "lipsum", + "log", + "once_cell", + "rand", + "serde", + "wasm-logger", + "yew", + "yew-router", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "serde" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d614f89548720367ded108b3c843be93f3a341e22d5674ca0dd5cd57f34926af" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fe589678c688e44177da4f27152ee2d190757271dc7f1d5b6b9f68d869d641" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "serde_json" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simple_ssr" +version = "0.1.0" +dependencies = [ + "bytes", + "clap", + "futures 0.3.28", + "log", + "reqwest", + "serde", + "tokio", + "uuid", + "warp", + "wasm-bindgen-futures", + "wasm-logger", + "yew", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "ssr_router" +version = "0.1.0" +dependencies = [ + "axum", + "clap", + "env_logger", + "function_router", + "futures 0.3.28", + "hyper", + "jemallocator", + "log", + "tokio", + "tower", + "tower-http", + "wasm-bindgen-futures", + "wasm-logger", + "yew", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9f3bd7d2e45dcc5e265fbb88d6513e4747d8ef9444cf01a533119bce28a157" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.27", +] + +[[package]] +name = "suspense" +version = "0.1.0" +dependencies = [ + "gloo", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "tabled" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce69a5028cd9576063ec1f48edb2c75339fd835e6094ef3e05b3a079bf594a6" +dependencies = [ + "papergrid", + "tabled_derive", + "unicode-width", +] + +[[package]] +name = "tabled_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "timer" +version = "0.1.0" +dependencies = [ + "gloo", + "js-sys", + "wasm-bindgen", + "yew", +] + +[[package]] +name = "timer_functional" +version = "0.1.0" +dependencies = [ + "gloo", + "js-sys", + "yew", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "todomvc" +version = "0.1.0" +dependencies = [ + "gloo", + "serde", + "serde_derive", + "strum", + "strum_macros", + "web-sys", + "yew", +] + +[[package]] +name = "tokio" +version = "1.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374442f06ee49c3a28a8fc9f01a2596fed7559c6b99b31279c3261778e77d84f" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "trybuild" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501dbdbb99861e4ab6b60eb6a7493956a9defb644fd034bc4a5ef27c693c8a3a" +dependencies = [ + "basic-toml", + "glob", + "once_cell", + "serde", + "serde_derive", + "serde_json", + "termcolor", +] + +[[package]] +name = "tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "two_apps" +version = "0.1.0" +dependencies = [ + "gloo", + "yew", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unidecode" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" +dependencies = [ + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba431ef570df1287f7f8b07e376491ad54f84d26ac473489427231e1718e1f69" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "multer", + "percent-encoding", + "pin-project", + "rustls-pemfile", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.27", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6e302a7ea94f83a6d09e78e7dc7d9ca7b186bc2829c24a22d0753efd680671" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecb993dd8c836930ed130e020e77d9b2e65dd0fbab1b67c790b0f5d80b11a575" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "wasm-logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webgl" +version = "0.1.0" +dependencies = [ + "js-sys", + "wasm-bindgen", + "web-sys", + "yew", +] + +[[package]] +name = "weblog" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eec0958e0181982bc8b4577b1363f21300a670603615c58643f172c92c47357" +dependencies = [ + "wasm-bindgen", + "web-sys", + "weblog-proc-macro", +] + +[[package]] +name = "weblog-proc-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9abb8ee84ede5408a346721d72fb216a27f53a539ff3c83ed1bf7625af7104" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "website-test" +version = "0.1.0" +dependencies = [ + "boolinator", + "derive_more", + "glob", + "gloo", + "js-sys", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "weblog", + "yew", + "yew-agent", + "yew-router", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "yew" +version = "0.20.0" +dependencies = [ + "base64ct", + "bincode", + "console_error_panic_hook", + "futures 0.3.28", + "gloo", + "html-escape", + "implicit-clone", + "indexmap 2.0.0", + "js-sys", + "prokio", + "rustversion", + "serde", + "slab", + "thiserror", + "tokio", + "tracing", + "trybuild", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", + "yew-macro", +] + +[[package]] +name = "yew-agent" +version = "0.2.0" +dependencies = [ + "futures 0.3.28", + "gloo-worker", + "pin-project", + "serde", + "wasm-bindgen", + "yew", + "yew-agent-macro", +] + +[[package]] +name = "yew-agent-macro" +version = "0.1.0" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", + "trybuild", + "yew-agent", +] + +[[package]] +name = "yew-macro" +version = "0.20.0" +dependencies = [ + "boolinator", + "once_cell", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.27", + "trybuild", + "yew", +] + +[[package]] +name = "yew-router" +version = "0.17.0" +dependencies = [ + "gloo", + "js-sys", + "route-recognizer", + "serde", + "serde_urlencoded", + "tracing", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", + "yew", + "yew-router-macro", +] + +[[package]] +name = "yew-router-macro" +version = "0.17.0" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.27", + "trybuild", + "yew-router", +] + +[[package]] +name = "yew-worker-fib" +version = "0.1.0" +dependencies = [ + "js-sys", + "postcard", + "serde", + "wasm-bindgen", + "web-sys", + "yew", + "yew-agent", +] + +[[package]] +name = "zxcvbn" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "103fa851fff70ea29af380e87c25c48ff7faac5c530c70bd0e65366d4e0c94e4" +dependencies = [ + "derive_builder", + "fancy-regex", + "itertools", + "js-sys", + "lazy_static", + "quick-error", + "regex", + "time 0.3.22", +] diff --git a/Cargo.toml b/Cargo.toml index cddeb6d7861..9205d8e3dcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,54 +1,20 @@ [workspace] members = [ - # Packages - "packages/yew", - "packages/yew-macro", - "packages/yew-agent", - "packages/yew-agent-macro", - "packages/yew-router", - "packages/yew-router-macro", - - # Examples - "examples/boids", - "examples/contexts", - "examples/counter", - "examples/counter_functional", - "examples/dyn_create_destroy_apps", - "examples/file_upload", - "examples/function_memory_game", - "examples/function_router", - "examples/function_todomvc", - "examples/futures", - "examples/game_of_life", - "examples/inner_html", - "examples/js_callback", - "examples/keyed_list", - "examples/mount_point", - "examples/nested_list", - "examples/node_refs", - "examples/password_strength", - "examples/portals", - "examples/router", - "examples/simple_ssr", - "examples/timer", - "examples/todomvc", - "examples/two_apps", - "examples/webgl", - "examples/web_worker_fib", - "examples/ssr_router", - "examples/suspense", - "examples/immutable", - - # Tools - "tools/benchmark-struct", - "tools/benchmark-hooks", - "tools/benchmark-ssr", - "tools/changelog", - "tools/process-benchmark-results", - "tools/website-test", + "packages/*", + "tools/*", + "examples/*", +] +default-members = [ + "packages/*", ] resolver = "2" +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +opt-level = "z" + [profile.bench] lto = true codegen-units = 1 diff --git a/README.md b/README.md index 13d11553030..74653adafed 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- +

Yew

@@ -20,7 +20,7 @@ | Documentation (latest) | - Examples + Examples | Changelog | @@ -46,7 +46,7 @@ ## Contributing -Yew is a community effort and we welcome all kinds of contributions, big or small, from developers of all backgrounds. We want the Yew community to be a fun and friendly place, so please review our [Code of Conduct](CODE_OF_CONDUCT.md) to learn what behavior will not be tolerated. +Yew is a community effort and we welcome all kinds of contributions, big or small, from developers of all backgrounds. We want the Yew community to be a fun and friendly place, so please review our [Code of Conduct](https://github.com/yewstack/yew/blob/master/CODE_OF_CONDUCT.md) to learn what behavior will not be tolerated. #### 🤠 New to Yew? @@ -62,7 +62,7 @@ Feel free to drop into our [Discord chatroom](https://discord.gg/VQck8X4) or ope #### 🙂 Ready to dive into the code? -After reviewing the [Contribution Guide](CONTRIBUTING.md), check out the ["Good First Issues"](https://github.com/yewstack/yew/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) (they are eager for attention!). Once you find one that interests you, feel free to assign yourself to an issue and don't hesitate to reach out for guidance, the issues vary in complexity. +After reviewing the [Contribution Guide](https://github.com/yewstack/yew/blob/master/CONTRIBUTING.md), check out the ["Good First Issues"](https://github.com/yewstack/yew/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) (they are eager for attention!). Once you find one that interests you, feel free to assign yourself to an issue and don't hesitate to reach out for guidance, the issues vary in complexity. #### 😱 Found a bug? diff --git a/SECURITY.md b/SECURITY.md index 16231a59967..ae43ac043ec 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,11 +2,23 @@ ## Reporting a Vulnerability -To report a security vulnerability, please email the maintainers at `maintainers@yew.rs`. Please do not create a Github issue -for security vulnerabilities. - -If you can, please include the following details: -* An MCVE (minimum complete verifiable example) – this is a short code snippet which demonstrates the error in the -the simplest possible (or just a simple) way. -* Which versions of Yew the vulnerability is present in -* What effects the vulnerability has and how serious the vulnerability is +Please do not create a GitHub issue for security vulnerabilities, Instead do the following: + +1. Click on the **Security** tab and then click on the **"Report a Vulnerability"** button. + +![Report a Vulnerability](https://github.com/shubhsharma19/yew/assets/69891912/810b0297-65c0-42e1-9935-08f026387bf7) + +2. After that give a **title** and a **description**. + +![Title and Description](https://github.com/shubhsharma19/yew/assets/69891912/3686459b-c7b4-49ea-92cf-5313c4ccd756) + + +When reporting the vulnerability, please provide the following information, if possible: + +- **MCVE (Minimum Complete Verifiable Example)**: Please include a concise code snippet that demonstrates the error in a simplified manner. + +- **Versions of Yew**: Specify the versions of Yew in which the vulnerability is present. This helps us narrow down the scope of the issue and assess its impact accurately. + +- **Impact and Severity**: Describe the effects of the vulnerability and its seriousness. Provide details about any potential risks, security breaches, or the impact it may have on the system. + +> For contacting the maintainers, you can reach out to them via email at maintainers@yew.rs diff --git a/api-docs/before-content.html b/api-docs/before-content.html index acd9b0294ac..620e1d46150 100644 --- a/api-docs/before-content.html +++ b/api-docs/before-content.html @@ -1,4 +1,4 @@
-

This is unreleased documentation for Yew Next version.

-

For up-to-date documentation, see the latest version on docs.rs.

+
This is unreleased documentation for Yew Next version.
+
For up-to-date documentation, see the latest version on docs.rs.
diff --git a/api-docs/styles.css b/api-docs/styles.css index 9a6d18c087c..6df5e9397ea 100644 --- a/api-docs/styles.css +++ b/api-docs/styles.css @@ -1,13 +1,23 @@ #unreleased-version-header { - background-color: rgb(77, 56, 0); + background-color: rgb(200, 237, 248); z-index: 400; - position: fixed; + position: absolute; left: 0; top: 0; right: 0; height: 70px; padding-top: 10px; + padding-bottom: 10px; text-align: center; + font-family: sans-serif; + box-shadow: 0 0 5px 0 rgb(100, 100, 100); +} + +@media (prefers-color-scheme: dark) { + #unreleased-version-header { + background-color: rgb(32, 43, 57); + box-shadow: 0 0 5px 0 black; + } } body { diff --git a/ci/build-examples.sh b/ci/build-examples.sh index 072d88ffeca..2bc3f56b184 100755 --- a/ci/build-examples.sh +++ b/ci/build-examples.sh @@ -24,12 +24,13 @@ for path in examples/*; do # shellcheck disable=SC2164 cd "$path" dist_dir="$output/$example" + export RUSTFLAGS="--cfg nightly_yew" if [[ "$example" == "boids" || "$example" == "password_strength" ]]; then # works around issue rust-lang/rust#96486 # where the compiler forgets to link some symbols connected to const_eval # only an issue on nightly and with build-std enabled which we do for code size # this deoptimizes only the examples that otherwise fail to build - export RUSTFLAGS="-Zshare-generics=n -Clto=thin" + export RUSTFLAGS="-Zshare-generics=n -Clto=thin $RUSTFLAGS" fi trunk build --release --dist "$dist_dir" --public-url "$PUBLIC_URL_PREFIX$example" diff --git a/ci/collect_sizes.py b/ci/collect_sizes.py index 71688b5b9a6..2f980ffc78a 100644 --- a/ci/collect_sizes.py +++ b/ci/collect_sizes.py @@ -32,15 +32,10 @@ def find_example_sizes(parent_dir: Path) -> Dict[str, int]: def main() -> None: - master_sizes = find_example_sizes(Path("yew-master")) - pr_sizes = find_example_sizes(Path("current-pr")) - - example_names = sorted(set([*master_sizes.keys(), *pr_sizes.keys()])) - - joined_sizes = [(i, [master_sizes.get(i), pr_sizes.get(i)]) for i in example_names] + sizes = find_example_sizes(Path.cwd()) size_cmp_info = { - "sizes": joined_sizes, + "sizes": sizes, "issue_number": os.environ["ISSUE_NUMBER"], } diff --git a/ci/feature-soundness-release.sh b/ci/feature-soundness-release.sh index 6de18c3ab23..0fd501fba3c 100755 --- a/ci/feature-soundness-release.sh +++ b/ci/feature-soundness-release.sh @@ -18,15 +18,3 @@ cargo clippy --release --no-default-features --features default,ssr -- --deny=wa cargo clippy --release --no-default-features --features csr,default,ssr -- --deny=warnings cargo clippy --release --no-default-features --features hydration,ssr -- --deny=warnings cargo clippy --release --no-default-features --features default,hydration,ssr -- --deny=warnings -cargo clippy --release --no-default-features --features tokio -- --deny=warnings -cargo clippy --release --no-default-features --features csr,tokio -- --deny=warnings -cargo clippy --release --no-default-features --features default,tokio -- --deny=warnings -cargo clippy --release --no-default-features --features csr,default,tokio -- --deny=warnings -cargo clippy --release --no-default-features --features hydration,tokio -- --deny=warnings -cargo clippy --release --no-default-features --features default,hydration,tokio -- --deny=warnings -cargo clippy --release --no-default-features --features ssr,tokio -- --deny=warnings -cargo clippy --release --no-default-features --features csr,ssr,tokio -- --deny=warnings -cargo clippy --release --no-default-features --features default,ssr,tokio -- --deny=warnings -cargo clippy --release --no-default-features --features csr,default,ssr,tokio -- --deny=warnings -cargo clippy --release --no-default-features --features hydration,ssr,tokio -- --deny=warnings -cargo clippy --release --no-default-features --features default,hydration,ssr,tokio -- --deny=warnings diff --git a/ci/feature-soundness.sh b/ci/feature-soundness.sh index 5686b2fa392..b23b2592f73 100755 --- a/ci/feature-soundness.sh +++ b/ci/feature-soundness.sh @@ -18,15 +18,3 @@ cargo clippy --no-default-features --features default,ssr -- --deny=warnings cargo clippy --no-default-features --features csr,default,ssr -- --deny=warnings cargo clippy --no-default-features --features hydration,ssr -- --deny=warnings cargo clippy --no-default-features --features default,hydration,ssr -- --deny=warnings -cargo clippy --no-default-features --features tokio -- --deny=warnings -cargo clippy --no-default-features --features csr,tokio -- --deny=warnings -cargo clippy --no-default-features --features default,tokio -- --deny=warnings -cargo clippy --no-default-features --features csr,default,tokio -- --deny=warnings -cargo clippy --no-default-features --features hydration,tokio -- --deny=warnings -cargo clippy --no-default-features --features default,hydration,tokio -- --deny=warnings -cargo clippy --no-default-features --features ssr,tokio -- --deny=warnings -cargo clippy --no-default-features --features csr,ssr,tokio -- --deny=warnings -cargo clippy --no-default-features --features default,ssr,tokio -- --deny=warnings -cargo clippy --no-default-features --features csr,default,ssr,tokio -- --deny=warnings -cargo clippy --no-default-features --features hydration,ssr,tokio -- --deny=warnings -cargo clippy --no-default-features --features default,hydration,ssr,tokio -- --deny=warnings diff --git a/ci/install-wasm-bindgen-cli.sh b/ci/install-wasm-bindgen-cli.sh new file mode 100755 index 00000000000..e592a9c83b2 --- /dev/null +++ b/ci/install-wasm-bindgen-cli.sh @@ -0,0 +1,12 @@ +if [ ! -f "Cargo.lock" ]; then + cargo fetch +fi + +VERSION=$(cargo pkgid --frozen wasm-bindgen | cut -d ":" -f 3) + +# Cargo decided to change syntax after 1.61 +if [ "$VERSION" = "" ]; then + VERSION=$(cargo pkgid --frozen wasm-bindgen | cut -d "@" -f 2) +fi + +cargo +stable install --version $VERSION wasm-bindgen-cli diff --git a/ci/make_benchmark_ssr_cmt.py b/ci/make_benchmark_ssr_cmt.py index c2f31939a1d..14f04340a5e 100644 --- a/ci/make_benchmark_ssr_cmt.py +++ b/ci/make_benchmark_ssr_cmt.py @@ -16,18 +16,19 @@ def write_benchmark(lines: List[str], content: List[Dict[str, str]]) -> None: for i in content: lines.append( - "| {i.name} | {i.round} | {i.min} | {i.max} | {i.mean} | {i.std_dev} |" + f"| {i['name']} | {i['round']} | {i['min']} | {i['max']} | {i['mean']} | {i['std_dev']} |" ) lines.append("") lines.append("") + lines.append("") def main() -> None: - with open("benchmark-ssr/yew-master/output.json") as f: + with open("benchmark-ssr/yew-master/tools/output.json") as f: master_content = json.loads(f.read()) - with open("benchmark-ssr/current-pr/output.json") as f: + with open("benchmark-ssr/current-pr/tools/output.json") as f: pr_content = json.loads(f.read()) lines: List[str] = [] @@ -49,7 +50,6 @@ def main() -> None: with open(os.environ["GITHUB_ENV"], "a+") as f: f.write(f"YEW_BENCH_SSR={json.dumps(output)}\n") - f.write(f"PR_NUMBER={issue_number}\n") if __name__ == "__main__": diff --git a/ci/make_example_size_cmt.py b/ci/make_example_size_cmt.py index 7be01aed28f..8171128e366 100644 --- a/ci/make_example_size_cmt.py +++ b/ci/make_example_size_cmt.py @@ -35,11 +35,22 @@ def format_diff_size( def main() -> None: - with open("size-cmp-info/.SIZE_CMP_INFO") as f: - content = json.loads(f.read()) + with open("size-cmp-pr-info/.SIZE_CMP_INFO") as f: + pr_content = json.loads(f.read()) - joined_sizes = content["sizes"] - issue_number = content["issue_number"] + with open("size-cmp-master-info/.SIZE_CMP_INFO") as f: + master_content = json.loads(f.read()) + + master_sizes: dict[str, int] = master_content["sizes"] + pr_sizes: dict[str, int] = pr_content["sizes"] + + example_names = sorted(set([*master_sizes.keys(), *pr_sizes.keys()])) + joined_sizes = [(i, [master_sizes.get(i), pr_sizes.get(i)]) for i in example_names] + + assert pr_content["issue_number"] == master_content["issue_number"], \ + "Issue number differs between master and pr?" + + issue_number = pr_content["issue_number"] lines: List[str] = [] significant_lines: List[str] = [] diff --git a/ci/write-optimisation-flags.sh b/ci/write-optimisation-flags.sh deleted file mode 100755 index 678180bbe87..00000000000 --- a/ci/write-optimisation-flags.sh +++ /dev/null @@ -1,15 +0,0 @@ -# Must be run from root of the repo: -# yew $ ./ci/write-optimisation-flags.sh - -# this goes in [unstable] section -cat >> .cargo/config.toml << EOF -build-std = ["std", "panic_abort"] -build-std-features = ["panic_immediate_abort"] -EOF -cat >> Cargo.toml << EOF -[profile.release] -lto = true -codegen-units = 1 -panic = "abort" -opt-level = "z" -EOF diff --git a/examples/README.md b/examples/README.md index 552177fac25..e9735ebd199 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # Yew Examples -## How to run +## How to Run The examples are built with [trunk](https://github.com/thedodd/trunk). You can install it with the following command: @@ -17,53 +17,66 @@ Running an example is as easy as running a single command: cd examples/todomvc # build and serve the example -trunk serve --release +trunk serve --open ``` +Some examples may perform better using the `release` profile. If something is slow, you can try running it with the `--release` argument. + We're also publicly hosting the examples at `https://examples.yew.rs/`. As an example, check out the TodoMVC example here: -## List of examples - - - CT - Type of most components , "F" for function components and "S" for struct components, "SF" for a mix of both. - -| Example | CT | Description | -| -------------------------------------------------- | -- | ---------------------------------------------------------------------------------------------------------------------------------- | -| [boids](boids) | S | Yew port of [Boids](https://en.wikipedia.org/wiki/Boids) | -| [contexts](contexts) | F | A technical demonstration of Context API. | -| [counter](counter) | S | Simple counter which can be incremented and decremented | -| [counter_functional](counter_functional) | F | Simple counter which can be incremented and decremented made using function components | -| [dyn_create_destroy_apps](dyn_create_destroy_apps) | S | Uses the function `start_app_in_element` and the `AppHandle` struct to dynamically create and delete Yew apps | -| [file_upload](file_upload) | S | Uses the `gloo::file` to read the content of user uploaded files | -| [function_todomvc](function_todomvc) | F | Implementation of [TodoMVC](http://todomvc.com/) using function components and hooks. | -| [futures](futures) | S | Demonstrates how you can use futures and async code with Yew. Features a Markdown renderer. | -| [game_of_life](game_of_life) | S | Implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) | -| [immutable](immutable) | SF | Using immutable types in components. | -| [inner_html](inner_html) | S | Embeds an external document as raw HTML by manually managing the element | -| [js_callback](js_callback) | F | Interacts with JavaScript code | -| [keyed_list](keyed_list) | S | Demonstrates how to use keys to improve the performance of lists | -| [mount_point](mount_point) | S | Shows how to mount the root component to a custom element | -| [nested_list](nested_list) | S | Renders a styled list which tracks hover events | -| [node_refs](node_refs) | S | Uses a [`NodeRef`](https://yew.rs/docs/concepts/components/refs) to focus the input element under the cursor | -| [password_strength](password_strength) | SF | A password strength estimator implemented in Yew | -| [portals](portals) | S | Renders elements into out-of-tree nodes with the help of portals | -| [router](router) | S | The best yew blog built with `yew-router` | -| [simple_ssr](simple_ssr) | F | Demonstrates server-side rendering | -| [suspense](suspense) | F | This is an example that demonstrates `` support | -| [function_memory_game](function_memory_game) | F | Implementation of [Memory Game](https://github.com/bradlygreen/Memory-Game) | -| [timer](timer) | S | Demonstrates the use of the interval and timeout services | -| [todomvc](todomvc) | S | Implementation of [TodoMVC](http://todomvc.com/) | -| [two_apps](two_apps) | S | Runs two separate Yew apps which can communicate with each other | -| [web_worker_fib](web_worker_fib) | F | Calculate fibonacci value of a number in a web worker thread using `yew-agent` | -| [webgl](webgl) | S | Controls a [WebGL canvas](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Getting_started_with_WebGL) from Yew | - -## Next steps - -Have a look at Yew's [starter templates](https://yew.rs/docs/getting-started/starter-templates) when starting a project using Yew – they can significantly simplify things. - -## Help out - -If one of the examples catches your interest, look for the "improvements" section in its `README` file. +## List of Examples + +| Example | [CT] | Description | +| ----------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------- | +| [async_clock](async_clock) | [S] | Demonstrates the use of asynchronous tasks in a yew component. | +| [boids](boids) | [S] | Yew port of [Boids](https://en.wikipedia.org/wiki/Boids) | +| [communication_child_to_parent](communication_child_to_parent) | [S] | Communication from child to parent components. | +| [communication_grandchild_with_grandparent](communication_grandchild_with_grandparent) | [S] | Communication from grandchildren to grandparent components. | +| [communication_grandparent_to_grandchild](communication_grandparent_to_grandchild) | [S] | Communication from grandparent to grandchild components. | +| [communication_parent_to_child](communication_parent_to_child) | [S] | Communication from parent to child components. | +| [contexts](contexts) | [F] | A technical demonstration of the Context API. | +| [counter](counter) | [S] | Simple counter which can be incremented and decremented. | +| [counter_functional](counter_functional) | [F] | Simple counter which can be incremented and decremented made using function components. | +| [dyn_create_destroy_apps](dyn_create_destroy_apps) | [S] | Uses the function `Renderer::with_root_and_props` and the `AppHandle` struct to dynamically create and delete Yew apps. | +| [file_upload](file_upload) | [S] | Uses [`gloo::file`](https://docs.rs/gloo-file/latest/gloo_file/index.html) to read the content of user uploaded files. | +| [function_memory_game](function_memory_game) | [F] | Implementation of [Memory Game](https://github.com/bradlygreen/Memory-Game). | +| [function_router](function_router) | [F] | Identical to [`router`](router) but using function components. | +| [function_todomvc](function_todomvc) | [F] | Implementation of [TodoMVC](http://todomvc.com/) using function components and hooks. | +| [futures](futures) | [S] | Demonstrates how you can use futures and async code with Yew. Features a Markdown renderer. | +| [game_of_life](game_of_life) | [S] | Implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life). | +| [immutable](immutable) | [SF] | Using immutable types in components. | +| [inner_html](inner_html) | [S] | Embeds an external document as raw HTML by manually managing the element. | +| [js_callback](js_callback) | [F] | Interacts with JavaScript code. | +| [keyed_list](keyed_list) | [S] | Demonstrates how to use keys to improve the performance of lists. | +| [mount_point](mount_point) | [S] | Shows how to mount the root component to a custom element. | +| [nested_list](nested_list) | [S] | Renders a styled list which tracks hover events. | +| [node_refs](node_refs) | [S] | Uses a [`NodeRef`](https://yew.rs/docs/concepts/components/refs) to focus the input element under the cursor. | +| [password_strength](password_strength) | [SF] | A password strength estimator implemented in Yew. | +| [portals](portals) | [S] | Renders elements into out-of-tree nodes with the help of portals. | +| [router](router) | [S] | The best yew blog built with `yew-router`. | +| [simple_ssr](simple_ssr) | [F] | Demonstrates server-side rendering. | +| [ssr_router](ssr_router) | [F] | Demonstrates server-side rendering with routing. | +| [suspense](suspense) | [F] | This is an example that demonstrates `` support. | +| [timer](timer) | [S] | Demonstrates the use of the interval and timeout services. | +| [timer_functional](timer_functional) | [F] | Demonstrates the use of the interval and timeout services using function components | +| [todomvc](todomvc) | [S] | Implementation of [TodoMVC](http://todomvc.com/). | +| [two_apps](two_apps) | [S] | Runs two separate Yew apps which can communicate with each other. | +| [web_worker_fib](web_worker_fib) | [S] | Calculate Fibonacci numbers in a web worker thread using [`gloo-worker`](https://docs.rs/gloo-worker/latest/gloo_worker/). | +| [webgl](webgl) | [S] | Controls a [WebGL canvas](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Getting_started_with_WebGL) from Yew. | + +[CT]: ## "Component Type" +[S]: ## "Struct Components" +[F]: ## "Function Components" +[SF]: ## "Struct and Function Components" + +## Next Steps + +Have a look at Yew's [starter templates](https://yew.rs/docs/getting-started/build-a-sample-app#using-a-starter-template) when starting a project using Yew – they can significantly simplify things. + +## Helping Out + +If one of the examples catches your interest, look for the "improvements" section in its `README.md` file. Most examples list a few ideas for how to improve them. Consider starting with those but don't hesitate to improve an example in other ways either. diff --git a/examples/async_clock/Cargo.toml b/examples/async_clock/Cargo.toml new file mode 100644 index 00000000000..e2cd22750f3 --- /dev/null +++ b/examples/async_clock/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "async_clock" +version = "0.0.1" +authors = ["Marcel Ibes "] +edition = "2021" +license = "MIT OR Apache-2.0" + +[dependencies] +yew = { path = "../../packages/yew", features = ["csr"] } +chrono = "0.4" +futures = "0.3" +gloo-net = "0.3" diff --git a/examples/async_clock/README.md b/examples/async_clock/README.md new file mode 100644 index 00000000000..e4ade4014dd --- /dev/null +++ b/examples/async_clock/README.md @@ -0,0 +1,20 @@ +# Asynchronous coding in Yew + +An example of using asynchronous tasks in a component. This example creates a clock in the background and +continuously awaits clock-ticks. When the clock updates the new time is sent to the UI component to display. +In parallel it fetches online jokes to make the clock more entertaining to watch. + +Its main purpose is to demonstrate various ways of using async code in a yew component. It uses the following async +features: +- send_future +- send_stream +- spawn_local +- mpsc::unbounded channels + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/async_clock/index.html b/examples/async_clock/index.html new file mode 100644 index 00000000000..2b5515dae15 --- /dev/null +++ b/examples/async_clock/index.html @@ -0,0 +1,11 @@ + + + + + Yew • Async Examples + + + + + + diff --git a/examples/async_clock/index.scss b/examples/async_clock/index.scss new file mode 100644 index 00000000000..bb2b4f716a2 --- /dev/null +++ b/examples/async_clock/index.scss @@ -0,0 +1,42 @@ +body { + font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; + font-size: 16pt; +} + +.app { + display: flex; + justify-content: center; + flex-direction: row; +} + +.time-display { + display: flex; + justify-content: center; + color: darkblue; + font-size: 24pt; + font-weight: bold; + margin-bottom: 15px; +} + +.joke-display { + display: flex; + justify-content: center; + color: orangered; + max-width: 75%; + border-color: darkblue; + border-style: dashed; + border-width: 1px; + padding: 10px; +} + +.fun-score-display { + font-style: italic; +} + + +.clock { + display: flex; + flex-direction: column; + row-gap: 15px; + align-items: center; +} diff --git a/examples/async_clock/src/main.rs b/examples/async_clock/src/main.rs new file mode 100644 index 00000000000..84ade6a82b0 --- /dev/null +++ b/examples/async_clock/src/main.rs @@ -0,0 +1,126 @@ +use chrono::{DateTime, Local}; +use futures::{FutureExt, StreamExt}; +use services::compute_fun_score; +use yew::platform::pinned::mpsc::UnboundedSender; +use yew::{html, AttrValue, Component, Context, Html}; + +use crate::services::{emit_jokes, initialize_atomic_clocks, stream_time}; + +mod services; + +/// The AsyncComponent displays the current time and some silly jokes. Its main purpose is to +/// demonstrate the use of async code in a yew component. It uses the following async features: +/// - send_future +/// - send_stream +/// - spawn_local +/// - mpsc::unbounded channels +pub struct AsyncComponent { + clock: Option, + joke: Option, + fun_score: Option, + fun_score_channel: UnboundedSender, +} + +pub enum Msg { + ClockInitialized(()), + ClockTicked(DateTime), + Joke(AttrValue), + FunScore(i16), +} + +impl Component for AsyncComponent { + type Message = Msg; + type Properties = (); + + fn create(ctx: &Context) -> Self { + // Demonstrate how we can send a message to the component when a future completes. + // This is the most straightforward way to use async code in a yew component. + let is_initialized = initialize_atomic_clocks(); + ctx.link() + .send_future(is_initialized.map(Msg::ClockInitialized)); + + // The compute_fun_score launches a background task that is ready to compute the fun score + // from jokes that are delivered on this channel. The outcome of the computation is + // sent back to the component via the Msg::FunScore callback. + let fun_score_cb = ctx.link().callback(Msg::FunScore); + let fun_score_channel = compute_fun_score(fun_score_cb); + + Self { + clock: None, + joke: None, + fun_score: None, + fun_score_channel, + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::ClockTicked(current_time) => { + // Update the clock display + self.clock = Some(AttrValue::from(current_time.to_rfc2822())); + } + Msg::ClockInitialized(_) => { + // Now that the clock is initialized, we can start the time stream. + self.clock = Some(AttrValue::from("Initialized")); + + // The stream_time method returns a stream of time updates. We use send_stream to + // update the component with a Msg::ClockTicked message every time + // the stream produces a new value. + let time_steam = stream_time(); + ctx.link().send_stream(time_steam.map(Msg::ClockTicked)); + + // In parallel we launch a background task that produces jokes to make the clock + // more fun to watch. The jokes are emitted back to the component + // throught the Msg::Joke callback. + let joke_cb = ctx.link().callback(Msg::Joke); + emit_jokes(joke_cb); + } + Msg::Joke(joke) => { + // Update the joke + self.joke = Some(joke.clone()); + + // Reset the fun score + self.fun_score = None; + + // Send the joke to the background task that computes the fun score. + self.fun_score_channel + .send_now(joke) + .expect("failed to send joke"); + } + Msg::FunScore(score) => { + self.fun_score = Some(score); + } + } + true + } + + fn view(&self, _ctx: &Context) -> Html { + let display = self.clock.as_deref().unwrap_or("Loading..."); + let joke = self.joke.as_deref().unwrap_or("Loading..."); + let fun_score = self + .fun_score + .map(|score| format!("Fun score: {score}")) + .unwrap_or_else(|| "Computing...".to_string()); + + html! { +
+
+

{ "Asynchronous Examples" }

+
+ { display } +
+
+ { joke } +
+
+ { fun_score } +
+
+
+ } + } +} + +fn main() { + yew::Renderer::::new().render(); +} diff --git a/examples/async_clock/src/services.rs b/examples/async_clock/src/services.rs new file mode 100644 index 00000000000..b9025f735df --- /dev/null +++ b/examples/async_clock/src/services.rs @@ -0,0 +1,60 @@ +use std::time::Duration; + +use chrono::{DateTime, Local}; +use futures::{Stream, StreamExt}; +use gloo_net::http::Request; +use yew::platform::pinned::mpsc::UnboundedSender; +use yew::platform::spawn_local; +use yew::platform::time::{interval, sleep}; +use yew::{AttrValue, Callback}; + +const ONE_SEC: Duration = Duration::from_secs(1); +const TEN_SECS: Duration = Duration::from_secs(10); + +/// Demonstration code to show how to use async code in a yew component. +pub async fn initialize_atomic_clocks() { + // aligning with atomic clocks :-) + sleep(ONE_SEC).await; +} + +/// Returns a stream of time updates. +pub fn stream_time() -> impl Stream> { + interval(ONE_SEC).map(|_| Local::now()) +} + +/// Emit entertaining jokes every 10 seconds. +pub fn emit_jokes(joke_cb: Callback) { + // Spawn a background task that will fetch a joke and send it to the component. + spawn_local(async move { + loop { + // Fetch the online joke + let fun_fact = Request::get("https://v2.jokeapi.dev/joke/Programming?format=txt") + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + // Emit it to the component + joke_cb.emit(AttrValue::from(fun_fact)); + sleep(TEN_SECS).await; + } + }); +} + +/// Background task that computes the fun score from jokes that are delivered on the channel. +pub fn compute_fun_score(fun_score_cb: Callback) -> UnboundedSender { + let (tx, mut rx) = yew::platform::pinned::mpsc::unbounded::(); + + // Read endlessly from the UnboundedReceiver and compute the fun score. + spawn_local(async move { + while let Some(joke) = rx.next().await { + sleep(ONE_SEC).await; + let score = joke.len() as i16; + fun_score_cb.emit(score); + } + }); + + tx +} diff --git a/examples/boids/README.md b/examples/boids/README.md index baf3fe533cb..fc3f883bbfd 100644 --- a/examples/boids/README.md +++ b/examples/boids/README.md @@ -7,14 +7,6 @@ A version of [Boids](https://en.wikipedia.org/wiki/Boids) implemented in Yew. This example doesn't make use of a [Canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API), instead, each boid has its own element demonstrating the performance of Yew's virtual DOM. -## Running - -You should run this example with the `--release` flag: - -```bash -trunk serve --release -``` - ## Concepts The example uses [`gloo::timers`](https://docs.rs/gloo-timers/latest/gloo_timers/) implementation of `setInterval` to drive the Yew game loop. @@ -29,3 +21,11 @@ The example uses [`gloo::timers`](https://docs.rs/gloo-timers/latest/gloo_timers - Share settings by encoding them into the URL - Resize the boids when "Spacing" is changed. The setting should then also be renamed to something like "Size". + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/boids/src/boid.rs b/examples/boids/src/boid.rs index fdc32a49eb9..88050bc7aba 100644 --- a/examples/boids/src/boid.rs +++ b/examples/boids/src/boid.rs @@ -132,7 +132,7 @@ impl Boid { let Vector2D { x, y } = self.position + offset; // Write to string will never fail. - let _ = write!(points, "{:.2},{:.2} ", x, y); + let _ = write!(points, "{x:.2},{y:.2} "); } html! { } diff --git a/examples/boids/src/simulation.rs b/examples/boids/src/simulation.rs index 6f74703b917..ff47a28a98f 100644 --- a/examples/boids/src/simulation.rs +++ b/examples/boids/src/simulation.rs @@ -25,7 +25,6 @@ pub struct Props { pub struct Simulation { boids: Vec, interval: Interval, - settings: Settings, generation: usize, } impl Component for Simulation { @@ -49,7 +48,6 @@ impl Component for Simulation { Self { boids, interval, - settings, generation, } } @@ -73,10 +71,10 @@ impl Component for Simulation { } } - fn changed(&mut self, ctx: &Context) -> bool { + fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { let props = ctx.props(); - let should_reset = self.settings != props.settings || self.generation != props.generation; - self.settings = props.settings.clone(); + let should_reset = + old_props.settings != props.settings || self.generation != props.generation; self.generation = props.generation; if should_reset { self.boids.clear(); diff --git a/examples/boids/src/slider.rs b/examples/boids/src/slider.rs index f9f09bb7609..7e1e6ca72bc 100644 --- a/examples/boids/src/slider.rs +++ b/examples/boids/src/slider.rs @@ -56,12 +56,12 @@ impl Component for Slider { step, } = *ctx.props(); - let precision = precision.unwrap_or(if percentage { 1 } else { 0 }); + let precision = precision.unwrap_or_else(|| usize::from(percentage)); let display_value = if percentage { format!("{:.p$}%", 100.0 * value, p = precision) } else { - format!("{:.p$}", value, p = precision) + format!("{value:.precision$}") }; let id = format!("slider-{}", self.id); diff --git a/examples/communication_child_to_parent/Cargo.toml b/examples/communication_child_to_parent/Cargo.toml new file mode 100644 index 00000000000..a23fc8b429e --- /dev/null +++ b/examples/communication_child_to_parent/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "communication_child_to_parent" +version = "0.0.1" +authors = ["Marcel Ibes "] +edition = "2021" +license = "MIT OR Apache-2.0" + +[dependencies] +yew = { path = "../../packages/yew", features = ["csr"] } \ No newline at end of file diff --git a/examples/communication_child_to_parent/README.md b/examples/communication_child_to_parent/README.md new file mode 100644 index 00000000000..3c3ca0355e6 --- /dev/null +++ b/examples/communication_child_to_parent/README.md @@ -0,0 +1,11 @@ +# Child-to-Parent Example + +A simple example of updating a parent from two children using a callback mechanism. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` diff --git a/examples/communication_child_to_parent/index.html b/examples/communication_child_to_parent/index.html new file mode 100644 index 00000000000..ee0bd8ce3a3 --- /dev/null +++ b/examples/communication_child_to_parent/index.html @@ -0,0 +1,11 @@ + + + + + Yew • Child-to-Parent Communication + + + + + + diff --git a/examples/communication_child_to_parent/index.scss b/examples/communication_child_to_parent/index.scss new file mode 100644 index 00000000000..f6badc33020 --- /dev/null +++ b/examples/communication_child_to_parent/index.scss @@ -0,0 +1,149 @@ +.page { + min-height: 100vh; + min-width: 100vw; + + line-height: 1.5; + tab-size: 4; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-feature-settings: normal; + + margin: 0; + line-height: inherit; +} + +.parent { + color: rgb(244 244 245); + background-color: rgb(24 24 27); + + justify-content: center; + align-items: center; + flex-direction: column; + display: flex; + + min-height: 100vh; + min-width: 100vw; +} + +.title { + font-size: 2.25rem; + line-height: 2.5rem; + font-weight: inherit; + + margin: 0; + margin-bottom: 2rem; +} + +// Bodies + +.parent-body, .child-body { + border-width: 4px; + border-radius: 1rem; + + border-style: solid; +} + +.parent-body { + border-color: rgb(22 163 74); +} + +.child-body { + border-color: rgb(249 115 22); + + flex-grow: 1; +} + +// Tags + +.parent-tag, .child-tag { + font-weight: 500; + + padding-bottom: 0.25rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + + line-height: 24px; +} + +.parent-tag { + background-color: rgb(22 163 74); +} + +.child-tag { + background-color: rgb(249 115 22); +} + +// Content + +.parent-content { + padding-top: 0.75rem; + padding-bottom: 1.25rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + + flex-direction: column; + display: flex; +} + +.parent-content > span { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.parent-content > span > span { + font-weight: 700; +} + +.parent-content > span:nth-child(2) { + margin-top: 0.75rem; + margin-bottom: 0.75rem; +} + +.parent-content > div { + column-gap: 1.25rem; + display: flex; + + margin-top: 0.75rem; +} + +.child-content { + padding-top: 1.25rem; + padding-bottom: 1.25rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + + justify-content: space-between; + align-items: center; + display: flex; +} + +.child-content > span { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.child-content > button { + font-weight: 500; + font-size: 1.125rem; + line-height: 1.75rem; + text-transform: none; + font-family: inherit; + + padding-top: 0.25rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + + background-color: rgb(249 115 22); + + border-radius: 0.75rem; + border-width: 0; + color: rgb(244 244 245); + cursor: pointer; +} + +.child-content > button:hover { + background-color: rgb(194 65 12); +} \ No newline at end of file diff --git a/examples/communication_child_to_parent/src/child.rs b/examples/communication_child_to_parent/src/child.rs new file mode 100644 index 00000000000..4a3b4e63886 --- /dev/null +++ b/examples/communication_child_to_parent/src/child.rs @@ -0,0 +1,40 @@ +use super::*; + +/// The `Child` component is the child of the `Parent` component, and will send updates to the +/// parent using a Callback. +pub struct Child; + +#[derive(Clone, PartialEq, Properties)] +pub struct ChildProps { + pub name: AttrValue, + pub on_clicked: Callback, +} + +impl Component for Child { + type Message = (); + type Properties = ChildProps; + + fn create(_ctx: &Context) -> Self { + Self {} + } + + fn view(&self, ctx: &Context) -> Html { + let name = format!("I'm {}.", ctx.props().name); + let my_name = ctx.props().name.clone(); + + // Here we emit the callback to the parent component, whenever the button is clicked. + let onclick = ctx.props().on_clicked.reform(move |_| my_name.clone()); + + html! { +
+
+ { "Child" } +
+
+ { name } + +
+
+ } + } +} diff --git a/examples/communication_child_to_parent/src/main.rs b/examples/communication_child_to_parent/src/main.rs new file mode 100644 index 00000000000..f880cfd9498 --- /dev/null +++ b/examples/communication_child_to_parent/src/main.rs @@ -0,0 +1,10 @@ +use child::Child; +use parent::Parent; +use yew::{html, AttrValue, Callback, Component, Context, Html, Properties}; + +mod child; +mod parent; + +fn main() { + yew::Renderer::::new().render(); +} diff --git a/examples/communication_child_to_parent/src/parent.rs b/examples/communication_child_to_parent/src/parent.rs new file mode 100644 index 00000000000..ccd179d37fe --- /dev/null +++ b/examples/communication_child_to_parent/src/parent.rs @@ -0,0 +1,67 @@ +use super::*; + +pub enum Msg { + ButtonClick(AttrValue), +} + +/// The `Parent` component holds some state that is updated when its children are clicked +pub struct Parent { + /// The total number of clicks received + total_clicks: u32, + /// The name of the child that was last clicked + last_updated: Option, +} +impl Component for Parent { + type Message = Msg; + type Properties = (); + + fn create(_ctx: &Context) -> Self { + Self { + total_clicks: 0, + last_updated: None, + } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::ButtonClick(childs_name) => { + // Keep track of the name of the child that was clicked + self.last_updated = Some(childs_name); + + // Increment the total number of clicks + self.total_clicks += 1; + true + } + } + } + + fn view(&self, ctx: &Context) -> Html { + let last_updated_msg = if let Some(last_updated) = self.last_updated.as_ref() { + format!("The last child you clicked was {last_updated}.") + } else { + "Waiting for you to click a child...".to_string() + }; + + let on_clicked = ctx.link().callback(Msg::ButtonClick); + html! { +
+
+

{ "Child-to-Parent Communication Example" }

+
+
+ { "Parent" } +
+
+ { "My children have been clicked " }{ self.total_clicks }{ " times." } + { last_updated_msg } +
+ + +
+
+
+
+
+ } + } +} diff --git a/examples/communication_grandchild_with_grandparent/Cargo.toml b/examples/communication_grandchild_with_grandparent/Cargo.toml new file mode 100644 index 00000000000..16144f8cf18 --- /dev/null +++ b/examples/communication_grandchild_with_grandparent/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "communication_grandchild_with_grandparent" +version = "0.0.1" +authors = ["Marcel Ibes "] +edition = "2021" +license = "MIT OR Apache-2.0" + +[dependencies] +yew = { path = "../../packages/yew", features = ["csr"] } diff --git a/examples/communication_grandchild_with_grandparent/README.md b/examples/communication_grandchild_with_grandparent/README.md new file mode 100644 index 00000000000..88e919d65ce --- /dev/null +++ b/examples/communication_grandchild_with_grandparent/README.md @@ -0,0 +1,12 @@ +# Grandchild-with-Grandparent Example + +A simple example of updating a grandparent component from two grandchildren using a callback passed down using the context API. +The grandchildren themselves also update whenever the grandparent updates the shared state. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/communication_grandchild_with_grandparent/index.html b/examples/communication_grandchild_with_grandparent/index.html new file mode 100644 index 00000000000..1915ef59e00 --- /dev/null +++ b/examples/communication_grandchild_with_grandparent/index.html @@ -0,0 +1,11 @@ + + + + + Yew • Grandchild-to-Grandparent Communication + + + + + + diff --git a/examples/communication_grandchild_with_grandparent/index.scss b/examples/communication_grandchild_with_grandparent/index.scss new file mode 100644 index 00000000000..c8972fdfa3a --- /dev/null +++ b/examples/communication_grandchild_with_grandparent/index.scss @@ -0,0 +1,179 @@ +.page { + min-height: 100vh; + min-width: 100vw; + + line-height: 1.5; + tab-size: 4; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-feature-settings: normal; + + margin: 0; + line-height: inherit; +} + +.grandparent { + color: rgb(244 244 245); + background-color: rgb(24 24 27); + + justify-content: center; + align-items: center; + flex-direction: column; + display: flex; + + min-height: 100vh; + min-width: 100vw; +} + +.title { + font-size: 2.25rem; + line-height: 2.5rem; + font-weight: inherit; + + margin: 0; + margin-bottom: 2rem; +} + +// Bodies + +.grandparent-body, .parent-body, .child-body { + border-width: 4px; + border-radius: 1rem; + + border-style: solid; +} + +.grandparent-body { + border-color: rgb(22 163 74); +} + +.parent-body, .child-body { + flex-grow: 1; +} + +.parent-body { + border-color: rgb(249 115 22); +} + +.child-body { + border-color: rgb(147 51 234); + + margin-top: 0.5rem; +} + +.child-body > div:nth-child(2) { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.child-body > div:nth-child(2) > span { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.child-body > div:nth-child(2) > span > span { + font-weight: 700; +} + +// Tags + +.grandparent-tag, .parent-tag, .child-tag { + font-weight: 500; + + padding-bottom: 0.25rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + + line-height: 24px; +} + +.grandparent-tag { + background-color: rgb(22 163 74); +} + +.parent-tag { + background-color: rgb(249 115 22); +} + +.child-tag { + background-color: rgb(147 51 234); +} + +// Content + +.grandparent-content { + padding-top: 0.75rem; + padding-bottom: 1.25rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + + flex-direction: column; + display: flex; +} + +.grandparent-content > span { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.grandparent-content > span:nth-child(2) { + margin-top: 0.75rem; + margin-bottom: 0.75rem; +} + +.grandparent-content > span > span { + font-weight: 700; +} + +.parent-content { + padding-top: 0.75rem; + padding-bottom: 1.25rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + + column-gap: 1.25rem; + display: flex; +} + +.child-content { + padding-bottom: 1.25rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + + justify-content: space-between; + align-items: center; + display: flex; +} + +.child-content > span { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.child-content > button { + font-weight: 500; + font-size: 1.125rem; + line-height: 1.75rem; + text-transform: none; + font-family: inherit; + + padding-top: 0.25rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + + background-color: rgb(147 51 234); + + border-radius: 0.75rem; + border-width: 0; + color: rgb(244 244 245); + cursor: pointer; +} + +.child-content > button:hover { + background-color: rgb(107 33 168); +} \ No newline at end of file diff --git a/examples/communication_grandchild_with_grandparent/src/child.rs b/examples/communication_grandchild_with_grandparent/src/child.rs new file mode 100644 index 00000000000..83f363d4e1a --- /dev/null +++ b/examples/communication_grandchild_with_grandparent/src/child.rs @@ -0,0 +1,65 @@ +use super::*; + +/// The `Child` component is the child of the `Parent` component, and will send and receive updates +/// to/from the grandparent using the context. +pub struct Child { + state: Rc, + _listener: ContextHandle>, +} + +pub enum ChildMsg { + ContextChanged(Rc), +} + +#[derive(Clone, Eq, PartialEq, Properties)] +pub struct ChildProps { + pub name: AttrValue, +} + +impl Component for Child { + type Message = ChildMsg; + type Properties = ChildProps; + + fn create(ctx: &Context) -> Self { + // Here we fetch the shared state from the context. For a demonstration on the use of + // context in a functional component, have a look at the `examples/contexts` code. + let (state, _listener) = ctx + .link() + .context::>(ctx.link().callback(ChildMsg::ContextChanged)) + .expect("context to be set"); + + Self { state, _listener } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + ChildMsg::ContextChanged(state) => { + self.state = state; + true + } + } + } + + fn view(&self, ctx: &Context) -> Html { + let my_name = ctx.props().name.clone(); + let name = format!("I'm {my_name}."); + + // Here we emit the callback to the grandparent component, whenever the button is clicked. + let onclick = self.state.child_clicked.reform(move |_| (my_name.clone())); + + html! { +
+
+ { "Child" } +
+
+ { "We've been clicked " }{ self.state.total_clicks }{ " times." } +
+
+ { name } + +
+
+ } + } +} diff --git a/examples/communication_grandchild_with_grandparent/src/grandparent.rs b/examples/communication_grandchild_with_grandparent/src/grandparent.rs new file mode 100644 index 00000000000..222e5fb91fa --- /dev/null +++ b/examples/communication_grandchild_with_grandparent/src/grandparent.rs @@ -0,0 +1,66 @@ +use super::*; + +pub enum Msg { + ButtonClick(AttrValue), +} + +/// Our top-level (grandparent) component that holds a reference to the shared state. +pub struct GrandParent { + state: Rc, +} +impl Component for GrandParent { + type Message = Msg; + type Properties = (); + + fn create(ctx: &Context) -> Self { + let child_clicked = ctx.link().callback(Msg::ButtonClick); + let state = Rc::new(AppState { + total_clicks: 0, + child_clicked, + last_clicked: None, + }); + Self { state } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::ButtonClick(childs_name) => { + // Update the shared state + let shared_state = Rc::make_mut(&mut self.state); + shared_state.total_clicks += 1; + shared_state.last_clicked = Some(childs_name); + true + } + } + } + + fn view(&self, _ctx: &Context) -> Html { + let app_state = self.state.clone(); + + let detail_msg = if let Some(last_clicked) = &self.state.last_clicked { + format!("The last child you clicked was {last_clicked}.") + } else { + "Waiting for you to click a grandchild...".to_string() + }; + + html! { + > context={app_state}> +
+
+

{ "Grandchild-with-Grandparent Communication Example" }

+
+
+ { "Grandparent" } +
+
+ { "My grandchildren have been clicked " }{ self.state.total_clicks }{ " times." } + {detail_msg} + +
+
+
+
+
>> + } + } +} diff --git a/examples/communication_grandchild_with_grandparent/src/main.rs b/examples/communication_grandchild_with_grandparent/src/main.rs new file mode 100644 index 00000000000..33ffe9702ae --- /dev/null +++ b/examples/communication_grandchild_with_grandparent/src/main.rs @@ -0,0 +1,30 @@ +use std::rc::Rc; + +use child::Child; +use grandparent::GrandParent; +use parent::Parent; + +mod child; +mod grandparent; +mod parent; + +use yew::{ + function_component, html, AttrValue, Callback, Component, Context, ContextHandle, + ContextProvider, Html, Properties, +}; + +/// This is the shared state between the parent and child components. +#[derive(Clone, PartialEq)] +pub struct AppState { + /// Total number of clicks received. + total_clicks: u32, + /// Callback used when a child is clicked. The AttrValue is the name of the child that was + /// clicked. + child_clicked: Callback, + /// The name of the child that was last clicked. + last_clicked: Option, +} + +fn main() { + yew::Renderer::::new().render(); +} diff --git a/examples/communication_grandchild_with_grandparent/src/parent.rs b/examples/communication_grandchild_with_grandparent/src/parent.rs new file mode 100644 index 00000000000..5b44bb0eb65 --- /dev/null +++ b/examples/communication_grandchild_with_grandparent/src/parent.rs @@ -0,0 +1,18 @@ +use super::*; + +/// The `Parent` component is the parent of the `Child` component. It has no logic, and is here to +/// show there is no direct relation between grandchild and grandparent. +#[function_component] +pub fn Parent() -> Html { + html! { +
+
+ { "Parent" } +
+
+ + +
+
+ } +} diff --git a/examples/communication_grandparent_to_grandchild/Cargo.toml b/examples/communication_grandparent_to_grandchild/Cargo.toml new file mode 100644 index 00000000000..e95f1ca9ede --- /dev/null +++ b/examples/communication_grandparent_to_grandchild/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "communication_grandparent_to_grandchild" +version = "0.0.1" +authors = ["Marcel Ibes "] +edition = "2021" +license = "MIT OR Apache-2.0" + +[dependencies] +yew = { path = "../../packages/yew", features = ["csr"] } \ No newline at end of file diff --git a/examples/communication_grandparent_to_grandchild/README.md b/examples/communication_grandparent_to_grandchild/README.md new file mode 100644 index 00000000000..8b14b86852b --- /dev/null +++ b/examples/communication_grandparent_to_grandchild/README.md @@ -0,0 +1,11 @@ +# Grandparent-to-Grandchild Example + +A simple example of updating a grandchild component from a grandparent using a shared state through the ContextProvider. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/communication_grandparent_to_grandchild/index.html b/examples/communication_grandparent_to_grandchild/index.html new file mode 100644 index 00000000000..eb397c080bb --- /dev/null +++ b/examples/communication_grandparent_to_grandchild/index.html @@ -0,0 +1,11 @@ + + + + + Yew • Granparent-to-GrandChild Communication + + + + + + diff --git a/examples/communication_grandparent_to_grandchild/index.scss b/examples/communication_grandparent_to_grandchild/index.scss new file mode 100644 index 00000000000..6db8c55d5ab --- /dev/null +++ b/examples/communication_grandparent_to_grandchild/index.scss @@ -0,0 +1,167 @@ +.page { + min-height: 100vh; + min-width: 100vw; + + line-height: 1.5; + tab-size: 4; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-feature-settings: normal; + + margin: 0; + line-height: inherit; +} + +.grandparent { + color: rgb(244 244 245); + background-color: rgb(24 24 27); + + justify-content: center; + align-items: center; + flex-direction: column; + display: flex; + + min-height: 100vh; + min-width: 100vw; +} + +.title { + font-size: 2.25rem; + line-height: 2.5rem; + font-weight: inherit; + + margin: 0; + margin-bottom: 2rem; +} + +// Bodies + +.grandparent-body, .parent-body, .child-body { + border-width: 4px; + border-radius: 1rem; + + border-style: solid; +} + +.grandparent-body { + border-color: rgb(22 163 74); +} + +.parent-body, .child-body { + flex-grow: 1; +} + +.parent-body { + border-color: rgb(249 115 22); +} + +.child-body { + border-color: rgb(147 51 234); + + margin-top: 0.5rem; +} + +.child-body > div:nth-child(2) { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.child-body > div:nth-child(2) > span { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.child-body > div:nth-child(2) > span > span { + font-weight: 700; +} + +// Tags + +.grandparent-tag, .parent-tag, .child-tag { + font-weight: 500; + + padding-bottom: 0.25rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + + line-height: 24px; +} + +.grandparent-tag { + background-color: rgb(22 163 74); +} + +.parent-tag { + background-color: rgb(249 115 22); +} + +.child-tag { + background-color: rgb(147 51 234); +} + +// Content + +.grandparent-content { + padding-top: 1.25rem; + padding-bottom: 1.25rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + + flex-direction: column; + display: flex; +} + +.grandparent-content > button { + font-weight: 500; + font-size: 1.125rem; + line-height: 1.75rem; + text-transform: none; + font-family: inherit; + + padding-top: 0.25rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + + margin-bottom: 1.25rem; + + background-color: rgb(22 163 74); + + border-radius: 0.75rem; + border-width: 0; + color: rgb(244 244 245); + cursor: pointer; +} + +.grandparent-content > button:hover { + background-color: rgb(22 101 52); +} + +.parent-content { + padding-top: 0.75rem; + padding-bottom: 1.25rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + + column-gap: 1.25rem; + display: flex; +} + +.child-content { + padding-bottom: 1.25rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + + justify-content: space-between; + align-items: center; + display: flex; +} + +.child-content > span { + font-size: 1.25rem; + line-height: 1.75rem; +} \ No newline at end of file diff --git a/examples/communication_grandparent_to_grandchild/src/child.rs b/examples/communication_grandparent_to_grandchild/src/child.rs new file mode 100644 index 00000000000..5c8a3fb3d7d --- /dev/null +++ b/examples/communication_grandparent_to_grandchild/src/child.rs @@ -0,0 +1,51 @@ +use super::*; + +/// The `Child` component is the child of the `Parent` component, and will receive updates from the +/// grandparent using the context. +pub struct Child { + state: Rc, + _listener: ContextHandle>, +} + +pub enum ChildMsg { + ContextChanged(Rc), +} + +impl Component for Child { + type Message = ChildMsg; + type Properties = (); + + fn create(ctx: &Context) -> Self { + // Here we fetch the shared state from the context. For a demonstration on the use of + // context in a functional component, have a look at the `examples/contexts` code. + let (state, _listener) = ctx + .link() + .context::>(ctx.link().callback(ChildMsg::ContextChanged)) + .expect("context to be set"); + + Self { state, _listener } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + ChildMsg::ContextChanged(state) => { + self.state = state; + true + } + } + } + + fn view(&self, _ctx: &Context) -> Html { + html! { + +
+
+ { "Child" } +
+
+ { "My grandparent has been clicked " }{ self.state.total_clicks }{ " times." } +
+
+ } + } +} diff --git a/examples/communication_grandparent_to_grandchild/src/grandparent.rs b/examples/communication_grandparent_to_grandchild/src/grandparent.rs new file mode 100644 index 00000000000..598d629d27a --- /dev/null +++ b/examples/communication_grandparent_to_grandchild/src/grandparent.rs @@ -0,0 +1,54 @@ +use super::*; + +/// Our top-level (grandparent) component that holds a reference to the shared state. +pub struct GrandParent { + state: Rc, +} + +pub enum Msg { + ButtonClick, +} + +impl Component for GrandParent { + type Message = Msg; + type Properties = (); + + fn create(_ctx: &Context) -> Self { + let state = Rc::new(AppState { total_clicks: 0 }); + Self { state } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::ButtonClick => { + Rc::make_mut(&mut self.state).total_clicks += 1; + true + } + } + } + + fn view(&self, ctx: &Context) -> Html { + let onclick = ctx.link().callback(|_| Msg::ButtonClick); + let app_state = self.state.clone(); + + html! { + > context={app_state}> +
+
+

{ "Grandparent-to-Grandchild Communication Example" }

+ +
+
+ { "Grandparent" } +
+
+ + +
+
+
+
+
>> + } + } +} diff --git a/examples/communication_grandparent_to_grandchild/src/main.rs b/examples/communication_grandparent_to_grandchild/src/main.rs new file mode 100644 index 00000000000..36e9ce73c84 --- /dev/null +++ b/examples/communication_grandparent_to_grandchild/src/main.rs @@ -0,0 +1,22 @@ +use std::rc::Rc; + +use child::Child; +use grandparent::GrandParent; +use parent::Parent; + +mod child; +mod grandparent; +mod parent; + +use yew::{function_component, html, Component, Context, ContextHandle, ContextProvider, Html}; + +/// This is the shared state between the parent and child components. +#[derive(Clone, Eq, PartialEq)] +pub struct AppState { + /// The total number of clicks received. + total_clicks: u32, +} + +fn main() { + yew::Renderer::::new().render(); +} diff --git a/examples/communication_grandparent_to_grandchild/src/parent.rs b/examples/communication_grandparent_to_grandchild/src/parent.rs new file mode 100644 index 00000000000..a716c034ee7 --- /dev/null +++ b/examples/communication_grandparent_to_grandchild/src/parent.rs @@ -0,0 +1,17 @@ +use super::*; + +/// The `Parent` component is the parent of the `Child` component. It has no logic, and is here to +/// show there is no direct relation between grandchild and grandparent. +#[function_component] +pub fn Parent() -> Html { + html! { +
+
+ { "Parent" } +
+
+ +
+
+ } +} diff --git a/examples/communication_parent_to_child/Cargo.toml b/examples/communication_parent_to_child/Cargo.toml new file mode 100644 index 00000000000..f6c505cdcf7 --- /dev/null +++ b/examples/communication_parent_to_child/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "communication_parent_to_child" +version = "0.0.1" +authors = ["Marcel Ibes "] +edition = "2021" +license = "MIT OR Apache-2.0" + +[dependencies] +yew = { path = "../../packages/yew", features = ["csr"] } \ No newline at end of file diff --git a/examples/communication_parent_to_child/README.md b/examples/communication_parent_to_child/README.md new file mode 100644 index 00000000000..b9009d20ff5 --- /dev/null +++ b/examples/communication_parent_to_child/README.md @@ -0,0 +1,11 @@ +# Parent-to-Child Example + +A simple example of updating a child from a parent using properties + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/communication_parent_to_child/index.html b/examples/communication_parent_to_child/index.html new file mode 100644 index 00000000000..7b0e3ca5934 --- /dev/null +++ b/examples/communication_parent_to_child/index.html @@ -0,0 +1,11 @@ + + + + + Yew • Parent-to-Child Communication + + + + + + diff --git a/examples/communication_parent_to_child/index.scss b/examples/communication_parent_to_child/index.scss new file mode 100644 index 00000000000..74926d834ea --- /dev/null +++ b/examples/communication_parent_to_child/index.scss @@ -0,0 +1,130 @@ +.page { + min-height: 100vh; + min-width: 100vw; + + line-height: 1.5; + tab-size: 4; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-feature-settings: normal; + + margin: 0; + line-height: inherit; +} + +.parent { + color: rgb(244 244 245); + background-color: rgb(24 24 27); + + justify-content: center; + align-items: center; + flex-direction: column; + display: flex; + + min-height: 100vh; + min-width: 100vw; +} + +.title { + font-size: 2.25rem; + line-height: 2.5rem; + font-weight: inherit; + + margin: 0; + margin-bottom: 2rem; +} + +// Bodies + +.parent-body, .child-body { + border-width: 4px; + border-radius: 1rem; + + border-style: solid; +} + +.parent-body { + border-color: rgb(22 163 74); +} + +.child-body { + border-color: rgb(249 115 22); + + flex-grow: 1; +} + +// Tags + +.parent-tag, .child-tag { + font-weight: 500; + + padding-bottom: 0.25rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + + line-height: 24px; +} + +.parent-tag { + background-color: rgb(22 163 74); +} + +.child-tag { + background-color: rgb(249 115 22); +} + +// Content + +.parent-content { + padding-top: 1.25rem; + padding-bottom: 1.25rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + + flex-direction: column; + display: flex; +} + +.parent-content > button { + font-weight: 500; + font-size: 1.125rem; + line-height: 1.75rem; + text-transform: none; + font-family: inherit; + + padding-top: 0.25rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + + margin-bottom: 1.25rem; + + background-color: rgb(22 163 74); + + border-radius: 0.75rem; + border-width: 0; + color: rgb(244 244 245); + cursor: pointer; +} + +.parent-content > button:hover { + background-color: rgb(22 101 52); +} + +.child-content { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.child-content > span { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.child-content > span > span { + font-weight: 700; +} \ No newline at end of file diff --git a/examples/communication_parent_to_child/src/child.rs b/examples/communication_parent_to_child/src/child.rs new file mode 100644 index 00000000000..e2379b5b484 --- /dev/null +++ b/examples/communication_parent_to_child/src/child.rs @@ -0,0 +1,32 @@ +use super::*; + +/// The `Child` component is the child of the `Parent` component, and will receive updates from the +/// parent using properties. +pub struct Child; + +#[derive(Clone, Eq, PartialEq, Properties)] +pub struct ChildProps { + pub clicks: u32, +} + +impl Component for Child { + type Message = (); + type Properties = ChildProps; + + fn create(_ctx: &Context) -> Self { + Self {} + } + + fn view(&self, ctx: &Context) -> Html { + html! { +
+
+ { "Child" } +
+
+ { "My parent has been clicked " }{ ctx.props().clicks }{ " times." } +
+
+ } + } +} diff --git a/examples/communication_parent_to_child/src/main.rs b/examples/communication_parent_to_child/src/main.rs new file mode 100644 index 00000000000..056688797d9 --- /dev/null +++ b/examples/communication_parent_to_child/src/main.rs @@ -0,0 +1,10 @@ +use child::Child; +use parent::Parent; +use yew::{html, Component, Context, Html, Properties}; + +mod child; +mod parent; + +fn main() { + yew::Renderer::::new().render(); +} diff --git a/examples/communication_parent_to_child/src/parent.rs b/examples/communication_parent_to_child/src/parent.rs new file mode 100644 index 00000000000..cb06bd9fdf9 --- /dev/null +++ b/examples/communication_parent_to_child/src/parent.rs @@ -0,0 +1,53 @@ +use super::*; + +/// The `Parent` component holds some state that is passed down to the children. +pub struct Parent { + /// The total number of clicks received + nr_of_clicks: u32, +} + +pub enum Msg { + ButtonClick, +} + +impl Component for Parent { + type Message = Msg; + type Properties = (); + + fn create(_ctx: &Context) -> Self { + Self { nr_of_clicks: 0 } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::ButtonClick => { + self.nr_of_clicks += 1; + true + } + } + } + + fn view(&self, ctx: &Context) -> Html { + let onclick = ctx.link().callback(|_| Msg::ButtonClick); + + // Here we pass down "our" nr_of_clicks to the child by setting the "clicks" property. + let clicks = self.nr_of_clicks; + + html! { +
+
+

{ "Parent-to-Child Communication Example" }

+
+
+ { "Parent" } +
+
+ + +
+
+
+
+ } + } +} diff --git a/examples/contexts/README.md b/examples/contexts/README.md index 56d8c416122..570e803b5b4 100644 --- a/examples/contexts/README.md +++ b/examples/contexts/README.md @@ -8,3 +8,11 @@ This is currently a technical demonstration of Context API. The example has two components, which communicates through a context as opposed to the traditional method using component links. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/contexts/src/msg_ctx.rs b/examples/contexts/src/msg_ctx.rs index 9eea9cdbcbe..ceebd199d3f 100644 --- a/examples/contexts/src/msg_ctx.rs +++ b/examples/contexts/src/msg_ctx.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use yew::prelude::*; -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Message { pub inner: String, } @@ -20,7 +20,7 @@ pub type MessageContext = UseReducerHandle; #[derive(Properties, Debug, PartialEq)] pub struct MessageProviderProps { #[prop_or_default] - pub children: Children, + pub children: Html, } #[function_component] diff --git a/examples/counter/Cargo.toml b/examples/counter/Cargo.toml index 6710ec24fcd..47bb8f10f92 100644 --- a/examples/counter/Cargo.toml +++ b/examples/counter/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -gloo-console = "0.2" +gloo = "0.8" js-sys = "0.3" yew = { path = "../../packages/yew", features = ["csr"] } wasm-bindgen = "0.2" diff --git a/examples/counter/README.md b/examples/counter/README.md index 994bc89a5ed..06a1fb7fff9 100644 --- a/examples/counter/README.md +++ b/examples/counter/README.md @@ -4,20 +4,14 @@ A simple example of a counter which can be increased or decreased with the press of a button. -## Running +## Concepts -Run a debug version of this application: +Demonstrates the use of messages to update state. -```bash -trunk serve -``` +## Running -Run a release version of this application: +Run this application with the trunk development server: ```bash -trunk serve --release -``` - -## Concepts - -Demonstrates the use of messages to update state. +trunk serve --open +``` \ No newline at end of file diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index d240ef192f5..e2b6bcc0de1 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,4 +1,4 @@ -use gloo_console as console; +use gloo::console; use js_sys::Date; use yew::{html, Component, Context, Html}; diff --git a/examples/counter_functional/README.md b/examples/counter_functional/README.md index 34d98622830..6bf3a4d423e 100644 --- a/examples/counter_functional/README.md +++ b/examples/counter_functional/README.md @@ -2,20 +2,14 @@ A simple example of a counter which can be increased or decreased with the press of a button implemented using function components -## Running +## Concepts -Run a debug version of this application: +Demonstrates the use of function components. -```bash -trunk serve -``` +## Running -Run a release version of this application: +Run this application with the trunk development server: ```bash -trunk serve --release -``` - -## Concepts - -Demonstrates the use of function components. \ No newline at end of file +trunk serve --open +``` \ No newline at end of file diff --git a/examples/counter_functional/src/main.rs b/examples/counter_functional/src/main.rs index c85b7549346..54dcd2806d1 100644 --- a/examples/counter_functional/src/main.rs +++ b/examples/counter_functional/src/main.rs @@ -14,13 +14,13 @@ fn App() -> Html { Callback::from(move |_| state.set(*state - 1)) }; - html!( + html! { <> -

{"current count: "} {*state}

- - +

{"current count: "} {*state}

+ + - ) + } } fn main() { diff --git a/examples/dyn_create_destroy_apps/Cargo.toml b/examples/dyn_create_destroy_apps/Cargo.toml index 743fdab4838..917024ed9de 100644 --- a/examples/dyn_create_destroy_apps/Cargo.toml +++ b/examples/dyn_create_destroy_apps/Cargo.toml @@ -8,13 +8,12 @@ license = "MIT OR Apache-2.0" [dependencies] js-sys = "0.3" yew = { path = "../../packages/yew", features = ["csr"] } -slab = "0.4.3" +slab = "0.4.8" gloo = "0.8" wasm-bindgen = "0.2" -gloo-utils = "0.1" [dependencies.web-sys] -version = "0.3.50" +version = "0.3.64" features = [ "Document", "Element", diff --git a/examples/dyn_create_destroy_apps/README.md b/examples/dyn_create_destroy_apps/README.md index 4f43e576587..d737f918102 100644 --- a/examples/dyn_create_destroy_apps/README.md +++ b/examples/dyn_create_destroy_apps/README.md @@ -2,20 +2,14 @@ An example of how to create and destroy Yew apps on demand. -## Running +## Concepts -Run a debug version of this application: +Demonstrates the use of the Yew app handle by dynamically creating and destroying apps. -```bash -trunk serve -``` +## Running -Run a release version of this application: +Run this application with the trunk development server: ```bash -trunk serve --release -``` - -## Concepts - -Demonstrates the use of the Yew app handle by dynamically creating and destroying apps. +trunk serve --open +``` \ No newline at end of file diff --git a/examples/dyn_create_destroy_apps/src/main.rs b/examples/dyn_create_destroy_apps/src/main.rs index 13740365eb4..1dd0408f2ec 100644 --- a/examples/dyn_create_destroy_apps/src/main.rs +++ b/examples/dyn_create_destroy_apps/src/main.rs @@ -1,4 +1,4 @@ -use gloo_utils::document; +use gloo::utils::document; use slab::Slab; use web_sys::Element; use yew::prelude::*; diff --git a/examples/file_upload/Cargo.toml b/examples/file_upload/Cargo.toml index 5f57593a292..f4a80b553b8 100644 --- a/examples/file_upload/Cargo.toml +++ b/examples/file_upload/Cargo.toml @@ -8,8 +8,9 @@ license = "MIT OR Apache-2.0" [dependencies] js-sys = "0.3" yew = { path = "../../packages/yew", features = ["csr"] } -gloo-file = "0.2" +base64 = "0.21.2" +gloo = "0.8" [dependencies.web-sys] version = "0.3" -features = ["File"] +features = ["File", "DragEvent", "DataTransfer"] diff --git a/examples/file_upload/README.md b/examples/file_upload/README.md index 2a3c6c08377..429a8c09189 100644 --- a/examples/file_upload/README.md +++ b/examples/file_upload/README.md @@ -14,3 +14,11 @@ Demonstrates reading from files in Yew with the help of [`gloo::file`](https://d - Show a progress bar if the file is read in chunks - Do something interesting with the uploaded file like displaying pictures - Improve the presentation of the example with CSS. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/file_upload/index.html b/examples/file_upload/index.html index 8f6ab22ee9a..1680759ac49 100644 --- a/examples/file_upload/index.html +++ b/examples/file_upload/index.html @@ -5,6 +5,11 @@ Yew • File Upload + + diff --git a/examples/file_upload/src/main.rs b/examples/file_upload/src/main.rs index 0df42015646..1e882fcbda7 100644 --- a/examples/file_upload/src/main.rs +++ b/examples/file_upload/src/main.rs @@ -1,24 +1,28 @@ +extern crate base64; use std::collections::HashMap; -use gloo_file::callbacks::FileReader; -use gloo_file::File; -use web_sys::{Event, HtmlInputElement}; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use gloo::file::callbacks::FileReader; +use gloo::file::File; +use web_sys::{DragEvent, Event, FileList, HtmlInputElement}; use yew::html::TargetCast; -use yew::{html, Component, Context, Html}; +use yew::{html, Callback, Component, Context, Html}; -type Chunks = bool; +struct FileDetails { + name: String, + file_type: String, + data: Vec, +} pub enum Msg { - Loaded(String, String), - LoadedBytes(String, Vec), - Files(Vec, Chunks), - ToggleReadBytes, + Loaded(String, String, Vec), + Files(Vec), } pub struct App { readers: HashMap, - files: Vec, - read_bytes: bool, + files: Vec, } impl Component for App { @@ -28,98 +32,116 @@ impl Component for App { fn create(_ctx: &Context) -> Self { Self { readers: HashMap::default(), - files: vec![], - read_bytes: false, + files: Vec::default(), } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - Msg::Loaded(file_name, data) => { - let info = format!("file_name: {}, data: {:?}", file_name, data); - self.files.push(info); + Msg::Loaded(file_name, file_type, data) => { + self.files.push(FileDetails { + data, + file_type, + name: file_name.clone(), + }); self.readers.remove(&file_name); true } - Msg::LoadedBytes(file_name, data) => { - let info = format!("file_name: {}, data: {:?}", file_name, data); - self.files.push(info); - self.readers.remove(&file_name); - true - } - Msg::Files(files, bytes) => { + Msg::Files(files) => { for file in files.into_iter() { let file_name = file.name(); + let file_type = file.raw_mime_type(); + let task = { - let file_name = file_name.clone(); let link = ctx.link().clone(); + let file_name = file_name.clone(); - if bytes { - gloo_file::callbacks::read_as_bytes(&file, move |res| { - link.send_message(Msg::LoadedBytes( - file_name, - res.expect("failed to read file"), - )) - }) - } else { - gloo_file::callbacks::read_as_text(&file, move |res| { - link.send_message(Msg::Loaded( - file_name, - res.unwrap_or_else(|e| e.to_string()), - )) - }) - } + gloo::file::callbacks::read_as_bytes(&file, move |res| { + link.send_message(Msg::Loaded( + file_name, + file_type, + res.expect("failed to read file"), + )) + }) }; self.readers.insert(file_name, task); } true } - Msg::ToggleReadBytes => { - self.read_bytes = !self.read_bytes; - true - } } } fn view(&self, ctx: &Context) -> Html { - let flag = self.read_bytes; html! { -
-
-

{ "Choose a file to upload to see the uploaded bytes" }

- +

{ "Upload Your Files To The Cloud" }

+ + +
+ { for self.files.iter().map(Self::view_file) }
-
    - { for self.files.iter().map(|f| Self::view_file(f)) } -
} } } impl App { - fn view_file(data: &str) -> Html { + fn view_file(file: &FileDetails) -> Html { html! { -
  • { data }
  • +
    +

    { format!("{}", file.name) }

    +
    + if file.file_type.contains("image") { + + } else if file.file_type.contains("video") { + + } +
    +
    + } + } + + fn upload_files(files: Option) -> Msg { + let mut result = Vec::new(); + + if let Some(files) = files { + let files = js_sys::try_iter(&files) + .unwrap() + .unwrap() + .map(|v| web_sys::File::from(v.unwrap())) + .map(File::from); + result.extend(files); } + Msg::Files(result) } } diff --git a/examples/file_upload/styles.css b/examples/file_upload/styles.css new file mode 100644 index 00000000000..84840196957 --- /dev/null +++ b/examples/file_upload/styles.css @@ -0,0 +1,78 @@ +* { + box-sizing: border-box; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; +} + +html, +body { + margin: 0; + padding: 0; + background: #2d3131; + color: #fcfcfc; +} + +img, +video { + max-width: 300px; + max-height: 500px; +} + +p { + text-align: center; +} + +label { + cursor: pointer; +} + +input#file-upload { + visibility: hidden; +} + +#wrapper { + width: 70%; + margin: auto; +} + +#title { + font-size: 2rem; + text-align: center; +} + +#drop-container { + padding: 4rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: #3d4141; + border: 1px dashed #fcfcfc; + border-radius: 1rem; +} + +#drop-container i { + font-size: 4rem; +} + +#preview-area { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: stretch; +} + +.preview-tile { + display: flex; + flex-direction: column; + padding: 2rem; + margin: 1rem; + background: #313737; + border-radius: 1rem; +} + +.preview-media { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; +} diff --git a/examples/function_memory_game/Cargo.toml b/examples/function_memory_game/Cargo.toml index 541ae6cf3a2..95d986fdc7f 100644 --- a/examples/function_memory_game/Cargo.toml +++ b/examples/function_memory_game/Cargo.toml @@ -7,8 +7,8 @@ license = "MIT OR Apache-2.0" [dependencies] serde = { version = "1.0", features = ["derive"] } -strum = "0.24" -strum_macros = "0.24" +strum = "0.25" +strum_macros = "0.25" gloo = "0.8" nanoid = "0.4" rand = "0.8" diff --git a/examples/function_memory_game/README.md b/examples/function_memory_game/README.md index 1ee70893618..2ff70c38d53 100644 --- a/examples/function_memory_game/README.md +++ b/examples/function_memory_game/README.md @@ -12,4 +12,12 @@ This is an implementation of [Memory Game](https://github.com/bradlygreen/Memory ## Note -Images are authorized by [@bradlygreen](https://github.com/bradlygreen), see [authorization-issue](https://github.com/bradlygreen/Memory-Game/issues/6) \ No newline at end of file +Images are authorized by [@bradlygreen](https://github.com/bradlygreen), see [authorization-issue](https://github.com/bradlygreen/Memory-Game/issues/6) + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/function_memory_game/src/components/app.rs b/examples/function_memory_game/src/components/app.rs index 67a954e0c77..226c3399f89 100644 --- a/examples/function_memory_game/src/components/app.rs +++ b/examples/function_memory_game/src/components/app.rs @@ -16,10 +16,11 @@ pub fn App() -> Html { let state = use_reducer(State::reset); let sec_past = use_state(|| 0_u32); let sec_past_timer: Rc>> = use_mut_ref(|| None); - let flip_back_timer: Rc>> = use_mut_ref(|| None); + let limit_flips_timer: Rc>> = use_mut_ref(|| None); let sec_past_time = *sec_past; - use_effect_with_deps( + use_effect_with(state.clone(), { + let limit_flips_timer = limit_flips_timer.clone(); move |state| { // game reset if state.status == Status::Ready { @@ -37,21 +38,24 @@ pub fn App() -> Html { // game over else if state.status == Status::Passed { *sec_past_timer.borrow_mut() = None; - *flip_back_timer.borrow_mut() = None; + *limit_flips_timer.borrow_mut() = None; state.dispatch(Action::TrySaveBestScore(*sec_past)); } // match failed else if state.rollback_cards.is_some() { let cloned_state = state.clone(); let cloned_rollback_cards = state.rollback_cards.clone().unwrap(); - *flip_back_timer.borrow_mut() = Some(Timeout::new(1000, move || { - cloned_state.dispatch(Action::RollbackCards(cloned_rollback_cards)); + *limit_flips_timer.borrow_mut() = Some(Timeout::new(1000, { + let limit_flips_timer = limit_flips_timer.clone(); + move || { + limit_flips_timer.borrow_mut().take(); + cloned_state.dispatch(Action::RollbackCards(cloned_rollback_cards)); + } })); } || () - }, - state.clone(), - ); + } + }); let on_reset = { let state = state.clone(); @@ -61,6 +65,17 @@ pub fn App() -> Html { let on_flip = { let state = state.clone(); Callback::from(move |card| { + if limit_flips_timer.borrow().is_some() { + return; + } + + *limit_flips_timer.borrow_mut() = Some(Timeout::new(1000, { + let limit_flips_timer = limit_flips_timer.clone(); + move || { + limit_flips_timer.borrow_mut().take(); + } + })); + state.dispatch(Action::FlipCard(card)); }) }; diff --git a/examples/function_memory_game/src/components/chessboard_card.rs b/examples/function_memory_game/src/components/chessboard_card.rs index a3c2ce43b80..77ed6c62e7a 100644 --- a/examples/function_memory_game/src/components/chessboard_card.rs +++ b/examples/function_memory_game/src/components/chessboard_card.rs @@ -42,7 +42,7 @@ pub fn ChessboardCard(props: &Props) -> Html { html! {
    -
    +
    card card
    diff --git a/examples/function_memory_game/src/components/score_board.rs b/examples/function_memory_game/src/components/score_board.rs index 03085f9ce40..615d6aaa2b5 100644 --- a/examples/function_memory_game/src/components/score_board.rs +++ b/examples/function_memory_game/src/components/score_board.rs @@ -4,7 +4,7 @@ use crate::components::score_board_best_score::BestScore; use crate::components::score_board_logo::Logo; use crate::components::score_board_progress::GameProgress; -#[derive(PartialEq, Properties, Clone)] +#[derive(PartialEq, Properties, Clone, Eq)] pub struct Props { pub unresolved_card_pairs: u8, pub best_score: u32, diff --git a/examples/function_memory_game/src/components/score_board_best_score.rs b/examples/function_memory_game/src/components/score_board_best_score.rs index 3af394a93a8..0ac6b7eb32f 100644 --- a/examples/function_memory_game/src/components/score_board_best_score.rs +++ b/examples/function_memory_game/src/components/score_board_best_score.rs @@ -1,6 +1,6 @@ use yew::{function_component, html, Html, Properties}; -#[derive(PartialEq, Properties, Clone)] +#[derive(PartialEq, Eq, Properties, Clone)] pub struct Props { pub best_score: u32, } diff --git a/examples/function_memory_game/src/components/score_board_progress.rs b/examples/function_memory_game/src/components/score_board_progress.rs index 13f9bc97848..8e45577c7d5 100644 --- a/examples/function_memory_game/src/components/score_board_progress.rs +++ b/examples/function_memory_game/src/components/score_board_progress.rs @@ -1,6 +1,6 @@ use yew::{function_component, html, Html, Properties}; -#[derive(PartialEq, Properties, Clone)] +#[derive(PartialEq, Eq, Properties, Clone)] pub struct Props { pub unresolved_card_pairs: u8, } diff --git a/examples/function_memory_game/src/constant.rs b/examples/function_memory_game/src/constant.rs index 0de39262e4a..078d766a13a 100644 --- a/examples/function_memory_game/src/constant.rs +++ b/examples/function_memory_game/src/constant.rs @@ -3,7 +3,7 @@ use strum_macros::{Display, EnumIter}; pub const KEY_BEST_SCORE: &str = "memory.game.best.score"; -#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Eq, Serialize, Deserialize)] pub enum CardName { EightBall, Kronos, @@ -15,7 +15,7 @@ pub enum CardName { Zeppelin, } -#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Eq, Serialize, Deserialize)] pub enum Status { Ready, Playing, diff --git a/examples/function_memory_game/src/state.rs b/examples/function_memory_game/src/state.rs index 3baf243221b..29bf93d00d6 100644 --- a/examples/function_memory_game/src/state.rs +++ b/examples/function_memory_game/src/state.rs @@ -7,20 +7,20 @@ use yew::prelude::*; use crate::constant::{CardName, Status, KEY_BEST_SCORE}; use crate::helper::shuffle_cards; -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct RawCard { pub id: String, pub name: CardName, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct Card { pub id: String, pub flipped: bool, pub name: CardName, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct State { pub unresolved_card_pairs: u8, pub best_score: u32, diff --git a/examples/function_router/Cargo.toml b/examples/function_router/Cargo.toml index d2b7cf4dd3c..71ae3871c0c 100644 --- a/examples/function_router/Cargo.toml +++ b/examples/function_router/Cargo.toml @@ -5,13 +5,13 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -lipsum = "0.8.2" +lipsum = "0.9.0" log = "0.4" rand = { version = "0.8", features = ["small_rng"] } yew = { path = "../../packages/yew" } yew-router = { path = "../../packages/yew-router" } serde = { version = "1.0", features = ["derive"] } -gloo-timers = "0.2" +gloo = "0.8" wasm-logger = "0.2" instant = { version = "0.1", features = ["wasm-bindgen"] } once_cell = "1" diff --git a/examples/function_router/README.md b/examples/function_router/README.md index 2a78b7ba732..c2853933425 100644 --- a/examples/function_router/README.md +++ b/examples/function_router/README.md @@ -9,16 +9,6 @@ A blog all about yew. The best way to figure out what this example is about is to just open it up. It's mobile friendly too! -## Running - -While not strictly necessary, this example should be built in release mode: - -```bash -trunk serve --release -``` - -Content generation can take up quite a bit of time in debug builds. - ## Concepts This example involves many different parts, here are just the Yew specific things: @@ -47,3 +37,15 @@ Take a look at [`Route`](src/main.rs) for the implementation. - Detect sub-path from `--public-url` value passed to Trunk. See: thedodd/trunk#51 [`yew-router`]: https://docs.rs/yew-router/latest/yew_router/ + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` + +### Notes + +Content generation can take up quite a bit of time in debug builds. It may be better to run this example in release mode if it is slow. \ No newline at end of file diff --git a/examples/function_router/src/app.rs b/examples/function_router/src/app.rs index 87408e8123b..c6b7c5efb41 100644 --- a/examples/function_router/src/app.rs +++ b/examples/function_router/src/app.rs @@ -12,7 +12,7 @@ use crate::pages::page_not_found::PageNotFound; use crate::pages::post::Post; use crate::pages::post_list::PostList; -#[derive(Routable, PartialEq, Clone, Debug)] +#[derive(Routable, PartialEq, Eq, Clone, Debug)] pub enum Route { #[at("/posts/:id")] Post { id: u32 }, @@ -52,7 +52,7 @@ pub fn App() -> Html { } } -#[derive(Properties, PartialEq, Debug)] +#[derive(Properties, PartialEq, Eq, Debug)] pub struct ServerAppProps { pub url: AttrValue, pub queries: HashMap, diff --git a/examples/function_router/src/components/author_card.rs b/examples/function_router/src/components/author_card.rs index 300d03e1ff5..1e3eaddecc5 100644 --- a/examples/function_router/src/components/author_card.rs +++ b/examples/function_router/src/components/author_card.rs @@ -7,12 +7,12 @@ use crate::content::Author; use crate::generator::Generated; use crate::Route; -#[derive(Clone, Debug, PartialEq, Properties)] +#[derive(Clone, Debug, PartialEq, Eq, Properties)] pub struct Props { pub seed: u32, } -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Eq, Debug)] pub struct AuthorState { pub inner: Author, } @@ -38,14 +38,11 @@ pub fn AuthorCard(props: &Props) -> Html { { let author_dispatcher = author.dispatcher(); - use_effect_with_deps( - move |seed| { - author_dispatcher.dispatch(*seed); + use_effect_with(seed, move |seed| { + author_dispatcher.dispatch(*seed); - || {} - }, - seed, - ); + || {} + }); } let author = &author.inner; diff --git a/examples/function_router/src/components/pagination.rs b/examples/function_router/src/components/pagination.rs index 80989bfb7eb..d9b779e27c6 100644 --- a/examples/function_router/src/components/pagination.rs +++ b/examples/function_router/src/components/pagination.rs @@ -8,12 +8,12 @@ use crate::Route; const ELLIPSIS: &str = "\u{02026}"; -#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] pub struct PageQuery { pub page: u32, } -#[derive(Clone, Debug, PartialEq, Properties)] +#[derive(Clone, Debug, PartialEq, Eq, Properties)] pub struct Props { pub page: u32, pub total_pages: u32, @@ -50,7 +50,7 @@ pub fn RelNavButtons(props: &Props) -> Html { } } -#[derive(Properties, Clone, Debug, PartialEq)] +#[derive(Properties, Clone, Debug, PartialEq, Eq)] pub struct RenderLinksProps { range: Range, len: usize, @@ -88,7 +88,7 @@ pub fn RenderLinks(props: &RenderLinksProps) -> Html { } } -#[derive(Properties, Clone, Debug, PartialEq)] +#[derive(Properties, Clone, Debug, PartialEq, Eq)] pub struct RenderLinkProps { to_page: u32, props: Props, diff --git a/examples/function_router/src/components/post_card.rs b/examples/function_router/src/components/post_card.rs index 02ae0005e05..b05d314e5c2 100644 --- a/examples/function_router/src/components/post_card.rs +++ b/examples/function_router/src/components/post_card.rs @@ -7,12 +7,12 @@ use crate::content::PostMeta; use crate::generator::Generated; use crate::Route; -#[derive(Clone, Debug, PartialEq, Properties)] +#[derive(Clone, Debug, PartialEq, Eq, Properties)] pub struct Props { pub seed: u32, } -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Eq, Debug)] pub struct PostMetaState { inner: PostMeta, } @@ -38,14 +38,11 @@ pub fn PostCard(props: &Props) -> Html { { let post_dispatcher = post.dispatcher(); - use_effect_with_deps( - move |seed| { - post_dispatcher.dispatch(*seed); + use_effect_with(seed, move |seed| { + post_dispatcher.dispatch(*seed); - || {} - }, - seed, - ); + || {} + }); } let post = &post.inner; diff --git a/examples/function_router/src/components/progress_delay.rs b/examples/function_router/src/components/progress_delay.rs index f017aa46723..141b984a0c2 100644 --- a/examples/function_router/src/components/progress_delay.rs +++ b/examples/function_router/src/components/progress_delay.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use gloo_timers::callback::Interval; +use gloo::timers::callback::Interval; use instant::Instant; use yew::prelude::*; @@ -81,29 +81,22 @@ pub fn ProgressDelay(props: &Props) -> Html { { let value = value.clone(); - use_effect_with_deps( - move |_| { - let interval = (duration_ms / RESOLUTION).min(MIN_INTERVAL_MS); - let interval = - Interval::new(interval as u32, move || value.dispatch(ValueAction::Tick)); - - || { - let _interval = interval; - } - }, - (), - ); + use_effect_with((), move |_| { + let interval = (duration_ms / RESOLUTION).min(MIN_INTERVAL_MS); + let interval = Interval::new(interval, move || value.dispatch(ValueAction::Tick)); + + || { + let _interval = interval; + } + }); } { let value = value.clone(); - use_effect_with_deps( - move |props| { - value.dispatch(ValueAction::Props(props.clone())); - || {} - }, - props.clone(), - ); + use_effect_with(props.clone(), move |props| { + value.dispatch(ValueAction::Props(props.clone())); + || {} + }); } let value = &value.value; diff --git a/examples/function_router/src/pages/author.rs b/examples/function_router/src/pages/author.rs index 645f5d4e844..97cb2f4cd25 100644 --- a/examples/function_router/src/pages/author.rs +++ b/examples/function_router/src/pages/author.rs @@ -19,14 +19,11 @@ pub fn Author(props: &Props) -> Html { { let author_dispatcher = author.dispatcher(); - use_effect_with_deps( - move |seed| { - author_dispatcher.dispatch(*seed); + use_effect_with(seed, move |seed| { + author_dispatcher.dispatch(*seed); - || {} - }, - seed, - ); + || {} + }); } let author = &author.inner; diff --git a/examples/function_router/src/pages/post.rs b/examples/function_router/src/pages/post.rs index b5796b89da3..c542c433ae9 100644 --- a/examples/function_router/src/pages/post.rs +++ b/examples/function_router/src/pages/post.rs @@ -12,7 +12,7 @@ pub struct Props { pub seed: u32, } -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Eq, Debug)] pub struct PostState { pub inner: content::Post, } @@ -38,14 +38,11 @@ pub fn Post(props: &Props) -> Html { { let post_dispatcher = post.dispatcher(); - use_effect_with_deps( - move |seed| { - post_dispatcher.dispatch(*seed); - - || {} - }, - seed, - ); + use_effect_with(seed, move |seed| { + post_dispatcher.dispatch(*seed); + + || {} + }); } let post = &post.inner; diff --git a/examples/function_todomvc/Cargo.toml b/examples/function_todomvc/Cargo.toml index 9c9eaa5f3f9..d5304aed422 100644 --- a/examples/function_todomvc/Cargo.toml +++ b/examples/function_todomvc/Cargo.toml @@ -7,8 +7,8 @@ license = "MIT OR Apache-2.0" [dependencies] serde = { version = "1.0", features = ["derive"] } -strum = "0.24" -strum_macros = "0.24" +strum = "0.25" +strum_macros = "0.25" gloo = "0.8" yew = { path = "../../packages/yew", features = ["csr"] } diff --git a/examples/function_todomvc/README.md b/examples/function_todomvc/README.md index c14b26a8b96..7e1a7ca93e3 100644 --- a/examples/function_todomvc/README.md +++ b/examples/function_todomvc/README.md @@ -13,3 +13,11 @@ This is an implementation of [TodoMVC](http://todomvc.com/) for Yew using functi - Use `yew-router` for the hash based routing - Clean up the code + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/function_todomvc/src/components/entry.rs b/examples/function_todomvc/src/components/entry.rs index 147e8e1ad4a..685fc556014 100644 --- a/examples/function_todomvc/src/components/entry.rs +++ b/examples/function_todomvc/src/components/entry.rs @@ -48,6 +48,15 @@ pub fn entry(props: &EntryProps) -> Html { move |_| onremove.emit(id) }; + let onedit = { + let onedit = props.onedit.clone(); + let edit_toggle = edit_toggle.clone(); + move |value| { + edit_toggle.clone().toggle(); + onedit.emit(value) + } + }; + html! {
  • @@ -62,7 +71,7 @@ pub fn entry(props: &EntryProps) -> Html {
    - +
  • } } diff --git a/examples/function_todomvc/src/main.rs b/examples/function_todomvc/src/main.rs index a9eb5c6eb21..3b87a25a608 100644 --- a/examples/function_todomvc/src/main.rs +++ b/examples/function_todomvc/src/main.rs @@ -22,13 +22,10 @@ fn app() -> Html { }); // Effect - use_effect_with_deps( - move |state| { - LocalStorage::set(KEY, &state.clone().entries).expect("failed to set"); - || () - }, - state.clone(), - ); + use_effect_with(state.clone(), move |state| { + LocalStorage::set(KEY, &state.clone().entries).expect("failed to set"); + || () + }); // Callbacks let onremove = { @@ -135,7 +132,7 @@ fn app() -> Html { }) } diff --git a/examples/function_todomvc/src/state.rs b/examples/function_todomvc/src/state.rs index e1ba147805a..1a461b59e08 100644 --- a/examples/function_todomvc/src/state.rs +++ b/examples/function_todomvc/src/state.rs @@ -4,20 +4,20 @@ use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumIter}; use yew::prelude::*; -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct State { pub entries: Vec, pub filter: Filter, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Entry { pub id: usize, pub description: String, pub completed: bool, } -#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Eq, Serialize, Deserialize)] pub enum Filter { All, Active, @@ -42,6 +42,12 @@ impl Filter { } } +impl ToHtml for Filter { + fn to_html(&self) -> Html { + html! {<>{self.to_string()}} + } +} + pub enum Action { Add(String), Edit((usize, String)), diff --git a/examples/futures/Cargo.toml b/examples/futures/Cargo.toml index 697c051e3c4..1ce604b8508 100644 --- a/examples/futures/Cargo.toml +++ b/examples/futures/Cargo.toml @@ -9,8 +9,8 @@ license = "MIT OR Apache-2.0" pulldown-cmark = { version = "0.9", default-features = false } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" -yew = { path = "../../packages/yew", features = ["tokio", "csr"] } -gloo-utils = "0.1" +yew = { path = "../../packages/yew", features = ["csr"] } +gloo = "0.8" [dependencies.web-sys] version = "0.3" diff --git a/examples/futures/README.md b/examples/futures/README.md index bccbcf4e372..a23a9a7effc 100644 --- a/examples/futures/README.md +++ b/examples/futures/README.md @@ -18,3 +18,11 @@ It also contains a Markdown renderer which manually creates `Html` without using - Since this features a Markdown renderer it should be possible to render more than just one document. [`linkfuture`]: https://docs.rs/yewtil/latest/yewtil/future/trait.LinkFuture.html + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/futures/src/main.rs b/examples/futures/src/main.rs index cfd076c71c4..e1e6cb7694c 100644 --- a/examples/futures/src/main.rs +++ b/examples/futures/src/main.rs @@ -49,7 +49,7 @@ async fn fetch_markdown(url: &'static str) -> Result { let request = Request::new_with_str_and_init(url, &opts)?; - let window = gloo_utils::window(); + let window = gloo::utils::window(); let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; let resp: Response = resp_value.dyn_into().unwrap(); diff --git a/examples/futures/src/markdown.rs b/examples/futures/src/markdown.rs index 4bbbc0bedac..5c85b7a55e4 100644 --- a/examples/futures/src/markdown.rs +++ b/examples/futures/src/markdown.rs @@ -54,10 +54,12 @@ pub fn render_markdown(src: &str) -> Html { top = pre; } else if let Tag::Table(aligns) = tag { if let Some(top_children) = top.children_mut() { - for r in top_children.iter_mut() { + for r in top_children.to_vlist_mut().iter_mut() { if let VNode::VTag(ref mut vtag) = r { if let Some(vtag_children) = vtag.children_mut() { - for (i, c) in vtag_children.iter_mut().enumerate() { + for (i, c) in + vtag_children.to_vlist_mut().iter_mut().enumerate() + { if let VNode::VTag(ref mut vtag) = c { match aligns[i] { Alignment::None => {} @@ -73,7 +75,7 @@ pub fn render_markdown(src: &str) -> Html { } } else if let Tag::TableHead = tag { if let Some(top_children) = top.children_mut() { - for c in top_children.iter_mut() { + for c in top_children.to_vlist_mut().iter_mut() { if let VNode::VTag(ref mut vtag) = c { // TODO // vtag.tag = "th".into(); @@ -92,7 +94,7 @@ pub fn render_markdown(src: &str) -> Html { Event::Rule => add_child!(VTag::new("hr").into()), Event::SoftBreak => add_child!(VText::new("\n").into()), Event::HardBreak => add_child!(VTag::new("br").into()), - _ => println!("Unknown event: {:#?}", ev), + _ => println!("Unknown event: {ev:#?}"), } } diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index b5bc69697be..27c053aac5b 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -15,4 +15,4 @@ log = "0.4" rand = "0.8" wasm-logger = "0.2" yew = { path = "../../packages/yew", features = ["csr"] } -gloo-timers = "0.2" +gloo = "0.8" diff --git a/examples/game_of_life/README.md b/examples/game_of_life/README.md index e4c7538a059..1ae9fb9cae5 100644 --- a/examples/game_of_life/README.md +++ b/examples/game_of_life/README.md @@ -17,3 +17,11 @@ trunk serve --release - Uses [`gloo_timer`](https://docs.rs/gloo-timers/latest/gloo_timers/) to automatically step the simulation. - Logs to the console using the [`weblog`](https://crates.io/crates/weblog) crate. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/game_of_life/src/cell.rs b/examples/game_of_life/src/cell.rs index ced3d08706f..a30b3de1523 100644 --- a/examples/game_of_life/src/cell.rs +++ b/examples/game_of_life/src/cell.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum State { Alive, Dead, diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 5699d785c6f..25eb8bcfab5 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -1,5 +1,5 @@ use cell::Cellule; -use gloo_timers::callback::Interval; +use gloo::timers::callback::Interval; use rand::Rng; use yew::html::Scope; use yew::{classes, html, Component, Context, Html}; diff --git a/examples/immutable/Cargo.toml b/examples/immutable/Cargo.toml index c0a549e3ab4..df3a2b41b0f 100644 --- a/examples/immutable/Cargo.toml +++ b/examples/immutable/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -implicit-clone = { version = "0.3", features = ["map"] } +implicit-clone = { version = "0.4", features = ["map"] } wasm-bindgen = "0.2" web-sys = "0.3" yew = { path = "../../packages/yew", features = ["csr"] } diff --git a/examples/immutable/README.md b/examples/immutable/README.md index 6604c64e5c4..c9b15db7d1d 100644 --- a/examples/immutable/README.md +++ b/examples/immutable/README.md @@ -3,3 +3,11 @@ [![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Fimmutable)](https://examples.yew.rs/immutable) This is a technical demonstration for how to use immutables types in Yew. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/inner_html/Cargo.toml b/examples/inner_html/Cargo.toml index adb57cca236..345860ef7bb 100644 --- a/examples/inner_html/Cargo.toml +++ b/examples/inner_html/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" [dependencies] yew = { path = "../../packages/yew", features = ["csr"] } -gloo-utils = "0.1" +gloo = "0.8" [dependencies.web-sys] version = "0.3" diff --git a/examples/inner_html/README.md b/examples/inner_html/README.md index 48a9d498ce2..4b6388dbc19 100644 --- a/examples/inner_html/README.md +++ b/examples/inner_html/README.md @@ -8,3 +8,11 @@ This example renders unescaped HTML by manually handling the DOM element. - Manually creating `Html` without the `html!` macro. - Using `web-sys` to manipulate the DOM. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/inner_html/src/document.html b/examples/inner_html/src/document.html index dbff84fe611..8ccc6707657 100644 --- a/examples/inner_html/src/document.html +++ b/examples/inner_html/src/document.html @@ -4,10 +4,6 @@

    Inline HTML with SVG

    Rust source code. The code queries the DOM, creates a new element, and applies this snippet of HTML to the element's innerHTML.

    -

    - If you look at your browser's console you can see the DOM element (logged to - the console). -

    ) -> Self { - Self { value: 0 } + Self } fn view(&self, _ctx: &Context) -> Html { - let div = gloo_utils::document().create_element("div").unwrap(); - div.set_inner_html(HTML); - // See - console::log_1(&div); - - Html::VRef(div.into()) + Html::from_html_unchecked(HTML.into()) } } diff --git a/examples/js_callback/Cargo.toml b/examples/js_callback/Cargo.toml index 9e3bceb0314..0801247f867 100644 --- a/examples/js_callback/Cargo.toml +++ b/examples/js_callback/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" [dependencies] wasm-bindgen = "0.2" -yew = { path = "../../packages/yew", features = ["csr", "tokio"] } +yew = { path = "../../packages/yew", features = ["csr"] } wasm-bindgen-futures = "0.4" js-sys = "0.3" once_cell = "1" diff --git a/examples/js_callback/README.md b/examples/js_callback/README.md index 8f409b89b0f..926f0fadd03 100644 --- a/examples/js_callback/README.md +++ b/examples/js_callback/README.md @@ -40,3 +40,11 @@ The best way to improve this example would be to incorporate this concept into a - Do something more complex in the Javascript code to demonstrate more of `wasm-bindgen`'s capabilities. - Improve the presentation of the example with CSS. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/js_callback/src/bindings.rs b/examples/js_callback/src/bindings.rs index c99cefaf8fa..06c91617243 100644 --- a/examples/js_callback/src/bindings.rs +++ b/examples/js_callback/src/bindings.rs @@ -1,9 +1,12 @@ use wasm_bindgen::prelude::*; -#[wasm_bindgen] +// https://github.com/rustwasm/wasm-bindgen/issues/3208 +#[wasm_bindgen(inline_js = "export function import2(path) { return import(path); }")] extern "C" { // this should be in js-sys but is not. see https://github.com/rustwasm/wasm-bindgen/issues/2865 - pub fn import(s: &str) -> js_sys::Promise; + // pub fn import(s: &str) -> js_sys::Promise; + #[wasm_bindgen(js_name = "import2")] + pub fn import(name: &str) -> js_sys::Promise; pub type Window; diff --git a/examples/js_callback/src/main.rs b/examples/js_callback/src/main.rs index 6ba10851ce5..ce8ac9c378d 100644 --- a/examples/js_callback/src/main.rs +++ b/examples/js_callback/src/main.rs @@ -11,7 +11,7 @@ static WASM_BINDGEN_SNIPPETS_PATH: OnceCell = OnceCell::new(); #[function_component] fn Important() -> Html { - let msg = use_memo(|_| bindings::hello(), ()); + let msg = use_memo((), |_| bindings::hello()); html! { <>

    {"Important"}

    @@ -24,7 +24,7 @@ fn Important() -> Html { fn use_do_bye() -> SuspensionResult { let path = WASM_BINDGEN_SNIPPETS_PATH .get() - .map(|path| format!("{}/js/unimp.js", path)) + .map(|path| format!("{path}/js/unimp.js")) .unwrap(); let s = use_future(|| async move { let promise = bindings::import(&path); diff --git a/examples/keyed_list/Cargo.toml b/examples/keyed_list/Cargo.toml index 0d05c02af50..2dd60c5789d 100644 --- a/examples/keyed_list/Cargo.toml +++ b/examples/keyed_list/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -fake = "2.5.0" +fake = "2.6.1" getrandom = { version = "0.2", features = ["js"] } instant = { version = "0.1", features = ["wasm-bindgen"] } log = "0.4" diff --git a/examples/keyed_list/README.md b/examples/keyed_list/README.md index 6ff3b52ee2b..f1258bcac5a 100644 --- a/examples/keyed_list/README.md +++ b/examples/keyed_list/README.md @@ -4,13 +4,9 @@ This example consists of a list which can be manipulated in various ways. -## Running - -Because this example is a performance demonstration you should use it with the `--release` flag: +### Notes -```bash -trunk serve --release -``` +If you would like to view this example as a performance demonstation, run this example in `release` mode. ## Concepts @@ -21,3 +17,11 @@ Demonstrates how using keyed elements improves the performance of comparing chan - Improve the name and address generation so that they don't look like gibberish - Show the time it took to update the DOM on the page instead of just the console - Improve the presentation of the example with CSS + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/keyed_list/src/person.rs b/examples/keyed_list/src/person.rs index c7eda7e0cbf..05e89e41399 100644 --- a/examples/keyed_list/src/person.rs +++ b/examples/keyed_list/src/person.rs @@ -23,7 +23,7 @@ impl PersonInfo { let city = CityName(EN).fake::(); let street = StreetName(EN).fake::(); - Rc::from(format!("{} {} St., {}, {}", no, street, city, state).as_str()) + Rc::from(format!("{no} {street} St., {city}, {state}").as_str()) }; Self { diff --git a/examples/mount_point/Cargo.toml b/examples/mount_point/Cargo.toml index bb55ead5744..cacd672df83 100644 --- a/examples/mount_point/Cargo.toml +++ b/examples/mount_point/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [dependencies] wasm-bindgen = "0.2" yew = { path = "../../packages/yew", features = ["csr"] } -gloo-utils = "0.1" +gloo = "0.8" [dependencies.web-sys] version = "0.3" diff --git a/examples/mount_point/README.md b/examples/mount_point/README.md index 88121a49917..75ddf585b8a 100644 --- a/examples/mount_point/README.md +++ b/examples/mount_point/README.md @@ -14,3 +14,11 @@ which contains the contents of the input box in reverse. This example is very similar to [`two_apps`](../two_apps). The two should be merged into a single example. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/mount_point/src/main.rs b/examples/mount_point/src/main.rs index 3354caebe55..1675b0355f7 100644 --- a/examples/mount_point/src/main.rs +++ b/examples/mount_point/src/main.rs @@ -60,7 +60,7 @@ fn create_canvas(document: &Document) -> HtmlCanvasElement { } fn main() { - let document = gloo_utils::document(); + let document = gloo::utils::document(); let body = document.query_selector("body").unwrap().unwrap(); let canvas = create_canvas(&document); diff --git a/examples/nested_list/README.md b/examples/nested_list/README.md index e98963d9a70..999716e0443 100644 --- a/examples/nested_list/README.md +++ b/examples/nested_list/README.md @@ -13,3 +13,11 @@ This example shows a nested list and displays which item was last hovered. - `ListItem` Component has a `hide` prop which is currently only used statically. It should be possible to make the hidden items visible by pressing a button. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/nested_list/src/list.rs b/examples/nested_list/src/list.rs index 66d999cce9d..53c2101945e 100644 --- a/examples/nested_list/src/list.rs +++ b/examples/nested_list/src/list.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use yew::html::{ChildrenRenderer, NodeRef}; +use yew::html::ChildrenRenderer; use yew::prelude::*; use yew::virtual_dom::{VChild, VComp}; @@ -46,10 +46,8 @@ where impl From for Html { fn from(variant: ListVariant) -> Html { match variant.props { - Variants::Header(props) => { - VComp::new::(props, NodeRef::default(), None).into() - } - Variants::Item(props) => VComp::new::(props, NodeRef::default(), None).into(), + Variants::Header(props) => VComp::new::(props, None).into(), + Variants::Item(props) => VComp::new::(props, None).into(), } } } diff --git a/examples/nested_list/src/main.rs b/examples/nested_list/src/main.rs index 881c0471b17..3f8a93def78 100644 --- a/examples/nested_list/src/main.rs +++ b/examples/nested_list/src/main.rs @@ -8,7 +8,8 @@ use std::fmt; use std::ops::Deref; use std::rc::Rc; -use yew::html::{Component, ImplicitClone, Scope}; +use yew::html::{ImplicitClone, Scope}; +use yew::prelude::*; pub struct WeakComponentLink(Rc>>>); @@ -62,6 +63,12 @@ impl fmt::Display for Hovered { } } +impl ToHtml for Hovered { + fn to_html(&self) -> yew::Html { + html! {<>{self.to_string()}} + } +} + fn main() { wasm_logger::init(wasm_logger::Config::default()); yew::Renderer::::new().render(); diff --git a/examples/node_refs/README.md b/examples/node_refs/README.md index d76a14f1d0b..204a9b86296 100644 --- a/examples/node_refs/README.md +++ b/examples/node_refs/README.md @@ -8,3 +8,11 @@ This example shows two input fields which are automatically focused when hovered The example uses [Refs](https://yew.rs/docs/concepts/components/refs/) to manipulate the underlying DOM element directly. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/node_refs/src/input.rs b/examples/node_refs/src/input.rs index 26c8b3f95c8..e6f749980dd 100644 --- a/examples/node_refs/src/input.rs +++ b/examples/node_refs/src/input.rs @@ -8,6 +8,7 @@ pub enum Msg { pub struct Props { pub on_hover: Callback<()>, pub placeholder: AttrValue, + pub input_ref: NodeRef, } pub struct InputComponent; @@ -33,6 +34,7 @@ impl Component for InputComponent { let placeholder = ctx.props().placeholder.clone(); html! { { "Email" } diff --git a/examples/password_strength/Cargo.toml b/examples/password_strength/Cargo.toml index 8f6acf66e48..2e832429346 100644 --- a/examples/password_strength/Cargo.toml +++ b/examples/password_strength/Cargo.toml @@ -7,9 +7,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] yew = { path = "../../packages/yew", features = ["csr"] } -zxcvbn = "2.2.1" -time = "=0.3.9" # remove version pin with msrv 1.60 -js-sys = "0.3.46" +zxcvbn = "2.2.2" +time = "0.3.22" +js-sys = "0.3.64" web-sys = { version = "0.3", features = ["Event","EventTarget","InputEvent"] } wasm-bindgen = "0.2" chrono = { version = "0.4", features = ["wasmbind"] } diff --git a/examples/password_strength/README.md b/examples/password_strength/README.md index c5becc98fc0..37f25e3f43f 100644 --- a/examples/password_strength/README.md +++ b/examples/password_strength/README.md @@ -4,13 +4,9 @@ A password strength estimator implemented in Yew. -## Running - -You should run this example with the `--release` flag: +### Notes -```bash -trunk serve --release -``` +If this example is a bit slow, you should try running it with the `release` profile. ## Concepts @@ -19,3 +15,11 @@ This example - makes use of controlled components. - extracts new value from `InputEvent` - calls out to `js_sys` to invoke a foreign function, `Math.random()` + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/password_strength/src/app.rs b/examples/password_strength/src/app.rs index b64f3e2829f..3ffbcfe58ae 100644 --- a/examples/password_strength/src/app.rs +++ b/examples/password_strength/src/app.rs @@ -34,7 +34,7 @@ impl App { 3 => "Good", _ => "Great!", }; - format!("Complexity = {}", estimate_text) + format!("Complexity = {estimate_text}") } } diff --git a/examples/password_strength/src/password.rs b/examples/password_strength/src/password.rs index 0f32a5ffacf..dec6820ed76 100644 --- a/examples/password_strength/src/password.rs +++ b/examples/password_strength/src/password.rs @@ -2,13 +2,13 @@ const PASSWORD_LEN: i32 = 17; pub fn generate_password() -> String { let mut space: Vec = vec![]; - for c in 'a'..'z' { + for c in 'a'..='z' { space.push(c); } - for c in 'A'..'Z' { + for c in 'A'..='Z' { space.push(c); } - for c in '0'..'9' { + for c in '0'..='9' { space.push(c); } space.extend( diff --git a/examples/portals/Cargo.toml b/examples/portals/Cargo.toml index 308c2f8db5b..00aa66823d9 100644 --- a/examples/portals/Cargo.toml +++ b/examples/portals/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" [dependencies] yew = { path = "../../packages/yew", features = ["csr"] } -gloo-utils = "0.1" +gloo = "0.8" wasm-bindgen = "0.2" [dependencies.web-sys] diff --git a/examples/portals/README.md b/examples/portals/README.md index c22deba50fa..17247825287 100644 --- a/examples/portals/README.md +++ b/examples/portals/README.md @@ -8,3 +8,11 @@ This example renders elements into out-of-tree nodes with the help of portals. - Manually creating `Html` without the `html!` macro. - Using `web-sys` to manipulate the DOM. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs index da751f49c28..6119397178b 100644 --- a/examples/portals/src/main.rs +++ b/examples/portals/src/main.rs @@ -1,11 +1,11 @@ use wasm_bindgen::JsCast; use web_sys::{Element, ShadowRootInit, ShadowRootMode}; -use yew::{create_portal, html, Children, Component, Context, Html, NodeRef, Properties}; +use yew::{create_portal, html, Component, Context, Html, NodeRef, Properties}; #[derive(Properties, PartialEq)] pub struct ShadowDOMProps { #[prop_or_default] - pub children: Children, + pub children: Html, } pub struct ShadowDOMHost { @@ -33,7 +33,7 @@ impl Component for ShadowDOMHost { .unchecked_into::() .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open)) .expect("installing shadow root succeeds"); - let inner_host = gloo_utils::document() + let inner_host = gloo::utils::document() .create_element("div") .expect("can create inner wrapper"); shadow_root @@ -50,12 +50,7 @@ impl Component for ShadowDOMHost { fn view(&self, ctx: &Context) -> Html { let contents = if let Some(ref inner_host) = self.inner_host { - create_portal( - html! { - {for ctx.props().children.iter()} - }, - inner_host.clone(), - ) + create_portal(ctx.props().children.clone(), inner_host.clone()) } else { html! { <> } }; @@ -82,7 +77,7 @@ impl Component for App { type Properties = (); fn create(_ctx: &Context) -> Self { - let document_head = gloo_utils::document() + let document_head = gloo::utils::document() .head() .expect("head element to be present"); let title_element = document_head diff --git a/examples/router/Cargo.toml b/examples/router/Cargo.toml index 54bba23c24f..f6b8e6d9bca 100644 --- a/examples/router/Cargo.toml +++ b/examples/router/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT OR Apache-2.0" [dependencies] instant = { version = "0.1", features = ["wasm-bindgen"] } -lipsum = "0.8.2" +lipsum = "0.9.0" log = "0.4" getrandom = { version = "0.2", features = ["js"] } rand = { version = "0.8", features = ["small_rng"] } @@ -15,4 +15,4 @@ yew = { path = "../../packages/yew", features = ["csr"] } yew-router = { path = "../../packages/yew-router" } serde = { version = "1.0", features = ["derive"] } once_cell = "1" -gloo-timers = "0.2" +gloo = "0.8" diff --git a/examples/router/README.md b/examples/router/README.md index f0e353caab7..de98f54d193 100644 --- a/examples/router/README.md +++ b/examples/router/README.md @@ -6,16 +6,6 @@ A blog all about yew. The best way to figure out what this example is about is to just open it up. It's mobile friendly too! -## Running - -While not strictly necessary, this example should be built in release mode: - -```bash -trunk serve --release -``` - -Content generation can take up quite a bit of time in debug builds. - ## Concepts This example involves many different parts, here are just the Yew specific things: @@ -44,3 +34,15 @@ Take a look at [`Route`](src/main.rs) for the implementation. - Detect sub-path from `--public-url` value passed to Trunk. See: thedodd/trunk#51 [`yew-router`]: https://docs.rs/yew-router/latest/yew_router/ + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` + +### Notes + +Content generation can take up quite a bit of time in debug builds. If it is too slow, you should try running with the `release` profile. \ No newline at end of file diff --git a/examples/router/src/components/author_card.rs b/examples/router/src/components/author_card.rs index 74e0ecbb1ca..090953a3f0f 100644 --- a/examples/router/src/components/author_card.rs +++ b/examples/router/src/components/author_card.rs @@ -5,7 +5,7 @@ use crate::content::Author; use crate::generator::Generated; use crate::Route; -#[derive(Clone, Debug, PartialEq, Properties)] +#[derive(Clone, Debug, PartialEq, Eq, Properties)] pub struct Props { pub seed: u64, } @@ -23,7 +23,7 @@ impl Component for AuthorCard { } } - fn changed(&mut self, ctx: &Context) -> bool { + fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { self.author = Author::generate_from_seed(ctx.props().seed); true } diff --git a/examples/router/src/components/pagination.rs b/examples/router/src/components/pagination.rs index 76301d00ef9..2a51636ca9e 100644 --- a/examples/router/src/components/pagination.rs +++ b/examples/router/src/components/pagination.rs @@ -6,12 +6,12 @@ use crate::Route; const ELLIPSIS: &str = "\u{02026}"; -#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] pub struct PageQuery { pub page: u64, } -#[derive(Clone, Debug, PartialEq, Properties)] +#[derive(Clone, Debug, PartialEq, Eq, Properties)] pub struct Props { pub page: u64, pub total_pages: u64, diff --git a/examples/router/src/components/post_card.rs b/examples/router/src/components/post_card.rs index a29a5506438..f24ca6ae9a0 100644 --- a/examples/router/src/components/post_card.rs +++ b/examples/router/src/components/post_card.rs @@ -5,7 +5,7 @@ use crate::content::PostMeta; use crate::generator::Generated; use crate::Route; -#[derive(Clone, Debug, PartialEq, Properties)] +#[derive(Clone, Debug, PartialEq, Eq, Properties)] pub struct Props { pub seed: u64, } @@ -23,7 +23,7 @@ impl Component for PostCard { } } - fn changed(&mut self, ctx: &Context) -> bool { + fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { self.post = PostMeta::generate_from_seed(ctx.props().seed); true } diff --git a/examples/router/src/components/progress_delay.rs b/examples/router/src/components/progress_delay.rs index 888887674c9..01d9a576f0c 100644 --- a/examples/router/src/components/progress_delay.rs +++ b/examples/router/src/components/progress_delay.rs @@ -1,4 +1,4 @@ -use gloo_timers::callback::Interval; +use gloo::timers::callback::Interval; use instant::Instant; use yew::prelude::*; diff --git a/examples/router/src/main.rs b/examples/router/src/main.rs index 70ecf9254e1..11fb9ef2d74 100644 --- a/examples/router/src/main.rs +++ b/examples/router/src/main.rs @@ -13,7 +13,7 @@ use pages::post::Post; use pages::post_list::PostList; use yew::html::Scope; -#[derive(Routable, PartialEq, Clone, Debug)] +#[derive(Routable, PartialEq, Eq, Clone, Debug)] pub enum Route { #[at("/posts/:id")] Post { id: u64 }, diff --git a/examples/router/src/pages/author.rs b/examples/router/src/pages/author.rs index 02843274900..af13c95fcda 100644 --- a/examples/router/src/pages/author.rs +++ b/examples/router/src/pages/author.rs @@ -21,7 +21,7 @@ impl Component for Author { } } - fn changed(&mut self, ctx: &Context) -> bool { + fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { self.author = content::Author::generate_from_seed(ctx.props().seed); true } diff --git a/examples/router/src/pages/post.rs b/examples/router/src/pages/post.rs index 92f716e3853..6d29957f470 100644 --- a/examples/router/src/pages/post.rs +++ b/examples/router/src/pages/post.rs @@ -23,7 +23,7 @@ impl Component for Post { } } - fn changed(&mut self, ctx: &Context) -> bool { + fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { self.post = content::Post::generate_from_seed(ctx.props().seed); true } diff --git a/examples/simple_ssr/Cargo.toml b/examples/simple_ssr/Cargo.toml index 1f796fdba61..0a27c870458 100644 --- a/examples/simple_ssr/Cargo.toml +++ b/examples/simple_ssr/Cargo.toml @@ -4,14 +4,21 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "simple_ssr_hydrate" +required-features = ["hydration"] + +[[bin]] +name = "simple_ssr_server" +required-features = ["ssr"] [dependencies] yew = { path = "../../packages/yew" } -reqwest = { version = "0.11.8", features = ["json"] } -serde = { version = "1.0.132", features = ["derive"] } -uuid = { version = "1.0.0", features = ["serde"] } +reqwest = { version = "0.11.18", features = ["json"] } +serde = { version = "1.0.164", features = ["derive"] } +uuid = { version = "1.4.0", features = ["serde"] } futures = "0.3" -bytes = "1.0" +bytes = "1.4" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4" @@ -19,10 +26,10 @@ wasm-logger = "0.2" log = "0.4" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { version = "1.15.0", features = ["full"] } +tokio = { version = "1.29.0", features = ["full"] } warp = "0.3" -clap = { version = "3.1.7", features = ["derive"] } +clap = { version = "4", features = ["derive"] } [features] hydration = ["yew/hydration"] -ssr = ["yew/ssr", "yew/tokio"] +ssr = ["yew/ssr"] diff --git a/examples/simple_ssr/README.md b/examples/simple_ssr/README.md index 41105b934dd..91d8b4638fd 100644 --- a/examples/simple_ssr/README.md +++ b/examples/simple_ssr/README.md @@ -2,16 +2,12 @@ This example demonstrates server-side rendering. -# How to run this example +# Running -1. build hydration bundle +1. Build hydration bundle -`trunk build examples/simple_ssr/index.html` +`trunk build index.html` 2. Run the server -`cargo run --features=ssr --bin simple_ssr_server -- --dir examples/simple_ssr/dist` - -3. Open Browser - -Navigate to http://localhost:8080/ to view results. +`cargo run --features=ssr --bin simple_ssr_server -- --dir dist` \ No newline at end of file diff --git a/examples/simple_ssr/src/bin/simple_ssr_server.rs b/examples/simple_ssr/src/bin/simple_ssr_server.rs index a6127c68bb9..18a7c26709f 100644 --- a/examples/simple_ssr/src/bin/simple_ssr_server.rs +++ b/examples/simple_ssr/src/bin/simple_ssr_server.rs @@ -13,7 +13,7 @@ type BoxedError = Box; #[derive(Parser, Debug)] struct Opt { /// the "dist" created by trunk directory to be served for hydration. - #[structopt(short, long, parse(from_os_str))] + #[structopt(short, long)] dir: PathBuf, } @@ -25,7 +25,7 @@ async fn render( Box::new( stream::once(async move { index_html_before }) - .chain(renderer.render_stream().await) + .chain(renderer.render_stream()) .chain(stream::once(async move { index_html_after })) .map(|m| Result::<_, BoxedError>::Ok(m.into())), ) @@ -54,6 +54,5 @@ async fn main() { let routes = html.or(warp::fs::dir(opts.dir)); println!("You can view the website at: http://localhost:8080/"); - warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; } diff --git a/examples/ssr_router/Cargo.toml b/examples/ssr_router/Cargo.toml index 8c64e440c2a..a3300cc4e1d 100644 --- a/examples/ssr_router/Cargo.toml +++ b/examples/ssr_router/Cargo.toml @@ -5,23 +5,33 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "ssr_router_hydrate" +required-features = ["hydration"] + +[[bin]] +name = "ssr_router_server" +required-features = ["ssr"] + [dependencies] yew = { path = "../../packages/yew" } function_router = { path = "../function_router" } log = "0.4" -futures = "0.3" +futures = { version = "0.3", features = ["std"], default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4" wasm-logger = "0.2" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { version = "1.15.0", features = ["full"] } -axum = "0.5" +tokio = { version = "1.29.0", features = ["full"] } +axum = "0.6" tower = { version = "0.4", features = ["make"] } tower-http = { version = "0.3", features = ["fs"] } -env_logger = "0.9" -clap = { version = "3.1.7", features = ["derive"] } +env_logger = "0.10" +clap = { version = "4", features = ["derive"] } +hyper = { version = "0.14", features = ["server", "http1"] } +jemallocator = "0.5" [features] ssr = ["yew/ssr"] diff --git a/examples/ssr_router/README.md b/examples/ssr_router/README.md index 013c3b63c4a..c5120ecd19c 100644 --- a/examples/ssr_router/README.md +++ b/examples/ssr_router/README.md @@ -8,12 +8,8 @@ of the function router example. 1. Build Hydration Bundle -`trunk build examples/ssr_router/index.html` +`trunk build index.html` 2. Run the server -`cargo run --features=ssr --bin ssr_router_server -- --dir examples/ssr_router/dist` - -3. Open Browser - -Navigate to http://localhost:8080/ to view results. +`cargo run --features=ssr --bin ssr_router_server -- --dir dist` diff --git a/examples/ssr_router/src/bin/ssr_router_server.rs b/examples/ssr_router/src/bin/ssr_router_server.rs index 5aa8c1c86ea..2aa83880da4 100644 --- a/examples/ssr_router/src/bin/ssr_router_server.rs +++ b/examples/ssr_router/src/bin/ssr_router_server.rs @@ -1,35 +1,42 @@ use std::collections::HashMap; use std::convert::Infallible; +use std::future::Future; use std::path::PathBuf; -use axum::body::{Body, StreamBody}; +use axum::body::StreamBody; use axum::error_handling::HandleError; -use axum::extract::Query; -use axum::handler::Handler; -use axum::http::{Request, StatusCode}; +use axum::extract::{Query, State}; +use axum::handler::HandlerWithoutStateExt; +use axum::http::{StatusCode, Uri}; use axum::response::IntoResponse; use axum::routing::get; -use axum::{Extension, Router}; +use axum::Router; use clap::Parser; use function_router::{ServerApp, ServerAppProps}; use futures::stream::{self, StreamExt}; +use hyper::server::Server; use tower::ServiceExt; use tower_http::services::ServeDir; +use yew::platform::Runtime; + +// We use jemalloc as it produces better performance. +#[global_allocator] +static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; /// A basic example #[derive(Parser, Debug)] struct Opt { /// the "dist" created by trunk directory to be served for hydration. - #[clap(short, long, parse(from_os_str))] + #[clap(short, long)] dir: PathBuf, } async fn render( - Extension((index_html_before, index_html_after)): Extension<(String, String)>, - url: Request, + url: Uri, Query(queries): Query>, + State((index_html_before, index_html_after)): State<(String, String)>, ) -> impl IntoResponse { - let url = url.uri().to_string(); + let url = url.to_string(); let renderer = yew::ServerRenderer::::with_props(move || ServerAppProps { url: url.into(), @@ -38,14 +45,38 @@ async fn render( StreamBody::new( stream::once(async move { index_html_before }) - .chain(renderer.render_stream().await) + .chain(renderer.render_stream()) .chain(stream::once(async move { index_html_after })) .map(Result::<_, Infallible>::Ok), ) } +// An executor to process requests on the Yew runtime. +// +// By spawning requests on the Yew runtime, +// it processes request on the same thread as the rendering task. +// +// This increases performance in some environments (e.g.: in VM). +#[derive(Clone, Default)] +struct Executor { + inner: Runtime, +} + +impl hyper::rt::Executor for Executor +where + F: Future + Send + 'static, +{ + fn execute(&self, fut: F) { + self.inner.spawn_pinned(move || async move { + fut.await; + }); + } +} + #[tokio::main] async fn main() { + let exec = Executor::default(); + env_logger::init(); let opts = Opt::parse(); @@ -63,30 +94,26 @@ async fn main() { let handle_error = |e| async move { ( StatusCode::INTERNAL_SERVER_ERROR, - format!("error occurred: {}", e), + format!("error occurred: {e}"), ) }; - let app = Router::new() - .route("/api/test", get(|| async move { "Hello World" })) - .fallback(HandleError::new( - ServeDir::new(opts.dir) - .append_index_html_on_directories(false) - .fallback( - render - .layer(Extension(( - index_html_before.clone(), - index_html_after.clone(), - ))) - .into_service() - .map_err(|err| -> std::io::Error { match err {} }), - ), - handle_error, - )); + let app = Router::new().fallback_service(HandleError::new( + ServeDir::new(opts.dir) + .append_index_html_on_directories(false) + .fallback( + get(render) + .with_state((index_html_before.clone(), index_html_after.clone())) + .into_service() + .map_err(|err| -> std::io::Error { match err {} }), + ), + handle_error, + )); println!("You can view the website at: http://localhost:8080/"); - axum::Server::bind(&"0.0.0.0:8080".parse().unwrap()) + Server::bind(&"127.0.0.1:8080".parse().unwrap()) + .executor(exec) .serve(app.into_make_service()) .await .unwrap(); diff --git a/examples/suspense/Cargo.toml b/examples/suspense/Cargo.toml index 67cdd00b790..389bd4b19ba 100644 --- a/examples/suspense/Cargo.toml +++ b/examples/suspense/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [dependencies] yew = { path = "../../packages/yew", features = ["csr"] } -gloo-timers = { version = "0.2.2", features = ["futures"] } +gloo = { version = "0.8", features = ["futures"] } wasm-bindgen-futures = "0.4" wasm-bindgen = "0.2" diff --git a/examples/suspense/README.md b/examples/suspense/README.md index 137b848a254..96f2ef8881a 100644 --- a/examples/suspense/README.md +++ b/examples/suspense/README.md @@ -6,5 +6,13 @@ This is an example that demonstrates `` support. ## Concepts -This example shows that how `` works in Yew and how you can +This example shows how `` works in Yew and how you can create hooks that utilises suspense. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/suspense/src/use_sleep.rs b/examples/suspense/src/use_sleep.rs index 98bf4e60f12..8bf90431260 100644 --- a/examples/suspense/src/use_sleep.rs +++ b/examples/suspense/src/use_sleep.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use std::time::Duration; -use gloo_timers::future::sleep; +use gloo::timers::future::sleep; use yew::prelude::*; use yew::suspense::{Suspension, SuspensionResult}; diff --git a/examples/timer/README.md b/examples/timer/README.md index b6146afa66e..683c0deb0d4 100644 --- a/examples/timer/README.md +++ b/examples/timer/README.md @@ -13,3 +13,11 @@ more advanced web console features. ## Improvements - Apply the concept to something more fun than just a dry technical demonstration + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/timer/src/main.rs b/examples/timer/src/main.rs index 00d0df4504b..073c38d0ca3 100644 --- a/examples/timer/src/main.rs +++ b/examples/timer/src/main.rs @@ -60,7 +60,7 @@ impl Component for App { Msg::StartTimeout => { let handle = { let link = ctx.link().clone(); - Timeout::new(3, move || link.send_message(Msg::Done)) + Timeout::new(3000, move || link.send_message(Msg::Done)) }; self.timeout = Some(handle); @@ -75,7 +75,7 @@ impl Component for App { Msg::StartInterval => { let handle = { let link = ctx.link().clone(); - Interval::new(1, move || link.send_message(Msg::Tick)) + Interval::new(1000, move || link.send_message(Msg::Tick)) }; self.interval = Some(handle); diff --git a/examples/timer_functional/Cargo.toml b/examples/timer_functional/Cargo.toml new file mode 100644 index 00000000000..3d0c36b56de --- /dev/null +++ b/examples/timer_functional/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "timer_functional" +version = "0.1.0" +authors = ["Zahash "] +edition = "2021" +license = "MIT OR Apache-2.0" + +[dependencies] +gloo = "0.8.1" +js-sys = "0.3.64" +yew = { path = "../../packages/yew", features = ["csr"] } diff --git a/examples/timer_functional/README.md b/examples/timer_functional/README.md new file mode 100644 index 00000000000..78b605bbc65 --- /dev/null +++ b/examples/timer_functional/README.md @@ -0,0 +1,17 @@ +# Timer Example + +[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Ftimer_functional)](https://examples.yew.rs/timer_functional) + +This is a technical demonstration for how to use timeouts and intervals. + +## Concepts + +The example mainly demonstrates the use of [`gloo_timer`](https://docs.rs/gloo-timers/ ) but does so using only [`function components`](https://yew.rs/docs/concepts/function-components). + +## Running + +Run a debug version of this application: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/timer_functional/index.html b/examples/timer_functional/index.html new file mode 100644 index 00000000000..b3942ccfcc2 --- /dev/null +++ b/examples/timer_functional/index.html @@ -0,0 +1,14 @@ + + + + + + Yew • Timer + + + + + + + + diff --git a/examples/timer_functional/index.scss b/examples/timer_functional/index.scss new file mode 100644 index 00000000000..d801a262079 --- /dev/null +++ b/examples/timer_functional/index.scss @@ -0,0 +1,29 @@ +$font-stack: Roboto, sans-serif; +$primary-color: #008f53; + +body { + font: 100% $font-stack; + color: white; + background-color: $primary-color; +} + +#buttons { + text-align: right; +} + +#wrapper { + overflow: hidden; + width: 100%; +} + +#time { + text-align: center; + font-size: 17vh; + padding: 15% 0; + float: left; +} + +#messages { + float: right; +} + diff --git a/examples/timer_functional/src/main.rs b/examples/timer_functional/src/main.rs new file mode 100644 index 00000000000..30da755dd0b --- /dev/null +++ b/examples/timer_functional/src/main.rs @@ -0,0 +1,167 @@ +use std::rc::Rc; + +use gloo::timers::callback::{Interval, Timeout}; +use yew::prelude::*; + +fn get_current_time() -> String { + let date = js_sys::Date::new_0(); + String::from(date.to_locale_time_string("en-US")) +} + +enum TimerAction { + Add(&'static str), + Cancel, + SetInterval(Interval), + SetTimeout(Timeout), + TimeoutDone, +} + +#[derive(Clone, Debug)] +struct TimerState { + messages: Vec<&'static str>, + interval_handle: Option>, + timeout_handle: Option>, +} + +impl PartialEq for TimerState { + fn eq(&self, other: &Self) -> bool { + self.messages == other.messages + && self.interval_handle.is_some() == other.interval_handle.is_some() + } +} + +impl Reducible for TimerState { + type Action = TimerAction; + + fn reduce(self: Rc, action: TimerAction) -> Rc { + match action { + TimerAction::Add(message) => { + let mut messages = self.messages.clone(); + messages.push(message); + Rc::new(TimerState { + messages, + interval_handle: self.interval_handle.clone(), + timeout_handle: self.timeout_handle.clone(), + }) + } + TimerAction::SetInterval(t) => Rc::new(TimerState { + messages: vec!["Interval started!"], + interval_handle: Some(Rc::from(t)), + timeout_handle: self.timeout_handle.clone(), + }), + TimerAction::SetTimeout(t) => Rc::new(TimerState { + messages: vec!["Timer started!!"], + interval_handle: self.interval_handle.clone(), + timeout_handle: Some(Rc::from(t)), + }), + TimerAction::TimeoutDone => { + let mut messages = self.messages.clone(); + messages.push("Done!"); + Rc::new(TimerState { + messages, + interval_handle: self.interval_handle.clone(), + timeout_handle: None, + }) + } + TimerAction::Cancel => { + let mut messages = self.messages.clone(); + messages.push("Canceled!"); + Rc::new(TimerState { + messages, + interval_handle: None, + timeout_handle: None, + }) + } + } + } +} + +#[function_component(Clock)] +fn clock() -> Html { + let time = use_state(get_current_time); + + { + let time = time.clone(); + use_effect_with((), |_| { + Interval::new(1000, move || time.set(get_current_time())).forget(); + }); + } + html!( +
    { time.as_str() }
    + ) +} + +#[function_component] +fn App() -> Html { + let state = use_reducer(|| TimerState { + messages: Vec::new(), + interval_handle: None, + timeout_handle: None, + }); + + let mut key = 0; + let messages: Html = state + .messages + .iter() + .map(|message| { + key += 1; + html! {

    { message }

    } + }) + .collect(); + + let has_job = state.interval_handle.is_some() || state.timeout_handle.is_some(); + + let on_add_timeout = { + let state = state.clone(); + + Callback::from(move |_: MouseEvent| { + let timeout_state = state.clone(); + let message_state = state.clone(); + let t = Timeout::new(3000, move || { + message_state.dispatch(TimerAction::TimeoutDone); + }); + + timeout_state.dispatch(TimerAction::SetTimeout(t)); + }) + }; + + let on_add_interval = { + let state = state.clone(); + + Callback::from(move |_: MouseEvent| { + let interval_state = state.clone(); + let message_state = state.clone(); + let i = Interval::new(1000, move || { + message_state.dispatch(TimerAction::Add("Tick..")); + }); + + interval_state.dispatch(TimerAction::SetInterval(i)); + }) + }; + + let on_cancel = { + Callback::from(move |_: MouseEvent| { + state.dispatch(TimerAction::Cancel); + }) + }; + + html!( + <> +
    + + + +
    +
    + +
    + { messages } +
    +
    + + ) +} + +fn main() { + yew::Renderer::::new().render(); +} diff --git a/examples/todomvc/Cargo.toml b/examples/todomvc/Cargo.toml index 3ed9135acae..10f64d9c36d 100644 --- a/examples/todomvc/Cargo.toml +++ b/examples/todomvc/Cargo.toml @@ -6,8 +6,8 @@ edition = "2021" license = "MIT OR Apache-2.0" [dependencies] -strum = "0.24" -strum_macros = "0.24" +strum = "0.25" +strum_macros = "0.25" serde = "1" serde_derive = "1" yew = { path = "../../packages/yew", features = ["csr"] } diff --git a/examples/todomvc/README.md b/examples/todomvc/README.md index 77c1edd217d..64304df4fc9 100644 --- a/examples/todomvc/README.md +++ b/examples/todomvc/README.md @@ -18,3 +18,11 @@ including: all entries, entered text and chosen filter. - Clean up the code [`refs`]: https://yew.rs/docs/concepts/components/refs/ + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs index 1fa5e93a830..2024805290d 100644 --- a/examples/todomvc/src/main.rs +++ b/examples/todomvc/src/main.rs @@ -45,9 +45,10 @@ impl Component for App { fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { match msg { Msg::Add(description) => { + let description = description.trim(); if !description.is_empty() { let entry = Entry { - description: description.trim().to_string(), + description: description.to_string(), completed: false, editing: false, }; @@ -65,7 +66,14 @@ impl Component for App { self.state.filter = filter; } Msg::ToggleEdit(idx) => { - self.state.edit_value = self.state.entries[idx].description.clone(); + let entry = self + .state + .entries + .iter() + .filter(|e| self.state.filter.fits(e)) + .nth(idx) + .unwrap(); + self.state.edit_value = entry.description.clone(); self.state.clear_all_edit(); self.state.toggle_edit(idx); } diff --git a/examples/todomvc/src/state.rs b/examples/todomvc/src/state.rs index 6d960c3032c..251c4925e62 100644 --- a/examples/todomvc/src/state.rs +++ b/examples/todomvc/src/state.rs @@ -1,5 +1,6 @@ use serde_derive::{Deserialize, Serialize}; use strum_macros::{Display, EnumIter}; +use yew::prelude::*; #[derive(Debug, Serialize, Deserialize)] pub struct State { @@ -117,7 +118,7 @@ pub struct Entry { pub editing: bool, } -#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Serialize, Deserialize, Eq)] pub enum Filter { All, Active, @@ -140,3 +141,9 @@ impl Filter { } } } + +impl ToHtml for Filter { + fn to_html(&self) -> yew::Html { + html! { <>{self.to_string()} } + } +} diff --git a/examples/two_apps/Cargo.toml b/examples/two_apps/Cargo.toml index 509795052ff..9797f473743 100644 --- a/examples/two_apps/Cargo.toml +++ b/examples/two_apps/Cargo.toml @@ -7,4 +7,4 @@ license = "MIT OR Apache-2.0" [dependencies] yew = { path = "../../packages/yew", features = ["csr"] } -gloo-utils = "0.1" +gloo = "0.8" diff --git a/examples/two_apps/README.md b/examples/two_apps/README.md index 31630cdc064..ed08a45ac39 100644 --- a/examples/two_apps/README.md +++ b/examples/two_apps/README.md @@ -15,3 +15,11 @@ One of the components could even accept a generic "remote" component using a tra This example is very similar to [`mount_point`](../mount_point). The two should be merged into a single example. + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/two_apps/src/main.rs b/examples/two_apps/src/main.rs index ef91b2dacb1..24313fdf324 100644 --- a/examples/two_apps/src/main.rs +++ b/examples/two_apps/src/main.rs @@ -70,7 +70,7 @@ impl Component for App { } fn mount_app(selector: &'static str) -> AppHandle { - let document = gloo_utils::document(); + let document = gloo::utils::document(); let element = document.query_selector(selector).unwrap().unwrap(); yew::Renderer::::with_root(element).render() } diff --git a/examples/web_worker_fib/README.md b/examples/web_worker_fib/README.md index f592453d96c..6cec5845f4a 100644 --- a/examples/web_worker_fib/README.md +++ b/examples/web_worker_fib/README.md @@ -12,3 +12,11 @@ The example illustrates how to use `yew-agent` to send tasks to a worker thread - [insou22](https://github.com/insou22) for writing up the demo. - [https://github.com/yvt/img2text](https://github.com/yvt/img2text) -- for how to make web workers compile in wasm + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/webgl/Cargo.toml b/examples/webgl/Cargo.toml index f54a08bad9d..7075a8e144e 100644 --- a/examples/webgl/Cargo.toml +++ b/examples/webgl/Cargo.toml @@ -9,7 +9,6 @@ license = "MIT OR Apache-2.0" js-sys = "0.3" wasm-bindgen = "0.2" yew = { path = "../../packages/yew", features = ["csr"] } -gloo-render = "0.1" [dependencies.web-sys] version = "0.3" diff --git a/examples/webgl/README.md b/examples/webgl/README.md index f61efc9f69a..74480c6dae6 100644 --- a/examples/webgl/README.md +++ b/examples/webgl/README.md @@ -13,3 +13,11 @@ a render loop, and draw to the canvas with basic shaders using `web-sys`. ## Improvements - Use a much more flashy shader + +## Running + +Run this application with the trunk development server: + +```bash +trunk serve --open +``` \ No newline at end of file diff --git a/examples/webgl/src/main.rs b/examples/webgl/src/main.rs index cc1166873fb..04f0443aa55 100644 --- a/examples/webgl/src/main.rs +++ b/examples/webgl/src/main.rs @@ -1,41 +1,23 @@ -use gloo_render::{request_animation_frame, AnimationFrame}; +use std::cell::RefCell; +use std::rc::Rc; + +use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use web_sys::{HtmlCanvasElement, WebGlRenderingContext as GL}; -use yew::html::Scope; +use web_sys::{window, HtmlCanvasElement, WebGlRenderingContext as GL, WebGlRenderingContext}; use yew::{html, Component, Context, Html, NodeRef}; -pub enum Msg { - Render(f64), -} - +// Wrap gl in Rc (Arc for multi-threaded) so it can be injected into the render-loop closure. pub struct App { - gl: Option, node_ref: NodeRef, - _render_loop: Option, } impl Component for App { - type Message = Msg; + type Message = (); type Properties = (); fn create(_ctx: &Context) -> Self { Self { - gl: None, node_ref: NodeRef::default(), - _render_loop: None, - } - } - - fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - match msg { - Msg::Render(timestamp) => { - // Render functions are likely to get quite large, so it is good practice to split - // it into it's own function rather than keeping it inline in the update match - // case. This also allows for updating other UI elements that may be rendered in - // the DOM like a framerate counter, or other overlaid textual elements. - self.render_gl(timestamp, ctx.link()); - false - } } } @@ -45,44 +27,41 @@ impl Component for App { } } - fn rendered(&mut self, ctx: &Context, first_render: bool) { + fn rendered(&mut self, _ctx: &Context, first_render: bool) { + // Only start the render loop if it's the first render + // There's no loop cancellation taking place, so if multiple renders happen, + // there would be multiple loops running. That doesn't *really* matter here because + // there's no props update and no SSR is taking place, but it is something to keep in + // consideration + if !first_render { + return; + } // Once rendered, store references for the canvas and GL context. These can be used for // resizing the rendering area when the window or canvas element are resized, as well as // for making GL calls. - let canvas = self.node_ref.cast::().unwrap(); - let gl: GL = canvas .get_context("webgl") .unwrap() .unwrap() .dyn_into() .unwrap(); - - self.gl = Some(gl); - - // In a more complex use-case, there will be additional WebGL initialization that should be - // done here, such as enabling or disabling depth testing, depth functions, face - // culling etc. - - if first_render { - // The callback to request animation frame is passed a time value which can be used for - // rendering motion independent of the framerate which may vary. - let handle = { - let link = ctx.link().clone(); - request_animation_frame(move |time| link.send_message(Msg::Render(time))) - }; - - // A reference to the handle must be stored, otherwise it is dropped and the render - // won't occur. - self._render_loop = Some(handle); - } + Self::render_gl(gl); } } impl App { - fn render_gl(&mut self, timestamp: f64, link: &Scope) { - let gl = self.gl.as_ref().expect("GL Context not initialized!"); + fn request_animation_frame(f: &Closure) { + window() + .unwrap() + .request_animation_frame(f.as_ref().unchecked_ref()) + .expect("should register `requestAnimationFrame` OK"); + } + + fn render_gl(gl: WebGlRenderingContext) { + // This should log only once -- not once per frame + + let mut timestamp = 0.0; let vert_code = include_str!("./basic.vert"); let frag_code = include_str!("./basic.frag"); @@ -123,13 +102,24 @@ impl App { gl.draw_arrays(GL::TRIANGLES, 0, 6); - let handle = { - let link = link.clone(); - request_animation_frame(move |time| link.send_message(Msg::Render(time))) - }; + // Gloo-render's request_animation_frame has this extra closure + // wrapping logic running every frame, unnecessary cost. + // Here constructing the wrapped closure just once. + + let cb = Rc::new(RefCell::new(None)); + + *cb.borrow_mut() = Some(Closure::wrap(Box::new({ + let cb = cb.clone(); + move || { + // This should repeat every frame + timestamp += 20.0; + gl.uniform1f(time.as_ref(), timestamp as f32); + gl.draw_arrays(GL::TRIANGLES, 0, 6); + App::request_animation_frame(cb.borrow().as_ref().unwrap()); + } + }) as Box)); - // A reference to the new handle must be retained for the next render to run. - self._render_loop = Some(handle); + App::request_animation_frame(cb.borrow().as_ref().unwrap()); } } diff --git a/firebase.json b/firebase.json index 4dcb1d8a582..c8baff9ff88 100644 --- a/firebase.json +++ b/firebase.json @@ -50,6 +50,13 @@ ] } ], + "headers": [ { + "source": "tutorial/*.json", + "headers": [ { + "key": "Access-Control-Allow-Origin", + "value": "*" + } ] + } ], "emulators": { "hosting": { "port": 5000 diff --git a/packages/yew-agent/Cargo.toml b/packages/yew-agent/Cargo.toml index 2a4bb824053..75b20ba5060 100644 --- a/packages/yew-agent/Cargo.toml +++ b/packages/yew-agent/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "yew-agent" -version = "0.1.0" +version = "0.2.0" authors = ["Hamza "] +repository = "https://github.com/yewstack/yew" +homepage = "https://yew.rs" +documentation = "https://docs.rs/yew/" edition = "2021" readme = "../../README.md" description = "Agents for Yew" @@ -9,11 +12,13 @@ license = "MIT OR Apache-2.0" rust-version = "1.60.0" [dependencies] -yew = { version = "0.19.3", path = "../yew" } -gloo = { version = "0.8", features = ["futures"] } +yew = { version = "0.20.0", path = "../yew" } gloo-worker = { version = "0.2", features = ["futures"] } wasm-bindgen = "0.2" serde = { version = "1", features = ["derive"] } futures = "0.3" pin-project = "1.0.10" yew-agent-macro = { version = "0.1", path = "../yew-agent-macro" } + +[dev-dependencies] +serde = "1.0.164" diff --git a/packages/yew-macro/Cargo.toml b/packages/yew-macro/Cargo.toml index 875fb88cde6..7de16286e07 100644 --- a/packages/yew-macro/Cargo.toml +++ b/packages/yew-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yew-macro" -version = "0.19.3" +version = "0.20.0" edition = "2021" authors = ["Justin Starry "] repository = "https://github.com/yewstack/yew" @@ -10,7 +10,7 @@ license = "MIT OR Apache-2.0" keywords = ["web", "wasm", "frontend", "webasm", "webassembly"] categories = ["gui", "web-programming", "wasm"] description = "A framework for making client-side single-page apps" -rust-version = "1.60.0" +rust-version = "1.64.0" [lib] proc-macro = true @@ -20,18 +20,12 @@ boolinator = "2" proc-macro-error = "1" proc-macro2 = "1" quote = "1" -syn = { version = "1", features = ["full", "extra-traits", "visit-mut"] } +syn = { version = "2", features = ["full", "extra-traits", "visit-mut"] } once_cell = "1" -prettyplease = "0.1.1" +prettyplease = "0.2" # testing [dev-dependencies] rustversion = "1" trybuild = "1" yew = { path = "../yew" } - -[build-dependencies] - -[features] -lints = [] -nightly = [] diff --git a/packages/yew-macro/Makefile.toml b/packages/yew-macro/Makefile.toml index 999a235920b..b679fa4fa51 100644 --- a/packages/yew-macro/Makefile.toml +++ b/packages/yew-macro/Makefile.toml @@ -1,6 +1,6 @@ [tasks.test] clear = true -toolchain = "1.60.0" +toolchain = "1.64.0" command = "cargo" # test target can be optionally specified like `cargo make test html_macro`, args = ["test", "${@}"] diff --git a/packages/yew-macro/src/classes/mod.rs b/packages/yew-macro/src/classes/mod.rs index 76e7ee9b198..9cf80bc9719 100644 --- a/packages/yew-macro/src/classes/mod.rs +++ b/packages/yew-macro/src/classes/mod.rs @@ -10,7 +10,9 @@ pub struct Classes(Punctuated); impl Parse for Classes { fn parse(input: ParseStream) -> syn::Result { - input.parse_terminated(ClassExpr::parse).map(Self) + input + .parse_terminated(ClassExpr::parse, Token![,]) + .map(Self) } } @@ -52,12 +54,11 @@ impl Parse for ClassExpr { if classes.len() > 1 { let fix = classes .into_iter() - .map(|class| format!("\"{}\"", class)) + .map(|class| format!("\"{class}\"")) .collect::>() .join(", "); let msg = format!( - "string literals must not contain more than one class (hint: use `{}`)", - fix + "string literals must not contain more than one class (hint: use `{fix}`)" ); Err(syn::Error::new(lit_str.span(), msg)) diff --git a/packages/yew-macro/src/derive_props/field.rs b/packages/yew-macro/src/derive_props/field.rs index 657b503e475..cbd6973c346 100644 --- a/packages/yew-macro/src/derive_props/field.rs +++ b/packages/yew-macro/src/derive_props/field.rs @@ -201,17 +201,17 @@ impl PropField { // Detect Properties 2.0 attributes fn attribute(named_field: &Field) -> Result { let attr = named_field.attrs.iter().find(|attr| { - attr.path.is_ident("prop_or") - || attr.path.is_ident("prop_or_else") - || attr.path.is_ident("prop_or_default") + attr.path().is_ident("prop_or") + || attr.path().is_ident("prop_or_else") + || attr.path().is_ident("prop_or_default") }); if let Some(attr) = attr { - if attr.path.is_ident("prop_or") { + if attr.path().is_ident("prop_or") { Ok(PropAttr::PropOr(attr.parse_args()?)) - } else if attr.path.is_ident("prop_or_else") { + } else if attr.path().is_ident("prop_or_else") { Ok(PropAttr::PropOrElse(attr.parse_args()?)) - } else if attr.path.is_ident("prop_or_default") { + } else if attr.path().is_ident("prop_or_default") { Ok(PropAttr::PropOrDefault) } else { unreachable!() diff --git a/packages/yew-macro/src/derive_props/mod.rs b/packages/yew-macro/src/derive_props/mod.rs index 2a31a2ae479..db18527222f 100644 --- a/packages/yew-macro/src/derive_props/mod.rs +++ b/packages/yew-macro/src/derive_props/mod.rs @@ -30,7 +30,7 @@ fn should_preserve_attr(attr: &Attribute) -> bool { // sometimes. If not preserved, results in "no-such-field" errors generating // the field setter for `build` #[allow(...)]: silences warnings from clippy, such as // dead_code etc. #[deny(...)]: enable additional warnings from clippy - let path = &attr.path; + let path = attr.path(); path.is_ident("allow") || path.is_ident("deny") || path.is_ident("cfg") } diff --git a/packages/yew-macro/src/function_component.rs b/packages/yew-macro/src/function_component.rs index 2c36fe4c465..d17b5b65155 100644 --- a/packages/yew-macro/src/function_component.rs +++ b/packages/yew-macro/src/function_component.rs @@ -102,7 +102,7 @@ impl Parse for FunctionComponent { if ty.mutability.is_some() { return Err(syn::Error::new_spanned( - &ty.mutability, + ty.mutability, "reference must not be mutable", )); } @@ -158,7 +158,7 @@ impl FunctionComponent { self.attrs .iter() .filter_map(|m| { - m.path + m.path() .get_ident() .and_then(|ident| match ident.to_string().as_str() { "doc" | "allow" => Some(m.clone()), @@ -173,7 +173,7 @@ impl FunctionComponent { self.attrs .iter() .filter_map(|m| { - m.path + m.path() .get_ident() .and_then(|ident| match ident.to_string().as_str() { "allow" => Some(m.clone()), @@ -298,7 +298,7 @@ impl FunctionComponent { } #[inline] - fn changed(&mut self, _ctx: &::yew::html::Context) -> ::std::primitive::bool { + fn changed(&mut self, _ctx: &::yew::html::Context, _old_props: &Self::Properties) -> ::std::primitive::bool { true } @@ -332,7 +332,7 @@ impl FunctionComponent { let component_name = self.component_name(); let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); - let component_name_lit = LitStr::new(&format!("{}<_>", component_name), Span::mixed_site()); + let component_name_lit = LitStr::new(&format!("{component_name}<_>"), Span::mixed_site()); quote! { #[automatically_derived] diff --git a/packages/yew-macro/src/hook/body.rs b/packages/yew-macro/src/hook/body.rs index 4bdba7bfc6c..ff49193e36b 100644 --- a/packages/yew-macro/src/hook/body.rs +++ b/packages/yew-macro/src/hook/body.rs @@ -49,7 +49,7 @@ impl VisitMut for BodyRewriter { m, "hooks cannot be called at this position."; help = "move hooks to the top-level of your function."; - note = "see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks" + note = "see: https://yew.rs/docs/next/concepts/function-components/hooks" ); } else { *i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #ctx_ident) }; @@ -75,7 +75,7 @@ impl VisitMut for BodyRewriter { ident, "hooks cannot be called at this position."; help = "move hooks to the top-level of your function."; - note = "see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks" + note = "see: https://yew.rs/docs/next/concepts/function-components/hooks" ); } else { *i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #ctx_ident) }; @@ -98,12 +98,12 @@ impl VisitMut for BodyRewriter { visit_mut::visit_attribute_mut(self, it); } - visit_mut::visit_expr_mut(self, &mut *i.cond); + visit_mut::visit_expr_mut(self, &mut i.cond); self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.then_branch)); if let Some(it) = &mut i.else_branch { - self.with_branch(|m| visit_mut::visit_expr_mut(m, &mut *(it).1)); + self.with_branch(|m| visit_mut::visit_expr_mut(m, &mut (it).1)); } } @@ -119,7 +119,7 @@ impl VisitMut for BodyRewriter { visit_mut::visit_label_mut(self, it); } visit_mut::visit_pat_mut(self, &mut i.pat); - visit_mut::visit_expr_mut(self, &mut *i.expr); + visit_mut::visit_expr_mut(self, &mut i.expr); self.with_branch(|m| visit_mut::visit_block_mut(m, &mut i.body)); } @@ -129,7 +129,7 @@ impl VisitMut for BodyRewriter { visit_mut::visit_attribute_mut(self, it); } - visit_mut::visit_expr_mut(self, &mut *i.expr); + visit_mut::visit_expr_mut(self, &mut i.expr); self.with_branch(|m| { for it in &mut i.arms { diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index ba186d323b2..f22fd4f722e 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -133,8 +133,8 @@ pub fn hook_impl(hook: HookFn) -> syn::Result { let inner_type_impl = if hook_sig.needs_boxing { let with_output = !matches!(hook_sig.output_type, Type::ImplTrait(_),); - let inner_fn_rt = with_output.then(|| &inner_fn_rt); - let output_type = with_output.then(|| &output_type); + let inner_fn_rt = with_output.then_some(&inner_fn_rt); + let output_type = with_output.then_some(&output_type); let hook_lifetime = &hook_sig.hook_lifetime; let hook_lifetime_plus = quote! { #hook_lifetime + }; diff --git a/packages/yew-macro/src/html_tree/html_component.rs b/packages/yew-macro/src/html_tree/html_component.rs index 015267acef3..58e58988325 100644 --- a/packages/yew-macro/src/html_tree/html_component.rs +++ b/packages/yew-macro/src/html_tree/html_component.rs @@ -1,18 +1,13 @@ -use boolinator::Boolinator; use proc_macro2::Span; use quote::{quote, quote_spanned, ToTokens}; -use syn::buffer::Cursor; +use syn::parse::discouraged::Speculative; use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; use syn::spanned::Spanned; -use syn::{ - AngleBracketedGenericArguments, GenericArgument, Path, PathArguments, PathSegment, Token, Type, - TypePath, -}; +use syn::{Token, Type}; use super::{HtmlChildrenTree, TagTokens}; +use crate::is_ide_completion; use crate::props::ComponentProps; -use crate::PeekValue; pub struct HtmlComponent { ty: Type, @@ -21,24 +16,26 @@ pub struct HtmlComponent { close: Option, } -impl PeekValue<()> for HtmlComponent { - fn peek(cursor: Cursor) -> Option<()> { - HtmlComponentOpen::peek(cursor) - .or_else(|| HtmlComponentClose::peek(cursor)) - .map(|_| ()) - } -} - impl Parse for HtmlComponent { fn parse(input: ParseStream) -> syn::Result { - if HtmlComponentClose::peek(input.cursor()).is_some() { - return match input.parse::() { - Ok(close) => Err(syn::Error::new_spanned( - close.to_spanned(), - "this closing tag has no corresponding opening tag", - )), - Err(err) => Err(err), - }; + // check if the next tokens are (); + if !is_ide_completion() { + return match close { + Ok(close) => Err(syn::Error::new_spanned( + close.to_spanned(), + "this closing tag has no corresponding opening tag", + )), + Err(err) => Err(err), + }; + } } let open = input.parse::()?; @@ -53,23 +50,59 @@ impl Parse for HtmlComponent { } let mut children = HtmlChildrenTree::new(); - loop { + let close = loop { if input.is_empty() { + if is_ide_completion() { + break None; + } return Err(syn::Error::new_spanned( open.to_spanned(), "this opening tag has no corresponding closing tag", )); } - if let Some(ty) = HtmlComponentClose::peek(input.cursor()) { - if open.ty == ty { - break; + + if trying_to_close() { + fn format_token_stream(ts: impl ToTokens) -> String { + let string = ts.to_token_stream().to_string(); + // remove unnecessary spaces + string.replace(' ', "") } - } + let fork = input.fork(); + let close = TagTokens::parse_end_content(&fork, |i_fork, tag| { + let ty = i_fork.parse().map_err(|e| { + syn::Error::new( + e.span(), + format!( + "expected a valid closing tag for component\nnote: found opening \ + tag `{lt}{0}{gt}`\nhelp: try `{lt}/{0}{gt}`", + format_token_stream(&open.ty), + lt = open.tag.lt.to_token_stream(), + gt = open.tag.gt.to_token_stream(), + ), + ) + })?; + + if ty != open.ty && !is_ide_completion() { + let open_ty = &open.ty; + Err(syn::Error::new_spanned( + quote!(#open_ty #ty), + format!( + "mismatched closing tags: expected `{}`, found `{}`", + format_token_stream(open_ty), + format_token_stream(ty) + ), + )) + } else { + let close = HtmlComponentClose { tag, ty }; + input.advance_to(&fork); + Ok(close) + } + })?; + break Some(close); + } children.parse_child(input)?; - } - - let close = input.parse::()?; + }; if !children.is_empty() { if let Some(children_prop) = open.props.children() { @@ -84,7 +117,7 @@ impl Parse for HtmlComponent { ty: open.ty, props: open.props, children, - close: Some(close), + close, }) } } @@ -100,151 +133,30 @@ impl ToTokens for HtmlComponent { let ty_span = ty.span().resolved_at(Span::call_site()); let props_ty = quote_spanned!(ty_span=> <#ty as ::yew::html::BaseComponent>::Properties); - let children_renderer = if children.is_empty() { - None - } else { - Some(quote! { ::yew::html::ChildrenRenderer::new(#children) }) - }; + let children_renderer = children.to_children_renderer_tokens(); let build_props = props.build_properties_tokens(&props_ty, children_renderer); - - let special_props = props.special(); - let node_ref = if let Some(node_ref) = &special_props.node_ref { - let value = &node_ref.value; - quote! { #value } - } else { - quote! { <::yew::html::NodeRef as ::std::default::Default>::default() } - }; - - let key = if let Some(key) = &special_props.key { - let value = &key.value; - quote_spanned! {value.span().resolved_at(Span::call_site())=> - #[allow(clippy::useless_conversion)] - Some(::std::convert::Into::<::yew::virtual_dom::Key>::into(#value)) - } - } else { - quote! { ::std::option::Option::None } - }; - let use_close_tag = if let Some(close) = close { - let close_ty = &close.ty; - quote_spanned! {close_ty.span()=> - let _ = |_:#close_ty| {}; - } - } else { - Default::default() - }; + let key = props.special().wrap_key_attr(); + let use_close_tag = close + .as_ref() + .map(|close| { + let close_ty = &close.ty; + quote_spanned! {close_ty.span()=> + let _ = |_:#close_ty| {}; + } + }) + .unwrap_or_default(); tokens.extend(quote_spanned! {ty_span=> { #use_close_tag + #[allow(clippy::let_unit_value)] let __yew_props = #build_props; - ::yew::virtual_dom::VChild::<#ty>::new(__yew_props, #node_ref, #key) + ::yew::virtual_dom::VChild::<#ty>::new(__yew_props, #key) } }); } } -impl HtmlComponent { - fn double_colon(mut cursor: Cursor) -> Option { - for _ in 0..2 { - let (punct, c) = cursor.punct()?; - (punct.as_char() == ':').as_option()?; - cursor = c; - } - - Some(cursor) - } - - /// Refer to the [`syn::parse::Parse`] implementation for [`AngleBracketedGenericArguments`]. - fn path_arguments(mut cursor: Cursor) -> Option<(PathArguments, Cursor)> { - let (punct, c) = cursor.punct()?; - cursor = c; - (punct.as_char() == '<').as_option()?; - - let mut args = Punctuated::new(); - - loop { - let punct = cursor.punct(); - if let Some((punct, c)) = punct { - if punct.as_char() == '>' { - cursor = c; - break; - } - } - - let (ty, c) = Self::peek_type(cursor); - cursor = c; - - args.push_value(GenericArgument::Type(ty)); - - let punct = cursor.punct(); - if let Some((punct, c)) = punct { - cursor = c; - if punct.as_char() == '>' { - break; - } else if punct.as_char() == ',' { - args.push_punct(Token![,](Span::mixed_site())) - } - } - } - - Some(( - PathArguments::AngleBracketed(AngleBracketedGenericArguments { - colon2_token: None, - lt_token: Token![<](Span::mixed_site()), - args, - gt_token: Token![>](Span::mixed_site()), - }), - cursor, - )) - } - - fn peek_type(mut cursor: Cursor) -> (Type, Cursor) { - let mut colons_optional = true; - let mut leading_colon = None; - let mut segments = Punctuated::new(); - - loop { - let mut post_colons_cursor = cursor; - if let Some(c) = Self::double_colon(post_colons_cursor) { - if colons_optional { - leading_colon = Some(Token![::](Span::mixed_site())); - } - post_colons_cursor = c; - } else if !colons_optional { - break; - } - - if let Some((ident, c)) = post_colons_cursor.ident() { - cursor = c; - let arguments = if let Some((args, c)) = Self::path_arguments(cursor) { - cursor = c; - args - } else { - PathArguments::None - }; - - segments.push(PathSegment { ident, arguments }); - } else { - break; - } - - // only first `::` is optional - colons_optional = false; - } - - ( - Type::Path(TypePath { - qself: None, - path: Path { - leading_colon, - segments, - }, - }), - cursor, - ) - } -} - struct HtmlComponentOpen { tag: TagTokens, ty: Type, @@ -260,20 +172,19 @@ impl HtmlComponentOpen { } } -impl PeekValue for HtmlComponentOpen { - fn peek(cursor: Cursor) -> Option { - let (punct, cursor) = cursor.punct()?; - (punct.as_char() == '<').as_option()?; - let (typ, _) = HtmlComponent::peek_type(cursor); - Some(typ) - } -} - impl Parse for HtmlComponentOpen { fn parse(input: ParseStream) -> syn::Result { TagTokens::parse_start_content(input, |input, tag| { let ty = input.parse()?; - let props = input.parse()?; + let props: ComponentProps = input.parse()?; + + if let Some(ref node_ref) = props.special().node_ref { + return Err(syn::Error::new_spanned( + &node_ref.label, + "cannot use `ref` with components. If you want to specify a property, use \ + `r#ref` here instead.", + )); + } Ok(Self { tag, ty, props }) }) @@ -290,22 +201,6 @@ impl HtmlComponentClose { } } -impl PeekValue for HtmlComponentClose { - fn peek(cursor: Cursor) -> Option { - let (punct, cursor) = cursor.punct()?; - (punct.as_char() == '<').as_option()?; - - let (punct, cursor) = cursor.punct()?; - (punct.as_char() == '/').as_option()?; - - let (typ, cursor) = HtmlComponent::peek_type(cursor); - - let (punct, _) = cursor.punct()?; - (punct.as_char() == '>').as_option()?; - - Some(typ) - } -} impl Parse for HtmlComponentClose { fn parse(input: ParseStream) -> syn::Result { TagTokens::parse_end_content(input, |input, tag| { diff --git a/packages/yew-macro/src/html_tree/html_dashed_name.rs b/packages/yew-macro/src/html_tree/html_dashed_name.rs index 72b3471d217..6699b779f5a 100644 --- a/packages/yew-macro/src/html_tree/html_dashed_name.rs +++ b/packages/yew-macro/src/html_tree/html_dashed_name.rs @@ -12,7 +12,7 @@ use syn::{LitStr, Token}; use crate::stringify::Stringify; use crate::{non_capitalized_ascii, Peek}; -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Eq)] pub struct HtmlDashedName { pub name: Ident, pub extended: Vec<(Token![-], Ident)>, @@ -45,7 +45,7 @@ impl fmt::Display for HtmlDashedName { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name)?; for (_, ident) in &self.extended { - write!(f, "-{}", ident)?; + write!(f, "-{ident}")?; } Ok(()) } @@ -79,7 +79,7 @@ impl Parse for HtmlDashedName { let name = input.call(Ident::parse_any)?; let mut extended = Vec::new(); while input.peek(Token![-]) { - extended.push((input.parse::()?, input.parse::()?)); + extended.push((input.parse::()?, input.call(Ident::parse_any)?)); } Ok(HtmlDashedName { name, extended }) diff --git a/packages/yew-macro/src/html_tree/html_element.rs b/packages/yew-macro/src/html_tree/html_element.rs index 8b981a10d1c..143823c00b8 100644 --- a/packages/yew-macro/src/html_tree/html_element.rs +++ b/packages/yew-macro/src/html_tree/html_element.rs @@ -8,9 +8,9 @@ use syn::spanned::Spanned; use syn::{Block, Expr, Ident, Lit, LitStr, Token}; use super::{HtmlChildrenTree, HtmlDashedName, TagTokens}; -use crate::props::{ClassesForm, ElementProps, Prop}; +use crate::props::{ClassesForm, ElementProps, Prop, PropDirective}; use crate::stringify::{Stringify, Value}; -use crate::{non_capitalized_ascii, Peek, PeekValue}; +use crate::{is_ide_completion, non_capitalized_ascii, Peek, PeekValue}; pub struct HtmlElement { pub name: TagName, @@ -59,9 +59,8 @@ impl Parse for HtmlElement { return Err(syn::Error::new_spanned( open.to_spanned(), format!( - "the tag `<{}>` is a void element and cannot have children (hint: \ - rewrite this as `<{0}/>`)", - name + "the tag `<{name}>` is a void element and cannot have children (hint: \ + rewrite this as `<{name} />`)", ), )); } @@ -73,6 +72,9 @@ impl Parse for HtmlElement { let mut children = HtmlChildrenTree::new(); loop { if input.is_empty() { + if is_ide_completion() { + break; + } return Err(syn::Error::new_spanned( open.to_spanned(), "this opening tag has no corresponding closing tag", @@ -87,7 +89,9 @@ impl Parse for HtmlElement { children.parse_child(input)?; } - input.parse::()?; + if !input.is_empty() || !is_ide_completion() { + input.parse::()?; + } Ok(Self { name: open.name, @@ -112,82 +116,81 @@ impl ToTokens for HtmlElement { booleans, value, checked, - node_ref, - key, listeners, + special, } = &props; // attributes with special treatment - let node_ref = node_ref - .as_ref() - .map(|attr| { - let value = &attr.value; - quote_spanned! {value.span().resolved_at(Span::call_site())=> - ::yew::html::IntoPropValue::<::yew::html::NodeRef> - ::into_prop_value(#value) - } - }) - .unwrap_or(quote! { ::std::default::Default::default() }); - let key = key - .as_ref() - .map(|attr| { - let value = attr.value.optimize_literals(); - quote_spanned! {value.span().resolved_at(Span::call_site())=> - ::std::option::Option::Some( - ::std::convert::Into::<::yew::virtual_dom::Key>::into(#value) - ) - } - }) - .unwrap_or(quote! { ::std::option::Option::None }); + let node_ref = special.wrap_node_ref_attr(); + let key = special.wrap_key_attr(); let value = value .as_ref() - .map(wrap_attr_prop) + .map(|prop| wrap_attr_value(prop.value.optimize_literals())) .unwrap_or(quote! { ::std::option::Option::None }); let checked = checked .as_ref() .map(|attr| { let value = &attr.value; - quote_spanned! {value.span()=> #value} + quote! { ::std::option::Option::Some( #value ) } }) - .unwrap_or(quote! { false }); + .unwrap_or(quote! { ::std::option::Option::None }); // other attributes let attributes = { - let normal_attrs = attributes.iter().map(|Prop { label, value, .. }| { - (label.to_lit_str(), value.optimize_literals_tagged()) - }); - let boolean_attrs = booleans.iter().filter_map(|Prop { label, value, .. }| { - let key = label.to_lit_str(); - Some(( - key.clone(), - match value { - Expr::Lit(e) => match &e.lit { - Lit::Bool(b) => Value::Static(if b.value { - quote! { #key } - } else { - return None; - }), - _ => Value::Dynamic(quote_spanned! {value.span()=> { - ::yew::utils::__ensure_type::<::std::primitive::bool>(#value); - #key - }}), - }, - expr => Value::Dynamic( - quote_spanned! {expr.span().resolved_at(Span::call_site())=> - if #expr { - ::std::option::Option::Some( - ::yew::virtual_dom::AttrValue::Static(#key) - ) + let normal_attrs = attributes.iter().map( + |Prop { + label, + value, + directive, + .. + }| { + ( + label.to_lit_str(), + value.optimize_literals_tagged(), + *directive, + ) + }, + ); + let boolean_attrs = booleans.iter().filter_map( + |Prop { + label, + value, + directive, + .. + }| { + let key = label.to_lit_str(); + Some(( + key.clone(), + match value { + Expr::Lit(e) => match &e.lit { + Lit::Bool(b) => Value::Static(if b.value { + quote! { #key } } else { - ::std::option::Option::None - } + return None; + }), + _ => Value::Dynamic(quote_spanned! {value.span()=> { + ::yew::utils::__ensure_type::<::std::primitive::bool>(#value); + #key + }}), }, - ), - }, - )) - }); + expr => Value::Dynamic( + quote_spanned! {expr.span().resolved_at(Span::call_site())=> + if #expr { + ::std::option::Option::Some( + ::yew::virtual_dom::AttrValue::Static(#key) + ) + } else { + ::std::option::Option::None + } + }, + ), + }, + *directive, + )) + }, + ); let class_attr = classes.as_ref().and_then(|classes| match classes { ClassesForm::Tuple(classes) => { let span = classes.span(); @@ -216,6 +219,7 @@ impl ToTokens for HtmlElement { __yew_classes } }), + None, )) } ClassesForm::Single(classes) => { @@ -227,6 +231,7 @@ impl ToTokens for HtmlElement { Some(( LitStr::new("class", lit.span()), Value::Static(quote! { #lit }), + None, )) } } @@ -236,21 +241,34 @@ impl ToTokens for HtmlElement { Value::Dynamic(quote! { ::std::convert::Into::<::yew::html::Classes>::into(#classes) }), + None, )) } } } }); + fn apply_as(directive: Option<&PropDirective>) -> TokenStream { + match directive { + Some(PropDirective::ApplyAsProperty(token)) => { + quote_spanned!(token.span()=> ::yew::virtual_dom::ApplyAttributeAs::Property) + } + None => quote!(::yew::virtual_dom::ApplyAttributeAs::Attribute), + } + } + /// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Static` - fn try_into_static(src: &[(LitStr, Value)]) -> Option { + fn try_into_static( + src: &[(LitStr, Value, Option)], + ) -> Option { let mut kv = Vec::with_capacity(src.len()); - for (k, v) in src.iter() { + for (k, v, directive) in src.iter() { let v = match v { Value::Static(v) => quote! { #v }, Value::Dynamic(_) => return None, }; - kv.push(quote! { [ #k, #v ] }); + let apply_as = apply_as(directive.as_ref()); + kv.push(quote! { ( #k, #v, #apply_as ) }); } Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) }) @@ -259,16 +277,13 @@ impl ToTokens for HtmlElement { let attrs = normal_attrs .chain(boolean_attrs) .chain(class_attr) - .collect::>(); + .collect::)>>(); try_into_static(&attrs).unwrap_or_else(|| { - let keys = attrs.iter().map(|(k, _)| quote! { #k }); - let values = attrs.iter().map(|(_, v)| { - quote_spanned! {v.span()=> - ::yew::html::IntoPropValue::< - ::std::option::Option::<::yew::virtual_dom::AttrValue> - > - ::into_prop_value(#v) - } + let keys = attrs.iter().map(|(k, ..)| quote! { #k }); + let values = attrs.iter().map(|(_, v, directive)| { + let apply_as = apply_as(directive.as_ref()); + let value = wrap_attr_value(v); + quote! { ::std::option::Option::map(#value, |it| (it, #apply_as)) } }); quote! { ::yew::virtual_dom::Attributes::Dynamic{ @@ -298,12 +313,7 @@ impl ToTokens for HtmlElement { // TODO: if none of the children have possibly None expressions or literals as keys, we can // compute `VList.fully_keyed` at compile time. - let child_list = quote! { - ::yew::virtual_dom::VList::with_children( - #children, - ::std::option::Option::None, - ) - }; + let children = children.to_vnode_tokens(); tokens.extend(match &name { TagName::Lit(dashedname) => { @@ -313,10 +323,8 @@ impl ToTokens for HtmlElement { emit_warning!( dashedname.span(), format!( - "The tag '{0}' is not matching its normalized form '{1}'. If you want \ - to keep this form, change this to a dynamic tag `@{{\"{0}\"}}`.", - dashedname, - name, + "The tag '{dashedname}' is not matching its normalized form '{name}'. If you want \ + to keep this form, change this to a dynamic tag `@{{\"{dashedname}\"}}`." ) ) } @@ -357,7 +365,7 @@ impl ToTokens for HtmlElement { #key, #attributes, #listeners, - #child_list, + #children, ), ) } @@ -379,6 +387,8 @@ impl ToTokens for HtmlElement { let expr = &name.expr; let vtag_name = Ident::new("__yew_vtag_name", expr.span()); + let void_children = Ident::new("__yew_void_children", Span::mixed_site()); + // handle special attribute value let handle_value_attr = props.value.as_ref().map(|prop| { let v = prop.value.optimize_literals(); @@ -387,16 +397,16 @@ impl ToTokens for HtmlElement { }} }); - #[cfg(feature = "nightly")] + #[cfg(nightly_yew)] let invalid_void_tag_msg_start = { let span = vtag.span().unwrap(); let source_file = span.source_file().path(); let source_file = source_file.display(); let start = span.start(); - format!("[{}:{}:{}] ", source_file, start.line, start.column) + format!("[{}:{}:{}] ", source_file, start.line(), start.column()) }; - #[cfg(not(feature = "nightly"))] + #[cfg(not(nightly_yew))] let invalid_void_tag_msg_start = ""; // this way we get a nice error message (with the correct span) when the expression @@ -442,7 +452,7 @@ impl ToTokens for HtmlElement { #key, #attributes, #listeners, - #child_list, + #children, ); #handle_value_attr @@ -455,7 +465,10 @@ impl ToTokens for HtmlElement { // For literal tags this is already done at compile-time. // // check void element - if !#vtag.children().is_empty() { + if !::std::matches!( + ::yew::virtual_dom::VTag::children(&#vtag), + ::std::option::Option::Some(::yew::virtual_dom::VNode::VList(ref #void_children)) if ::std::vec::Vec::is_empty(#void_children) + ) { ::std::debug_assert!( !::std::matches!(#vtag.tag().to_ascii_lowercase().as_str(), "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" @@ -473,8 +486,7 @@ impl ToTokens for HtmlElement { } } -fn wrap_attr_prop(prop: &Prop) -> TokenStream { - let value = prop.value.optimize_literals(); +fn wrap_attr_value(value: T) -> TokenStream { quote_spanned! {value.span()=> ::yew::html::IntoPropValue::< ::std::option::Option< diff --git a/packages/yew-macro/src/html_tree/lint/mod.rs b/packages/yew-macro/src/html_tree/lint/mod.rs index 77fd51d96a3..bbb34f24648 100644 --- a/packages/yew-macro/src/html_tree/lint/mod.rs +++ b/packages/yew-macro/src/html_tree/lint/mod.rs @@ -26,6 +26,9 @@ pub fn lint(tree: &HtmlTree) where L: Lint, { + #[cfg(not(yew_lints))] + let _ = tree; + #[cfg(yew_lints)] match tree { HtmlTree::List(list) => { for child in &list.children.0 { @@ -64,11 +67,11 @@ impl Lint for AHrefLint { match href_value.as_ref() { "#" | "javascript:void(0)" => emit_warning!( lit.span(), - format!("'{}' is not a suitable value for the `href` attribute. \ + format!("'{href_value}' is not a suitable value for the `href` attribute. \ Without a meaningful attribute assistive technologies \ will struggle to understand your webpage. \ https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#onclick_events" - ,href_value)), + )), _ => {} } diff --git a/packages/yew-macro/src/html_tree/mod.rs b/packages/yew-macro/src/html_tree/mod.rs index 8b9e1e2898a..0361231b6d4 100644 --- a/packages/yew-macro/src/html_tree/mod.rs +++ b/packages/yew-macro/src/html_tree/mod.rs @@ -6,7 +6,7 @@ use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::{braced, token, Token}; -use crate::PeekValue; +use crate::{is_ide_completion, PeekValue}; mod html_block; mod html_component; @@ -29,6 +29,8 @@ use html_list::HtmlList; use html_node::HtmlNode; use tag::TagTokens; +use self::html_block::BlockContent; + pub enum HtmlType { Block, Component, @@ -101,6 +103,7 @@ impl HtmlTree { Some(HtmlType::List) } else if ident_str.chars().next().unwrap().is_ascii_uppercase() || input.peek(Token![::]) + || is_ide_completion() && ident_str.chars().any(|c| c.is_ascii_uppercase()) { Some(HtmlType::Component) } else { @@ -279,6 +282,64 @@ impl HtmlChildrenTree { Ok(children) } + + pub fn to_children_renderer_tokens(&self) -> Option { + match self.0[..] { + [] => None, + [HtmlTree::Component(ref children)] => Some(quote! { #children }), + [HtmlTree::Element(ref children)] => Some(quote! { #children }), + [HtmlTree::Block(ref m)] => { + // We only want to process `{vnode}` and not `{for vnodes}`. + // This should be converted into a if let guard once https://github.com/rust-lang/rust/issues/51114 is stable. + // Or further nested once deref pattern (https://github.com/rust-lang/rust/issues/87121) is stable. + if let HtmlBlock { + content: BlockContent::Node(children), + .. + } = m.as_ref() + { + Some(quote! { #children }) + } else { + Some(quote! { ::yew::html::ChildrenRenderer::new(#self) }) + } + } + _ => Some(quote! { ::yew::html::ChildrenRenderer::new(#self) }), + } + } + + pub fn to_vnode_tokens(&self) -> TokenStream { + match self.0[..] { + [] => quote! {::std::default::Default::default() }, + [HtmlTree::Component(ref children)] => { + quote! { ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(#children) } + } + [HtmlTree::Element(ref children)] => { + quote! { ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(#children) } + } + [HtmlTree::Block(ref m)] => { + // We only want to process `{vnode}` and not `{for vnodes}`. + // This should be converted into a if let guard once https://github.com/rust-lang/rust/issues/51114 is stable. + // Or further nested once deref pattern (https://github.com/rust-lang/rust/issues/87121) is stable. + if let HtmlBlock { + content: BlockContent::Node(children), + .. + } = m.as_ref() + { + quote! { ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value(#children) } + } else { + quote! { + ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value( + ::yew::html::ChildrenRenderer::new(#self) + ) + } + } + } + _ => quote! { + ::yew::html::IntoPropValue::<::yew::virtual_dom::VNode>::into_prop_value( + ::yew::html::ChildrenRenderer::new(#self) + ) + }, + } + } } impl ToTokens for HtmlChildrenTree { diff --git a/packages/yew-macro/src/html_tree/tag.rs b/packages/yew-macro/src/html_tree/tag.rs index 7c492f9843f..4c66f887089 100644 --- a/packages/yew-macro/src/html_tree/tag.rs +++ b/packages/yew-macro/src/html_tree/tag.rs @@ -7,7 +7,7 @@ use syn::Token; /// The implementation is really silly but I couldn't find another way to do it on stable. /// This check isn't required to be fully accurate so it's not the end of the world if it breaks. fn span_eq_hack(a: &Span, b: &Span) -> bool { - format!("{:?}", a) == format!("{:?}", b) + format!("{a:?}") == format!("{b:?}") } /// Change all occurrences of span `from` to `to` in the given error. @@ -109,7 +109,7 @@ impl TagTokens { match punct.as_char() { '/' => { if angle_count == 1 && input.peek(Token![>]) { - div = Some(syn::token::Div { + div = Some(syn::token::Slash { spans: [punct.span()], }); gt = input.parse()?; diff --git a/packages/yew-macro/src/lib.rs b/packages/yew-macro/src/lib.rs index 5822f0325ff..5d7216d84a0 100644 --- a/packages/yew-macro/src/lib.rs +++ b/packages/yew-macro/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(feature = "nightly", feature(proc_macro_span))] +#![cfg_attr(nightly_yew, feature(proc_macro_span))] //! This crate provides Yew's procedural macro `html!` which allows using JSX-like syntax //! for generating html and the `Properties` derive macro for deriving the `Properties` trait @@ -98,6 +98,13 @@ fn join_errors(mut it: impl Iterator) -> syn::Result<()> { }) } +fn is_ide_completion() -> bool { + match std::env::var_os("RUST_IDE_PROC_MACRO_COMPLETION_DUMMY_IDENTIFIER") { + None => false, + Some(dummy_identifier) => !dummy_identifier.is_empty(), + } +} + #[proc_macro_derive(Properties, attributes(prop_or, prop_or_else, prop_or_default))] pub fn derive_props(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DerivePropsInput); diff --git a/packages/yew-macro/src/props/component.rs b/packages/yew-macro/src/props/component.rs index d08dbeee809..3c0984e611c 100644 --- a/packages/yew-macro/src/props/component.rs +++ b/packages/yew-macro/src/props/component.rs @@ -4,33 +4,33 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; -use syn::token::Dot2; +use syn::token::DotDot; use syn::Expr; use super::{Prop, Props, SpecialProps, CHILDREN_LABEL}; struct BaseExpr { - pub dot2: Dot2, + pub dot_dot: DotDot, pub expr: Expr, } impl Parse for BaseExpr { fn parse(input: ParseStream) -> syn::Result { - let dot2 = input.parse()?; + let dot_dot = input.parse()?; let expr = input.parse().map_err(|expr_error| { let mut error = - syn::Error::new_spanned(dot2, "expected base props expression after `..`"); + syn::Error::new_spanned(dot_dot, "expected base props expression after `..`"); error.combine(expr_error); error })?; - Ok(Self { dot2, expr }) + Ok(Self { dot_dot, expr }) } } impl ToTokens for BaseExpr { fn to_tokens(&self, tokens: &mut TokenStream) { - let BaseExpr { dot2, expr } = self; - tokens.extend(quote! { #dot2 #expr }); + let BaseExpr { dot_dot, expr } = self; + tokens.extend(quote! { #dot_dot #expr }); } } @@ -64,7 +64,7 @@ impl ComponentProps { .iter() .map(|Prop { label, .. }| { quote_spanned! {Span::call_site().located_at(label.span())=> - let _ = #props_ident.#label; + let _ = &#props_ident.#label; } }) .collect(); @@ -131,7 +131,7 @@ impl ComponentProps { }); let set_children = children_renderer.map(|children| { quote_spanned! {props_ty.span()=> - #ident.children = #children; + #ident.children = ::yew::html::IntoPropValue::into_prop_value(#children); } }); let init_base = quote_spanned! {expr.span().resolved_at(Span::call_site())=> diff --git a/packages/yew-macro/src/props/element.rs b/packages/yew-macro/src/props/element.rs index f80af1c2adb..cd50b71b692 100644 --- a/packages/yew-macro/src/props/element.rs +++ b/packages/yew-macro/src/props/element.rs @@ -26,8 +26,7 @@ pub struct ElementProps { pub booleans: Vec, pub value: Option, pub checked: Option, - pub node_ref: Option, - pub key: Option, + pub special: SpecialProps, } impl Parse for ElementProps { @@ -48,8 +47,7 @@ impl Parse for ElementProps { .map(|prop| ClassesForm::from_expr(prop.value)); let value = props.pop("value"); let checked = props.pop("checked"); - - let SpecialProps { node_ref, key } = props.special; + let special = props.special; Ok(Self { attributes: props.prop_list.into_vec(), @@ -58,8 +56,7 @@ impl Parse for ElementProps { checked, booleans: booleans.into_vec(), value, - node_ref, - key, + special, }) } } @@ -94,6 +91,8 @@ static BOOLEAN_SET: Lazy> = Lazy::new(|| { "reversed", "selected", "truespeed", + // Not-yet-standardized + "webkitdirectory", ] .into() }); diff --git a/packages/yew-macro/src/props/prop.rs b/packages/yew-macro/src/props/prop.rs index 6fb4d7522fd..f1c0ad48079 100644 --- a/packages/yew-macro/src/props/prop.rs +++ b/packages/yew-macro/src/props/prop.rs @@ -1,26 +1,38 @@ -use std::cmp::Ordering; use std::convert::TryFrom; use std::ops::{Deref, DerefMut}; -use proc_macro2::{Spacing, TokenTree}; +use proc_macro2::{Spacing, Span, TokenStream, TokenTree}; +use quote::{quote, quote_spanned}; use syn::parse::{Parse, ParseBuffer, ParseStream}; +use syn::spanned::Spanned; use syn::token::Brace; -use syn::{braced, Block, Expr, ExprBlock, ExprPath, ExprRange, Stmt, Token}; +use syn::{braced, Block, Expr, ExprBlock, ExprMacro, ExprPath, ExprRange, Stmt, Token}; -use super::CHILDREN_LABEL; use crate::html_tree::HtmlDashedName; +use crate::stringify::Stringify; + +#[derive(Copy, Clone)] +pub enum PropDirective { + ApplyAsProperty(Token![~]), +} pub struct Prop { + pub directive: Option, pub label: HtmlDashedName, /// Punctuation between `label` and `value`. pub value: Expr, } + impl Parse for Prop { fn parse(input: ParseStream) -> syn::Result { + let directive = input + .parse::() + .map(PropDirective::ApplyAsProperty) + .ok(); if input.peek(Brace) { - Self::parse_shorthand_prop_assignment(input) + Self::parse_shorthand_prop_assignment(input, directive) } else { - Self::parse_prop_assignment(input) + Self::parse_prop_assignment(input, directive) } } } @@ -30,7 +42,10 @@ impl Prop { /// Parse a prop using the shorthand syntax `{value}`, short for `value={value}` /// This only allows for labels with no hyphens, as it would otherwise create /// an ambiguity in the syntax - fn parse_shorthand_prop_assignment(input: ParseStream) -> syn::Result { + fn parse_shorthand_prop_assignment( + input: ParseStream, + directive: Option, + ) -> syn::Result { let value; let _brace = braced!(value in input); let expr = value.parse::()?; @@ -41,7 +56,7 @@ impl Prop { }) = expr { if let (Some(ident), true) = (path.get_ident(), attrs.is_empty()) { - syn::Result::Ok(HtmlDashedName::from(ident.clone())) + Ok(HtmlDashedName::from(ident.clone())) } else { Err(syn::Error::new_spanned( path, @@ -56,19 +71,25 @@ impl Prop { )); }?; - Ok(Self { label, value: expr }) + Ok(Self { + label, + value: expr, + directive, + }) } /// Parse a prop of the form `label={value}` - fn parse_prop_assignment(input: ParseStream) -> syn::Result { + fn parse_prop_assignment( + input: ParseStream, + directive: Option, + ) -> syn::Result { let label = input.parse::()?; let equals = input.parse::().map_err(|_| { syn::Error::new_spanned( &label, format!( - "`{}` doesn't have a value. (hint: set the value to `true` or `false` for \ - boolean attributes)", - label + "`{label}` doesn't have a value. (hint: set the value to `true` or `false` \ + for boolean attributes)" ), ) })?; @@ -80,7 +101,11 @@ impl Prop { } let value = parse_prop_value(input)?; - Ok(Self { label, value }) + Ok(Self { + label, + value, + directive, + }) } } @@ -89,23 +114,25 @@ fn parse_prop_value(input: &ParseBuffer) -> syn::Result { strip_braces(input.parse()?) } else { let expr = if let Some(ExprRange { - from: Some(from), .. + start: Some(start), .. }) = range_expression_peek(input) { // If a range expression is seen, treat the left-side expression as the value // and leave the right-side expression to be parsed as a base expression advance_until_next_dot2(input)?; - *from + *start } else { input.parse()? }; match &expr { Expr::Lit(_) => Ok(expr), - _ => Err(syn::Error::new_spanned( + ref exp => Err(syn::Error::new_spanned( &expr, - "the property value must be either a literal or enclosed in braces. Consider \ - adding braces around your expression.", + format!( + "the property value must be either a literal or enclosed in braces. Consider \ + adding braces around your expression.: {exp:#?}" + ), )), } } @@ -119,7 +146,11 @@ fn strip_braces(block: ExprBlock) -> syn::Result { } if stmts.len() == 1 => { let stmt = stmts.remove(0); match stmt { - Stmt::Expr(expr) => Ok(expr), + Stmt::Expr(expr, None) => Ok(expr), + Stmt::Macro(mac) => Ok(Expr::Macro(ExprMacro { + attrs: vec![], + mac: mac.mac, + })), // See issue #2267, we want to parse macro invocations as expressions Stmt::Item(syn::Item::Macro(mac)) if mac.ident.is_none() && mac.semi_token.is_none() => @@ -129,7 +160,7 @@ fn strip_braces(block: ExprBlock) -> syn::Result { mac: mac.mac, })) } - Stmt::Semi(_expr, semi) => Err(syn::Error::new_spanned( + Stmt::Expr(_, Some(semi)) => Err(syn::Error::new_spanned( semi, "only an expression may be assigned as a property. Consider removing this \ semicolon", @@ -186,36 +217,21 @@ fn advance_until_next_dot2(input: &ParseBuffer) -> syn::Result<()> { /// /// The list may contain multiple props with the same label. /// Use `check_no_duplicates` to ensure that there are no duplicates. -pub struct SortedPropList(Vec); -impl SortedPropList { +pub struct PropList(Vec); +impl PropList { /// Create a new `SortedPropList` from a vector of props. /// The given `props` doesn't need to be sorted. - pub fn new(mut props: Vec) -> Self { - props.sort_by(|a, b| Self::cmp_label(&a.label.to_string(), &b.label.to_string())); + pub fn new(props: Vec) -> Self { Self(props) } - fn cmp_label(a: &str, b: &str) -> Ordering { - if a == b { - Ordering::Equal - } else if a == CHILDREN_LABEL { - Ordering::Greater - } else if b == CHILDREN_LABEL { - Ordering::Less - } else { - a.cmp(b) - } - } - fn position(&self, key: &str) -> Option { - self.0 - .binary_search_by(|prop| Self::cmp_label(prop.label.to_string().as_str(), key)) - .ok() + self.0.iter().position(|it| it.label.to_string() == key) } /// Get the first prop with the given key. pub fn get_by_label(&self, key: &str) -> Option<&Prop> { - self.position(key).and_then(|i| self.0.get(i)) + self.0.iter().find(|it| it.label.to_string() == key) } /// Pop the first prop with the given key. @@ -230,7 +246,7 @@ impl SortedPropList { if let Some(other_prop) = self.get_by_label(key) { return Err(syn::Error::new_spanned( &other_prop.label, - format!("`{}` can only be specified once", key), + format!("`{key}` can only be specified once"), )); } } @@ -282,7 +298,7 @@ impl SortedPropList { })) } } -impl Parse for SortedPropList { +impl Parse for PropList { fn parse(input: ParseStream) -> syn::Result { let mut props: Vec = Vec::new(); // Stop parsing props if a base expression preceded by `..` is reached @@ -293,7 +309,7 @@ impl Parse for SortedPropList { Ok(Self::new(props)) } } -impl Deref for SortedPropList { +impl Deref for PropList { type Target = [Prop]; fn deref(&self) -> &Self::Target { @@ -310,7 +326,7 @@ impl SpecialProps { const KEY_LABEL: &'static str = "key"; const REF_LABEL: &'static str = "ref"; - fn pop_from(props: &mut SortedPropList) -> syn::Result { + fn pop_from(props: &mut PropList) -> syn::Result { let node_ref = props.pop_unique(Self::REF_LABEL)?; let key = props.pop_unique(Self::KEY_LABEL)?; Ok(Self { node_ref, key }) @@ -325,19 +341,46 @@ impl SpecialProps { pub fn check_all(&self, f: impl FnMut(&Prop) -> syn::Result<()>) -> syn::Result<()> { crate::join_errors(self.iter().map(f).filter_map(Result::err)) } + + pub fn wrap_node_ref_attr(&self) -> TokenStream { + self.node_ref + .as_ref() + .map(|attr| { + let value = &attr.value; + quote_spanned! {value.span().resolved_at(Span::call_site())=> + ::yew::html::IntoPropValue::<::yew::html::NodeRef> + ::into_prop_value(#value) + } + }) + .unwrap_or(quote! { ::std::default::Default::default() }) + } + + pub fn wrap_key_attr(&self) -> TokenStream { + self.key + .as_ref() + .map(|attr| { + let value = attr.value.optimize_literals(); + quote_spanned! {value.span().resolved_at(Span::call_site())=> + ::std::option::Option::Some( + ::std::convert::Into::<::yew::virtual_dom::Key>::into(#value) + ) + } + }) + .unwrap_or(quote! { ::std::option::Option::None }) + } } pub struct Props { pub special: SpecialProps, - pub prop_list: SortedPropList, + pub prop_list: PropList, } impl Parse for Props { fn parse(input: ParseStream) -> syn::Result { - Self::try_from(input.parse::()?) + Self::try_from(input.parse::()?) } } impl Deref for Props { - type Target = SortedPropList; + type Target = PropList; fn deref(&self) -> &Self::Target { &self.prop_list @@ -349,10 +392,10 @@ impl DerefMut for Props { } } -impl TryFrom for Props { +impl TryFrom for Props { type Error = syn::Error; - fn try_from(mut prop_list: SortedPropList) -> Result { + fn try_from(mut prop_list: PropList) -> Result { let special = SpecialProps::pop_from(&mut prop_list)?; Ok(Self { special, prop_list }) } diff --git a/packages/yew-macro/src/props/prop_macro.rs b/packages/yew-macro/src/props/prop_macro.rs index 172b5f53be6..2bddcebceb0 100644 --- a/packages/yew-macro/src/props/prop_macro.rs +++ b/packages/yew-macro/src/props/prop_macro.rs @@ -8,7 +8,7 @@ use syn::spanned::Spanned; use syn::token::Brace; use syn::{Expr, Token, TypePath}; -use super::{ComponentProps, Prop, Props, SortedPropList}; +use super::{ComponentProps, Prop, PropList, Props}; use crate::html_tree::HtmlDashedName; /// Pop from `Punctuated` without leaving it in a state where it has trailing punctuation. @@ -61,7 +61,11 @@ impl Parse for PropValue { impl From for Prop { fn from(prop_value: PropValue) -> Prop { let PropValue { label, value } = prop_value; - Prop { label, value } + Prop { + label, + value, + directive: None, + } } } @@ -86,7 +90,7 @@ impl Parse for PropsExpr { let content; let brace_token = syn::braced!(content in input); - let fields = content.parse_terminated(PropValue::parse)?; + let fields = content.parse_terminated(PropValue::parse, Token![,])?; Ok(Self { ty, _brace_token: brace_token, @@ -102,7 +106,7 @@ pub struct PropsMacroInput { impl Parse for PropsMacroInput { fn parse(input: ParseStream) -> syn::Result { let PropsExpr { ty, fields, .. } = input.parse()?; - let prop_list = SortedPropList::new(fields.into_iter().map(Into::into).collect()); + let prop_list = PropList::new(fields.into_iter().map(Into::into).collect()); let props: Props = prop_list.try_into()?; props.special.check_all(|prop| { let label = &prop.label; diff --git a/packages/yew-macro/src/stringify.rs b/packages/yew-macro/src/stringify.rs index d1b995ed763..4ea9d6483c4 100644 --- a/packages/yew-macro/src/stringify.rs +++ b/packages/yew-macro/src/stringify.rs @@ -83,6 +83,7 @@ impl Stringify for Lit { Lit::Int(v) => v.base10_digits().to_string(), Lit::Float(v) => v.base10_digits().to_string(), Lit::Bool(_) | Lit::ByteStr(_) | Lit::Byte(_) | Lit::Verbatim(_) => return None, + _ => unreachable!("unknown Lit"), }; Some(LitStr::new(&s, self.span())) } diff --git a/packages/yew-macro/src/use_prepared_state.rs b/packages/yew-macro/src/use_prepared_state.rs index a65dab70099..737c2368c19 100644 --- a/packages/yew-macro/src/use_prepared_state.rs +++ b/packages/yew-macro/src/use_prepared_state.rs @@ -59,7 +59,7 @@ impl Parse for PreparedState { impl PreparedState { // Async closure is not stable, so we rewrite it to closure + async block - #[cfg(not(feature = "nightly"))] + #[cfg(not(nightly_yew))] pub fn rewrite_to_closure_with_async_block(&self) -> ExprClosure { use proc_macro2::Span; use syn::parse_quote; @@ -95,7 +95,7 @@ impl PreparedState { closure } - #[cfg(feature = "nightly")] + #[cfg(nightly_yew)] pub fn rewrite_to_closure_with_async_block(&self) -> ExprClosure { self.closure.clone() } diff --git a/packages/yew-macro/tests/classes_macro/classes-fail.stderr b/packages/yew-macro/tests/classes_macro/classes-fail.stderr index 9e1ec462e91..57cf0f21fb7 100644 --- a/packages/yew-macro/tests/classes_macro/classes-fail.stderr +++ b/packages/yew-macro/tests/classes_macro/classes-fail.stderr @@ -11,64 +11,76 @@ error: string literals must not contain more than one class (hint: use `"two", " | ^^^^^^^^^^^ error[E0277]: the trait bound `Classes: From<{integer}>` is not satisfied - --> tests/classes_macro/classes-fail.rs:4:14 - | -4 | classes!(42); - | ^^ the trait `From<{integer}>` is not implemented for `Classes` - | - = help: the following implementations were found: - > - >> - > - > - and 4 others - = note: required because of the requirements on the impl of `Into` for `{integer}` + --> tests/classes_macro/classes-fail.rs:4:14 + | +4 | classes!(42); + | ^^ the trait `From<{integer}>` is not implemented for `Classes` + | + = help: the following other types implement trait `From`: + > + >> + > + > + > + >> + >> + > + and $N others + = note: required because of the requirements on the impl of `Into` for `{integer}` note: required by a bound in `Classes::push` - --> $WORKSPACE/packages/yew/src/html/classes.rs - | - | pub fn push>(&mut self, class: T) { - | ^^^^^^^^^^ required by this bound in `Classes::push` + --> $WORKSPACE/packages/yew/src/html/classes.rs + | + | pub fn push>(&mut self, class: T) { + | ^^^^^^^^^^ required by this bound in `Classes::push` error[E0277]: the trait bound `Classes: From<{float}>` is not satisfied - --> tests/classes_macro/classes-fail.rs:5:14 - | -5 | classes!(42.0); - | ^^^^ the trait `From<{float}>` is not implemented for `Classes` - | - = help: the following implementations were found: - > - >> - > - > - and 4 others - = note: required because of the requirements on the impl of `Into` for `{float}` + --> tests/classes_macro/classes-fail.rs:5:14 + | +5 | classes!(42.0); + | ^^^^ the trait `From<{float}>` is not implemented for `Classes` + | + = help: the following other types implement trait `From`: + > + >> + > + > + > + >> + >> + > + and $N others + = note: required because of the requirements on the impl of `Into` for `{float}` note: required by a bound in `Classes::push` - --> $WORKSPACE/packages/yew/src/html/classes.rs - | - | pub fn push>(&mut self, class: T) { - | ^^^^^^^^^^ required by this bound in `Classes::push` + --> $WORKSPACE/packages/yew/src/html/classes.rs + | + | pub fn push>(&mut self, class: T) { + | ^^^^^^^^^^ required by this bound in `Classes::push` error[E0277]: the trait bound `Classes: From<{integer}>` is not satisfied - --> tests/classes_macro/classes-fail.rs:9:14 - | -9 | classes!(vec![42]); - | ^^^ the trait `From<{integer}>` is not implemented for `Classes` - | - = help: the following implementations were found: - > - >> - > - > - and 4 others - = note: required because of the requirements on the impl of `Into` for `{integer}` - = note: required because of the requirements on the impl of `From>` for `Classes` - = note: 1 redundant requirement hidden - = note: required because of the requirements on the impl of `Into` for `Vec<{integer}>` + --> tests/classes_macro/classes-fail.rs:9:14 + | +9 | classes!(vec![42]); + | ^^^ the trait `From<{integer}>` is not implemented for `Classes` + | + = help: the following other types implement trait `From`: + > + >> + > + > + > + >> + >> + > + and $N others + = note: required because of the requirements on the impl of `Into` for `{integer}` + = note: required because of the requirements on the impl of `From>` for `Classes` + = note: 1 redundant requirement hidden + = note: required because of the requirements on the impl of `Into` for `Vec<{integer}>` note: required by a bound in `Classes::push` - --> $WORKSPACE/packages/yew/src/html/classes.rs - | - | pub fn push>(&mut self, class: T) { - | ^^^^^^^^^^ required by this bound in `Classes::push` + --> $WORKSPACE/packages/yew/src/html/classes.rs + | + | pub fn push>(&mut self, class: T) { + | ^^^^^^^^^^ required by this bound in `Classes::push` error[E0277]: the trait bound `Classes: From<{integer}>` is not satisfied --> tests/classes_macro/classes-fail.rs:13:14 @@ -76,12 +88,16 @@ error[E0277]: the trait bound `Classes: From<{integer}>` is not satisfied 13 | classes!(some); | ^^^^ the trait `From<{integer}>` is not implemented for `Classes` | - = help: the following implementations were found: + = help: the following other types implement trait `From`: > >> > > - and 4 others + > + >> + >> + > + and $N others = note: required because of the requirements on the impl of `Into` for `{integer}` = note: required because of the requirements on the impl of `From>` for `Classes` = note: 1 redundant requirement hidden @@ -98,12 +114,16 @@ error[E0277]: the trait bound `Classes: From` is not satisfied 14 | classes!(none); | ^^^^ the trait `From` is not implemented for `Classes` | - = help: the following implementations were found: + = help: the following other types implement trait `From`: > >> > > - and 4 others + > + >> + >> + > + and $N others = note: required because of the requirements on the impl of `Into` for `u32` = note: required because of the requirements on the impl of `From>` for `Classes` = note: 1 redundant requirement hidden @@ -120,12 +140,16 @@ error[E0277]: the trait bound `Classes: From<{integer}>` is not satisfied 16 | classes!("one", 42); | ^^ the trait `From<{integer}>` is not implemented for `Classes` | - = help: the following implementations were found: + = help: the following other types implement trait `From`: > >> > > - and 4 others + > + >> + >> + > + and $N others = note: required because of the requirements on the impl of `Into` for `{integer}` note: required by a bound in `Classes::push` --> $WORKSPACE/packages/yew/src/html/classes.rs diff --git a/packages/yew-macro/tests/classes_macro_test.rs b/packages/yew-macro/tests/classes_macro_test.rs index 84b1b5fda48..e2cadf8cf39 100644 --- a/packages/yew-macro/tests/classes_macro_test.rs +++ b/packages/yew-macro/tests/classes_macro_test.rs @@ -1,5 +1,5 @@ #[allow(dead_code)] -#[rustversion::attr(stable(1.60), test)] +#[rustversion::attr(stable(1.64), test)] fn classes_macro() { let t = trybuild::TestCases::new(); t.pass("tests/classes_macro/*-pass.rs"); diff --git a/packages/yew-macro/tests/derive_props/fail.stderr b/packages/yew-macro/tests/derive_props/fail.stderr index 35c88342d29..441b184c78d 100644 --- a/packages/yew-macro/tests/derive_props/fail.stderr +++ b/packages/yew-macro/tests/derive_props/fail.stderr @@ -25,33 +25,18 @@ note: these functions exist but are inaccessible 102 | fn foo() -> i32 { | ^^^^^^^^^^^^^^^ `crate::t10::foo`: not accessible -error[E0277]: the trait bound `AssertAllProps: HasProp` is not satisfied - --> tests/derive_props/fail.rs:35:24 - | -35 | ::yew::props!{ Props { } }; - | ^^^^^ the trait `HasProp` is not implemented for `AssertAllProps` - | -note: required because of the requirements on the impl of `HasAllProps` for `t3::CheckPropsAll` - --> tests/derive_props/fail.rs:29:21 - | -29 | #[derive(Clone, Properties, PartialEq)] - | ^^^^^^^^^^ - = note: required because of the requirements on the impl of `AllPropsFor` for `AssertAllProps` -note: required by a bound in `html::component::properties::__macro::PreBuild::::build` - --> $WORKSPACE/packages/yew/src/html/component/properties.rs - | - | Token: AllPropsFor, - | ^^^^^^^^^^^^^^^^^^^ required by this bound in `html::component::properties::__macro::PreBuild::::build` - = note: this error originates in the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0277]: the trait bound `Value: Default` is not satisfied - --> tests/derive_props/fail.rs:9:21 - | -9 | #[derive(Clone, Properties, PartialEq)] - | ^^^^^^^^^^ the trait `Default` is not implemented for `Value` - | + --> tests/derive_props/fail.rs:9:21 + | +9 | #[derive(Clone, Properties, PartialEq)] + | ^^^^^^^^^^ the trait `Default` is not implemented for `Value` + | note: required by a bound in `Option::::unwrap_or_default` - = note: this error originates in the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider annotating `Value` with `#[derive(Default)]` + | +8 | #[derive(Default)] + | error[E0369]: binary operation `==` cannot be applied to type `Value` --> tests/derive_props/fail.rs:13:9 @@ -66,7 +51,7 @@ note: an implementation of `PartialEq<_>` might be missing for `Value` --> tests/derive_props/fail.rs:8:5 | 8 | struct Value; - | ^^^^^^^^^^^^^ must implement `PartialEq<_>` + | ^^^^^^^^^^^^ must implement `PartialEq<_>` = note: this error originates in the derive macro `PartialEq` (in Nightly builds, run with -Z macro-backtrace for more info) help: consider annotating `Value` with `#[derive(PartialEq)]` | @@ -86,19 +71,52 @@ note: an implementation of `PartialEq<_>` might be missing for `Value` --> tests/derive_props/fail.rs:8:5 | 8 | struct Value; - | ^^^^^^^^^^^^^ must implement `PartialEq<_>` + | ^^^^^^^^^^^^ must implement `PartialEq<_>` = note: this error originates in the derive macro `PartialEq` (in Nightly builds, run with -Z macro-backtrace for more info) help: consider annotating `Value` with `#[derive(PartialEq)]` | 8 | #[derive(PartialEq)] | +error[E0277]: the trait bound `AssertAllProps: HasProp` is not satisfied + --> tests/derive_props/fail.rs:35:24 + | +35 | ::yew::props!{ Props { } }; + | ^^^^^ the trait `HasProp` is not implemented for `AssertAllProps` + | + = help: the following other types implement trait `HasProp`: + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + and $N others +note: required because of the requirements on the impl of `HasAllProps` for `t3::CheckPropsAll` + --> tests/derive_props/fail.rs:29:21 + | +29 | #[derive(Clone, Properties, PartialEq)] + | ^^^^^^^^^^ + = note: required because of the requirements on the impl of `AllPropsFor` for `AssertAllProps` +note: required by a bound in `html::component::properties::__macro::PreBuild::::build` + --> $WORKSPACE/packages/yew/src/html/component/properties.rs + | + | Token: AllPropsFor, + | ^^^^^^^^^^^^^^^^^^^ required by this bound in `html::component::properties::__macro::PreBuild::::build` + = note: this error originates in the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0308]: mismatched types --> tests/derive_props/fail.rs:54:19 | 54 | #[prop_or(123)] - | ^^^ expected struct `String`, found integer + | ^^^ + | | + | expected struct `String`, found integer + | arguments to this function are incorrect | +note: associated function defined here help: try using a conversion method | 54 | #[prop_or(123.to_string())] @@ -107,30 +125,30 @@ help: try using a conversion method | ++++++++++++ error[E0277]: expected a `FnOnce<()>` closure, found `{integer}` - --> tests/derive_props/fail.rs:64:24 - | -64 | #[prop_or_else(123)] - | ^^^ expected an `FnOnce<()>` closure, found `{integer}` - | - = help: the trait `FnOnce<()>` is not implemented for `{integer}` - = note: wrap the `{integer}` in a closure with no arguments: `|| { /* code */ }` + --> tests/derive_props/fail.rs:64:24 + | +64 | #[prop_or_else(123)] + | ^^^ expected an `FnOnce<()>` closure, found `{integer}` + | + = help: the trait `FnOnce<()>` is not implemented for `{integer}` + = note: wrap the `{integer}` in a closure with no arguments: `|| { /* code */ }` note: required by a bound in `Option::::unwrap_or_else` error[E0593]: function is expected to take 0 arguments, but it takes 1 argument - --> tests/derive_props/fail.rs:84:24 - | -84 | #[prop_or_else(foo)] - | ^^^ expected function that takes 0 arguments + --> tests/derive_props/fail.rs:84:24 + | +84 | #[prop_or_else(foo)] + | ^^^ expected function that takes 0 arguments ... -88 | fn foo(bar: i32) -> String { - | -------------------------- takes 1 argument - | +88 | fn foo(bar: i32) -> String { + | -------------------------- takes 1 argument + | note: required by a bound in `Option::::unwrap_or_else` error[E0271]: type mismatch resolving ` i32 {t10::foo} as FnOnce<()>>::Output == String` - --> tests/derive_props/fail.rs:98:24 - | -98 | #[prop_or_else(foo)] - | ^^^ expected struct `String`, found `i32` - | + --> tests/derive_props/fail.rs:98:24 + | +98 | #[prop_or_else(foo)] + | ^^^ expected struct `String`, found `i32` + | note: required by a bound in `Option::::unwrap_or_else` diff --git a/packages/yew-macro/tests/derive_props_test.rs b/packages/yew-macro/tests/derive_props_test.rs index c4d8892e03c..c98e8feb953 100644 --- a/packages/yew-macro/tests/derive_props_test.rs +++ b/packages/yew-macro/tests/derive_props_test.rs @@ -1,5 +1,5 @@ #[allow(dead_code)] -#[rustversion::attr(stable(1.60), test)] +#[rustversion::attr(stable(1.64), test)] fn derive_props() { let t = trybuild::TestCases::new(); t.pass("tests/derive_props/pass.rs"); diff --git a/packages/yew-macro/tests/function_attr_test.rs b/packages/yew-macro/tests/function_attr_test.rs index d9409490389..ed3cf9d044a 100644 --- a/packages/yew-macro/tests/function_attr_test.rs +++ b/packages/yew-macro/tests/function_attr_test.rs @@ -1,5 +1,5 @@ #[allow(dead_code)] -#[rustversion::attr(stable(1.60), test)] +#[rustversion::attr(stable(1.64), test)] fn tests() { let t = trybuild::TestCases::new(); t.pass("tests/function_component_attr/*-pass.rs"); diff --git a/packages/yew-macro/tests/function_component_attr/bad-name-fail.stderr b/packages/yew-macro/tests/function_component_attr/bad-name-fail.stderr index 1a6c4c90058..6618a3f6633 100644 --- a/packages/yew-macro/tests/function_component_attr/bad-name-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/bad-name-fail.stderr @@ -1,4 +1,4 @@ -error: expected identifier +error: expected identifier, found keyword `let` --> tests/function_component_attr/bad-name-fail.rs:8:22 | 8 | #[function_component(let)] diff --git a/packages/yew-macro/tests/function_component_attr/bad-return-type-fail.stderr b/packages/yew-macro/tests/function_component_attr/bad-return-type-fail.stderr index 6e99e7d6473..9b8e8a2ab5d 100644 --- a/packages/yew-macro/tests/function_component_attr/bad-return-type-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/bad-return-type-fail.stderr @@ -10,4 +10,7 @@ error[E0277]: the trait bound `u32: IntoHtmlResult` is not satisfied 11 | #[function_component(Comp)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoHtmlResult` is not implemented for `u32` | + = help: the following other types implement trait `IntoHtmlResult`: + Result + VNode = note: this error originates in the attribute macro `function_component` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr b/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr index e298c0cc8ac..e60f09af641 100644 --- a/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr @@ -13,6 +13,16 @@ error[E0277]: the trait bound `AssertAllProps: HasProp` is not satisfied 22 | html! { /> }; | ^^^^ the trait `HasProp` is not implemented for `AssertAllProps` | + = help: the following other types implement trait `HasProp`: + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + and $N others note: required because of the requirements on the impl of `HasAllProps` for `CheckPropsAll` --> tests/function_component_attr/generic-props-fail.rs:3:17 | @@ -24,7 +34,7 @@ note: required by a bound in `yew::html::component::properties::__macro::PreBuil | | Token: AllPropsFor, | ^^^^^^^^^^^^^^^^^^^ required by this bound in `yew::html::component::properties::__macro::PreBuild::::build` - = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `html` which comes from the expansion of the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `Comp: yew::BaseComponent` is not satisfied --> tests/function_component_attr/generic-props-fail.rs:27:14 @@ -32,33 +42,26 @@ error[E0277]: the trait bound `Comp: yew::BaseComponent` is n 27 | html! { /> }; | ^^^^ the trait `yew::BaseComponent` is not implemented for `Comp` | - = help: the following implementations were found: - as yew::BaseComponent> + = help: the trait `yew::BaseComponent` is implemented for `Comp

    ` = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0599]: the function or associated item `new` exists for struct `VChild>`, but its trait bounds were not satisfied - --> tests/function_component_attr/generic-props-fail.rs:27:14 - | -8 | #[function_component(Comp)] - | --------------------------- doesn't satisfy `Comp: yew::BaseComponent` + --> tests/function_component_attr/generic-props-fail.rs:27:14 + | +8 | #[function_component(Comp)] + | ------------------------- doesn't satisfy `Comp: yew::BaseComponent` ... -27 | html! { /> }; - | ^^^^ function or associated item cannot be called on `VChild>` due to unsatisfied trait bounds - | - = note: the following trait bounds were not satisfied: - `Comp: yew::BaseComponent` +27 | html! { /> }; + | ^^^^ function or associated item cannot be called on `VChild>` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `Comp: yew::BaseComponent` note: the following trait must be implemented - --> $WORKSPACE/packages/yew/src/html/component/mod.rs - | - | / pub trait BaseComponent: Sized + 'static { - | | /// The Component's Message. - | | type Message: 'static; - | | -... | - | | fn prepare_state(&self) -> Option; - | | } - | |_^ - = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) + --> $WORKSPACE/packages/yew/src/html/component/mod.rs + | + | pub trait BaseComponent: Sized + 'static { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied --> tests/function_component_attr/generic-props-fail.rs:27:14 @@ -66,6 +69,12 @@ error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisf 27 | html! { /> }; | ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds` | + = help: the following other types implement trait `yew::Properties`: + () + ChildrenProps + ContextProviderProps + Props + SuspenseProps note: required by a bound in `Comp` --> tests/function_component_attr/generic-props-fail.rs:11:8 | diff --git a/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr b/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr index 7c65da37aaa..b9d144d7803 100644 --- a/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/hook_location-fail.stderr @@ -1,7 +1,7 @@ error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/function_component_attr/hook_location-fail.rs:9:9 | @@ -11,7 +11,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/function_component_attr/hook_location-fail.rs:14:9 | @@ -21,7 +21,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/function_component_attr/hook_location-fail.rs:19:9 | @@ -31,7 +31,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/function_component_attr/hook_location-fail.rs:22:26 | @@ -41,7 +41,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/function_component_attr/hook_location-fail.rs:23:9 | @@ -51,7 +51,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/function_component_attr/hook_location-fail.rs:27:20 | @@ -61,7 +61,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/function_component_attr/hook_location-fail.rs:34:9 | diff --git a/packages/yew-macro/tests/hook_attr/hook_location-fail.stderr b/packages/yew-macro/tests/hook_attr/hook_location-fail.stderr index 0836a6a1ac7..5042e390a35 100644 --- a/packages/yew-macro/tests/hook_attr/hook_location-fail.stderr +++ b/packages/yew-macro/tests/hook_attr/hook_location-fail.stderr @@ -1,7 +1,7 @@ error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/hook_attr/hook_location-fail.rs:9:9 | @@ -11,7 +11,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/hook_attr/hook_location-fail.rs:14:9 | @@ -21,7 +21,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/hook_attr/hook_location-fail.rs:19:9 | @@ -31,7 +31,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/hook_attr/hook_location-fail.rs:22:26 | @@ -41,7 +41,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/hook_attr/hook_location-fail.rs:23:9 | @@ -51,7 +51,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/hook_attr/hook_location-fail.rs:27:20 | @@ -61,7 +61,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/hook_attr/hook_location-fail.rs:34:9 | diff --git a/packages/yew-macro/tests/hook_attr/hook_macro-fail.stderr b/packages/yew-macro/tests/hook_attr/hook_macro-fail.stderr index 13e8c3ba574..d898838de3c 100644 --- a/packages/yew-macro/tests/hook_attr/hook_macro-fail.stderr +++ b/packages/yew-macro/tests/hook_attr/hook_macro-fail.stderr @@ -1,7 +1,7 @@ error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/hook_attr/hook_macro-fail.rs:20:9 | @@ -11,7 +11,7 @@ error: hooks cannot be called at this position. error: hooks cannot be called at this position. = help: move hooks to the top-level of your function. - = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + = note: see: https://yew.rs/docs/next/concepts/function-components/hooks --> tests/hook_attr/hook_macro-fail.rs:22:9 | diff --git a/packages/yew-macro/tests/hook_attr_test.rs b/packages/yew-macro/tests/hook_attr_test.rs index dae90940ee7..bd0a3b28812 100644 --- a/packages/yew-macro/tests/hook_attr_test.rs +++ b/packages/yew-macro/tests/hook_attr_test.rs @@ -1,5 +1,5 @@ #[allow(dead_code)] -#[rustversion::attr(stable(1.60), test)] +#[rustversion::attr(stable(1.64), test)] fn tests() { let t = trybuild::TestCases::new(); t.pass("tests/hook_attr/*-pass.rs"); diff --git a/packages/yew-macro/tests/hook_macro_test.rs b/packages/yew-macro/tests/hook_macro_test.rs index d860ad28821..01e81a41345 100644 --- a/packages/yew-macro/tests/hook_macro_test.rs +++ b/packages/yew-macro/tests/hook_macro_test.rs @@ -1,5 +1,5 @@ #[allow(dead_code)] -#[rustversion::attr(stable(1.56), test)] +#[rustversion::attr(stable(1.64), test)] fn tests() { let t = trybuild::TestCases::new(); t.pass("tests/hook_macro/*-pass.rs"); diff --git a/packages/yew-macro/tests/html_lints/fail.stderr b/packages/yew-macro/tests/html_lints/fail.stderr index 54698329b1d..64551afcdc0 100644 --- a/packages/yew-macro/tests/html_lints/fail.stderr +++ b/packages/yew-macro/tests/html_lints/fail.stderr @@ -1,27 +1,3 @@ -warning: All `` elements should have a `href` attribute. This makes it possible for assistive technologies to correctly interpret what your links point to. https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#more_on_links - --> tests/html_lints/fail.rs:5:10 - | -5 | { "I don't have a href attribute" } - | ^ - -warning: '#' is not a suitable value for the `href` attribute. Without a meaningful attribute assistive technologies will struggle to understand your webpage. https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#onclick_events - --> tests/html_lints/fail.rs:8:17 - | -8 | { "I have a malformed href attribute" } - | ^^^ - -warning: 'javascript:void(0)' is not a suitable value for the `href` attribute. Without a meaningful attribute assistive technologies will struggle to understand your webpage. https://developer.mozilla.org/en-US/docs/Learn/Accessibility/HTML#onclick_events - --> tests/html_lints/fail.rs:11:17 - | -11 | { "I have a malformed href attribute" } - | ^^^^^^^^^^^^^^^^^^^^ - -warning: All `` tags should have an `alt` attribute which provides a human-readable description - --> tests/html_lints/fail.rs:14:10 - | -14 | - | ^^^ - warning: The tag 'tExTAreA' is not matching its normalized form 'textarea'. If you want to keep this form, change this to a dynamic tag `@{"tExTAreA"}`. --> tests/html_lints/fail.rs:17:10 | diff --git a/packages/yew-macro/tests/html_lints_test.rs b/packages/yew-macro/tests/html_lints_test.rs index af71e2d826f..35ba0496c6d 100644 --- a/packages/yew-macro/tests/html_lints_test.rs +++ b/packages/yew-macro/tests/html_lints_test.rs @@ -1,5 +1,5 @@ #[allow(dead_code)] -#[cfg(feature = "lints")] +#[cfg(yew_lints)] #[rustversion::attr(nightly, test)] fn test_html_lints() { let t = trybuild::TestCases::new(); diff --git a/packages/yew-macro/tests/html_macro/block-fail.stderr b/packages/yew-macro/tests/html_macro/block-fail.stderr index 2ef39785479..e659ec66f26 100644 --- a/packages/yew-macro/tests/html_macro/block-fail.stderr +++ b/packages/yew-macro/tests/html_macro/block-fail.stderr @@ -13,21 +13,6 @@ error[E0277]: `()` doesn't implement `std::fmt::Display` = note: required because of the requirements on the impl of `Into>` for `()` = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: `()` doesn't implement `std::fmt::Display` - --> tests/html_macro/block-fail.rs:12:16 - | -12 |

    { not_tree() }
    - | ^^^^^^^^ `()` cannot be formatted with the default formatter - | - = help: the trait `std::fmt::Display` is not implemented for `()` - = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead - = note: required because of the requirements on the impl of `ToString` for `()` - = note: required because of the requirements on the impl of `From<()>` for `VNode` - = note: required because of the requirements on the impl of `Into` for `()` - = note: 2 redundant requirements hidden - = note: required because of the requirements on the impl of `Into>` for `()` - = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0277]: `()` doesn't implement `std::fmt::Display` --> tests/html_macro/block-fail.rs:15:17 | diff --git a/packages/yew-macro/tests/html_macro/component-any-children-pass.rs b/packages/yew-macro/tests/html_macro/component-any-children-pass.rs new file mode 100644 index 00000000000..cf5a924e506 --- /dev/null +++ b/packages/yew-macro/tests/html_macro/component-any-children-pass.rs @@ -0,0 +1,332 @@ +#![no_implicit_prelude] + +// Shadow primitives +#[allow(non_camel_case_types)] +pub struct bool; +#[allow(non_camel_case_types)] +pub struct char; +#[allow(non_camel_case_types)] +pub struct f32; +#[allow(non_camel_case_types)] +pub struct f64; +#[allow(non_camel_case_types)] +pub struct i128; +#[allow(non_camel_case_types)] +pub struct i16; +#[allow(non_camel_case_types)] +pub struct i32; +#[allow(non_camel_case_types)] +pub struct i64; +#[allow(non_camel_case_types)] +pub struct i8; +#[allow(non_camel_case_types)] +pub struct isize; +#[allow(non_camel_case_types)] +pub struct str; +#[allow(non_camel_case_types)] +pub struct u128; +#[allow(non_camel_case_types)] +pub struct u16; +#[allow(non_camel_case_types)] +pub struct u32; +#[allow(non_camel_case_types)] +pub struct u64; +#[allow(non_camel_case_types)] +pub struct u8; +#[allow(non_camel_case_types)] +pub struct usize; + +#[derive( + ::std::clone::Clone, ::yew::Properties, ::std::default::Default, ::std::cmp::PartialEq, +)] +pub struct ContainerProperties { + pub int: ::std::primitive::i32, + // You can use Html as Children. + #[prop_or_default] + pub children: ::yew::Html, + #[prop_or_default] + pub header: ::yew::Html, +} + +pub struct Container; +impl ::yew::Component for Container { + type Message = (); + type Properties = ContainerProperties; + + fn create(_ctx: &::yew::Context) -> Self { + ::std::unimplemented!() + } + + fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { + ::std::unimplemented!() + } +} + +#[derive(::std::clone::Clone, ::std::cmp::PartialEq)] +pub enum ChildrenVariants { + Child(::yew::virtual_dom::VChild), + AltChild(::yew::virtual_dom::VChild), +} + +impl ::std::convert::From<::yew::virtual_dom::VChild> for ChildrenVariants { + fn from(comp: ::yew::virtual_dom::VChild) -> Self { + ChildrenVariants::Child(comp) + } +} + +impl ::std::convert::From<::yew::virtual_dom::VChild> for ChildrenVariants { + fn from(comp: ::yew::virtual_dom::VChild) -> Self { + ChildrenVariants::AltChild(comp) + } +} + +impl ::std::convert::Into<::yew::virtual_dom::VNode> for ChildrenVariants { + fn into(self) -> ::yew::virtual_dom::VNode { + match self { + Self::Child(comp) => ::yew::virtual_dom::VNode::VComp(::std::convert::Into::< + ::yew::virtual_dom::VComp, + >::into(comp)), + Self::AltChild(comp) => ::yew::virtual_dom::VNode::VComp(::std::convert::Into::< + ::yew::virtual_dom::VComp, + >::into(comp)), + } + } +} + +#[derive( + ::std::clone::Clone, ::yew::Properties, ::std::default::Default, ::std::cmp::PartialEq, +)] +pub struct ChildProperties { + #[prop_or_default] + pub string: ::std::string::String, + #[prop_or_default] + pub r#fn: ::std::primitive::i32, + #[prop_or_default] + pub r#ref: ::yew::NodeRef, + pub int: ::std::primitive::i32, + #[prop_or_default] + pub opt_str: ::std::option::Option<::std::string::String>, + #[prop_or_default] + pub vec: ::std::vec::Vec<::std::primitive::i32>, + #[prop_or_default] + pub optional_callback: ::std::option::Option<::yew::Callback<()>>, +} + +pub struct Child; +impl ::yew::Component for Child { + type Message = (); + type Properties = ChildProperties; + + fn create(_ctx: &::yew::Context) -> Self { + ::std::unimplemented!() + } + + fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { + ::std::unimplemented!() + } +} + +pub struct AltChild; +impl ::yew::Component for AltChild { + type Message = (); + type Properties = (); + + fn create(_ctx: &::yew::Context) -> Self { + ::std::unimplemented!() + } + + fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { + ::std::unimplemented!() + } +} + +mod scoped { + pub use super::{Child, Container}; +} + +#[derive( + ::std::clone::Clone, ::yew::Properties, ::std::default::Default, ::std::cmp::PartialEq, +)] +pub struct RenderPropProps { + // You can use Callback<()> as Children. + #[prop_or_default] + pub children: ::yew::Callback<()>, +} + +#[::yew::function_component] +pub fn RenderPropComp(_props: &RenderPropProps) -> ::yew::Html { + ::yew::html! {} +} + +fn compile_pass() { + ::yew::html! { }; + ::yew::html! { }; + + ::yew::html! { + <> + + + + }; + + let props = <::Properties as ::std::default::Default>::default(); + let node_ref = <::yew::NodeRef as ::std::default::Default>::default(); + ::yew::html! { + <> + + + + + + ::Properties as ::std::default::Default>::default() /> + ::Properties as ::std::default::Default>::default() /> + + }; + + ::yew::html! { + <> + + + + + >::from("child")} int=1 /> + + + >::from("child")} int=1 /> + + >::from("child"))} int=1 /> + + }; + + let name_expr = "child"; + ::yew::html! { + + }; + + let string = "child"; + let int = 1; + ::yew::html! { + + }; + + ::yew::html! { + <> + + as ::std::convert::From<_>>::from(|_| ()))} /> + as ::std::convert::From<_>>::from(|_| ())} /> + >} /> + + }; + + let node_ref = <::yew::NodeRef as ::std::default::Default>::default(); + ::yew::html! { + <> + + + }; + + let int = 1; + let node_ref = <::yew::NodeRef as ::std::default::Default>::default(); + ::yew::html! { + <> + + + }; + + let props = <::Properties as ::std::default::Default>::default(); + let child_props = + <::Properties as ::std::default::Default>::default(); + ::yew::html! { + <> + + + + +
    { "hello world" }
    +
    + + +
    { "hello world" }
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + }} /> + + }; + + let variants = || -> ::std::vec::Vec { + ::std::vec![ + ChildrenVariants::Child(::yew::virtual_dom::VChild::new( + ::default(), + ::std::option::Option::None, + )), + ChildrenVariants::AltChild(::yew::virtual_dom::VChild::new( + (), + ::std::option::Option::None + )), + ] + }; + + ::yew::html! { + <> + { + ::std::iter::Iterator::collect::<::yew::virtual_dom::VNode>( + ::std::iter::Iterator::filter( + ::std::iter::IntoIterator::into_iter(variants()), + |c| match c { + ChildrenVariants::Child(_) => true, + _ => false, + } + ) + ) + } +
    + { + ::std::iter::Iterator::collect::<::yew::virtual_dom::VNode>( + ::std::iter::Iterator::filter( + ::std::iter::IntoIterator::into_iter(variants()), + |c| match c { + ChildrenVariants::AltChild(_) => true, + _ => false, + } + ) + ) + } +
    + + }; + + ::yew::html_nested! { 1 }; + + ::yew::html! { + + {|_arg| {}} + + }; +} +fn main() {} diff --git a/packages/yew-macro/tests/html_macro/component-fail.rs b/packages/yew-macro/tests/html_macro/component-fail.rs index e5c9e9985be..cdf9ccb34d2 100644 --- a/packages/yew-macro/tests/html_macro/component-fail.rs +++ b/packages/yew-macro/tests/html_macro/component-fail.rs @@ -65,6 +65,20 @@ fn compile_fail() { html! { }; html! { }; html! { }; + html! { }; + html! { }; + html! { }; + html! { }; + html! { }; + html! { }; + html! { }; + html! { }; + html! { }; + html! { }; + html! { }; + html! { }; + html! { }; + html! { }; html! { }; html! { }; html! { }; @@ -79,6 +93,8 @@ fn compile_fail() { html! { }; html! { }; html! { }; + html! { }; + html! { }; html! { }; html! { }; html! { }; @@ -132,4 +148,34 @@ fn not_expressions() { html! { }; } +fn mismatch_closing_tags() { + pub struct A; + impl Component for A { + type Message = (); + type Properties = (); + + fn create(_ctx: &Context) -> Self { + unimplemented!() + } + fn view(&self, _ctx: &Context) -> Html { + unimplemented!() + } + } + + pub struct B; + impl Component for B { + type Message = (); + type Properties = (); + + fn create(_ctx: &Context) -> Self { + unimplemented!() + } + fn view(&self, _ctx: &Context) -> Html { + unimplemented!() + } + } + let _ = html! { }; + let _ = html! { }; +} + fn main() {} diff --git a/packages/yew-macro/tests/html_macro/component-fail.stderr b/packages/yew-macro/tests/html_macro/component-fail.stderr index 3774f0a0182..371362c18bc 100644 --- a/packages/yew-macro/tests/html_macro/component-fail.stderr +++ b/packages/yew-macro/tests/html_macro/component-fail.stderr @@ -143,140 +143,263 @@ error: `with` doesn't have a value. (hint: set the value to `true` or `false` fo | ^^^^ error: `ref` can only be specified once - --> tests/html_macro/component-fail.rs:67:20 + --> tests/html_macro/component-fail.rs:67:29 | 67 | html! { }; - | ^^^ + | ^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> tests/html_macro/component-fail.rs:68:20 + | +68 | html! { }; + | ^^^^ error: base props expression must appear last in list of props + --> tests/html_macro/component-fail.rs:69:20 + | +69 | html! { }; + | ^^^^^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) --> tests/html_macro/component-fail.rs:70:20 | -70 | html! { }; +70 | html! { }; + | ^^^^ + +error: base props expression must appear last in list of props + --> tests/html_macro/component-fail.rs:71:20 + | +71 | html! { }; + | ^^^^^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> tests/html_macro/component-fail.rs:72:20 + | +72 | html! { }; + | ^^^^ + +error: base props expression must appear last in list of props + --> tests/html_macro/component-fail.rs:73:20 + | +73 | html! { }; + | ^^^^^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> tests/html_macro/component-fail.rs:74:20 + | +74 | html! { }; + | ^^^^ + +error: base props expression must appear last in list of props + --> tests/html_macro/component-fail.rs:75:20 + | +75 | html! { }; + | ^^^^^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> tests/html_macro/component-fail.rs:76:28 + | +76 | html! { }; + | ^^^^ + +error: base props expression must appear last in list of props + --> tests/html_macro/component-fail.rs:77:28 + | +77 | html! { }; + | ^^^^^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> tests/html_macro/component-fail.rs:78:39 + | +78 | html! { }; + | ^^^^ + +error: base props expression must appear last in list of props + --> tests/html_macro/component-fail.rs:79:39 + | +79 | html! { }; + | ^^^^^^^ + +error: `with` doesn't have a value. (hint: set the value to `true` or `false` for boolean attributes) + --> tests/html_macro/component-fail.rs:80:51 + | +80 | html! { }; + | ^^^^ + +error: `r#ref` can only be specified once but is given here again + --> tests/html_macro/component-fail.rs:81:31 + | +81 | html! { }; + | ^^^^^ + +error: base props expression must appear last in list of props + --> tests/html_macro/component-fail.rs:84:20 + | +84 | html! { }; | ^^^^^^^^ error: expected identifier, found keyword `type` - --> tests/html_macro/component-fail.rs:71:20 + --> tests/html_macro/component-fail.rs:85:20 | -71 | html! { }; +85 | html! { }; | ^^^^ expected identifier, found keyword | help: escape `type` to use it as an identifier | -71 | html! { }; +85 | html! { }; | ++ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. - --> tests/html_macro/component-fail.rs:72:24 +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple { + attrs: [], + paren_token: Paren, + elems: [], + } + --> tests/html_macro/component-fail.rs:86:24 | -72 | html! { }; +86 | html! { }; | ^^ error: expected a valid Rust identifier - --> tests/html_macro/component-fail.rs:73:20 + --> tests/html_macro/component-fail.rs:87:20 | -73 | html! { }; +87 | html! { }; | ^^^^^^^^^^^^^^^^^ error: expected an expression following this equals sign - --> tests/html_macro/component-fail.rs:75:26 + --> tests/html_macro/component-fail.rs:89:26 | -75 | html! { }; +89 | html! { }; | ^ error: `int` can only be specified once but is given here again - --> tests/html_macro/component-fail.rs:76:26 + --> tests/html_macro/component-fail.rs:90:26 | -76 | html! { }; +90 | html! { }; | ^^^ error: `int` can only be specified once but is given here again - --> tests/html_macro/component-fail.rs:76:32 + --> tests/html_macro/component-fail.rs:90:32 | -76 | html! { }; +90 | html! { }; | ^^^ +error: cannot use `ref` with components. If you want to specify a property, use `r#ref` here instead. + --> tests/html_macro/component-fail.rs:94:26 + | +94 | html! { }; + | ^^^ + error: `ref` can only be specified once - --> tests/html_macro/component-fail.rs:81:35 + --> tests/html_macro/component-fail.rs:95:35 | -81 | html! { }; +95 | html! { }; | ^^^ -error: this closing tag has no corresponding opening tag - --> tests/html_macro/component-fail.rs:84:13 +error: `r#ref` can only be specified once but is given here again + --> tests/html_macro/component-fail.rs:97:37 | -84 | html! { }; - | ^^^^^^^^ +97 | html! { }; + | ^^^^^ + +error: this closing tag has no corresponding opening tag + --> tests/html_macro/component-fail.rs:100:13 + | +100 | html! { }; + | ^^^^^^^^ error: this opening tag has no corresponding closing tag - --> tests/html_macro/component-fail.rs:85:13 - | -85 | html! { }; - | ^^^^^^^ + --> tests/html_macro/component-fail.rs:101:13 + | +101 | html! { }; + | ^^^^^^^ error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<>`) - --> tests/html_macro/component-fail.rs:86:28 - | -86 | html! { }; - | ^^^^^^^^^^^^^^^ - -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. - --> tests/html_macro/component-fail.rs:90:24 - | -90 | html! { }; - | ^^^ + --> tests/html_macro/component-fail.rs:102:28 + | +102 | html! { }; + | ^^^^^^^^^^^^^^^ + +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Path { + attrs: [], + qself: None, + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident { + ident: "num", + span: #0 bytes(3894..3897), + }, + arguments: PathArguments::None, + }, + ], + }, + } + --> tests/html_macro/component-fail.rs:106:24 + | +106 | html! { }; + | ^^^ error: cannot specify the `children` prop when the component already has children - --> tests/html_macro/component-fail.rs:108:26 + --> tests/html_macro/component-fail.rs:124:26 | -108 | +124 | | ^^^^^^^^ error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<>`) - --> tests/html_macro/component-fail.rs:115:9 + --> tests/html_macro/component-fail.rs:131:9 | -115 | { 2 } +131 | { 2 } | ^^^^^^^^^^^^^^^^^^ error: only simple identifiers are allowed in the shorthand property syntax - --> tests/html_macro/component-fail.rs:118:21 + --> tests/html_macro/component-fail.rs:134:21 | -118 | html! { }; +134 | html! { }; | ^^^^^^^^^^^^^^^^^^^^ error: missing label for property value. If trying to use the shorthand property syntax, only identifiers may be used - --> tests/html_macro/component-fail.rs:119:21 + --> tests/html_macro/component-fail.rs:135:21 | -119 | html! { }; +135 | html! { }; | ^^^^^ error: missing label for property value. If trying to use the shorthand property syntax, only identifiers may be used - --> tests/html_macro/component-fail.rs:120:21 + --> tests/html_macro/component-fail.rs:136:21 | -120 | html! { }; +136 | html! { }; | ^^^^^^^^^^^^^^ error: only an expression may be assigned as a property - --> tests/html_macro/component-fail.rs:131:34 + --> tests/html_macro/component-fail.rs:147:34 | -131 | html! { }; +147 | html! { }; | ^^^^^^^^^^^^^^^^^^^^^^^^ -error: only an expression may be assigned as a property. Consider removing this semicolon - --> tests/html_macro/component-fail.rs:132:61 +error: mismatched closing tags: expected `A`, found `B` + --> tests/html_macro/component-fail.rs:177:22 | -132 | html! { }; - | ^ +177 | let _ = html! { }; + | ^^^^^ + +error: expected a valid closing tag for component + note: found opening tag `` + help: try `` + --> tests/html_macro/component-fail.rs:178:24 + | +178 | let _ = html! { }; + | ^^^ error[E0425]: cannot find value `blah` in this scope - --> tests/html_macro/component-fail.rs:68:22 + --> tests/html_macro/component-fail.rs:82:22 | -68 | html! { }; +82 | html! { }; | ^^^^ not found in this scope error[E0425]: cannot find value `props` in this scope - --> tests/html_macro/component-fail.rs:69:30 + --> tests/html_macro/component-fail.rs:83:30 | -69 | html! { }; +83 | html! { }; | ^^^^^ not found in this scope error[E0308]: mismatched types @@ -291,55 +414,65 @@ error[E0308]: mismatched types found struct `std::ops::Range<_>` error[E0609]: no field `value` on type `ChildProperties` - --> tests/html_macro/component-fail.rs:69:20 + --> tests/html_macro/component-fail.rs:83:20 | -69 | html! { }; +83 | html! { }; | ^^^^^ unknown field | = note: available fields are: `string`, `int` error[E0609]: no field `r#type` on type `ChildProperties` - --> tests/html_macro/component-fail.rs:71:20 + --> tests/html_macro/component-fail.rs:85:20 | -71 | html! { }; +85 | html! { }; | ^^^^ unknown field | = note: available fields are: `string`, `int` error[E0599]: no method named `r#type` found for struct `ChildPropertiesBuilder` in the current scope - --> tests/html_macro/component-fail.rs:71:20 + --> tests/html_macro/component-fail.rs:85:20 | 4 | #[derive(Clone, Properties, PartialEq)] - | ---------- method `r#type` not found for this + | ---------- method `r#type` not found for this struct ... -71 | html! { }; +85 | html! { }; | ^^^^ method not found in `ChildPropertiesBuilder` error[E0609]: no field `unknown` on type `ChildProperties` - --> tests/html_macro/component-fail.rs:74:20 + --> tests/html_macro/component-fail.rs:88:20 | -74 | html! { }; +88 | html! { }; | ^^^^^^^ unknown field | = note: available fields are: `string`, `int` error[E0599]: no method named `unknown` found for struct `ChildPropertiesBuilder` in the current scope - --> tests/html_macro/component-fail.rs:74:20 + --> tests/html_macro/component-fail.rs:88:20 | 4 | #[derive(Clone, Properties, PartialEq)] - | ---------- method `unknown` not found for this + | ---------- method `unknown` not found for this struct ... -74 | html! { }; +88 | html! { }; | ^^^^^^^ method not found in `ChildPropertiesBuilder` error[E0277]: the trait bound `(): IntoPropValue` is not satisfied - --> tests/html_macro/component-fail.rs:77:33 + --> tests/html_macro/component-fail.rs:91:33 | -77 | html! { }; +91 | html! { }; | ------ ^^ the trait `IntoPropValue` is not implemented for `()` | | | required by a bound introduced by this call | + = help: the following other types implement trait `IntoPropValue`: + <&'static [(K, V)] as IntoPropValue>> + <&'static [T] as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue> + <&T as IntoPropValue>> + and $N others note: required by a bound in `ChildPropertiesBuilder::string` --> tests/html_macro/component-fail.rs:4:17 | @@ -351,13 +484,23 @@ note: required by a bound in `ChildPropertiesBuilder::string` = note: this error originates in the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `{integer}: IntoPropValue` is not satisfied - --> tests/html_macro/component-fail.rs:78:33 + --> tests/html_macro/component-fail.rs:92:33 | -78 | html! { }; +92 | html! { }; | ------ ^ the trait `IntoPropValue` is not implemented for `{integer}` | | | required by a bound introduced by this call | + = help: the following other types implement trait `IntoPropValue`: + f32 + f64 + i128 + i16 + i32 + i64 + i8 + isize + and $N others note: required by a bound in `ChildPropertiesBuilder::string` --> tests/html_macro/component-fail.rs:4:17 | @@ -369,13 +512,23 @@ note: required by a bound in `ChildPropertiesBuilder::string` = note: this error originates in the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `{integer}: IntoPropValue` is not satisfied - --> tests/html_macro/component-fail.rs:79:34 + --> tests/html_macro/component-fail.rs:93:34 | -79 | html! { }; +93 | html! { }; | ------ ^ the trait `IntoPropValue` is not implemented for `{integer}` | | | required by a bound introduced by this call | + = help: the following other types implement trait `IntoPropValue`: + f32 + f64 + i128 + i16 + i32 + i64 + i8 + isize + and $N others note: required by a bound in `ChildPropertiesBuilder::string` --> tests/html_macro/component-fail.rs:4:17 | @@ -386,20 +539,41 @@ note: required by a bound in `ChildPropertiesBuilder::string` | ------ required by a bound in this = note: this error originates in the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0308]: mismatched types - --> tests/html_macro/component-fail.rs:80:31 +error[E0609]: no field `r#ref` on type `ChildProperties` + --> tests/html_macro/component-fail.rs:96:26 + | +96 | html! { }; + | ^^^^^ unknown field | -80 | html! { }; - | ^^ expected struct `NodeRef`, found `()` + = note: available fields are: `string`, `int` + +error[E0599]: no method named `r#ref` found for struct `ChildPropertiesBuilder` in the current scope + --> tests/html_macro/component-fail.rs:96:26 + | +4 | #[derive(Clone, Properties, PartialEq)] + | ---------- method `r#ref` not found for this struct +... +96 | html! { }; + | ^^^^^ method not found in `ChildPropertiesBuilder` error[E0277]: the trait bound `u32: IntoPropValue` is not satisfied - --> tests/html_macro/component-fail.rs:82:24 + --> tests/html_macro/component-fail.rs:98:24 | -82 | html! { }; +98 | html! { }; | --- ^^^^ the trait `IntoPropValue` is not implemented for `u32` | | | required by a bound introduced by this call | + = help: the following other types implement trait `IntoPropValue`: + f32 + f64 + i128 + i16 + i32 + i64 + i8 + isize + and $N others note: required by a bound in `ChildPropertiesBuilder::int` --> tests/html_macro/component-fail.rs:4:17 | @@ -411,11 +585,21 @@ note: required by a bound in `ChildPropertiesBuilder::int` = note: this error originates in the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `AssertAllProps: HasProp` is not satisfied - --> tests/html_macro/component-fail.rs:83:14 + --> tests/html_macro/component-fail.rs:99:14 | -83 | html! { }; +99 | html! { }; | ^^^^^ the trait `HasProp` is not implemented for `AssertAllProps` | + = help: the following other types implement trait `HasProp`: + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp<_ChildContainerProperties::children, HasChildContainerPropertieschildren>> + as HasProp>> + and $N others note: required because of the requirements on the impl of `HasAllProps` for `CheckChildPropertiesAll` --> tests/html_macro/component-fail.rs:4:17 | @@ -427,62 +611,82 @@ note: required by a bound in `yew::html::component::properties::__macro::PreBuil | | Token: AllPropsFor, | ^^^^^^^^^^^^^^^^^^^ required by this bound in `yew::html::component::properties::__macro::PreBuild::::build` - = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `html` which comes from the expansion of the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0609]: no field `children` on type `ChildProperties` - --> tests/html_macro/component-fail.rs:87:14 - | -87 | html! { { "Not allowed" } }; - | ^^^^^ unknown field - | - = note: available fields are: `string`, `int` - = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) + --> tests/html_macro/component-fail.rs:103:14 + | +103 | html! { { "Not allowed" } }; + | ^^^^^ unknown field + | + = note: available fields are: `string`, `int` + = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0599]: no method named `children` found for struct `ChildPropertiesBuilder` in the current scope - --> tests/html_macro/component-fail.rs:87:14 - | -4 | #[derive(Clone, Properties, PartialEq)] - | ---------- method `children` not found for this + --> tests/html_macro/component-fail.rs:103:14 + | +4 | #[derive(Clone, Properties, PartialEq)] + | ---------- method `children` not found for this struct ... -87 | html! { { "Not allowed" } }; - | ^^^^^ method not found in `ChildPropertiesBuilder` - | - = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) +103 | html! { { "Not allowed" } }; + | ^^^^^ method not found in `ChildPropertiesBuilder` + | + = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0609]: no field `children` on type `ChildProperties` - --> tests/html_macro/component-fail.rs:94:10 - | -94 | - | ^^^^^ unknown field - | - = note: available fields are: `string`, `int` - = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) + --> tests/html_macro/component-fail.rs:110:10 + | +110 | + | ^^^^^ unknown field + | + = note: available fields are: `string`, `int` + = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `AssertAllProps: HasProp<_ChildContainerProperties::children, _>` is not satisfied - --> tests/html_macro/component-fail.rs:99:14 - | -99 | html! { }; - | ^^^^^^^^^^^^^^ the trait `HasProp<_ChildContainerProperties::children, _>` is not implemented for `AssertAllProps` - | + --> tests/html_macro/component-fail.rs:115:14 + | +115 | html! { }; + | ^^^^^^^^^^^^^^ the trait `HasProp<_ChildContainerProperties::children, _>` is not implemented for `AssertAllProps` + | + = help: the following other types implement trait `HasProp`: + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp<_ChildContainerProperties::children, HasChildContainerPropertieschildren>> + as HasProp>> + and $N others note: required because of the requirements on the impl of `HasAllProps` for `CheckChildContainerPropertiesAll` - --> tests/html_macro/component-fail.rs:24:17 - | -24 | #[derive(Clone, Properties, PartialEq)] - | ^^^^^^^^^^ - = note: required because of the requirements on the impl of `AllPropsFor` for `AssertAllProps` + --> tests/html_macro/component-fail.rs:24:17 + | +24 | #[derive(Clone, Properties, PartialEq)] + | ^^^^^^^^^^ + = note: required because of the requirements on the impl of `AllPropsFor` for `AssertAllProps` note: required by a bound in `yew::html::component::properties::__macro::PreBuild::::build` - --> $WORKSPACE/packages/yew/src/html/component/properties.rs - | - | Token: AllPropsFor, - | ^^^^^^^^^^^^^^^^^^^ required by this bound in `yew::html::component::properties::__macro::PreBuild::::build` - = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) + --> $WORKSPACE/packages/yew/src/html/component/properties.rs + | + | Token: AllPropsFor, + | ^^^^^^^^^^^^^^^^^^^ required by this bound in `yew::html::component::properties::__macro::PreBuild::::build` + = note: this error originates in the macro `html` which comes from the expansion of the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `AssertAllProps: HasProp<_ChildContainerProperties::children, _>` is not satisfied - --> tests/html_macro/component-fail.rs:100:14 + --> tests/html_macro/component-fail.rs:116:14 | -100 | html! { }; +116 | html! { }; | ^^^^^^^^^^^^^^ the trait `HasProp<_ChildContainerProperties::children, _>` is not implemented for `AssertAllProps` | + = help: the following other types implement trait `HasProp`: + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp>> + as HasProp<_ChildContainerProperties::children, HasChildContainerPropertieschildren>> + as HasProp>> + and $N others note: required because of the requirements on the impl of `HasAllProps` for `CheckChildContainerPropertiesAll` --> tests/html_macro/component-fail.rs:24:17 | @@ -494,28 +698,46 @@ note: required by a bound in `yew::html::component::properties::__macro::PreBuil | | Token: AllPropsFor, | ^^^^^^^^^^^^^^^^^^^ required by this bound in `yew::html::component::properties::__macro::PreBuild::::build` - = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `html` which comes from the expansion of the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: the trait bound `VChild: From` is not satisfied - --> tests/html_macro/component-fail.rs:101:31 +error[E0277]: the trait bound `yew::virtual_dom::VText: IntoPropValue>>` is not satisfied + --> tests/html_macro/component-fail.rs:117:14 | -101 | html! { { "Not allowed" } }; - | ^^^^^^^^^^^^^ the trait `From` is not implemented for `VChild` +117 | html! { { "Not allowed" } }; + | ^^^^^^^^^^^^^^ the trait `IntoPropValue>>` is not implemented for `yew::virtual_dom::VText` | - = note: required because of the requirements on the impl of `Into>` for `yew::virtual_dom::VText` + = help: the trait `IntoPropValue>` is implemented for `yew::virtual_dom::VText` +note: required by a bound in `ChildContainerPropertiesBuilder::children` + --> tests/html_macro/component-fail.rs:24:17 + | +24 | #[derive(Clone, Properties, PartialEq)] + | ^^^^^^^^^^ required by this bound in `ChildContainerPropertiesBuilder::children` +25 | pub struct ChildContainerProperties { +26 | pub children: ChildrenWithProps, + | -------- required by a bound in this + = note: this error originates in the macro `html` which comes from the expansion of the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `VChild: From` is not satisfied - --> tests/html_macro/component-fail.rs:102:29 + --> tests/html_macro/component-fail.rs:118:29 | -102 | html! { <> }; +118 | html! { <> }; | ^ the trait `From` is not implemented for `VChild` | = note: required because of the requirements on the impl of `Into>` for `VNode` -error[E0277]: the trait bound `VChild: From` is not satisfied - --> tests/html_macro/component-fail.rs:103:30 +error[E0277]: the trait bound `VNode: IntoPropValue>>` is not satisfied + --> tests/html_macro/component-fail.rs:119:14 | -103 | html! { }; - | ^^^^^ the trait `From` is not implemented for `VChild` +119 | html! { }; + | ^^^^^^^^^^^^^^ the trait `IntoPropValue>>` is not implemented for `VNode` | - = note: required because of the requirements on the impl of `Into>` for `VNode` + = help: the trait `IntoPropValue>` is implemented for `VNode` +note: required by a bound in `ChildContainerPropertiesBuilder::children` + --> tests/html_macro/component-fail.rs:24:17 + | +24 | #[derive(Clone, Properties, PartialEq)] + | ^^^^^^^^^^ required by this bound in `ChildContainerPropertiesBuilder::children` +25 | pub struct ChildContainerProperties { +26 | pub children: ChildrenWithProps, + | -------- required by a bound in this + = note: this error originates in the macro `html` which comes from the expansion of the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/packages/yew-macro/tests/html_macro/component-pass.rs b/packages/yew-macro/tests/html_macro/component-pass.rs index 707c08f5f7c..61f0de060f8 100644 --- a/packages/yew-macro/tests/html_macro/component-pass.rs +++ b/packages/yew-macro/tests/html_macro/component-pass.rs @@ -55,6 +55,7 @@ impl ::yew::Component for Container { fn create(_ctx: &::yew::Context) -> Self { ::std::unimplemented!() } + fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { ::std::unimplemented!() } @@ -99,6 +100,8 @@ pub struct ChildProperties { pub string: ::std::string::String, #[prop_or_default] pub r#fn: ::std::primitive::i32, + #[prop_or_default] + pub r#ref: ::yew::NodeRef, pub int: ::std::primitive::i32, #[prop_or_default] pub opt_str: ::std::option::Option<::std::string::String>, @@ -116,6 +119,7 @@ impl ::yew::Component for Child { fn create(_ctx: &::yew::Context) -> Self { ::std::unimplemented!() } + fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { ::std::unimplemented!() } @@ -129,6 +133,7 @@ impl ::yew::Component for AltChild { fn create(_ctx: &::yew::Context) -> Self { ::std::unimplemented!() } + fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { ::std::unimplemented!() } @@ -151,14 +156,14 @@ impl ::yew::Component for ChildContainer { fn create(_ctx: &::yew::Context) -> Self { ::std::unimplemented!() } + fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { ::std::unimplemented!() } } mod scoped { - pub use super::Child; - pub use super::Container; + pub use super::{Child, Container}; } fn compile_pass() { @@ -178,10 +183,11 @@ fn compile_pass() { <> - - - - ::Properties as ::std::default::Default>::default() /> + + + + ::Properties as ::std::default::Default>::default() /> + ::Properties as ::std::default::Default>::default() /> }; @@ -223,7 +229,7 @@ fn compile_pass() { let node_ref = <::yew::NodeRef as ::std::default::Default>::default(); ::yew::html! { <> - + }; @@ -231,7 +237,7 @@ fn compile_pass() { let node_ref = <::yew::NodeRef as ::std::default::Default>::default(); ::yew::html! { <> - + }; @@ -328,12 +334,10 @@ fn compile_pass() { ::std::vec![ ChildrenVariants::Child(::yew::virtual_dom::VChild::new( ::default(), - <::yew::NodeRef as ::std::default::Default>::default(), ::std::option::Option::None, )), ChildrenVariants::AltChild(::yew::virtual_dom::VChild::new( (), - <::yew::NodeRef as ::std::default::Default>::default(), ::std::option::Option::None )), ] @@ -371,4 +375,46 @@ fn compile_pass() { ::yew::html_nested! { 1 }; } +#[derive( + ::std::clone::Clone, ::yew::Properties, ::std::default::Default, ::std::cmp::PartialEq, +)] +pub struct HtmlPassedAsPropProperties { + pub value: ::yew::Html, +} + +pub struct HtmlPassedAsProp; +impl ::yew::Component for HtmlPassedAsProp { + type Message = (); + type Properties = HtmlPassedAsPropProperties; + + fn create(_ctx: &::yew::Context) -> Self { + ::std::unimplemented!() + } + + fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { + ::std::unimplemented!() + } +} + +pub struct HtmlPassedAsPropContainer; +impl ::yew::Component for HtmlPassedAsPropContainer { + type Message = (); + type Properties = (); + + fn create(_ctx: &::yew::Context) -> Self { + ::std::unimplemented!() + } + + fn view(&self, _ctx: &::yew::Context) -> ::yew::Html { + ::yew::html! { + <> + + + + + + } + } +} + fn main() {} diff --git a/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr b/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr index 480c0693b39..b229ced60b6 100644 --- a/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr +++ b/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr @@ -4,29 +4,24 @@ error[E0277]: the trait bound `Unimplemented: yew::Component` is not satisfied 6 | html! { }; | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Unimplemented` | + = help: the trait `yew::Component` is implemented for `ContextProvider` = note: required because of the requirements on the impl of `BaseComponent` for `Unimplemented` = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0599]: the function or associated item `new` exists for struct `VChild`, but its trait bounds were not satisfied - --> tests/html_macro/component-unimplemented-fail.rs:6:14 - | -3 | struct Unimplemented; - | --------------------- doesn't satisfy `Unimplemented: BaseComponent` + --> tests/html_macro/component-unimplemented-fail.rs:6:14 + | +3 | struct Unimplemented; + | -------------------- doesn't satisfy `Unimplemented: BaseComponent` ... -6 | html! { }; - | ^^^^^^^^^^^^^ function or associated item cannot be called on `VChild` due to unsatisfied trait bounds - | - = note: the following trait bounds were not satisfied: - `Unimplemented: BaseComponent` +6 | html! { }; + | ^^^^^^^^^^^^^ function or associated item cannot be called on `VChild` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `Unimplemented: BaseComponent` note: the following trait must be implemented - --> $WORKSPACE/packages/yew/src/html/component/mod.rs - | - | / pub trait BaseComponent: Sized + 'static { - | | /// The Component's Message. - | | type Message: 'static; - | | -... | - | | fn prepare_state(&self) -> Option; - | | } - | |_^ - = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) + --> $WORKSPACE/packages/yew/src/html/component/mod.rs + | + | pub trait BaseComponent: Sized + 'static { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/packages/yew-macro/tests/html_macro/element-fail.stderr b/packages/yew-macro/tests/html_macro/element-fail.stderr index 089444ed8ba..1946d88f0f1 100644 --- a/packages/yew-macro/tests/html_macro/element-fail.stderr +++ b/packages/yew-macro/tests/html_macro/element-fail.stderr @@ -101,24 +101,24 @@ error: `class` can only be specified once but is given here again | ^^^^^ error: `ref` can only be specified once - --> tests/html_macro/element-fail.rs:33:20 + --> tests/html_macro/element-fail.rs:33:29 | 33 | html! { }; - | ^^^ + | ^^^ error: `ref` can only be specified once - --> tests/html_macro/element-fail.rs:63:20 + --> tests/html_macro/element-fail.rs:63:29 | 63 | html! { }; - | ^^^ + | ^^^ -error: the tag `` is a void element and cannot have children (hint: rewrite this as ``) +error: the tag `` is a void element and cannot have children (hint: rewrite this as ``) --> tests/html_macro/element-fail.rs:66:13 | 66 | html! { }; | ^^^^^^^^^^^^^^^^^^^ -error: the tag `` is a void element and cannot have children (hint: rewrite this as ``) +error: the tag `` is a void element and cannot have children (hint: rewrite this as ``) --> tests/html_macro/element-fail.rs:68:13 | 68 | html! { }; @@ -142,61 +142,216 @@ error: dynamic closing tags must not have a body (hint: replace it with just ` }; | ^^^^^^^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple { + attrs: [], + paren_token: Paren, + elems: [ + Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "deprecated", + }, + }, + Comma, + Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "warning", + }, + }, + ], + } --> tests/html_macro/element-fail.rs:83:24 | 83 | html! {
    }; | ^^^^^^^^^^^^^^^^^^^^^^^^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple { + attrs: [], + paren_token: Paren, + elems: [], + } --> tests/html_macro/element-fail.rs:84:24 | 84 | html! { }; | ^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple { + attrs: [], + paren_token: Paren, + elems: [], + } --> tests/html_macro/element-fail.rs:85:24 | 85 | html! { }; | ^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Call { + attrs: [], + func: Expr::Path { + attrs: [], + qself: None, + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident { + ident: "Some", + span: #0 bytes(2632..2636), + }, + arguments: PathArguments::None, + }, + ], + }, + }, + paren_token: Paren, + args: [ + Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 5, + }, + }, + ], + } --> tests/html_macro/element-fail.rs:86:28 | 86 | html! { }; | ^^^^^^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Path { + attrs: [], + qself: None, + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident { + ident: "NotToString", + span: #0 bytes(2672..2683), + }, + arguments: PathArguments::None, + }, + ], + }, + } --> tests/html_macro/element-fail.rs:87:27 | 87 | html! { }; | ^^^^^^^^^^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Call { + attrs: [], + func: Expr::Path { + attrs: [], + qself: None, + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident { + ident: "Some", + span: #0 bytes(2711..2715), + }, + arguments: PathArguments::None, + }, + ], + }, + }, + paren_token: Paren, + args: [ + Expr::Path { + attrs: [], + qself: None, + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident { + ident: "NotToString", + span: #0 bytes(2716..2727), + }, + arguments: PathArguments::None, + }, + ], + }, + }, + ], + } --> tests/html_macro/element-fail.rs:88:22 | 88 | html! { }; | ^^^^^^^^^^^^^^^^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Call { + attrs: [], + func: Expr::Path { + attrs: [], + qself: None, + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident { + ident: "Some", + span: #0 bytes(2755..2759), + }, + arguments: PathArguments::None, + }, + ], + }, + }, + paren_token: Paren, + args: [ + Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 5, + }, + }, + ], + } --> tests/html_macro/element-fail.rs:89:21 | 89 | html! { }; | ^^^^^^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple { + attrs: [], + paren_token: Paren, + elems: [], + } --> tests/html_macro/element-fail.rs:90:25 | 90 | html! { }; | ^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Tuple { + attrs: [], + paren_token: Paren, + elems: [], + } --> tests/html_macro/element-fail.rs:91:26 | 91 | html! { }; | ^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::Path { + attrs: [], + qself: None, + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident { + ident: "NotToString", + span: #0 bytes(2862..2873), + }, + arguments: PathArguments::None, + }, + ], + }, + } --> tests/html_macro/element-fail.rs:92:27 | 92 | html! { }; @@ -214,22 +369,40 @@ error[E0308]: mismatched types --> tests/html_macro/element-fail.rs:36:28 | 36 | html! { }; - | ^ expected `bool`, found integer + | -----------------------^----- + | | | + | | expected `bool`, found integer + | arguments to this enum variant are incorrect + | +note: tuple variant defined here error[E0308]: mismatched types --> tests/html_macro/element-fail.rs:37:29 | 37 | html! { }; - | ^^^^^^^^^^^ expected `bool`, found enum `Option` + | ------------------------^^^^^^^^^^^------ + | | | + | | expected `bool`, found enum `Option` + | arguments to this enum variant are incorrect | = note: expected type `bool` found enum `Option` +note: tuple variant defined here error[E0308]: mismatched types --> tests/html_macro/element-fail.rs:38:29 | 38 | html! { }; - | ^ expected `bool`, found integer + | ^ + | | + | expected `bool`, found integer + | arguments to this function are incorrect + | +note: function defined here + --> $WORKSPACE/packages/yew/src/utils/mod.rs + | + | pub fn __ensure_type(_: T) {} + | ^^^^^^^^^^^^^ error[E0308]: mismatched types --> tests/html_macro/element-fail.rs:39:30 @@ -244,214 +417,303 @@ error[E0308]: mismatched types --> tests/html_macro/element-fail.rs:40:30 | 40 | html! { }; | ^^ the trait `IntoPropValue>` is not implemented for `()` + | + = help: the following other types implement trait `IntoPropValue`: + <&'static [(K, V)] as IntoPropValue>> + <&'static [T] as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue> + <&T as IntoPropValue>> + and $N others error[E0277]: the trait bound `NotToString: IntoPropValue>` is not satisfied --> tests/html_macro/element-fail.rs:46:28 | 46 | html! { }; | ^^^^^^^^^^^ the trait `IntoPropValue>` is not implemented for `NotToString` + | + = help: the following other types implement trait `IntoPropValue`: + <&'static [(K, V)] as IntoPropValue>> + <&'static [T] as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue> + <&T as IntoPropValue>> + and $N others error[E0277]: the trait bound `Option: IntoPropValue>` is not satisfied --> tests/html_macro/element-fail.rs:47:23 | 47 | html! { }; - | ----^^^^^^^^^^^^^ - | | - | the trait `IntoPropValue>` is not implemented for `Option` - | required by a bound introduced by this call + | ^^^^ the trait `IntoPropValue>` is not implemented for `Option` | - = help: the following implementations were found: + = help: the following other types implement trait `IntoPropValue`: as IntoPropValue>> as IntoPropValue>> as IntoPropValue>>> > as IntoPropValue>> - and 4 others + as IntoPropValue>> + > as IntoPropValue>>> error[E0277]: the trait bound `Option<{integer}>: IntoPropValue>` is not satisfied --> tests/html_macro/element-fail.rs:48:22 | 48 | html! { }; - | ----^^^ - | | - | the trait `IntoPropValue>` is not implemented for `Option<{integer}>` - | required by a bound introduced by this call + | ^^^^ the trait `IntoPropValue>` is not implemented for `Option<{integer}>` | - = help: the following implementations were found: + = help: the following other types implement trait `IntoPropValue`: as IntoPropValue>> as IntoPropValue>> as IntoPropValue>>> > as IntoPropValue>> - and 4 others + as IntoPropValue>> + > as IntoPropValue>>> error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `{integer}` - --> tests/html_macro/element-fail.rs:51:28 - | -51 | html! { }; - | -----------------------^----- - | | | - | | expected an `Fn<(MouseEvent,)>` closure, found `{integer}` - | required by a bound introduced by this call - | - = help: the trait `Fn<(MouseEvent,)>` is not implemented for `{integer}` - = note: required because of the requirements on the impl of `IntoEventCallback` for `{integer}` + --> tests/html_macro/element-fail.rs:51:28 + | +51 | html! { }; + | -----------------------^----- + | | | + | | expected an `Fn<(MouseEvent,)>` closure, found `{integer}` + | required by a bound introduced by this call + | + = help: the trait `Fn<(MouseEvent,)>` is not implemented for `{integer}` + = help: the following other types implement trait `IntoEventCallback`: + &yew::Callback + Option + Option> + yew::Callback + = note: required because of the requirements on the impl of `IntoEventCallback` for `{integer}` note: required by a bound in `yew::html::onclick::Wrapper::__macro_new` - --> $WORKSPACE/packages/yew/src/html/listener/events.rs - | - | / impl_short! { - | | onauxclick(MouseEvent) - | | onclick(MouseEvent) - | | -... | - | | ontransitionstart(TransitionEvent) - | | } - | |_^ required by this bound in `yew::html::onclick::Wrapper::__macro_new` - = note: this error originates in the macro `impl_action` (in Nightly builds, run with -Z macro-backtrace for more info) + --> $WORKSPACE/packages/yew/src/html/listener/events.rs + | + | / impl_short! { + | | onauxclick(MouseEvent) + | | onclick(MouseEvent) + | | +... | + | | ontransitionstart(TransitionEvent) + | | } + | |_^ required by this bound in `yew::html::onclick::Wrapper::__macro_new` + = note: this error originates in the macro `impl_action` which comes from the expansion of the macro `impl_short` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `yew::Callback` - --> tests/html_macro/element-fail.rs:52:29 - | -52 | html! { }; - | ------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^------ - | | | - | | expected an `Fn<(MouseEvent,)>` closure, found `yew::Callback` - | required by a bound introduced by this call - | - = help: the trait `Fn<(MouseEvent,)>` is not implemented for `yew::Callback` - = note: required because of the requirements on the impl of `IntoEventCallback` for `yew::Callback` + --> tests/html_macro/element-fail.rs:52:29 + | +52 | html! { }; + | ------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^------ + | | | + | | expected an `Fn<(MouseEvent,)>` closure, found `yew::Callback` + | required by a bound introduced by this call + | + = help: the trait `Fn<(MouseEvent,)>` is not implemented for `yew::Callback` + = help: the following other types implement trait `IntoEventCallback`: + &yew::Callback + yew::Callback + = note: required because of the requirements on the impl of `IntoEventCallback` for `yew::Callback` note: required by a bound in `yew::html::onclick::Wrapper::__macro_new` - --> $WORKSPACE/packages/yew/src/html/listener/events.rs - | - | / impl_short! { - | | onauxclick(MouseEvent) - | | onclick(MouseEvent) - | | -... | - | | ontransitionstart(TransitionEvent) - | | } - | |_^ required by this bound in `yew::html::onclick::Wrapper::__macro_new` - = note: this error originates in the macro `impl_action` (in Nightly builds, run with -Z macro-backtrace for more info) + --> $WORKSPACE/packages/yew/src/html/listener/events.rs + | + | / impl_short! { + | | onauxclick(MouseEvent) + | | onclick(MouseEvent) + | | +... | + | | ontransitionstart(TransitionEvent) + | | } + | |_^ required by this bound in `yew::html::onclick::Wrapper::__macro_new` + = note: this error originates in the macro `impl_action` which comes from the expansion of the macro `impl_short` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `Option<{integer}>: IntoEventCallback` is not satisfied - --> tests/html_macro/element-fail.rs:53:29 - | -53 | html! { }; - | ------------------------^^^^^^^------ - | | | - | | the trait `IntoEventCallback` is not implemented for `Option<{integer}>` - | required by a bound introduced by this call - | - = help: the following implementations were found: - as IntoEventCallback> - > as IntoEventCallback> + --> tests/html_macro/element-fail.rs:53:29 + | +53 | html! { }; + | ------------------------^^^^^^^------ + | | | + | | the trait `IntoEventCallback` is not implemented for `Option<{integer}>` + | required by a bound introduced by this call + | + = help: the following other types implement trait `IntoEventCallback`: + Option + Option> note: required by a bound in `yew::html::onfocus::Wrapper::__macro_new` - --> $WORKSPACE/packages/yew/src/html/listener/events.rs - | - | / impl_short! { - | | onauxclick(MouseEvent) - | | onclick(MouseEvent) - | | -... | - | | ontransitionstart(TransitionEvent) - | | } - | |_^ required by this bound in `yew::html::onfocus::Wrapper::__macro_new` - = note: this error originates in the macro `impl_action` (in Nightly builds, run with -Z macro-backtrace for more info) + --> $WORKSPACE/packages/yew/src/html/listener/events.rs + | + | / impl_short! { + | | onauxclick(MouseEvent) + | | onclick(MouseEvent) + | | +... | + | | ontransitionstart(TransitionEvent) + | | } + | |_^ required by this bound in `yew::html::onfocus::Wrapper::__macro_new` + = note: this error originates in the macro `impl_action` which comes from the expansion of the macro `impl_short` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `(): IntoPropValue` is not satisfied --> tests/html_macro/element-fail.rs:56:25 | 56 | html! { }; - | ^^ - | | - | the trait `IntoPropValue` is not implemented for `()` - | required by a bound introduced by this call + | ^^ the trait `IntoPropValue` is not implemented for `()` + | + = help: the following other types implement trait `IntoPropValue`: + <&'static [(K, V)] as IntoPropValue>> + <&'static [T] as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue> + <&T as IntoPropValue>> + and $N others + = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `Option: IntoPropValue` is not satisfied --> tests/html_macro/element-fail.rs:57:25 | 57 | html! { }; - | ----^^^^^^^^^^^^^^^^^^^^ - | | - | the trait `IntoPropValue` is not implemented for `Option` - | required by a bound introduced by this call + | ^^^^ the trait `IntoPropValue` is not implemented for `Option` | - = help: the following implementations were found: + = help: the following other types implement trait `IntoPropValue`: as IntoPropValue>> as IntoPropValue>> as IntoPropValue>>> > as IntoPropValue>> - and 4 others + as IntoPropValue>> + > as IntoPropValue>>> + = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: expected a `Fn<(MouseEvent,)>` closure, found `yew::Callback` - --> tests/html_macro/element-fail.rs:58:29 - | -58 | html! { }; - | ------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^------ - | | | - | | expected an `Fn<(MouseEvent,)>` closure, found `yew::Callback` - | required by a bound introduced by this call - | - = help: the trait `Fn<(MouseEvent,)>` is not implemented for `yew::Callback` - = note: required because of the requirements on the impl of `IntoEventCallback` for `yew::Callback` + --> tests/html_macro/element-fail.rs:58:29 + | +58 | html! { }; + | ------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^------ + | | | + | | expected an `Fn<(MouseEvent,)>` closure, found `yew::Callback` + | required by a bound introduced by this call + | + = help: the trait `Fn<(MouseEvent,)>` is not implemented for `yew::Callback` + = help: the following other types implement trait `IntoEventCallback`: + &yew::Callback + yew::Callback + = note: required because of the requirements on the impl of `IntoEventCallback` for `yew::Callback` note: required by a bound in `yew::html::onclick::Wrapper::__macro_new` - --> $WORKSPACE/packages/yew/src/html/listener/events.rs - | - | / impl_short! { - | | onauxclick(MouseEvent) - | | onclick(MouseEvent) - | | -... | - | | ontransitionstart(TransitionEvent) - | | } - | |_^ required by this bound in `yew::html::onclick::Wrapper::__macro_new` - = note: this error originates in the macro `impl_action` (in Nightly builds, run with -Z macro-backtrace for more info) + --> $WORKSPACE/packages/yew/src/html/listener/events.rs + | + | / impl_short! { + | | onauxclick(MouseEvent) + | | onclick(MouseEvent) + | | +... | + | | ontransitionstart(TransitionEvent) + | | } + | |_^ required by this bound in `yew::html::onclick::Wrapper::__macro_new` + = note: this error originates in the macro `impl_action` which comes from the expansion of the macro `impl_short` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `NotToString: IntoPropValue>` is not satisfied --> tests/html_macro/element-fail.rs:60:28 | 60 | html! { }; | ^^^^^^^^^^^ the trait `IntoPropValue>` is not implemented for `NotToString` + | + = help: the following other types implement trait `IntoPropValue`: + <&'static [(K, V)] as IntoPropValue>> + <&'static [T] as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue> + <&T as IntoPropValue>> + and $N others error[E0277]: the trait bound `(): IntoPropValue` is not satisfied --> tests/html_macro/element-fail.rs:62:25 | 62 | html! { }; - | ^^ - | | - | the trait `IntoPropValue` is not implemented for `()` - | required by a bound introduced by this call + | ^^ the trait `IntoPropValue` is not implemented for `()` + | + = help: the following other types implement trait `IntoPropValue`: + <&'static [(K, V)] as IntoPropValue>> + <&'static [T] as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue>> + <&'static str as IntoPropValue> + <&'static str as IntoPropValue> + <&T as IntoPropValue>> + and $N others + = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `Cow<'static, str>: From<{integer}>` is not satisfied --> tests/html_macro/element-fail.rs:77:15 | 77 | html! { <@{55}> }; - | ^--^ - | || - | |this tail expression is of type `_` - | the trait `From<{integer}>` is not implemented for `Cow<'static, str>` + | ^^^^ the trait `From<{integer}>` is not implemented for `Cow<'static, str>` | - = help: the following implementations were found: + = help: the following other types implement trait `From`: as From<&'a CStr>> as From<&'a CString>> as From> as From<&'a OsStr>> - and 11 others + as From<&'a OsString>> + as From> + as From<&'a Path>> + as From<&'a PathBuf>> + and $N others = note: required because of the requirements on the impl of `Into>` for `{integer}` diff --git a/packages/yew-macro/tests/html_macro/generic-component-fail.rs b/packages/yew-macro/tests/html_macro/generic-component-fail.rs index ed2163bcfc5..7d06180caab 100644 --- a/packages/yew-macro/tests/html_macro/generic-component-fail.rs +++ b/packages/yew-macro/tests/html_macro/generic-component-fail.rs @@ -40,9 +40,15 @@ where }} fn compile_fail() { + #[allow(unused_imports)] + use std::path::Path; + html! { > }; html! { > }; html! { >>> }; + + html! { >> }; + html! { > }; } fn main() {} diff --git a/packages/yew-macro/tests/html_macro/generic-component-fail.stderr b/packages/yew-macro/tests/html_macro/generic-component-fail.stderr index 33312601115..a0071a87b44 100644 --- a/packages/yew-macro/tests/html_macro/generic-component-fail.stderr +++ b/packages/yew-macro/tests/html_macro/generic-component-fail.stderr @@ -1,17 +1,31 @@ error: this opening tag has no corresponding closing tag - --> $DIR/generic-component-fail.rs:43:13 + --> tests/html_macro/generic-component-fail.rs:46:13 | -43 | html! { > }; +46 | html! { > }; | ^^^^^^^^^^^^^^^^^ -error: this closing tag has no corresponding opening tag - --> $DIR/generic-component-fail.rs:44:30 +error: mismatched closing tags: expected `Generic`, found `Generic` + --> tests/html_macro/generic-component-fail.rs:47:14 | -44 | html! { > }; - | ^^^^^^^^^^ +47 | html! { > }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ -error: this closing tag has no corresponding opening tag - --> $DIR/generic-component-fail.rs:45:30 +error: mismatched closing tags: expected `Generic`, found `Generic>` + --> tests/html_macro/generic-component-fail.rs:48:14 | -45 | html! { >>> }; - | ^^^^^^^^^^^^^^^^^^^^^^^ +48 | html! { >>> }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: mismatched closing tags: expected `Generic`, found `Generic` + --> tests/html_macro/generic-component-fail.rs:50:14 + | +50 | html! { >> }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: expected a valid closing tag for component + note: found opening tag `>` + help: try `>` + --> tests/html_macro/generic-component-fail.rs:51:30 + | +51 | html! { > }; + | ^^^ diff --git a/packages/yew-macro/tests/html_macro/generic-component-pass.rs b/packages/yew-macro/tests/html_macro/generic-component-pass.rs index 49d88cb56b8..4aca8a05457 100644 --- a/packages/yew-macro/tests/html_macro/generic-component-pass.rs +++ b/packages/yew-macro/tests/html_macro/generic-component-pass.rs @@ -77,6 +77,8 @@ where fn compile_pass() { ::yew::html! { /> }; + ::yew::html! { /> }; + ::yew::html! { >> }; ::yew::html! { >> }; ::yew::html! { > /> }; diff --git a/packages/yew-macro/tests/html_macro/html-element-pass.rs b/packages/yew-macro/tests/html_macro/html-element-pass.rs index c85989c96c9..dc81c66db22 100644 --- a/packages/yew-macro/tests/html_macro/html-element-pass.rs +++ b/packages/yew-macro/tests/html_macro/html-element-pass.rs @@ -51,6 +51,7 @@ fn compile_pass() { ::yew::html! {
    +
    @@ -117,6 +118,12 @@ fn compile_pass() { // handle misleading angle brackets ::yew::html! {
    ::default()}>
    }; ::yew::html! {
    }; + + // test for https://github.com/yewstack/yew/issues/2810 + ::yew::html! {
    }; + + let option_vnode = ::std::option::Option::Some(::yew::html! {}); + ::yew::html! {
    {option_vnode}
    }; } fn main() {} diff --git a/packages/yew-macro/tests/html_macro/iterable-fail.stderr b/packages/yew-macro/tests/html_macro/iterable-fail.stderr index f2ebfc0daac..54649e26bbe 100644 --- a/packages/yew-macro/tests/html_macro/iterable-fail.stderr +++ b/packages/yew-macro/tests/html_macro/iterable-fail.stderr @@ -17,55 +17,55 @@ error[E0277]: `()` is not an iterator --> tests/html_macro/iterable-fail.rs:6:17 | 6 | html! { for {()} }; - | ^--^ - | || - | |this tail expression is of type `_` - | `()` is not an iterator + | ^^^^ `()` is not an iterator | = help: the trait `Iterator` is not implemented for `()` = note: required because of the requirements on the impl of `IntoIterator` for `()` error[E0277]: `()` doesn't implement `std::fmt::Display` - --> tests/html_macro/iterable-fail.rs:7:17 - | -7 | html! { for Vec::<()>::new().into_iter() }; - | ^^^ `()` cannot be formatted with the default formatter - | - = help: the trait `std::fmt::Display` is not implemented for `()` - = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead - = note: required because of the requirements on the impl of `ToString` for `()` - = note: required because of the requirements on the impl of `From<()>` for `VNode` - = note: required because of the requirements on the impl of `Into` for `()` - = note: required because of the requirements on the impl of `FromIterator<()>` for `VNode` + --> tests/html_macro/iterable-fail.rs:7:17 + | +7 | html! { for Vec::<()>::new().into_iter() }; + | ^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = help: the trait `FromIterator
    ` is implemented for `VNode` + = note: required because of the requirements on the impl of `ToString` for `()` + = note: required because of the requirements on the impl of `From<()>` for `VNode` + = note: required because of the requirements on the impl of `Into` for `()` + = note: required because of the requirements on the impl of `FromIterator<()>` for `VNode` note: required by a bound in `collect` error[E0277]: `()` doesn't implement `std::fmt::Display` - --> tests/html_macro/iterable-fail.rs:10:17 - | -10 | html! { for empty }; - | ^^^^^ `()` cannot be formatted with the default formatter - | - = help: the trait `std::fmt::Display` is not implemented for `()` - = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead - = note: required because of the requirements on the impl of `ToString` for `()` - = note: required because of the requirements on the impl of `From<()>` for `VNode` - = note: required because of the requirements on the impl of `Into` for `()` - = note: required because of the requirements on the impl of `FromIterator<()>` for `VNode` + --> tests/html_macro/iterable-fail.rs:10:17 + | +10 | html! { for empty }; + | ^^^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = help: the trait `FromIterator` is implemented for `VNode` + = note: required because of the requirements on the impl of `ToString` for `()` + = note: required because of the requirements on the impl of `From<()>` for `VNode` + = note: required because of the requirements on the impl of `Into` for `()` + = note: required because of the requirements on the impl of `FromIterator<()>` for `VNode` note: required by a bound in `collect` error[E0277]: `()` doesn't implement `std::fmt::Display` - --> tests/html_macro/iterable-fail.rs:13:17 - | -13 | html! { for empty.iter() }; - | ^^^^^ `()` cannot be formatted with the default formatter - | - = help: the trait `std::fmt::Display` is not implemented for `()` - = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead - = note: required because of the requirements on the impl of `std::fmt::Display` for `&()` - = note: required because of the requirements on the impl of `ToString` for `&()` - = note: required because of the requirements on the impl of `From<&()>` for `VNode` - = note: required because of the requirements on the impl of `Into` for `&()` - = note: required because of the requirements on the impl of `FromIterator<&()>` for `VNode` + --> tests/html_macro/iterable-fail.rs:13:17 + | +13 | html! { for empty.iter() }; + | ^^^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = help: the trait `FromIterator` is implemented for `VNode` + = note: required because of the requirements on the impl of `std::fmt::Display` for `&()` + = note: required because of the requirements on the impl of `ToString` for `&()` + = note: required because of the requirements on the impl of `From<&()>` for `VNode` + = note: required because of the requirements on the impl of `Into` for `&()` + = note: required because of the requirements on the impl of `FromIterator<&()>` for `VNode` note: required by a bound in `collect` error[E0277]: `()` is not an iterator diff --git a/packages/yew-macro/tests/html_macro/list-fail.stderr b/packages/yew-macro/tests/html_macro/list-fail.stderr index 2c24fca6132..6fba4bb44ba 100644 --- a/packages/yew-macro/tests/html_macro/list-fail.stderr +++ b/packages/yew-macro/tests/html_macro/list-fail.stderr @@ -1,65 +1,81 @@ error: this opening fragment has no corresponding closing fragment - --> $DIR/list-fail.rs:5:13 + --> tests/html_macro/list-fail.rs:5:13 | 5 | html! { <> }; | ^^ error: this opening fragment has no corresponding closing fragment - --> $DIR/list-fail.rs:6:15 + --> tests/html_macro/list-fail.rs:6:15 | 6 | html! { <><> }; | ^^ error: this opening fragment has no corresponding closing fragment - --> $DIR/list-fail.rs:7:13 + --> tests/html_macro/list-fail.rs:7:13 | 7 | html! { <><> }; | ^^ error: this closing fragment has no corresponding opening fragment - --> $DIR/list-fail.rs:10:13 + --> tests/html_macro/list-fail.rs:10:13 | 10 | html! { }; | ^^^ error: this closing fragment has no corresponding opening fragment - --> $DIR/list-fail.rs:11:13 + --> tests/html_macro/list-fail.rs:11:13 | 11 | html! { }; | ^^^ error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<>`) - --> $DIR/list-fail.rs:14:18 + --> tests/html_macro/list-fail.rs:14:18 | 14 | html! { <><> }; | ^^^^^ error: expected a valid html element - --> $DIR/list-fail.rs:16:15 + --> tests/html_macro/list-fail.rs:16:15 | 16 | html! { <>invalid }; | ^^^^^^^ error: expected an expression following this equals sign - --> $DIR/list-fail.rs:18:17 + --> tests/html_macro/list-fail.rs:18:17 | 18 | html! { }; | ^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. - --> $DIR/list-fail.rs:20:18 +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Expr::MethodCall { + attrs: [], + receiver: Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "key", + }, + }, + dot_token: Dot, + method: Ident { + ident: "to_string", + span: #0 bytes(404..413), + }, + turbofish: None, + paren_token: Paren, + args: [], + } + --> tests/html_macro/list-fail.rs:20:18 | 20 | html! { }; | ^^^^^^^^^^^^^^^^^ error: only a single `key` prop is allowed on a fragment - --> $DIR/list-fail.rs:23:30 + --> tests/html_macro/list-fail.rs:23:30 | 23 | html! { }; | ^^^ error: fragments only accept the `key` prop - --> $DIR/list-fail.rs:25:14 + --> tests/html_macro/list-fail.rs:25:14 | 25 | html! { }; | ^^^^^^^^^ diff --git a/packages/yew-macro/tests/html_macro/node-fail.stderr b/packages/yew-macro/tests/html_macro/node-fail.stderr index 013688f83bd..9b5f5a70717 100644 --- a/packages/yew-macro/tests/html_macro/node-fail.stderr +++ b/packages/yew-macro/tests/html_macro/node-fail.stderr @@ -39,6 +39,13 @@ error[E0425]: cannot find value `invalid` in this scope | 7 | html! { invalid }; | ^^^^^^^ not found in this scope + | +help: consider importing one of these items + | +1 | use core::ptr::invalid; + | +1 | use std::ptr::invalid; + | error[E0277]: `()` doesn't implement `std::fmt::Display` --> tests/html_macro/node-fail.rs:6:13 @@ -48,6 +55,16 @@ error[E0277]: `()` doesn't implement `std::fmt::Display` | = help: the trait `std::fmt::Display` is not implemented for `()` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = help: the following other types implement trait `From`: + >> + > + >> + > + > + > + > + > + > = note: required because of the requirements on the impl of `ToString` for `()` = note: required because of the requirements on the impl of `From<()>` for `VNode` = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) @@ -60,6 +77,16 @@ error[E0277]: `()` doesn't implement `std::fmt::Display` | = help: the trait `std::fmt::Display` is not implemented for `()` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = help: the following other types implement trait `From`: + >> + > + >> + > + > + > + > + > + > = note: required because of the requirements on the impl of `ToString` for `()` = note: required because of the requirements on the impl of `From<()>` for `VNode` = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/packages/yew-macro/tests/html_macro_test.rs b/packages/yew-macro/tests/html_macro_test.rs index b802002897c..9ccd4ec41ac 100644 --- a/packages/yew-macro/tests/html_macro_test.rs +++ b/packages/yew-macro/tests/html_macro_test.rs @@ -1,7 +1,7 @@ use yew::{html, html_nested}; #[allow(dead_code)] -#[rustversion::attr(stable(1.60), test)] +#[rustversion::attr(stable(1.64), test)] fn html_macro() { let t = trybuild::TestCases::new(); diff --git a/packages/yew-macro/tests/props_macro/props-fail.stderr b/packages/yew-macro/tests/props_macro/props-fail.stderr index 3e3e0347386..ed0f4b24e1f 100644 --- a/packages/yew-macro/tests/props_macro/props-fail.stderr +++ b/packages/yew-macro/tests/props_macro/props-fail.stderr @@ -34,7 +34,7 @@ error[E0599]: no method named `fail` found for struct `PropsBuilder` in the curr --> tests/props_macro/props-fail.rs:10:31 | 3 | #[derive(Clone, Properties, PartialEq)] - | ---------- method `fail` not found for this + | ---------- method `fail` not found for this struct ... 10 | yew::props!(Props { a: 5, fail: 10 }); | ^^^^ method not found in `PropsBuilder` @@ -51,7 +51,7 @@ error[E0599]: no method named `does_not_exist` found for struct `PropsBuilder` i --> tests/props_macro/props-fail.rs:15:25 | 3 | #[derive(Clone, Properties, PartialEq)] - | ---------- method `does_not_exist` not found for this + | ---------- method `does_not_exist` not found for this struct ... 15 | yew::props!(Props { does_not_exist }); | ^^^^^^^^^^^^^^ method not found in `PropsBuilder` diff --git a/packages/yew-macro/tests/props_macro/resolve-prop-fail.stderr b/packages/yew-macro/tests/props_macro/resolve-prop-fail.stderr index ed69c4aa92f..d58e437a03f 100644 --- a/packages/yew-macro/tests/props_macro/resolve-prop-fail.stderr +++ b/packages/yew-macro/tests/props_macro/resolve-prop-fail.stderr @@ -11,3 +11,7 @@ note: required by a bound in `yew::Properties` | pub trait Properties: PartialEq { | ^^^^^^^^^ required by this bound in `yew::Properties` = note: this error originates in the derive macro `Properties` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider annotating `Props` with `#[derive(PartialEq)]` + | +4 | #[derive(PartialEq)] + | diff --git a/packages/yew-macro/tests/props_macro_test.rs b/packages/yew-macro/tests/props_macro_test.rs index 83234005a47..b82adc18486 100644 --- a/packages/yew-macro/tests/props_macro_test.rs +++ b/packages/yew-macro/tests/props_macro_test.rs @@ -1,7 +1,28 @@ #[allow(dead_code)] -#[rustversion::attr(stable(1.60), test)] +#[rustversion::attr(stable(1.64), test)] fn props_macro() { let t = trybuild::TestCases::new(); t.pass("tests/props_macro/*-pass.rs"); t.compile_fail("tests/props_macro/*-fail.rs"); } + +#[test] +fn props_order() { + #[derive(yew::Properties, PartialEq)] + struct Props { + first: usize, + second: usize, + last: usize, + } + + let mut g = 1..=3; + let props = yew::props!(Props { + first: g.next().unwrap(), + second: g.next().unwrap(), + last: g.next().unwrap() + }); + + assert_eq!(props.first, 1); + assert_eq!(props.second, 2); + assert_eq!(props.last, 3); +} diff --git a/packages/yew-router-macro/Cargo.toml b/packages/yew-router-macro/Cargo.toml index e0f5662e354..d2fc60697a4 100644 --- a/packages/yew-router-macro/Cargo.toml +++ b/packages/yew-router-macro/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "yew-router-macro" -version = "0.16.0" +version = "0.17.0" authors = ["Hamza "] edition = "2021" license = "MIT OR Apache-2.0" description = "Contains macros used with yew-router" repository = "https://github.com/yewstack/yew" -rust-version = "1.60.0" +rust-version = "1.64.0" [lib] proc-macro = true @@ -14,7 +14,7 @@ proc-macro = true [dependencies] proc-macro2 = "1" quote = "1" -syn = { version = "1", features = ["full","extra-traits"] } +syn = { version = "2", features = ["full","extra-traits"] } [dev-dependencies] rustversion = "1" diff --git a/packages/yew-router-macro/Makefile.toml b/packages/yew-router-macro/Makefile.toml index 7b2d8fcba8a..1eae4efc7c4 100644 --- a/packages/yew-router-macro/Makefile.toml +++ b/packages/yew-router-macro/Makefile.toml @@ -1,6 +1,6 @@ [tasks.test] clear = true -toolchain = "1.60.0" +toolchain = "1.64.0" command = "cargo" args = ["test"] diff --git a/packages/yew-router-macro/src/routable_derive.rs b/packages/yew-router-macro/src/routable_derive.rs index 992736e4294..2dd0804a86b 100644 --- a/packages/yew-router-macro/src/routable_derive.rs +++ b/packages/yew-router-macro/src/routable_derive.rs @@ -65,7 +65,7 @@ fn parse_variants_attributes( let attrs = &variant.attrs; let at_attrs = attrs .iter() - .filter(|attr| attr.path.is_ident(AT_ATTR_IDENT)) + .filter(|attr| attr.path().is_ident(AT_ATTR_IDENT)) .collect::>(); let attr = match at_attrs.len() { @@ -73,16 +73,13 @@ fn parse_variants_attributes( 0 => { return Err(syn::Error::new( variant.span(), - format!( - "{} attribute must be present on every variant", - AT_ATTR_IDENT - ), + format!("{AT_ATTR_IDENT} attribute must be present on every variant"), )) } _ => { return Err(syn::Error::new_spanned( quote! { #(#at_attrs)* }, - format!("only one {} attribute must be present", AT_ATTR_IDENT), + format!("only one {AT_ATTR_IDENT} attribute must be present"), )) } }; @@ -107,7 +104,7 @@ fn parse_variants_attributes( ats.push(lit); for attr in attrs.iter() { - if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) { + if attr.path().is_ident(NOT_FOUND_ATTR_IDENT) { not_found_attrs.push(attr); not_founds.push(variant.ident.clone()) } @@ -117,7 +114,7 @@ fn parse_variants_attributes( if not_founds.len() > 1 { return Err(syn::Error::new_spanned( quote! { #(#not_found_attrs)* }, - format!("there can only be one {}", NOT_FOUND_ATTR_IDENT), + format!("there can only be one {NOT_FOUND_ATTR_IDENT}"), )); } @@ -132,10 +129,15 @@ impl Routable { Fields::Unit => quote! { Self::#ident }, Fields::Named(field) => { let fields = field.named.iter().map(|it| { - //named fields have idents + // named fields have idents it.ident.as_ref().unwrap() }); - quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } } + quote! { Self::#ident { #(#fields: { + let param = params.get(stringify!(#fields))?; + let param = &*::yew_router::__macro::decode_for_url(param).ok()?; + let param = param.parse().ok()?; + param + },)* } } } Fields::Unnamed(_) => unreachable!(), // already checked }; @@ -174,12 +176,12 @@ impl Routable { // :param -> {param} // *param -> {param} // so we can pass it to `format!("...", param)` - right = right.replace(&format!(":{}", field), &format!("{{{}}}", field)); - right = right.replace(&format!("*{}", field), &format!("{{{}}}", field)); + right = right.replace(&format!(":{field}"), &format!("{{{field}}}")); + right = right.replace(&format!("*{field}"), &format!("{{{field}}}")); } quote! { - Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*) + Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = ::yew_router::__macro::encode_for_url(&::std::format!("{}", #fields))),*) } } Fields::Unnamed(_) => unreachable!(), // already checked diff --git a/packages/yew-router-macro/tests/routable_derive_test.rs b/packages/yew-router-macro/tests/routable_derive_test.rs index 5c2ef260863..59dbd5e0104 100644 --- a/packages/yew-router-macro/tests/routable_derive_test.rs +++ b/packages/yew-router-macro/tests/routable_derive_test.rs @@ -1,5 +1,5 @@ #[allow(dead_code)] -#[rustversion::attr(stable(1.60), test)] +#[rustversion::attr(stable(1.64), test)] fn tests() { let t = trybuild::TestCases::new(); t.pass("tests/routable_derive/*-pass.rs"); diff --git a/packages/yew-router/Cargo.toml b/packages/yew-router/Cargo.toml index fba240ddf52..f867ff0ae77 100644 --- a/packages/yew-router/Cargo.toml +++ b/packages/yew-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yew-router" -version = "0.16.0" +version = "0.17.0" authors = ["Hamza "] edition = "2021" license = "MIT OR Apache-2.0" @@ -9,11 +9,11 @@ keywords = ["web", "yew", "router"] categories = ["gui", "web-programming"] description = "A router implementation for the Yew framework" repository = "https://github.com/yewstack/yew" -rust-version = "1.60.0" +rust-version = "1.64.0" [dependencies] -yew = { version = "0.19.3", path = "../yew", default-features= false } -yew-router-macro = { version = "0.16.0", path = "../yew-router-macro" } +yew = { version = "0.20.0", path = "../yew", default-features= false } +yew-router-macro = { version = "0.17.0", path = "../yew-router-macro" } wasm-bindgen = "0.2" js-sys = "0.3" @@ -21,6 +21,8 @@ gloo = { version = "0.8", features = ["futures"] } route-recognizer = "0.3" serde = "1" serde_urlencoded = "0.7.1" +tracing = "0.1.37" +urlencoding = "2.1.2" [dependencies.web-sys] version = "0.3" @@ -33,7 +35,7 @@ features = [ [dev-dependencies] wasm-bindgen-test = "0.3" serde = { version = "1", features = ["derive"] } -yew = { version = "0.19.3", path = "../yew", features = ["csr"] } +yew = { version = "0.20.0", path = "../yew", features = ["csr"] } [dev-dependencies.web-sys] version = "0.3" diff --git a/packages/yew-router/src/components/link.rs b/packages/yew-router/src/components/link.rs index 72d724a6391..cf94c92b18c 100644 --- a/packages/yew-router/src/components/link.rs +++ b/packages/yew-router/src/components/link.rs @@ -24,8 +24,11 @@ where pub query: Option, #[prop_or_default] pub disabled: bool, + /// [`NodeRef`](yew::html::NodeRef) for the `` element. #[prop_or_default] - pub children: Children, + pub anchor_ref: NodeRef, + #[prop_or_default] + pub children: Html, } /// A wrapper around `` tag to be used with [`Router`](crate::Router) @@ -41,6 +44,7 @@ where children, disabled, query, + anchor_ref, } = props.clone(); let navigator = use_navigator().expect_throw("failed to get navigator"); @@ -51,8 +55,10 @@ where let query = query.clone(); Callback::from(move |e: MouseEvent| { + if e.meta_key() || e.ctrl_key() || e.shift_key() || e.alt_key() { + return; + } e.prevent_default(); - match query { None => { navigator.push(&to); @@ -86,6 +92,7 @@ where {href} {onclick} {disabled} + ref={anchor_ref} > { children } diff --git a/packages/yew-router/src/components/redirect.rs b/packages/yew-router/src/components/redirect.rs index bf7341bd7ca..067dc0b5a29 100644 --- a/packages/yew-router/src/components/redirect.rs +++ b/packages/yew-router/src/components/redirect.rs @@ -5,7 +5,7 @@ use crate::hooks::use_navigator; use crate::Routable; /// Props for [`Redirect`] -#[derive(Properties, Clone, PartialEq)] +#[derive(Properties, Clone, PartialEq, Eq)] pub struct RedirectProps { /// Route that will be pushed when the component is rendered. pub to: R, diff --git a/packages/yew-router/src/macro_helpers.rs b/packages/yew-router/src/macro_helpers.rs index a8d06e69d66..520fae36b8b 100644 --- a/packages/yew-router/src/macro_helpers.rs +++ b/packages/yew-router/src/macro_helpers.rs @@ -1,3 +1,5 @@ +pub use urlencoding::{decode as decode_for_url, encode as encode_for_url}; + use crate::utils::strip_slash_suffix; use crate::Routable; diff --git a/packages/yew-router/src/navigator.rs b/packages/yew-router/src/navigator.rs index c09e8552de5..48345609f83 100644 --- a/packages/yew-router/src/navigator.rs +++ b/packages/yew-router/src/navigator.rs @@ -9,7 +9,7 @@ pub type NavigationError = HistoryError; pub type NavigationResult = HistoryResult; /// The kind of Navigator Provider. -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum NavigatorKind { /// Browser History. Browser, @@ -162,7 +162,7 @@ impl Navigator { if route_s.is_empty() && route_s.is_empty() { Cow::from("/") } else { - Cow::from(format!("{}{}", base, route_s)) + Cow::from(format!("{base}{route_s}")) } } None => route_s.into(), @@ -178,7 +178,7 @@ impl Navigator { .unwrap_or(path); if !path.starts_with('/') { - path = format!("/{}", m).into(); + path = format!("/{m}").into(); } path diff --git a/packages/yew-router/src/routable.rs b/packages/yew-router/src/routable.rs index 301e15fb222..4cbda3b91d1 100644 --- a/packages/yew-router/src/routable.rs +++ b/packages/yew-router/src/routable.rs @@ -34,7 +34,7 @@ pub trait Routable: Clone + PartialEq { /// /// This can be used with [`History`](gloo::history::History) and /// [`Location`](gloo::history::Location) when the type of [`Routable`] is unknown. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct AnyRoute { path: String, } diff --git a/packages/yew-router/src/router.rs b/packages/yew-router/src/router.rs index cd2a3fc38f0..b3becda9ea1 100644 --- a/packages/yew-router/src/router.rs +++ b/packages/yew-router/src/router.rs @@ -11,7 +11,8 @@ use crate::utils::{base_url, strip_slash_suffix}; /// Props for [`Router`]. #[derive(Properties, PartialEq, Clone)] pub struct RouterProps { - pub children: Children, + #[prop_or_default] + pub children: Html, pub history: AnyHistory, #[prop_or_default] pub basename: Option, @@ -84,26 +85,23 @@ fn base_router(props: &RouterProps) -> Html { { let loc_ctx_dispatcher = loc_ctx.dispatcher(); - use_effect_with_deps( - move |history| { + use_effect_with(history, move |history| { + let history = history.clone(); + // Force location update when history changes. + loc_ctx_dispatcher.dispatch(history.location()); + + let history_cb = { let history = history.clone(); - // Force location update when history changes. - loc_ctx_dispatcher.dispatch(history.location()); - - let history_cb = { - let history = history.clone(); - move || loc_ctx_dispatcher.dispatch(history.location()) - }; - - let listener = history.listen(history_cb); - - // We hold the listener in the destructor. - move || { - std::mem::drop(listener); - } - }, - history, - ); + move || loc_ctx_dispatcher.dispatch(history.location()) + }; + + let listener = history.listen(history_cb); + + // We hold the listener in the destructor. + move || { + std::mem::drop(listener); + } + }); } html! { @@ -132,7 +130,7 @@ pub fn router(props: &RouterProps) -> Html { /// Props for [`BrowserRouter`] and [`HashRouter`]. #[derive(Properties, PartialEq, Clone)] pub struct ConcreteRouterProps { - pub children: Children, + pub children: Html, #[prop_or_default] pub basename: Option, } diff --git a/packages/yew-router/src/switch.rs b/packages/yew-router/src/switch.rs index ce8c53ce061..c7e69b91405 100644 --- a/packages/yew-router/src/switch.rs +++ b/packages/yew-router/src/switch.rs @@ -1,6 +1,5 @@ //! The [`Switch`] Component. -use gloo::console; use yew::prelude::*; use crate::prelude::*; @@ -41,7 +40,7 @@ where match route { Some(route) => props.render.emit(route), None => { - console::warn!("no route matched"); + tracing::warn!("no route matched"); Html::default() } } diff --git a/packages/yew-router/src/utils.rs b/packages/yew-router/src/utils.rs index c87555ca4ff..ba356701725 100644 --- a/packages/yew-router/src/utils.rs +++ b/packages/yew-router/src/utils.rs @@ -60,7 +60,7 @@ pub fn compose_path(pathname: &str, query: &str) -> Option { let query = query.trim(); if !query.is_empty() { - Some(format!("{}?{}", pathname, query)) + Some(format!("{pathname}?{query}")) } else { Some(pathname.to_owned()) } diff --git a/packages/yew-router/tests/basename.rs b/packages/yew-router/tests/basename.rs index e92fec0426c..0ba058c5cd2 100644 --- a/packages/yew-router/tests/basename.rs +++ b/packages/yew-router/tests/basename.rs @@ -1,9 +1,9 @@ use std::time::Duration; -use gloo::timers::future::sleep; use serde::{Deserialize, Serialize}; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use yew::functional::function_component; +use yew::platform::time::sleep; use yew::prelude::*; use yew_router::prelude::*; diff --git a/packages/yew-router/tests/browser_router.rs b/packages/yew-router/tests/browser_router.rs index 77c3002e27c..f3a5f49379c 100644 --- a/packages/yew-router/tests/browser_router.rs +++ b/packages/yew-router/tests/browser_router.rs @@ -1,9 +1,9 @@ use std::time::Duration; -use gloo::timers::future::sleep; use serde::{Deserialize, Serialize}; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use yew::functional::function_component; +use yew::platform::time::sleep; use yew::prelude::*; use yew_router::prelude::*; diff --git a/packages/yew-router/tests/hash_router.rs b/packages/yew-router/tests/hash_router.rs index 7f8e79f3eec..48f793ca346 100644 --- a/packages/yew-router/tests/hash_router.rs +++ b/packages/yew-router/tests/hash_router.rs @@ -1,9 +1,9 @@ use std::time::Duration; -use gloo::timers::future::sleep; use serde::{Deserialize, Serialize}; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use yew::functional::function_component; +use yew::platform::time::sleep; use yew::prelude::*; use yew_router::prelude::*; diff --git a/packages/yew-router/tests/link.rs b/packages/yew-router/tests/link.rs index ddff1bd9556..50510b79242 100644 --- a/packages/yew-router/tests/link.rs +++ b/packages/yew-router/tests/link.rs @@ -1,9 +1,9 @@ use std::time::Duration; -use gloo::timers::future::sleep; use serde::{Deserialize, Serialize}; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use yew::functional::function_component; +use yew::platform::time::sleep; use yew::prelude::*; use yew_router::prelude::*; diff --git a/packages/yew-router/tests/router_unit_tests.rs b/packages/yew-router/tests/router_unit_tests.rs index 869060643eb..30ad206d2f9 100644 --- a/packages/yew-router/tests/router_unit_tests.rs +++ b/packages/yew-router/tests/router_unit_tests.rs @@ -48,3 +48,26 @@ fn router_trailing_slash() { AppRoute::recognize("/category/cooking-recipes/") ); } + +#[test] +fn router_url_encoding() { + #[derive(Routable, Debug, Clone, PartialEq)] + enum AppRoute { + #[at("/")] + Root, + #[at("/search/:query")] + Search { query: String }, + } + + assert_eq!( + yew_router::__macro::decode_for_url("/search/a%2Fb/").unwrap(), + "/search/a/b/" + ); + + assert_eq!( + Some(AppRoute::Search { + query: "a/b".to_string() + }), + AppRoute::recognize("/search/a%2Fb/") + ); +} diff --git a/packages/yew-router/tests/url_encoded_routes.rs b/packages/yew-router/tests/url_encoded_routes.rs new file mode 100644 index 00000000000..272a1a82599 --- /dev/null +++ b/packages/yew-router/tests/url_encoded_routes.rs @@ -0,0 +1,63 @@ +use std::time::Duration; + +use yew::platform::time::sleep; +use yew::prelude::*; +use yew_router::prelude::*; + +mod utils; +use utils::*; +use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; +wasm_bindgen_test_configure!(run_in_browser); + +#[derive(Routable, Debug, Clone, PartialEq)] +enum AppRoute { + #[at("/")] + Root, + #[at("/search/:query")] + Search { query: String }, +} + +#[function_component] +fn Comp() -> Html { + let switch = move |routes: AppRoute| match routes { + AppRoute::Root => html! { + <> +

    { "Root" }

    + to={AppRoute::Search { query: "a/b".to_string() }}> + {"Click me"} + > + + }, + AppRoute::Search { query } => html! { +

    { query }

    + }, + }; + html! { + render={switch} /> + } +} + +#[function_component(Root)] +fn root() -> Html { + html! { + + + + } +} + +#[test] +async fn url_encoded_roundtrip() { + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .render(); + sleep(Duration::ZERO).await; + click("a"); + sleep(Duration::ZERO).await; + let res = obtain_result_by_id("q"); + assert_eq!(res, "a/b"); + + assert_eq!( + gloo::utils::window().location().pathname().unwrap(), + "/search/a%2Fb" + ) +} diff --git a/packages/yew-router/tests/utils.rs b/packages/yew-router/tests/utils.rs index 3185990aba3..ee0a70eb241 100644 --- a/packages/yew-router/tests/utils.rs +++ b/packages/yew-router/tests/utils.rs @@ -33,7 +33,7 @@ pub fn link_href(selector: &str) -> String { gloo::utils::document() .query_selector(selector) .expect("Failed to run query selector") - .unwrap_or_else(|| panic!("No such link: {}", selector)) + .unwrap_or_else(|| panic!("No such link: {selector}")) .get_attribute("href") .expect("No href attribute") } diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index 3f7f1edc8fc..f84c3d8279e 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -1,42 +1,49 @@ [package] name = "yew" -version = "0.19.3" +version = "0.20.0" edition = "2021" authors = [ "Denis Kolodin ", "Justin Starry ", ] repository = "https://github.com/yewstack/yew" -homepage = "https://github.com/yewstack/yew" +homepage = "https://yew.rs" documentation = "https://docs.rs/yew/" license = "MIT OR Apache-2.0" keywords = ["web", "webasm", "javascript"] categories = ["gui", "wasm", "web-programming"] -description = "A framework for making client-side single-page apps" +description = "A framework for creating reliable and efficient web applications" readme = "../../README.md" -rust-version = "1.60.0" +rust-version = "1.64.0" [dependencies] console_error_panic_hook = "0.1" -gloo = { version = "0.8", features = ["futures"] } -gloo-utils = "0.1.0" -indexmap = { version = "1", features = ["std"] } +gloo = "0.8" +indexmap = { version = "2", features = ["std"] } js-sys = "0.3" slab = "0.4" wasm-bindgen = "0.2" -yew-macro = { version = "^0.19.0", path = "../yew-macro" } +yew-macro = { version = "^0.20.0", path = "../yew-macro" } thiserror = "1.0" -futures = "0.3" -html-escape = { version = "0.2.9", optional = true } -implicit-clone = { version = "0.3", features = ["map"] } -base64ct = { version = "1.5.0", features = ["std"], optional = true } +futures = { version = "0.3", default-features = false, features = ["std"] } +html-escape = { version = "0.2.13", optional = true } +implicit-clone = { version = "0.4.0", features = ["map"] } +base64ct = { version = "1.6.0", features = ["std"], optional = true } bincode = { version = "1.3.3", optional = true } serde = { version = "1", features = ["derive"] } -tokio = { version = "1.19", features = ["rt", "time"], optional = true } -tokio-stream = { version = "0.1.9", features = ["sync"], optional = true } +tracing = "0.1.37" +prokio = "0.1.0" +rustversion = "1" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +# We still need tokio as we have docs linked to it. +tokio = { version = "1.29", features = ["rt"] } [dependencies.web-sys] -version = "0.3" +version = "^0.3.64" features = [ "AnimationEvent", "Document", @@ -49,6 +56,7 @@ features = [ "FocusEvent", "HtmlElement", "HtmlInputElement", + "HtmlCollection", "HtmlTextAreaElement", "InputEvent", "InputEventInit", @@ -67,44 +75,28 @@ features = [ "WheelEvent", "Window", "HtmlScriptElement", + "SubmitEvent", ] -[target.'cfg(target_arch = "wasm32")'.dependencies] -# we move it here so no promise-based spawn_local can present for -# non-wasm32 targets. -wasm-bindgen-futures = "0.4" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -num_cpus = { version = "1.13", optional = true } -tokio-util = { version = "0.7", features = ["rt"], optional = true } -once_cell = "1" +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { version = "1.29", features = ["full"] } [dev-dependencies] wasm-bindgen-test = "0.3" gloo = { version = "0.8", features = ["futures"] } -gloo-net = { version = "0.2", features = ["http"] } wasm-bindgen-futures = "0.4" -rustversion = "1" trybuild = "1" [dev-dependencies.web-sys] version = "0.3" -features = [ - "ShadowRootInit", - "ShadowRootMode", -] +features = ["ShadowRootInit", "ShadowRootMode", "HtmlButtonElement"] [features] -tokio = ["dep:tokio", "dep:tokio-stream", "dep:num_cpus", "dep:tokio-util"] ssr = ["dep:html-escape", "dep:base64ct", "dep:bincode"] csr = [] hydration = ["csr", "dep:bincode"] -nightly = ["yew-macro/nightly"] default = [] -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -tokio = { version = "1.19", features = ["full"] } - [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "documenting"] diff --git a/packages/yew/Makefile.toml b/packages/yew/Makefile.toml index da4903e1b63..643c4b948e7 100644 --- a/packages/yew/Makefile.toml +++ b/packages/yew/Makefile.toml @@ -1,6 +1,6 @@ [tasks.native-test] command = "cargo" -args = ["test", "--features", "csr,ssr,hydration,tokio"] +args = ["test", "--features", "csr,ssr,hydration"] [tasks.wasm-test] command = "wasm-pack" diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs index 5f6a9a04735..577d34165c7 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -5,11 +5,10 @@ use std::rc::Rc; use web_sys::Element; -use crate::dom_bundle::BSubtree; -use crate::html::{BaseComponent, NodeRef, Scope, Scoped}; +use crate::dom_bundle::{BSubtree, DomSlot, DynamicDomSlot}; +use crate::html::{BaseComponent, Scope, Scoped}; /// An instance of an application. -#[cfg_attr(documenting, doc(cfg(feature = "csr")))] #[derive(Debug)] pub struct AppHandle { /// `Scope` holder @@ -24,6 +23,11 @@ where /// similarly to the `program` function in Elm. You should provide an initial model, `update` /// function which will update the state of the model and a `view` function which /// will render the model to a virtual DOM tree. + #[tracing::instrument( + level = tracing::Level::DEBUG, + name = "mount", + skip(props), + )] pub(crate) fn mount_with_props(host: Element, props: Rc) -> Self { clear_element(&host); let app = Self { @@ -33,15 +37,34 @@ where app.scope.mount_in_place( hosting_root, host, - NodeRef::default(), - NodeRef::default(), + DomSlot::at_end(), + DynamicDomSlot::new_debug_trapped(), props, ); app } + /// Update the properties of the app's root component. + /// + /// This can be an alternative to sending and handling messages. The existing component will be + /// reused and have its properties updates. This will presumably trigger a re-render, refer to + /// the [`changed`] lifecycle for details. + /// + /// [`changed`]: crate::Component::changed + #[tracing::instrument( + level = tracing::Level::DEBUG, + skip_all, + )] + pub fn update(&mut self, new_props: COMP::Properties) { + self.scope.reuse(Rc::new(new_props), DomSlot::at_end()) + } + /// Schedule the app for destruction + #[tracing::instrument( + level = tracing::Level::DEBUG, + skip_all, + )] pub fn destroy(self) { self.scope.destroy(false) } @@ -65,7 +88,6 @@ fn clear_element(host: &Element) { } } -#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] #[cfg(feature = "hydration")] mod feat_hydration { use super::*; @@ -75,6 +97,11 @@ mod feat_hydration { where COMP: BaseComponent, { + #[tracing::instrument( + level = tracing::Level::DEBUG, + name = "hydrate", + skip(props), + )] pub(crate) fn hydrate_with_props(host: Element, props: Rc) -> Self { let app = Self { scope: Scope::new(None), @@ -87,11 +114,11 @@ mod feat_hydration { hosting_root, host.clone(), &mut fragment, - NodeRef::default(), + DynamicDomSlot::new_debug_trapped(), Rc::clone(&props), ); #[cfg(debug_assertions)] // Fix trapped next_sibling at the root - app.scope.reuse(props, NodeRef::default()); + app.scope.reuse(props, DomSlot::at_end()); // We remove all remaining nodes, this mimics the clear_element behaviour in // mount_with_props. diff --git a/packages/yew/src/callback.rs b/packages/yew/src/callback.rs index 84aff3ab3e3..68edd25a304 100644 --- a/packages/yew/src/callback.rs +++ b/packages/yew/src/callback.rs @@ -68,7 +68,7 @@ impl Default for Callback { impl Callback { /// Creates a new callback from another callback and a function - /// That when emited will call that function and will emit the original callback + /// That when emitted will call that function and will emit the original callback pub fn reform(&self, func: F) -> Callback where F: Fn(T) -> IN + 'static, @@ -80,6 +80,68 @@ impl Callback { }; Callback::from(func) } + + /// Creates a new callback from another callback and a function. + /// When emitted will call the function and, only if it returns `Some(value)`, will emit + /// `value` to the original callback. + pub fn filter_reform(&self, func: F) -> Callback> + where + F: Fn(T) -> Option + 'static, + { + let this = self.clone(); + let func = move |input| func(input).map(|output| this.emit(output)); + Callback::from(func) + } } impl ImplicitClone for Callback {} + +#[cfg(test)] +mod test { + use std::sync::Mutex; + + use super::*; + + /// emit the callback with the provided value + fn emit(values: I, f: F) -> Vec + where + I: IntoIterator, + F: FnOnce(Callback) -> Callback, + { + let result = Rc::new(Mutex::new(Vec::new())); + let cb_result = result.clone(); + let cb = f(Callback::::from(move |v| { + cb_result.lock().unwrap().push(v); + })); + for value in values { + cb.emit(value); + } + let x = result.lock().unwrap().clone(); + x + } + + #[test] + fn test_callback() { + assert_eq!(*emit([true, false], |cb| cb), vec![true, false]); + } + + #[test] + fn test_reform() { + assert_eq!( + *emit([true, false], |cb| cb.reform(|v: bool| !v)), + vec![false, true] + ); + } + + #[test] + fn test_filter_reform() { + assert_eq!( + *emit([1, 2, 3], |cb| cb.filter_reform(|v| match v { + 1 => Some(true), + 2 => Some(false), + _ => None, + })), + vec![true, false] + ); + } +} diff --git a/packages/yew/src/context.rs b/packages/yew/src/context.rs index a46321c77e4..1b77f104b4d 100644 --- a/packages/yew/src/context.rs +++ b/packages/yew/src/context.rs @@ -5,7 +5,7 @@ use std::cell::RefCell; use slab::Slab; use crate::html::Scope; -use crate::{html, Callback, Children, Component, Context, Html, Properties}; +use crate::{Callback, Component, Context, Html, Properties}; /// Props for [`ContextProvider`] #[derive(Debug, Clone, PartialEq, Properties)] @@ -13,7 +13,7 @@ pub struct ContextProviderProps { /// Context value to be passed down pub context: T, /// Children - pub children: Children, + pub children: Html, } /// The context provider component. @@ -24,7 +24,6 @@ pub struct ContextProviderProps { #[derive(Debug)] pub struct ContextProvider { context: T, - children: Children, consumers: RefCell>>, } @@ -85,20 +84,15 @@ impl Component for ContextProvider { fn create(ctx: &Context) -> Self { let props = ctx.props(); Self { - children: props.children.clone(), context: props.context.clone(), consumers: RefCell::new(Slab::new()), } } - fn changed(&mut self, ctx: &Context) -> bool { + fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { let props = ctx.props(); - let should_render = if self.children == props.children { - false - } else { - self.children = props.children.clone(); - true - }; + + let should_render = old_props.children != props.children; if self.context != props.context { self.context = props.context.clone(); @@ -108,7 +102,7 @@ impl Component for ContextProvider { should_render } - fn view(&self, _ctx: &Context) -> Html { - html! { <>{ self.children.clone() } } + fn view(&self, ctx: &Context) -> Html { + ctx.props().children.clone() } } diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index 0710174e7f8..5e6130f5497 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -6,21 +6,17 @@ use std::fmt; use web_sys::Element; -use super::{BNode, BSubtree, Reconcilable, ReconcileTarget}; +use super::{BNode, BSubtree, DomSlot, DynamicDomSlot, Reconcilable, ReconcileTarget}; use crate::html::{AnyScope, Scoped}; use crate::virtual_dom::{Key, VComp}; -use crate::NodeRef; /// A virtual component. Compare with [VComp]. pub(super) struct BComp { type_id: TypeId, scope: Box, - // A internal NodeRef passed around to track this components position. This - // is "stable", i.e. does not change when reconciled. - internal_ref: NodeRef, - // The user-passed NodeRef from VComp. Might change every time we reconcile. - // Gets linked to the internal ref - node_ref: NodeRef, + /// An internal [`DomSlot`] passed around to track this components position. This + /// will dynamically adjust when a lifecycle changes the render state of this component. + own_position: DynamicDomSlot, key: Option, } @@ -44,10 +40,10 @@ impl ReconcileTarget for BComp { self.scope.destroy_boxed(parent_to_detach); } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef { - self.scope.shift_node(next_parent.clone(), next_sibling); + fn shift(&self, next_parent: &Element, slot: DomSlot) -> DomSlot { + self.scope.shift_node(next_parent.clone(), slot); - self.internal_ref.clone() + self.own_position.to_position() } } @@ -59,31 +55,29 @@ impl Reconcilable for VComp { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { + slot: DomSlot, + ) -> (DomSlot, Self::Bundle) { let VComp { type_id, mountable, - node_ref, key, + .. } = self; - let internal_ref = NodeRef::default(); - node_ref.link(internal_ref.clone()); + let internal_ref = DynamicDomSlot::new_debug_trapped(); let scope = mountable.mount( root, parent_scope, parent.to_owned(), + slot, internal_ref.clone(), - next_sibling, ); ( - internal_ref.clone(), + internal_ref.to_position(), BComp { type_id, - node_ref, - internal_ref, + own_position: internal_ref, key, scope, }, @@ -95,17 +89,17 @@ impl Reconcilable for VComp { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut BNode, - ) -> NodeRef { + ) -> DomSlot { match bundle { // If the existing bundle is the same type, reuse it and update its properties BNode::Comp(ref mut bcomp) if self.type_id == bcomp.type_id && self.key == bcomp.key => { - self.reconcile(root, parent_scope, parent, next_sibling, bcomp) + self.reconcile(root, parent_scope, parent, slot, bcomp) } - _ => self.replace(root, parent_scope, parent, next_sibling, bundle), + _ => self.replace(root, parent_scope, parent, slot, bundle), } } @@ -114,21 +108,14 @@ impl Reconcilable for VComp { _root: &BSubtree, _parent_scope: &AnyScope, _parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bcomp: &mut Self::Bundle, - ) -> NodeRef { - let VComp { - mountable, - node_ref, - key, - type_id: _, - } = self; + ) -> DomSlot { + let VComp { mountable, key, .. } = self; bcomp.key = key; - let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref); - bcomp.node_ref.reuse(old_ref); - mountable.reuse(bcomp.scope.borrow(), next_sibling); - bcomp.internal_ref.clone() + mountable.reuse(bcomp.scope.borrow(), slot); + bcomp.own_position.to_position() } } @@ -144,15 +131,14 @@ mod feat_hydration { parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, - ) -> (NodeRef, Self::Bundle) { + ) -> Self::Bundle { let VComp { type_id, mountable, - node_ref, key, + .. } = self; - let internal_ref = NodeRef::default(); - node_ref.link(internal_ref.clone()); + let internal_ref = DynamicDomSlot::new_debug_trapped(); let scoped = mountable.hydrate( root.clone(), @@ -162,16 +148,12 @@ mod feat_hydration { fragment, ); - ( - internal_ref.clone(), - BComp { - type_id, - scope: scoped, - node_ref, - internal_ref, - key, - }, - ) + BComp { + type_id, + scope: scoped, + own_position: internal_ref, + key, + } } } } @@ -179,16 +161,14 @@ mod feat_hydration { #[cfg(target_arch = "wasm32")] #[cfg(test)] mod tests { - use std::ops::Deref; - - use gloo_utils::document; + use gloo::utils::document; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - use web_sys::{Element, Node}; + use web_sys::Element; use super::*; - use crate::dom_bundle::{Bundle, Reconcilable, ReconcileTarget}; + use crate::dom_bundle::Reconcilable; use crate::virtual_dom::{Key, VChild, VNode}; - use crate::{html, scheduler, Children, Component, Context, Html, NodeRef, Properties}; + use crate::{html, scheduler, Children, Component, Context, Html, Properties}; wasm_bindgen_test_configure!(run_in_browser); @@ -224,12 +204,12 @@ mod tests { let (root, scope, parent) = setup_parent(); let comp = html! { }; - let (_, mut bundle) = comp.attach(&root, &scope, &parent, NodeRef::default()); + let (_, mut bundle) = comp.attach(&root, &scope, &parent, DomSlot::at_end()); scheduler::start_now(); for _ in 0..10000 { let node = html! { }; - node.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut bundle); + node.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut bundle); scheduler::start_now(); } } @@ -282,31 +262,6 @@ mod tests { check_key(html! { }); } - #[test] - fn set_component_node_ref() { - let test_node: Node = document().create_text_node("test").into(); - let test_node_ref = NodeRef::new(test_node); - let check_node_ref = |vnode: VNode| { - let vcomp = match vnode { - VNode::VComp(vcomp) => vcomp, - _ => unreachable!("should be a vcomp"), - }; - assert_eq!(vcomp.node_ref, test_node_ref); - }; - - let props = Props { - field_1: 1, - field_2: 1, - }; - let props_2 = props.clone(); - - check_node_ref(html! { }); - check_node_ref(html! { }); - check_node_ref(html! { }); - check_node_ref(html! { }); - check_node_ref(html! { }); - } - #[test] fn vchild_partialeq() { let vchild1: VChild = VChild::new( @@ -314,7 +269,6 @@ mod tests { field_1: 1, field_2: 1, }, - NodeRef::default(), None, ); @@ -323,7 +277,6 @@ mod tests { field_1: 1, field_2: 1, }, - NodeRef::default(), None, ); @@ -332,7 +285,6 @@ mod tests { field_1: 2, field_2: 2, }, - NodeRef::default(), None, ); @@ -358,7 +310,7 @@ mod tests { unimplemented!(); } - fn changed(&mut self, _ctx: &Context) -> bool { + fn changed(&mut self, _ctx: &Context, _old_props: &Self::Properties) -> bool { unimplemented!(); } @@ -388,7 +340,7 @@ mod tests { // clear parent parent.set_inner_html(""); - node.attach(root, scope, parent, NodeRef::default()); + node.attach(root, scope, parent, DomSlot::at_end()); scheduler::start_now(); parent.inner_html() } @@ -437,48 +389,6 @@ mod tests { }; assert_eq!(get_html(for_method, &root, &scope, &parent), expected_html); } - - #[test] - fn reset_node_ref() { - let (root, scope, parent) = setup_parent(); - - let node_ref = NodeRef::default(); - let elem = html! { }; - let (_, elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); - scheduler::start_now(); - let parent_node = parent.deref(); - assert_eq!(node_ref.get(), parent_node.first_child()); - elem.detach(&root, &parent, false); - scheduler::start_now(); - assert!(node_ref.get().is_none()); - } - - #[test] - fn reset_ancestors_node_ref() { - let (root, scope, parent) = setup_parent(); - - let mut bundle = Bundle::new(); - let node_ref_a = NodeRef::default(); - let node_ref_b = NodeRef::default(); - let elem = html! { }; - let node_a = bundle.reconcile(&root, &scope, &parent, NodeRef::default(), elem); - scheduler::start_now(); - let node_a = node_a.get().unwrap(); - - assert!(node_ref_a.get().is_some(), "node_ref_a should be bound"); - - let elem = html! { }; - let node_b = bundle.reconcile(&root, &scope, &parent, NodeRef::default(), elem); - scheduler::start_now(); - let node_b = node_b.get().unwrap(); - - assert_eq!(node_a, node_b, "Comp should have reused the element"); - assert!(node_ref_b.get().is_some(), "node_ref_b should be bound"); - assert!( - node_ref_a.get().is_none(), - "node_ref_a should have been reset when the element was reused." - ); - } } #[cfg(target_arch = "wasm32")] diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 0f55824fe00..93a9af585a8 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -4,12 +4,13 @@ use std::cmp::Ordering; use std::collections::HashSet; use std::hash::Hash; use std::ops::Deref; +use std::rc::Rc; use web_sys::Element; -use super::{test_log, BNode, BSubtree}; +use super::{test_log, BNode, BSubtree, DomSlot}; use crate::dom_bundle::{Reconcilable, ReconcileTarget}; -use crate::html::{AnyScope, NodeRef}; +use crate::html::AnyScope; use crate::virtual_dom::{Key, VList, VNode, VText}; /// This struct represents a mounted [VList] @@ -22,6 +23,22 @@ pub(super) struct BList { key: Option, } +impl VList { + // Splits a VList for creating / reconciling to a BList. + fn split_for_blist(self) -> (Option, bool, Vec) { + let fully_keyed = self.fully_keyed(); + + let children = self + .children + .map(Rc::try_unwrap) + .unwrap_or_else(|| Ok(Vec::new())) + // Rc::unwrap_or_clone is not stable yet. + .unwrap_or_else(|m| m.to_vec()); + + (self.key, fully_keyed, children) + } +} + impl Deref for BList { type Target = Vec; @@ -36,7 +53,7 @@ struct NodeWriter<'s> { root: &'s BSubtree, parent_scope: &'s AnyScope, parent: &'s Element, - next_sibling: NodeRef, + slot: DomSlot, } impl<'s> NodeWriter<'s> { @@ -44,48 +61,33 @@ impl<'s> NodeWriter<'s> { fn add(self, node: VNode) -> (Self, BNode) { test_log!("adding: {:?}", node); test_log!( - " parent={:?}, next_sibling={:?}", + " parent={:?}, slot={:?}", self.parent.outer_html(), - self.next_sibling + self.slot ); - let (next, bundle) = - node.attach(self.root, self.parent_scope, self.parent, self.next_sibling); - test_log!(" next_position: {:?}", next); - ( - Self { - next_sibling: next, - ..self - }, - bundle, - ) + let (next, bundle) = node.attach(self.root, self.parent_scope, self.parent, self.slot); + test_log!(" next_slot: {:?}", next); + (Self { slot: next, ..self }, bundle) } /// Shift a bundle into place without patching it fn shift(&self, bundle: &mut BNode) { - bundle.shift(self.parent, self.next_sibling.clone()); + bundle.shift(self.parent, self.slot.clone()); } /// Patch a bundle with a new node fn patch(self, node: VNode, bundle: &mut BNode) -> Self { test_log!("patching: {:?} -> {:?}", bundle, node); test_log!( - " parent={:?}, next_sibling={:?}", + " parent={:?}, slot={:?}", self.parent.outer_html(), - self.next_sibling + self.slot ); // Advance the next sibling reference (from right to left) - let next = node.reconcile_node( - self.root, - self.parent_scope, - self.parent, - self.next_sibling, - bundle, - ); + let next = + node.reconcile_node(self.root, self.parent_scope, self.parent, self.slot, bundle); test_log!(" next_position: {:?}", next); - Self { - next_sibling: next, - ..self - } + Self { slot: next, ..self } } } /// Helper struct implementing [Eq] and [Hash] by only looking at a node's key @@ -148,15 +150,15 @@ impl BList { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, lefts: Vec, rights: &mut Vec, - ) -> NodeRef { + ) -> DomSlot { let mut writer = NodeWriter { root, parent_scope, parent, - next_sibling, + slot, }; // Remove extra nodes @@ -178,7 +180,7 @@ impl BList { rights.push(el); writer = next_writer; } - writer.next_sibling + writer.slot } /// Diff and patch fully keyed child lists. @@ -189,10 +191,10 @@ impl BList { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, left_vdoms: Vec, rev_bundles: &mut Vec, - ) -> NodeRef { + ) -> DomSlot { macro_rules! key { ($v:expr) => { $v.key().expect("unkeyed child in fully keyed list") @@ -216,14 +218,7 @@ impl BList { // Corresponds to adding or removing items from the back of the list if matching_len_end == std::cmp::min(left_vdoms.len(), rev_bundles.len()) { // No key changes - return Self::apply_unkeyed( - root, - parent_scope, - parent, - next_sibling, - left_vdoms, - rev_bundles, - ); + return Self::apply_unkeyed(root, parent_scope, parent, slot, left_vdoms, rev_bundles); } // We partially drain the new vnodes in several steps. @@ -232,7 +227,7 @@ impl BList { root, parent_scope, parent, - next_sibling, + slot, }; // Step 1. Diff matching children at the end let lefts_to = lefts.len() - matching_len_end; @@ -369,7 +364,7 @@ impl BList { writer = writer.patch(l, r); } - writer.next_sibling + writer.slot } } @@ -380,14 +375,12 @@ impl ReconcileTarget for BList { } } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef { - let mut next_sibling = next_sibling; - + fn shift(&self, next_parent: &Element, mut slot: DomSlot) -> DomSlot { for node in self.rev_children.iter() { - next_sibling = node.shift(next_parent, next_sibling.clone()); + slot = node.shift(next_parent, slot); } - next_sibling + slot } } @@ -399,10 +392,10 @@ impl Reconcilable for VList { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { + slot: DomSlot, + ) -> (DomSlot, Self::Bundle) { let mut self_ = BList::new(); - let node_ref = self.reconcile(root, parent_scope, parent, next_sibling, &mut self_); + let node_ref = self.reconcile(root, parent_scope, parent, slot, &mut self_); (node_ref, self_) } @@ -411,23 +404,23 @@ impl Reconcilable for VList { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut BNode, - ) -> NodeRef { + ) -> DomSlot { // 'Forcefully' pretend the existing node is a list. Creates a // singleton list if it isn't already. let blist = bundle.make_list(); - self.reconcile(root, parent_scope, parent, next_sibling, blist) + self.reconcile(root, parent_scope, parent, slot, blist) } fn reconcile( - mut self, + self, root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, blist: &mut BList, - ) -> NodeRef { + ) -> DomSlot { // Here, we will try to diff the previous list elements with the new // ones we want to insert. For that, we will use two lists: // - lefts: new elements to render in the DOM @@ -436,16 +429,16 @@ impl Reconcilable for VList { // The left items are known since we want to insert them // (self.children). For the right ones, we will look at the bundle, // i.e. the current DOM list element that we want to replace with self. + let (key, mut fully_keyed, mut lefts) = self.split_for_blist(); - if self.children.is_empty() { + if lefts.is_empty() { // Without a placeholder the next element becomes first // and corrupts the order of rendering // We use empty text element to stake out a place - self.add_child(VText::new("").into()); + lefts.push(VText::new("").into()); + fully_keyed = false; } - let fully_keyed = self.fully_keyed(); - let lefts = self.children; let rights = &mut blist.rev_children; test_log!("lefts: {:?}", lefts); test_log!("rights: {:?}", rights); @@ -454,12 +447,12 @@ impl Reconcilable for VList { rights.reserve_exact(additional); } let first = if fully_keyed && blist.fully_keyed { - BList::apply_keyed(root, parent_scope, parent, next_sibling, lefts, rights) + BList::apply_keyed(root, parent_scope, parent, slot, lefts, rights) } else { - BList::apply_unkeyed(root, parent_scope, parent, next_sibling, lefts, rights) + BList::apply_unkeyed(root, parent_scope, parent, slot, lefts, rights) }; blist.fully_keyed = fully_keyed; - blist.key = self.key; + blist.key = key; test_log!("result: {:?}", rights); first } @@ -477,32 +470,24 @@ mod feat_hydration { parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, - ) -> (NodeRef, Self::Bundle) { - let node_ref = NodeRef::default(); - let fully_keyed = self.fully_keyed(); - let vchildren = self.children; - let mut children = Vec::with_capacity(vchildren.len()); + ) -> Self::Bundle { + let (key, fully_keyed, vchildren) = self.split_for_blist(); - for (index, child) in vchildren.into_iter().enumerate() { - let (child_node_ref, child) = child.hydrate(root, parent_scope, parent, fragment); + let mut children = Vec::with_capacity(vchildren.len()); - if index == 0 { - node_ref.link(child_node_ref); - } + for child in vchildren.into_iter() { + let child = child.hydrate(root, parent_scope, parent, fragment); children.push(child); } children.reverse(); - ( - node_ref, - BList { - rev_children: children, - fully_keyed, - key: self.key, - }, - ) + BList { + rev_children: children, + fully_keyed, + key, + } } } } @@ -652,7 +637,7 @@ mod layout_tests_keys { fn diff() { let mut layouts = vec![]; - let vref_node: Node = gloo_utils::document().create_element("i").unwrap().into(); + let vref_node: Node = gloo::utils::document().create_element("i").unwrap().into(); layouts.push(TestLayout { name: "All VNode types as children", node: html! { diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 104e803a494..d6fdbe5aa47 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -2,12 +2,11 @@ use std::fmt; -use gloo::console; use web_sys::{Element, Node}; -use super::{BComp, BList, BPortal, BSubtree, BSuspense, BTag, BText}; +use super::{BComp, BList, BPortal, BRaw, BSubtree, BSuspense, BTag, BText, DomSlot}; use crate::dom_bundle::{Reconcilable, ReconcileTarget}; -use crate::html::{AnyScope, NodeRef}; +use crate::html::AnyScope; use crate::virtual_dom::{Key, VNode}; /// The bundle implementation to [VNode]. @@ -26,6 +25,8 @@ pub(super) enum BNode { Ref(Node), /// A suspendible document fragment. Suspense(Box), + /// A raw HTML string, represented by [`AttrValue`](crate::AttrValue). + Raw(BRaw), } impl BNode { @@ -39,6 +40,7 @@ impl BNode { Self::Text(_) => None, Self::Portal(bportal) => bportal.key(), Self::Suspense(bsusp) => bsusp.key(), + Self::Raw(_) => None, } } } @@ -54,29 +56,29 @@ impl ReconcileTarget for BNode { Self::Ref(ref node) => { // Always remove user-defined nodes to clear possible parent references of them if parent.remove_child(node).is_err() { - console::warn!("Node not found to remove VRef"); + tracing::warn!("Node not found to remove VRef"); } } Self::Portal(bportal) => bportal.detach(root, parent, parent_to_detach), Self::Suspense(bsusp) => bsusp.detach(root, parent, parent_to_detach), + Self::Raw(raw) => raw.detach(root, parent, parent_to_detach), } } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef { + fn shift(&self, next_parent: &Element, slot: DomSlot) -> DomSlot { match self { - Self::Tag(ref vtag) => vtag.shift(next_parent, next_sibling), - Self::Text(ref btext) => btext.shift(next_parent, next_sibling), - Self::Comp(ref bsusp) => bsusp.shift(next_parent, next_sibling), - Self::List(ref vlist) => vlist.shift(next_parent, next_sibling), + Self::Tag(ref vtag) => vtag.shift(next_parent, slot), + Self::Text(ref btext) => btext.shift(next_parent, slot), + Self::Comp(ref bsusp) => bsusp.shift(next_parent, slot), + Self::List(ref vlist) => vlist.shift(next_parent, slot), Self::Ref(ref node) => { - next_parent - .insert_before(node, next_sibling.get().as_ref()) - .unwrap(); + slot.insert(next_parent, node); - NodeRef::new(node.clone()) + DomSlot::at(node.clone()) } - Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling), - Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling), + Self::Portal(ref vportal) => vportal.shift(next_parent, slot), + Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, slot), + Self::Raw(ref braw) => braw.shift(next_parent, slot), } } } @@ -89,38 +91,41 @@ impl Reconcilable for VNode { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { + slot: DomSlot, + ) -> (DomSlot, Self::Bundle) { match self { VNode::VTag(vtag) => { - let (node_ref, tag) = vtag.attach(root, parent_scope, parent, next_sibling); + let (node_ref, tag) = vtag.attach(root, parent_scope, parent, slot); (node_ref, tag.into()) } VNode::VText(vtext) => { - let (node_ref, text) = vtext.attach(root, parent_scope, parent, next_sibling); + let (node_ref, text) = vtext.attach(root, parent_scope, parent, slot); (node_ref, text.into()) } VNode::VComp(vcomp) => { - let (node_ref, comp) = vcomp.attach(root, parent_scope, parent, next_sibling); + let (node_ref, comp) = vcomp.attach(root, parent_scope, parent, slot); (node_ref, comp.into()) } VNode::VList(vlist) => { - let (node_ref, list) = vlist.attach(root, parent_scope, parent, next_sibling); + let (node_ref, list) = vlist.attach(root, parent_scope, parent, slot); (node_ref, list.into()) } VNode::VRef(node) => { - super::insert_node(&node, parent, next_sibling.get().as_ref()); - (NodeRef::new(node.clone()), BNode::Ref(node)) + slot.insert(parent, &node); + (DomSlot::at(node.clone()), BNode::Ref(node)) } VNode::VPortal(vportal) => { - let (node_ref, portal) = vportal.attach(root, parent_scope, parent, next_sibling); + let (node_ref, portal) = vportal.attach(root, parent_scope, parent, slot); (node_ref, portal.into()) } VNode::VSuspense(vsuspsense) => { - let (node_ref, suspsense) = - vsuspsense.attach(root, parent_scope, parent, next_sibling); + let (node_ref, suspsense) = vsuspsense.attach(root, parent_scope, parent, slot); (node_ref, suspsense.into()) } + VNode::VRaw(vraw) => { + let (node_ref, raw) = vraw.attach(root, parent_scope, parent, slot); + (node_ref, raw.into()) + } } } @@ -129,10 +134,10 @@ impl Reconcilable for VNode { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut BNode, - ) -> NodeRef { - self.reconcile(root, parent_scope, parent, next_sibling, bundle) + ) -> DomSlot { + self.reconcile(root, parent_scope, parent, slot, bundle) } fn reconcile( @@ -140,43 +145,25 @@ impl Reconcilable for VNode { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut BNode, - ) -> NodeRef { + ) -> DomSlot { match self { - VNode::VTag(vtag) => { - vtag.reconcile_node(root, parent_scope, parent, next_sibling, bundle) - } - VNode::VText(vtext) => { - vtext.reconcile_node(root, parent_scope, parent, next_sibling, bundle) - } - VNode::VComp(vcomp) => { - vcomp.reconcile_node(root, parent_scope, parent, next_sibling, bundle) - } - VNode::VList(vlist) => { - vlist.reconcile_node(root, parent_scope, parent, next_sibling, bundle) - } - VNode::VRef(node) => { - let _existing = match bundle { - BNode::Ref(ref n) if &node == n => n, - _ => { - return VNode::VRef(node).replace( - root, - parent_scope, - parent, - next_sibling, - bundle, - ); - } - }; - NodeRef::new(node) - } + VNode::VTag(vtag) => vtag.reconcile_node(root, parent_scope, parent, slot, bundle), + VNode::VText(vtext) => vtext.reconcile_node(root, parent_scope, parent, slot, bundle), + VNode::VComp(vcomp) => vcomp.reconcile_node(root, parent_scope, parent, slot, bundle), + VNode::VList(vlist) => vlist.reconcile_node(root, parent_scope, parent, slot, bundle), + VNode::VRef(node) => match bundle { + BNode::Ref(ref n) if &node == n => DomSlot::at(node), + _ => VNode::VRef(node).replace(root, parent_scope, parent, slot, bundle), + }, VNode::VPortal(vportal) => { - vportal.reconcile_node(root, parent_scope, parent, next_sibling, bundle) + vportal.reconcile_node(root, parent_scope, parent, slot, bundle) } VNode::VSuspense(vsuspsense) => { - vsuspsense.reconcile_node(root, parent_scope, parent, next_sibling, bundle) + vsuspsense.reconcile_node(root, parent_scope, parent, slot, bundle) } + VNode::VRaw(vraw) => vraw.reconcile_node(root, parent_scope, parent, slot, bundle), } } } @@ -223,6 +210,13 @@ impl From for BNode { } } +impl From for BNode { + #[inline] + fn from(braw: BRaw) -> Self { + Self::Raw(braw) + } +} + impl fmt::Debug for BNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { @@ -233,6 +227,7 @@ impl fmt::Debug for BNode { Self::Ref(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)), Self::Portal(ref vportal) => vportal.fmt(f), Self::Suspense(ref bsusp) => bsusp.fmt(f), + Self::Raw(ref braw) => braw.fmt(f), } } } @@ -249,24 +244,12 @@ mod feat_hydration { parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, - ) -> (NodeRef, Self::Bundle) { + ) -> Self::Bundle { match self { - VNode::VTag(vtag) => { - let (node_ref, tag) = vtag.hydrate(root, parent_scope, parent, fragment); - (node_ref, tag.into()) - } - VNode::VText(vtext) => { - let (node_ref, text) = vtext.hydrate(root, parent_scope, parent, fragment); - (node_ref, text.into()) - } - VNode::VComp(vcomp) => { - let (node_ref, comp) = vcomp.hydrate(root, parent_scope, parent, fragment); - (node_ref, comp.into()) - } - VNode::VList(vlist) => { - let (node_ref, list) = vlist.hydrate(root, parent_scope, parent, fragment); - (node_ref, list.into()) - } + VNode::VTag(vtag) => vtag.hydrate(root, parent_scope, parent, fragment).into(), + VNode::VText(vtext) => vtext.hydrate(root, parent_scope, parent, fragment).into(), + VNode::VComp(vcomp) => vcomp.hydrate(root, parent_scope, parent, fragment).into(), + VNode::VList(vlist) => vlist.hydrate(root, parent_scope, parent, fragment).into(), // You cannot hydrate a VRef. VNode::VRef(_) => { panic!( @@ -281,11 +264,10 @@ mod feat_hydration { use_effect." ) } - VNode::VSuspense(vsuspense) => { - let (node_ref, suspense) = - vsuspense.hydrate(root, parent_scope, parent, fragment); - (node_ref, suspense.into()) - } + VNode::VSuspense(vsuspense) => vsuspense + .hydrate(root, parent_scope, parent, fragment) + .into(), + VNode::VRaw(vraw) => vraw.hydrate(root, parent_scope, parent, fragment).into(), } } } @@ -303,7 +285,7 @@ mod layout_tests { #[test] fn diff() { - let document = gloo_utils::document(); + let document = gloo::utils::document(); let vref_node_1 = VNode::VRef(document.create_element("i").unwrap().into()); let vref_node_2 = VNode::VRef(document.create_element("b").unwrap().into()); diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index 669ff6ad2af..a7e5d76c9a0 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -1,10 +1,10 @@ //! This module contains the bundle implementation of a portal [BPortal]. -use web_sys::Element; +use web_sys::{Element, Node}; -use super::{test_log, BNode, BSubtree}; +use super::{test_log, BNode, BSubtree, DomSlot}; use crate::dom_bundle::{Reconcilable, ReconcileTarget}; -use crate::html::{AnyScope, NodeRef}; +use crate::html::AnyScope; use crate::virtual_dom::{Key, VPortal}; /// The bundle implementation to [VPortal]. @@ -15,7 +15,7 @@ pub struct BPortal { /// The element under which the content is inserted. host: Element, /// The next sibling after the inserted content - inner_sibling: NodeRef, + inner_sibling: Option, /// The inserted node node: Box, } @@ -26,10 +26,9 @@ impl ReconcileTarget for BPortal { self.node.detach(&self.inner_root, &self.host, false); } - fn shift(&self, _next_parent: &Element, next_sibling: NodeRef) -> NodeRef { - // portals have nothing in it's original place of DOM, we also do nothing. - - next_sibling + fn shift(&self, _next_parent: &Element, slot: DomSlot) -> DomSlot { + // portals have nothing in its original place of DOM, we also do nothing. + slot } } @@ -41,17 +40,18 @@ impl Reconcilable for VPortal { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - host_next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { + host_slot: DomSlot, + ) -> (DomSlot, Self::Bundle) { let Self { host, inner_sibling, node, } = self; + let inner_slot = DomSlot::create(inner_sibling.clone()); let inner_root = root.create_subroot(parent.clone(), &host); - let (_, inner) = node.attach(&inner_root, parent_scope, &host, inner_sibling.clone()); + let (_, inner) = node.attach(&inner_root, parent_scope, &host, inner_slot); ( - host_next_sibling, + host_slot, BPortal { inner_root, host, @@ -66,14 +66,12 @@ impl Reconcilable for VPortal { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut BNode, - ) -> NodeRef { + ) -> DomSlot { match bundle { - BNode::Portal(portal) => { - self.reconcile(root, parent_scope, parent, next_sibling, portal) - } - _ => self.replace(root, parent_scope, parent, next_sibling, bundle), + BNode::Portal(portal) => self.reconcile(root, parent_scope, parent, slot, portal), + _ => self.replace(root, parent_scope, parent, slot, bundle), } } @@ -81,10 +79,10 @@ impl Reconcilable for VPortal { self, _root: &BSubtree, parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, + _parent: &Element, + host_slot: DomSlot, portal: &mut Self::Bundle, - ) -> NodeRef { + ) -> DomSlot { let Self { host, inner_sibling, @@ -92,22 +90,24 @@ impl Reconcilable for VPortal { } = self; let old_host = std::mem::replace(&mut portal.host, host); - let old_inner_sibling = std::mem::replace(&mut portal.inner_sibling, inner_sibling); - if old_host != portal.host || old_inner_sibling != portal.inner_sibling { + let should_shift = old_host != portal.host || portal.inner_sibling != inner_sibling; + portal.inner_sibling = inner_sibling; + let inner_slot = DomSlot::create(portal.inner_sibling.clone()); + + if should_shift { // Remount the inner node somewhere else instead of diffing // Move the node, but keep the state - let inner_sibling = portal.inner_sibling.clone(); - portal.node.shift(&portal.host, inner_sibling); + portal.node.shift(&portal.host, inner_slot.clone()); } node.reconcile_node( &portal.inner_root, parent_scope, - parent, - next_sibling.clone(), + &portal.host, + inner_slot, &mut portal.node, ); - next_sibling + host_slot } } @@ -123,22 +123,26 @@ impl BPortal { mod layout_tests { extern crate self as yew; + use gloo::utils::document; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + use web_sys::HtmlInputElement; use yew::virtual_dom::VPortal; - use crate::html; + use super::*; + use crate::html::NodeRef; use crate::tests::layout_tests::{diff_layouts, TestLayout}; use crate::virtual_dom::VNode; + use crate::{create_portal, html}; wasm_bindgen_test_configure!(run_in_browser); #[test] fn diff() { let mut layouts = vec![]; - let first_target = gloo_utils::document().create_element("i").unwrap(); - let second_target = gloo_utils::document().create_element("o").unwrap(); - let target_with_child = gloo_utils::document().create_element("i").unwrap(); - let target_child = gloo_utils::document().create_element("s").unwrap(); + let first_target = gloo::utils::document().create_element("i").unwrap(); + let second_target = gloo::utils::document().create_element("o").unwrap(); + let target_with_child = gloo::utils::document().create_element("i").unwrap(); + let target_child = gloo::utils::document().create_element("s").unwrap(); target_with_child.append_child(&target_child).unwrap(); layouts.push(TestLayout { @@ -171,6 +175,21 @@ mod layout_tests { }, expected: "
    PORTALAFTER
    ", }); + layouts.push(TestLayout { + name: "Portal - update inner content", + node: html! { +
    + {VNode::VRef(first_target.clone().into())} + {VNode::VRef(second_target.clone().into())} + {VNode::VPortal(VPortal::new( + html! { <> {"PORTAL"} }, + second_target.clone(), + ))} + {"AFTER"} +
    + }, + expected: "
    PORTALAFTER
    ", + }); layouts.push(TestLayout { name: "Portal - replaced by text", node: html! { @@ -200,4 +219,43 @@ mod layout_tests { diff_layouts(layouts) } + + fn setup_parent_with_portal() -> (BSubtree, AnyScope, Element, Element) { + let scope = AnyScope::test(); + let parent = document().create_element("div").unwrap(); + let portal_host = document().create_element("div").unwrap(); + let root = BSubtree::create_root(&parent); + + let body = document().body().unwrap(); + body.append_child(&parent).unwrap(); + body.append_child(&portal_host).unwrap(); + + (root, scope, parent, portal_host) + } + + #[test] + fn test_no_shift() { + // Portals shouldn't shift (which e.g. causes internal inputs to unfocus) when sibling + // doesn't change. + let (root, scope, parent, portal_host) = setup_parent_with_portal(); + let input_ref = NodeRef::default(); + + let portal = create_portal( + html! { }, + portal_host, + ); + let (_, mut bundle) = portal + .clone() + .attach(&root, &scope, &parent, DomSlot::at_end()); + + // Focus the input, then reconcile again + let input_el = input_ref.cast::().unwrap(); + input_el.focus().unwrap(); + + let _ = portal.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut bundle); + + let new_input_el = input_ref.cast::().unwrap(); + assert_eq!(input_el, new_input_el); + assert_eq!(document().active_element(), Some(new_input_el.into())); + } } diff --git a/packages/yew/src/dom_bundle/braw.rs b/packages/yew/src/dom_bundle/braw.rs new file mode 100644 index 00000000000..0d122a87c7e --- /dev/null +++ b/packages/yew/src/dom_bundle/braw.rs @@ -0,0 +1,405 @@ +use wasm_bindgen::JsCast; +use web_sys::{Element, Node}; + +use super::{BNode, BSubtree, DomSlot, Reconcilable, ReconcileTarget}; +use crate::html::AnyScope; +use crate::virtual_dom::VRaw; +use crate::AttrValue; + +#[derive(Debug)] +pub struct BRaw { + reference: Option, + children_count: usize, + html: AttrValue, +} + +impl BRaw { + fn create_elements(html: &str) -> Vec { + let div = gloo::utils::document().create_element("div").unwrap(); + div.set_inner_html(html); + let children = div.child_nodes(); + let children = js_sys::Array::from(&children); + let children = children.to_vec(); + children + .into_iter() + .map(|it| it.unchecked_into()) + .collect::>() + } + + fn detach_bundle(&self, parent: &Element) { + let mut next_node = self.reference.clone(); + for _ in 0..self.children_count { + if let Some(node) = next_node { + next_node = node.next_sibling(); + parent.remove_child(&node).unwrap(); + } + } + } + + fn position(&self, next_slot: DomSlot) -> DomSlot { + self.reference + .as_ref() + .map(|n| DomSlot::at(n.clone())) + .unwrap_or(next_slot) + } +} + +impl ReconcileTarget for BRaw { + fn detach(self, _root: &BSubtree, parent: &Element, _parent_to_detach: bool) { + self.detach_bundle(parent); + } + + fn shift(&self, next_parent: &Element, slot: DomSlot) -> DomSlot { + let mut next_node = self.reference.clone(); + for _ in 0..self.children_count { + if let Some(node) = next_node { + next_node = node.next_sibling(); + slot.insert(next_parent, &node); + } + } + self.position(slot) + } +} + +impl Reconcilable for VRaw { + type Bundle = BRaw; + + fn attach( + self, + _root: &BSubtree, + _parent_scope: &AnyScope, + parent: &Element, + slot: DomSlot, + ) -> (DomSlot, Self::Bundle) { + let elements = BRaw::create_elements(&self.html); + let count = elements.len(); + let mut iter = elements.into_iter(); + let reference = iter.next(); + if let Some(ref first) = reference { + slot.insert(parent, first); + for ref child in iter { + slot.insert(parent, child); + } + } + let this = BRaw { + reference, + children_count: count, + html: self.html, + }; + (this.position(slot), this) + } + + fn reconcile_node( + self, + root: &BSubtree, + parent_scope: &AnyScope, + parent: &Element, + slot: DomSlot, + bundle: &mut BNode, + ) -> DomSlot { + match bundle { + BNode::Raw(raw) if raw.html == self.html => raw.position(slot), + BNode::Raw(raw) => self.reconcile(root, parent_scope, parent, slot, raw), + _ => self.replace(root, parent_scope, parent, slot, bundle), + } + } + + fn reconcile( + self, + root: &BSubtree, + parent_scope: &AnyScope, + parent: &Element, + slot: DomSlot, + bundle: &mut Self::Bundle, + ) -> DomSlot { + if self.html != bundle.html { + // we don't have a way to diff what's changed in the string so we remove the node and + // reattach it + bundle.detach_bundle(parent); + let (node_ref, braw) = self.attach(root, parent_scope, parent, slot); + *bundle = braw; + node_ref + } else { + bundle.position(slot) + } + } +} + +#[cfg(feature = "hydration")] +mod feat_hydration { + use super::*; + use crate::dom_bundle::{Fragment, Hydratable}; + use crate::virtual_dom::Collectable; + + impl Hydratable for VRaw { + fn hydrate( + self, + _root: &BSubtree, + _parent_scope: &AnyScope, + parent: &Element, + fragment: &mut Fragment, + ) -> Self::Bundle { + let collectable = Collectable::Raw; + let fallback_fragment = Fragment::collect_between(fragment, &collectable, parent); + + let Self { html } = self; + + BRaw { + children_count: fallback_fragment.len(), + reference: fallback_fragment.iter().next().cloned(), + html, + } + } + } +} + +#[cfg(target_arch = "wasm32")] +#[cfg(test)] +mod tests { + use gloo::utils::document; + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + use super::*; + use crate::dom_bundle::utils::{setup_parent, setup_parent_and_sibling, SIBLING_CONTENT}; + use crate::virtual_dom::VNode; + + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn braw_works_one_node() { + let (root, scope, parent) = setup_parent(); + + const HTML: &str = "text"; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), HTML) + } + + #[test] + fn braw_works_no_node() { + let (root, scope, parent) = setup_parent(); + + const HTML: &str = ""; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), HTML) + } + + #[test] + fn braw_works_one_node_nested() { + let (root, scope, parent) = setup_parent(); + + const HTML: &str = + r#"

    one link more paragraph

    here
    "#; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), HTML) + } + #[test] + fn braw_works_multi_top_nodes() { + let (root, scope, parent) = setup_parent(); + + const HTML: &str = r#"

    paragraph

    link"#; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), HTML) + } + + #[test] + fn braw_detach_works_multi_node() { + let (root, scope, parent) = setup_parent(); + + const HTML: &str = r#"

    paragraph

    link"#; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), HTML); + elem.detach(&root, &parent, false); + assert_eq!(parent.inner_html(), ""); + } + + #[test] + fn braw_detach_works_single_node() { + let (root, scope, parent) = setup_parent(); + + const HTML: &str = r#"

    paragraph

    "#; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), HTML); + elem.detach(&root, &parent, false); + assert_eq!(parent.inner_html(), ""); + } + + #[test] + fn braw_detach_works_empty() { + let (root, scope, parent) = setup_parent(); + + const HTML: &str = ""; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), HTML); + elem.detach(&root, &parent, false); + assert_eq!(parent.inner_html(), ""); + } + + #[test] + fn braw_works_one_node_sibling_attached() { + let (root, scope, parent, sibling) = setup_parent_and_sibling(); + + const HTML: &str = "text"; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT)); + } + + #[test] + fn braw_works_no_node_sibling_attached() { + let (root, scope, parent, sibling) = setup_parent_and_sibling(); + + const HTML: &str = ""; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT)); + } + + #[test] + fn braw_works_one_node_nested_sibling_attached() { + let (root, scope, parent, sibling) = setup_parent_and_sibling(); + + const HTML: &str = + r#"

    one link more paragraph

    here
    "#; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT)); + } + #[test] + fn braw_works_multi_top_nodes_sibling_attached() { + let (root, scope, parent, sibling) = setup_parent_and_sibling(); + + const HTML: &str = r#"

    paragraph

    link"#; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT)); + } + + #[test] + fn braw_detach_works_multi_node_sibling_attached() { + let (root, scope, parent, sibling) = setup_parent_and_sibling(); + + const HTML: &str = r#"

    paragraph

    link"#; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT)); + elem.detach(&root, &parent, false); + assert_eq!(parent.inner_html(), format!("{}", SIBLING_CONTENT)) + } + + #[test] + fn braw_detach_works_single_node_sibling_attached() { + let (root, scope, parent, sibling) = setup_parent_and_sibling(); + + const HTML: &str = r#"

    paragraph

    "#; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT)); + elem.detach(&root, &parent, false); + assert_eq!(parent.inner_html(), format!("{}", SIBLING_CONTENT)) + } + + #[test] + fn braw_detach_works_empty_sibling_attached() { + let (root, scope, parent, sibling) = setup_parent_and_sibling(); + + const HTML: &str = ""; + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT)); + elem.detach(&root, &parent, false); + assert_eq!(parent.inner_html(), format!("{}", SIBLING_CONTENT)) + } + + #[test] + fn braw_shift_works() { + let (root, scope, parent) = setup_parent(); + const HTML: &str = r#"

    paragraph

    "#; + + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), HTML); + + let new_parent = document().create_element("section").unwrap(); + document().body().unwrap().append_child(&parent).unwrap(); + + elem.shift(&new_parent, DomSlot::at_end()); + + assert_eq!(new_parent.inner_html(), HTML); + assert_eq!(parent.inner_html(), ""); + } + + #[test] + fn braw_shift_with_sibling_works() { + let (root, scope, parent, sibling) = setup_parent_and_sibling(); + const HTML: &str = r#"

    paragraph

    "#; + + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT)); + + let new_parent = document().create_element("section").unwrap(); + document().body().unwrap().append_child(&parent).unwrap(); + + let new_sibling = document().create_text_node(SIBLING_CONTENT); + new_parent.append_child(&new_sibling).unwrap(); + let new_sibling_ref = DomSlot::at(new_sibling.into()); + + elem.shift(&new_parent, new_sibling_ref); + + assert_eq!(parent.inner_html(), SIBLING_CONTENT); + + assert_eq!( + new_parent.inner_html(), + format!("{}{}", HTML, SIBLING_CONTENT) + ); + } + + #[test] + fn braw_shift_works_multi_node() { + let (root, scope, parent) = setup_parent(); + const HTML: &str = r#"

    paragraph

    link"#; + + let elem = VNode::from_html_unchecked(HTML.into()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); + assert_braw(&mut elem); + assert_eq!(parent.inner_html(), HTML); + + let new_parent = document().create_element("section").unwrap(); + document().body().unwrap().append_child(&parent).unwrap(); + + elem.shift(&new_parent, DomSlot::at_end()); + + assert_eq!(parent.inner_html(), ""); + assert_eq!(new_parent.inner_html(), HTML); + } + + fn assert_braw(node: &mut BNode) -> &mut BRaw { + if let BNode::Raw(braw) = node { + return braw; + } + panic!("should be braw"); + } +} diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index d9ecb216063..38bb3cf7bb0 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -5,10 +5,9 @@ use web_sys::Element; #[cfg(feature = "hydration")] use super::Fragment; -use super::{BNode, BSubtree, Reconcilable, ReconcileTarget}; +use super::{BNode, BSubtree, DomSlot, Reconcilable, ReconcileTarget}; use crate::html::AnyScope; use crate::virtual_dom::{Key, VSuspense}; -use crate::NodeRef; #[derive(Debug)] enum Fallback { @@ -60,12 +59,12 @@ impl ReconcileTarget for BSuspense { } } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef { + fn shift(&self, next_parent: &Element, slot: DomSlot) -> DomSlot { match self.fallback.as_ref() { - Some(Fallback::Bundle(bundle)) => bundle.shift(next_parent, next_sibling), + Some(Fallback::Bundle(bundle)) => bundle.shift(next_parent, slot), #[cfg(feature = "hydration")] - Some(Fallback::Fragment(fragment)) => fragment.shift(next_parent, next_sibling), - None => self.children_bundle.shift(next_parent, next_sibling), + Some(Fallback::Fragment(fragment)) => fragment.shift(next_parent, slot), + None => self.children_bundle.shift(next_parent, slot), } } } @@ -78,8 +77,8 @@ impl Reconcilable for VSuspense { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { + slot: DomSlot, + ) -> (DomSlot, Self::Bundle) { let VSuspense { children, fallback, @@ -94,9 +93,8 @@ impl Reconcilable for VSuspense { // tree while rendering fallback UI into the original place where children resides in. if suspended { let (_child_ref, children_bundle) = - children.attach(root, parent_scope, &detached_parent, NodeRef::default()); - let (fallback_ref, fallback) = - fallback.attach(root, parent_scope, parent, next_sibling); + children.attach(root, parent_scope, &detached_parent, DomSlot::at_end()); + let (fallback_ref, fallback) = fallback.attach(root, parent_scope, parent, slot); ( fallback_ref, BSuspense { @@ -107,8 +105,7 @@ impl Reconcilable for VSuspense { }, ) } else { - let (child_ref, children_bundle) = - children.attach(root, parent_scope, parent, next_sibling); + let (child_ref, children_bundle) = children.attach(root, parent_scope, parent, slot); ( child_ref, BSuspense { @@ -126,15 +123,15 @@ impl Reconcilable for VSuspense { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut BNode, - ) -> NodeRef { + ) -> DomSlot { match bundle { // We only preserve the child state if they are the same suspense. BNode::Suspense(m) if m.key == self.key => { - self.reconcile(root, parent_scope, parent, next_sibling, m) + self.reconcile(root, parent_scope, parent, slot, m) } - _ => self.replace(root, parent_scope, parent, next_sibling, bundle), + _ => self.replace(root, parent_scope, parent, slot, bundle), } } @@ -143,9 +140,9 @@ impl Reconcilable for VSuspense { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, suspense: &mut Self::Bundle, - ) -> NodeRef { + ) -> DomSlot { let VSuspense { children, fallback: vfallback, @@ -165,41 +162,40 @@ impl Reconcilable for VSuspense { root, parent_scope, &suspense.detached_parent, - NodeRef::default(), + DomSlot::at_end(), children_bundle, ); match fallback { Fallback::Bundle(bundle) => { - vfallback.reconcile_node(root, parent_scope, parent, next_sibling, bundle) + vfallback.reconcile_node(root, parent_scope, parent, slot, bundle) } #[cfg(feature = "hydration")] Fallback::Fragment(fragment) => match fragment.front().cloned() { - Some(m) => NodeRef::new(m), - None => next_sibling, + Some(m) => DomSlot::at(m), + None => slot, }, } } // Not suspended, just reconcile the children into the DOM (false, None) => { - children.reconcile_node(root, parent_scope, parent, next_sibling, children_bundle) + children.reconcile_node(root, parent_scope, parent, slot, children_bundle) } // Freshly suspended. Shift children into the detached parent, then add fallback to the // DOM (true, None) => { - children_bundle.shift(&suspense.detached_parent, NodeRef::default()); + children_bundle.shift(&suspense.detached_parent, DomSlot::at_end()); children.reconcile_node( root, parent_scope, &suspense.detached_parent, - NodeRef::default(), + DomSlot::at_end(), children_bundle, ); // first render of fallback - let (fallback_ref, fallback) = - vfallback.attach(root, parent_scope, parent, next_sibling); + let (fallback_ref, fallback) = vfallback.attach(root, parent_scope, parent, slot); suspense.fallback = Some(Fallback::Bundle(fallback)); fallback_ref } @@ -218,8 +214,8 @@ impl Reconcilable for VSuspense { } }; - children_bundle.shift(parent, next_sibling.clone()); - children.reconcile_node(root, parent_scope, parent, next_sibling, children_bundle) + children_bundle.shift(parent, slot.clone()); + children.reconcile_node(root, parent_scope, parent, slot, children_bundle) } } } @@ -238,7 +234,7 @@ mod feat_hydration { parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, - ) -> (NodeRef, Self::Bundle) { + ) -> Self::Bundle { let detached_parent = document() .create_element("div") .expect("failed to create detached element"); @@ -254,33 +250,24 @@ mod feat_hydration { // Even if initially suspended, these children correspond to the first non-suspended // content Refer to VSuspense::render_to_string - let (_, children_bundle) = + let children_bundle = self.children .hydrate(root, parent_scope, &detached_parent, &mut nodes); // We trim all leading text nodes before checking as it's likely these are whitespaces. - nodes.trim_start_text_nodes(&detached_parent); + nodes.trim_start_text_nodes(); assert!(nodes.is_empty(), "expected end of suspense, found node."); - let node_ref = fallback_fragment - .front() - .cloned() - .map(NodeRef::new) - .unwrap_or_default(); + BSuspense { + children_bundle, + detached_parent, + key: self.key, - ( - node_ref, - BSuspense { - children_bundle, - detached_parent, - key: self.key, - - // We start hydration with the BSuspense being suspended. - // A subsequent render will resume the BSuspense if not needed to be suspended. - fallback: Some(Fallback::Fragment(fallback_fragment)), - }, - ) + // We start hydration with the BSuspense being suspended. + // A subsequent render will resume the BSuspense if not needed to be suspended. + fallback: Some(Fallback::Fragment(fallback_fragment)), + } } } } diff --git a/packages/yew/src/dom_bundle/btag/attributes.rs b/packages/yew/src/dom_bundle/btag/attributes.rs index 05bbf507286..e2f41e11498 100644 --- a/packages/yew/src/dom_bundle/btag/attributes.rs +++ b/packages/yew/src/dom_bundle/btag/attributes.rs @@ -2,13 +2,14 @@ use std::collections::HashMap; use std::ops::Deref; use indexmap::IndexMap; +use wasm_bindgen::{intern, JsValue}; use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; use yew::AttrValue; use super::Apply; use crate::dom_bundle::BSubtree; use crate::virtual_dom::vtag::{InputFields, Value}; -use crate::virtual_dom::Attributes; +use crate::virtual_dom::{ApplyAttributeAs, Attributes}; impl Apply for Value { type Bundle = Self; @@ -66,18 +67,22 @@ impl Apply for InputFields { type Element = InputElement; fn apply(mut self, root: &BSubtree, el: &Self::Element) -> Self { - // IMPORTANT! This parameter has to be set every time + // IMPORTANT! This parameter has to be set every time it's explicitly given // to prevent strange behaviour in the browser when the DOM changes - el.set_checked(self.checked); + if let Some(checked) = self.checked { + el.set_checked(checked); + } self.value = self.value.apply(root, el); self } fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut Self) { - // IMPORTANT! This parameter has to be set every time + // IMPORTANT! This parameter has to be set every time it's explicitly given // to prevent strange behaviour in the browser when the DOM changes - el.set_checked(self.checked); + if let Some(checked) = self.checked { + el.set_checked(checked); + } self.value.apply_diff(root, el, &mut bundle.value); } @@ -87,23 +92,23 @@ impl Attributes { #[cold] fn apply_diff_index_maps( el: &Element, - new: &IndexMap, - old: &IndexMap, + new: &IndexMap, + old: &IndexMap, ) { for (key, value) in new.iter() { match old.get(key) { Some(old_value) => { if value != old_value { - Self::set_attribute(el, key, value); + Self::set(el, key, value.0.as_ref(), value.1); } } - None => Self::set_attribute(el, key, value), + None => Self::set(el, key, value.0.as_ref(), value.1), } } - for (key, _value) in old.iter() { + for (key, (_, apply_as)) in old.iter() { if !new.contains_key(key) { - Self::remove_attribute(el, key); + Self::remove(el, key, *apply_as); } } } @@ -112,17 +117,26 @@ impl Attributes { /// Works with any [Attributes] variants. #[cold] fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) { - fn collect(src: &Attributes) -> HashMap<&str, &str> { + fn collect(src: &Attributes) -> HashMap<&str, (&str, ApplyAttributeAs)> { use Attributes::*; match src { - Static(arr) => (*arr).iter().map(|[k, v]| (*k, *v)).collect(), + Static(arr) => (*arr) + .iter() + .map(|(k, v, apply_as)| (*k, (*v, *apply_as))) + .collect(), Dynamic { keys, values } => keys .iter() .zip(values.iter()) - .filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref()))) + .filter_map(|(k, v)| { + v.as_ref() + .map(|(v, apply_as)| (*k, (v.as_ref(), *apply_as))) + }) + .collect(), + IndexMap(m) => m + .iter() + .map(|(k, (v, apply_as))| (k.as_ref(), (v.as_ref(), *apply_as))) .collect(), - IndexMap(m) => m.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect(), } } @@ -135,25 +149,42 @@ impl Attributes { Some(old) => old != new, None => true, } { - el.set_attribute(k, new).unwrap(); + Self::set(el, k, new.0, new.1); } } // Remove missing - for k in old.keys() { + for (k, (_, apply_as)) in old.iter() { if !new.contains_key(k) { - Self::remove_attribute(el, k); + Self::remove(el, k, *apply_as); } } } - fn set_attribute(el: &Element, key: &str, value: &str) { - el.set_attribute(key, value).expect("invalid attribute key") + fn set(el: &Element, key: &str, value: &str, apply_as: ApplyAttributeAs) { + match apply_as { + ApplyAttributeAs::Attribute => el + .set_attribute(intern(key), value) + .expect("invalid attribute key"), + ApplyAttributeAs::Property => { + let key = JsValue::from_str(key); + let value = JsValue::from_str(value); + js_sys::Reflect::set(el.as_ref(), &key, &value).expect("could not set property"); + } + } } - fn remove_attribute(el: &Element, key: &str) { - el.remove_attribute(key) - .expect("could not remove attribute") + fn remove(el: &Element, key: &str, apply_as: ApplyAttributeAs) { + match apply_as { + ApplyAttributeAs::Attribute => el + .remove_attribute(intern(key)) + .expect("could not remove attribute"), + ApplyAttributeAs::Property => { + let key = JsValue::from_str(key); + js_sys::Reflect::set(el.as_ref(), &key, &JsValue::UNDEFINED) + .expect("could not remove property"); + } + } } } @@ -164,20 +195,20 @@ impl Apply for Attributes { fn apply(self, _root: &BSubtree, el: &Element) -> Self { match &self { Self::Static(arr) => { - for kv in arr.iter() { - Self::set_attribute(el, kv[0], kv[1]); + for (k, v, apply_as) in arr.iter() { + Self::set(el, k, v, *apply_as); } } Self::Dynamic { keys, values } => { for (k, v) in keys.iter().zip(values.iter()) { - if let Some(v) = v { - Self::set_attribute(el, k, v) + if let Some((v, apply_as)) = v { + Self::set(el, k, v, *apply_as) } } } Self::IndexMap(m) => { - for (k, v) in m.iter() { - Self::set_attribute(el, k, v) + for (k, (v, apply_as)) in m.iter() { + Self::set(el, k, v, *apply_as) } } } @@ -217,7 +248,7 @@ impl Apply for Attributes { } macro_rules! set { ($new:expr) => { - Self::set_attribute(el, key!(), $new) + Self::set(el, key!(), $new.0.as_ref(), $new.1) }; } @@ -228,8 +259,8 @@ impl Apply for Attributes { } } (Some(new), None) => set!(new), - (None, Some(_)) => { - Self::remove_attribute(el, key!()); + (None, Some(old)) => { + Self::remove(el, key!(), old.1); } (None, None) => (), } @@ -237,7 +268,7 @@ impl Apply for Attributes { } // For VTag's constructed outside the html! macro (Self::IndexMap(new), Self::IndexMap(ref old)) => { - Self::apply_diff_index_maps(el, &*new, &*old); + Self::apply_diff_index_maps(el, new, old); } // Cold path. Happens only with conditional swapping and reordering of `VTag`s with the // same tag and no keys. @@ -247,3 +278,111 @@ impl Apply for Attributes { } } } + +#[cfg(target_arch = "wasm32")] +#[cfg(test)] +mod tests { + use std::time::Duration; + + use gloo::utils::document; + use js_sys::Reflect; + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + use super::*; + use crate::{function_component, html, Html}; + + wasm_bindgen_test_configure!(run_in_browser); + + fn create_element() -> (Element, BSubtree) { + let element = document() + .create_element("a") + .expect("failed to create element"); + let btree = BSubtree::create_root(&element); + (element, btree) + } + + #[test] + fn properties_are_set() { + let attrs = Attributes::Static(&[ + ("href", "https://example.com/", ApplyAttributeAs::Property), + ("alt", "somewhere", ApplyAttributeAs::Property), + ]); + let (element, btree) = create_element(); + attrs.apply(&btree, &element); + assert_eq!( + Reflect::get(element.as_ref(), &JsValue::from_str("href")) + .expect("no href") + .as_string() + .expect("not a string"), + "https://example.com/", + "property `href` not set properly" + ); + assert_eq!( + Reflect::get(element.as_ref(), &JsValue::from_str("alt")) + .expect("no alt") + .as_string() + .expect("not a string"), + "somewhere", + "property `alt` not set properly" + ); + } + + #[test] + fn respects_apply_as() { + let attrs = Attributes::Static(&[ + ("href", "https://example.com/", ApplyAttributeAs::Attribute), + ("alt", "somewhere", ApplyAttributeAs::Property), + ]); + let (element, btree) = create_element(); + attrs.apply(&btree, &element); + assert_eq!( + element.outer_html(), + "", + "should be set as attribute" + ); + assert_eq!( + Reflect::get(element.as_ref(), &JsValue::from_str("alt")) + .expect("no alt") + .as_string() + .expect("not a string"), + "somewhere", + "property `alt` not set properly" + ); + } + + #[test] + fn class_is_always_attrs() { + let attrs = Attributes::Static(&[("class", "thing", ApplyAttributeAs::Attribute)]); + + let (element, btree) = create_element(); + attrs.apply(&btree, &element); + assert_eq!(element.get_attribute("class").unwrap(), "thing"); + } + + #[test] + async fn macro_syntax_works() { + #[function_component] + fn Comp() -> Html { + html! { } + } + + let output = gloo::utils::document().get_element_by_id("output").unwrap(); + yew::Renderer::::with_root(output.clone()).render(); + + gloo::timers::future::sleep(Duration::from_secs(1)).await; + let element = output.query_selector("a").unwrap().unwrap(); + assert_eq!( + element.get_attribute("href").unwrap(), + "https://example.com/" + ); + + assert_eq!( + Reflect::get(element.as_ref(), &JsValue::from_str("alt")) + .expect("no alt") + .as_string() + .expect("not a string"), + "abc", + "property `alt` not set properly" + ); + } +} diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index 0ebbc345e25..8866896f38c 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -66,7 +66,7 @@ impl Apply for Listeners { (Pending(pending), Registered(ref id)) => { // Reuse the ID test_log!("reusing listeners for {}", id); - root.with_listener_registry(|reg| reg.patch(root, id, &*pending)); + root.with_listener_registry(|reg| reg.patch(root, id, &pending)); } (Pending(pending), bundle @ NoReg) => { *bundle = ListenerRegistration::register(root, el, &pending); @@ -202,10 +202,10 @@ mod tests { use std::marker::PhantomData; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - use web_sys::{Event, EventInit, HtmlElement, MouseEvent}; + use web_sys::{Event, EventInit, FocusEvent, HtmlElement, MouseEvent}; wasm_bindgen_test_configure!(run_in_browser); - use gloo_utils::document; + use gloo::utils::document; use wasm_bindgen::JsCast; use yew::Callback; @@ -536,6 +536,45 @@ mod tests { assert_count(&el, 2); } + #[test] + fn non_bubbling() { + #[derive(Default, PartialEq, Properties)] + struct NonBubbling; + + impl Mixin for NonBubbling { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component>, + { + let onfocus = ctx.link().callback(|_| Message::Action); + let onfocus_inner = ctx.link().callback(|e: FocusEvent| { + assert!(!e.bubbles(), "event should be non-bubbling"); + Message::Action + }); + + html! { +
    + +
    + } + } + } + // Should only trigger the inner listener, not also the outer one + let (_, el) = init::(); + + assert_count(&el, 0); + el.get() + .unwrap() + .dyn_into::() + .unwrap() + .focus() + .unwrap(); + scheduler::start_now(); + assert_count(&el, 1); + } + /// Here an event is being delivered to a DOM node which is contained /// in a portal. It should bubble through the portal and reach the containing /// element. @@ -710,4 +749,43 @@ mod tests { .unwrap() }) } + + #[test] + fn reentrant_listener() { + #[derive(PartialEq, Properties, Default)] + struct Reetrant { + secondary_target_ref: NodeRef, + } + impl Mixin for Reetrant { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component>, + { + let targetref = &ctx.props().wrapped.secondary_target_ref; + let onclick = { + let targetref = targetref.clone(); + ctx.link().callback(move |_| { + // Note: `click` (and dispatchEvent for that matter) swallows errors thrown + // from listeners and reports them as uncaught to the console. Hence, we + // assert that we got to the second event listener instead, by dispatching a + // second Message::Action + click(&targetref); + Message::Action + }) + }; + let onclick2 = ctx.link().callback(move |_| Message::Action); + html! { +
    + } + } + } + let (_, el) = init::(); + + assert_count(&el, 0); + click(&el); + assert_count(&el, 2); + } } diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index d96bb364aeb..fca3b521344 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -4,19 +4,20 @@ mod attributes; mod listeners; use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; use std::hint::unreachable_unchecked; use std::ops::DerefMut; -use gloo::console; -use gloo_utils::document; +use gloo::utils::document; use listeners::ListenerRegistration; pub use listeners::Registry; use wasm_bindgen::JsCast; use web_sys::{Element, HtmlTextAreaElement as TextAreaElement}; -use super::{insert_node, BList, BNode, BSubtree, Reconcilable, ReconcileTarget}; +use super::{BNode, BSubtree, DomSlot, Reconcilable, ReconcileTarget}; use crate::html::AnyScope; -use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE}; +use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, MATHML_NAMESPACE, SVG_NAMESPACE}; use crate::virtual_dom::{Attributes, Key, VTag}; use crate::NodeRef; @@ -51,8 +52,8 @@ enum BTagInner { Other { /// A tag of the element. tag: Cow<'static, str>, - /// List of child nodes - child_bundle: BList, + /// Child node. + child_bundle: BNode, }, } @@ -84,7 +85,7 @@ impl ReconcileTarget for BTag { let result = parent.remove_child(&node); if result.is_err() { - console::warn!("Node not found to remove VTag"); + tracing::warn!("Node not found to remove VTag"); } } // It could be that the ref was already reused when rendering another element. @@ -94,12 +95,10 @@ impl ReconcileTarget for BTag { } } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef { - next_parent - .insert_before(&self.reference, next_sibling.get().as_ref()) - .unwrap(); + fn shift(&self, next_parent: &Element, slot: DomSlot) -> DomSlot { + slot.insert(next_parent, &self.reference); - self.node_ref.clone() + DomSlot::at(self.reference.clone().into()) } } @@ -111,8 +110,8 @@ impl Reconcilable for VTag { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { + slot: DomSlot, + ) -> (DomSlot, Self::Bundle) { let el = self.create_element(parent); let Self { listeners, @@ -121,7 +120,7 @@ impl Reconcilable for VTag { key, .. } = self; - insert_node(&el, parent, next_sibling.get().as_ref()); + slot.insert(parent, &el); let attributes = attributes.apply(root, &el); let listeners = listeners.apply(root, &el); @@ -136,14 +135,13 @@ impl Reconcilable for VTag { BTagInner::Textarea { value } } VTagInner::Other { children, tag } => { - let (_, child_bundle) = - children.attach(root, parent_scope, &el, NodeRef::default()); + let (_, child_bundle) = children.attach(root, parent_scope, &el, DomSlot::at_end()); BTagInner::Other { child_bundle, tag } } }; node_ref.set(Some(el.clone().into())); ( - node_ref.clone(), + DomSlot::at(el.clone().into()), BTag { inner, listeners, @@ -160,9 +158,9 @@ impl Reconcilable for VTag { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut BNode, - ) -> NodeRef { + ) -> DomSlot { // This kind of branching patching routine reduces branch predictor misses and the need to // unpack the enums (including `Option`s) all the time, resulting in a more streamlined // patching flow @@ -180,18 +178,12 @@ impl Reconcilable for VTag { } _ => false, } { - return self.reconcile( - root, - parent_scope, - parent, - next_sibling, - ex.deref_mut(), - ); + return self.reconcile(root, parent_scope, parent, slot, ex.deref_mut()); } } _ => {} }; - self.replace(root, parent_scope, parent, next_sibling, bundle) + self.replace(root, parent_scope, parent, slot, bundle) } fn reconcile( @@ -199,9 +191,9 @@ impl Reconcilable for VTag { root: &BSubtree, parent_scope: &AnyScope, _parent: &Element, - _next_sibling: NodeRef, + _slot: DomSlot, tag: &mut Self::Bundle, - ) -> NodeRef { + ) -> DomSlot { let el = &tag.reference; self.attributes.apply_diff(root, el, &mut tag.attributes); self.listeners.apply_diff(root, el, &mut tag.listeners); @@ -219,7 +211,7 @@ impl Reconcilable for VTag { child_bundle: old, .. }, ) => { - new.reconcile(root, parent_scope, el, NodeRef::default(), old); + new.reconcile(root, parent_scope, el, DomSlot::at_end(), old); } // Can not happen, because we checked for tag equability above _ => unsafe { unreachable_unchecked() }, @@ -235,13 +227,14 @@ impl Reconcilable for VTag { tag.node_ref.set(Some(el.clone().into())); } - tag.node_ref.clone() + DomSlot::at(el.clone().into()) } } impl VTag { fn create_element(&self, parent: &Element) -> Element { let tag = self.tag(); + if tag == "svg" || parent .namespace_uri() @@ -251,10 +244,41 @@ impl VTag { document() .create_element_ns(namespace, tag) .expect("can't create namespaced element for vtag") - } else { + } else if tag == "math" + || parent + .namespace_uri() + .map_or(false, |ns| ns == MATHML_NAMESPACE) + { + let namespace = Some(MATHML_NAMESPACE); document() - .create_element(tag) - .expect("can't create element for vtag") + .create_element_ns(namespace, tag) + .expect("can't create namespaced element for vtag") + } else { + thread_local! { + static CACHED_ELEMENTS: RefCell> = RefCell::new(HashMap::with_capacity(32)); + } + + CACHED_ELEMENTS.with(|cache| { + let mut cache = cache.borrow_mut(); + let cached = cache.get(tag).map(|el| { + el.clone_node() + .expect("couldn't clone cached element") + .unchecked_into::() + }); + cached.unwrap_or_else(|| { + let to_be_cached = document() + .create_element(tag) + .expect("can't create element for vtag"); + cache.insert( + tag.to_string(), + to_be_cached + .clone_node() + .expect("couldn't clone node to be cached") + .unchecked_into(), + ); + to_be_cached + }) + }) } } } @@ -273,10 +297,10 @@ impl BTag { #[cfg(target_arch = "wasm32")] #[cfg(test)] - fn children(&self) -> &[BNode] { + fn children(&self) -> Option<&BNode> { match &self.inner { - BTagInner::Other { child_bundle, .. } => child_bundle, - _ => &[], + BTagInner::Other { child_bundle, .. } => Some(child_bundle), + _ => None, } } @@ -303,9 +327,9 @@ mod feat_hydration { self, root: &BSubtree, parent_scope: &AnyScope, - parent: &Element, + _parent: &Element, fragment: &mut Fragment, - ) -> (NodeRef, Self::Bundle) { + ) -> Self::Bundle { let tag_name = self.tag().to_owned(); let Self { @@ -317,11 +341,11 @@ mod feat_hydration { } = self; // We trim all text nodes as it's likely these are whitespaces. - fragment.trim_start_text_nodes(parent); + fragment.trim_start_text_nodes(); let node = fragment .pop_front() - .unwrap_or_else(|| panic!("expected element of type {}, found EOF.", tag_name)); + .unwrap_or_else(|| panic!("expected element of type {tag_name}, found EOF.")); assert_eq!( node.node_type(), @@ -356,9 +380,9 @@ mod feat_hydration { } VTagInner::Other { children, tag } => { let mut nodes = Fragment::collect_children(&el); - let (_, child_bundle) = children.hydrate(root, parent_scope, &el, &mut nodes); + let child_bundle = children.hydrate(root, parent_scope, &el, &mut nodes); - nodes.trim_start_text_nodes(parent); + nodes.trim_start_text_nodes(); assert!(nodes.is_empty(), "expected EOF, found node."); @@ -368,17 +392,14 @@ mod feat_hydration { node_ref.set(Some((*el).clone())); - ( - node_ref.clone(), - BTag { - inner, - listeners, - attributes, - reference: el, - node_ref, - key, - }, - ) + BTag { + inner, + listeners, + attributes, + reference: el, + node_ref, + key, + } } } } @@ -386,30 +407,19 @@ mod feat_hydration { #[cfg(target_arch = "wasm32")] #[cfg(test)] mod tests { - use gloo_utils::document; use wasm_bindgen::JsCast; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use web_sys::HtmlInputElement as InputElement; use super::*; + use crate::dom_bundle::utils::setup_parent; use crate::dom_bundle::{BNode, Reconcilable, ReconcileTarget}; - use crate::html::AnyScope; use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE}; use crate::virtual_dom::{AttrValue, VNode, VTag}; use crate::{html, Html, NodeRef}; wasm_bindgen_test_configure!(run_in_browser); - fn setup_parent() -> (BSubtree, AnyScope, Element) { - let scope = AnyScope::test(); - let parent = document().create_element("div").unwrap(); - let root = BSubtree::create_root(&parent); - - document().body().unwrap().append_child(&parent).unwrap(); - - (root, scope, parent) - } - #[test] fn it_compares_tags() { let a = html! { @@ -598,20 +608,33 @@ mod tests { let svg_node = html! { {path_node} }; let svg_tag = assert_vtag(svg_node); - let (_, svg_tag) = svg_tag.attach(&root, &scope, &parent, NodeRef::default()); + let (_, svg_tag) = svg_tag.attach(&root, &scope, &parent, DomSlot::at_end()); assert_namespace(&svg_tag, SVG_NAMESPACE); - let path_tag = assert_btag_ref(svg_tag.children().get(0).unwrap()); + let path_tag = assert_btag_ref(svg_tag.children().unwrap()); assert_namespace(path_tag, SVG_NAMESPACE); let g_tag = assert_vtag(g_node.clone()); - let (_, g_tag) = g_tag.attach(&root, &scope, &parent, NodeRef::default()); + let (_, g_tag) = g_tag.attach(&root, &scope, &parent, DomSlot::at_end()); assert_namespace(&g_tag, HTML_NAMESPACE); let g_tag = assert_vtag(g_node); - let (_, g_tag) = g_tag.attach(&root, &scope, &svg_el, NodeRef::default()); + let (_, g_tag) = g_tag.attach(&root, &scope, &svg_el, DomSlot::at_end()); assert_namespace(&g_tag, SVG_NAMESPACE); } + #[test] + fn supports_mathml() { + let (root, scope, parent) = setup_parent(); + let mfrac_node = html! { }; + let math_node = html! { {mfrac_node} }; + + let math_tag = assert_vtag(math_node); + let (_, math_tag) = math_tag.attach(&root, &scope, &parent, DomSlot::at_end()); + assert_namespace(&math_tag, MATHML_NAMESPACE); + let mfrac_tag = assert_btag_ref(math_tag.children().unwrap()); + assert_namespace(mfrac_tag, MATHML_NAMESPACE); + } + #[test] fn it_compares_values() { let a = html! { @@ -707,7 +730,7 @@ mod tests { let (root, scope, parent) = setup_parent(); let elem = html! {
    }; - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); let vtag = assert_btag_mut(&mut elem); // test if the className has not been set assert!(!vtag.reference().has_attribute("class")); @@ -717,7 +740,7 @@ mod tests { let (root, scope, parent) = setup_parent(); let elem = gen_html(); - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); let vtag = assert_btag_mut(&mut elem); // test if the className has been set assert!(vtag.reference().has_attribute("class")); @@ -741,7 +764,7 @@ mod tests { // Initial state let elem = html! { }; - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); let vtag = assert_btag_ref(&elem); // User input @@ -753,7 +776,7 @@ mod tests { let elem_vtag = assert_vtag(next_elem); // Sync happens here - elem_vtag.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); + elem_vtag.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem); let vtag = assert_btag_ref(&elem); // Get new current value of the input element @@ -772,7 +795,7 @@ mod tests { // Initial state let elem = html! { }; - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); let vtag = assert_btag_ref(&elem); // User input @@ -784,7 +807,7 @@ mod tests { let elem_vtag = assert_vtag(next_elem); // Value should not be refreshed - elem_vtag.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); + elem_vtag.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem); let vtag = assert_btag_ref(&elem); // Get user value of the input element @@ -811,7 +834,7 @@ mod tests { builder }/> }; - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); let vtag = assert_btag_mut(&mut elem); // make sure the new tag name is used internally assert_eq!(vtag.tag(), "a"); @@ -869,7 +892,7 @@ mod tests { let node_ref = NodeRef::default(); let elem: VNode = html! {
    }; assert_vtag_ref(&elem); - let (_, elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); + let (_, elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); assert_eq!(node_ref.get(), parent.first_child()); elem.detach(&root, &parent, false); assert!(node_ref.get().is_none()); @@ -881,14 +904,14 @@ mod tests { let node_ref_a = NodeRef::default(); let elem_a = html! {
    }; - let (_, mut elem) = elem_a.attach(&root, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem_a.attach(&root, &scope, &parent, DomSlot::at_end()); // save the Node to check later that it has been reused. let node_a = node_ref_a.get().unwrap(); let node_ref_b = NodeRef::default(); let elem_b = html! {
    }; - elem_b.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); + elem_b.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem); let node_b = node_ref_b.get().unwrap(); @@ -918,8 +941,8 @@ mod tests { // The point of this diff is to first render the "after" div and then detach the "before" // div, while both should be bound to the same node ref - let (_, mut elem) = before.attach(&root, &scope, &parent, NodeRef::default()); - after.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); + let (_, mut elem) = before.attach(&root, &scope, &parent, DomSlot::at_end()); + after.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem); assert_eq!( test_ref @@ -950,7 +973,7 @@ mod tests { let elem = VNode::VTag(Box::new(vtag)); - let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, DomSlot::at_end()); // Create
    (removed first attribute "disabled") let mut vtag = VTag::new("div"); @@ -961,7 +984,7 @@ mod tests { // Sync happens here // this should remove the the "disabled" attribute - elem_vtag.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); + elem_vtag.reconcile_node(&root, &scope, &parent, DomSlot::at_end(), &mut elem); assert_eq!( test_ref @@ -971,7 +994,7 @@ mod tests { .unwrap() .outer_html(), "
    " - ) + ); } } diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index 7e6e0f1a908..881f1a34000 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -1,13 +1,11 @@ //! This module contains the bundle implementation of text [BText]. -use gloo::console; -use gloo_utils::document; +use gloo::utils::document; use web_sys::{Element, Text as TextNode}; -use super::{insert_node, BNode, BSubtree, Reconcilable, ReconcileTarget}; +use super::{BNode, BSubtree, DomSlot, Reconcilable, ReconcileTarget}; use crate::html::AnyScope; use crate::virtual_dom::{AttrValue, VText}; -use crate::NodeRef; /// The bundle implementation to [VText] pub(super) struct BText { @@ -21,19 +19,15 @@ impl ReconcileTarget for BText { let result = parent.remove_child(&self.text_node); if result.is_err() { - console::warn!("Node not found to remove VText"); + tracing::warn!("Node not found to remove VText"); } } } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef { - let node = &self.text_node; + fn shift(&self, next_parent: &Element, slot: DomSlot) -> DomSlot { + slot.insert(next_parent, &self.text_node); - next_parent - .insert_before(node, next_sibling.get().as_ref()) - .unwrap(); - - NodeRef::new(self.text_node.clone().into()) + DomSlot::at(self.text_node.clone().into()) } } @@ -45,12 +39,12 @@ impl Reconcilable for VText { _root: &BSubtree, _parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { + slot: DomSlot, + ) -> (DomSlot, Self::Bundle) { let Self { text } = self; let text_node = document().create_text_node(&text); - insert_node(&text_node, parent, next_sibling.get().as_ref()); - let node_ref = NodeRef::new(text_node.clone().into()); + slot.insert(parent, &text_node); + let node_ref = DomSlot::at(text_node.clone().into()); (node_ref, BText { text, text_node }) } @@ -60,12 +54,12 @@ impl Reconcilable for VText { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut BNode, - ) -> NodeRef { + ) -> DomSlot { match bundle { - BNode::Text(btext) => self.reconcile(root, parent_scope, parent, next_sibling, btext), - _ => self.replace(root, parent_scope, parent, next_sibling, bundle), + BNode::Text(btext) => self.reconcile(root, parent_scope, parent, slot, btext), + _ => self.replace(root, parent_scope, parent, slot, bundle), } } @@ -74,15 +68,15 @@ impl Reconcilable for VText { _root: &BSubtree, _parent_scope: &AnyScope, _parent: &Element, - _next_sibling: NodeRef, + _slot: DomSlot, btext: &mut Self::Bundle, - ) -> NodeRef { + ) -> DomSlot { let Self { text } = self; let ancestor_text = std::mem::replace(&mut btext.text, text); if btext.text != ancestor_text { btext.text_node.set_node_value(Some(&btext.text)); } - NodeRef::new(btext.text_node.clone().into()) + DomSlot::at(btext.text_node.clone().into()) } } @@ -103,52 +97,47 @@ mod feat_hydration { impl Hydratable for VText { fn hydrate( self, - root: &BSubtree, - parent_scope: &AnyScope, + _root: &BSubtree, + _parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, - ) -> (NodeRef, Self::Bundle) { - if let Some(m) = fragment.front().cloned() { + ) -> Self::Bundle { + let next_sibling = if let Some(m) = fragment.front().cloned() { // better safe than sorry. if m.node_type() == Node::TEXT_NODE { - if let Ok(m) = m.dyn_into::() { - // pop current node. - fragment.pop_front(); - - // TODO: It may make sense to assert the text content in the text node - // against the VText when #[cfg(debug_assertions)] - // is true, but this may be complicated. - // We always replace the text value for now. - // - // Please see the next comment for a detailed explanation. - m.set_node_value(Some(self.text.as_ref())); - - return ( - NodeRef::new(m.clone().into()), - BText { - text: self.text, - text_node: m, - }, - ); - } + let m = m.unchecked_into::(); + // pop current node. + fragment.pop_front(); + + // TODO: It may make sense to assert the text content in the text node + // against the VText when #[cfg(debug_assertions)] + // is true, but this may be complicated. + // We always replace the text value for now. + // + // Please see the next comment for a detailed explanation. + m.set_node_value(Some(self.text.as_ref())); + + return BText { + text: self.text, + text_node: m, + }; } - } + Some(m) + } else { + fragment.sibling_at_end().cloned() + }; // If there are multiple text nodes placed back-to-back in SSR, it may be parsed as a // single text node by browser, hence we need to add extra text nodes here // if the next node is not a text node. Similarly, the value of the text // node may be a combination of multiple VText vnodes. So we always need to // override their values. - self.attach( - root, - parent_scope, - parent, - fragment - .front() - .cloned() - .map(NodeRef::new) - .unwrap_or_default(), - ) + let text_node = document().create_text_node(""); + DomSlot::create(next_sibling).insert(parent, &text_node); + BText { + text: "".into(), + text_node, + } } } } diff --git a/packages/yew/src/dom_bundle/fragment.rs b/packages/yew/src/dom_bundle/fragment.rs index ccc9037044f..4ae51273138 100644 --- a/packages/yew/src/dom_bundle/fragment.rs +++ b/packages/yew/src/dom_bundle/fragment.rs @@ -1,15 +1,15 @@ use std::collections::VecDeque; use std::ops::{Deref, DerefMut}; +use wasm_bindgen::JsCast; use web_sys::{Element, Node}; -use crate::dom_bundle::BSubtree; -use crate::html::NodeRef; +use super::{BSubtree, DomSlot}; use crate::virtual_dom::Collectable; /// A Hydration Fragment #[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct Fragment(VecDeque); +pub(crate) struct Fragment(VecDeque, Option); impl Deref for Fragment { type Target = VecDeque; @@ -39,7 +39,7 @@ impl Fragment { fragment.push_back(m); } - Self(fragment) + Self(fragment, None) } /// Collects nodes for a Component Bundle or a BSuspense. @@ -49,21 +49,21 @@ impl Fragment { parent: &Element, ) -> Self { let is_open_tag = |node: &Node| { - let comment_text = node.text_content().unwrap_or_else(|| "".to_string()); + let comment_text = node.text_content().unwrap_or_default(); comment_text.starts_with(collect_for.open_start_mark()) && comment_text.ends_with(collect_for.end_mark()) }; let is_close_tag = |node: &Node| { - let comment_text = node.text_content().unwrap_or_else(|| "".to_string()); + let comment_text = node.text_content().unwrap_or_default(); comment_text.starts_with(collect_for.close_start_mark()) && comment_text.ends_with(collect_for.end_mark()) }; // We trim all leading text nodes as it's likely these are whitespaces. - collect_from.trim_start_text_nodes(parent); + collect_from.trim_start_text_nodes(); let first_node = collect_from .pop_front() @@ -115,19 +115,20 @@ impl Fragment { } } - nodes.push_back(current_node.clone()); + nodes.push_back(current_node); } - Self(nodes) + let next_child = collect_from.0.front().cloned(); + Self(nodes, next_child) } /// Remove child nodes until first non-text node. - pub fn trim_start_text_nodes(&mut self, parent: &Element) { + pub fn trim_start_text_nodes(&mut self) { while let Some(ref m) = self.front().cloned() { if m.node_type() == Node::TEXT_NODE { self.pop_front(); - parent.remove_child(m).unwrap(); + m.unchecked_ref::().remove(); } else { break; } @@ -141,7 +142,8 @@ impl Fragment { .map(|m| m.clone_node_with_deep(true).expect("failed to clone node.")) .collect::>(); - Self(nodes) + // the cloned nodes are disconnected from the real dom, so next_child is `None` + Self(nodes, None) } // detaches current fragment. @@ -156,16 +158,16 @@ impl Fragment { } /// Shift current Fragment into a different position in the dom. - pub fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef { + pub fn shift(&self, next_parent: &Element, slot: DomSlot) -> DomSlot { for node in self.iter() { - next_parent - .insert_before(node, next_sibling.get().as_ref()) - .unwrap(); + slot.insert(next_parent, node); } - self.front() - .cloned() - .map(NodeRef::new) - .unwrap_or(next_sibling) + self.front().cloned().map(DomSlot::at).unwrap_or(slot) + } + + /// Return the node that comes after all the nodes in this fragment + pub fn sibling_at_end(&self) -> Option<&Node> { + self.1.as_ref() } } diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index a8cac1c900d..d35225a0a7d 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -7,16 +7,18 @@ use web_sys::Element; -use crate::html::{AnyScope, NodeRef}; +use crate::html::AnyScope; use crate::virtual_dom::VNode; mod bcomp; mod blist; mod bnode; mod bportal; +mod braw; mod bsuspense; mod btag; mod btext; +mod position; mod subtree_root; mod traits; @@ -26,13 +28,15 @@ use bcomp::BComp; use blist::BList; use bnode::BNode; use bportal::BPortal; +use braw::BRaw; use bsuspense::BSuspense; use btag::{BTag, Registry}; use btext::BText; +pub(crate) use position::{DomSlot, DynamicDomSlot}; use subtree_root::EventDescriptor; pub use subtree_root::{set_event_bubbling, BSubtree}; use traits::{Reconcilable, ReconcileTarget}; -use utils::{insert_node, test_log}; +use utils::test_log; /// A Bundle. /// @@ -51,8 +55,8 @@ impl Bundle { } /// Shifts the bundle into a different position. - pub fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - self.0.shift(next_parent, next_sibling); + pub fn shift(&self, next_parent: &Element, slot: DomSlot) { + self.0.shift(next_parent, slot); } /// Applies a virtual dom layout to current bundle. @@ -61,10 +65,10 @@ impl Bundle { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, next_node: VNode, - ) -> NodeRef { - next_node.reconcile_node(root, parent_scope, parent, next_sibling, &mut self.0) + ) -> DomSlot { + next_node.reconcile_node(root, parent_scope, parent, slot, &mut self.0) } /// Detaches current bundle. @@ -80,7 +84,7 @@ mod feat_hydration { pub(super) use super::utils::node_type_str; #[path = "./fragment.rs"] mod fragment; - pub use fragment::Fragment; + pub(crate) use fragment::Fragment; use super::*; impl Bundle { @@ -91,9 +95,9 @@ mod feat_hydration { parent: &Element, fragment: &mut Fragment, node: VNode, - ) -> (NodeRef, Self) { - let (node_ref, bundle) = node.hydrate(root, parent_scope, parent, fragment); - (node_ref, Self(bundle)) + ) -> Self { + let bundle = node.hydrate(root, parent_scope, parent, fragment); + Self(bundle) } } } diff --git a/packages/yew/src/dom_bundle/position.rs b/packages/yew/src/dom_bundle/position.rs new file mode 100644 index 00000000000..8a493a4e1a6 --- /dev/null +++ b/packages/yew/src/dom_bundle/position.rs @@ -0,0 +1,256 @@ +//! Structs for keeping track where in the DOM a node belongs + +use std::cell::RefCell; +use std::rc::Rc; + +use web_sys::{Element, Node}; + +/// A position in the list of children of an implicit parent [`Element`]. +/// +/// This can either be in front of a `DomSlot::at(next_sibling)`, at the end of the list with +/// `DomSlot::at_end()`, or a dynamic position in the list with [`DynamicDomSlot::to_position`]. +#[derive(Clone)] +pub(crate) struct DomSlot { + variant: DomSlotVariant, +} + +#[derive(Clone)] +enum DomSlotVariant { + Node(Option), + Chained(DynamicDomSlot), +} + +/// A dynamic dom slot can be reassigned. This change is also seen by the [`DomSlot`] from +/// [`Self::to_position`] before the reassignment took place. +#[derive(Clone)] +pub(crate) struct DynamicDomSlot { + target: Rc>, +} + +impl std::fmt::Debug for DomSlot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.with_next_sibling(|n| { + write!( + f, + "DomSlot {{ next_sibling: {:?} }}", + n.map(crate::utils::print_node) + ) + }) + } +} + +impl std::fmt::Debug for DynamicDomSlot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:#?}", *self.target.borrow()) + } +} + +#[cfg(debug_assertions)] +thread_local! { + // A special marker element that should not be referenced + static TRAP: Node = gloo::utils::document().create_element("div").unwrap().into(); +} + +impl DomSlot { + /// Denotes the position just before the given node in its parent's list of children. + pub fn at(next_sibling: Node) -> Self { + Self::create(Some(next_sibling)) + } + + /// Denotes the position at the end of a list of children. The parent is implicit. + pub fn at_end() -> Self { + Self::create(None) + } + + pub fn create(next_sibling: Option) -> Self { + Self { + variant: DomSlotVariant::Node(next_sibling), + } + } + + /// A new "placeholder" [DomSlot] that should not be used to insert nodes + #[inline] + pub fn new_debug_trapped() -> Self { + #[cfg(debug_assertions)] + { + Self::at(TRAP.with(|trap| trap.clone())) + } + #[cfg(not(debug_assertions))] + { + Self::at_end() + } + } + + /// Get the [Node] that comes just after the position, or `None` if this denotes the position at + /// the end + fn with_next_sibling(&self, f: impl FnOnce(Option<&Node>) -> R) -> R { + let checkedf = |node: Option<&Node>| { + #[cfg(debug_assertions)] + TRAP.with(|trap| { + assert!( + node != Some(trap), + "Should not use a trapped DomSlot. Please report this as an internal bug in \ + yew." + ) + }); + f(node) + }; + + match &self.variant { + DomSlotVariant::Node(ref n) => checkedf(n.as_ref()), + DomSlotVariant::Chained(ref chain) => chain.with_next_sibling(checkedf), + } + } + + /// Insert a [Node] at the position denoted by this slot. `parent` must be the actual parent + /// element of the children that this slot is implicitly a part of. + pub(super) fn insert(&self, parent: &Element, node: &Node) { + self.with_next_sibling(|next_sibling| { + parent + .insert_before(node, next_sibling) + .unwrap_or_else(|err| { + let msg = if next_sibling.is_some() { + "failed to insert node before next sibling" + } else { + "failed to append child" + }; + // Log normally, so we can inspect the nodes in console + gloo::console::error!(msg, err, parent, next_sibling, node); + // Log via tracing for consistency + tracing::error!(msg); + // Panic to short-curcuit and fail + panic!("{}", msg) + }); + }); + } + + #[cfg(target_arch = "wasm32")] + #[cfg(test)] + fn get(&self) -> Option { + self.with_next_sibling(|n| n.cloned()) + } +} + +impl DynamicDomSlot { + /// Create a dynamic dom slot that initially represents ("targets") the same slot as the + /// argument. + pub fn new(initial_position: DomSlot) -> Self { + Self { + target: Rc::new(RefCell::new(initial_position)), + } + } + + pub fn new_debug_trapped() -> Self { + Self::new(DomSlot::new_debug_trapped()) + } + + /// Change the [`DomSlot`] that is targeted. Subsequently, this will behave as if `self` was + /// created from the passed DomSlot in the first place. + pub fn reassign(&self, next_position: DomSlot) { + // TODO: is not defensive against accidental reference loops + *self.target.borrow_mut() = next_position; + } + + /// Get a [`DomSlot`] that gets automatically updated when `self` gets reassigned. All such + /// slots are equivalent to each other and point to the same position. + pub fn to_position(&self) -> DomSlot { + DomSlot { + variant: DomSlotVariant::Chained(self.clone()), + } + } + + fn with_next_sibling(&self, f: impl FnOnce(Option<&Node>) -> R) -> R { + // we use an iterative approach to traverse a possible long chain for references + // see for example issue #3043 why a recursive call is impossible for large lists in vdom + + // TODO: there could be some data structure that performs better here. E.g. a balanced tree + // with parent pointers come to mind, but they are a bit fiddly to implement in rust + let mut this = self.target.clone(); + loop { + // v------- borrow lives for this match expression + let next_this = match &this.borrow().variant { + DomSlotVariant::Node(ref n) => break f(n.as_ref()), + // We clone an Rc here temporarily, so that we don't have to consume stack + // space. The alternative would be to keep the + // `Ref<'_, DomSlot>` above in some temporary buffer + DomSlotVariant::Chained(ref chain) => chain.target.clone(), + }; + this = next_this; + } + } +} + +#[cfg(target_arch = "wasm32")] +#[cfg(test)] +mod layout_tests { + use gloo::utils::document; + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + use super::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn new_at_and_get() { + let node = document().create_element("p").unwrap(); + let position = DomSlot::at(node.clone().into()); + assert_eq!( + position.get().unwrap(), + node.clone().into(), + "expected the DomSlot to be at {node:#?}" + ); + } + + #[test] + fn new_at_end_and_get() { + let position = DomSlot::at_end(); + assert!( + position.get().is_none(), + "expected the DomSlot to not have a next sibling" + ); + } + + #[test] + fn get_through_dynamic() { + let original = DomSlot::at(document().create_element("p").unwrap().into()); + let target = DynamicDomSlot::new(original.clone()); + assert_eq!( + target.to_position().get(), + original.get(), + "expected {target:#?} to point to the same position as {original:#?}" + ); + } + + #[test] + fn get_after_reassign() { + let target = DynamicDomSlot::new(DomSlot::at_end()); + let target_pos = target.to_position(); + // We reassign *after* we called `to_position` here to be strict in the test + let replacement = DomSlot::at(document().create_element("p").unwrap().into()); + target.reassign(replacement.clone()); + assert_eq!( + target_pos.get(), + replacement.get(), + "expected {target:#?} to point to the same position as {replacement:#?}" + ); + } + + #[test] + fn get_chain_after_reassign() { + let middleman = DynamicDomSlot::new(DomSlot::at_end()); + let target = DynamicDomSlot::new(middleman.to_position()); + let target_pos = target.to_position(); + assert!( + target.to_position().get().is_none(), + "should not yet point to a node" + ); + // Now reassign the middle man, but get the node from `target` + let replacement = DomSlot::at(document().create_element("p").unwrap().into()); + middleman.reassign(replacement.clone()); + assert_eq!( + target_pos.get(), + replacement.get(), + "expected {target:#?} to point to the same position as {replacement:#?}" + ); + } +} diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs index c717123e683..0a9f4954bae 100644 --- a/packages/yew/src/dom_bundle/subtree_root.rs +++ b/packages/yew/src/dom_bundle/subtree_root.rs @@ -1,15 +1,17 @@ //! Per-subtree state of apps +use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::rc::{Rc, Weak}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; -use wasm_bindgen::prelude::wasm_bindgen; -use wasm_bindgen::JsCast; -use web_sys::{Element, Event, EventTarget as HtmlEventTarget, ShadowRoot}; +use wasm_bindgen::prelude::{wasm_bindgen, Closure}; +use wasm_bindgen::{intern, JsCast, UnwrapThrowExt}; +use web_sys::{ + AddEventListenerOptions, Element, Event, EventTarget as HtmlEventTarget, ShadowRoot, +}; use super::{test_log, Registry}; use crate::virtual_dom::{Listener, ListenerKind}; @@ -113,6 +115,70 @@ impl From<&dyn Listener> for EventDescriptor { } } +// FIXME: this is a reproduction of gloo's EventListener to work around #2989 +// change back to gloo's implementation once it has been decided how to fix this upstream +// The important part is that we use `Fn` instead of `FnMut` below! +type EventClosure = Closure; +#[derive(Debug)] +#[must_use = "event listener will never be called after being dropped"] +struct EventListener { + target: HtmlEventTarget, + event_type: Cow<'static, str>, + callback: Option, +} + +impl Drop for EventListener { + #[inline] + fn drop(&mut self) { + if let Some(ref callback) = self.callback { + self.target + .remove_event_listener_with_callback_and_bool( + &self.event_type, + callback.as_ref().unchecked_ref(), + true, // Always capture + ) + .unwrap_throw(); + } + } +} + +impl EventListener { + fn new( + target: &HtmlEventTarget, + desc: &EventDescriptor, + callback: impl 'static + Fn(&Event), + ) -> Self { + let event_type = desc.kind.type_name(); + + let callback = Closure::wrap(Box::new(callback) as Box); + // defaults: { once: false } + let mut options = AddEventListenerOptions::new(); + options.capture(true).passive(desc.passive); + + target + .add_event_listener_with_callback_and_add_event_listener_options( + intern(&event_type), + callback.as_ref().unchecked_ref(), + &options, + ) + .unwrap_throw(); + + EventListener { + target: target.clone(), + event_type, + callback: Some(callback), + } + } + + #[cfg(not(test))] + fn forget(mut self) { + if let Some(callback) = self.callback.take() { + // Should always match, but no need to introduce a panic path here + callback.forget(); + } + } +} + /// Ensures event handler registration. // Separate struct to DRY, while avoiding partial struct mutability. #[derive(Debug)] @@ -135,15 +201,8 @@ impl HostHandlers { } } - fn add_listener(&mut self, desc: &EventDescriptor, callback: impl 'static + FnMut(&Event)) { - let cl = { - let desc = desc.clone(); - let options = EventListenerOptions { - phase: EventListenerPhase::Capture, - passive: desc.passive, - }; - EventListener::new_with_options(&self.host, desc.kind.type_name(), options, callback) - }; + fn add_listener(&mut self, desc: &EventDescriptor, callback: impl 'static + Fn(&Event)) { + let cl = EventListener::new(&self.host, desc, callback); // Never drop the closure as this event handler is static #[cfg(not(test))] @@ -232,7 +291,7 @@ static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); /// handler has no effect. /// /// This function should be called before any component is mounted. -#[cfg_attr(documenting, doc(cfg(feature = "csr")))] +#[cfg(feature = "csr")] pub fn set_event_bubbling(bubble: bool) { BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); } @@ -370,7 +429,7 @@ impl SubtreeData { // We're tasked with finding the subtree that is reponsible with handling the event, and/or // run the handling if that's `self`. let target = event_path.get(0).dyn_into::().ok()?; - let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed); + let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed) && event.bubbles(); // We say that the most deeply nested subtree is "responsible" for handling the event. let (responsible_tree_id, bubbling_start) = if let Some(branding) = cached_branding { (branding, target.clone()) @@ -484,7 +543,7 @@ impl BSubtree { /// Run f with access to global Registry #[inline] pub fn with_listener_registry(&self, f: impl FnOnce(&mut Registry) -> R) -> R { - f(&mut *self.0.event_registry().borrow_mut()) + f(&mut self.0.event_registry().borrow_mut()) } pub fn brand_element(&self, el: &dyn EventGrating) { diff --git a/packages/yew/src/dom_bundle/traits.rs b/packages/yew/src/dom_bundle/traits.rs index 10712be2c04..16a423d7c23 100644 --- a/packages/yew/src/dom_bundle/traits.rs +++ b/packages/yew/src/dom_bundle/traits.rs @@ -1,7 +1,7 @@ use web_sys::Element; -use super::{BNode, BSubtree}; -use crate::html::{AnyScope, NodeRef}; +use super::{BNode, BSubtree, DomSlot}; +use crate::html::AnyScope; /// A Reconcile Target. /// @@ -16,7 +16,7 @@ pub(super) trait ReconcileTarget { /// Move elements from one parent to another parent. /// This is for example used by `VSuspense` to preserve component state without detaching /// (which destroys component state). - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef; + fn shift(&self, next_parent: &Element, slot: DomSlot) -> DomSlot; } /// This trait provides features to update a tree by calculating a difference against another tree. @@ -29,17 +29,19 @@ pub(super) trait Reconcilable { /// - `root`: bundle of the subtree root /// - `parent_scope`: the parent `Scope` used for passing messages to the parent `Component`. /// - `parent`: the parent node in the DOM. - /// - `next_sibling`: to find where to put the node. + /// - `slot`: to find where to put the node. /// /// Returns a reference to the newly inserted element. + /// The [`DomSlot`] points the first element (if there are multiple nodes created), + /// or is the passed in `slot` if there are no element is created. fn attach( self, root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle); + slot: DomSlot, + ) -> (DomSlot, Self::Bundle); /// Scoped diff apply to other tree. /// @@ -50,7 +52,7 @@ pub(super) trait Reconcilable { /// Parameters: /// - `parent_scope`: the parent `Scope` used for passing messages to the parent `Component`. /// - `parent`: the parent node in the DOM. - /// - `next_sibling`: the next sibling, used to efficiently find where to put the node. + /// - `slot`: the slot in `parent`'s children where to put the node. /// - `bundle`: the node that this node will be replacing in the DOM. This method will remove /// the `bundle` from the `parent` if it is of the wrong kind, and otherwise reuse it. /// @@ -61,18 +63,18 @@ pub(super) trait Reconcilable { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut BNode, - ) -> NodeRef; + ) -> DomSlot; fn reconcile( self, root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut Self::Bundle, - ) -> NodeRef; + ) -> DomSlot; /// Replace an existing bundle by attaching self and detaching the existing one fn replace( @@ -81,14 +83,14 @@ pub(super) trait Reconcilable { root: &BSubtree, parent_scope: &AnyScope, parent: &Element, - next_sibling: NodeRef, + slot: DomSlot, bundle: &mut BNode, - ) -> NodeRef + ) -> DomSlot where Self: Sized, Self::Bundle: Into, { - let (self_ref, self_) = self.attach(root, parent_scope, parent, next_sibling); + let (self_ref, self_) = self.attach(root, parent_scope, parent, slot); let ancestor = std::mem::replace(bundle, self_.into()); ancestor.detach(root, parent, false); self_ref @@ -114,7 +116,7 @@ mod feat_hydration { parent_scope: &AnyScope, parent: &Element, fragment: &mut Fragment, - ) -> (NodeRef, Self::Bundle); + ) -> Self::Bundle; } } diff --git a/packages/yew/src/dom_bundle/utils.rs b/packages/yew/src/dom_bundle/utils.rs index f4973dad37a..b877da9002d 100644 --- a/packages/yew/src/dom_bundle/utils.rs +++ b/packages/yew/src/dom_bundle/utils.rs @@ -1,18 +1,3 @@ -use web_sys::{Element, Node}; - -/// Insert a concrete [Node] into the DOM -pub(super) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { - match next_sibling { - Some(next_sibling) => parent - .insert_before(node, Some(next_sibling)) - .unwrap_or_else(|err| { - gloo::console::error!("failed to insert node", err, parent, next_sibling, node); - panic!("failed to insert tag before next sibling") - }), - None => parent.append_child(node).expect("failed to append child"), - }; -} - #[cfg(all(test, target_arch = "wasm32", verbose_tests))] macro_rules! test_log { ($fmt:literal, $($arg:expr),* $(,)?) => { @@ -35,9 +20,7 @@ mod feat_hydration { use std::borrow::Cow; use wasm_bindgen::JsCast; - use web_sys::Element; - - use super::*; + use web_sys::{Element, Node}; pub(in crate::dom_bundle) fn node_type_str(node: &Node) -> Cow<'static, str> { match node.node_type() { @@ -47,7 +30,7 @@ mod feat_hydration { .map(|m| m.tag_name().to_lowercase()) .unwrap_or_else(|| "unknown".to_owned()); - format!("{} element node", tag).into() + format!("{tag} element node").into() } Node::ATTRIBUTE_NODE => "attribute node".into(), Node::TEXT_NODE => "text node".into(), @@ -67,3 +50,45 @@ mod feat_hydration { #[cfg(feature = "hydration")] pub(super) use feat_hydration::*; + +#[cfg(test)] +mod tests { + #![allow(dead_code)] + + use gloo::utils::document; + use web_sys::Element; + + use crate::dom_bundle::{BSubtree, DomSlot}; + use crate::html::AnyScope; + + pub fn setup_parent() -> (BSubtree, AnyScope, Element) { + let scope = AnyScope::test(); + let parent = document().create_element("div").unwrap(); + let root = BSubtree::create_root(&parent); + + document().body().unwrap().append_child(&parent).unwrap(); + + (root, scope, parent) + } + + pub const SIBLING_CONTENT: &str = "END"; + + pub(crate) fn setup_parent_and_sibling() -> (BSubtree, AnyScope, Element, DomSlot) { + let scope = AnyScope::test(); + let parent = document().create_element("div").unwrap(); + let root = BSubtree::create_root(&parent); + + document().body().unwrap().append_child(&parent).unwrap(); + + let end = document().create_text_node(SIBLING_CONTENT); + parent.append_child(&end).unwrap(); + let sibling = DomSlot::at(end.into()); + + (root, scope, parent, sibling) + } +} + +#[cfg(test)] +// this is needed because clippy doesn't like the import not being used +#[allow(unused_imports)] +pub(super) use tests::*; diff --git a/packages/yew/src/functional/hooks/use_callback.rs b/packages/yew/src/functional/hooks/use_callback.rs index af74cf0cec2..3906d5df290 100644 --- a/packages/yew/src/functional/hooks/use_callback.rs +++ b/packages/yew/src/functional/hooks/use_callback.rs @@ -38,19 +38,13 @@ use crate::functional::{hook, use_memo}; /// }; /// /// // This callback depends on (), so it's created only once, then MyComponent -/// // will be rendered only once even when you click the button mutiple times. -/// let callback = use_callback(move |name, _| format!("Hello, {}!", name), ()); +/// // will be rendered only once even when you click the button multiple times. +/// let callback = use_callback((), move |name, _| format!("Hello, {}!", name)); /// /// // It can also be used for events, this callback depends on `counter`. -/// let oncallback = { -/// let counter = counter.clone(); -/// use_callback( -/// move |_e, counter| { -/// let _ = **counter; -/// }, -/// counter, -/// ) -/// }; +/// let oncallback = use_callback(counter.clone(), move |_e, counter| { +/// let _ = **counter; +/// }); /// /// html! { ///
    @@ -66,7 +60,7 @@ use crate::functional::{hook, use_memo}; /// } /// ``` #[hook] -pub fn use_callback(f: F, deps: D) -> Callback +pub fn use_callback(deps: D, f: F) -> Callback where IN: 'static, OUT: 'static, @@ -75,13 +69,10 @@ where { let deps = Rc::new(deps); - (*use_memo( - move |deps| { - let deps = deps.clone(); - let f = move |value: IN| f(value, deps.as_ref()); - Callback::from(f) - }, - deps, - )) + (*use_memo(deps, move |deps| { + let deps = deps.clone(); + let f = move |value: IN| f(value, deps.as_ref()); + Callback::from(f) + })) .clone() } diff --git a/packages/yew/src/functional/hooks/use_effect.rs b/packages/yew/src/functional/hooks/use_effect.rs index 57105008c2a..92000418f11 100644 --- a/packages/yew/src/functional/hooks/use_effect.rs +++ b/packages/yew/src/functional/hooks/use_effect.rs @@ -2,11 +2,27 @@ use std::cell::RefCell; use crate::functional::{hook, Effect, Hook, HookContext}; +/// Trait describing the destructor of [`use_effect`] hook. +pub trait TearDown: Sized + 'static { + /// The function that is executed when destructor is called + fn tear_down(self); +} + +impl TearDown for () { + fn tear_down(self) {} +} + +impl TearDown for F { + fn tear_down(self) { + self() + } +} + struct UseEffectBase where F: FnOnce(&T) -> D + 'static, T: 'static, - D: FnOnce() + 'static, + D: TearDown, { runner_with_deps: Option<(T, F)>, destructor: Option, @@ -18,7 +34,7 @@ impl Effect for RefCell> where F: FnOnce(&T) -> D + 'static, T: 'static, - D: FnOnce() + 'static, + D: TearDown, { fn rendered(&self) { let mut this = self.borrow_mut(); @@ -29,7 +45,7 @@ where } if let Some(de) = this.destructor.take() { - de(); + de.tear_down(); } let new_destructor = runner(&deps); @@ -44,11 +60,11 @@ impl Drop for UseEffectBase where F: FnOnce(&T) -> D + 'static, T: 'static, - D: FnOnce() + 'static, + D: TearDown, { fn drop(&mut self) { if let Some(destructor) = self.destructor.take() { - destructor() + destructor.tear_down() } } } @@ -60,13 +76,13 @@ fn use_effect_base( ) -> impl Hook where T: 'static, - D: FnOnce() + 'static, + D: TearDown, { struct HookProvider where F: FnOnce(&T) -> D + 'static, T: 'static, - D: FnOnce() + 'static, + D: TearDown, { runner: F, deps: T, @@ -77,7 +93,7 @@ where where F: FnOnce(&T) -> D + 'static, T: 'static, - D: FnOnce() + 'static, + D: TearDown, { type Output = (); @@ -125,10 +141,10 @@ where /// let counter_one = counter.clone(); /// use_effect(move || { /// // Make a call to DOM API after component is rendered -/// gloo_utils::document().set_title(&format!("You clicked {} times", *counter_one)); +/// gloo::utils::document().set_title(&format!("You clicked {} times", *counter_one)); /// /// // Perform the cleanup -/// || gloo_utils::document().set_title(&format!("You clicked 0 times")) +/// || gloo::utils::document().set_title(&format!("You clicked 0 times")) /// }); /// /// let onclick = { @@ -141,11 +157,20 @@ where /// } /// } /// ``` +/// +/// # Destructor +/// +/// Any type implementing [`TearDown`] can be used as destructor, which is called when the component +/// is re-rendered +/// +/// ## Tip +/// +/// The callback can return [`()`] if there is no destructor to run. #[hook] pub fn use_effect(f: F) where F: FnOnce() -> D + 'static, - D: FnOnce() + 'static, + D: TearDown, { use_effect_base(|_| f(), (), |_, _| true); } @@ -153,7 +178,7 @@ where /// This hook is similar to [`use_effect`] but it accepts dependencies. /// /// Whenever the dependencies are changed, the effect callback is called again. -/// To detect changes, dependencies must implement `PartialEq`. +/// To detect changes, dependencies must implement [`PartialEq`]. /// /// # Note /// The destructor also runs when dependencies change. @@ -161,7 +186,7 @@ where /// # Example /// /// ```rust -/// use yew::{function_component, html, use_effect_with_deps, Html, Properties}; +/// use yew::{function_component, html, use_effect_with, Html, Properties}; /// # use gloo::console::log; /// /// #[derive(Properties, PartialEq)] @@ -173,15 +198,13 @@ where /// fn HelloWorld(props: &Props) -> Html { /// let is_loading = props.is_loading.clone(); /// -/// use_effect_with_deps( -/// move |_| { -/// log!(" Is loading prop changed!"); -/// || () -/// }, -/// is_loading, -/// ); +/// use_effect_with(is_loading, move |_| { +/// log!(" Is loading prop changed!"); +/// }); /// -/// html! { <>{"Am I loading? - "}{is_loading} } +/// html! { +/// <>{"Am I loading? - "}{is_loading} +/// } /// } /// ``` /// @@ -193,18 +216,14 @@ where /// render of a component. /// /// ```rust -/// use yew::{function_component, html, use_effect_with_deps, Html}; +/// use yew::{function_component, html, use_effect_with, Html}; /// # use gloo::console::log; /// /// #[function_component] /// fn HelloWorld() -> Html { -/// use_effect_with_deps( -/// move |_| { -/// log!("I got rendered, yay!"); -/// || () -/// }, -/// (), -/// ); +/// use_effect_with((), move |_| { +/// log!("I got rendered, yay!"); +/// }); /// /// html! { "Hello" } /// } @@ -216,28 +235,31 @@ where /// It will only get called when the component is removed from view / gets destroyed. /// /// ```rust -/// use yew::{function_component, html, use_effect_with_deps, Html}; +/// use yew::{function_component, html, use_effect_with, Html}; /// # use gloo::console::log; /// /// #[function_component] /// fn HelloWorld() -> Html { -/// use_effect_with_deps( -/// move |_| { -/// || { -/// log!("Noo dont kill me, ahhh!"); -/// } -/// }, -/// (), -/// ); +/// use_effect_with((), move |_| { +/// || { +/// log!("Noo dont kill me, ahhh!"); +/// } +/// }); +/// /// html! { "Hello" } /// } /// ``` -#[hook] -pub fn use_effect_with_deps(f: F, deps: T) +/// +/// Any type implementing [`TearDown`] can be used as destructor +/// +/// ### Tip +/// +/// The callback can return [`()`] if there is no destructor to run. +pub fn use_effect_with(deps: T, f: F) -> impl Hook where T: PartialEq + 'static, F: FnOnce(&T) -> D + 'static, - D: FnOnce() + 'static, + D: TearDown, { use_effect_base(f, deps, |lhs, rhs| lhs != rhs) } diff --git a/packages/yew/src/functional/hooks/use_force_update.rs b/packages/yew/src/functional/hooks/use_force_update.rs index 417273a015b..d3e1c485623 100644 --- a/packages/yew/src/functional/hooks/use_force_update.rs +++ b/packages/yew/src/functional/hooks/use_force_update.rs @@ -23,7 +23,7 @@ impl UseForceUpdateHandle { } } -#[cfg(feature = "nightly")] +#[cfg(nightly_yew)] mod feat_nightly { use super::*; @@ -112,7 +112,7 @@ pub fn use_force_update() -> impl Hook { UseRerenderHook } -#[cfg(all(test, feature = "nightly"))] +#[cfg(all(test, nightly_yew))] mod nightly_test { use yew::prelude::*; diff --git a/packages/yew/src/functional/hooks/use_memo.rs b/packages/yew/src/functional/hooks/use_memo.rs index a40e0a13cab..e1fd7f27aa3 100644 --- a/packages/yew/src/functional/hooks/use_memo.rs +++ b/packages/yew/src/functional/hooks/use_memo.rs @@ -59,10 +59,9 @@ where /// #[function_component(UseMemo)] /// fn memo(props: &Props) -> Html { /// // Will only get recalculated if `props.step` value changes -/// let message = use_memo( -/// |step| format!("{}. Do Some Expensive Calculation", step), -/// props.step, -/// ); +/// let message = use_memo(props.step, |step| { +/// format!("{}. Do Some Expensive Calculation", step) +/// }); /// /// html! { ///
    @@ -72,7 +71,7 @@ where /// } /// ``` #[hook] -pub fn use_memo(f: F, deps: D) -> Rc +pub fn use_memo(deps: D, f: F) -> Rc where T: 'static, F: FnOnce(&D) -> T, diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs index c6e6fde3ccf..6ca568b2cf2 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_hydration.rs @@ -14,7 +14,7 @@ use crate::suspense::{Suspension, SuspensionResult}; #[cfg(target_arch = "wasm32")] async fn decode_base64(s: &str) -> Result, JsValue> { - use gloo_utils::window; + use gloo::utils::window; use js_sys::Uint8Array; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; @@ -75,7 +75,7 @@ where let data = data.clone(); ctx.next_prepared_state(move |_re_render, buf| -> PreparedStateBase { if let Some(buf) = buf { - let buf = format!("data:application/octet-binary;base64,{}", buf); + let buf = format!("data:application/octet-binary;base64,{buf}"); spawn_local(async move { let buf = decode_base64(&buf) diff --git a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs index 4297cea931d..1720bbd1a44 100644 --- a/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs +++ b/packages/yew/src/functional/hooks/use_prepared_state/feat_ssr.rs @@ -46,7 +46,7 @@ where let state = { let deps = deps.clone(); - use_memo(move |_| f(deps), ()).run(ctx) + use_memo((), move |_| f(deps)).run(ctx) }; let state = PreparedStateBase { diff --git a/packages/yew/src/functional/hooks/use_reducer.rs b/packages/yew/src/functional/hooks/use_reducer.rs index 15ae1af68d7..7b01dce76cd 100644 --- a/packages/yew/src/functional/hooks/use_reducer.rs +++ b/packages/yew/src/functional/hooks/use_reducer.rs @@ -59,7 +59,7 @@ where type Target = T; fn deref(&self) -> &Self::Target { - &*self.value + &self.value } } @@ -297,7 +297,7 @@ where /// /// The dispatch function is guaranteed to be the same across the entire /// component lifecycle. You can safely omit the `UseReducerHandle` from the -/// dependents of `use_effect_with_deps` if you only intend to dispatch +/// dependents of `use_effect_with` if you only intend to dispatch /// values from within the hooks. /// /// # Caution @@ -305,7 +305,7 @@ where /// The value held in the handle will reflect the value of at the time the /// handle is returned by the `use_reducer`. It is possible that the handle does /// not dereference to an up to date value if you are moving it into a -/// `use_effect_with_deps` hook. You can register the +/// `use_effect_with` hook. You can register the /// state to the dependents so the hook can be updated when the value changes. #[hook] pub fn use_reducer(init_fn: F) -> UseReducerHandle diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index 0c5f41c28ba..e03db8a2130 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -38,7 +38,7 @@ impl T> Hook for UseMutRef { /// let message_count = use_mut_ref(|| 0); /// /// let onclick = Callback::from(move |e| { -/// let window = gloo_utils::window(); +/// let window = gloo::utils::window(); /// /// if *message_count.borrow_mut() > 3 { /// window.alert_with_message("Message limit reached"); @@ -83,7 +83,7 @@ where /// use wasm_bindgen::prelude::Closure; /// use wasm_bindgen::JsCast; /// use web_sys::{Event, HtmlElement}; -/// use yew::{function_component, html, use_effect_with_deps, use_node_ref, Html}; +/// use yew::{function_component, html, use_effect_with, use_node_ref, Html}; /// /// #[function_component(UseNodeRef)] /// pub fn node_ref_hook() -> Html { @@ -92,32 +92,26 @@ where /// { /// let div_ref = div_ref.clone(); /// -/// use_effect_with_deps( -/// |div_ref| { -/// let div = div_ref -/// .cast::() -/// .expect("div_ref not attached to div element"); +/// use_effect_with(div_ref, |div_ref| { +/// let div = div_ref +/// .cast::() +/// .expect("div_ref not attached to div element"); /// -/// let listener = Closure::::wrap(Box::new(|_| { -/// web_sys::console::log_1(&"Clicked!".into()); -/// })); +/// let listener = Closure::::wrap(Box::new(|_| { +/// web_sys::console::log_1(&"Clicked!".into()); +/// })); /// -/// div.add_event_listener_with_callback( +/// div.add_event_listener_with_callback("click", listener.as_ref().unchecked_ref()) +/// .unwrap(); +/// +/// move || { +/// div.remove_event_listener_with_callback( /// "click", /// listener.as_ref().unchecked_ref(), /// ) /// .unwrap(); -/// -/// move || { -/// div.remove_event_listener_with_callback( -/// "click", -/// listener.as_ref().unchecked_ref(), -/// ) -/// .unwrap(); -/// } -/// }, -/// div_ref, -/// ); +/// } +/// }); /// } /// /// html! { @@ -131,7 +125,7 @@ where /// # Tip /// /// When conditionally rendering elements you can use `NodeRef` in conjunction with -/// `use_effect_with_deps` to perform actions each time an element is rendered and just before the +/// `use_effect_with` to perform actions each time an element is rendered and just before the /// component where the hook is used in is going to be removed from the DOM. #[hook] pub fn use_node_ref() -> NodeRef { diff --git a/packages/yew/src/functional/hooks/use_state.rs b/packages/yew/src/functional/hooks/use_state.rs index 489e1d1d292..dc68aedc793 100644 --- a/packages/yew/src/functional/hooks/use_state.rs +++ b/packages/yew/src/functional/hooks/use_state.rs @@ -63,14 +63,14 @@ where /// The value held in the handle will reflect the value of at the time the /// handle is returned by the `use_reducer`. It is possible that the handle does /// not dereference to an up to date value if you are moving it into a -/// `use_effect_with_deps` hook. You can register the +/// `use_effect_with` hook. You can register the /// state to the dependents so the hook can be updated when the value changes. /// /// # Tip /// /// The setter function is guaranteed to be the same across the entire /// component lifecycle. You can safely omit the `UseStateHandle` from the -/// dependents of `use_effect_with_deps` if you only intend to set +/// dependents of `use_effect_with` if you only intend to set /// values from within the hook. #[hook] pub fn use_state(init_fn: F) -> UseStateHandle diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 82b6ba1ac09..6ba9f601f91 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -27,7 +27,6 @@ use std::rc::Rc; use wasm_bindgen::prelude::*; -#[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(all(feature = "hydration", feature = "ssr"))] use crate::html::RenderMode; use crate::html::{AnyScope, BaseComponent, Context, HtmlResult}; @@ -68,7 +67,6 @@ pub use yew_macro::hook; type ReRender = Rc; /// Primitives of a prepared state hook. -#[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(any(feature = "hydration", feature = "ssr"))] pub(crate) trait PreparedState { #[cfg(feature = "ssr")] @@ -83,7 +81,6 @@ pub(crate) trait Effect { /// A hook context to be passed to hooks. pub struct HookContext { pub(crate) scope: AnyScope, - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(all(feature = "hydration", feature = "ssr"))] creation_mode: RenderMode, re_render: ReRender, @@ -91,14 +88,11 @@ pub struct HookContext { states: Vec>, effects: Vec>, - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(any(feature = "hydration", feature = "ssr"))] prepared_states: Vec>, - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "hydration")] prepared_states_data: Vec>, - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "hydration")] prepared_state_counter: usize, @@ -111,29 +105,22 @@ impl HookContext { fn new( scope: AnyScope, re_render: ReRender, - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] - #[cfg(all(feature = "hydration", feature = "ssr"))] - creation_mode: RenderMode, - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] - #[cfg(feature = "hydration")] - prepared_state: Option<&str>, + #[cfg(all(feature = "hydration", feature = "ssr"))] creation_mode: RenderMode, + #[cfg(feature = "hydration")] prepared_state: Option<&str>, ) -> RefCell { RefCell::new(HookContext { scope, re_render, - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(all(feature = "hydration", feature = "ssr"))] creation_mode, states: Vec::new(), - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(any(feature = "hydration", feature = "ssr"))] prepared_states: Vec::new(), effects: Vec::new(), - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "hydration")] prepared_states_data: { match prepared_state { @@ -141,7 +128,6 @@ impl HookContext { None => Vec::new(), } }, - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "hydration")] prepared_state_counter: 0, @@ -187,7 +173,6 @@ impl HookContext { t } - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(any(feature = "hydration", feature = "ssr"))] pub(crate) fn next_prepared_state( &mut self, @@ -220,7 +205,6 @@ impl HookContext { #[inline(always)] fn prepare_run(&mut self) { - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "hydration")] { self.prepared_state_counter = 0; @@ -277,15 +261,11 @@ impl HookContext { } } - #[cfg(any( - not(feature = "ssr"), - not(any(target_arch = "wasm32", feature = "tokio")) - ))] + #[cfg(not(feature = "ssr"))] fn prepare_state(&self) -> Option { None } - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "ssr")] fn prepare_state(&self) -> Option { if self.prepared_states.is_empty() { @@ -357,14 +337,12 @@ where }; Self { - _never: std::marker::PhantomData::default(), + _never: std::marker::PhantomData, hook_ctx: HookContext::new( scope, re_render, - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(all(feature = "hydration", feature = "ssr"))] ctx.creation_mode(), - #[cfg(any(target_arch = "wasm32", feature = "tokio"))] #[cfg(feature = "hydration")] ctx.prepared_state(), ), @@ -378,7 +356,7 @@ where hook_ctx.prepare_run(); #[allow(clippy::let_and_return)] - let result = T::run(&mut *hook_ctx, props); + let result = T::run(&mut hook_ctx, props); #[cfg(debug_assertions)] hook_ctx.assert_hook_context(result.is_ok()); diff --git a/packages/yew/src/html/classes.rs b/packages/yew/src/html/classes.rs index 5311ca6e163..f7217278427 100644 --- a/packages/yew/src/html/classes.rs +++ b/packages/yew/src/html/classes.rs @@ -1,34 +1,61 @@ -use std::borrow::{Borrow, Cow}; -use std::hint::unreachable_unchecked; +use std::borrow::Cow; use std::iter::FromIterator; use std::rc::Rc; +use implicit_clone::ImplicitClone; use indexmap::IndexSet; use super::IntoPropValue; use crate::virtual_dom::AttrValue; -/// A set of classes. +/// A set of classes, cheap to clone. /// /// The preferred way of creating this is using the [`classes!`][yew::classes!] macro. #[derive(Debug, Clone, Default)] pub struct Classes { - set: IndexSet>, + set: Rc>, +} + +impl ImplicitClone for Classes {} + +/// helper method to efficiently turn a set of classes into a space-separated +/// string. Abstracts differences between ToString and IntoPropValue. The +/// `rest` iterator is cloned to pre-compute the length of the String; it +/// should be cheap to clone. +fn build_attr_value(first: AttrValue, rest: impl Iterator + Clone) -> AttrValue { + // The length of the string is known to be the length of all the + // components, plus one space for each element in `rest`. + let mut s = String::with_capacity( + rest.clone() + .map(|class| class.len()) + .chain([first.len(), rest.size_hint().0]) + .sum(), + ); + + s.push_str(first.as_str()); + // NOTE: this can be improved once Iterator::intersperse() becomes stable + for class in rest { + s.push(' '); + s.push_str(class.as_str()); + } + s.into() } impl Classes { /// Creates an empty set of classes. (Does not allocate.) + #[inline] pub fn new() -> Self { Self { - set: IndexSet::new(), + set: Rc::new(IndexSet::new()), } } /// Creates an empty set of classes with capacity for n elements. (Does not allocate if n is /// zero.) + #[inline] pub fn with_capacity(n: usize) -> Self { Self { - set: IndexSet::with_capacity(n), + set: Rc::new(IndexSet::with_capacity(n)), } } @@ -37,7 +64,11 @@ impl Classes { /// If the provided class has already been added, this method will ignore it. pub fn push>(&mut self, class: T) { let classes_to_add: Self = class.into(); - self.set.extend(classes_to_add.set); + if self.is_empty() { + *self = classes_to_add + } else { + Rc::make_mut(&mut self.set).extend(classes_to_add.set.iter().cloned()) + } } /// Adds a class to a set. @@ -49,18 +80,20 @@ impl Classes { /// # Safety /// /// This function will not split the string into multiple classes. Please do not use it unless - /// you are absolutely certain that the string does not contain any whitespace. Using `push()` - /// is preferred. - pub unsafe fn unchecked_push>>(&mut self, class: T) { - self.set.insert(class.into()); + /// you are absolutely certain that the string does not contain any whitespace and it is not + /// empty. Using `push()` is preferred. + pub unsafe fn unchecked_push>(&mut self, class: T) { + Rc::make_mut(&mut self.set).insert(class.into()); } /// Check the set contains a class. + #[inline] pub fn contains>(&self, class: T) -> bool { self.set.contains(class.as_ref()) } /// Check the set is empty. + #[inline] pub fn is_empty(&self) -> bool { self.set.is_empty() } @@ -68,15 +101,13 @@ impl Classes { impl IntoPropValue for Classes { #[inline] - fn into_prop_value(mut self) -> AttrValue { - if self.set.len() == 1 { - match self.set.pop() { - Some(attr) => AttrValue::Rc(Rc::from(attr)), - // SAFETY: the collection is checked to be non-empty above - None => unsafe { unreachable_unchecked() }, - } - } else { - AttrValue::Rc(Rc::from(self.to_string())) + fn into_prop_value(self) -> AttrValue { + let mut classes = self.set.iter().cloned(); + + match classes.next() { + None => AttrValue::Static(""), + Some(class) if classes.len() == 0 => class, + Some(first) => build_attr_value(first, classes), } } } @@ -93,6 +124,7 @@ impl IntoPropValue> for Classes { } impl IntoPropValue for &'static str { + #[inline] fn into_prop_value(self) -> Classes { self.into() } @@ -100,11 +132,7 @@ impl IntoPropValue for &'static str { impl> Extend for Classes { fn extend>(&mut self, iter: I) { - let classes = iter - .into_iter() - .map(Into::into) - .flat_map(|classes| classes.set); - self.set.extend(classes); + iter.into_iter().for_each(|classes| self.push(classes)) } } @@ -117,21 +145,36 @@ impl> FromIterator for Classes { } impl IntoIterator for Classes { - type IntoIter = indexmap::set::IntoIter>; - type Item = Cow<'static, str>; + type IntoIter = indexmap::set::IntoIter; + type Item = AttrValue; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + // NOTE: replace this by Rc::unwrap_or_clone() when it becomes stable + Rc::try_unwrap(self.set) + .unwrap_or_else(|rc| (*rc).clone()) + .into_iter() + } +} +impl IntoIterator for &Classes { + type IntoIter = indexmap::set::IntoIter; + type Item = AttrValue; + + #[inline] fn into_iter(self) -> Self::IntoIter { - self.set.into_iter() + (*self.set).clone().into_iter() } } impl ToString for Classes { fn to_string(&self) -> String { - self.set - .iter() - .map(Borrow::borrow) - .collect::>() - .join(" ") + let mut iter = self.set.iter().cloned(); + + iter.next() + .map(|first| build_attr_value(first, iter)) + .unwrap_or_default() + .to_string() } } @@ -146,14 +189,25 @@ impl From> for Classes { impl From<&'static str> for Classes { fn from(t: &'static str) -> Self { - let set = t.split_whitespace().map(Cow::Borrowed).collect(); - Self { set } + let set = t.split_whitespace().map(AttrValue::Static).collect(); + Self { set: Rc::new(set) } } } impl From for Classes { fn from(t: String) -> Self { - Self::from(&t) + match t.contains(|c: char| c.is_whitespace()) { + // If the string only contains a single class, we can just use it + // directly (rather than cloning it into a new string). Need to make + // sure it's not empty, though. + false => match t.is_empty() { + true => Self::new(), + false => Self { + set: Rc::new(IndexSet::from_iter([AttrValue::from(t)])), + }, + }, + true => Self::from(&t), + } } } @@ -162,9 +216,37 @@ impl From<&String> for Classes { let set = t .split_whitespace() .map(ToOwned::to_owned) - .map(Cow::Owned) + .map(AttrValue::from) .collect(); - Self { set } + Self { set: Rc::new(set) } + } +} + +impl From<&AttrValue> for Classes { + fn from(t: &AttrValue) -> Self { + let set = t + .split_whitespace() + .map(ToOwned::to_owned) + .map(AttrValue::from) + .collect(); + Self { set: Rc::new(set) } + } +} + +impl From for Classes { + fn from(t: AttrValue) -> Self { + match t.contains(|c: char| c.is_whitespace()) { + // If the string only contains a single class, we can just use it + // directly (rather than cloning it into a new string). Need to make + // sure it's not empty, though. + false => match t.is_empty() { + true => Self::new(), + false => Self { + set: Rc::new(IndexSet::from_iter([t])), + }, + }, + true => Self::from(&t), + } } } @@ -198,6 +280,8 @@ impl PartialEq for Classes { } } +impl Eq for Classes {} + #[cfg(test)] mod tests { use super::*; @@ -273,6 +357,7 @@ mod tests { other.push("foo"); other.push("bar"); let mut subject = Classes::new(); + subject.extend(&other); subject.extend(other); assert!(subject.contains("foo")); assert!(subject.contains("bar")); @@ -285,4 +370,11 @@ mod tests { assert!(subject.contains("foo")); assert!(subject.contains("bar")); } + + #[test] + fn ignores_empty_string() { + let classes = String::from(""); + let subject = Classes::from(classes); + assert!(subject.is_empty()) + } } diff --git a/packages/yew/src/html/component/children.rs b/packages/yew/src/html/component/children.rs index efd2149b6d9..94f6dc0d2e8 100644 --- a/packages/yew/src/html/component/children.rs +++ b/packages/yew/src/html/component/children.rs @@ -3,8 +3,8 @@ use std::fmt; use crate::html::Html; -use crate::virtual_dom::{VChild, VNode}; -use crate::Properties; +use crate::virtual_dom::{VChild, VComp, VList, VNode}; +use crate::{BaseComponent, Properties}; /// A type used for accepting children elements in Component::Properties. /// @@ -163,7 +163,7 @@ impl PartialEq for ChildrenRenderer { impl ChildrenRenderer where - T: Clone + Into, + T: Clone, { /// Create children pub fn new(children: Vec) -> Self { @@ -186,6 +186,28 @@ where // This way `self.iter().next()` only has to clone a single node. self.children.iter().cloned() } + + /// Convert the children elements to another object (if there are any). + /// + /// ``` + /// # let children = Children::new(Vec::new()); + /// # use yew::{classes, html, Children}; + /// children.map(|children| { + /// html! { + ///
    + /// {children} + ///
    + /// } + /// }) + /// # ; + /// ``` + pub fn map(&self, closure: impl FnOnce(&Self) -> OUT) -> OUT { + if self.is_empty() { + Default::default() + } else { + closure(self) + } + } } impl Default for ChildrenRenderer { @@ -211,10 +233,61 @@ impl IntoIterator for ChildrenRenderer { } } +impl From> for Html { + fn from(mut val: ChildrenRenderer) -> Self { + if val.children.len() == 1 { + if let Some(m) = val.children.pop() { + return m; + } + } + + Html::VList(val.into()) + } +} + +impl From> for VList { + fn from(val: ChildrenRenderer) -> Self { + if val.is_empty() { + return VList::new(); + } + VList::with_children(val.children, None) + } +} + +impl From>> for ChildrenRenderer +where + COMP: BaseComponent, +{ + fn from(value: ChildrenRenderer>) -> Self { + Self::new( + value + .into_iter() + .map(VComp::from) + .map(VNode::from) + .collect(), + ) + } +} + /// A [Properties] type with Children being the only property. #[derive(Debug, Properties, PartialEq)] pub struct ChildrenProps { /// The Children of a Component. #[prop_or_default] - pub children: Children, + pub children: Html, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn children_map() { + let children = Children::new(vec![]); + let res = children.map(|children| Some(children.clone())); + assert!(res.is_none()); + let children = Children::new(vec![Default::default()]); + let res = children.map(|children| Some(children.clone())); + assert!(res.is_some()); + } } diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 84c49b7b917..ad5ee160965 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -11,9 +11,7 @@ use super::BaseComponent; #[cfg(feature = "hydration")] use crate::dom_bundle::Fragment; #[cfg(feature = "csr")] -use crate::dom_bundle::{BSubtree, Bundle}; -#[cfg(feature = "csr")] -use crate::html::NodeRef; +use crate::dom_bundle::{BSubtree, Bundle, DomSlot, DynamicDomSlot}; #[cfg(feature = "hydration")] use crate::html::RenderMode; use crate::html::{Html, RenderError}; @@ -27,18 +25,20 @@ pub(crate) enum ComponentRenderState { bundle: Bundle, root: BSubtree, parent: Element, - next_sibling: NodeRef, - internal_ref: NodeRef, + /// The dom position in front of the next sibling + sibling_slot: DynamicDomSlot, + /// The dom position in front of this component. Adjusted whenever this component + /// re-renders. + own_slot: DynamicDomSlot, }, #[cfg(feature = "hydration")] Hydration { fragment: Fragment, root: BSubtree, parent: Element, - next_sibling: NodeRef, - internal_ref: NodeRef, + sibling_slot: DynamicDomSlot, + own_slot: DynamicDomSlot, }, - #[cfg(feature = "ssr")] Ssr { sender: Option>, @@ -53,31 +53,31 @@ impl std::fmt::Debug for ComponentRenderState { ref bundle, root, ref parent, - ref next_sibling, - ref internal_ref, + ref sibling_slot, + ref own_slot, } => f .debug_struct("ComponentRenderState::Render") .field("bundle", bundle) .field("root", root) .field("parent", parent) - .field("next_sibling", next_sibling) - .field("internal_ref", internal_ref) + .field("sibling_slot", sibling_slot) + .field("own_slot", own_slot) .finish(), #[cfg(feature = "hydration")] Self::Hydration { ref fragment, ref parent, - ref next_sibling, - ref internal_ref, + ref sibling_slot, + ref own_slot, ref root, } => f .debug_struct("ComponentRenderState::Hydration") .field("fragment", fragment) .field("root", root) .field("parent", parent) - .field("next_sibling", next_sibling) - .field("internal_ref", internal_ref) + .field("sibling_slot", sibling_slot) + .field("own_slot", own_slot) .finish(), #[cfg(feature = "ssr")] @@ -97,31 +97,31 @@ impl std::fmt::Debug for ComponentRenderState { #[cfg(feature = "csr")] impl ComponentRenderState { - pub(crate) fn shift(&mut self, next_parent: Element, next_next_sibling: NodeRef) { + pub(crate) fn shift(&mut self, next_parent: Element, next_slot: DomSlot) { match self { #[cfg(feature = "csr")] Self::Render { bundle, parent, - next_sibling, + sibling_slot, .. } => { - bundle.shift(&next_parent, next_next_sibling.clone()); + bundle.shift(&next_parent, next_slot.clone()); *parent = next_parent; - next_sibling.link(next_next_sibling); + sibling_slot.reassign(next_slot); } #[cfg(feature = "hydration")] Self::Hydration { fragment, parent, - next_sibling, + sibling_slot, .. } => { - fragment.shift(&next_parent, next_next_sibling.clone()); + fragment.shift(&next_parent, next_slot.clone()); *parent = next_parent; - next_sibling.link(next_next_sibling); + sibling_slot.reassign(next_slot); } #[cfg(feature = "ssr")] @@ -206,8 +206,8 @@ where }; if self.context.props != props { - self.context.props = props; - self.component.changed(&self.context) + let old_props = std::mem::replace(&mut self.context.props, props); + self.component.changed(&self.context, &old_props) } else { false } @@ -238,6 +238,12 @@ pub(crate) struct ComponentState { } impl ComponentState { + #[tracing::instrument( + level = tracing::Level::DEBUG, + name = "create", + skip_all, + fields(component.id = scope.id), + )] fn new( initial_render_state: ComponentRenderState, scope: Scope, @@ -292,6 +298,15 @@ impl ComponentState { .downcast_ref::>() .map(|m| &m.component) } + + fn resume_existing_suspension(&mut self) { + if let Some(m) = self.suspension.take() { + let comp_scope = self.inner.any_scope(); + + let suspense_scope = comp_scope.find_parent_scope::().unwrap(); + BaseSuspense::resume(&suspense_scope, m); + } + } } pub(crate) struct CreateRunner { @@ -306,9 +321,6 @@ impl Runnable for CreateRunner { fn run(self: Box) { let mut current_state = self.scope.state.borrow_mut(); if current_state.is_none() { - #[cfg(debug_assertions)] - super::log_event(self.scope.id, "create"); - *current_state = Some(ComponentState::new( self.initial_render_state, self.scope.clone(), @@ -320,126 +332,27 @@ impl Runnable for CreateRunner { } } -#[cfg(feature = "csr")] -pub(crate) struct PropsUpdateRunner { - pub props: Option>, +pub(crate) struct UpdateRunner { pub state: Shared>, - pub next_sibling: Option, } -#[cfg(feature = "csr")] -impl Runnable for PropsUpdateRunner { - fn run(self: Box) { - let Self { - next_sibling, - props, - state: shared_state, - } = *self; - - if let Some(state) = shared_state.borrow_mut().as_mut() { - if let Some(next_sibling) = next_sibling { - // When components are updated, their siblings were likely also updated - // We also need to shift the bundle so next sibling will be synced to child - // components. - match state.render_state { - #[cfg(feature = "csr")] - ComponentRenderState::Render { - next_sibling: ref current_next_sibling, - .. - } => { - current_next_sibling.link(next_sibling); - } - - #[cfg(feature = "hydration")] - ComponentRenderState::Hydration { - next_sibling: ref current_next_sibling, - .. - } => { - current_next_sibling.link(next_sibling); - } - - #[cfg(feature = "ssr")] - ComponentRenderState::Ssr { .. } => { - #[cfg(debug_assertions)] - panic!("properties do not change during SSR"); - } - } - } - - let should_render = |props: Option>, state: &mut ComponentState| -> bool { - props.map(|m| state.inner.props_changed(m)).unwrap_or(false) - }; - - #[cfg(feature = "hydration")] - let should_render_hydration = - |props: Option>, state: &mut ComponentState| -> bool { - if let Some(props) = props.or_else(|| state.pending_props.take()) { - match state.has_rendered { - true => { - state.pending_props = None; - state.inner.props_changed(props) - } - false => { - state.pending_props = Some(props); - false - } - } - } else { - false - } - }; - - // Only trigger changed if props were changed / next sibling has changed. - let schedule_render = { - #[cfg(feature = "hydration")] - { - if state.inner.creation_mode() == RenderMode::Hydration { - should_render_hydration(props, state) - } else { - should_render(props, state) - } - } - - #[cfg(not(feature = "hydration"))] - should_render(props, state) - }; - - #[cfg(debug_assertions)] - super::log_event( - state.comp_id, - format!( - "props_update(has_rendered={} schedule_render={})", - state.has_rendered, schedule_render - ), - ); - - if schedule_render { - scheduler::push_component_render( - state.comp_id, - Box::new(RenderRunner { - state: shared_state.clone(), - }), - ); - // Only run from the scheduler, so no need to call `scheduler::start()` - } - }; +impl ComponentState { + #[tracing::instrument( + level = tracing::Level::DEBUG, + skip(self), + fields(component.id = self.comp_id) + )] + fn update(&mut self) -> bool { + let schedule_render = self.inner.flush_messages(); + tracing::trace!(schedule_render); + schedule_render } } -pub(crate) struct UpdateRunner { - pub state: Shared>, -} - impl Runnable for UpdateRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { - let schedule_render = state.inner.flush_messages(); - - #[cfg(debug_assertions)] - super::log_event( - state.comp_id, - format!("update(schedule_render={})", schedule_render), - ); + let schedule_render = state.update(); if schedule_render { scheduler::push_component_render( @@ -459,93 +372,92 @@ pub(crate) struct DestroyRunner { pub parent_to_detach: bool, } -impl Runnable for DestroyRunner { - fn run(self: Box) { - if let Some(mut state) = self.state.borrow_mut().take() { - #[cfg(debug_assertions)] - super::log_event(state.comp_id, "destroy"); - - state.inner.destroy(); - - match state.render_state { - #[cfg(feature = "csr")] - ComponentRenderState::Render { - bundle, - ref parent, - ref internal_ref, - ref root, - .. - } => { - bundle.detach(root, parent, self.parent_to_detach); - - internal_ref.set(None); - } - // We need to detach the hydrate fragment if the component is not hydrated. - #[cfg(feature = "hydration")] - ComponentRenderState::Hydration { - ref root, - fragment, - ref parent, - ref internal_ref, - .. - } => { - fragment.detach(root, parent, self.parent_to_detach); - - internal_ref.set(None); - } +impl ComponentState { + #[tracing::instrument( + level = tracing::Level::DEBUG, + skip(self), + fields(component.id = self.comp_id) + )] + fn destroy(mut self, parent_to_detach: bool) { + self.inner.destroy(); + self.resume_existing_suspension(); + + match self.render_state { + #[cfg(feature = "csr")] + ComponentRenderState::Render { + bundle, + ref parent, + ref root, + .. + } => { + bundle.detach(root, parent, parent_to_detach); + } + // We need to detach the hydrate fragment if the component is not hydrated. + #[cfg(feature = "hydration")] + ComponentRenderState::Hydration { + ref root, + fragment, + ref parent, + .. + } => { + fragment.detach(root, parent, parent_to_detach); + } - #[cfg(feature = "ssr")] - ComponentRenderState::Ssr { .. } => { - let _ = self.parent_to_detach; - } + #[cfg(feature = "ssr")] + ComponentRenderState::Ssr { .. } => { + let _ = parent_to_detach; } } } } +impl Runnable for DestroyRunner { + fn run(self: Box) { + if let Some(state) = self.state.borrow_mut().take() { + state.destroy(self.parent_to_detach); + } + } +} + pub(crate) struct RenderRunner { pub state: Shared>, } -impl Runnable for RenderRunner { - fn run(self: Box) { - if let Some(state) = self.state.borrow_mut().as_mut() { - #[cfg(debug_assertions)] - super::log_event(state.comp_id, "render"); - - match state.inner.view() { - Ok(m) => self.render(state, m), - Err(RenderError::Suspended(m)) => self.suspend(state, m), - }; - } +impl ComponentState { + #[tracing::instrument( + level = tracing::Level::DEBUG, + skip_all, + fields(component.id = self.comp_id) + )] + fn render(&mut self, shared_state: &Shared>) { + match self.inner.view() { + Ok(vnode) => self.commit_render(shared_state, vnode), + Err(RenderError::Suspended(susp)) => self.suspend(shared_state, susp), + }; } -} -impl RenderRunner { - fn suspend(&self, state: &mut ComponentState, suspension: Suspension) { + fn suspend(&mut self, shared_state: &Shared>, suspension: Suspension) { // Currently suspended, we re-use previous root node and send // suspension to parent element. - let shared_state = self.state.clone(); - - let comp_id = state.comp_id; if suspension.resumed() { // schedule a render immediately if suspension is resumed. scheduler::push_component_render( - comp_id, + self.comp_id, Box::new(RenderRunner { - state: shared_state, + state: shared_state.clone(), }), ); } else { // We schedule a render after current suspension is resumed. - let comp_scope = state.inner.any_scope(); + let comp_scope = self.inner.any_scope(); let suspense_scope = comp_scope .find_parent_scope::() .expect("To suspend rendering, a component is required."); - let suspense = suspense_scope.get_component().unwrap(); + let comp_id = self.comp_id; + let shared_state = shared_state.clone(); suspension.listen(Callback::from(move |_| { scheduler::push_component_render( comp_id, @@ -556,56 +468,46 @@ impl RenderRunner { scheduler::start(); })); - if let Some(ref last_suspension) = state.suspension { + if let Some(ref last_suspension) = self.suspension { if &suspension != last_suspension { // We remove previous suspension from the suspense. - suspense.resume(last_suspension.clone()); + BaseSuspense::resume(&suspense_scope, last_suspension.clone()); } } - state.suspension = Some(suspension.clone()); + self.suspension = Some(suspension.clone()); - suspense.suspend(suspension); + BaseSuspense::suspend(&suspense_scope, suspension); } } - fn render(&self, state: &mut ComponentState, new_root: Html) { + fn commit_render(&mut self, shared_state: &Shared>, new_root: Html) { // Currently not suspended, we remove any previous suspension and update // normally. - if let Some(m) = state.suspension.take() { - let comp_scope = state.inner.any_scope(); + self.resume_existing_suspension(); - let suspense_scope = comp_scope.find_parent_scope::().unwrap(); - let suspense = suspense_scope.get_component().unwrap(); - - suspense.resume(m); - } - - match state.render_state { + match self.render_state { #[cfg(feature = "csr")] ComponentRenderState::Render { ref mut bundle, ref parent, ref root, - ref next_sibling, - ref internal_ref, + ref sibling_slot, + ref mut own_slot, .. } => { - let scope = state.inner.any_scope(); - - #[cfg(feature = "hydration")] - next_sibling.debug_assert_not_trapped(); + let scope = self.inner.any_scope(); let new_node_ref = - bundle.reconcile(root, &scope, parent, next_sibling.clone(), new_root); - internal_ref.link(new_node_ref); + bundle.reconcile(root, &scope, parent, sibling_slot.to_position(), new_root); + own_slot.reassign(new_node_ref); - let first_render = !state.has_rendered; - state.has_rendered = true; + let first_render = !self.has_rendered; + self.has_rendered = true; scheduler::push_component_rendered( - state.comp_id, + self.comp_id, Box::new(RenderedRunner { - state: self.state.clone(), + state: shared_state.clone(), first_render, }), first_render, @@ -616,44 +518,46 @@ impl RenderRunner { ComponentRenderState::Hydration { ref mut fragment, ref parent, - ref internal_ref, - ref next_sibling, + ref mut own_slot, + ref mut sibling_slot, ref root, } => { // We schedule a "first" render to run immediately after hydration, - // to fix NodeRefs (first_node and next_sibling). + // to fix NodeRefs (first_node and slot). scheduler::push_component_priority_render( - state.comp_id, + self.comp_id, Box::new(RenderRunner { - state: self.state.clone(), + state: shared_state.clone(), }), ); - let scope = state.inner.any_scope(); + let scope = self.inner.any_scope(); // This first node is not guaranteed to be correct here. // As it may be a comment node that is removed afterwards. // but we link it anyways. - let (node, bundle) = Bundle::hydrate(root, &scope, parent, fragment, new_root); + let bundle = Bundle::hydrate(root, &scope, parent, fragment, new_root); // We trim all text nodes before checking as it's likely these are whitespaces. - fragment.trim_start_text_nodes(parent); + fragment.trim_start_text_nodes(); assert!(fragment.is_empty(), "expected end of component, found node"); - internal_ref.link(node); - - state.render_state = ComponentRenderState::Render { + self.render_state = ComponentRenderState::Render { root: root.clone(), bundle, parent: parent.clone(), - internal_ref: internal_ref.clone(), - next_sibling: next_sibling.clone(), + own_slot: std::mem::replace(own_slot, DynamicDomSlot::new_debug_trapped()), + sibling_slot: std::mem::replace( + sibling_slot, + DynamicDomSlot::new_debug_trapped(), + ), }; } #[cfg(feature = "ssr")] ComponentRenderState::Ssr { ref mut sender } => { + let _ = shared_state; if let Some(tx) = sender.take() { tx.send(new_root).unwrap(); } @@ -662,31 +566,170 @@ impl RenderRunner { } } +impl Runnable for RenderRunner { + fn run(self: Box) { + let mut state = self.state.borrow_mut(); + let state = match state.as_mut() { + None => return, // skip for components that have already been destroyed + Some(state) => state, + }; + + state.render(&self.state); + } +} + #[cfg(feature = "csr")] mod feat_csr { use super::*; + pub(crate) struct PropsUpdateRunner { + pub state: Shared>, + pub props: Option>, + pub next_sibling_slot: Option, + } + + impl ComponentState { + #[tracing::instrument( + level = tracing::Level::DEBUG, + skip(self), + fields(component.id = self.comp_id) + )] + fn changed( + &mut self, + props: Option>, + next_sibling_slot: Option, + ) -> bool { + if let Some(next_sibling_slot) = next_sibling_slot { + // When components are updated, their siblings were likely also updated + // We also need to shift the bundle so next sibling will be synced to child + // components. + match &mut self.render_state { + #[cfg(feature = "csr")] + ComponentRenderState::Render { sibling_slot, .. } => { + sibling_slot.reassign(next_sibling_slot); + } + + #[cfg(feature = "hydration")] + ComponentRenderState::Hydration { sibling_slot, .. } => { + sibling_slot.reassign(next_sibling_slot); + } + + #[cfg(feature = "ssr")] + ComponentRenderState::Ssr { .. } => { + #[cfg(debug_assertions)] + panic!("properties do not change during SSR"); + } + } + } + + let should_render = |props: Option>, state: &mut ComponentState| -> bool { + props.map(|m| state.inner.props_changed(m)).unwrap_or(false) + }; + + #[cfg(feature = "hydration")] + let should_render_hydration = + |props: Option>, state: &mut ComponentState| -> bool { + if let Some(props) = props.or_else(|| state.pending_props.take()) { + match state.has_rendered { + true => { + state.pending_props = None; + state.inner.props_changed(props) + } + false => { + state.pending_props = Some(props); + false + } + } + } else { + false + } + }; + + // Only trigger changed if props were changed / next sibling has changed. + let schedule_render = { + #[cfg(feature = "hydration")] + { + if self.inner.creation_mode() == RenderMode::Hydration { + should_render_hydration(props, self) + } else { + should_render(props, self) + } + } + + #[cfg(not(feature = "hydration"))] + should_render(props, self) + }; + + tracing::trace!( + "props_update(has_rendered={} schedule_render={})", + self.has_rendered, + schedule_render + ); + schedule_render + } + } + + impl Runnable for PropsUpdateRunner { + fn run(self: Box) { + let Self { + next_sibling_slot, + props, + state: shared_state, + } = *self; + + if let Some(state) = shared_state.borrow_mut().as_mut() { + let schedule_render = state.changed(props, next_sibling_slot); + + if schedule_render { + scheduler::push_component_render( + state.comp_id, + Box::new(RenderRunner { + state: shared_state.clone(), + }), + ); + // Only run from the scheduler, so no need to call `scheduler::start()` + } + }; + } + } + pub(crate) struct RenderedRunner { pub state: Shared>, pub first_render: bool, } + impl ComponentState { + #[tracing::instrument( + level = tracing::Level::DEBUG, + skip(self), + fields(component.id = self.comp_id) + )] + fn rendered(&mut self, first_render: bool) -> bool { + if self.suspension.is_none() { + self.inner.rendered(first_render); + } + + #[cfg(feature = "hydration")] + { + self.pending_props.is_some() + } + #[cfg(not(feature = "hydration"))] + { + false + } + } + } + impl Runnable for RenderedRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { - #[cfg(debug_assertions)] - super::super::log_event(state.comp_id, "rendered"); - - if state.suspension.is_none() { - state.inner.rendered(self.first_render); - } + let has_pending_props = state.rendered(self.first_render); - #[cfg(feature = "hydration")] - if state.pending_props.is_some() { + if has_pending_props { scheduler::push_component_props_update(Box::new(PropsUpdateRunner { - props: None, state: self.state.clone(), - next_sibling: None, + props: None, + next_sibling_slot: None, })); } } @@ -695,7 +738,7 @@ mod feat_csr { } #[cfg(feature = "csr")] -use feat_csr::*; +pub(super) use feat_csr::*; #[cfg(target_arch = "wasm32")] #[cfg(test)] @@ -741,7 +784,7 @@ mod tests { false } - fn changed(&mut self, _ctx: &Context) -> bool { + fn changed(&mut self, _ctx: &Context, _old_props: &Self::Properties) -> bool { false } @@ -801,7 +844,7 @@ mod tests { msg } - fn changed(&mut self, ctx: &Context) -> bool { + fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { self.lifecycle = Rc::clone(&ctx.props().lifecycle); self.lifecycle.borrow_mut().push("change".into()); false @@ -823,7 +866,7 @@ mod tests { } fn test_lifecycle(props: Props, expected: &[&str]) { - let document = gloo_utils::document(); + let document = gloo::utils::document(); let scope = Scope::::new(None); let parent = document.create_element("div").unwrap(); let root = BSubtree::create_root(&parent); @@ -834,8 +877,8 @@ mod tests { scope.mount_in_place( root, parent, - NodeRef::default(), - NodeRef::default(), + DomSlot::at_end(), + DynamicDomSlot::new_debug_trapped(), Rc::new(props), ); crate::scheduler::start_now(); diff --git a/packages/yew/src/html/component/marker.rs b/packages/yew/src/html/component/marker.rs index 9a970b6d242..abd2d152d5e 100644 --- a/packages/yew/src/html/component/marker.rs +++ b/packages/yew/src/html/component/marker.rs @@ -1,7 +1,7 @@ //! Primitive Components & Properties Types +use crate::function_component; use crate::html::{BaseComponent, ChildrenProps, Html}; -use crate::{function_component, html}; /// A Component to represent a component that does not exist in current implementation. /// @@ -142,5 +142,5 @@ pub fn PhantomComponent(props: &ChildrenProps) -> Html where T: BaseComponent, { - html! { <>{props.children.clone()} } + props.children.clone() } diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index 990f980666f..5640cf6d979 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -18,42 +18,6 @@ pub use scope::{AnyScope, Scope, SendAsMessage}; use super::{Html, HtmlResult, IntoHtmlResult}; -#[cfg(debug_assertions)] -#[cfg(any(feature = "csr", feature = "ssr"))] -mod feat_csr_ssr { - use wasm_bindgen::prelude::wasm_bindgen; - use wasm_bindgen::JsValue; - - thread_local! { - static EVENT_HISTORY: std::cell::RefCell>> - = Default::default(); - } - - /// Push [Component] event to lifecycle debugging registry - pub(crate) fn log_event(comp_id: usize, event: impl ToString) { - EVENT_HISTORY.with(|h| { - h.borrow_mut() - .entry(comp_id) - .or_default() - .push(event.to_string()) - }); - } - - /// Get [Component] event log from lifecycle debugging registry - #[wasm_bindgen(js_name = "yewGetEventLog")] - pub fn _get_event_log(comp_id: usize) -> Option> { - EVENT_HISTORY.with(|h| { - Some( - h.borrow() - .get(&comp_id)? - .iter() - .map(|l| (*l).clone().into()) - .collect(), - ) - }) - } -} - #[cfg(feature = "hydration")] #[derive(Debug, Clone, Copy, PartialEq)] pub(crate) enum RenderMode { @@ -63,11 +27,7 @@ pub(crate) enum RenderMode { Ssr, } -#[cfg(debug_assertions)] -#[cfg(any(feature = "csr", feature = "ssr"))] -pub(crate) use feat_csr_ssr::*; - -/// The [`Component`]'s context. This contains component's [`Scope`] and and props and +/// The [`Component`]'s context. This contains component's [`Scope`] and props and /// is passed to every lifecycle method. #[derive(Debug)] pub struct Context { @@ -90,7 +50,7 @@ impl Context { /// The component's props #[inline] pub fn props(&self) -> &COMP::Properties { - &*self.props + &self.props } #[cfg(feature = "hydration")] @@ -140,7 +100,7 @@ pub trait BaseComponent: Sized + 'static { fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool; /// React to changes of component properties. - fn changed(&mut self, ctx: &Context) -> bool; + fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool; /// Returns a component layout to be rendered. fn view(&self, ctx: &Context) -> HtmlResult; @@ -181,16 +141,20 @@ pub trait Component: Sized + 'static { /// to update their state and (optionally) re-render themselves. /// /// Returned bool indicates whether to render this Component after update. + /// + /// By default, this function will return true and thus make the component re-render. #[allow(unused_variables)] fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - false + true } /// Called when properties passed to the component change /// /// Returned bool indicates whether to render this Component after changed. + /// + /// By default, this function will return true and thus make the component re-render. #[allow(unused_variables)] - fn changed(&mut self, ctx: &Context) -> bool { + fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { true } @@ -242,8 +206,8 @@ where Component::update(self, ctx, msg) } - fn changed(&mut self, ctx: &Context) -> bool { - Component::changed(self, ctx) + fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { + Component::changed(self, ctx, old_props) } fn view(&self, ctx: &Context) -> HtmlResult { @@ -262,3 +226,39 @@ where Component::prepare_state(self) } } + +#[cfg(test)] +#[cfg(any(feature = "ssr", feature = "csr"))] +mod tests { + use super::*; + + struct MyCustomComponent; + + impl Component for MyCustomComponent { + type Message = (); + type Properties = (); + + fn create(_ctx: &Context) -> Self { + Self + } + + fn view(&self, _ctx: &Context) -> Html { + Default::default() + } + } + + #[test] + fn make_sure_component_update_and_changed_rerender() { + let mut comp = MyCustomComponent; + let ctx = Context { + scope: Scope::new(None), + props: Rc::new(()), + #[cfg(feature = "hydration")] + creation_mode: crate::html::RenderMode::Hydration, + #[cfg(feature = "hydration")] + prepared_state: None, + }; + assert!(Component::update(&mut comp, &ctx, ())); + assert!(Component::changed(&mut comp, &ctx, &Rc::new(()))); + } +} diff --git a/packages/yew/src/html/component/properties.rs b/packages/yew/src/html/component/properties.rs index 13476e83251..d61915339b9 100644 --- a/packages/yew/src/html/component/properties.rs +++ b/packages/yew/src/html/component/properties.rs @@ -93,7 +93,7 @@ mod __macro { pub struct AssertAllProps; /// Builder for when a component has no properties - #[derive(Debug, PartialEq)] + #[derive(Debug, PartialEq, Eq)] pub struct EmptyBuilder; impl super::Properties for () { diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 05229fad2ac..f4b0fbd4b65 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -7,6 +7,8 @@ use std::ops::Deref; use std::rc::Rc; use std::{fmt, iter}; +use futures::{Stream, StreamExt}; + #[cfg(any(feature = "csr", feature = "ssr"))] use super::lifecycle::ComponentState; use super::BaseComponent; @@ -236,6 +238,35 @@ impl Scope { spawn_local(js_future); } + /// This method asynchronously awaits a [`Stream`] that returns a series of messages and sends + /// them to the linked component. + /// + /// # Panics + /// If the stream panics, then the promise will not resolve, and will leak. + /// + /// # Note + /// + /// This method will not notify the component when the stream has been fully exhausted. If + /// you want this feature, you can add an EOF message variant for your component and use + /// [`StreamExt::chain`] and [`stream::once`](futures::stream::once) to chain an EOF message to + /// the original stream. If your stream is produced by another crate, you can use + /// [`StreamExt::map`] to transform the stream's item type to the component message type. + pub fn send_stream(&self, stream: S) + where + M: Into, + S: Stream + 'static, + { + let link = self.clone(); + let js_future = async move { + futures::pin_mut!(stream); + while let Some(msg) = stream.next().await { + let message: COMP::Message = msg.into(); + link.send_message(message); + } + }; + spawn_local(js_future); + } + /// Returns the linked component if available pub fn get_component(&self) -> Option + '_> { self.arch_get_component() @@ -260,11 +291,13 @@ impl Scope { #[cfg(feature = "ssr")] mod feat_ssr { + use std::fmt::Write; + use super::*; use crate::html::component::lifecycle::{ ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner, }; - use crate::platform::fmt::BufWrite; + use crate::platform::fmt::BufWriter; use crate::platform::pinned::oneshot; use crate::scheduler; use crate::virtual_dom::Collectable; @@ -272,7 +305,7 @@ mod feat_ssr { impl Scope { pub(crate) async fn render_into_stream( &self, - w: &mut dyn BufWrite, + w: &mut BufWriter, props: Rc, hydratable: bool, ) { @@ -311,9 +344,9 @@ mod feat_ssr { .await; if let Some(prepared_state) = self.get_component().unwrap().prepare_state() { - w.write(r#""#.into()); + let _ = w.write_str(r#""#); } if hydratable { @@ -419,11 +452,11 @@ mod feat_csr_ssr { } } + #[rustversion::before(1.63)] #[inline] pub(super) fn arch_get_component(&self) -> Option + '_> { self.state.try_borrow().ok().and_then(|state_ref| { state_ref.as_ref()?; - // TODO: Replace unwrap with Ref::filter_map once it becomes stable. Some(Ref::map(state_ref, |state| { state .as_ref() @@ -433,6 +466,18 @@ mod feat_csr_ssr { }) } + #[rustversion::since(1.63)] + #[inline] + pub(super) fn arch_get_component(&self) -> Option + '_> { + self.state.try_borrow().ok().and_then(|state_ref| { + // Ref::filter_map is only available since 1.63 + Ref::filter_map(state_ref, |state| { + state.as_ref().and_then(|m| m.downcast_comp_ref::()) + }) + .ok() + }) + } + #[inline] fn schedule_update(&self) { scheduler::push_component_update(Box::new(UpdateRunner { @@ -475,11 +520,10 @@ mod feat_csr { use web_sys::Element; use super::*; - use crate::dom_bundle::{BSubtree, Bundle}; + use crate::dom_bundle::{BSubtree, Bundle, DomSlot, DynamicDomSlot}; use crate::html::component::lifecycle::{ ComponentRenderState, CreateRunner, DestroyRunner, PropsUpdateRunner, RenderRunner, }; - use crate::html::NodeRef; use crate::scheduler; impl AnyScope { @@ -496,11 +540,11 @@ mod feat_csr { fn schedule_props_update( state: Shared>, props: Rc, - next_sibling: NodeRef, + next_sibling_slot: DomSlot, ) { scheduler::push_component_props_update(Box::new(PropsUpdateRunner { state, - next_sibling: Some(next_sibling), + next_sibling_slot: Some(next_sibling_slot), props: Some(props), })); // Not guaranteed to already have the scheduler started @@ -516,20 +560,20 @@ mod feat_csr { &self, root: BSubtree, parent: Element, - next_sibling: NodeRef, - internal_ref: NodeRef, + slot: DomSlot, + internal_ref: DynamicDomSlot, props: Rc, ) { let bundle = Bundle::new(); - internal_ref.link(next_sibling.clone()); - let stable_next_sibling = NodeRef::default(); - stable_next_sibling.link(next_sibling); + let sibling_slot = DynamicDomSlot::new(slot); + internal_ref.reassign(sibling_slot.to_position()); + let state = ComponentRenderState::Render { bundle, root, - internal_ref, + own_slot: internal_ref, parent, - next_sibling: stable_next_sibling, + sibling_slot, }; scheduler::push_component_create( @@ -549,11 +593,8 @@ mod feat_csr { scheduler::start(); } - pub(crate) fn reuse(&self, props: Rc, next_sibling: NodeRef) { - #[cfg(debug_assertions)] - super::super::log_event(self.id, "reuse"); - - schedule_props_update(self.state.clone(), props, next_sibling) + pub(crate) fn reuse(&self, props: Rc, slot: DomSlot) { + schedule_props_update(self.state.clone(), props, slot) } } @@ -562,7 +603,7 @@ mod feat_csr { /// Get the render state if it hasn't already been destroyed fn render_state(&self) -> Option>; /// Shift the node associated with this scope to a new place - fn shift_node(&self, parent: Element, next_sibling: NodeRef); + fn shift_node(&self, parent: Element, slot: DomSlot); /// Process an event to destroy a component fn destroy(self, parent_to_detach: bool); fn destroy_boxed(self: Box, parent_to_detach: bool); @@ -598,10 +639,10 @@ mod feat_csr { self.destroy(parent_to_detach) } - fn shift_node(&self, parent: Element, next_sibling: NodeRef) { + fn shift_node(&self, parent: Element, slot: DomSlot) { let mut state_ref = self.state.borrow_mut(); if let Some(render_state) = state_ref.as_mut() { - render_state.render_state.shift(parent, next_sibling) + render_state.render_state.shift(parent, slot) } } } @@ -609,16 +650,14 @@ mod feat_csr { #[cfg(feature = "csr")] pub(crate) use feat_csr::*; -#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] #[cfg(feature = "hydration")] mod feat_hydration { use wasm_bindgen::JsCast; use web_sys::{Element, HtmlScriptElement}; use super::*; - use crate::dom_bundle::{BSubtree, Fragment}; + use crate::dom_bundle::{BSubtree, DomSlot, DynamicDomSlot, Fragment}; use crate::html::component::lifecycle::{ComponentRenderState, CreateRunner, RenderRunner}; - use crate::html::NodeRef; use crate::scheduler; use crate::virtual_dom::Collectable; @@ -639,29 +678,27 @@ mod feat_hydration { root: BSubtree, parent: Element, fragment: &mut Fragment, - internal_ref: NodeRef, + internal_ref: DynamicDomSlot, props: Rc, ) { // This is very helpful to see which component is failing during hydration // which means this component may not having a stable layout / differs between // client-side and server-side. - #[cfg(debug_assertions)] - super::super::log_event( - self.id, - format!("hydration(type = {})", std::any::type_name::()), + tracing::trace!( + component.id = self.id, + "hydration(type = {})", + std::any::type_name::() ); let collectable = Collectable::for_component::(); let mut fragment = Fragment::collect_between(fragment, &collectable, &parent); - match fragment.front().cloned() { - front @ Some(_) => internal_ref.set(front), - None => - { - #[cfg(debug_assertions)] - internal_ref.link(NodeRef::new_debug_trapped()) - } - } + let next_sibling = if let Some(n) = fragment.front() { + Some(n.clone()) + } else { + fragment.sibling_at_end().cloned() + }; + internal_ref.reassign(DomSlot::create(next_sibling)); let prepared_state = match fragment .back() @@ -679,8 +716,8 @@ mod feat_hydration { let state = ComponentRenderState::Hydration { parent, root, - internal_ref, - next_sibling: NodeRef::new_debug_trapped(), + own_slot: internal_ref, + sibling_slot: DynamicDomSlot::new_debug_trapped(), fragment, }; diff --git a/packages/yew/src/html/conversion.rs b/packages/yew/src/html/conversion/into_prop_value.rs similarity index 81% rename from packages/yew/src/html/conversion.rs rename to packages/yew/src/html/conversion/into_prop_value.rs index 465f0d58563..8489900f1d8 100644 --- a/packages/yew/src/html/conversion.rs +++ b/packages/yew/src/html/conversion/into_prop_value.rs @@ -3,9 +3,10 @@ use std::rc::Rc; use implicit_clone::unsync::{IArray, IMap}; pub use implicit_clone::ImplicitClone; -use super::super::callback::Callback; -use super::{BaseComponent, Children, ChildrenRenderer, Component, NodeRef, Scope}; -use crate::virtual_dom::{AttrValue, VChild, VNode}; +use super::ToHtml; +use crate::callback::Callback; +use crate::html::{BaseComponent, ChildrenRenderer, Component, NodeRef, Scope}; +use crate::virtual_dom::{AttrValue, VChild, VNode, VText}; impl ImplicitClone for NodeRef {} impl ImplicitClone for Scope {} @@ -81,63 +82,74 @@ where } } -impl IntoPropValue>> for VChild +impl IntoPropValue> for VChild where T: BaseComponent, + C: Clone + Into, + VChild: Into, { #[inline] - fn into_prop_value(self) -> ChildrenRenderer> { - ChildrenRenderer::new(vec![self]) + fn into_prop_value(self) -> ChildrenRenderer { + ChildrenRenderer::new(vec![self.into()]) } } -impl IntoPropValue>>> for VChild +impl IntoPropValue>> for VChild where T: BaseComponent, + C: Clone + Into, + VChild: Into, { #[inline] - fn into_prop_value(self) -> Option>> { - Some(ChildrenRenderer::new(vec![self])) + fn into_prop_value(self) -> Option> { + Some(ChildrenRenderer::new(vec![self.into()])) } } -impl IntoPropValue>>> for Option> +impl IntoPropValue>> for Option> where T: BaseComponent, + C: Clone + Into, + VChild: Into, { #[inline] - fn into_prop_value(self) -> Option>> { - self.map(|m| ChildrenRenderer::new(vec![m])) + fn into_prop_value(self) -> Option> { + self.map(|m| ChildrenRenderer::new(vec![m.into()])) } } -impl IntoPropValue>> for Vec> +impl IntoPropValue> for Vec where - T: BaseComponent, + T: Into, + R: Clone + Into, { #[inline] - fn into_prop_value(self) -> ChildrenRenderer> { - ChildrenRenderer::new(self) + fn into_prop_value(self) -> ChildrenRenderer { + ChildrenRenderer::new(self.into_iter().map(|m| m.into()).collect()) } } -impl IntoPropValue>>> for Vec> +impl IntoPropValue for T where - T: BaseComponent, + T: ToHtml, { #[inline] - fn into_prop_value(self) -> Option>> { - Some(ChildrenRenderer::new(self)) + fn into_prop_value(self) -> VNode { + self.into_html() } } -impl IntoPropValue>>> for Option>> -where - T: BaseComponent, -{ +impl IntoPropValue> for VNode { + #[inline] + fn into_prop_value(self) -> ChildrenRenderer { + ChildrenRenderer::new(vec![self]) + } +} + +impl IntoPropValue> for VText { #[inline] - fn into_prop_value(self) -> Option>> { - self.map(ChildrenRenderer::new) + fn into_prop_value(self) -> ChildrenRenderer { + ChildrenRenderer::new(vec![self.into()]) } } @@ -171,11 +183,9 @@ macro_rules! impl_into_prop { // implemented with literals in mind impl_into_prop!(|value: &'static str| -> String { value.to_owned() }); - impl_into_prop!(|value: &'static str| -> AttrValue { AttrValue::Static(value) }); impl_into_prop!(|value: String| -> AttrValue { AttrValue::Rc(Rc::from(value)) }); impl_into_prop!(|value: Rc| -> AttrValue { AttrValue::Rc(value) }); -impl_into_prop!(|value: VNode| -> Children { Children::new(vec![value]) }); impl IntoPropValue> for &'static [T] { fn into_prop_value(self) -> IArray { @@ -306,19 +316,19 @@ mod test { html! {
    - {header} + {for header}
    {children}
    - {footer} + {for footer}
    } } - let header = VChild::new((), NodeRef::default(), None); + let header = VChild::new((), None); let footer = html_nested! { }; let children = html! {
    {"main"}
    }; diff --git a/packages/yew/src/html/conversion/mod.rs b/packages/yew/src/html/conversion/mod.rs new file mode 100644 index 00000000000..70181c553a2 --- /dev/null +++ b/packages/yew/src/html/conversion/mod.rs @@ -0,0 +1,5 @@ +mod into_prop_value; +mod to_html; + +pub use into_prop_value::*; +pub use to_html::*; diff --git a/packages/yew/src/html/conversion/to_html.rs b/packages/yew/src/html/conversion/to_html.rs new file mode 100644 index 00000000000..c7d3eeaa57b --- /dev/null +++ b/packages/yew/src/html/conversion/to_html.rs @@ -0,0 +1,203 @@ +use std::borrow::Cow; +use std::rc::Rc; +use std::sync::Arc; + +use crate::html::{ChildrenRenderer, IntoPropValue}; +use crate::virtual_dom::{VChild, VList, VNode, VText}; +use crate::{AttrValue, BaseComponent, Html}; + +/// A trait implemented for types be rendered as a part of a Html. +/// +/// Types that implements this trait can define a virtual dom layout that itself should be rendered +/// into via `html!` and can be referenced / consumed as `{value}` in an `html!` macro invocation. +pub trait ToHtml { + /// Converts this type to a [`Html`]. + fn to_html(&self) -> Html; + + /// Converts this type into a [`Html`]. + fn into_html(self) -> Html + where + Self: Sized, + { + self.to_html() + } +} + +// Implementations for common data types. + +impl ToHtml for Option +where + T: ToHtml, +{ + #[inline(always)] + fn to_html(&self) -> Html { + self.as_ref().map(ToHtml::to_html).unwrap_or_default() + } + + #[inline(always)] + fn into_html(self) -> Html { + self.map(ToHtml::into_html).unwrap_or_default() + } +} + +impl ToHtml for Vec +where + T: ToHtml, +{ + #[inline(always)] + fn to_html(&self) -> Html { + Html::VList(VList::with_children( + self.iter().map(ToHtml::to_html).collect(), + None, + )) + } + + #[inline(always)] + fn into_html(self) -> Html { + Html::VList(VList::with_children( + self.into_iter().map(ToHtml::into_html).collect(), + None, + )) + } +} + +impl ToHtml for Option { + #[inline(always)] + fn to_html(&self) -> Html { + self.clone().into_html() + } + + #[inline(always)] + fn into_html(self) -> Html { + self.unwrap_or_default() + } +} + +impl ToHtml for Vec { + #[inline(always)] + fn to_html(&self) -> Html { + self.clone().into_html() + } + + #[inline(always)] + fn into_html(self) -> Html { + Html::VList(VList::with_children(self, None)) + } +} + +impl ToHtml for VText { + #[inline(always)] + fn to_html(&self) -> Html { + self.clone().into() + } + + #[inline(always)] + fn into_html(self) -> Html { + Html::VText(self) + } +} + +impl ToHtml for VList { + #[inline(always)] + fn to_html(&self) -> Html { + self.clone().into() + } + + #[inline(always)] + fn into_html(self) -> Html { + Html::VList(self) + } +} + +impl ToHtml for ChildrenRenderer { + #[inline(always)] + fn to_html(&self) -> Html { + self.clone().into() + } + + #[inline(always)] + fn into_html(self) -> Html { + self.into() + } +} + +impl ToHtml for VChild +where + T: BaseComponent, +{ + #[inline(always)] + fn to_html(&self) -> Html { + self.clone().into() + } + + #[inline(always)] + fn into_html(self) -> Html { + VNode::VComp(self.into()) + } +} + +impl ToHtml for () { + #[inline(always)] + fn to_html(&self) -> Html { + VNode::default() + } + + #[inline(always)] + fn into_html(self) -> Html { + VNode::default() + } +} + +impl ToHtml for &'_ T +where + T: ToHtml, +{ + fn to_html(&self) -> Html { + (*self).to_html() + } +} + +macro_rules! impl_to_html_via_display { + ($from_ty: ty) => { + impl ToHtml for $from_ty { + #[inline(always)] + fn to_html(&self) -> Html { + Html::VText(VText::from(self)) + } + } + + // Mirror ToHtml to Children implementation. + impl IntoPropValue> for $from_ty { + #[inline(always)] + fn into_prop_value(self) -> ChildrenRenderer { + ChildrenRenderer::new(vec![VText::from(self).into()]) + } + } + }; +} + +// These are a selection of types implemented via display. +impl_to_html_via_display!(bool); +impl_to_html_via_display!(char); +impl_to_html_via_display!(String); +impl_to_html_via_display!(&str); +impl_to_html_via_display!(Rc); +impl_to_html_via_display!(Rc); +impl_to_html_via_display!(Arc); +impl_to_html_via_display!(Arc); +impl_to_html_via_display!(AttrValue); +impl_to_html_via_display!(Cow<'_, str>); +impl_to_html_via_display!(u8); +impl_to_html_via_display!(u16); +impl_to_html_via_display!(u32); +impl_to_html_via_display!(u64); +impl_to_html_via_display!(u128); +impl_to_html_via_display!(usize); +impl_to_html_via_display!(i8); +impl_to_html_via_display!(i16); +impl_to_html_via_display!(i32); +impl_to_html_via_display!(i64); +impl_to_html_via_display!(i128); +impl_to_html_via_display!(isize); +impl_to_html_via_display!(f32); +impl_to_html_via_display!(f64); diff --git a/packages/yew/src/html/listener/events.rs b/packages/yew/src/html/listener/events.rs index a1b8677f6d6..398ed53c68d 100644 --- a/packages/yew/src/html/listener/events.rs +++ b/packages/yew/src/html/listener/events.rs @@ -166,7 +166,7 @@ impl_short! { oninput(InputEvent) - onsubmit(FocusEvent) + onsubmit(SubmitEvent) onanimationcancel(AnimationEvent) onanimationend(AnimationEvent) diff --git a/packages/yew/src/html/mod.rs b/packages/yew/src/html/mod.rs index a9acab8e6b7..87e3cc03372 100644 --- a/packages/yew/src/html/mod.rs +++ b/packages/yew/src/html/mod.rs @@ -92,7 +92,7 @@ pub struct NodeRef(Rc>); impl PartialEq for NodeRef { fn eq(&self, other: &Self) -> bool { - self.0.as_ptr() == other.0.as_ptr() || Some(self) == other.0.borrow().link.as_ref() + self.0.as_ptr() == other.0.as_ptr() } } @@ -109,14 +109,13 @@ impl std::fmt::Debug for NodeRef { #[derive(PartialEq, Debug, Default, Clone)] struct NodeRefInner { node: Option, - link: Option, } impl NodeRef { /// Get the wrapped Node reference if it exists pub fn get(&self) -> Option { let inner = self.0.borrow(); - inner.node.clone().or_else(|| inner.link.as_ref()?.get()) + inner.node.clone() } /// Try converting the node reference into another form @@ -124,13 +123,6 @@ impl NodeRef { let node = self.get(); node.map(Into::into).map(INTO::from) } - - /// Place a Node in a reference for later use - pub(crate) fn set(&self, node: Option) { - let mut this = self.0.borrow_mut(); - this.node = node; - this.link = None; - } } #[cfg(feature = "csr")] @@ -138,73 +130,9 @@ mod feat_csr { use super::*; impl NodeRef { - /// Reuse an existing `NodeRef` - pub(crate) fn reuse(&self, node_ref: Self) { - // Avoid circular references - if self == &node_ref { - return; - } - - let mut this = self.0.borrow_mut(); - let mut existing = node_ref.0.borrow_mut(); - this.node = existing.node.take(); - this.link = existing.link.take(); - } - - /// Link a downstream `NodeRef` - pub(crate) fn link(&self, node_ref: Self) { - // Avoid circular references - if self == &node_ref { - return; - } - - let mut this = self.0.borrow_mut(); - this.node = None; - this.link = Some(node_ref); - } - - /// Wrap an existing `Node` in a `NodeRef` - pub(crate) fn new(node: Node) -> Self { - let node_ref = NodeRef::default(); - node_ref.set(Some(node)); - node_ref - } - } -} - -#[cfg(feature = "hydration")] -mod feat_hydration { - use super::*; - - #[cfg(debug_assertions)] - thread_local! { - // A special marker element that should not be referenced - static TRAP: Node = gloo::utils::document().create_element("div").unwrap().into(); - } - - impl NodeRef { - // A new "placeholder" node ref that should not be accessed - #[inline] - pub(crate) fn new_debug_trapped() -> Self { - #[cfg(debug_assertions)] - { - Self::new(TRAP.with(|trap| trap.clone())) - } - #[cfg(not(debug_assertions))] - { - Self::default() - } - } - - #[inline] - pub(crate) fn debug_assert_not_trapped(&self) { - #[cfg(debug_assertions)] - TRAP.with(|trap| { - assert!( - self.get().as_ref() != Some(trap), - "should not use a trapped node ref" - ) - }) + pub(crate) fn set(&self, new_ref: Option) { + let mut inner = self.0.borrow_mut(); + inner.node = new_ref; } } } @@ -216,30 +144,3 @@ mod feat_hydration { pub fn create_portal(child: Html, host: Element) -> Html { VNode::VPortal(VPortal::new(child, host)) } - -#[cfg(target_arch = "wasm32")] -#[cfg(test)] -mod tests { - use gloo_utils::document; - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - use super::*; - - wasm_bindgen_test_configure!(run_in_browser); - - #[test] - fn self_linking_node_ref() { - let node: Node = document().create_text_node("test node").into(); - let node_ref = NodeRef::new(node.clone()); - let node_ref_2 = NodeRef::new(node.clone()); - - // Link to self - node_ref.link(node_ref.clone()); - assert_eq!(node, node_ref.get().unwrap()); - - // Create cycle of two node refs - node_ref.link(node_ref_2.clone()); - node_ref_2.link(node_ref); - assert_eq!(node, node_ref_2.get().unwrap()); - } -} diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 5371cda33ee..65a5cdd4566 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -1,10 +1,8 @@ #![allow(clippy::needless_doctest_main)] #![doc(html_logo_url = "https://yew.rs/img/logo.png")] #![cfg_attr(documenting, feature(doc_cfg))] -#![cfg_attr( - feature = "nightly", - feature(fn_traits, async_closure, unboxed_closures) -)] +#![cfg_attr(documenting, feature(doc_auto_cfg))] +#![cfg_attr(nightly_yew, feature(fn_traits, async_closure, unboxed_closures))] //! # Yew Framework - API Documentation //! @@ -28,7 +26,6 @@ //! - `csr`: Enables Client-side Rendering support and [`Renderer`]. Only enable this feature if you //! are making a Yew application (not a library). //! - `ssr`: Enables Server-side Rendering support and [`ServerRenderer`]. -//! - `tokio`: Enables future-based APIs on non-wasm32 targets with tokio runtime. //! - `hydration`: Enables Hydration support. //! //! ## Example @@ -307,7 +304,8 @@ pub mod events { #[doc(no_inline)] pub use web_sys::{ AnimationEvent, DragEvent, ErrorEvent, Event, FocusEvent, InputEvent, KeyboardEvent, - MouseEvent, PointerEvent, ProgressEvent, TouchEvent, TransitionEvent, UiEvent, WheelEvent, + MouseEvent, PointerEvent, ProgressEvent, SubmitEvent, TouchEvent, TransitionEvent, UiEvent, + WheelEvent, }; #[cfg(feature = "csr")] @@ -338,7 +336,7 @@ pub mod prelude { pub use crate::functional::*; pub use crate::html::{ create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context, - Html, HtmlResult, NodeRef, Properties, + Html, HtmlResult, NodeRef, Properties, ToHtml, }; pub use crate::macros::{classes, html, html_nested}; pub use crate::suspense::Suspense; diff --git a/packages/yew/src/platform.rs b/packages/yew/src/platform.rs new file mode 100644 index 00000000000..20c35fba80f --- /dev/null +++ b/packages/yew/src/platform.rs @@ -0,0 +1,48 @@ +//! Yew's compatibility between JavaScript Runtime and Native Runtimes. +//! +//! This module is also published under the name [prokio] on crates.io. +//! +//! # Rationale +//! +//! When designing components and libraries that works on both WebAssembly targets backed by +//! JavaScript Runtime and non-WebAssembly targets with Native Runtimes. Developers usually face +//! challenges that requires applying multiple feature flags throughout their application: +//! +//! 1. Select I/O and timers that works with the target runtime. +//! 2. Native Runtimes usually require `Send` futures and WebAssembly types are usually `!Send`. +//! +//! # Implementation +//! +//! To alleviate these issues, Yew implements a single-threaded runtime that executes `?Send` +//! (`Send` or `!Send`) futures. +//! +//! On platforms with multi-threading support, Yew spawns multiple independent runtimes +//! proportional to the CPU core number. When tasks are spawned with a runtime handle, it will +//! randomly select a worker thread from the internal pool. All tasks spawned with `spawn_local` +//! will run on the same thread as the thread the task was running. When the runtime runs in a +//! WebAssembly target, all tasks will be scheduled on the main thread. +//! +//! This runtime is designed in favour of IO-bounded workload with similar runtime cost. +//! When running I/O workloads, it would produce a slightly better performance as tasks are +//! never moved to another thread. However, If a worker thread is busy, +//! other threads will not be able to steal tasks scheduled on the busy thread. +//! When you have a CPU-bounded task where CPU time is significantly +//! more expensive, it should be spawned with a dedicated thread (or Web Worker) and communicates +//! with the application using channels. +//! +//! Yew platform provides the following components: +//! +//! 1. A Task Scheduler that is capable of running non-Send tasks. +//! 2. A Timer that is compatible with the scheduler backend. +//! 3. Task Synchronisation Mechanisms. +//! +//! # Runtime Backend +//! +//! The Yew runtime is implemented with different runtimes depending on the target platform and can +//! use all features (timers / IO / task synchronisation) from the selected native runtime: +//! +//! - `wasm-bindgen-futures` (WebAssembly targets) +//! - `tokio` (non-WebAssembly targets) + +#[doc(inline)] +pub use prokio::*; diff --git a/packages/yew/src/platform/fmt.rs b/packages/yew/src/platform/fmt.rs deleted file mode 100644 index 72d9775655c..00000000000 --- a/packages/yew/src/platform/fmt.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! This module contains types for I/O functionality. - -// This module should remain private until impl trait type alias becomes available so -// `BufReader` can be produced with an existential type. - -use std::borrow::Cow; - -use crate::platform::pinned; - -// Same as std::io::BufWriter and futures::io::BufWriter. -pub(crate) const DEFAULT_BUF_SIZE: usize = 8 * 1024; - -pub(crate) trait BufSend { - fn buf_send(&self, item: String); -} - -impl BufSend for pinned::mpsc::UnboundedSender { - fn buf_send(&self, item: String) { - let _ = self.send_now(item); - } -} - -impl BufSend for futures::channel::mpsc::UnboundedSender { - fn buf_send(&self, item: String) { - let _ = self.unbounded_send(item); - } -} - -pub trait BufWrite { - fn capacity(&self) -> usize; - fn write(&mut self, s: Cow<'_, str>); -} - -/// A [`futures::io::BufWriter`], but operates over string and yields into a Stream. -pub(crate) struct BufWriter -where - S: BufSend, -{ - buf: String, - tx: S, - capacity: usize, -} - -// Implementation Notes: -// -// When jemalloc is used and a reasonable buffer length is chosen, -// performance of this buffer is related to the number of allocations -// instead of the amount of memory that is allocated. -// -// A Bytes-based implementation is also tested, and yielded a similar performance to String-based -// buffer. -// -// Having a String-based buffer avoids unsafe / cost of conversion between String and Bytes -// when text based content is needed (e.g.: post-processing). -// -// `Bytes::from` can be used to convert a `String` to `Bytes` if web server asks for an -// `impl Stream`. This conversion incurs no memory allocation. -// -// Yielding the output with a Stream provides a couple advantages: -// -// 1. All child components of a VList can have their own buffer and be rendered concurrently. -// 2. If a fixed buffer is used, the rendering process can become blocked if the buffer is filled. -// Using a stream avoids this side effect and allows the renderer to finish rendering -// without being actively polled. -impl BufWriter -where - S: BufSend, -{ - pub fn new(tx: S, capacity: usize) -> Self { - Self { - buf: String::new(), - tx, - capacity, - } - } - - #[inline] - fn drain(&mut self) { - if !self.buf.is_empty() { - self.tx.buf_send(self.buf.split_off(0)); - } - } - - #[inline] - fn reserve(&mut self) { - if self.buf.is_empty() { - self.buf.reserve(self.capacity); - } - } - - /// Returns `True` if the internal buffer has capacity to fit a string of certain length. - #[inline] - fn has_capacity_of(&self, next_part_len: usize) -> bool { - self.buf.capacity() >= self.buf.len() + next_part_len - } -} - -impl BufWrite for BufWriter -where - S: BufSend, -{ - #[inline] - fn capacity(&self) -> usize { - self.capacity - } - - /// Writes a string into the buffer, optionally drains the buffer. - fn write(&mut self, s: Cow<'_, str>) { - // Try to reserve the capacity first. - self.reserve(); - - if !self.has_capacity_of(s.len()) { - // There isn't enough capacity, we drain the buffer. - self.drain(); - } - - if self.has_capacity_of(s.len()) { - // The next part is going to fit into the buffer, we push it onto the buffer. - self.buf.push_str(&s); - } else { - // if the next part is more than buffer size, we send the next part. - - // We don't need to drain the buffer here as the result of self.has_capacity_of() only - // changes if the buffer was drained. If the buffer capacity didn't change, - // then it means self.has_capacity_of() has returned true the first time which will be - // guaranteed to be matched by the left hand side of this implementation. - self.tx.buf_send(s.into_owned()); - } - } -} - -impl Drop for BufWriter -where - S: BufSend, -{ - fn drop(&mut self) { - if !self.buf.is_empty() { - let mut buf = String::new(); - std::mem::swap(&mut buf, &mut self.buf); - self.tx.buf_send(buf); - } - } -} diff --git a/packages/yew/src/platform/mod.rs b/packages/yew/src/platform/mod.rs deleted file mode 100644 index 165d4f6a047..00000000000 --- a/packages/yew/src/platform/mod.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Compatibility between JavaScript Runtime and Native Runtimes. -//! -//! When designing components and libraries that works on both WebAssembly targets backed by -//! JavaScript Runtime and non-WebAssembly targets with Native Runtimes. Developers usually face -//! challenges that requires applying multiple feature flags throughout their application: -//! -//! 1. Select I/O and timers that works with the target runtime. -//! 2. Native Runtimes usually require `Send` futures and WebAssembly usually use `!Send` -//! primitives for better performance during Client-side Rendering. -//! -//! To alleviate these issues, Yew implements a single-threaded runtime that executes `?Send` -//! (`Send` or `!Send`) futures. When your application starts with `yew::Renderer` or is rendered by -//! `yew::ServerRenderer`, it is executed within the Yew runtime. On systems with multi-threading -//! support, it spawns multiple independent runtimes in a worker pool proportional to the CPU -//! core number. The renderer will randomly select a worker thread from the internal pool. All tasks -//! spawned with `spawn_local` in the application will run on the same thread as the -//! rendering thread the renderer has selected. When the renderer runs in a WebAssembly target, all -//! tasks will be scheduled on the main thread. -//! -//! This runtime is designed in favour of IO-bounded workload with similar runtime cost. It produces -//! better performance by pinning tasks to a single worker thread. However, this means that if a -//! worker thread is back-logged, other threads will not be able to "help" by running tasks -//! scheduled on the busy thread. When you have a CPU-bounded task where CPU time is significantly -//! more expensive than rendering tasks, it should be spawned with a dedicated thread or -//! `yew-agent` and communicates with the application using channels or agent bridges. -//! -//! # Runtime Backend -//! -//! Yew runtime is implemented with different runtimes depending on the target platform and can use -//! all features (timers / IO / task synchronisation) from the selected native runtime: -//! -//! - `wasm-bindgen-futures` (WebAssembly targets) -//! - `tokio` (non-WebAssembly targets) -//! -//! # Compatibility with other async runtimes -//! -//! Yew's ServerRenderer can also be executed in applications using other async runtimes(e.g.: -//! `async-std`). Rendering tasks will enter Yew runtime and be executed with `tokio`. When the -//! rendering task finishes, the result is returned to the original runtime. This process is -//! transparent to the future that executes the renderer. The Yew application still needs to use -//! `tokio`'s timer, IO and task synchronisation primitives. - -use std::future::Future; -use std::io::Result; - -#[cfg(feature = "ssr")] -pub(crate) mod fmt; - -pub mod pinned; -pub mod time; - -#[cfg(target_arch = "wasm32")] -#[path = "rt_wasm_bindgen/mod.rs"] -mod imp; -#[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] -#[path = "rt_tokio/mod.rs"] -mod imp; -#[cfg(all(not(target_arch = "wasm32"), not(feature = "tokio")))] -#[path = "rt_none/mod.rs"] -mod imp; - -/// Spawns a task on current thread. -/// -/// # Panics -/// -/// This function will panic when not being executed from within a Yew Application. -#[inline(always)] -pub fn spawn_local(f: F) -where - F: Future + 'static, -{ - imp::spawn_local(f); -} - -/// A Runtime Builder. -#[derive(Debug)] -pub struct RuntimeBuilder { - worker_threads: usize, -} - -impl Default for RuntimeBuilder { - fn default() -> Self { - Self { - worker_threads: imp::get_default_runtime_size(), - } - } -} - -impl RuntimeBuilder { - /// Creates a new Runtime Builder. - pub fn new() -> Self { - Self::default() - } - - /// Sets the number of worker threads the Runtime will use. - /// - /// # Default - /// - /// The default number of worker threads is double the number of available CPU cores. - /// - /// # Note - /// - /// This setting has no effect if current platform has no thread support (e.g.: WebAssembly). - pub fn worker_threads(&mut self, val: usize) -> &mut Self { - self.worker_threads = val; - - self - } - - /// Creates a Runtime. - pub fn build(&mut self) -> Result { - Ok(Runtime { - inner: imp::Runtime::new(self.worker_threads)?, - }) - } -} - -/// A Yew runtime that runs on the current thread. -#[derive(Debug)] -pub struct LocalRuntime { - inner: imp::LocalRuntime, -} - -impl LocalRuntime { - /// Creates a new LocalRuntime. - pub fn new() -> Result { - Ok(Self { - inner: imp::LocalRuntime::new()?, - }) - } - - /// Runs a Future until completion with current thread blocked. - /// - /// # Panic - /// - /// This method will panic if it is called from within a runtime. - /// If the runtime backend is `wasm-bindgen`, a runtime is started before passing through the - /// WebAssembly boundary and this method will always panic. - pub fn block_on(&self, f: F) -> F::Output - where - F: Future + 'static, - F::Output: 'static, - { - self.inner.block_on(f) - } -} - -/// The Yew Runtime. -#[derive(Debug, Clone, Default)] -pub struct Runtime { - inner: imp::Runtime, -} - -impl Runtime { - /// Creates a Builder to create a runtime. - pub fn builder() -> RuntimeBuilder { - RuntimeBuilder::new() - } - - /// Runs a task with it pinned to a worker thread. - /// - /// This can be used to execute non-Send futures without blocking the current thread. - /// - /// [`spawn_local`] is available with tasks executed with `run_pinned`. - pub async fn run_pinned(&self, create_task: F) -> Fut::Output - where - F: FnOnce() -> Fut, - F: Send + 'static, - Fut: Future + 'static, - Fut::Output: Send + 'static, - { - self.inner.run_pinned(create_task).await - } -} diff --git a/packages/yew/src/renderer.rs b/packages/yew/src/renderer.rs index fbf57fd1894..442268a281c 100644 --- a/packages/yew/src/renderer.rs +++ b/packages/yew/src/renderer.rs @@ -14,7 +14,7 @@ thread_local! { /// Set a custom panic hook. /// Unless a panic hook is set through this function, Yew will /// overwrite any existing panic hook when an application is rendered with [Renderer]. -#[cfg_attr(documenting, doc(cfg(feature = "csr")))] +#[cfg(feature = "csr")] pub fn set_custom_panic_hook(hook: Box) + Sync + Send + 'static>) { std::panic::set_hook(hook); PANIC_HOOK_IS_SET.with(|hook_is_set| hook_is_set.set(true)); @@ -29,7 +29,7 @@ fn set_default_panic_hook() { /// The Yew Renderer. /// /// This is the main entry point of a Yew application. -#[cfg_attr(documenting, doc(cfg(feature = "csr")))] +#[cfg(feature = "csr")] #[derive(Debug)] #[must_use = "Renderer does nothing unless render() is called."] pub struct Renderer @@ -73,7 +73,7 @@ where /// Creates a [Renderer] that renders into the document body with custom properties. pub fn with_props(props: COMP::Properties) -> Self { Self::with_root_and_props( - gloo_utils::document() + gloo::utils::document() .body() .expect("no body node found") .into(), @@ -93,7 +93,6 @@ where } } -#[cfg_attr(documenting, doc(cfg(feature = "hydration")))] #[cfg(feature = "hydration")] mod feat_hydration { use super::*; diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index 3fc57f94dcf..6f9113bf346 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; use std::collections::BTreeMap; use std::rc::Rc; -/// Alias for Rc> +/// Alias for `Rc>` pub type Shared = Rc>; /// A routine which could be run. @@ -13,32 +13,84 @@ pub trait Runnable { fn run(self: Box); } +struct QueueEntry { + task: Box, +} + +#[derive(Default)] +struct FifoQueue { + inner: Vec, +} + +impl FifoQueue { + fn push(&mut self, task: Box) { + self.inner.push(QueueEntry { task }); + } + + fn drain_into(&mut self, queue: &mut Vec) { + queue.append(&mut self.inner); + } +} + +#[derive(Default)] + +struct TopologicalQueue { + /// The Binary Tree Map guarantees components with lower id (parent) is rendered first + inner: BTreeMap, +} + +impl TopologicalQueue { + #[cfg(any(feature = "ssr", feature = "csr"))] + fn push(&mut self, component_id: usize, task: Box) { + self.inner.insert(component_id, QueueEntry { task }); + } + + /// Take a single entry, preferring parents over children + #[rustversion::before(1.66)] + fn pop_topmost(&mut self) -> Option { + // BTreeMap::pop_first is available after 1.66. + let key = *self.inner.keys().next()?; + self.inner.remove(&key) + } + + /// Take a single entry, preferring parents over children + #[rustversion::since(1.66)] + #[inline] + fn pop_topmost(&mut self) -> Option { + self.inner.pop_first().map(|(_, v)| v) + } + + /// Drain all entries, such that children are queued before parents + fn drain_post_order_into(&mut self, queue: &mut Vec) { + if self.inner.is_empty() { + return; + } + let rendered = std::mem::take(&mut self.inner); + // Children rendered lifecycle happen before parents. + queue.extend(rendered.into_values().rev()); + } +} + /// This is a global scheduler suitable to schedule and run any tasks. #[derive(Default)] #[allow(missing_debug_implementations)] // todo struct Scheduler { // Main queue - main: Vec>, + main: FifoQueue, // Component queues - destroy: Vec>, - create: Vec>, + destroy: FifoQueue, + create: FifoQueue, - props_update: Vec>, - update: Vec>, + props_update: FifoQueue, + update: FifoQueue, - /// The Binary Tree Map guarantees components with lower id (parent) is rendered first and - /// no more than 1 render can be scheduled before a component is rendered. - /// - /// Parent can destroy child components but not otherwise, we can save unnecessary render by - /// rendering parent first. - render: BTreeMap>, - render_first: BTreeMap>, - render_priority: BTreeMap>, - - /// Binary Tree Map to guarantee children rendered are always called before parent calls - rendered_first: BTreeMap>, - rendered: BTreeMap>, + render: TopologicalQueue, + render_first: TopologicalQueue, + render_priority: TopologicalQueue, + + rendered_first: TopologicalQueue, + rendered: TopologicalQueue, } /// Execute closure with a mutable reference to the scheduler @@ -52,7 +104,7 @@ fn with(f: impl FnOnce(&mut Scheduler) -> R) -> R { static SCHEDULER: RefCell = Default::default(); } - SCHEDULER.with(|s| f(&mut *s.borrow_mut())) + SCHEDULER.with(|s| f(&mut s.borrow_mut())) } /// Push a generic [Runnable] to be executed @@ -74,7 +126,7 @@ mod feat_csr_ssr { ) { with(|s| { s.create.push(create); - s.render_first.insert(component_id, first_render); + s.render_first.push(component_id, first_render); }); } @@ -86,7 +138,7 @@ mod feat_csr_ssr { /// Push a component render [Runnable]s to be executed pub(crate) fn push_component_render(component_id: usize, render: Box) { with(|s| { - s.render.insert(component_id, render); + s.render.push(component_id, render); }); } @@ -110,9 +162,9 @@ mod feat_csr { ) { with(|s| { if first_render { - s.rendered_first.insert(component_id, rendered); + s.rendered_first.push(component_id, rendered); } else { - s.rendered.insert(component_id, rendered); + s.rendered.push(component_id, rendered); } }); } @@ -131,7 +183,7 @@ mod feat_hydration { pub(crate) fn push_component_priority_render(component_id: usize, render: Box) { with(|s| { - s.render_priority.insert(component_id, render); + s.render_priority.push(component_id, render); }); } } @@ -141,6 +193,20 @@ pub(crate) use feat_hydration::*; /// Execute any pending [Runnable]s pub(crate) fn start_now() { + #[tracing::instrument(level = tracing::Level::DEBUG)] + fn scheduler_loop() { + let mut queue = vec![]; + loop { + with(|s| s.fill_queue(&mut queue)); + if queue.is_empty() { + break; + } + for r in queue.drain(..) { + r.task.run(); + } + } + } + thread_local! { // The lock is used to prevent recursion. If the lock cannot be acquired, it is because the // `start()` method is being called recursively as part of a `runnable.run()`. @@ -149,16 +215,7 @@ pub(crate) fn start_now() { LOCK.with(|l| { if let Ok(_lock) = l.try_borrow_mut() { - let mut queue = vec![]; - loop { - with(|s| s.fill_queue(&mut queue)); - if queue.is_empty() { - break; - } - for r in queue.drain(..) { - r.run(); - } - } + scheduler_loop(); } }); } @@ -196,13 +253,13 @@ impl Scheduler { /// This method is optimized for typical usage, where possible, but does not break on /// non-typical usage (like scheduling renders in [crate::Component::create()] or /// [crate::Component::rendered()] calls). - fn fill_queue(&mut self, to_run: &mut Vec>) { + fn fill_queue(&mut self, to_run: &mut Vec) { // Placed first to avoid as much needless work as possible, handling all the other events. // Drained completely, because they are the highest priority events anyway. - to_run.append(&mut self.destroy); + self.destroy.drain_into(to_run); // Create events can be batched, as they are typically just for object creation - to_run.append(&mut self.create); + self.create.drain_into(to_run); // These typically do nothing and don't spawn any other events - can be batched. // Should be run only after all first renders have finished. @@ -215,52 +272,32 @@ impl Scheduler { // // Should be processed one at time, because they can spawn more create and rendered events // for their children. - // - // To be replaced with BTreeMap::pop_first once it is stable. - if let Some(r) = self - .render_first - .keys() - .next() - .cloned() - .and_then(|m| self.render_first.remove(&m)) - { + if let Some(r) = self.render_first.pop_topmost() { to_run.push(r); - } - - if !to_run.is_empty() { return; } - to_run.append(&mut self.props_update); + self.props_update.drain_into(to_run); // Priority rendering // // This is needed for hydration susequent render to fix node refs. - if let Some(r) = self - .render_priority - .keys() - .next() - .cloned() - .and_then(|m| self.render_priority.remove(&m)) - { + if let Some(r) = self.render_priority.pop_topmost() { to_run.push(r); return; } - if !self.rendered_first.is_empty() { - let rendered_first = std::mem::take(&mut self.rendered_first); - // Children rendered lifecycle happen before parents. - to_run.extend(rendered_first.into_values().rev()); - } + // Children rendered lifecycle happen before parents. + self.rendered_first.drain_post_order_into(to_run); // Updates are after the first render to ensure we always have the entire child tree // rendered, once an update is processed. // // Can be batched, as they can cause only non-first renders. - to_run.append(&mut self.update); + self.update.drain_into(to_run); // Likely to cause duplicate renders via component updates, so placed before them - to_run.append(&mut self.main); + self.main.drain_into(to_run); // Run after all possible updates to avoid duplicate renders. // @@ -270,30 +307,16 @@ impl Scheduler { return; } - // To be replaced with BTreeMap::pop_first once it is stable. // Should be processed one at time, because they can spawn more create and rendered events // for their children. - if let Some(r) = self - .render - .keys() - .next() - .cloned() - .and_then(|m| self.render.remove(&m)) - { + if let Some(r) = self.render.pop_topmost() { to_run.push(r); + return; } - // These typically do nothing and don't spawn any other events - can be batched. // Should be run only after all renders have finished. - if !to_run.is_empty() { - return; - } - - if !self.rendered.is_empty() { - let rendered = std::mem::take(&mut self.rendered); - // Children rendered lifecycle happen before parents. - to_run.extend(rendered.into_values().rev()); - } + // Children rendered lifecycle happen before parents. + self.rendered.drain_post_order_into(to_run); } } diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index 84478c97617..7086349a194 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -1,10 +1,13 @@ use std::fmt; +use std::future::Future; +use futures::pin_mut; use futures::stream::{Stream, StreamExt}; +use tracing::Instrument; use crate::html::{BaseComponent, Scope}; -use crate::platform::fmt::{BufWriter, DEFAULT_BUF_SIZE}; -use crate::platform::{spawn_local, Runtime}; +use crate::platform::fmt::BufStream; +use crate::platform::{LocalHandle, Runtime}; /// A Yew Server-side Renderer that renders on the current thread. /// @@ -12,11 +15,10 @@ use crate::platform::{spawn_local, Runtime}; /// /// This renderer does not spawn its own runtime and can only be used when: /// -/// - `wasm-bindgen` is selected as the backend of Yew runtime. -/// - running within `actix_rt`. -/// - running within a [`LocalRuntime`](crate::platform::LocalRuntime). -/// - running within a tokio [`LocalSet`](tokio::task::LocalSet). -#[cfg_attr(documenting, doc(cfg(feature = "ssr")))] +/// - `wasm-bindgen-futures` is selected as the backend of Yew runtime. +/// - running within a [`Runtime`](crate::platform::Runtime). +/// - running within a tokio [`LocalSet`](struct@tokio::task::LocalSet). +#[cfg(feature = "ssr")] #[derive(Debug)] pub struct LocalServerRenderer where @@ -24,7 +26,6 @@ where { props: COMP::Properties, hydratable: bool, - capacity: usize, } impl Default for LocalServerRenderer @@ -57,19 +58,9 @@ where Self { props, hydratable: true, - capacity: DEFAULT_BUF_SIZE, } } - /// Sets the capacity of renderer buffer. - /// - /// Default: `8192` - pub fn capacity(mut self, capacity: usize) -> Self { - self.capacity = capacity; - - self - } - /// Sets whether an the rendered result is hydratable. /// /// Defaults to `true`. @@ -84,53 +75,80 @@ where /// Renders Yew Application. pub async fn render(self) -> String { - let mut s = String::new(); - - self.render_to_string(&mut s).await; + let s = self.render_stream(); + futures::pin_mut!(s); - s + s.collect().await } /// Renders Yew Application to a String. pub async fn render_to_string(self, w: &mut String) { - let mut s = self.render_stream(); + let s = self.render_stream(); + futures::pin_mut!(s); while let Some(m) = s.next().await { w.push_str(&m); } } - /// Renders Yew Applications into a string Stream - pub fn render_stream(self) -> impl Stream { - let (tx, rx) = crate::platform::pinned::mpsc::unbounded(); - - let mut w = BufWriter::new(tx, self.capacity); - + fn render_stream_inner(self) -> impl Stream { let scope = Scope::::new(None); - spawn_local(async move { + + let outer_span = tracing::Span::current(); + BufStream::new(move |mut w| async move { + let render_span = tracing::debug_span!("render_stream_item"); + render_span.follows_from(outer_span); scope .render_into_stream(&mut w, self.props.into(), self.hydratable) + .instrument(render_span) .await; - }); + }) + } - rx + // The duplicate implementation below is to selectively suppress clippy lints. + // These implementations should be merged once https://github.com/tokio-rs/tracing/issues/2503 is resolved. + + /// Renders Yew Application into a string Stream + #[rustversion::since(1.70)] + #[allow(clippy::let_with_type_underscore)] + #[tracing::instrument( + level = tracing::Level::DEBUG, + name = "render_stream", + skip(self), + fields(hydratable = self.hydratable), + )] + #[inline(always)] + pub fn render_stream(self) -> impl Stream { + self.render_stream_inner() + } + + /// Renders Yew Application into a string Stream + #[rustversion::before(1.70)] + #[tracing::instrument( + level = tracing::Level::DEBUG, + name = "render_stream", + skip(self), + fields(hydratable = self.hydratable), + )] + #[inline(always)] + pub fn render_stream(self) -> impl Stream { + self.render_stream_inner() } } /// A Yew Server-side Renderer. /// -/// This renderer spawns the rendering task to an internal worker pool and receives result when +/// This renderer spawns the rendering task to a Yew [`Runtime`]. and receives result when /// the rendering process has finished. /// /// See [`yew::platform`] for more information. -#[cfg_attr(documenting, doc(cfg(feature = "ssr")))] +#[cfg(feature = "ssr")] pub struct ServerRenderer where COMP: BaseComponent, { create_props: Box COMP::Properties>, hydratable: bool, - capacity: usize, rt: Option, } @@ -181,7 +199,6 @@ where Self { create_props: Box::new(create_props), hydratable: true, - capacity: DEFAULT_BUF_SIZE, rt: None, } } @@ -193,15 +210,6 @@ where self } - /// Sets the capacity of renderer buffer. - /// - /// Default: `8192` - pub fn capacity(mut self, capacity: usize) -> Self { - self.capacity = capacity; - - self - } - /// Sets whether an the rendered result is hydratable. /// /// Defaults to `true`. @@ -216,53 +224,78 @@ where /// Renders Yew Application. pub async fn render(self) -> String { - let mut s = String::new(); + let Self { + create_props, + hydratable, + rt, + } = self; - self.render_to_string(&mut s).await; + let (tx, rx) = futures::channel::oneshot::channel(); + let create_task = move || async move { + let props = create_props(); + let s = LocalServerRenderer::::with_props(props) + .hydratable(hydratable) + .render() + .await; + + let _ = tx.send(s); + }; - s + Self::spawn_rendering_task(rt, create_task); + + rx.await.expect("failed to render application") } /// Renders Yew Application to a String. pub async fn render_to_string(self, w: &mut String) { - let mut s = self.render_stream().await; + let mut s = self.render_stream(); while let Some(m) = s.next().await { w.push_str(&m); } } - /// Renders Yew Applications into a string Stream. - /// - /// # Note - /// - /// Unlike [`LocalServerRenderer::render_stream`], this method is `async fn`. - pub async fn render_stream(self) -> impl Send + Stream { + #[inline] + fn spawn_rendering_task(rt: Option, create_task: F) + where + F: 'static + Send + FnOnce() -> Fut, + Fut: Future + 'static, + { + match rt { + // If a runtime is specified, spawn to the specified runtime. + Some(m) => m.spawn_pinned(create_task), + None => match LocalHandle::try_current() { + // If within a Yew Runtime, spawn to the current runtime. + Some(m) => m.spawn_local(create_task()), + // Outside of Yew Runtime, spawn to the default runtime. + None => Runtime::default().spawn_pinned(create_task), + }, + } + } + + /// Renders Yew Application into a string Stream. + pub fn render_stream(self) -> impl Send + Stream { let Self { create_props, hydratable, - capacity, rt, } = self; - let rt = rt.unwrap_or_default(); - - // We use run_pinned to switch to our runtime. - rt.run_pinned(move || async move { + let (tx, rx) = futures::channel::mpsc::unbounded(); + let create_task = move || async move { let props = create_props(); - let scope = Scope::::new(None); + let s = LocalServerRenderer::::with_props(props) + .hydratable(hydratable) + .render_stream(); + pin_mut!(s); - let (tx, rx) = futures::channel::mpsc::unbounded(); - let mut w = BufWriter::new(tx, capacity); + while let Some(m) = s.next().await { + let _ = tx.unbounded_send(m); + } + }; - spawn_local(async move { - scope - .render_into_stream(&mut w, props.into(), hydratable) - .await; - }); + Self::spawn_rendering_task(rt, create_task); - rx - }) - .await + rx } } diff --git a/packages/yew/src/suspense/component.rs b/packages/yew/src/suspense/component.rs index ebe9dcc1dc5..17c68053fea 100644 --- a/packages/yew/src/suspense/component.rs +++ b/packages/yew/src/suspense/component.rs @@ -1,11 +1,11 @@ -use crate::html::{Children, Html, Properties}; +use crate::html::{Html, Properties}; /// Properties for [Suspense]. #[derive(Properties, PartialEq, Debug, Clone)] pub struct SuspenseProps { /// The Children of the current Suspense Component. #[prop_or_default] - pub children: Children, + pub children: Html, /// The Fallback UI of the current Suspense Component. #[prop_or_default] @@ -15,7 +15,7 @@ pub struct SuspenseProps { #[cfg(any(feature = "csr", feature = "ssr"))] mod feat_csr_ssr { use super::*; - use crate::html::{Children, Component, Context, Html, Scope}; + use crate::html::{Component, Context, Html, Scope}; use crate::suspense::Suspension; #[cfg(feature = "hydration")] use crate::suspense::SuspensionHandle; @@ -24,7 +24,7 @@ mod feat_csr_ssr { #[derive(Properties, PartialEq, Debug, Clone)] pub(crate) struct BaseSuspenseProps { - pub children: Children, + pub children: Html, pub fallback: Option, } @@ -36,7 +36,6 @@ mod feat_csr_ssr { #[derive(Debug)] pub(crate) struct BaseSuspense { - link: Scope, suspensions: Vec, #[cfg(feature = "hydration")] hydration_handle: Option, @@ -46,7 +45,7 @@ mod feat_csr_ssr { type Message = BaseSuspenseMsg; type Properties = BaseSuspenseProps; - fn create(ctx: &Context) -> Self { + fn create(_ctx: &Context) -> Self { #[cfg(not(feature = "hydration"))] let suspensions = Vec::new(); @@ -56,9 +55,9 @@ mod feat_csr_ssr { use crate::callback::Callback; use crate::html::RenderMode; - match ctx.creation_mode() { + match _ctx.creation_mode() { RenderMode::Hydration => { - let link = ctx.link().clone(); + let link = _ctx.link().clone(); let (s, handle) = Suspension::new(); s.listen(Callback::from(move |s| { link.send_message(BaseSuspenseMsg::Resume(s)); @@ -70,7 +69,6 @@ mod feat_csr_ssr { }; Self { - link: ctx.link().clone(), suspensions, #[cfg(feature = "hydration")] hydration_handle, @@ -89,6 +87,11 @@ mod feat_csr_ssr { return false; } + // If a suspension already exists, ignore it. + if self.suspensions.iter().any(|n| n == &m) { + return false; + } + self.suspensions.push(m); true @@ -133,12 +136,12 @@ mod feat_csr_ssr { } impl BaseSuspense { - pub(crate) fn suspend(&self, s: Suspension) { - self.link.send_message(BaseSuspenseMsg::Suspend(s)); + pub(crate) fn suspend(scope: &Scope, s: Suspension) { + scope.send_message(BaseSuspenseMsg::Suspend(s)); } - pub(crate) fn resume(&self, s: Suspension) { - self.link.send_message(BaseSuspenseMsg::Resume(s)); + pub(crate) fn resume(scope: &Scope, s: Suspension) { + scope.send_message(BaseSuspenseMsg::Resume(s)); } } @@ -148,7 +151,7 @@ mod feat_csr_ssr { let SuspenseProps { children, fallback } = props.clone(); let fallback = html! { - + {fallback} }; diff --git a/packages/yew/src/suspense/hooks.rs b/packages/yew/src/suspense/hooks.rs index 611f6ea2535..04a5e51c722 100644 --- a/packages/yew/src/suspense/hooks.rs +++ b/packages/yew/src/suspense/hooks.rs @@ -19,7 +19,7 @@ impl Deref for UseFutureHandle { type Target = O; fn deref(&self) -> &Self::Target { - &*self.inner.as_ref().unwrap() + self.inner.as_ref().unwrap() } } @@ -44,7 +44,7 @@ impl fmt::Debug for UseFutureHandle { /// ``` /// # use yew::prelude::*; /// # use yew::suspense::use_future; -/// use gloo_net::http::Request; +/// use gloo::net::http::Request; /// /// const URL: &str = "https://en.wikipedia.org/w/api.php?\ /// action=query&origin=*&format=json&generator=search&\ @@ -52,7 +52,7 @@ impl fmt::Debug for UseFutureHandle { /// /// #[function_component] /// fn WikipediaSearch() -> HtmlResult { -/// let res = use_future(|| async { Request::new(URL).send().await?.text().await })?; +/// let res = use_future(|| async { Request::get(URL).send().await?.text().await })?; /// let result_html = match *res { /// Ok(ref res) => html! { res }, /// Err(ref failure) => failure.to_string().into(), diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs index 2b0ee690b07..657cc96a9d5 100644 --- a/packages/yew/src/tests/layout_tests.rs +++ b/packages/yew/src/tests/layout_tests.rs @@ -2,9 +2,8 @@ //! //! This tests must be run in browser and thus require the `csr` feature to be enabled use gloo::console::log; -use yew::NodeRef; -use crate::dom_bundle::{BSubtree, Bundle}; +use crate::dom_bundle::{BSubtree, Bundle, DomSlot}; use crate::html::AnyScope; use crate::virtual_dom::VNode; use crate::{scheduler, Component, Context, Html}; @@ -22,7 +21,7 @@ impl Component for Comp { unimplemented!(); } - fn changed(&mut self, _ctx: &Context) -> bool { + fn changed(&mut self, _ctx: &Context, _: &Self::Properties) -> bool { unimplemented!() } @@ -39,7 +38,7 @@ pub struct TestLayout<'a> { } pub fn diff_layouts(layouts: Vec>) { - let document = gloo_utils::document(); + let document = gloo::utils::document(); let scope: AnyScope = AnyScope::test(); let parent_element = document.create_element("div").unwrap(); let root = BSubtree::create_root(&parent_element); @@ -48,14 +47,14 @@ pub fn diff_layouts(layouts: Vec>) { parent_element.append_child(&end_node).unwrap(); // Tests each layout independently - let next_sibling = NodeRef::new(end_node.into()); + let slot = DomSlot::at(end_node.into()); for layout in layouts.iter() { // Apply the layout let vnode = layout.node.clone(); log!("Independently apply layout '{}'", layout.name); let mut bundle = Bundle::new(); - bundle.reconcile(&root, &scope, &parent_element, next_sibling.clone(), vnode); + bundle.reconcile(&root, &scope, &parent_element, slot.clone(), vnode); scheduler::start_now(); assert_eq!( parent_element.inner_html(), @@ -69,7 +68,7 @@ pub fn diff_layouts(layouts: Vec>) { log!("Independently reapply layout '{}'", layout.name); - bundle.reconcile(&root, &scope, &parent_element, next_sibling.clone(), vnode); + bundle.reconcile(&root, &scope, &parent_element, slot.clone(), vnode); scheduler::start_now(); assert_eq!( parent_element.inner_html(), @@ -95,13 +94,7 @@ pub fn diff_layouts(layouts: Vec>) { let next_vnode = layout.node.clone(); log!("Sequentially apply layout '{}'", layout.name); - bundle.reconcile( - &root, - &scope, - &parent_element, - next_sibling.clone(), - next_vnode, - ); + bundle.reconcile(&root, &scope, &parent_element, slot.clone(), next_vnode); scheduler::start_now(); assert_eq!( @@ -117,13 +110,7 @@ pub fn diff_layouts(layouts: Vec>) { let next_vnode = layout.node.clone(); log!("Sequentially detach layout '{}'", layout.name); - bundle.reconcile( - &root, - &scope, - &parent_element, - next_sibling.clone(), - next_vnode, - ); + bundle.reconcile(&root, &scope, &parent_element, slot.clone(), next_vnode); scheduler::start_now(); assert_eq!( diff --git a/packages/yew/src/utils/mod.rs b/packages/yew/src/utils/mod.rs index e5b999e72c5..98934ed054b 100644 --- a/packages/yew/src/utils/mod.rs +++ b/packages/yew/src/utils/mod.rs @@ -4,7 +4,7 @@ use std::marker::PhantomData; use yew::html::ChildrenRenderer; -/// Map IntoIterator> to Iterator +/// Map `IntoIterator>` to `Iterator` pub fn into_node_iter(it: IT) -> impl Iterator where IT: IntoIterator, @@ -19,25 +19,25 @@ pub struct NodeSeq(Vec, PhantomData); impl, OUT> From for NodeSeq { fn from(val: IN) -> Self { - Self(vec![val.into()], PhantomData::default()) + Self(vec![val.into()], PhantomData) + } +} + +impl, OUT> From> for NodeSeq { + fn from(val: Option) -> Self { + Self(val.map(|s| vec![s.into()]).unwrap_or_default(), PhantomData) } } impl, OUT> From> for NodeSeq { fn from(val: Vec) -> Self { - Self( - val.into_iter().map(|x| x.into()).collect(), - PhantomData::default(), - ) + Self(val.into_iter().map(|x| x.into()).collect(), PhantomData) } } -impl, OUT> From> for NodeSeq { - fn from(val: ChildrenRenderer) -> Self { - Self( - val.into_iter().map(|x| x.into()).collect(), - PhantomData::default(), - ) +impl + Clone, OUT> From<&ChildrenRenderer> for NodeSeq { + fn from(val: &ChildrenRenderer) -> Self { + Self(val.iter().map(|x| x.into()).collect(), PhantomData) } } diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index 99085f73bea..940c46021f9 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -13,6 +13,8 @@ pub mod vnode; #[doc(hidden)] pub mod vportal; #[doc(hidden)] +pub mod vraw; +#[doc(hidden)] pub mod vsuspense; #[doc(hidden)] pub mod vtag; @@ -36,6 +38,8 @@ pub use self::vnode::VNode; #[doc(inline)] pub use self::vportal::VPortal; #[doc(inline)] +pub use self::vraw::VRaw; +#[doc(inline)] pub use self::vsuspense::VSuspense; #[doc(inline)] pub use self::vtag::VTag; @@ -60,21 +64,32 @@ mod feat_ssr_hydration { /// This indicates a kind that can be collected from fragment to be processed at a later time pub enum Collectable { Component(ComponentName), + Raw, Suspense, } impl Collectable { + #[cfg(not(debug_assertions))] + #[inline(always)] + pub fn for_component() -> Self { + use std::marker::PhantomData; + // This suppresses the clippy lint about unused generic. + // We inline this function + // so the function body is copied to its caller and generics get optimised away. + let _comp_type: PhantomData = PhantomData; + Self::Component(PhantomData) + } + + #[cfg(debug_assertions)] pub fn for_component() -> Self { - #[cfg(debug_assertions)] let comp_name = std::any::type_name::(); - #[cfg(not(debug_assertions))] - let comp_name = std::marker::PhantomData; Self::Component(comp_name) } pub fn open_start_mark(&self) -> &'static str { match self { Self::Component(_) => "<[", + Self::Raw => "<#", Self::Suspense => " &'static str { match self { Self::Component(_) => " " " &'static str { match self { Self::Component(_) => "]>", + Self::Raw => ">", Self::Suspense => ">", } } @@ -97,9 +114,10 @@ mod feat_ssr_hydration { pub fn name(&self) -> Cow<'static, str> { match self { #[cfg(debug_assertions)] - Self::Component(m) => format!("Component({})", m).into(), + Self::Component(m) => format!("Component({m})").into(), #[cfg(not(debug_assertions))] Self::Component(_) => "Component".into(), + Self::Raw => "Raw".into(), Self::Suspense => "Suspense".into(), } } @@ -111,40 +129,56 @@ pub(crate) use feat_ssr_hydration::*; #[cfg(feature = "ssr")] mod feat_ssr { + use std::fmt::Write; + use super::*; - use crate::platform::fmt::BufWrite; + use crate::platform::fmt::BufWriter; impl Collectable { - pub(crate) fn write_open_tag(&self, w: &mut dyn BufWrite) { - w.write("".into()); + let _ = w.write_str(self.end_mark()); + let _ = w.write_str("-->"); } - pub(crate) fn write_close_tag(&self, w: &mut dyn BufWrite) { - w.write("".into()); + let _ = w.write_str(self.end_mark()); + let _ = w.write_str("-->"); } } } +/// Defines if the [`Attributes`] is set as element's attribute or property +#[allow(missing_docs)] +#[derive(PartialEq, Eq, Copy, Clone, Debug)] +pub enum ApplyAttributeAs { + Attribute, + Property, +} + /// A collection of attributes for an element #[derive(PartialEq, Eq, Clone, Debug)] pub enum Attributes { @@ -152,7 +186,7 @@ pub enum Attributes { /// /// Allows optimizing comparison to a simple pointer equality check and reducing allocations, /// if the attributes do not change on a node. - Static(&'static [[&'static str; 2]]), + Static(&'static [(&'static str, &'static str, ApplyAttributeAs)]), /// Static list of attribute keys with possibility to exclude attributes and dynamic attribute /// values. @@ -165,12 +199,12 @@ pub enum Attributes { /// Attribute values. Matches [keys](Attributes::Dynamic::keys). Optional attributes are /// designated by setting [None]. - values: Box<[Option]>, + values: Box<[Option<(AttrValue, ApplyAttributeAs)>]>, }, /// IndexMap is used to provide runtime attribute deduplication in cases where the html! macro /// was not used to guarantee it. - IndexMap(IndexMap), + IndexMap(IndexMap), } impl Attributes { @@ -183,19 +217,19 @@ impl Attributes { /// This function is suboptimal and does not inline well. Avoid on hot paths. pub fn iter<'a>(&'a self) -> Box + 'a> { match self { - Self::Static(arr) => Box::new(arr.iter().map(|kv| (kv[0], kv[1] as &'a str))), + Self::Static(arr) => Box::new(arr.iter().map(|(k, v, _)| (*k, *v as &'a str))), Self::Dynamic { keys, values } => Box::new( keys.iter() .zip(values.iter()) - .filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref()))), + .filter_map(|(k, v)| v.as_ref().map(|(v, _)| (*k, v.as_ref()))), ), - Self::IndexMap(m) => Box::new(m.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))), + Self::IndexMap(m) => Box::new(m.iter().map(|(k, (v, _))| (k.as_ref(), v.as_ref()))), } } /// Get a mutable reference to the underlying `IndexMap`. /// If the attributes are stored in the `Vec` variant, it will be converted. - pub fn get_mut_index_map(&mut self) -> &mut IndexMap { + pub fn get_mut_index_map(&mut self) -> &mut IndexMap { macro_rules! unpack { () => { match self { @@ -209,7 +243,11 @@ impl Attributes { match self { Self::IndexMap(m) => m, Self::Static(arr) => { - *self = Self::IndexMap(arr.iter().map(|kv| (kv[0].into(), kv[1].into())).collect()); + *self = Self::IndexMap( + arr.iter() + .map(|(k, v, ty)| ((*k).into(), ((*v).into(), *ty))) + .collect(), + ); unpack!() } Self::Dynamic { keys, values } => { @@ -227,7 +265,11 @@ impl Attributes { } impl From> for Attributes { - fn from(v: IndexMap) -> Self { + fn from(map: IndexMap) -> Self { + let v = map + .into_iter() + .map(|(k, v)| (k, (v, ApplyAttributeAs::Attribute))) + .collect(); Self::IndexMap(v) } } @@ -236,7 +278,7 @@ impl From> for Attributes { fn from(v: IndexMap<&'static str, AttrValue>) -> Self { let v = v .into_iter() - .map(|(k, v)| (AttrValue::Static(k), v)) + .map(|(k, v)| (AttrValue::Static(k), (v, ApplyAttributeAs::Attribute))) .collect(); Self::IndexMap(v) } diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 2bd555a84d1..6d533db4371 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -1,6 +1,6 @@ //! This module contains the implementation of a virtual component (`VComp`). -use std::any::TypeId; +use std::any::{Any, TypeId}; use std::fmt; use std::rc::Rc; @@ -10,31 +10,31 @@ use futures::future::{FutureExt, LocalBoxFuture}; use web_sys::Element; use super::Key; -#[cfg(feature = "csr")] -use crate::dom_bundle::BSubtree; #[cfg(feature = "hydration")] use crate::dom_bundle::Fragment; #[cfg(feature = "csr")] +use crate::dom_bundle::{BSubtree, DomSlot, DynamicDomSlot}; +use crate::html::BaseComponent; +#[cfg(feature = "csr")] use crate::html::Scoped; #[cfg(any(feature = "ssr", feature = "csr"))] use crate::html::{AnyScope, Scope}; -use crate::html::{BaseComponent, NodeRef}; #[cfg(feature = "ssr")] -use crate::platform::fmt::BufWrite; +use crate::platform::fmt::BufWriter; /// A virtual component. pub struct VComp { pub(crate) type_id: TypeId, pub(crate) mountable: Box, - pub(crate) node_ref: NodeRef, pub(crate) key: Option, + // for some reason, this reduces the bundle size by ~2-3 KBs + _marker: u32, } impl fmt::Debug for VComp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("VComp") .field("type_id", &self.type_id) - .field("node_ref", &self.node_ref) .field("mountable", &"..") .field("key", &self.key) .finish() @@ -46,8 +46,8 @@ impl Clone for VComp { Self { type_id: self.type_id, mountable: self.mountable.copy(), - node_ref: self.node_ref.clone(), key: self.key.clone(), + _marker: 0, } } } @@ -55,23 +55,26 @@ impl Clone for VComp { pub(crate) trait Mountable { fn copy(&self) -> Box; + fn mountable_eq(&self, rhs: &dyn Mountable) -> bool; + fn as_any(&self) -> &dyn Any; + #[cfg(feature = "csr")] fn mount( self: Box, root: &BSubtree, parent_scope: &AnyScope, parent: Element, - internal_ref: NodeRef, - next_sibling: NodeRef, + slot: DomSlot, + internal_ref: DynamicDomSlot, ) -> Box; #[cfg(feature = "csr")] - fn reuse(self: Box, scope: &dyn Scoped, next_sibling: NodeRef); + fn reuse(self: Box, scope: &dyn Scoped, slot: DomSlot); #[cfg(feature = "ssr")] fn render_into_stream<'a>( &'a self, - w: &'a mut dyn BufWrite, + w: &'a mut BufWriter, parent_scope: &'a AnyScope, hydratable: bool, ) -> LocalBoxFuture<'a, ()>; @@ -82,7 +85,7 @@ pub(crate) trait Mountable { root: BSubtree, parent_scope: &AnyScope, parent: Element, - internal_ref: NodeRef, + internal_ref: DynamicDomSlot, fragment: &mut Fragment, ) -> Box; } @@ -105,31 +108,42 @@ impl Mountable for PropsWrapper { Box::new(wrapper) } + fn as_any(&self) -> &dyn Any { + self + } + + fn mountable_eq(&self, rhs: &dyn Mountable) -> bool { + rhs.as_any() + .downcast_ref::() + .map(|rhs| self.props == rhs.props) + .unwrap_or(false) + } + #[cfg(feature = "csr")] fn mount( self: Box, root: &BSubtree, parent_scope: &AnyScope, parent: Element, - internal_ref: NodeRef, - next_sibling: NodeRef, + slot: DomSlot, + internal_ref: DynamicDomSlot, ) -> Box { let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.mount_in_place(root.clone(), parent, next_sibling, internal_ref, self.props); + scope.mount_in_place(root.clone(), parent, slot, internal_ref, self.props); Box::new(scope) } #[cfg(feature = "csr")] - fn reuse(self: Box, scope: &dyn Scoped, next_sibling: NodeRef) { + fn reuse(self: Box, scope: &dyn Scoped, slot: DomSlot) { let scope: Scope = scope.to_any().downcast::(); - scope.reuse(self.props, next_sibling); + scope.reuse(self.props, slot); } #[cfg(feature = "ssr")] fn render_into_stream<'a>( &'a self, - w: &'a mut dyn BufWrite, + w: &'a mut BufWriter, parent_scope: &'a AnyScope, hydratable: bool, ) -> LocalBoxFuture<'a, ()> { @@ -149,7 +163,7 @@ impl Mountable for PropsWrapper { root: BSubtree, parent_scope: &AnyScope, parent: Element, - internal_ref: NodeRef, + internal_ref: DynamicDomSlot, fragment: &mut Fragment, ) -> Box { let scope: Scope = Scope::new(Some(parent_scope.clone())); @@ -164,7 +178,6 @@ pub struct VChild { /// The component properties pub props: Rc, /// Reference to the mounted node - node_ref: NodeRef, key: Option, } @@ -172,7 +185,6 @@ impl Clone for VChild { fn clone(&self) -> Self { VChild { props: Rc::clone(&self.props), - node_ref: self.node_ref.clone(), key: self.key.clone(), } } @@ -192,10 +204,9 @@ where COMP: BaseComponent, { /// Creates a child component that can be accessed and modified by its parent. - pub fn new(props: COMP::Properties, node_ref: NodeRef, key: Option) -> Self { + pub fn new(props: COMP::Properties, key: Option) -> Self { Self { props: Rc::new(props), - node_ref, key, } } @@ -206,28 +217,30 @@ where COMP: BaseComponent, { fn from(vchild: VChild) -> Self { - VComp::new::(vchild.props, vchild.node_ref, vchild.key) + VComp::new::(vchild.props, vchild.key) } } impl VComp { /// Creates a new `VComp` instance. - pub fn new(props: Rc, node_ref: NodeRef, key: Option) -> Self + pub fn new(props: Rc, key: Option) -> Self where COMP: BaseComponent, { VComp { type_id: TypeId::of::(), - node_ref, mountable: Box::new(PropsWrapper::::new(props)), key, + _marker: 0, } } } impl PartialEq for VComp { fn eq(&self, other: &VComp) -> bool { - self.type_id == other.type_id + self.key == other.key + && self.type_id == other.type_id + && self.mountable.mountable_eq(other.mountable.as_ref()) } } @@ -246,7 +259,7 @@ mod feat_ssr { #[inline] pub(crate) async fn render_into_stream( &self, - w: &mut dyn BufWrite, + w: &mut BufWriter, parent_scope: &AnyScope, hydratable: bool, ) { diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index b5dea7e086f..ed451c48b0c 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1,5 +1,6 @@ //! This module contains fragments implementation. use std::ops::{Deref, DerefMut}; +use std::rc::Rc; use super::{Key, VNode}; @@ -14,7 +15,7 @@ enum FullyKeyedState { #[derive(Clone, Debug)] pub struct VList { /// The list of child [VNode]s - pub(crate) children: Vec, + pub(crate) children: Option>>, /// All [VNode]s in the VList have keys fully_keyed: FullyKeyedState, @@ -24,7 +25,7 @@ pub struct VList { impl PartialEq for VList { fn eq(&self, other: &Self) -> bool { - self.children == other.children && self.key == other.key + self.key == other.key && self.children == other.children } } @@ -38,14 +39,22 @@ impl Deref for VList { type Target = Vec; fn deref(&self) -> &Self::Target { - &self.children + match self.children { + Some(ref m) => m, + None => { + // This is mutable because the Vec is not Sync + static mut EMPTY: Vec = Vec::new(); + // SAFETY: The EMPTY value is always read-only + unsafe { &EMPTY } + } + } } } impl DerefMut for VList { fn deref_mut(&mut self) -> &mut Self::Target { self.fully_keyed = FullyKeyedState::Unknown; - &mut self.children + self.children_mut() } } @@ -53,7 +62,7 @@ impl VList { /// Creates a new empty [VList] instance. pub const fn new() -> Self { Self { - children: Vec::new(), + children: None, key: None, fully_keyed: FullyKeyedState::KnownFullyKeyed, } @@ -63,30 +72,40 @@ impl VList { pub fn with_children(children: Vec, key: Option) -> Self { let mut vlist = VList { fully_keyed: FullyKeyedState::Unknown, - children, + children: Some(Rc::new(children)), key, }; - vlist.fully_keyed = if vlist.fully_keyed() { - FullyKeyedState::KnownFullyKeyed - } else { - FullyKeyedState::KnownMissingKeys - }; + vlist.recheck_fully_keyed(); vlist } + // Returns a mutable reference to children, allocates the children if it hasn't been done. + // + // This method does not reassign key state. So it should only be used internally. + fn children_mut(&mut self) -> &mut Vec { + loop { + match self.children { + Some(ref mut m) => return Rc::make_mut(m), + None => { + self.children = Some(Rc::new(Vec::new())); + } + } + } + } + /// Add [VNode] child. pub fn add_child(&mut self, child: VNode) { if self.fully_keyed == FullyKeyedState::KnownFullyKeyed && !child.has_key() { self.fully_keyed = FullyKeyedState::KnownMissingKeys; } - self.children.push(child); + self.children_mut().push(child); } /// Add multiple [VNode] children. pub fn add_children(&mut self, children: impl IntoIterator) { let it = children.into_iter(); let bound = it.size_hint(); - self.children.reserve(bound.1.unwrap_or(bound.0)); + self.children_mut().reserve(bound.1.unwrap_or(bound.0)); for ch in it { self.add_child(ch); } @@ -108,7 +127,7 @@ impl VList { match self.fully_keyed { FullyKeyedState::KnownFullyKeyed => true, FullyKeyedState::KnownMissingKeys => false, - FullyKeyedState::Unknown => self.children.iter().all(|c| c.has_key()), + FullyKeyedState::Unknown => self.iter().all(|c| c.has_key()), } } } @@ -156,49 +175,89 @@ mod test { #[cfg(feature = "ssr")] mod feat_ssr { - use futures::stream::{FuturesOrdered, StreamExt}; + use std::fmt::Write; + use std::task::Poll; + + use futures::stream::StreamExt; + use futures::{join, pin_mut, poll, FutureExt}; use super::*; use crate::html::AnyScope; - use crate::platform::fmt::{BufWrite, BufWriter}; - use crate::platform::pinned::mpsc; + use crate::platform::fmt::{self, BufWriter}; impl VList { pub(crate) async fn render_into_stream( &self, - w: &mut dyn BufWrite, + w: &mut BufWriter, parent_scope: &AnyScope, hydratable: bool, ) { - match &self.children[..] { + match &self[..] { [] => {} [child] => { child.render_into_stream(w, parent_scope, hydratable).await; } _ => { - let buf_capacity = w.capacity(); - - // Concurrently render all children. - let mut children: FuturesOrdered<_> = self - .children - .iter() - .map(|m| async move { - let (tx, rx) = mpsc::unbounded(); - - let mut w = BufWriter::new(tx, buf_capacity); - - m.render_into_stream(&mut w, parent_scope, hydratable).await; - drop(w); - - rx - }) - .collect(); - - while let Some(mut r) = children.next().await { - while let Some(next_chunk) = r.next().await { - w.write(next_chunk.into()); + async fn render_child_iter<'a, I>( + mut children: I, + w: &mut BufWriter, + parent_scope: &AnyScope, + hydratable: bool, + ) where + I: Iterator, + { + let mut w = w; + while let Some(m) = children.next() { + let child_fur = async move { + // Rust's Compiler does not release the mutable reference to + // BufWriter until the end of the loop, regardless of whether an + // await statement has dropped the child_fur. + // + // We capture and return the mutable reference to avoid this. + + m.render_into_stream(w, parent_scope, hydratable).await; + w + }; + pin_mut!(child_fur); + + match poll!(child_fur.as_mut()) { + Poll::Pending => { + let (mut next_w, next_r) = fmt::buffer(); + // Move buf writer into an async block for it to be dropped at + // the end of the future. + let rest_render_fur = async move { + render_child_iter( + children, + &mut next_w, + parent_scope, + hydratable, + ) + .await; + } + // boxing to avoid recursion + .boxed_local(); + + let transfer_fur = async move { + let w = child_fur.await; + + pin_mut!(next_r); + while let Some(m) = next_r.next().await { + let _ = w.write_str(m.as_str()); + } + }; + + join!(rest_render_fur, transfer_fur); + break; + } + Poll::Ready(w_) => { + w = w_; + } + } } } + + let children = self.iter(); + render_child_iter(children, w, parent_scope, hydratable).await; } } } diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 0d3145bb206..87b571f8b70 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -1,16 +1,18 @@ //! This module contains the implementation of abstract virtual node. use std::cmp::PartialEq; -use std::fmt; use std::iter::FromIterator; +use std::{fmt, mem}; use web_sys::Node; use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText}; use crate::html::BaseComponent; +use crate::virtual_dom::VRaw; +use crate::AttrValue; /// Bind virtual element to a DOM reference. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub enum VNode { /// A bind between `VTag` and `Element`. VTag(Box), @@ -26,6 +28,10 @@ pub enum VNode { VRef(Node), /// A suspendible document fragment. VSuspense(VSuspense), + /// A raw HTML string, represented by [`AttrValue`](crate::AttrValue). + /// + /// Also see: [`VNode::from_html_unchecked`] + VRaw(VRaw), } impl VNode { @@ -38,6 +44,7 @@ impl VNode { VNode::VText(_) => None, VNode::VPortal(vportal) => vportal.node.key(), VNode::VSuspense(vsuspense) => vsuspense.key.as_ref(), + VNode::VRaw(_) => None, } } @@ -45,6 +52,54 @@ impl VNode { pub fn has_key(&self) -> bool { self.key().is_some() } + + /// Acquires a mutable reference of current VNode as a VList. + /// + /// Creates a VList with the current node as the first child if current VNode is not a VList. + pub fn to_vlist_mut(&mut self) -> &mut VList { + loop { + match *self { + Self::VList(ref mut m) => return m, + _ => { + *self = VNode::VList(VList::with_children(vec![mem::take(self)], None)); + } + } + } + } + + /// Create a [`VNode`] from a string of HTML + /// + /// # Behavior in browser + /// + /// In the browser, this function creates an element, sets the passed HTML to its `innerHTML` + /// and inserts the contents of it into the DOM. + /// + /// # Behavior on server + /// + /// When rendering on the server, the contents of HTML are directly injected into the HTML + /// stream. + /// + /// ## Warning + /// + /// The contents are **not** sanitized or validated. You, as the developer, are responsible to + /// ensure the HTML string passed to this method are _valid_ and _not malicious_ + /// + /// # Example + /// + /// ```rust + /// use yew::{html, AttrValue, Html}; + /// # fn _main() { + /// let parsed = Html::from_html_unchecked(AttrValue::from("
    content
    ")); + /// let _: Html = html! { + ///
    + /// {parsed} + ///
    + /// }; + /// # } + /// ``` + pub fn from_html_unchecked(html: AttrValue) -> Self { + VNode::VRaw(VRaw { html }) + } } impl Default for VNode { @@ -129,20 +184,7 @@ impl fmt::Debug for VNode { VNode::VRef(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)), VNode::VPortal(ref vportal) => vportal.fmt(f), VNode::VSuspense(ref vsuspense) => vsuspense.fmt(f), - } - } -} - -impl PartialEq for VNode { - fn eq(&self, other: &VNode) -> bool { - match (self, other) { - (VNode::VTag(a), VNode::VTag(b)) => a == b, - (VNode::VText(a), VNode::VText(b)) => a == b, - (VNode::VList(a), VNode::VList(b)) => a == b, - (VNode::VRef(a), VNode::VRef(b)) => a == b, - // TODO: Need to improve PartialEq for VComp before enabling. - (VNode::VComp(_), VNode::VComp(_)) => false, - _ => false, + VNode::VRaw(ref vraw) => write!(f, "VRaw {{ {} }}", vraw.html), } } } @@ -153,18 +195,18 @@ mod feat_ssr { use super::*; use crate::html::AnyScope; - use crate::platform::fmt::BufWrite; + use crate::platform::fmt::BufWriter; impl VNode { pub(crate) fn render_into_stream<'a>( &'a self, - w: &'a mut dyn BufWrite, + w: &'a mut BufWriter, parent_scope: &'a AnyScope, hydratable: bool, ) -> LocalBoxFuture<'a, ()> { async fn render_into_stream_( this: &VNode, - w: &mut dyn BufWrite, + w: &mut BufWriter, parent_scope: &AnyScope, hydratable: bool, ) { @@ -194,6 +236,8 @@ mod feat_ssr { .render_into_stream(w, parent_scope, hydratable) .await } + + VNode::VRaw(vraw) => vraw.render_into_stream(w, parent_scope, hydratable).await, } } diff --git a/packages/yew/src/virtual_dom/vportal.rs b/packages/yew/src/virtual_dom/vportal.rs index 1e418a867a2..b0b7b301f5a 100644 --- a/packages/yew/src/virtual_dom/vportal.rs +++ b/packages/yew/src/virtual_dom/vportal.rs @@ -3,14 +3,13 @@ use web_sys::{Element, Node}; use super::VNode; -use crate::html::NodeRef; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct VPortal { /// The element under which the content is inserted. pub host: Element, /// The next sibling after the inserted content. Most be a child of `host`. - pub inner_sibling: NodeRef, + pub inner_sibling: Option, /// The inserted node pub node: Box, } @@ -20,22 +19,18 @@ impl VPortal { pub fn new(content: VNode, host: Element) -> Self { Self { host, - inner_sibling: NodeRef::default(), + inner_sibling: None, node: Box::new(content), } } /// Creates a [VPortal] rendering `content` in the DOM hierarchy under `host`. - /// If `next_sibling` is given, the content is inserted before that [Node]. - /// The parent of `next_sibling`, if given, must be `host`. + /// If `inner_sibling` is given, the content is inserted before that [Node]. + /// The parent of `inner_sibling`, if given, must be `host`. pub fn new_before(content: VNode, host: Element, inner_sibling: Option) -> Self { Self { host, - inner_sibling: { - let sib_ref = NodeRef::default(); - sib_ref.set(inner_sibling); - sib_ref - }, + inner_sibling, node: Box::new(content), } } diff --git a/packages/yew/src/virtual_dom/vraw.rs b/packages/yew/src/virtual_dom/vraw.rs new file mode 100644 index 00000000000..366e5248f00 --- /dev/null +++ b/packages/yew/src/virtual_dom/vraw.rs @@ -0,0 +1,44 @@ +use crate::AttrValue; + +/// A raw HTML string to be used in VDOM. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VRaw { + pub html: AttrValue, +} + +impl From for VRaw { + fn from(html: AttrValue) -> Self { + Self { html } + } +} + +#[cfg(feature = "ssr")] +mod feat_ssr { + use std::fmt::Write; + + use super::*; + use crate::html::AnyScope; + use crate::platform::fmt::BufWriter; + use crate::virtual_dom::Collectable; + + impl VRaw { + pub(crate) async fn render_into_stream( + &self, + w: &mut BufWriter, + _parent_scope: &AnyScope, + hydratable: bool, + ) { + let collectable = Collectable::Raw; + + if hydratable { + collectable.write_open_tag(w); + } + + let _ = w.write_str(self.html.as_ref()); + + if hydratable { + collectable.write_close_tag(w); + } + } + } +} diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index e36498193f1..63247b102b8 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -28,13 +28,13 @@ impl VSuspense { mod feat_ssr { use super::*; use crate::html::AnyScope; - use crate::platform::fmt::BufWrite; + use crate::platform::fmt::BufWriter; use crate::virtual_dom::Collectable; impl VSuspense { pub(crate) async fn render_into_stream( &self, - w: &mut dyn BufWrite, + w: &mut BufWriter, parent_scope: &AnyScope, hydratable: bool, ) { @@ -63,15 +63,15 @@ mod ssr_tests { use std::rc::Rc; use std::time::Duration; + use tokio::task::{spawn_local, LocalSet}; use tokio::test; - use yew::platform::spawn_local; - use yew::platform::time::sleep; + use crate::platform::time::sleep; use crate::prelude::*; use crate::suspense::{Suspension, SuspensionResult}; use crate::ServerRenderer; - #[test] + #[test(flavor = "multi_thread", worker_threads = 2)] async fn test_suspense() { #[derive(PartialEq)] pub struct SleepState { @@ -82,7 +82,9 @@ mod ssr_tests { fn new() -> Self { let (s, handle) = Suspension::new(); + // we use tokio spawn local here. spawn_local(async move { + // we use tokio sleep here. sleep(Duration::from_millis(50)).await; handle.resume(); @@ -135,9 +137,15 @@ mod ssr_tests { } } - let s = ServerRenderer::::new() - .hydratable(false) - .render() + let local = LocalSet::new(); + + let s = local + .run_until(async move { + ServerRenderer::::new() + .hydratable(false) + .render() + .await + }) .await; assert_eq!( diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 382b368dd52..07de0263da9 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -9,12 +9,15 @@ use std::rc::Rc; use web_sys::{HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; -use super::{AttrValue, Attributes, Key, Listener, Listeners, VList, VNode}; +use super::{ApplyAttributeAs, AttrValue, Attributes, Key, Listener, Listeners, VNode}; use crate::html::{IntoPropValue, NodeRef}; /// SVG namespace string used for creating svg elements pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; +/// MathML namespace string used for creating MathML elements +pub const MATHML_NAMESPACE: &str = "http://www.w3.org/1998/Math/MathML"; + /// Default namespace for html elements pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; @@ -62,7 +65,7 @@ pub(crate) struct InputFields { /// It exists to override standard behavior of `checked` attribute, because /// in original HTML it sets `defaultChecked` value of `InputElement`, but for reactive /// frameworks it's more useful to control `checked` value of an `InputElement`. - pub(crate) checked: bool, + pub(crate) checked: Option, } impl Deref for InputFields { @@ -81,7 +84,7 @@ impl DerefMut for InputFields { impl InputFields { /// Crate new attributes for an [InputElement] element - fn new(value: Option, checked: bool) -> Self { + fn new(value: Option, checked: Option) -> Self { Self { value: Value::new(value), checked, @@ -109,8 +112,8 @@ pub(crate) enum VTagInner { Other { /// A tag of the element. tag: Cow<'static, str>, - /// List of child nodes - children: VList, + /// children of the element. + children: VNode, }, } @@ -164,7 +167,7 @@ impl VTag { #[allow(clippy::too_many_arguments)] pub fn __new_input( value: Option, - checked: bool, + checked: Option, node_ref: NodeRef, key: Option, // at bottom for more readable macro-expanded coded @@ -229,7 +232,7 @@ impl VTag { // at bottom for more readable macro-expanded coded attributes: Attributes, listeners: Listeners, - children: VList, + children: VNode, ) -> Self { VTag::new_base( VTagInner::Other { tag, children }, @@ -271,45 +274,41 @@ impl VTag { /// Add [VNode] child. pub fn add_child(&mut self, child: VNode) { if let VTagInner::Other { children, .. } = &mut self.inner { - children.add_child(child) + children.to_vlist_mut().add_child(child) } } /// Add multiple [VNode] children. pub fn add_children(&mut self, children: impl IntoIterator) { if let VTagInner::Other { children: dst, .. } = &mut self.inner { - dst.add_children(children) + dst.to_vlist_mut().add_children(children) } } - /// Returns a reference to the children of this [VTag] - pub fn children(&self) -> &VList { + /// Returns a reference to the children of this [VTag], if the node can have + /// children + pub fn children(&self) -> Option<&VNode> { match &self.inner { - VTagInner::Other { children, .. } => children, - _ => { - // This is mutable because the VList is not Sync - static mut EMPTY: VList = VList::new(); - - // SAFETY: The EMPTY value is always read-only - unsafe { &EMPTY } - } + VTagInner::Other { children, .. } => Some(children), + _ => None, } } /// Returns a mutable reference to the children of this [VTag], if the node can have /// children - pub fn children_mut(&mut self) -> Option<&mut VList> { + pub fn children_mut(&mut self) -> Option<&mut VNode> { match &mut self.inner { VTagInner::Other { children, .. } => Some(children), _ => None, } } - /// Returns the children of this [VTag] - pub fn into_children(self) -> VList { + /// Returns the children of this [VTag], if the node can have + /// children + pub fn into_children(self) -> Option { match self.inner { - VTagInner::Other { children, .. } => children, - _ => VList::new(), + VTagInner::Other { children, .. } => Some(children), + _ => None, } } @@ -341,20 +340,29 @@ impl VTag { /// Returns `checked` property of an /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). - /// (Not a value of node's attribute). - pub fn checked(&self) -> bool { + /// (Does not affect the value of the node's attribute). + pub fn checked(&self) -> Option { match &self.inner { VTagInner::Input(f) => f.checked, - _ => false, + _ => None, } } /// Sets `checked` property of an /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). - /// (Not a value of node's attribute). + /// (Does not affect the value of the node's attribute). pub fn set_checked(&mut self, value: bool) { if let VTagInner::Input(f) = &mut self.inner { - f.checked = value; + f.checked = Some(value); + } + } + + /// Keeps the current value of the `checked` property of an + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + /// (Does not affect the value of the node's attribute). + pub fn preserve_checked(&mut self) { + if let VTagInner::Input(f) = &mut self.inner { + f.checked = None; } } @@ -363,9 +371,20 @@ impl VTag { /// Not every attribute works when it set as an attribute. We use workarounds for: /// `value` and `checked`. pub fn add_attribute(&mut self, key: &'static str, value: impl Into) { - self.attributes - .get_mut_index_map() - .insert(AttrValue::Static(key), value.into()); + self.attributes.get_mut_index_map().insert( + AttrValue::Static(key), + (value.into(), ApplyAttributeAs::Attribute), + ); + } + + /// Set the given key as property on the element + /// + /// [`js_sys::Reflect`] is used for setting properties. + pub fn add_property(&mut self, key: &'static str, value: impl Into) { + self.attributes.get_mut_index_map().insert( + AttrValue::Static(key), + (value.into(), ApplyAttributeAs::Property), + ); } /// Sets attributes to a virtual node. @@ -378,9 +397,10 @@ impl VTag { #[doc(hidden)] pub fn __macro_push_attr(&mut self, key: &'static str, value: impl IntoPropValue) { - self.attributes - .get_mut_index_map() - .insert(AttrValue::from(key), value.into_prop_value()); + self.attributes.get_mut_index_map().insert( + AttrValue::from(key), + (value.into_prop_value(), ApplyAttributeAs::Property), + ); } /// Add event listener on the [VTag]'s [Element](web_sys::Element). @@ -428,9 +448,11 @@ impl PartialEq for VTag { #[cfg(feature = "ssr")] mod feat_ssr { + use std::fmt::Write; + use super::*; use crate::html::AnyScope; - use crate::platform::fmt::BufWrite; + use crate::platform::fmt::BufWriter; use crate::virtual_dom::VText; // Elements that cannot have any child elements. @@ -442,21 +464,21 @@ mod feat_ssr { impl VTag { pub(crate) async fn render_into_stream( &self, - w: &mut dyn BufWrite, + w: &mut BufWriter, parent_scope: &AnyScope, hydratable: bool, ) { - w.write("<".into()); - w.write(self.tag().into()); + let _ = w.write_str("<"); + let _ = w.write_str(self.tag()); - let write_attr = |w: &mut dyn BufWrite, name: &str, val: Option<&str>| { - w.write(" ".into()); - w.write(name.into()); + let write_attr = |w: &mut BufWriter, name: &str, val: Option<&str>| { + let _ = w.write_str(" "); + let _ = w.write_str(name); if let Some(m) = val { - w.write("=\"".into()); - w.write(html_escape::encode_double_quoted_attribute(m)); - w.write("\"".into()); + let _ = w.write_str("=\""); + let _ = w.write_str(&html_escape::encode_double_quoted_attribute(m)); + let _ = w.write_str("\""); } }; @@ -465,7 +487,9 @@ mod feat_ssr { write_attr(w, "value", Some(m)); } - if self.checked() { + // Setting is as an attribute sets the `defaultChecked` property. Only emit this + // if it's explicitly set to checked. + if self.checked() == Some(true) { write_attr(w, "checked", None); } } @@ -474,7 +498,7 @@ mod feat_ssr { write_attr(w, k, Some(v)); } - w.write(">".into()); + let _ = w.write_str(">"); match self.inner { VTagInner::Input(_) => {} @@ -485,7 +509,7 @@ mod feat_ssr { .await; } - w.write("".into()); + let _ = w.write_str(""); } VTagInner::Other { ref tag, @@ -497,12 +521,18 @@ mod feat_ssr { .render_into_stream(w, parent_scope, hydratable) .await; - w.write(Cow::Borrowed("")); + let _ = w.write_str(""); } else { // We don't write children of void elements nor closing tags. - debug_assert!(children.is_empty(), "{} cannot have any children!", tag); + debug_assert!( + match children { + VNode::VList(m) => m.is_empty(), + _ => false, + }, + "{tag} cannot have any children!" + ); } } } diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index c01091140e1..30f0b246742 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -32,22 +32,30 @@ impl PartialEq for VText { } } +impl From for VText { + fn from(value: T) -> Self { + VText::new(value.to_string()) + } +} + #[cfg(feature = "ssr")] mod feat_ssr { + use std::fmt::Write; + use super::*; use crate::html::AnyScope; - use crate::platform::fmt::BufWrite; + use crate::platform::fmt::BufWriter; impl VText { pub(crate) async fn render_into_stream( &self, - w: &mut dyn BufWrite, + w: &mut BufWriter, _parent_scope: &AnyScope, _hydratable: bool, ) { let s = html_escape::encode_text(&self.text); - w.write(s); + let _ = w.write_str(&s); } } } diff --git a/packages/yew/tests/common/mod.rs b/packages/yew/tests/common/mod.rs index 8da829ab7d3..533e5e0c567 100644 --- a/packages/yew/tests/common/mod.rs +++ b/packages/yew/tests/common/mod.rs @@ -1,15 +1,19 @@ #![allow(dead_code)] pub fn obtain_result() -> String { - gloo_utils::document() + gloo::utils::document() .get_element_by_id("result") .expect("No result found. Most likely, the application crashed and burned") .inner_html() } pub fn obtain_result_by_id(id: &str) -> String { - gloo_utils::document() + gloo::utils::document() .get_element_by_id(id) .expect("No result found. Most likely, the application crashed and burned") .inner_html() } + +pub fn output_element() -> web_sys::Element { + gloo::utils::document().get_element_by_id("output").unwrap() +} diff --git a/packages/yew/tests/hydration.rs b/packages/yew/tests/hydration.rs index 52fb193adcf..ecaa11eb1f4 100644 --- a/packages/yew/tests/hydration.rs +++ b/packages/yew/tests/hydration.rs @@ -8,14 +8,15 @@ use std::time::Duration; mod common; use common::{obtain_result, obtain_result_by_id}; -use gloo::timers::future::sleep; use wasm_bindgen::JsCast; use wasm_bindgen_futures::spawn_local; use wasm_bindgen_test::*; use web_sys::{HtmlElement, HtmlTextAreaElement}; +use yew::platform::time::sleep; use yew::prelude::*; use yew::suspense::{use_future, Suspension, SuspensionResult}; -use yew::{Renderer, ServerRenderer}; +use yew::virtual_dom::VNode; +use yew::{function_component, Renderer, ServerRenderer}; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -63,7 +64,7 @@ async fn hydration_works() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::ZERO).await; @@ -76,7 +77,7 @@ async fn hydration_works() { r#"
    Counter: 0
    "# ); - gloo_utils::document() + gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() @@ -94,6 +95,60 @@ async fn hydration_works() { ); } +#[wasm_bindgen_test] +async fn hydration_with_raw() { + #[function_component(Content)] + fn content() -> Html { + let vnode = VNode::from_html_unchecked("

    Hello World

    ".into()); + + html! { +
    + {vnode} +
    + } + } + + #[function_component(App)] + fn app() -> Html { + html! { +
    + +
    + } + } + + let s = ServerRenderer::::new().render().await; + + gloo::utils::document() + .query_selector("#output") + .unwrap() + .unwrap() + .set_inner_html(&s); + + sleep(Duration::from_millis(10)).await; + + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .hydrate(); + + let result = obtain_result(); + + // still hydrating, during hydration, the server rendered result is shown. + assert_eq!( + result.as_str(), + r#"

    Hello World

    "# + ); + + sleep(Duration::from_millis(50)).await; + + let result = obtain_result(); + + // hydrated. + assert_eq!( + result.as_str(), + r#"

    Hello World

    "# + ); +} + #[wasm_bindgen_test] async fn hydration_with_suspense() { #[derive(PartialEq)] @@ -184,7 +239,7 @@ async fn hydration_with_suspense() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::from_millis(10)).await; @@ -207,7 +262,7 @@ async fn hydration_with_suspense() { r#"
    0
    "# ); - gloo_utils::document() + gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() @@ -223,7 +278,7 @@ async fn hydration_with_suspense() { r#"
    1
    "# ); - gloo_utils::document() + gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() @@ -340,7 +395,7 @@ async fn hydration_with_suspense_not_suspended_at_start() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::from_millis(10)).await; @@ -351,7 +406,7 @@ async fn hydration_with_suspense_not_suspended_at_start() { result.as_str(), r#"
    "# ); - gloo_utils::document() + gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() @@ -471,7 +526,7 @@ async fn hydration_nested_suspense_works() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); // outer suspense is hydrating... @@ -500,7 +555,7 @@ async fn hydration_nested_suspense_works() { r#"
    "# ); - gloo_utils::document() + gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() @@ -521,7 +576,7 @@ async fn hydration_nested_suspense_works() { r#"
    "# ); - gloo_utils::document() + gloo::utils::document() .query_selector(".take-a-break2") .unwrap() .unwrap() @@ -608,7 +663,7 @@ async fn hydration_node_ref_works() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::ZERO).await; @@ -619,7 +674,7 @@ async fn hydration_node_ref_works() { r#"
    testtesttesttest
    "# ); - gloo_utils::document() + gloo::utils::document() .query_selector("span") .unwrap() .unwrap() @@ -701,7 +756,7 @@ async fn hydration_list_order_works() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); // Wait until all suspended components becomes revealed. @@ -769,7 +824,7 @@ async fn hydration_suspense_no_flickering() { #[hook] pub fn use_suspend() -> SuspensionResult<()> { use_future(|| async { - gloo::timers::future::sleep(std::time::Duration::from_millis(200)).await; + yew::platform::time::sleep(std::time::Duration::from_millis(200)).await; })?; Ok(()) } @@ -784,7 +839,7 @@ async fn hydration_suspense_no_flickering() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); // Wait until all suspended components becomes revealed. @@ -897,7 +952,7 @@ async fn hydration_order_issue_nested_suspense() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); // Wait until all suspended components becomes revealed. @@ -922,13 +977,10 @@ async fn hydration_props_blocked_until_hydrated() { let range = use_state(|| 0u32..2); { let range = range.clone(); - use_effect_with_deps( - move |_| { - range.set(0..3); - || () - }, - (), - ); + use_effect_with((), move |_| { + range.set(0..3); + || () + }); } html! { @@ -985,13 +1037,10 @@ async fn hydrate_empty() { let trigger = use_state(|| false); { let trigger = trigger.clone(); - use_effect_with_deps( - move |_| { - trigger.set(true); - || {} - }, - (), - ); + use_effect_with((), move |_| { + trigger.set(true); + || {} + }); } if *trigger { html! {
    {"after"}
    } diff --git a/packages/yew/tests/layout.rs b/packages/yew/tests/layout.rs index 3cdaf918238..0369ba66fa5 100644 --- a/packages/yew/tests/layout.rs +++ b/packages/yew/tests/layout.rs @@ -7,9 +7,9 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); use std::time::Duration; use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_futures::spawn_local; use wasm_bindgen_test::*; +use yew::platform::time::sleep; use yew::prelude::*; #[wasm_bindgen_test] @@ -20,16 +20,13 @@ async fn change_nested_after_append() { { let delayed_trigger = delayed_trigger.clone(); - use_effect_with_deps( - move |_| { - spawn_local(async move { - sleep(Duration::from_millis(50)).await; - delayed_trigger.set(false); - }); - || {} - }, - (), - ); + use_effect_with((), move |_| { + spawn_local(async move { + sleep(Duration::from_millis(50)).await; + delayed_trigger.set(false); + }); + || {} + }); } if *delayed_trigger { @@ -51,13 +48,10 @@ async fn change_nested_after_append() { { let show_bottom = show_bottom.clone(); - use_effect_with_deps( - move |_| { - show_bottom.set(true); - || {} - }, - (), - ); + use_effect_with((), move |_| { + show_bottom.set(true); + || {} + }); } html! { @@ -70,7 +64,7 @@ async fn change_nested_after_append() { } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); sleep(Duration::from_millis(100)).await; diff --git a/packages/yew/tests/mod.rs b/packages/yew/tests/mod.rs index f61f01d24f8..a5eba7d5765 100644 --- a/packages/yew/tests/mod.rs +++ b/packages/yew/tests/mod.rs @@ -5,8 +5,8 @@ mod common; use std::time::Duration; use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; +use yew::platform::time::sleep; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -29,7 +29,7 @@ async fn props_are_passed() { } yew::Renderer::::with_root_and_props( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), PropsPassedFunctionProps { value: "props".to_string(), }, diff --git a/packages/yew/tests/raw_html.rs b/packages/yew/tests/raw_html.rs new file mode 100644 index 00000000000..031ba7f50db --- /dev/null +++ b/packages/yew/tests/raw_html.rs @@ -0,0 +1,251 @@ +mod common; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsCast; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::wasm_bindgen_test as test; +use yew::prelude::*; +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); +#[cfg(not(target_arch = "wasm32"))] +use tokio::test; + +macro_rules! create_test { + ($name:ident, $html:expr) => { + create_test!($name, $html, $html); + }; + ($name:ident, $raw:expr, $expected:expr) => { + #[test] + async fn $name() { + #[function_component] + fn App() -> Html { + let raw = Html::from_html_unchecked(AttrValue::from($raw)); + html! { +
    + {raw} +
    + } + } + + #[cfg(target_arch = "wasm32")] + { + use std::time::Duration; + + use yew::platform::time::sleep; + + yew::Renderer::::with_root( + gloo::utils::document().get_element_by_id("output").unwrap(), + ) + .render(); + + // wait for render to finish + sleep(Duration::from_millis(100)).await; + + let e = gloo::utils::document() + .get_element_by_id("raw-container") + .unwrap(); + assert_eq!(e.inner_html(), $expected); + } + #[cfg(not(target_arch = "wasm32"))] + { + let actual = yew::ServerRenderer::::new() + .hydratable(false) + .render() + .await; + assert_eq!( + actual, + format!(r#"
    {}
    "#, $expected) + ); + } + } + }; +} + +create_test!(empty_string, ""); +create_test!(one_node, "text"); +create_test!( + one_but_nested_node, + r#"

    one link more paragraph

    "# +); +create_test!( + multi_node, + r#"

    paragraph

    link"# +); + +macro_rules! create_update_html_test { + ($name:ident, $initial:expr, $updated:expr) => { + #[cfg(target_arch = "wasm32")] + #[test] + async fn $name() { + #[function_component] + fn App() -> Html { + let raw_html = use_state(|| ($initial)); + let onclick = { + let raw_html = raw_html.clone(); + move |_| raw_html.set($updated) + }; + let raw = Html::from_html_unchecked(AttrValue::from(*raw_html)); + html! { + <> +
    + {raw} +
    + + + } + } + use std::time::Duration; + + use yew::platform::time::sleep; + + yew::Renderer::::with_root( + gloo::utils::document().get_element_by_id("output").unwrap(), + ) + .render(); + + // wait for render to finish + sleep(Duration::from_millis(100)).await; + + let e = gloo::utils::document() + .get_element_by_id("raw-container") + .unwrap(); + assert_eq!(e.inner_html(), $initial); + + gloo::utils::document() + .get_element_by_id("click-me-btn") + .unwrap() + .unchecked_into::() + .click(); + + sleep(Duration::from_millis(100)).await; + + let e = gloo::utils::document() + .get_element_by_id("raw-container") + .unwrap(); + assert_eq!(e.inner_html(), $updated); + } + }; +} + +create_update_html_test!( + set_new_html_string, + "first", + "second" +); + +create_update_html_test!( + set_new_html_string_multiple_children, + "firstsecond", + "second" +); + +create_update_html_test!( + clear_html_string_multiple_children, + "firstsecond", + "" +); +create_update_html_test!( + nothing_changes, + "firstsecond", + "firstsecond" +); + +#[cfg(target_arch = "wasm32")] +#[test] +async fn change_vnode_types_from_other_to_vraw() { + #[function_component] + fn App() -> Html { + let node = use_state(|| html!("text")); + let onclick = { + let node = node.clone(); + move |_| { + node.set(Html::from_html_unchecked(AttrValue::from( + "second", + ))) + } + }; + html! { + <> +
    + {(*node).clone()} +
    + + + } + } + use std::time::Duration; + + use yew::platform::time::sleep; + + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .render(); + + // wait for render to finish + sleep(Duration::from_millis(100)).await; + + let e = gloo::utils::document() + .get_element_by_id("raw-container") + .unwrap(); + assert_eq!(e.inner_html(), "text"); + + gloo::utils::document() + .get_element_by_id("click-me-btn") + .unwrap() + .unchecked_into::() + .click(); + + sleep(Duration::from_millis(100)).await; + + let e = gloo::utils::document() + .get_element_by_id("raw-container") + .unwrap(); + assert_eq!(e.inner_html(), "second"); +} + +#[cfg(target_arch = "wasm32")] +#[test] +async fn change_vnode_types_from_vraw_to_other() { + #[function_component] + fn App() -> Html { + let node = use_state(|| Html::from_html_unchecked(AttrValue::from("second"))); + let onclick = { + let node = node.clone(); + move |_| node.set(html!("text")) + }; + html! { + <> +
    + {(*node).clone()} +
    + + + } + } + use std::time::Duration; + + use yew::platform::time::sleep; + + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .render(); + + // wait for render to finish + sleep(Duration::from_millis(100)).await; + + let e = gloo::utils::document() + .get_element_by_id("raw-container") + .unwrap(); + assert_eq!(e.inner_html(), "second"); + + gloo::utils::document() + .get_element_by_id("click-me-btn") + .unwrap() + .unchecked_into::() + .click(); + + sleep(Duration::from_millis(100)).await; + + let e = gloo::utils::document() + .get_element_by_id("raw-container") + .unwrap(); + assert_eq!(e.inner_html(), "text"); +} diff --git a/packages/yew/tests/suspense.rs b/packages/yew/tests/suspense.rs index eb045a7c37b..cc3f0bf7e10 100644 --- a/packages/yew/tests/suspense.rs +++ b/packages/yew/tests/suspense.rs @@ -2,20 +2,21 @@ mod common; -use common::obtain_result; -use wasm_bindgen_test::*; -use yew::prelude::*; - -wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - use std::cell::RefCell; use std::rc::Rc; +use std::time::Duration; -use gloo::timers::future::TimeoutFuture; +use common::obtain_result; use wasm_bindgen::JsCast; -use wasm_bindgen_futures::spawn_local; +use wasm_bindgen_test::*; use web_sys::{HtmlElement, HtmlTextAreaElement}; +use yew::platform::spawn_local; +use yew::platform::time::sleep; +use yew::prelude::*; use yew::suspense::{use_future, use_future_with_deps, Suspension, SuspensionResult}; +use yew::UseStateHandle; + +wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn suspense_works() { @@ -29,7 +30,7 @@ async fn suspense_works() { let (s, handle) = Suspension::new(); spawn_local(async move { - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; handle.resume(); }); @@ -97,14 +98,14 @@ async fn suspense_works() { } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
    wait...
    "); - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( @@ -112,9 +113,9 @@ async fn suspense_works() { r#"
    0
    "# ); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; - gloo_utils::document() + gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() @@ -122,9 +123,9 @@ async fn suspense_works() { .unwrap() .click(); - TimeoutFuture::new(0).await; + sleep(Duration::ZERO).await; - gloo_utils::document() + gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() @@ -132,7 +133,7 @@ async fn suspense_works() { .unwrap() .click(); - TimeoutFuture::new(1).await; + sleep(Duration::from_millis(1)).await; let result = obtain_result(); assert_eq!( @@ -140,7 +141,7 @@ async fn suspense_works() { r#"
    2
    "# ); - gloo_utils::document() + gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() @@ -148,11 +149,11 @@ async fn suspense_works() { .unwrap() .click(); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
    wait...
    "); - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( @@ -181,7 +182,7 @@ async fn suspense_not_suspended_at_start() { let (s, handle) = Suspension::new(); spawn_local(async move { - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; handle.resume(); }); @@ -247,17 +248,17 @@ async fn suspense_not_suspended_at_start() { } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
    "# ); - gloo_utils::document() + gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() @@ -265,11 +266,11 @@ async fn suspense_not_suspended_at_start() { .unwrap() .click(); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
    wait...
    "); - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( @@ -290,7 +291,7 @@ async fn suspense_nested_suspense_works() { let (s, handle) = Suspension::new(); spawn_local(async move { - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; handle.resume(); }); @@ -366,14 +367,14 @@ async fn suspense_nested_suspense_works() { } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
    wait...(outer)
    "); - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( @@ -381,7 +382,7 @@ async fn suspense_nested_suspense_works() { r#"
    wait...(inner)
    "# ); - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( @@ -389,7 +390,7 @@ async fn suspense_nested_suspense_works() { r#"
    "# ); - gloo_utils::document() + gloo::utils::document() .query_selector(".take-a-break2") .unwrap() .unwrap() @@ -397,14 +398,14 @@ async fn suspense_nested_suspense_works() { .unwrap() .click(); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!( result.as_str(), r#"
    wait...(inner)
    "# ); - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( @@ -425,7 +426,7 @@ async fn effects_not_run_when_suspended() { let (s, handle) = Suspension::new(); spawn_local(async move { - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; handle.resume(); }); @@ -523,17 +524,17 @@ async fn effects_not_run_when_suspended() { }; yew::Renderer::::with_root_and_props( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), props, ) .render(); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
    wait...
    "); assert_eq!(*counter.borrow(), 0); // effects not called. - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( @@ -542,9 +543,9 @@ async fn effects_not_run_when_suspended() { ); assert_eq!(*counter.borrow(), 1); // effects ran 1 time. - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; - gloo_utils::document() + gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() @@ -552,9 +553,9 @@ async fn effects_not_run_when_suspended() { .unwrap() .click(); - TimeoutFuture::new(0).await; + sleep(Duration::ZERO).await; - gloo_utils::document() + gloo::utils::document() .query_selector(".increase") .unwrap() .unwrap() @@ -562,7 +563,7 @@ async fn effects_not_run_when_suspended() { .unwrap() .click(); - TimeoutFuture::new(0).await; + sleep(Duration::from_millis(0)).await; let result = obtain_result(); assert_eq!( @@ -571,7 +572,7 @@ async fn effects_not_run_when_suspended() { ); assert_eq!(*counter.borrow(), 3); // effects ran 3 times. - gloo_utils::document() + gloo::utils::document() .query_selector(".take-a-break") .unwrap() .unwrap() @@ -579,12 +580,12 @@ async fn effects_not_run_when_suspended() { .unwrap() .click(); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
    wait...
    "); assert_eq!(*counter.borrow(), 3); // effects ran 3 times. - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!( @@ -599,7 +600,7 @@ async fn use_suspending_future_works() { #[function_component(Content)] fn content() -> HtmlResult { let _sleep_handle = use_future(|| async move { - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; })?; Ok(html! { @@ -622,14 +623,14 @@ async fn use_suspending_future_works() { } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
    wait...
    "); - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!(result.as_str(), r#"
    Content
    "#); @@ -639,14 +640,14 @@ async fn use_suspending_future_works() { async fn use_suspending_future_with_deps_works() { #[derive(PartialEq, Properties)] struct ContentProps { - delay_millis: u32, + delay_millis: u64, } #[function_component(Content)] fn content(ContentProps { delay_millis }: &ContentProps) -> HtmlResult { let delayed_result = use_future_with_deps( |delay_millis| async move { - TimeoutFuture::new(*delay_millis).await; + sleep(Duration::from_millis(*delay_millis)).await; 42 }, *delay_millis, @@ -672,15 +673,150 @@ async fn use_suspending_future_with_deps_works() { } } - yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .render(); - TimeoutFuture::new(10).await; + sleep(Duration::from_millis(10)).await; let result = obtain_result(); assert_eq!(result.as_str(), "
    wait...
    "); - TimeoutFuture::new(50).await; + sleep(Duration::from_millis(50)).await; let result = obtain_result(); assert_eq!(result.as_str(), r#"
    42
    "#); } + +#[wasm_bindgen_test] +async fn test_suspend_forever() { + /// A component that its suspension never resumes. + /// We test that this can be used with to trigger a suspension and unsuspend upon unmount. + #[function_component] + fn SuspendForever() -> HtmlResult { + let (s, handle) = Suspension::new(); + use_state(move || handle); + Err(s.into()) + } + + #[function_component] + fn App() -> Html { + let page = use_state(|| 1); + + { + let page_setter = page.setter(); + use_effect_with((), move |_| { + spawn_local(async move { + sleep(Duration::from_secs(1)).await; + page_setter.set(2); + }); + }); + } + + let content = if *page == 1 { + html! { } + } else { + html! {
    {"OK"}
    } + }; + + html! { + {"Loading..."}
    }}> + {content} + + } + } + + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .render(); + + sleep(Duration::from_millis(1500)).await; + + let result = obtain_result(); + assert_eq!(result.as_str(), r#"OK"#); +} + +#[wasm_bindgen_test] +async fn resume_after_unmount() { + #[derive(Clone, Properties, PartialEq)] + struct ContentProps { + state: UseStateHandle, + } + + #[function_component(Content)] + fn content(ContentProps { state }: &ContentProps) -> HtmlResult { + let state = state.clone(); + let _sleep_handle = use_future(|| async move { + sleep(Duration::from_millis(50)).await; + state.set(false); + sleep(Duration::from_millis(50)).await; + })?; + + Ok(html! { +
    {"Content"}
    + }) + } + + #[function_component(App)] + fn app() -> Html { + let fallback = html! {
    {"wait..."}
    }; + let state = use_state(|| true); + + html! { +
    + if *state { + + + + } else { +
    {"Content replacement"}
    + } +
    + } + } + + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .render(); + + sleep(Duration::from_millis(25)).await; + let result = obtain_result(); + assert_eq!(result.as_str(), "
    wait...
    "); + + sleep(Duration::from_millis(50)).await; + let result = obtain_result(); + assert_eq!(result.as_str(), "
    Content replacement
    "); +} + +#[wasm_bindgen_test] +async fn test_duplicate_suspension() { + use yew::html::ChildrenProps; + + #[function_component] + fn FetchingProvider(props: &ChildrenProps) -> HtmlResult { + use_future(|| async { + sleep(Duration::ZERO).await; + })?; + Ok(html! { <>{props.children.clone()} }) + } + + #[function_component] + fn Child() -> Html { + html! {
    {"hello!"}
    } + } + + #[function_component] + fn App() -> Html { + let fallback = Html::default(); + html! { + + + + + + } + } + + yew::Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) + .render(); + + sleep(Duration::from_millis(50)).await; + let result = obtain_result(); + assert_eq!(result.as_str(), "hello!"); +} diff --git a/packages/yew/tests/use_callback.rs b/packages/yew/tests/use_callback.rs index b7f8a45c1f8..28130a092fd 100644 --- a/packages/yew/tests/use_callback.rs +++ b/packages/yew/tests/use_callback.rs @@ -7,8 +7,8 @@ mod common; use std::time::Duration; use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; +use yew::platform::time::sleep; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -43,7 +43,7 @@ async fn use_callback_works() { fn use_callback_comp() -> Html { let state = use_state(|| 0); - let callback = use_callback(move |name, _| format!("Hello, {}!", name), ()); + let callback = use_callback((), move |name, _| format!("Hello, {}!", name)); use_effect(move || { if *state < 5 { @@ -61,7 +61,7 @@ async fn use_callback_works() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); diff --git a/packages/yew/tests/use_context.rs b/packages/yew/tests/use_context.rs index 19f58f494e1..d4123c8111c 100644 --- a/packages/yew/tests/use_context.rs +++ b/packages/yew/tests/use_context.rs @@ -6,8 +6,8 @@ use std::rc::Rc; use std::time::Duration; use common::obtain_result_by_id; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; +use yew::platform::time::sleep; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -65,7 +65,7 @@ async fn use_context_scoping_works() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); @@ -148,7 +148,7 @@ async fn use_context_works_with_multiple_types() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); @@ -240,22 +240,22 @@ async fn use_context_update_works() { html! { - - + + } } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); sleep(Duration::ZERO).await; - // 1 initial render + 3 update steps - assert_eq!(obtain_result_by_id("test-0"), "total: 4"); + // 1 initial render + 1 magic + assert_eq!(obtain_result_by_id("test-0"), "total: 2"); // 1 initial + 2 context update assert_eq!( diff --git a/packages/yew/tests/use_effect.rs b/packages/yew/tests/use_effect.rs index c7969544be5..e5ffa0373ae 100644 --- a/packages/yew/tests/use_effect.rs +++ b/packages/yew/tests/use_effect.rs @@ -7,8 +7,8 @@ use std::rc::Rc; use std::time::Duration; use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; +use yew::platform::time::sleep; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -39,14 +39,11 @@ async fn use_effect_destroys_on_component_drop() { fn use_effect_comp(props: &FunctionProps) -> Html { let effect_called = props.effect_called.clone(); let destroy_called = props.destroy_called.clone(); - use_effect_with_deps( - move |_| { - effect_called(); - #[allow(clippy::redundant_closure)] // Otherwise there is a build error - move || destroy_called() - }, - (), - ); + use_effect_with((), move |_| { + effect_called(); + #[allow(clippy::redundant_closure)] // Otherwise there is a build error + move || destroy_called() + }); html! {} } @@ -68,7 +65,7 @@ async fn use_effect_destroys_on_component_drop() { let destroy_counter = Rc::new(std::cell::RefCell::new(0)); let destroy_counter_c = destroy_counter.clone(); yew::Renderer::::with_root_and_props( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), WrapperProps { destroy_called: Rc::new(move || *destroy_counter_c.borrow_mut().deref_mut() += 1), }, @@ -87,15 +84,12 @@ async fn use_effect_works_many_times() { let counter = use_state(|| 0); let counter_clone = counter.clone(); - use_effect_with_deps( - move |_| { - if *counter_clone < 4 { - counter_clone.set(*counter_clone + 1); - } - || {} - }, - *counter, - ); + use_effect_with(*counter, move |_| { + if *counter_clone < 4 { + counter_clone.set(*counter_clone + 1); + } + || {} + }); html! {
    @@ -107,7 +101,7 @@ async fn use_effect_works_many_times() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); @@ -123,13 +117,10 @@ async fn use_effect_works_once() { let counter = use_state(|| 0); let counter_clone = counter.clone(); - use_effect_with_deps( - move |_| { - counter_clone.set(*counter_clone + 1); - || panic!("Destructor should not have been called") - }, - (), - ); + use_effect_with((), move |_| { + counter_clone.set(*counter_clone + 1); + || panic!("Destructor should not have been called") + }); html! {
    @@ -141,7 +132,7 @@ async fn use_effect_works_once() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); sleep(Duration::ZERO).await; @@ -161,24 +152,21 @@ async fn use_effect_refires_on_dependency_change() { let number_ref2_c = number_ref2.clone(); let arg = *number_ref.borrow_mut().deref_mut(); let counter = use_state(|| 0); - use_effect_with_deps( - move |dep| { - let mut ref_mut = number_ref_c.borrow_mut(); - let inner_ref_mut = ref_mut.deref_mut(); - if *inner_ref_mut < 1 { - *inner_ref_mut += 1; - assert_eq!(dep, &0); - } else { - assert_eq!(dep, &1); - } - counter.set(10); // we just need to make sure it does not panic - move || { - counter.set(11); - *number_ref2_c.borrow_mut().deref_mut() += 1; - } - }, - arg, - ); + use_effect_with(arg, move |dep| { + let mut ref_mut = number_ref_c.borrow_mut(); + let inner_ref_mut = ref_mut.deref_mut(); + if *inner_ref_mut < 1 { + *inner_ref_mut += 1; + assert_eq!(dep, &0); + } else { + assert_eq!(dep, &1); + } + counter.set(10); // we just need to make sure it does not panic + move || { + counter.set(11); + *number_ref2_c.borrow_mut().deref_mut() += 1; + } + }); html! {
    {"The test result is"} @@ -189,7 +177,7 @@ async fn use_effect_refires_on_dependency_change() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); diff --git a/packages/yew/tests/use_memo.rs b/packages/yew/tests/use_memo.rs index c62b71852c6..e67ce0567d1 100644 --- a/packages/yew/tests/use_memo.rs +++ b/packages/yew/tests/use_memo.rs @@ -7,8 +7,8 @@ mod common; use std::time::Duration; use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; +use yew::platform::time::sleep; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -19,18 +19,15 @@ async fn use_memo_works() { fn use_memo_comp() -> Html { let state = use_state(|| 0); - let memoed_val = use_memo( - |_| { - static CTR: AtomicBool = AtomicBool::new(false); + let memoed_val = use_memo((), |_| { + static CTR: AtomicBool = AtomicBool::new(false); - if CTR.swap(true, Ordering::Relaxed) { - panic!("multiple times rendered!"); - } + if CTR.swap(true, Ordering::Relaxed) { + panic!("multiple times rendered!"); + } - "true" - }, - (), - ); + "true" + }); use_effect(move || { if *state < 5 { @@ -50,7 +47,7 @@ async fn use_memo_works() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); diff --git a/packages/yew/tests/use_prepared_state.rs b/packages/yew/tests/use_prepared_state.rs index 2263aff4f46..8081c3014a4 100644 --- a/packages/yew/tests/use_prepared_state.rs +++ b/packages/yew/tests/use_prepared_state.rs @@ -1,14 +1,14 @@ #![cfg(target_arch = "wasm32")] #![cfg(feature = "hydration")] -#![cfg_attr(feature = "nightly", feature(async_closure))] +#![cfg_attr(nightly_yew, feature(async_closure))] use std::time::Duration; mod common; use common::obtain_result_by_id; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; +use yew::platform::time::sleep; use yew::prelude::*; use yew::{Renderer, ServerRenderer}; @@ -53,7 +53,7 @@ async fn use_prepared_state_works() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::from_millis(100)).await; @@ -103,7 +103,7 @@ async fn use_prepared_state_with_suspension_works() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::from_millis(100)).await; diff --git a/packages/yew/tests/use_reducer.rs b/packages/yew/tests/use_reducer.rs index dddde7c8541..b08403bca52 100644 --- a/packages/yew/tests/use_reducer.rs +++ b/packages/yew/tests/use_reducer.rs @@ -4,11 +4,11 @@ use std::collections::HashSet; use std::rc::Rc; use std::time::Duration; -use gloo::timers::future::sleep; -use gloo_utils::document; +use gloo::utils::document; use wasm_bindgen::JsCast; use wasm_bindgen_test::*; use web_sys::HtmlElement; +use yew::platform::time::sleep; use yew::prelude::*; mod common; @@ -40,13 +40,10 @@ async fn use_reducer_works() { let counter = use_reducer(|| CounterState { counter: 10 }); let counter_clone = counter.clone(); - use_effect_with_deps( - move |_| { - counter_clone.dispatch(1); - || {} - }, - (), - ); + use_effect_with((), move |_| { + counter_clone.dispatch(1); + || {} + }); html! {
    {"The test result is"} @@ -57,7 +54,7 @@ async fn use_reducer_works() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); sleep(Duration::ZERO).await; diff --git a/packages/yew/tests/use_ref.rs b/packages/yew/tests/use_ref.rs index d0e34e4dc8a..2ba8e133975 100644 --- a/packages/yew/tests/use_ref.rs +++ b/packages/yew/tests/use_ref.rs @@ -6,8 +6,8 @@ use std::ops::DerefMut; use std::time::Duration; use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; +use yew::platform::time::sleep; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -32,7 +32,7 @@ async fn use_ref_works() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); sleep(Duration::ZERO).await; diff --git a/packages/yew/tests/use_state.rs b/packages/yew/tests/use_state.rs index 68800dae0f9..c48bc0f295d 100644 --- a/packages/yew/tests/use_state.rs +++ b/packages/yew/tests/use_state.rs @@ -5,8 +5,8 @@ mod common; use std::time::Duration; use common::obtain_result; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; +use yew::platform::time::sleep; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -29,7 +29,7 @@ async fn use_state_works() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); sleep(Duration::ZERO).await; @@ -43,14 +43,11 @@ async fn multiple_use_state_setters() { fn use_state_comp() -> Html { let counter = use_state(|| 0); let counter_clone = counter.clone(); - use_effect_with_deps( - move |_| { - // 1st location - counter_clone.set(*counter_clone + 1); - || {} - }, - (), - ); + use_effect_with((), move |_| { + // 1st location + counter_clone.set(*counter_clone + 1); + || {} + }); let another_scope = { let counter = counter.clone(); move || { @@ -72,7 +69,7 @@ async fn multiple_use_state_setters() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); sleep(Duration::ZERO).await; @@ -101,7 +98,7 @@ async fn use_state_eq_works() { } yew::Renderer::::with_root( - gloo_utils::document().get_element_by_id("output").unwrap(), + gloo::utils::document().get_element_by_id("output").unwrap(), ) .render(); sleep(Duration::ZERO).await; diff --git a/packages/yew/tests/use_transitive_state.rs b/packages/yew/tests/use_transitive_state.rs index 3e180d2187d..5cb663adbde 100644 --- a/packages/yew/tests/use_transitive_state.rs +++ b/packages/yew/tests/use_transitive_state.rs @@ -6,8 +6,8 @@ use std::time::Duration; mod common; use common::obtain_result_by_id; -use gloo::timers::future::sleep; use wasm_bindgen_test::*; +use yew::platform::time::sleep; use yew::prelude::*; use yew::{Renderer, ServerRenderer}; @@ -53,7 +53,7 @@ async fn use_transitive_state_works() { sleep(Duration::ZERO).await; - Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + Renderer::::with_root(gloo::utils::document().get_element_by_id("output").unwrap()) .hydrate(); sleep(Duration::from_millis(100)).await; diff --git a/tools/benchmark-hooks/Cargo.toml b/tools/benchmark-hooks/Cargo.toml index 368d7fcfd96..22ef8c0f4f4 100644 --- a/tools/benchmark-hooks/Cargo.toml +++ b/tools/benchmark-hooks/Cargo.toml @@ -8,11 +8,11 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -rand = { version = "0.8.4", features = ["small_rng"] } -getrandom = { version = "0.2.1", features = ["js"] } -wasm-bindgen = "0.2.80" -web-sys = { version = "0.3.55", features = ["Window"]} -yew = "0.19.3" +rand = { version = "0.8.5", features = ["small_rng"] } +getrandom = { version = "0.2.10", features = ["js"] } +wasm-bindgen = "0.2.87" +web-sys = { version = "0.3.64", features = ["Window"]} +yew = { version = "0.20.0", features = ["csr"], path = "../../packages/yew" } [package.metadata.wasm-pack.profile.release] wasm-opt = ['-O4'] diff --git a/tools/benchmark-hooks/package-lock.json b/tools/benchmark-hooks/package-lock.json index 7d51555197f..4699b4eb70d 100644 --- a/tools/benchmark-hooks/package-lock.json +++ b/tools/benchmark-hooks/package-lock.json @@ -1,8 +1,170 @@ { - "name": "js-framework-benchmark-non-keyed-yew", + "name": "js-framework-benchmark-non-keyed-yew-hooks", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "js-framework-benchmark-non-keyed-yew-hooks", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "cpr": "^3.0.1", + "rimraf": "^2.6.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cpr": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cpr/-/cpr-3.0.1.tgz", + "integrity": "sha512-Xch4PXQ/KC8lJ+KfJ9JI6eG/nmppLrPPWg5Q+vh65Qr9EjuJEubxh/H/Le1TmCZ7+Xv7iJuNRqapyOFZB+wsxA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.5", + "minimist": "^1.2.0", + "mkdirp": "~0.5.1", + "rimraf": "^2.5.4" + }, + "bin": { + "cpr": "bin/cpr" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + }, "dependencies": { "balanced-match": { "version": "1.0.2", @@ -23,13 +185,13 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "cpr": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cpr/-/cpr-3.0.1.tgz", - "integrity": "sha1-uaVQOLfNgaNcF7l2GJW9hJau8eU=", + "integrity": "sha512-Xch4PXQ/KC8lJ+KfJ9JI6eG/nmppLrPPWg5Q+vh65Qr9EjuJEubxh/H/Le1TmCZ7+Xv7iJuNRqapyOFZB+wsxA==", "dev": true, "requires": { "graceful-fs": "^4.1.5", @@ -41,33 +203,33 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "requires": { "once": "^1.3.0", @@ -81,9 +243,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -96,18 +258,18 @@ "dev": true }, "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "requires": { - "minimist": "^1.2.5" + "minimist": "^1.2.6" } }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "requires": { "wrappy": "1" @@ -116,7 +278,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true }, "rimraf": { @@ -131,7 +293,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true } } diff --git a/tools/benchmark-hooks/package.json b/tools/benchmark-hooks/package.json index 91d5c76b88d..14ba90be5ef 100644 --- a/tools/benchmark-hooks/package.json +++ b/tools/benchmark-hooks/package.json @@ -1,10 +1,14 @@ { - "name": "js-framework-benchmark-non-keyed-yew-hooks", + "name": "js-framework-benchmark-keyed-yew-hooks", "version": "1.0.0", "description": "Benchmark for Yew Hooks", "license": "ISC", "js-framework-benchmark": { - "frameworkVersion": "latest" + "frameworkVersion": "latest", + "frameworkHomeURL": "https://yew.rs/", + "issues": [ + 1139 + ] }, "scripts": { "build-prod": "echo This is a no-op. && echo Due to heavy dependencies, the generated javascript is already provided. && echo If you really want to rebuild from source use: && echo npm run build-prod-force", diff --git a/tools/benchmark-hooks/src/lib.rs b/tools/benchmark-hooks/src/lib.rs index 171938e6ab6..027fab5b0cf 100644 --- a/tools/benchmark-hooks/src/lib.rs +++ b/tools/benchmark-hooks/src/lib.rs @@ -228,7 +228,7 @@ fn jumbotron(props: &JumbotronProps) -> Html {
    -

    { "Yew" }

    +

    { "Yew-Hooks" }

    @@ -287,5 +287,5 @@ fn row(props: &RowProps) -> Html { pub fn start() { let document = window().unwrap().document().unwrap(); let mount_el = document.query_selector("#main").unwrap().unwrap(); - yew::start_app_in_element::(mount_el); + yew::Renderer::::with_root(mount_el).render(); } diff --git a/tools/benchmark-ssr/Cargo.toml b/tools/benchmark-ssr/Cargo.toml index df278aa22e4..e190d1b3fdb 100644 --- a/tools/benchmark-ssr/Cargo.toml +++ b/tools/benchmark-ssr/Cargo.toml @@ -6,13 +6,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -yew = { path = "../../packages/yew", features = ["tokio", "ssr"] } +yew = { path = "../../packages/yew", features = ["ssr"] } function_router = { path = "../../examples/function_router" } -tokio = { version = "1.19", features = ["macros", "rt-multi-thread", "parking_lot"] } +tokio = { version = "1.29", features = ["full"] } jemallocator = "0.5.0" average = "0.13.1" -tabled = "0.7.0" -indicatif = "0.16.2" -serde = { version = "1.0.138", features = ["derive"] } -serde_json = "1.0.82" -clap = { version = "3.2.8", features = ["derive"] } +tabled = "0.12.2" +indicatif = "0.17.5" +serde = { version = "1.0.164", features = ["derive"] } +serde_json = "1.0.104" +clap = { version = "4", features = ["derive"] } diff --git a/tools/benchmark-ssr/src/main.rs b/tools/benchmark-ssr/src/main.rs index be36d266006..011194e2287 100644 --- a/tools/benchmark-ssr/src/main.rs +++ b/tools/benchmark-ssr/src/main.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; -use std::fs; -use std::io::Write; +use std::fs::File; use std::path::PathBuf; use std::time::{Duration, Instant}; @@ -9,8 +8,10 @@ use clap::Parser; use function_router::{ServerApp, ServerAppProps}; use indicatif::{ProgressBar, ProgressStyle}; use serde::{Deserialize, Serialize}; -use tabled::{Style, TableIteratorExt, Tabled}; -use yew::platform::LocalRuntime; +use tabled::settings::Style; +use tabled::{Table, Tabled}; +use tokio::task::{spawn_local, LocalSet}; +use yew::platform::time::sleep; use yew::prelude::*; #[global_allocator] @@ -24,9 +25,13 @@ struct Args { /// Write the report to an output path in json format. #[clap(long)] output_path: Option, + + /// The number of rounds to run. + #[clap(long, default_value_t = 10)] + rounds: usize, } -fn dur_to_millis(dur: Duration) -> f64 { +fn dur_as_millis_f64(dur: Duration) -> f64 { i32::try_from(dur.as_micros()).map(f64::from).unwrap() / 1000.0 } @@ -44,7 +49,7 @@ fn bench_baseline() -> Duration { start_time.elapsed() } -fn bench_hello_world() -> Duration { +async fn bench_hello_world() -> Duration { static TOTAL: usize = 1_000_000; #[function_component] @@ -52,36 +57,135 @@ fn bench_hello_world() -> Duration { html! {
    {"Hello, World!"}
    } } - let rt = LocalRuntime::new().expect("failed to create runtime."); + let start_time = Instant::now(); + + for _ in 0..TOTAL { + yew::LocalServerRenderer::::new().render().await; + } + + start_time.elapsed() +} + +async fn bench_router_app() -> Duration { + static TOTAL: usize = 100_000; let start_time = Instant::now(); - rt.block_on(async move { - for _ in 0..TOTAL { - yew::LocalServerRenderer::::new().render().await; + for _ in 0..TOTAL { + yew::LocalServerRenderer::::with_props(ServerAppProps { + url: "/".into(), + queries: HashMap::new(), + }) + .render() + .await; + } + + start_time.elapsed() +} + +async fn bench_many_providers() -> Duration { + static TOTAL: usize = 250_000; + + #[derive(Properties, PartialEq, Clone)] + struct ProviderProps { + children: Html, + } + + #[function_component] + fn Provider(props: &ProviderProps) -> Html { + let ProviderProps { children } = props.clone(); + + children + } + + #[function_component] + fn App() -> Html { + // Let's make 10 providers. + html! { + + + + + + + + + + {"Hello, World!"} + + + + + + + + + } - }); + } + + let start_time = Instant::now(); + + for _ in 0..TOTAL { + yew::LocalServerRenderer::::new().render().await; + } start_time.elapsed() } -fn bench_router_app() -> Duration { - static TOTAL: usize = 100_000; +async fn bench_concurrent_task() -> Duration { + static TOTAL: usize = 100; let start_time = Instant::now(); - let rt = LocalRuntime::new().expect("failed to create runtime."); + #[function_component] + fn Comp() -> HtmlResult { + let _state = use_prepared_state!( + async move |_| -> () { + sleep(Duration::from_secs(1)).await; + }, + () + )?; + + Ok(Html::default()) + } - rt.block_on(async move { - for _ in 0..TOTAL { - yew::LocalServerRenderer::::with_props(ServerAppProps { - url: "/".into(), - queries: HashMap::new(), - }) - .render() - .await; + #[function_component] + fn Parent() -> Html { + html! { + <> + + + + + } - }); + } + + #[function_component] + fn App() -> Html { + html! { + + + + + + + + } + } + + let mut tasks = Vec::new(); + + for _ in 0..TOTAL { + tasks.push(spawn_local(async { + yew::LocalServerRenderer::::new().render().await; + })); + } + + for task in tasks { + task.await.expect("failed to finish task"); + } start_time.elapsed() } @@ -102,150 +206,148 @@ struct Statistics { std_dev: String, } -static ROUND: u16 = 10; +impl Statistics { + fn from_results(name: S, round: usize, mut results: Vec) -> Self + where + S: Into, + { + let name = name.into(); + + results.sort(); + + let var: Variance = results.iter().cloned().map(dur_as_millis_f64).collect(); + + Self { + name, + round: round.to_string(), + min: format!("{:.3}", dur_as_millis_f64(results[0])), + max: format!( + "{:.3}", + dur_as_millis_f64(*results.last().expect("array is empty?")) + ), + std_dev: format!("{:.3}", var.sample_variance().sqrt()), + mean: format!("{:.3}", var.mean()), + } + } +} + +fn create_progress(tests: usize, rounds: usize) -> ProgressBar { + let bar = ProgressBar::new((tests * rounds) as u64); + // Progress Bar needs to be updated in a different thread. + { + let bar = bar.downgrade(); + std::thread::spawn(move || { + while let Some(bar) = bar.upgrade() { + bar.tick(); + std::thread::sleep(Duration::from_millis(100)); + } + }); + } + + bar.set_style( + ProgressStyle::default_bar() + .template(&format!( + "{{spinner:.green}} {{prefix}} [{{elapsed_precise}}] [{{bar:40.cyan/blue}}] round \ + {{msg}}/{rounds}", + )) + .expect("failed to parse template") + // .tick_chars("-\\|/") + .progress_chars("=>-"), + ); + + bar +} + +#[tokio::main] +async fn main() { + let local_set = LocalSet::new(); -fn main() { let args = Args::parse(); - let mut baseline_results = Vec::new(); - let mut hello_world_results = Vec::new(); - let mut function_router_results = Vec::new(); - - let bar = if args.no_term { - None - } else { - // There are 3 items per round. - let bar = ProgressBar::new(u64::from(ROUND * 3)); - // Progress Bar needs to be updated in a different thread. - { - let bar = bar.downgrade(); - std::thread::spawn(move || { - while let Some(bar) = bar.upgrade() { - bar.tick(); - std::thread::sleep(Duration::from_millis(100)); + // Tests in each round. + static TESTS: usize = 5; + + let mut baseline_results = Vec::with_capacity(args.rounds); + let mut hello_world_results = Vec::with_capacity(args.rounds); + let mut function_router_results = Vec::with_capacity(args.rounds); + let mut concurrent_tasks_results = Vec::with_capacity(args.rounds); + let mut many_provider_results = Vec::with_capacity(args.rounds); + + let bar = (!args.no_term).then(|| create_progress(TESTS, args.rounds)); + + local_set + .run_until(async { + for i in 0..=args.rounds { + if let Some(ref bar) = bar { + bar.set_message(i.to_string()); + if i == 0 { + bar.set_prefix("Warming up"); + } else { + bar.set_prefix("Running "); + } } - }); - } - bar.set_style( - ProgressStyle::default_bar() - .template(&format!( - "{{spinner:.green}} {{prefix}} [{{elapsed_precise}}] [{{bar:40.cyan/blue}}] \ - round {{msg}}/{}", - ROUND - )) - // .tick_chars("-\\|/") - .progress_chars("=>-"), - ); - - Some(bar) - }; - - for i in 0..=ROUND { - if let Some(ref bar) = bar { - bar.set_message(i.to_string()); - if i == 0 { - bar.set_prefix("Warming up"); - } else { - bar.set_prefix("Running "); - } - } + let dur = bench_baseline(); + if i > 0 { + baseline_results.push(dur); + if let Some(ref bar) = bar { + bar.inc(1); + } + } - let dur = bench_baseline(); - if i > 0 { - baseline_results.push(dur); - if let Some(ref bar) = bar { - bar.inc(1); - } - } + let dur = bench_hello_world().await; + if i > 0 { + hello_world_results.push(dur); + if let Some(ref bar) = bar { + bar.inc(1); + } + } - let dur = bench_hello_world(); - if i > 0 { - hello_world_results.push(dur); - if let Some(ref bar) = bar { - bar.inc(1); - } - } + let dur = bench_router_app().await; + if i > 0 { + function_router_results.push(dur); + if let Some(ref bar) = bar { + bar.inc(1); + } + } - let dur = bench_router_app(); - if i > 0 { - function_router_results.push(dur); - if let Some(ref bar) = bar { - bar.inc(1); + let dur = bench_concurrent_task().await; + if i > 0 { + concurrent_tasks_results.push(dur); + if let Some(ref bar) = bar { + bar.inc(1); + } + } + + let dur = bench_many_providers().await; + if i > 0 { + many_provider_results.push(dur); + if let Some(ref bar) = bar { + bar.inc(1); + } + } } - } - } + }) + .await; if let Some(ref bar) = bar { bar.finish_and_clear(); } drop(bar); - baseline_results.sort(); - hello_world_results.sort(); - function_router_results.sort(); - - let base_var: Variance = baseline_results - .iter() - .cloned() - .map(dur_to_millis) - .collect(); - - let hw_var: Variance = hello_world_results - .iter() - .cloned() - .map(dur_to_millis) - .collect(); - - let fr_var: Variance = function_router_results - .iter() - .cloned() - .map(dur_to_millis) - .collect(); - let output = [ - Statistics { - name: "Baseline".into(), - round: ROUND.to_string(), - min: format!("{:.3}", dur_to_millis(baseline_results[0])), - max: format!("{:.3}", dur_to_millis(baseline_results[9])), - std_dev: format!("{:.3}", base_var.sample_variance().sqrt()), - mean: format!("{:.3}", base_var.mean()), - }, - Statistics { - name: "Hello World".into(), - round: ROUND.to_string(), - min: format!("{:.3}", dur_to_millis(hello_world_results[0])), - max: format!("{:.3}", dur_to_millis(hello_world_results[9])), - std_dev: format!("{:.3}", hw_var.sample_variance().sqrt()), - mean: format!("{:.3}", hw_var.mean()), - }, - Statistics { - name: "Function Router".into(), - round: ROUND.to_string(), - min: format!("{:.3}", dur_to_millis(function_router_results[0])), - max: format!("{:.3}", dur_to_millis(function_router_results[9])), - std_dev: format!("{:.3}", fr_var.sample_variance().sqrt()), - mean: format!("{:.3}", fr_var.mean()), - }, + Statistics::from_results("Baseline", args.rounds, baseline_results), + Statistics::from_results("Hello World", args.rounds, hello_world_results), + Statistics::from_results("Function Router", args.rounds, function_router_results), + Statistics::from_results("Concurrent Task", args.rounds, concurrent_tasks_results), + Statistics::from_results("Many Providers", args.rounds, many_provider_results), ]; - println!("{}", output.as_ref().table().with(Style::rounded())); + println!("{}", Table::new(&output).with(Style::rounded())); if let Some(ref p) = args.output_path { - let mut f = fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(p) - .expect("failed to write output."); - - f.write_all( - serde_json::to_string_pretty(&output) - .expect("failed to write output.") - .as_bytes(), - ) - .expect("failed to write output."); + let mut f = File::create(p).expect("failed to write output."); + serde_json::to_writer_pretty(&mut f, &output).expect("failed to write output."); println!(); println!("Result has been written to: {}", p.display()); diff --git a/tools/benchmark-struct/Cargo.toml b/tools/benchmark-struct/Cargo.toml index f201ba9ce75..27ccfbbdd28 100644 --- a/tools/benchmark-struct/Cargo.toml +++ b/tools/benchmark-struct/Cargo.toml @@ -8,11 +8,11 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -rand = { version = "0.8.4", features = ["small_rng"] } -getrandom = { version = "0.2.1", features = ["js"] } -wasm-bindgen = "0.2.80" -web-sys = { version = "0.3.55", features = ["Window"]} -yew = "0.19.3" +rand = { version = "0.8.5", features = ["small_rng"] } +getrandom = { version = "0.2.10", features = ["js"] } +wasm-bindgen = "0.2.87" +web-sys = { version = "0.3.64", features = ["Window"]} +yew = { version = "0.20.0", features = ["csr"], path = "../../packages/yew" } [package.metadata.wasm-pack.profile.release] wasm-opt = ['-O4'] diff --git a/tools/benchmark-struct/package-lock.json b/tools/benchmark-struct/package-lock.json index 7d51555197f..ad757df6b12 100644 --- a/tools/benchmark-struct/package-lock.json +++ b/tools/benchmark-struct/package-lock.json @@ -1,8 +1,170 @@ { "name": "js-framework-benchmark-non-keyed-yew", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "js-framework-benchmark-non-keyed-yew", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "cpr": "^3.0.1", + "rimraf": "^2.6.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cpr": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cpr/-/cpr-3.0.1.tgz", + "integrity": "sha512-Xch4PXQ/KC8lJ+KfJ9JI6eG/nmppLrPPWg5Q+vh65Qr9EjuJEubxh/H/Le1TmCZ7+Xv7iJuNRqapyOFZB+wsxA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.5", + "minimist": "^1.2.0", + "mkdirp": "~0.5.1", + "rimraf": "^2.5.4" + }, + "bin": { + "cpr": "bin/cpr" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + } + }, "dependencies": { "balanced-match": { "version": "1.0.2", @@ -23,13 +185,13 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "cpr": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cpr/-/cpr-3.0.1.tgz", - "integrity": "sha1-uaVQOLfNgaNcF7l2GJW9hJau8eU=", + "integrity": "sha512-Xch4PXQ/KC8lJ+KfJ9JI6eG/nmppLrPPWg5Q+vh65Qr9EjuJEubxh/H/Le1TmCZ7+Xv7iJuNRqapyOFZB+wsxA==", "dev": true, "requires": { "graceful-fs": "^4.1.5", @@ -41,33 +203,33 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "requires": { "once": "^1.3.0", @@ -81,9 +243,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -96,18 +258,18 @@ "dev": true }, "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "requires": { - "minimist": "^1.2.5" + "minimist": "^1.2.6" } }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "requires": { "wrappy": "1" @@ -116,7 +278,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true }, "rimraf": { @@ -131,7 +293,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true } } diff --git a/tools/benchmark-struct/package.json b/tools/benchmark-struct/package.json index 27cfaadf715..db741af2e88 100644 --- a/tools/benchmark-struct/package.json +++ b/tools/benchmark-struct/package.json @@ -1,10 +1,14 @@ { - "name": "js-framework-benchmark-non-keyed-yew", + "name": "js-framework-benchmark-keyed-yew", "version": "1.0.0", "description": "Benchmark for Yew", "license": "ISC", "js-framework-benchmark": { - "frameworkVersion": "latest" + "frameworkVersion": "latest", + "frameworkHomeURL": "https://yew.rs/", + "issues": [ + 1139 + ] }, "scripts": { "build-prod": "echo This is a no-op. && echo Due to heavy dependencies, the generated javascript is already provided. && echo If you really want to rebuild from source use: && echo npm run build-prod-force", diff --git a/tools/benchmark-struct/src/lib.rs b/tools/benchmark-struct/src/lib.rs index c9c805fd47a..d808460453b 100644 --- a/tools/benchmark-struct/src/lib.rs +++ b/tools/benchmark-struct/src/lib.rs @@ -202,7 +202,7 @@ impl Component for Jumbotron {
    -

    { "Yew-Hooks" }

    +

    { "Yew" }

    @@ -257,7 +257,7 @@ impl Component for Row { } } - fn changed(&mut self, ctx: &Context) -> bool { + fn changed(&mut self, ctx: &Context, _: &Self::Properties) -> bool { let id = ctx.props().data.id; self.on_select = ctx.props().on_select.reform(move |_| id); self.on_remove = ctx.props().on_remove.reform(move |_| id); @@ -286,5 +286,5 @@ impl Component for Row { pub fn start() { let document = window().unwrap().document().unwrap(); let mount_el = document.query_selector("#main").unwrap().unwrap(); - yew::start_app_in_element::(mount_el); + yew::Renderer::::with_root(mount_el).render(); } diff --git a/tools/changelog/Cargo.toml b/tools/changelog/Cargo.toml index 1f2afa64bff..03c9bd110d4 100644 --- a/tools/changelog/Cargo.toml +++ b/tools/changelog/Cargo.toml @@ -9,11 +9,11 @@ edition = "2021" [dependencies] anyhow = "1" chrono = "0.4" -git2 = "0.14" +git2 = "0.17" regex = "1" reqwest = { version = "0.11", features = ["blocking", "json"] } serde = { version = "1", features = ["derive"] } -strum = { version = "0.24", features = ["derive"] } -clap = { version = "3", features = ["derive"] } +strum = { version = "0.25", features = ["derive"] } +clap = { version = "4", features = ["derive"] } semver = "1.0" once_cell = "1" diff --git a/tools/changelog/src/cli.rs b/tools/changelog/src/cli.rs index 5eaf66c140a..4d1e434d3f7 100644 --- a/tools/changelog/src/cli.rs +++ b/tools/changelog/src/cli.rs @@ -27,7 +27,7 @@ pub struct Cli { pub to: String, /// Path to changelog file - #[clap(short = 'f', long, default_value = "CHANGELOG.md")] + #[clap(short = 'f', long, default_value = "../CHANGELOG.md")] pub changelog_path: String, /// Skip writing changelog file @@ -72,7 +72,7 @@ impl Cli { let from_ref = match from { Some(some) => some, - None => format!("refs/tags/{}-v{}", package, latest_version), + None => format!("refs/tags/{package}-v{latest_version}"), }; (from_ref, next_version) }; diff --git a/tools/changelog/src/create_log_line.rs b/tools/changelog/src/create_log_line.rs index 28a06bf1127..29afafaf54c 100644 --- a/tools/changelog/src/create_log_line.rs +++ b/tools/changelog/src/create_log_line.rs @@ -6,12 +6,10 @@ use once_cell::sync::Lazy; use regex::Regex; use crate::github_issue_labels_fetcher::GitHubIssueLabelsFetcher; -use crate::github_user_fetcher::GitHubUsersFetcher; use crate::log_line::LogLine; static REGEX_FOR_ISSUE_ID_CAPTURE: Lazy = Lazy::new(|| Regex::new(r"\s*\(#(\d+)\)").unwrap()); -static GITHUB_USERS_FETCHER: Lazy> = Lazy::new(Default::default); static GITHUB_ISSUE_LABELS_FETCHER: Lazy> = Lazy::new(Default::default); @@ -21,6 +19,7 @@ pub fn create_log_line( oid: Result, token: Option, ) -> Result> { + println!("Commit oid: {oid:?}"); let oid = oid?; let commit = repo.find_commit(oid)?; let commit_first_line = commit @@ -31,9 +30,16 @@ pub fn create_log_line( .context("Missing commit message")? .to_string(); let author = commit.author(); + let author_name = author.name().unwrap_or("Unknown"); let email = author.email().context("Missing author's email")?; - if email.contains("dependabot") || email.contains("github-action") { + if email.contains("dependabot") { + println!("email contains dependabot"); + return Ok(None); + } + + if email.contains("github-action") { + println!("email contains github-action"); return Ok(None); } @@ -44,7 +50,7 @@ pub fn create_log_line( let captures = match mb_captures { Some(some) => some, None => { - eprintln!("Missing issue for commit: {}", oid); + eprintln!("Missing issue for commit: {oid}"); return Ok(None); } }; @@ -61,32 +67,36 @@ pub fn create_log_line( .as_str() .to_string(); - let user = GITHUB_USERS_FETCHER - .lock() - .map_err(|err| anyhow!("Failed to lock GITHUB_USERS_FETCHER: {}", err))? - .fetch_user_by_commit_author(email, oid.to_string(), token.clone()) - .with_context(|| format!("Could not find GitHub user for commit: {}", oid))? - .to_string(); - let issue_labels = GITHUB_ISSUE_LABELS_FETCHER .lock() - .map_err(|err| anyhow!("Failed to lock GITHUB_ISSUE_LABELS_FETCHER: {}", err))? + .map_err(|err| anyhow!("Failed to lock GITHUB_ISSUE_LABELS_FETCHER: {err}"))? .fetch_issue_labels(issue_id.clone(), token) - .with_context(|| format!("Could not find GitHub labels for issue: {}", issue_id))?; + .with_context(|| format!("Could not find GitHub labels for issue: {issue_id}"))?; let is_issue_for_this_package = issue_labels - .into_iter() + .iter() .any(|label| package_labels.contains(&label.as_str())); if !is_issue_for_this_package { + println!("Issue {issue_id} is not for {package_labels:?} packages"); + let leftovers = issue_labels.iter().filter(|label| { + !(label.starts_with("A-") || *label == "documentation" || *label == "meta") + }); + let count = leftovers.count(); + if count > 0 { + println!("Potentially invalidly labeled issue: {issue_id}. Neither A-* (area), documentation nor meta labels found. \ + inspect/re-tag at https://github.com/yewstack/yew/issues/{issue_id}"); + } return Ok(None); } let log_line = LogLine { message, - user, + user: author_name.to_string(), issue_id, }; + println!("{log_line:?}"); + Ok(Some(log_line)) } diff --git a/tools/changelog/src/create_log_lines.rs b/tools/changelog/src/create_log_lines.rs index 6377dc792fe..9f859ff825b 100644 --- a/tools/changelog/src/create_log_lines.rs +++ b/tools/changelog/src/create_log_lines.rs @@ -28,7 +28,6 @@ pub fn create_log_lines( revwalk.push(to_oid)?; revwalk - .into_iter() .filter_map(|oid| create_log_line(&repo, package_labels, oid, token.clone()).transpose()) .collect() } diff --git a/tools/changelog/src/get_latest_version.rs b/tools/changelog/src/get_latest_version.rs index a1634877d5f..56e54be832a 100644 --- a/tools/changelog/src/get_latest_version.rs +++ b/tools/changelog/src/get_latest_version.rs @@ -5,8 +5,8 @@ use semver::{Error, Version}; use crate::yew_package::YewPackage; pub fn get_latest_version(package: &YewPackage) -> Result { - let common_tag_pattern = format!("{}-v", package); - let search_pattern = format!("{}*", common_tag_pattern); + let common_tag_pattern = format!("{package}-v"); + let search_pattern = format!("{common_tag_pattern}*"); let mut tags: Vec = Repository::open_from_env()? .tag_names(Some(&search_pattern))? diff --git a/tools/changelog/src/github_fetch.rs b/tools/changelog/src/github_fetch.rs index 7f6f07ca297..8b759da5bb6 100644 --- a/tools/changelog/src/github_fetch.rs +++ b/tools/changelog/src/github_fetch.rs @@ -10,7 +10,7 @@ pub fn github_fetch(url: &str, token: Option) -> Re thread::sleep(Duration::from_secs(1)); let mut optional_headers = HeaderMap::new(); if let Some(token) = token { - optional_headers.insert(AUTHORIZATION, format!("Bearer {}", token).parse().unwrap()); + optional_headers.insert(AUTHORIZATION, format!("Bearer {token}").parse().unwrap()); } let request_client = Client::new(); @@ -27,7 +27,7 @@ pub fn github_fetch(url: &str, token: Option) -> Re bail!("GitHub API limit reached."); } } - bail!("GitHub API request error: {}", status); + bail!("GitHub API request error: {status}"); } Ok(resp.json()?) } diff --git a/tools/changelog/src/github_issue_labels_fetcher.rs b/tools/changelog/src/github_issue_labels_fetcher.rs index 76dccf7a084..276aca39f97 100644 --- a/tools/changelog/src/github_issue_labels_fetcher.rs +++ b/tools/changelog/src/github_issue_labels_fetcher.rs @@ -26,7 +26,7 @@ impl GitHubIssueLabelsFetcher { .or_insert_with(|| match Self::inner_fetch(&issue, token) { Ok(labels) => labels, Err(err) => { - eprintln!("fetch_issue_labels Error: {}", err); + eprintln!("fetch_issue_labels Error: {err}"); None } }) @@ -34,10 +34,7 @@ impl GitHubIssueLabelsFetcher { } fn inner_fetch(q: &str, token: Option) -> Result>> { - let url = format!( - "https://api.github.com/repos/yewstack/yew/issues/{}/labels", - q, - ); + let url = format!("https://api.github.com/repos/yewstack/yew/issues/{q}/labels"); let body: Vec = github_fetch(&url, token)?; let label_names: Vec = body.into_iter().map(|label| label.name).collect(); Ok(Some(label_names)) diff --git a/tools/changelog/src/github_user_fetcher.rs b/tools/changelog/src/github_user_fetcher.rs index b6bb8944f59..0843c65194e 100644 --- a/tools/changelog/src/github_user_fetcher.rs +++ b/tools/changelog/src/github_user_fetcher.rs @@ -32,7 +32,7 @@ impl GitHubUsersFetcher { .or_insert_with(|| match Self::inner_fetch(commit, token) { Ok(value) => value, Err(err) => { - eprintln!("fetch_user_by_commit_author Error: {}", err); + eprintln!("fetch_user_by_commit_author Error: {err}"); None } }) diff --git a/tools/changelog/src/log_line.rs b/tools/changelog/src/log_line.rs index d5ee1b6f3f5..e8d5596fd7c 100644 --- a/tools/changelog/src/log_line.rs +++ b/tools/changelog/src/log_line.rs @@ -1,3 +1,4 @@ +#[derive(Debug)] pub struct LogLine { pub message: String, pub user: String, diff --git a/tools/changelog/src/write_changelog_file.rs b/tools/changelog/src/write_changelog_file.rs index 23d12c3a8b7..4eb95d99943 100644 --- a/tools/changelog/src/write_changelog_file.rs +++ b/tools/changelog/src/write_changelog_file.rs @@ -6,17 +6,17 @@ use anyhow::{Context, Result}; pub fn write_changelog(changelog_path: &str, version_changelog: &[u8]) -> Result<()> { let old_changelog = File::open(changelog_path) - .context(format!("could not open {} for reading", changelog_path))?; + .context(format!("could not open {changelog_path} for reading"))?; let old_changelog_reader = BufReader::new(old_changelog); - let changelog_path_new = &format!("{}.new", changelog_path); + let changelog_path_new = &format!("../{changelog_path}.new"); let mut new_changelog = fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(changelog_path_new) - .context(format!("could not open {} for writing", changelog_path_new))?; + .context(format!("could not open {changelog_path_new} for writing"))?; new_changelog.write_all(version_changelog)?; @@ -26,10 +26,9 @@ pub fn write_changelog(changelog_path: &str, version_changelog: &[u8]) -> Result drop(new_changelog); - fs::remove_file(changelog_path).context(format!("Could not delete {}", changelog_path))?; + fs::remove_file(changelog_path).context(format!("Could not delete {changelog_path}"))?; fs::rename(changelog_path_new, changelog_path).context(format!( - "Could not replace {} with {}", - changelog_path, changelog_path_new + "Could not replace {changelog_path} with {changelog_path_new}" ))?; Ok(()) diff --git a/tools/changelog/src/write_log_lines.rs b/tools/changelog/src/write_log_lines.rs index 12260c2460f..152dcf0735d 100644 --- a/tools/changelog/src/write_log_lines.rs +++ b/tools/changelog/src/write_log_lines.rs @@ -14,11 +14,8 @@ pub fn write_log_lines(log_lines: Vec) -> Result> { { writeln!( logs_list, - "- {message}. [[@{user}](https://github.com/{user}), [#{issue_id}](https://github.com/yewstack/yew/pull/{issue_id})]", - message = message, - user = user, - issue_id = issue_id - )?; + "- {message}. [[@{user}](https://github.com/{user}), [#{issue_id}](https://github.com/yewstack/yew/pull/{issue_id})]", + )?; } Ok(logs_list) } diff --git a/tools/website-test/Cargo.toml b/tools/website-test/Cargo.toml index fd0664feed7..f6f61e59ae9 100644 --- a/tools/website-test/Cargo.toml +++ b/tools/website-test/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" build = "build.rs" publish = false +rust-version = "1.62" [dependencies] yew-agent = { path = "../../packages/yew-agent/" } @@ -18,7 +19,7 @@ wasm-bindgen-futures = "0.4" weblog = "0.3.0" yew = { path = "../../packages/yew/", features = ["ssr", "csr"] } yew-router = { path = "../../packages/yew-router/" } -tokio = { version = "1.15.0", features = ["rt", "macros"] } +tokio = { version = "1.29.0", features = ["rt", "macros"] } [dev-dependencies.web-sys] version = "0.3" diff --git a/tools/website-test/build.rs b/tools/website-test/build.rs index 8f3ac705cdb..45b5746cdbc 100644 --- a/tools/website-test/build.rs +++ b/tools/website-test/build.rs @@ -13,10 +13,10 @@ struct Level { fn main() { let home = env::var("CARGO_MANIFEST_DIR").unwrap(); - let pattern = format!("{}/../../website/docs/**/*.md*", home); - let base = format!("{}/../../website", home); + let pattern = format!("{home}/../../website/docs/**/*.md*"); + let base = format!("{home}/../../website"); let base = Path::new(&base).canonicalize().unwrap(); - let dir_pattern = format!("{}/../../website/docs/**", home); + let dir_pattern = format!("{home}/../../website/docs/**"); for dir in glob(&dir_pattern).unwrap() { println!("cargo:rerun-if-changed={}", dir.unwrap().display()); } @@ -40,7 +40,7 @@ fn main() { let out = format!("{}/website_tests.rs", env::var("OUT_DIR").unwrap()); - fs::write(&out, level.to_contents()).unwrap(); + fs::write(out, level.to_contents()).unwrap(); } impl Level { @@ -63,7 +63,7 @@ impl Level { fn write_into(&self, dst: &mut String, name: &str, level: usize) -> fmt::Result { self.write_space(dst, level); let name = name.replace(|c| c == '-' || c == '.', "_"); - writeln!(dst, "pub mod {} {{", name)?; + writeln!(dst, "pub mod {name} {{")?; self.write_inner(dst, level + 1)?; @@ -92,7 +92,7 @@ impl Level { writeln!(dst, "#[doc = include_str!(r\"{}\")]", file.display())?; self.write_space(dst, level); - writeln!(dst, "pub fn {}_md() {{}}", stem)?; + writeln!(dst, "pub fn {stem}_md() {{}}")?; } Ok(()) diff --git a/tools/website-test/src/tutorial.rs b/tools/website-test/src/tutorial.rs index 393110051d2..86a04e262a3 100644 --- a/tools/website-test/src/tutorial.rs +++ b/tools/website-test/src/tutorial.rs @@ -1,4 +1,4 @@ -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Eq)] pub struct Video { pub id: usize, pub title: String, diff --git a/website/blog/2022-11-24-release-0-20.md b/website/blog/2022-11-24-release-0-20.md new file mode 100644 index 00000000000..5b9e480a1f5 --- /dev/null +++ b/website/blog/2022-11-24-release-0-20.md @@ -0,0 +1,33 @@ +--- +title: Releasing Yew 0.20 +authors: [hamza] tags: [release] +--- + +The Yew team is happy to announce a new, long overdue, version of Yew: v0.20. +Yew is a framework for creating reliable and efficient web applications. + + + +## What's new + +This release comes with new features aimed at improving the user experience, such as server-rendering and render-as-you-fetch approach for data-fetching. + +### SSR + +Yew now fully supports rendering on the server. Rendering on the server means users will get a rendered HTML and will not have to wait to be able to see anything until the entire WebAssembly bundle is downloaded and initial render has completed. With SSR, the page will be visible instantly, and interactable as soon as hydration finishes. + +Learn more at [Server-side rendering](/docs/advanced-topics/server-side-rendering) + +### Data fetching + +With SSR comes new ways of data-fetching. The newly added [`use_prepared_state!`](https://api.yew.rs/next/yew/functional/macro.use_prepared_state.html) hook can be used to fetch data while rendering on the server and seemlessly use it in the component. + +For client-side fetching, Yew now supports render-as-you-fetch approach with [Suspense](/docs/concepts/suspense). + +## How to upgrade + +There have been breaking changes in this release. Our [migration guides](/docs/migration-guides/yew/from-0_19_0-to-0_20_0) go over how to upgrade each over of the new crates. + +## Thanks! + +Many people came together to help make this release happen. We couldn't have done it without all of you. Thanks! diff --git a/website/community/awesome.md b/website/community/awesome.md index cf203e3dcb0..d5d881d7fd1 100644 --- a/website/community/awesome.md +++ b/website/community/awesome.md @@ -30,25 +30,29 @@ description: 'Community projects built using yew' - [DevAndDev](https://github.com/alepez/devand) - A website where developers can find pair-programming partners. Written in Rust, Yew frontend. - [yew-octicons](https://github.com/io12/yew-octicons) - An easy interface for using Octicons in Yew projects. - [Pipe](https://github.com/pipe-fun/pipe) - This is a Rust / Wasm client web app which is a task control center. -- [note-to-yew](https://github.com/GalAster/note-to-yew) - Convert your markups into Yew macro online, which is also made by Yew. +- [note-to-yew](https://github.com/oovm/note-to-yew) - Convert your markups into Yew macro online, which is also made by Yew. - [ASCII-Hangman](https://github.com/getreu/ascii-hangman) - Configurable Hangman game for children with ASCII-art rewarding. - [dotdotyew](https://github.com/shaunbennett/dotdotyew) - [Dot-voting](https://en.wikipedia.org/wiki/Dot-voting) using Yew, with Rust powering the backend API. - [wasm-2048](https://github.com/dev-family/wasm-2048) - 2048 game implemented with Rust and Yew and compiled to Wasm. - [website-wasm](https://github.com/kamiyaa/website-wasm) - My personal website written in Rust via Yew/Wasm. - [KeyPress](https://github.com/rayylee/keypress) - A Rust WebAssembly Website example for practising english for chinese. - [yew-train-ticket](https://github.com/anthhub/yew-train-ticket) - A Rust WebAssembly Webapp example basing Yew newest hooks and functional API, the code style is extremely like React Function Component. -- [yew-d3-example](https://github.com/i-schuetz/yew-d3-example) - Showing a d3 chart with Yew. +- [yew-d3-example](https://github.com/ivanschuetz/yew-d3-example) - Showing a d3 chart with Yew. - [Oxfeed](https://github.com/sanpii/oxfeed) - A feed reader written in Rust with a Yew frontend. - [Flow.er](https://github.com/LighghtEeloo/flow.er) - A notebook app integrated with todo lists utility. Developed with Rust, WebAssembly, Yew and Trunk. - [Fullstack-Rust](https://github.com/vascokk/fullstack-rust) - A Full Stack Rust application (Connect5 game) with Actix-web, Yew, Bulma CSS and Diesel. - [Sea_battle](https://github.com/MAE664128/sea_battle) - A simple example of a sea battle game. Rust + Yew. - [tide-async-graphql-mongodb](https://github.com/zzy/tide-async-graphql-mongodb) - Clean boilerplate for graphql services, with wasm/yew frontend. -- [surfer](https://github.com/zzy/surfer) - A blog built on yew + graphql, with [live demo site](https://gaiding.com). Backend for graphql services, and frontend for web application. +- [surfer](https://github.com/zzy/surfer) - A blog built on yew + graphql, with [live demo site](https://niqin.com). Backend for graphql services, and frontend for web application. - [qubit](https://abhimanyu003.github.io/qubit) - A handy calculator, based on Rust and WebAssembly, [Live Demo](https://abhimanyu003.github.io/qubit/). - [Paudle](https://github.com/pmsanford/paudle) - A reimplementation of the excellent word game Wordle by Josh Wardle. - [Rust algorithms](https://github.com/Jondolf/rust-algorithms) - A website with interactive implementations of various algorithms (only sorting algorithms for now). - [Marc Portfolio](https://gitlab.com/marcempunkt/maeurerdev) - A software developer portfolio, [Live Demo](https://maeurer.dev/). -- [zzhack](https://github.com/youncccat/zzhack) - A personal blog, based on Rust & Yew, [Live Demo](https://www.zzhack.fun/technology). +- [zzhack](https://github.com/zzhack-stack/zzhack) - A personal blog, based on Rust & Yew, [Live Demo](https://www.zzhack.fun/technology). +- [viz.rs](https://github.com/viz-rs/viz-rs.github.io) - A website for viz web framework, [Live Demo](https://viz.rs/). +- [hurlurl](https://github.com/lucasmerlin/hurlurl) - A randomizing link shortener, [Live Demo](https://hurlurl.com/). +- [Macige](https://github.com/tramlinehq/macige) - CI workflow generator for mobile app development, [Live Demo](https://macige.tramline.app). +- [Spaceman](https://github.com/eliaperantoni/spaceman) - A cross-platform and graphical client for the gRPC communication protocol. ## Templates @@ -78,6 +82,7 @@ description: 'Community projects built using yew' - [Yew Form](https://github.com/jfbilodeau/yew_form) - Components to simplify handling forms with Yew. - [yew-component-size](https://github.com/AircastDev/yew-component-size) - A Yew component that emits events when the parent component changes width/height. - [yew-virtual-scroller](https://github.com/AircastDev/yew-virtual-scroller) - A Yew component for virtual scrolling / scroll windowing. +- [yew-autoprops](https://crates.io/crates/yew-autoprops) - proc-macro to automatically derive Properties structs from args for Yew components. ### Hooks diff --git a/website/community/external-libs.mdx b/website/community/external-libs.mdx index 29d1d3c22d8..ea3229ca922 100644 --- a/website/community/external-libs.mdx +++ b/website/community/external-libs.mdx @@ -22,14 +22,10 @@ libraries with Rust and Wasm. Gloo provides ergonomic Rust APIs for working with - [Dialogs](https://crates.io/crates/gloo-dialogs) - [Events](https://crates.io/crates/gloo-events) - [Files](https://crates.io/crates/gloo-file) +- [Requests](https://crates.io/crates/gloo-net) - [Timers](https://crates.io/crates/gloo-timers) - [Web Storage](https://crates.io/crates/gloo-storage) -## Reqwasm - -[Reqwasm](https://crates.io/crates/reqwasm) is an HTTP requests library for WASM Apps. -It provides idiomatic Rust API for the browser's `fetch` and `WebSocket` API. - ## Looking For Libraries that the ecosystem needs, but doesn't have yet. diff --git a/website/docs/advanced-topics/how-it-works.mdx b/website/docs/advanced-topics/how-it-works.mdx index 2415e5f1fda..2a281690057 100644 --- a/website/docs/advanced-topics/how-it-works.mdx +++ b/website/docs/advanced-topics/how-it-works.mdx @@ -15,25 +15,25 @@ update of `yew-macro`, the generated code will be more efficient and handle any without many (if any) modifications to the `html!` syntax. Because the `html!` macro allows you to write code in a declarative style, your UI layout code will -closely match the HTML that is generated to the page. This becomes increasingly useful as your -application gets more interactive and your codebase gets larger. Rather than manually writing the +closely match the HTML that is generated for the page. This becomes increasingly useful as your +application gets more interactive and your codebase gets larger. Rather than manually writing all of the code to manipulate the DOM yourself, the macro will handle it for you. -Using the `html!` macro can feel pretty magic, but it has nothing to hide. If you're curious about -how it works, try expanding the `html!` macro calls in your program. There's a useful command called -`cargo expand` which allows you to see the expansion of Rust macros. `cargo expand` isn't shipped with -`cargo` by default so you'll need to install it with `cargo install cargo-expand` if you haven't +Using the `html!` macro can feel pretty magical, but it has nothing to hide. If you are curious about +how it works, try expanding the `html!` macro calls in your program. There is a useful command called +`cargo expand` which allows you to see the expansion of Rust macros. `cargo expand` does not ship with +`cargo` by default so you will need to install it with `cargo install cargo-expand` if you have not already. [Rust-Analyzer](https://rust-analyzer.github.io/) also provides a mechanism for [obtaining macro output from within an IDE](https://rust-analyzer.github.io/manual.html#expand-macro-recursively). Output from the `html!` macro is often pretty terse! This is a feature: machine-generated code can -sometimes clash with other code in an application. In order to prevent issues, `proc_macro` +sometimes clash with other code in an application. To prevent issues, `proc_macro` "hygiene" is adhered to. Some examples include: 1. Instead of using `yew::` the macro generates `::yew::` to make sure that the Yew package is referenced correctly. This is also why `::alloc::vec::Vec::new()` is called instead of just `Vec::new()`. -2. Due to potential trait method name collisions, `` is used to make sure that we're +2. Due to potential trait method name collisions, `` is used to make sure that we are using members from the correct trait. ## What is a virtual DOM? @@ -43,11 +43,11 @@ for your web page. A "virtual" DOM is simply a copy of the DOM that is held in a a virtual DOM results in a higher memory overhead, but allows for batching and faster reads by avoiding or delaying the use of browser APIs. -Having a copy of the DOM in memory can be really helpful for libraries which promote the use of +Having a copy of the DOM in memory can be helpful for libraries that promote the use of declarative UIs. Rather than needing specific code for describing how the DOM should be modified in response to a user event, the library can use a generalized approach with DOM "diffing". When a Yew component is updated and wants to change how it is rendered, the Yew library will build a second copy -of the virtual DOM and directly compare to a virtual DOM which mirrors what is currently on screen. +of the virtual DOM and directly compare it to a virtual DOM which mirrors what is currently on screen. The "diff" (or difference) between the two can be broken down into incremental updates and applied in a batch with browser APIs. Once the updates are applied, the old virtual DOM copy is discarded and the new copy is saved for future diff checks. diff --git a/website/docs/advanced-topics/immutable.mdx b/website/docs/advanced-topics/immutable.mdx index 0ae35885c37..332ed94164f 100644 --- a/website/docs/advanced-topics/immutable.mdx +++ b/website/docs/advanced-topics/immutable.mdx @@ -12,7 +12,7 @@ to update a value, you must instantiate a new value. Properties, like in React, are propagated from ancestors to children. This means that the properties must live when each component is -updated. This is why properties should —ideally— be cheap to clone. In order to +updated. This is why properties should —ideally— be cheap to clone. To achieve this we usually wrap things in `Rc`. Immutable types are a great fit for holding property's values because they can diff --git a/website/docs/advanced-topics/optimizations.mdx b/website/docs/advanced-topics/optimizations.mdx index 700559606b4..9aef595c93b 100644 --- a/website/docs/advanced-topics/optimizations.mdx +++ b/website/docs/advanced-topics/optimizations.mdx @@ -9,7 +9,7 @@ description: 'Make your app faster' **Note: if you're unsure about some of the terms used in this section, the Rust Book has a useful [chapter about smart pointers](https://doc.rust-lang.org/book/ch15-00-smart-pointers.html).** -In an effort to avoid cloning large amounts of data to create props when re-rendering, we can use +To avoid cloning large amounts of data to create props when re-rendering, we can use smart pointers to only clone a reference to the data instead of the data itself. If you pass references to the relevant data in your props and child components instead of the actual data you can avoid cloning any data until you need to modify it in the child component, where you can @@ -31,12 +31,12 @@ data cheaply, then it isn't worth putting it behind a smart pointer. For structu can be data-heavy like `Vec`s, `HashMap`s, and `String`s using smart pointers is likely to bring performance improvements. -This optimization works best if the values are never updated by the children, and even better, if +This optimization works best if the values are never updated by the children, and even better if they are rarely updated by parents. This makes `Rc<_>s` a good choice for wrapping property values -in for pure components. +in pure components. However, it must be noted that unless you need to clone the data yourself in the child component, -this optimization is not only useless, it also adds unnecessary cost of reference counting. Props +this optimization is not only useless, but it also adds the unnecessary cost of reference counting. Props in Yew are already reference counted and no data clones occur internally. ## View functions @@ -62,45 +62,32 @@ identical props. Yew compares the props internally and so the UI is only re-rend Arguably, the largest drawback to using Yew is the long time it takes to compile Yew apps. The time taken to compile a project seems to be related to the quantity of code passed to the `html!` macro. -This tends to not be much of an issue for smaller projects, but for larger applications it makes +This tends to not be much of an issue for smaller projects, but for larger applications, it makes sense to split code across multiple crates to minimize the amount of work the compiler has to do for each change made to the application. One possible approach is to make your main crate handle routing/page selection, and then make a -different crate for each page, where each page could be a different component, or just a big -function that produces `Html`. Code which is shared between the crates containing different parts of -the application could be stored in a separate crate which is depended on throughout the project. -In the best case scenario, you go from rebuilding all of your code on each compile to rebuilding +different crate for each page, where each page could be a different component or just a big +function that produces `Html`. Code that is shared between the crates containing different parts of +the application could be stored in a separate crate which the project depends on. +In the best-case scenario, you go from rebuilding all of your code on each compile to rebuilding only the main crate, and one of your page crates. In the worst case, where you edit something in the "common" crate, you will be right back to where you started: compiling all code that depends on that commonly shared crate, which is probably everything else. If your main crate is too heavyweight, or you want to rapidly iterate on a deeply nested page \(eg. a page that renders on top of another page\), you can use an example crate to create a simplified -implementation of the main page and render the component you are working on on top of that. +implementation of the main page and additionally render the component you are working on. ## Reducing binary sizes - optimize Rust code - - `wee_alloc` \( using tiny allocator \) - - `cargo.toml` \( defining release profile \) +- `cargo.toml` \( defining release profile \) - optimize wasm code using `wasm-opt` **Note: more information about reducing binary sizes can be found in the [Rust Wasm Book](https://rustwasm.github.io/book/reference/code-size.html#optimizing-builds-for-code-size).** -### wee_alloc - -[wee_alloc](https://github.com/rustwasm/wee_alloc) is a tiny allocator that is much smaller than the allocator that is normally used in Rust binaries. Replacing the default allocator with this one will result in smaller Wasm file sizes, at the expense of speed and memory overhead. - -The slower speed and memory overhead are minor in comparison to the size gains made by not including the default allocator. This smaller file size means that your page will load faster, and so it is generally recommended that you use this allocator over the default, unless your app is doing some allocation-heavy work. - -```rust ,ignore -// Use `wee_alloc` as the global allocator. -#[global_allocator] -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; -``` - ### Cargo.toml It is possible to configure release builds to be smaller using the available settings in the @@ -146,7 +133,7 @@ that require occasional attention and tweaking. Use these experimental options w ### wasm-opt -Further more it is possible to optimize size of `wasm` code. +Further, it is possible to optimize the size of `wasm` code. The Rust Wasm Book has a section about reducing the size of Wasm binaries: [Shrinking .wasm size](https://rustwasm.github.io/book/game-of-life/code-size.html) diff --git a/website/docs/advanced-topics/portals.mdx b/website/docs/advanced-topics/portals.mdx index a8dae1e80a9..3a5d1de28bd 100644 --- a/website/docs/advanced-topics/portals.mdx +++ b/website/docs/advanced-topics/portals.mdx @@ -6,7 +6,7 @@ description: 'Rendering into out-of-tree DOM nodes' ## What is a portal? Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. -`yew::create_portal(child, host)` returns a `Html` value that renders `child` not hierarchically under its parent component, +`yew::create_portal(child, host)` returns an `Html` value that renders `child` not hierarchically under its parent component, but as a child of the `host` element. ## Usage diff --git a/website/docs/advanced-topics/server-side-rendering.md b/website/docs/advanced-topics/server-side-rendering.md index b9104bed575..8fef136efda 100644 --- a/website/docs/advanced-topics/server-side-rendering.md +++ b/website/docs/advanced-topics/server-side-rendering.md @@ -5,17 +5,17 @@ description: 'Render Yew on the server-side.' # Server-side Rendering -By default, Yew components render at the client side. When a viewer -visits a website, the server sends a skeleton html file without any actual +By default, Yew components render on the client side. When a viewer +visits a website, the server sends a skeleton HTML file without any actual content and a WebAssembly bundle to the browser. -Everything is rendered at the client side by the WebAssembly +Everything is rendered on the client side by the WebAssembly bundle. This is known as client-side rendering. This approach works fine for most websites, with some caveats: 1. Users will not be able to see anything until the entire WebAssembly - bundle is downloaded and initial render has completed. - This can result in poor user experience if the user is using a slow network. + bundle is downloaded and the initial render has been completed. + This can result in a poor experience for users on a slow network. 2. Some search engines do not support dynamically rendered web content and those who do usually rank dynamic websites lower in the search results. @@ -24,9 +24,9 @@ To solve these problems, we can render our website on the server side. ## How it Works Yew provides a `ServerRenderer` to render pages on the -server-side. +server side. -To render Yew components at the server-side, you can create a renderer +To render Yew components on the server side, you can create a renderer with `ServerRenderer::::new()` and call `renderer.render().await` to render `` into a `String`. @@ -59,27 +59,26 @@ async fn no_main() { The recommended way of working with server-side rendering is function components. -All hooks other than `use_effect` (and `use_effect_with_deps`) +All hooks other than `use_effect` (and `use_effect_with`) will function normally until a component successfully renders into `Html` for the first time. :::caution Web APIs are not available! Web APIs such as `web_sys` are not available when your component is -rendering on the server-side. +rendering on the server side. Your application will panic if you try to use them. You should isolate logics that need Web APIs in `use_effect` or -`use_effect_with_deps` as effects are not executed during server side -rendering. +`use_effect_with` as effects are not executed during server-side rendering. ::: :::danger Struct Components -Whilst it's possible to use Struct Components with server-side rendering, -there's no clear boundaries between client-side safe logic like the +While it is possible to use Struct Components with server-side rendering, +there are no clear boundaries between client-side safe logic like the `use_effect` hook for function components and lifecycle events are invoked -in a different order than client side. +in a different order than the client side. In addition, Struct Components will continue to accept messages until all of its children are rendered and `destroy` method is called. Developers need to @@ -93,33 +92,32 @@ prefer function components unless you have a good reason not to. ## Data Fetching during Server-side Rendering -Data fetching is one of the difficult point with server side rendering -and hydration. +Data fetching is one of the difficult points with server-side rendering and hydration. Traditionally, when a component renders, it is instantly available -(outputs a virtual dom to be rendered). This works fine when the +(outputs a virtual DOM to be rendered). This works fine when the component does not want to fetch any data. But what happens if the component wants to fetch some data during rendering? -In the past, there's no mechanism for Yew to detect whether a component is still -fetching data. The data fetching client is responsible to implement -a solution to detect what's being requested during initial render and triggers +In the past, there was no mechanism for Yew to detect whether a component is still +fetching data. The data-fetching client is responsible to implement +a solution to detect what is being requested during the initial render and triggers a second render after requests are fulfilled. The server repeats this process until no more pending requests are added during a render before returning a response. -Not only this wastes CPU resources by repeatedly rendering components, -but the data client also needs to provide a way to make the data fetched on -the server-side available during hydration process to make sure that the -virtual dom returned by initial render is consistent with the +This not only wastes CPU resources by repeatedly rendering components, +but the data client also needs to provide a way to make the data fetched on the +server side available during the hydration process to make sure that the +virtual DOM returned by the initial render is consistent with the server-side rendered DOM tree which can be hard to implement. Yew takes a different approach by trying to solve this issue with ``. -Suspense is a special component that when used on the client-side, -provides a way to show a fallback UI while the component is fetching +Suspense is a special component that when used on the client side, provides a +way to show a fallback UI while the component is fetching data (suspended) and resumes to normal UI when the data fetching completes. -When the application is rendered on the server-side, Yew waits until a +When the application is rendered on the server side, Yew waits until a component is no longer suspended before serializing it into the string buffer. @@ -127,21 +125,21 @@ During the hydration process, elements within a `` component remains dehydrated until all of its child components are no longer suspended. -With this approach, developers can build a client-agnostic, SSR ready +With this approach, developers can build a client-agnostic, SSR-ready application with data fetching with very little effort. ## SSR Hydration Hydration is the process that connects a Yew application to the server-side generated HTML file. By default, `ServerRender` prints -hydratable html string which includes additional information to facilitate hydration. -When the `Renderer::hydrate` method is called, instead of start rendering from +hydratable HTML string which includes additional information to facilitate hydration. +When the `Renderer::hydrate` method is called, instead of starting rendering from scratch, Yew will reconcile the Virtual DOM generated by the application -with the html string generated by the server renderer. +with the HTML string generated by the server renderer. :::caution -To successfully hydrate an html representation created by the +To successfully hydrate an HTML representation created by the `ServerRenderer`, the client must produce a Virtual DOM layout that exactly matches the one used for SSR including components that do not contain any elements. If you have any component that is only useful in @@ -153,15 +151,15 @@ position of the extra component. During Hydration, components schedule 2 consecutive renders after it is created. Any effects are called after the second render completes. -It is important to make sure that the render function of the your -component is side-effect free. It should not mutate any states or trigger +It is important to make sure that the render function of your +component is free of side effects. It should not mutate any states or trigger additional renders. If your component currently mutates states or triggers -additional renders, move them into an `use_effect` hook. +additional renders, move them into a `use_effect` hook. -It's possible to use Struct Components with server-side rendering in +It is possible to use Struct Components with server-side rendering in hydration, the view function will be called multiple times before the rendered function will be called. -The DOM is considered as not connected until rendered function is called, +The DOM is considered as not connected until the rendered function is called, you should prevent any access to rendered nodes until `rendered()` method is called. @@ -190,7 +188,7 @@ Example: [ssr_router](https://github.com/yewstack/yew/tree/master/examples/ssr_r :::caution -Server-side rendering is currently experiemental. If you find a bug, please file +Server-side rendering is currently experimental. If you find a bug, please file an issue on [GitHub](https://github.com/yewstack/yew/issues/new?assignees=&labels=bug&template=bug_report.md&title=). ::: diff --git a/website/docs/advanced-topics/struct-components/callbacks.mdx b/website/docs/advanced-topics/struct-components/callbacks.mdx index 2bc611588d5..8153778f496 100644 --- a/website/docs/advanced-topics/struct-components/callbacks.mdx +++ b/website/docs/advanced-topics/struct-components/callbacks.mdx @@ -40,7 +40,7 @@ impl Component for Comp { } ``` -The function passed to `callback` must always take a parameter. For example, the `onclick` handler requires a function which takes a parameter of type `MouseEvent`. The handler can then decide what kind of message should be sent to the component. This message is scheduled for the next update loop unconditionally. +The function passed to `callback` must always take a parameter. For example, the `onclick` handler requires a function that takes a parameter of type `MouseEvent`. The handler can then decide what kind of message should be sent to the component. This message is scheduled for the next update loop unconditionally. If you need a callback that might not need to cause an update, use `batch_callback`. diff --git a/website/docs/advanced-topics/struct-components/hoc.mdx b/website/docs/advanced-topics/struct-components/hoc.mdx index 2a9487de0d3..eea2038c116 100644 --- a/website/docs/advanced-topics/struct-components/hoc.mdx +++ b/website/docs/advanced-topics/struct-components/hoc.mdx @@ -2,13 +2,13 @@ title: 'Higher Order Components' --- -There are several cases where Struct components dont directly support a feature (ex. Suspense) or require a lot of boiler plate to use the features (ex. Context). +There are several cases where Struct components do not directly support a feature (ex. Suspense) or require a lot of boilerplate code to use the features (ex. Context). -In those cases it is recommended to create function components that are higher order components. +In those cases, it is recommended to create function components that are higher-order components. ## Higher Order Components Definition -Higher Order Components are components that dont add any new Html and only wrap some other component to provide extra functionality. +Higher Order Components are components that do not add any new HTML and only wrap some other components to provide extra functionality. ### Example diff --git a/website/docs/advanced-topics/struct-components/introduction.mdx b/website/docs/advanced-topics/struct-components/introduction.mdx index 675c76be66e..311fe6ae5a1 100644 --- a/website/docs/advanced-topics/struct-components/introduction.mdx +++ b/website/docs/advanced-topics/struct-components/introduction.mdx @@ -5,22 +5,22 @@ description: 'Components in Yew' ## What are Components? -Components are the building blocks of Yew. They manage their own state and can render themselves to the DOM. +Components are the building blocks of Yew. They manage an internal state and can render elements to the DOM. Components are created by implementing the `Component` trait for a type. ## Writing Component's markup Yew uses Virtual DOM to render elements to the DOM. The Virtual DOM tree can be constructed by using the -`html!` macro. `html!` uses syntax which is similar to HTML but is not exactly the same. The rules are also -much stricter. It also provides super-powers like conditional rendering and rendering of lists using iterators. +`html!` macro. `html!` uses a syntax which is similar to HTML but is not the same. The rules are also +much stricter. It also provides superpowers like conditional rendering and rendering of lists using iterators. :::info -[Learn more about the `html!` macro, how it's used and its syntax](concepts/html/introduction.mdx) +[Learn more about the `html!` macro, how it is used and its syntax](concepts/html/introduction.mdx) ::: ## Passing data to a component -Yew components use _props_ to communicate between parent and children. A parent component may pass any data as props to +Yew components use _props_ to communicate between parents and children. A parent component may pass any data as props to its children. Props are similar to HTML attributes but any Rust type can be passed as props. :::info diff --git a/website/docs/advanced-topics/struct-components/lifecycle.mdx b/website/docs/advanced-topics/struct-components/lifecycle.mdx index fae9aa4ae0a..e9cfdbe9d2d 100644 --- a/website/docs/advanced-topics/struct-components/lifecycle.mdx +++ b/website/docs/advanced-topics/struct-components/lifecycle.mdx @@ -17,7 +17,7 @@ stages in the lifecycle of a component. ### Create When a component is created, it receives properties from its parent component and is stored within -the `Context` that's passed down to the `create` method. The properties can be used to +the `Context` that is passed down to the `create` method. The properties can be used to initialize the component's state and the "link" can be used to register callbacks or send messages to the component. ```rust @@ -137,7 +137,7 @@ impl Component for MyComponent { ``` :::tip note -Note that this lifecycle method does not require an implementation and will do nothing by default. +Note that this lifecycle method does not require implementation and will do nothing by default. ::: ### Update @@ -147,7 +147,7 @@ Communication with components happens primarily through messages which are handl based on what the message was, and determine if it needs to re-render itself. Messages can be sent by event listeners, child components, Agents, Services, or Futures. -Here's an example of what an implementation of `update` could look like: +Here is an example of what an implementation of `update` could look like: ```rust use yew::{Component, Context, html, Html}; @@ -200,8 +200,8 @@ impl Component for MyComponent { ### Changed Components may be re-rendered by their parents. When this happens, they could receive new properties -and need to re-render. This design facilitates parent to child component communication by just -changing the values of a property. There is a default implementation which re-renders the component +and need to re-render. This design facilitates parent-to-child component communication by just +changing the values of a property. There is a default implementation that re-renders the component when props are changed. ### Destroy @@ -212,8 +212,8 @@ before it is destroyed. This method is optional and does nothing by default. ### Infinite loops -Infinite loops are possible with Yew's lifecycle methods, but are only caused when trying to update -the same component after every render when that update also requests the component to be rendered. +Infinite loops are possible with Yew's lifecycle methods but are only caused when trying to update +the same component after every render, when that update also requests the component to be rendered. A simple example can be seen below: @@ -259,7 +259,7 @@ Let's run through what happens here: component needs to re-render. 7. Jump back to 2. -You can still schedule updates in the `rendered` method and it's often useful to do so, but +You can still schedule updates in the `rendered` method and it is often useful to do so, but consider how your component will terminate this loop when you do. ## Associated Types @@ -276,7 +276,7 @@ impl Component for MyComponent { ``` The `Message` type is used to send messages to a component after an event has taken place; for -example you might want to undertake some action when a user clicks a button or scrolls down the +example, you might want to undertake some action when a user clicks a button or scrolls down the page. Because components tend to have to respond to more than one event, the `Message` type will normally be an enum, where each variant is an event to be handled. @@ -301,5 +301,5 @@ enum Msg { ## Lifecycle Context -All component lifecycle methods take a context object. This object provides a reference to component's scope, which +All component lifecycle methods take a context object. This object provides a reference to the component's scope, which allows sending messages to a component and the props passed to the component. diff --git a/website/docs/advanced-topics/struct-components/properties.mdx b/website/docs/advanced-topics/struct-components/properties.mdx index f9663a81375..e1eb7bdc678 100644 --- a/website/docs/advanced-topics/struct-components/properties.mdx +++ b/website/docs/advanced-topics/struct-components/properties.mdx @@ -5,7 +5,7 @@ description: 'Parent to child communication' Properties enable child and parent components to communicate with each other. Every component has an associated properties type which describes what is passed down from the parent. -In theory this can be any type that implements the `Properties` trait, but in practice there's no +In theory, this can be any type that implements the `Properties` trait, but in practice, there is no reason for it to be anything but a struct where each field represents a property. ## Derive macro @@ -17,7 +17,7 @@ Types for which you derive `Properties` must also implement `PartialEq`. ### Field attributes When deriving `Properties`, all fields are required by default. -The following attributes allow you to give your props initial values which will be used unless they're set to another value. +The following attributes allow you to give your props initial values which will be used unless they are set to another value. :::tip Attributes aren't visible in Rustdoc generated documentation. @@ -75,7 +75,7 @@ fn create_default_link_color() -> LinkColor { pub struct LinkProps { /// The link must have a target. href: AttrValue, - /// Also notice that we're using AttrValue instead of String + /// Also notice that we are using AttrValue instead of String text: AttrValue, /// Color of the link. Defaults to `Blue`. #[prop_or_else(create_default_link_color)] @@ -83,7 +83,7 @@ pub struct LinkProps { /// The view function will not specify a size if this is None. #[prop_or_default] size: Option, - /// When the view function doesn't specify active, it defaults to true. + /// When the view function does not specify active, it defaults to true. #[prop_or(true)] active: bool, } @@ -93,7 +93,7 @@ pub struct LinkProps { The `yew::props!` macro allows you to build properties the same way the `html!` macro does it. -The macro uses the same syntax as a struct expression except that you can't use attributes or a base expression (`Foo { ..base }`). +The macro uses the same syntax as a struct expression except that you cannot use attributes or a base expression (`Foo { ..base }`). The type path can either point to the props directly (`path::to::Props`) or the associated properties of a component (`MyComp::Properties`). ```rust diff --git a/website/docs/advanced-topics/struct-components/scope.mdx b/website/docs/advanced-topics/struct-components/scope.mdx index 0a4b0551c70..db4f5974fa7 100644 --- a/website/docs/advanced-topics/struct-components/scope.mdx +++ b/website/docs/advanced-topics/struct-components/scope.mdx @@ -5,7 +5,7 @@ description: "Component's Scope" ## Component's `Scope<_>` API -The component "`Scope`" is the mechanism through which components are able to create callbacks and update themselves +The component "`Scope`" is the mechanism through which components can create callbacks and update themselves using messages. We obtain a reference to this by calling `link()` on the context object passed to the component. ### `send_message` @@ -19,7 +19,7 @@ Sends multiple messages to the component at the same time. This is similar to `send_message` but if any of the messages cause the `update` method to return `true`, the component will re-render after all messages in the batch have been processed. -If the given vector is empty, this function doesn't do anything. +If the given vector is empty, this function does nothing. ### `callback` diff --git a/website/docs/concepts/agents.mdx b/website/docs/concepts/agents.mdx index 2a21d6d8fab..dc35203fa43 100644 --- a/website/docs/concepts/agents.mdx +++ b/website/docs/concepts/agents.mdx @@ -36,7 +36,7 @@ The code can be found in the tag of the svgs. - Private - Spawn a new agent in a web worker for every new bridge. This is good for moving shared but independent behavior that communicates with the browser out of components. When - the the connected bridge is dropped, the agent will disappear. + the connected bridge is dropped, the agent will disappear. - Global \(WIP\) @@ -61,4 +61,4 @@ with other threads, so the cost is substantially higher than just calling a func ## Further reading - The [web_worker_fib](https://github.com/yewstack/yew/tree/master/examples/web_worker_fib) example shows how - components can send message to and receive message from agents. + components can send messages to and receive messages from agents. diff --git a/website/docs/concepts/basic-web-technologies/css.mdx b/website/docs/concepts/basic-web-technologies/css.mdx index d5efddc82b8..49eafe971a2 100644 --- a/website/docs/concepts/basic-web-technologies/css.mdx +++ b/website/docs/concepts/basic-web-technologies/css.mdx @@ -1,14 +1,14 @@ --- title: 'CSS with classes!' description: 'A handy macro to handle classes' -comment: 'Keep this file as short and simple as possible. Its purpose is to ease in the reader into components in Yew instead of providing proper API docs' +comment: 'Keep this file as short and simple as possible. Its purpose is to ease the reader into components in Yew instead of providing proper API docs' --- import Tabs from '@theme/Tabs' import TabItem from '@theme/TabItem' -Yew does not natively provide a css in rust solution, but helps with styling by providing -programmatic ways to interact with the html `class` attribute. +Yew does not natively provide a CSS-in-Rust solution but helps with styling by providing +programmatic ways to interact with the HTML `class` attribute. ## Classes @@ -88,13 +88,13 @@ We will expand upon this concept in [more CSS](../../more/css). ## Inline Styles Currently Yew does not provide any special help with inline styles specified via the `styles` attribute, -but you can use it like any other html attribute: +but you can use it like any other HTML attribute: ```rust use yew::{classes, html}; html! { -
    +
    }; ``` diff --git a/website/docs/concepts/basic-web-technologies/html.mdx b/website/docs/concepts/basic-web-technologies/html.mdx index 8861c3f5c80..a2070935ec7 100644 --- a/website/docs/concepts/basic-web-technologies/html.mdx +++ b/website/docs/concepts/basic-web-technologies/html.mdx @@ -1,13 +1,13 @@ --- title: 'HTML with html!' description: 'Its HTML but not quite!' -comment: 'Keep this file as short and simple as possible. Its purpose is to ease in the reader into components in Yew instead of providing proper API docs' +comment: 'Keep this file as short and simple as possible. Its purpose is to ease the reader into components in Yew instead of providing proper API docs' --- import Tabs from '@theme/Tabs' import TabItem from '@theme/TabItem' -You can write expressions resembling HTML with the `html!` macro. Behind the scenes Yew turns +You can write expressions resembling HTML with the `html!` macro. Behind the scenes, Yew turns it into rust code representing the DOM to generate. ```rust @@ -19,7 +19,7 @@ let my_header: Html = html! { ``` Similar to format expressions, there is an easy way to embed values from the surrounding -context into the html by applying curly brackets: +context into the HTML by applying curly brackets: ```rust use yew::prelude::*; @@ -39,9 +39,9 @@ let combined_html: Html = html! { }; ``` -One rule major rule comes with use of `html!` - you can only return 1 wrapping node. +One major rule comes with the use of `html!` - you can only return 1 wrapping node. To render a list of multiple elements, `html!` allows fragments. Fragments are tags -without a name, that produce no html element by themselves. +without a name, that produce no HTML element by themselves. @@ -49,7 +49,7 @@ without a name, that produce no html element by themselves. ```rust , compile_fail use yew::html; -// error: only one root html element allowed +// error: only one root HTML element allowed html! {
    @@ -64,7 +64,7 @@ html! { ```rust use yew::html; -// fixed: using html fragments +// fixed: using HTML fragments html! { <>
    diff --git a/website/docs/concepts/basic-web-technologies/js.mdx b/website/docs/concepts/basic-web-technologies/js.mdx index bcce54f7f53..b38fcde085b 100644 --- a/website/docs/concepts/basic-web-technologies/js.mdx +++ b/website/docs/concepts/basic-web-technologies/js.mdx @@ -1,7 +1,7 @@ --- title: 'JS with RS' -description: 'Javascript with Rust' -comment: 'Keep this file as short and simple as possible. Its purpose is to ease in the reader into components in Yew instead of providing proper API docs' +description: 'JavaScript with Rust' +comment: 'Keep this file as short and simple as possible. Its purpose is to ease the reader into components in Yew instead of providing proper API docs' --- import Tabs from '@theme/Tabs' @@ -12,17 +12,17 @@ import TabItem from '@theme/TabItem' > accessible where necessary. As of today, WebAssembly is not feature-complete for DOM interactions. This means even in Yew we -sometimes rely on calling Javascript. What follows is an overview of the involved libraries. +sometimes rely on calling JavaScript. What follows is an overview of the involved libraries. ## wasm-bindgen -[`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) is a library and tool that enables calls to javascript from rust and back to rust from javascript. +[`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) is a library and tool that bridges calls between JavaScript and Rust functions. We highly recommend you take a look at their [documentation](https://rustwasm.github.io/docs/wasm-bindgen/) and our [quick guide](./wasm-bindgen.mdx). ## web-sys -The [`web-sys` crate](https://crates.io/crates/web-sys) provides bindings for Web APIs and allows us to write Javascript code in a rustyfied and safe way. +The [`web-sys` crate](https://crates.io/crates/web-sys) provides bindings for Web APIs and allows us to write JavaScript code in a rustyfied and safe way. Example: diff --git a/website/docs/concepts/basic-web-technologies/wasm-bindgen.mdx b/website/docs/concepts/basic-web-technologies/wasm-bindgen.mdx index a7d0e0f0ee3..e34c895ca8c 100644 --- a/website/docs/concepts/basic-web-technologies/wasm-bindgen.mdx +++ b/website/docs/concepts/basic-web-technologies/wasm-bindgen.mdx @@ -14,7 +14,7 @@ Yew uses `wasm-bindgen` to interact with the browser through a number of crates: - [`wasm-bindgen-futures`](https://crates.io/crates/wasm-bindgen-futures) - [`web-sys`](https://crates.io/crates/web-sys) -This section will explore some of these crates in a high level in order to make it easier to understand +This section will explore some of these crates at a high level, to make it easier to understand and use `wasm-bindgen` APIs with Yew. For a more in-depth guide to `wasm-bindgen` and its associated crates then check out [The `wasm-bindgen` Guide](https://rustwasm.github.io/docs/wasm-bindgen/). @@ -29,14 +29,14 @@ over using `wasm-bindgen`. This crate provides many of the building blocks for the rest of the crates above. In this section we are only going to cover two main areas of the `wasm-bindgen` crate and that is the macro and some -types / traits you will see pop up again and again. +types/traits you will see pop up again and again. ### `#[wasm_bindgen]` macro The `#[wasm_bindgen]` macro provides an interface between Rust and JavaScript, providing a system -for translating between the two. Using this macro is more advanced, and you shouldn't need to reach +for translating between the two. Using this macro is more advanced, and you should not need to reach for it unless you are trying to use an external JavaScript library. The `js-sys` and `web-sys` -crates expose `wasm-bindgen` definitions for built-in Javascript types and browser APIs. +crates expose `wasm-bindgen` definitions for built-in JavaScript types and browser APIs. Let's go over a simple example of using the `#[wasm-bindgen]` macro to import some specific flavours of the [`console.log`](https://developer.mozilla.org/en-US/docs/Web/API/Console/log) function. @@ -77,11 +77,11 @@ _This example was adapted from [1.2 Using console.log of The `wasm-bindgen` Guid ### Simulating inheritance -Inheritance between JavaScript classes is a core feature of the Javascript language, and the DOM +Inheritance between JavaScript classes is a core feature of the Javascript language and the DOM (Document Object Model) is designed around it. When types are imported using `wasm-bindgen` you can also add attributes that describe their inheritance. -In Rust this inheritance is represented using the [`Deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html) +In Rust, this inheritance is represented using the [`Deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html) and [`AsRef`](https://doc.rust-lang.org/std/convert/trait.AsRef.html) traits. An example of this might help; so say you have three types `A`, `B`, and `C` where `C` extends `B` which in turn extends `A`. @@ -97,17 +97,17 @@ traits in the following way: These implementations allow you to call a method from `A` on an instance of `C` and to use `C` as if it was `&B` or `&A`. -Its important to note that every single type imported using `#[wasm-bindgen]` has the same root type, +It is important to note that every single type imported using `#[wasm-bindgen]` has the same root type, you can think of it as the `A` in the example above, this type is [`JsValue`](#jsvalue) which has -its own section below. +its section below. _[extends section in The `wasm-bindgen` Guide](https://rustwasm.github.io/docs/wasm-bindgen/reference/attributes/on-js-imports/extends.html)_ ### [`JsValue`](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/struct.JsValue.html) {#jsvalue} This is a representation of an object owned by JavaScript, this is a root catch-all type for `wasm-bindgen`. -Any type that comes from `wasm-bindgen` is a `JsValue` and this is because JavaScript doesn't have -a strong type system so any function that accepts a variable `x` doesn't define its type so `x` can be +Any type that comes from `wasm-bindgen` is a `JsValue` and this is because JavaScript does not have +a strong type system so any function that accepts a variable `x` does not define its type so `x` can be a valid JavaScript value; hence `JsValue`. If you are working with imported functions or types that accept a `JsValue`, then any imported value is _technically_ valid. @@ -119,12 +119,12 @@ _[`JsValue` documentation](https://rustwasm.github.io/wasm-bindgen/api/wasm_bind ### [`JsCast`](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/trait.JsCast.html) {#JsCast} -Rust has a strong type system and JavaScript...doesn't 😞. In order for Rust to maintain these -strong types but still be convenient the WebAssembly group came up with a pretty neat trait `JsCast`. +Rust has a strong type system and JavaScript...doesn't 😞. For Rust to maintain these +strong types but still be convenient, the WebAssembly group came up with a pretty neat trait `JsCast`. Its job is to help you move from one JavaScript "type" to another, which sounds vague, but it means -that if you have one type which you know is really another then you can use the functions of `JsCast` -to jump from one type to the other. It's a nice trait to get to know when working with `web-sys`, -`wasm_bindgen`, `js-sys` - you'll notice lots of types will implement `JsCast` from those crates. +that if you have one type which you know is another, then you can use the functions of `JsCast` +to jump from one type to the other. It is a nice trait to get to know when working with `web-sys`, +`wasm_bindgen`, `js-sys` - you will notice lots of types will implement `JsCast` from those crates. `JsCast` provides both checked and unchecked methods of casting - so that at runtime if you are unsure what type a certain object is you can try to cast it which returns possible failure types like @@ -132,9 +132,9 @@ unsure what type a certain object is you can try to cast it which returns possib [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html). A common example of this in [`web-sys`](./web-sys.mdx) is when you are trying to get the -target of an event, you might know what the target element is but the -[`web_sys::Event`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html) API will always return an [`Option`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html#method.target) -so you will need to cast it to the element type. so you can call its methods. +target of an event. You might know what the target element is but the +[`web_sys::Event`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html) API will always return an [`Option`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Event.html#method.target). +You will need to cast it to the element type so you can call its methods. ```rust // need to import the trait. @@ -161,15 +161,15 @@ The [`dyn_ref`](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/trait.J method is a checked cast that returns an `Option<&T>` which means the original type can be used again if the cast failed and thus returned `None`. The [`dyn_into`](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/trait.JsCast.html#method.dyn_into) -method will consume `self`, as per convention for into methods in Rust, and the type returned is -`Result` this means if the casting fails then the value in `Err` is so you can try again +method will consume `self`, as per convention for `into` methods in Rust, and the type returned is +`Result`. If the casting fails, the original `Self` value is returned in `Err`. You can try again or do something else with the original type. _[`JsCast` documentation](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/trait.JsCast.html)._ ### [`Closure`](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/closure/struct.Closure.html) -The `Closure` type provides a way to transfer Rust closures to JavaScript, the closures past to +The `Closure` type provides a way to transfer Rust closures to JavaScript, the closures passed to JavaScript must have a `'static` lifetime for soundness reasons. This type is a "handle" in the sense that whenever it is dropped it will invalidate the JS @@ -185,7 +185,7 @@ _[`Closure` documentation](https://rustwasm.github.io/wasm-bindgen/api/wasm_bind ## [`js-sys`](https://crates.io/crates/js-sys) -The `js-sys` crate provides bindings / imports of JavaScript's standard, built-in objects, including +The `js-sys` crate provides bindings/imports of JavaScript's standard, built-in objects, including their methods and properties. This does not include any web APIs as this is what [`web-sys`](./web-sys.mdx) is for! @@ -204,8 +204,9 @@ There are three main interfaces in this crate currently: 1. [`JsFuture`](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen_futures/struct.JsFuture.html) - A type that is constructed with a [`Promise`](https://rustwasm.github.io/wasm-bindgen/api/js_sys/struct.Promise.html) - and can then be used as a `Future>`. This Rust future will resolve - or reject with the value coming out of the `Promise`. + and can then be used as a `Future>`. This `Future` will resolve to `Ok` if + the `Promise` is resolved and `Err` if the `Promise` is rejected, containing the resolved or rejected + value from the `Promise` respectively. 2. [`future_to_promise`](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen_futures/fn.future_to_promise.html) - Converts a Rust `Future>` into a diff --git a/website/docs/concepts/basic-web-technologies/web-sys.mdx b/website/docs/concepts/basic-web-technologies/web-sys.mdx index 6709d41be69..daffd9ad2b6 100644 --- a/website/docs/concepts/basic-web-technologies/web-sys.mdx +++ b/website/docs/concepts/basic-web-technologies/web-sys.mdx @@ -11,18 +11,18 @@ procedurally generated from browser WebIDL which is why some names are so long a ## Features in `web-sys` -The `web-sys` crate with all of it's features enabled can add lots of bloat to a Wasm application, -in order to get around this issue most types are feature gated so that you only include the types -you require for your application. Yew includes a number of features from `web-sys` and -exposes some types in it's public API, you will often need to add `web-sys` as a dependency yourself. +The `web-sys` crate with all of its features enabled can add lots of bloat to a Wasm application. +To get around this issue most types are feature gated so that you only include the types +you require for your application. Yew enables several features from `web-sys` and +exposes some types in its public API. You will often need to add `web-sys` as a dependency yourself. ## Inheritance in `web-sys` In the [Simulating inheritance section](./wasm-bindgen.mdx#simulating-inheritance) you can read how in general Rust provides an approach to simulate inheritance in JavaScript. This is very important in -`web-sys` as understanding what methods are available on a type means understanding it's inheritance. +`web-sys` as understanding what methods are available on a type means understanding its inheritance. -This section is going to look at a specific element and list out it's inheritance using Rust by +This section is going to look at a specific element and list out its inheritance using Rust by calling [`Deref::deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html#tymethod.deref) until the value is [`JsValue`](./wasm-bindgen.mdx#jsvalue): @@ -46,11 +46,11 @@ fn inheritance_of_text_area(text_area: HtmlTextAreaElement) { let event_target: &EventTarget = node.deref(); - // Notice we've moved from web-sys types now into built-in + // Notice we have moved from web-sys types now into built-in // JavaScript types which are in the js-sys crate. let object: &js_sys::Object = event_target.deref(); - // Notice we've moved from js-sys type to the root JsValue from + // Notice we have moved from js-sys type to the root JsValue from // the wasm-bindgen crate. let js_value: &wasm_bindgen::JsValue = object.deref(); @@ -79,13 +79,13 @@ _[Inheritance in `web-sys` in The `wasm-bindgen` Guide](https://rustwasm.github. ## The `Node` in `NodeRef` -Yew uses a [`NodeRef`](concepts/function-components/node-refs.mdx) in order to provide a way for keeping a reference to +Yew uses a [`NodeRef`](concepts/function-components/node-refs.mdx) to provide a way for keeping a reference to a `Node` made by the [`html!`](concepts/html/introduction.mdx) macro. The `Node` part of `NodeRef` is referring to [`web_sys::Node`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Node.html). The `NodeRef::get` method will return a `Option` value, however, most of the time in Yew you want -to cast this value to a specific element so you can use it's specific methods. This casting +to cast this value to a specific element so you can use its specific methods. This casting can be done using [`JsCast`](./wasm-bindgen.mdx#JsCast) on the `Node` value, if present, but Yew -provides the `NodeRef::cast` method to perform this casting for convenience and so that you don't +provides the `NodeRef::cast` method to perform this casting for convenience and so that you do not necessarily have to include the `wasm-bindgen` dependency for the `JsCast` trait. The two code blocks below do essentially the same thing, the first is using `NodeRef::cast` and @@ -128,8 +128,8 @@ fn with_jscast(node_ref: NodeRef) { ## JavaScript example to Rust -This section is to help show that any examples that use JavaScript to interact with the Web APIs -can be adapted and written using Rust with `web-sys`. +This section demonstrates examples of how JavaScript code which interact with the +Web APIs can be rewritten with `web-sys` in Rust. ### JavaScript example @@ -191,9 +191,9 @@ Document::new() ``` This version is much more verbose, but you will probably notice part of that is because of failure -types reminding us that some of these function calls have invariants that must be held otherwise will -cause a panic in Rust. Another part of the verbosity is the calls to `JsCast` in order to cast into -different types so that you can call it's specific methods. +types reminding us that some of these function calls have invariants that must be held, or otherwise will +cause a panic in Rust. Another part of the verbosity is the calls to `JsCast` to cast into +different types so that you can call its specific methods. ### Yew example @@ -204,7 +204,7 @@ the approach above: ```toml title=Cargo.toml [dependencies.web-sys] version = "0.3" -# We need to enable the `DomRect` feature in order to use the +# We need to enable the `DomRect` feature to use the # `get_bounding_client_rect` method. features = [ "console", @@ -239,7 +239,7 @@ html! { ## External libraries `web-sys` is a raw binding to the Web API so it comes with some pain in Rust because it was not -designed with Rust or even a strong type system in mind, this is where community crates come in to +designed with Rust or even a strong type system in mind, this is where community crates provide abstractions over `web-sys` to provide more idiomatic Rust APIs. _[External libraries page](/community/external-libs)_ diff --git a/website/docs/concepts/contexts.mdx b/website/docs/concepts/contexts.mdx index a33d6587c2d..7489747f822 100644 --- a/website/docs/concepts/contexts.mdx +++ b/website/docs/concepts/contexts.mdx @@ -6,16 +6,16 @@ description: 'Using contexts to pass deeply nested data' Usually, data is passed from a parent component to a child component via props. But passing props can become verbose and annoying if you have to pass them through many components in the middle, -or if many components in your app need the same information. Context solve this problem by allowing a +or if many components in your app need the same information. Context solves this problem by allowing a parent component to make data available to _any_ component in the tree below it, no matter how deep, without having to pass it down with props. ## The problem with props: "Prop Drilling" -Passing [props](./function-components/properties.mdx) is a great way to pass data directly from parent to a child. -They become cumbersome to pass down through deeply nested component tree or when multiple components share the same data. +Passing [props](./function-components/properties.mdx) is a great way to pass data directly from a parent to a child. +They become cumbersome to pass down through deeply nested component trees or when multiple components share the same data. A common solution to data sharing is lifting the data to a common ancestor and making the children take it as props. -However, this can lead to cases where the prop has to go through multiple components in order to reach the component needs it. +However, this can lead to cases where the prop has to go through multiple components to reach the component that needs it. This situation is called "Prop Drilling". Consider the following example which passes down the theme using props: @@ -84,14 +84,14 @@ fn App() -> Html { We "drill" the theme prop through `Navbar` so that it can reach `Title` and `NavButton`. It would be nice if `Title` and `NavButton`, the components that need access to the theme, can just access the theme -without having to pass it to them as prop. Contexts solve this problem by allowing a parent to pass data, theme in this case, +without having to pass it to them as a prop. Contexts solve this problem by allowing a parent to pass data, theme in this case, to its children. ## Using Contexts ### Step 1: Providing the context -A context provider is required to consume the context. `ContextProvider`, where `T` is the context struct is used as the provider. +A context provider is required to consume the context. `ContextProvider`, where `T` is the context struct used as the provider. `T` must implement `Clone` and `PartialEq`. `ContextProvider` is the component whose children will have the context available to them. The children are re-rendered when the context changes. A struct is used to define what data is to be passed. The `ContextProvider` can be used as: @@ -116,10 +116,10 @@ fn NavButton() -> Html { #[function_component] fn App() -> Html { - let theme = use_memo(|_| Theme { + let theme = use_memo((), |_| Theme { foreground: "yellow".to_owned(), background: "pink".to_owned(), - }, ()); + }); html! { > context={theme}> @@ -140,30 +140,30 @@ See [docs for use_context](https://yew-rs-api.web.app/next/yew/functional/fn.use We have 2 options to consume contexts in struct components: -- [Higher Order Components](../advanced-topics/struct-components/hoc.mdx): A higher order function component will consume the context and pass the data to the struct component which requires it. -- Consume context directly in struct component. See [example of struct component as a consumer](https://github.com/yewstack/yew/tree/master/examples/contexts/src/struct_component_subscriber.rs) +- [Higher Order Components](../advanced-topics/struct-components/hoc.mdx): A higher-order function component will consume the context and pass the data to the struct component which requires it. +- Consume context directly in the struct component. See [example of struct component as a consumer](https://github.com/yewstack/yew/tree/master/examples/contexts/src/struct_component_subscriber.rs) ## Use cases -Generally, if some data is needed by distant components in different parts of the tree, it's likely that context will help you. -Here's some examples of such cases: +Generally, if some data is needed by distant components in different parts of the tree, context will likely help you. +Here are some examples of such cases: - **Theming**: You can put a context at the top of the app that holds your app theme and use it to adjust the visual appearance, as shown in the above example. -- **Current user account**: In many cases, components need to know the current logged-in user. You can use a context to provide the current user object to the components. +- **Current user account**: In many cases, components need to know the currently logged-in user. You can use a context to provide the current user object to the components. ### Considerations to make before using contexts Contexts are very easy to use. That makes them very easy to misuse/overuse. -Just because you can use a context to share props to components multiple levels deep, doesn't mean that you should. +Just because you can use a context to share props to components multiple levels deep, does not mean that you should. For example, you may be able to extract a component and pass that component as a child to another component. For example, -you may have a `Layout` component which takes `articles` as prop and passes it down to `ArticleList` component. +you may have a `Layout` component that takes `articles` as a prop and passes it down to `ArticleList` component. You should refactor the `Layout` component to take children as props and display ` `. -## Mutating context value a child +## Mutating the context value of a child Because of Rust's ownership rules, a context cannot have a method that takes `&mut self` that can be called by children. -In order to mutate a context's value, we must combine it with a reducer. This is done by using the +To mutate a context's value, we must combine it with a reducer. This is done by using the [`use_reducer`](https://yew-rs-api.web.app/next/yew/functional/fn.use_reducer.html) hook. The [contexts example](https://github.com/yewstack/yew/tree/master/examples/contexts) demonstrates mutable contexts diff --git a/website/docs/concepts/function-components/callbacks.mdx b/website/docs/concepts/function-components/callbacks.mdx index 2c3bbf4d1ed..7e84d0f6d93 100644 --- a/website/docs/concepts/function-components/callbacks.mdx +++ b/website/docs/concepts/function-components/callbacks.mdx @@ -55,7 +55,7 @@ fn App() -> Html { Callbacks are also used to hook into DOM events. -For example here we define a callback that will be called when the user clicks the button: +For example, here we define a callback that will be called when the user clicks the button: ```rust use yew::{function_component, html, Html, Properties, Callback}; diff --git a/website/docs/concepts/function-components/children.mdx b/website/docs/concepts/function-components/children.mdx index 05e0105aa6a..5acafbf0b29 100644 --- a/website/docs/concepts/function-components/children.mdx +++ b/website/docs/concepts/function-components/children.mdx @@ -2,7 +2,7 @@ title: 'Children' --- -`Children` is a special prop type that allows you to recieve nested `Html` that is provided like html child elements. +`Children` is a special prop type that allows you to receive nested `Html` that is provided like html child elements. ```rust use yew::{function_component, html, Html, Properties, Children}; diff --git a/website/docs/concepts/function-components/communication.mdx b/website/docs/concepts/function-components/communication.mdx index 6b9c342faa8..dcd3660572a 100644 --- a/website/docs/concepts/function-components/communication.mdx +++ b/website/docs/concepts/function-components/communication.mdx @@ -4,7 +4,7 @@ title: 'Communication between components' ## Parent to child messaging -Pass data as [props](./properties) that cause a rerender, this is the way to pass messages to children. +Pass data as [props](./properties) that cause a re-render, this is the way to pass messages to children. ## Child to parent messaging diff --git a/website/docs/concepts/function-components/generics.mdx b/website/docs/concepts/function-components/generics.mdx index eae3c3ed2a9..758757b5ad6 100644 --- a/website/docs/concepts/function-components/generics.mdx +++ b/website/docs/concepts/function-components/generics.mdx @@ -10,7 +10,7 @@ The `#[function_component]` attribute also works with generic functions for crea ```rust use std::fmt::Display; -use yew::{function_component, html, Properties, Html}; +use yew::{function_component, html, Properties, Html, ToHtml}; #[derive(Properties, PartialEq)] pub struct Props @@ -23,7 +23,7 @@ where #[function_component] pub fn MyGenericComponent(props: &Props) -> Html where - T: PartialEq + Display, + T: PartialEq + ToHtml, { html! {

    diff --git a/website/docs/concepts/function-components/hooks/custom-hooks.mdx b/website/docs/concepts/function-components/hooks/custom-hooks.mdx index 0a1c2b094ce..44284100284 100644 --- a/website/docs/concepts/function-components/hooks/custom-hooks.mdx +++ b/website/docs/concepts/function-components/hooks/custom-hooks.mdx @@ -4,7 +4,7 @@ title: 'Custom Hooks' ## Defining custom Hooks -Component's stateful logic can be extracted into reusable function by creating custom Hooks. +The stateful logic of a component can be extracted into reusable functions by creating custom Hooks. Consider that we wish to create an event listener that listens to an event on the `window` object. @@ -34,13 +34,13 @@ pub fn show_storage_changed() -> Html { ``` There's one problem with this code: the logic can't be reused by another component. -If we build another component which keeps track of the an event, +If we build another component that listens to a different event, instead of copying the code, we can move the logic into a custom hook. We'll start by creating a new function called `use_event`. The `use_` prefix denotes that a function is a hook. -This function will take an event target, an event type and a callback. -All hooks must be marked by `#[hook]` to function as as hook. +This function will take an event target, an event type, and a callback. +All hooks must be marked by `#[hook]` on their function definition. ```rust use web_sys::{Event, EventTarget}; @@ -58,8 +58,8 @@ where } ``` -This is a simple hook which can be created by composing built-in hooks. For this example, we'll use the -`use_effect_with_deps` hook, so an event listener can be recreated when the hook arguments change. +This simple hook can be created by composing built-in hooks. For this example, we'll use the +`use_effect_with` hook, so an event listener can be recreated when the hook arguments change. ```rust use yew::prelude::*; @@ -87,7 +87,8 @@ where callback: Callback::from(callback), }; - use_effect_with_deps( + use_effect_with( + deps, |deps| { let EventDependents { target, @@ -103,7 +104,6 @@ where drop(listener); } }, - deps, ); } ``` diff --git a/website/docs/concepts/function-components/hooks/introduction.mdx b/website/docs/concepts/function-components/hooks/introduction.mdx index 7c884b6060b..d2c1896f22c 100644 --- a/website/docs/concepts/function-components/hooks/introduction.mdx +++ b/website/docs/concepts/function-components/hooks/introduction.mdx @@ -5,21 +5,21 @@ slug: /concepts/function-components/hooks ## Hooks -Hooks are functions that let you store state and perform side-effects. +Hooks are functions that let you store state and perform side effects. -Yew comes with a few pre-defined Hooks. You can also create your own or discover many [community made hooks](/community/awesome#hooks). +Yew comes with a few pre-defined hooks. You can also create your own or discover many [community-made hooks](/community/awesome#hooks). ## Rules of hooks 1. A hook function name always has to start with `use_` 2. Hooks can only be used in the following locations: - - Top level of a function / hook. - - Blocks inside a function / hook, given it's not already branched. - - In the condition of a top level `if` expression inside a function / hook. - - In the scrutinee of a top level `match` expression inside a function / hook. + - Top-level of a function/hook. + - Blocks inside a function/hook, given it is not already branched. + - In the condition of a top-level `if` expression inside a function/hook. + - In the scrutinee of a top-level `match` expression inside a function/hook. 3. Hooks must be called in the same order for every render. Returning early is only allowed when using [Suspense](../../suspense.mdx) -These rules are enforced by either compile time or run-time errors. +These rules are enforced by either compile-time or run-time errors. ### Pre-defined Hooks @@ -34,7 +34,7 @@ Yew comes with the following predefined Hooks: - `use_reducer` - `use_reducer_eq` - `use_effect` -- `use_effect_with_deps` +- `use_effect_with` - `use_context` - `use_force_update` @@ -48,4 +48,4 @@ See the [Defining custom hooks](concepts/function-components/hooks/custom-hooks. ## Further reading - The React documentation has a section on [React hooks](https://reactjs.org/docs/hooks-intro.html). - These are not exactly the same as Yew's hooks, but the underlying concept is similar. + These are not the same as Yew's hooks, but the underlying concept is similar. diff --git a/website/docs/concepts/function-components/introduction.mdx b/website/docs/concepts/function-components/introduction.mdx index 994b6e94e22..8acf12b6296 100644 --- a/website/docs/concepts/function-components/introduction.mdx +++ b/website/docs/concepts/function-components/introduction.mdx @@ -3,13 +3,13 @@ title: 'Function Components' slug: /concepts/function-components --- -Lets revisit this previous statement: +Let's revisit this previous statement: > Yew centrally operates on the idea of keeping everything that a reusable piece of > UI may need in one place - rust files. We will refine this statement, by introducing the concept that will define the logic and -presentation behaviour of an application: "components". +presentation behavior of an application: "components". ## What are Components? @@ -21,7 +21,7 @@ They: - Can have their own state - Compute pieces of HTML visible to the user (DOM) -## Two flavours of Yew Components +## Two flavors of Yew Components You are currently reading about function components - the recommended way to write components when starting with Yew and when writing simple presentation logic. @@ -55,7 +55,7 @@ fn App() -> Html { When rendering, Yew will build a virtual tree of these components. It will call the view function of each (function) component to compute a virtual version (VDOM) of the DOM that you as the library user see as the `Html` type. -For the previous example this would look like this: +For the previous example, this would look like this: ```xhtml @@ -71,6 +71,6 @@ This is what we call **rendering**. :::note -Behind the scenes `Html` is just an alias for `VNode` - virtual node. +Behind the scenes, `Html` is just an alias for `VNode` - a virtual node. ::: diff --git a/website/docs/concepts/function-components/properties.mdx b/website/docs/concepts/function-components/properties.mdx index 8bc39efc947..d331425ec92 100644 --- a/website/docs/concepts/function-components/properties.mdx +++ b/website/docs/concepts/function-components/properties.mdx @@ -18,9 +18,9 @@ A type has to implement the `Properties` trait before it can be used as the prop ## Reactivity -Yew checks if props have changed when reconciling the vdom during rerendering, to know if nested components needs to be rerendered. -This way Yew can be considered a very reactive framework as changes from the parent will always be propagated downwards -and the view will never be out of sync from the data coming from props/state. +Yew checks if props have changed when reconciling the Virtual DOM during re-rendering, to know if nested components need to be re-rendered. +This way Yew can be considered a very reactive framework, as changes from the parent will always be propagated downward, +and the view will never be out of sync with the data coming from props/state. :::tip @@ -102,7 +102,7 @@ fn App() -> Html { ## Derive macro field attributes When deriving `Properties` all fields are required by default. -The following attributes allow you to give your props default values which will be used when parent has not set them. +The following attributes allow you to give your props default values which will be used when the parent has not set them. :::tip Attributes aren't visible in Rustdoc generated documentation. @@ -262,3 +262,43 @@ fn App() -> Html { html! {} } ``` + +## Evaluation Order + +Props are evaluated in the order they're specified, as shown by the following example: + +```rust +#[derive(yew::Properties, PartialEq)] +struct Props { first: usize, second: usize, last: usize } + +fn main() { + let mut g = 1..=3; + let props = yew::props!(Props { first: g.next().unwrap(), second: g.next().unwrap(), last: g.next().unwrap() }); + + assert_eq!(props.first, 1); + assert_eq!(props.second, 2); + assert_eq!(props.last, 3); +} +``` + +## Anti Patterns + +While almost any Rust type can be passed as properties, there are some anti-patterns that should be avoided. +These include, but are not limited to: + +1. Using `String` type instead of `AttrValue`.
    + **Why is this bad?** `String` can be expensive to clone. + Cloning is often needed when the prop value is used with hooks and callbacks. `AttrValue` is either + a reference-counted string (`Rc`) or a `&'static str`, thus very cheap to clone.
    + **Note**: `AttrValue` internally is `IString` from [implicit-clone](https://crates.io/crates/implicit-clone) + See that crate to learn more. +2. Using interior mutability.
    + **Why is this bad?** Interior mutability (such as with `RefCell`, `Mutex`, etc.) should + _generally_ be avoided. It can cause problems with re-renders (Yew doesn't know when the state has changed) + so you may have to manually force a render. Like all things, it has its place. Use it with caution. +3. You tell us. Did you run into an edge-case you wish you knew about earlier? Feel free to create an issue + or PR a fix to this documentation. + +## yew-autoprops + +[yew-autoprops](https://crates.io/crates/yew-autoprops) is an experimental package that allows one to create the Props struct on the fly out of the arguments of your function. Might be useful, if the properties struct is never reused. diff --git a/website/docs/concepts/function-components/pure-components.mdx b/website/docs/concepts/function-components/pure-components.mdx index 4ae25155bc4..ec6285f9ab9 100644 --- a/website/docs/concepts/function-components/pure-components.mdx +++ b/website/docs/concepts/function-components/pure-components.mdx @@ -3,11 +3,11 @@ title: 'Pure Components' --- A function component is considered [pure] when the returned `Html` is deterministically derived -from its props, and its view function mutates no state or has other side-effects. +from its props when its view function does not mutate its state or has other side effects. [pure]: https://en.wikipedia.org/wiki/Pure_function -For example below is a pure component. For a given prop `is_loading` it will always result in the same `Html` without any side effects. +The example below is a pure component. For a given prop `is_loading` it will always result in the same `Html` without any side effects. ```rust use yew::{Properties, function_component, Html, html}; @@ -35,5 +35,5 @@ as a normal function returning `Html` and avoid a bit of overhead for Yew, relat ## Impure components -You might wonder if a component can be impure if it does not use any globals, since its just a function that is called every render. +You might wonder if a component can be impure if it does not use any globals, since it is just a function that is called every render. This is where the next topic comes in - [hooks](./hooks) diff --git a/website/docs/concepts/function-components/state.mdx b/website/docs/concepts/function-components/state.mdx index c4fdc713e66..316de772830 100644 --- a/website/docs/concepts/function-components/state.mdx +++ b/website/docs/concepts/function-components/state.mdx @@ -4,7 +4,7 @@ title: 'State' ## General view of how to store state -This table can be used as a guide when deciding what state storing type fits best for your use case: +This table can be used as a guide when deciding what state-storing type fits best for your use case: | Hook | Type | Rerender when? | Scope | | ------------------------ | -------------------------- | ---------------------------- | ------------------- | diff --git a/website/docs/concepts/html/classes.mdx b/website/docs/concepts/html/classes.mdx index 01e2ef3942e..a2b823ddb45 100644 --- a/website/docs/concepts/html/classes.mdx +++ b/website/docs/concepts/html/classes.mdx @@ -14,12 +14,11 @@ When pushing a string to the set, `Classes` ensures that there is one element for every class even if a single string might contain multiple classes. `Classes` can also be merged by using `Extend` (i.e. -`classes1.extend(classes2)`) or `push()` (i.e. `classes1.push(classes2)`). In -fact, anything that implements `Into` can be used to push new classes -to the set. +`classes1.extend(classes2)`) or `push()` (i.e. `classes1.push(classes2)`). +Any type that implements `Into` can be pushed onto an existing `Classes`. The macro `classes!` is a convenient macro that creates one single `Classes`. -Its input accepts a comma separated list of expressions. The only requirement +Its input accepts a comma-separated list of expressions. The only requirement is that every expression implements `Into`. diff --git a/website/docs/concepts/html/components.mdx b/website/docs/concepts/html/components.mdx index 3344a9fd85c..81cf19a0dae 100644 --- a/website/docs/concepts/html/components.mdx +++ b/website/docs/concepts/html/components.mdx @@ -55,7 +55,7 @@ html!{ ## Nested -Components can be passed children if they have a `children` field in their `Properties`. +Components can accept child components/elements if they have a `children` field in their `Properties` ```rust title="parent.rs" use yew::prelude::*; @@ -121,7 +121,7 @@ html! { ## Nested Children with Props -Nested component properties can be accessed and mutated if the containing component types its children. In the following example, the `List` component can wrap `ListItem` components. For a real world example of this pattern, check out the `yew-router` source code. For a more advanced example, check out the `nested-list` example in the main yew repository. +Nested component properties can be accessed and mutated if the containing component types its children. In the following example, the `List` component can wrap `ListItem` components. For a real-world example of this pattern, check out the `yew-router` source code. For a more advanced example, check out the `nested-list` example in the main yew repository. ```rust use std::rc::Rc; diff --git a/website/docs/concepts/html/elements.mdx b/website/docs/concepts/html/elements.mdx index 800550d7fd4..a6ae194e5dd 100644 --- a/website/docs/concepts/html/elements.mdx +++ b/website/docs/concepts/html/elements.mdx @@ -12,7 +12,7 @@ There are many reasons why you might want to create or manage DOM nodes manually when integrating with JS libraries that can cause conflicts with managed components. Using `web-sys`, you can create DOM elements and convert them into a `Node` - which can then be -used as a `Html` value using `VRef`: +used as an `Html` value using `VRef`: ```rust use web_sys::{Element, Node}; @@ -23,6 +23,7 @@ use gloo::utils::document; fn MyComponent() -> Html { // memoize as this only needs to be executed once let node = use_memo( + (), |_| { // Create a div element from the document let div: Element = document().create_element("div").unwrap(); @@ -33,7 +34,6 @@ fn MyComponent() -> Html { // Return that Node as a Html value Html::VRef(node) }, - (), ); // use_memo return Rc so we need to deref and clone @@ -44,8 +44,8 @@ fn MyComponent() -> Html { ## Dynamic tag names -When building a higher-order component you might find yourself in a situation where the element's tag name isn't static. -For example, you might have a `Title` component which can render anything from `h1` to `h6` depending on a level prop. +When building a higher-order component you might find yourself in a situation where the element's tag name is not static. +For example, you might have a `Title` component that can render anything from `h1` to `h6` depending on a level prop. Instead of having to use a big match expression, Yew allows you to set the tag name dynamically using `@{name}` where `name` can be any expression that returns a string. @@ -75,7 +75,7 @@ html! { }; ``` -This will result in **HTML** that's functionally equivalent to this: +This will result in **HTML** that is functionally equivalent to this: ```html

    @@ -104,7 +104,7 @@ This will result in the following **HTML**: ## String-like attributes -But apart from a select few boolean attributes, you will probably be dealing with a lot of string-like HTML attributes and Yew has a few option for those +But apart from a select few boolean attributes, you will probably be dealing with a lot of string-like HTML attributes and Yew has a few options to pass string-like values to components. ```rust use yew::{html, virtual_dom::AttrValue}; @@ -138,7 +138,7 @@ html! { }; ``` -If the attribute is set to `None`, the attribute won't be set in the DOM. +If the attribute is set to `None`, the attribute will not be set in the DOM. ## Relevant examples diff --git a/website/docs/concepts/html/events.mdx b/website/docs/concepts/html/events.mdx index f6337441176..23c0532d75d 100644 --- a/website/docs/concepts/html/events.mdx +++ b/website/docs/concepts/html/events.mdx @@ -16,7 +16,7 @@ below, see [Manual event listener](#manual-event-listener). :::tip All the event types mentioned in the following table are re-exported under `yew::events`. Using the types from `yew::events` makes it easier to ensure version compatibility than -if you were to manually include `web-sys` as a dependency in your crate because you won't +if you were to manually include `web-sys` as a dependency in your crate because you will not end up using a version which conflicts with the version that Yew specifies. ::: @@ -41,8 +41,8 @@ listens for `click` events. See the end of this page for a [full list of availab Events dispatched by Yew follow the virtual DOM hierarchy when bubbling up to listeners. Currently, only the bubbling phase is supported for listeners. Note that the virtual DOM hierarchy is most often, but not always, identical to the actual DOM hierarchy. The distinction is important when working with [portals](../../advanced-topics/portals.mdx) and other -more advanced techniques. The intuition for well implemented components should be that events bubble from children -to parents, so that the hierarchy in your coded `html!` is the one observed by event handlers. +more advanced techniques. The intuition for well-implemented components should be that events bubble from children +to parents. In this way the hierarchy in your coded `html!` is the one observed by event handlers. If you are not interested in event bubbling, you can turn it off by calling @@ -50,20 +50,20 @@ If you are not interested in event bubbling, you can turn it off by calling yew::set_event_bubbling(false); ``` -_before_ starting your app. This speeds up event handling, but some components may break from not receiving events they expect. +_before_ starting your app. This speeds up event handling, but some components may break from not receiving the events they expect. Use this with care! ## Event delegation It can be surprising that event listeners are _not_ directly registered on the element where they are rendered. Instead, events are delegated from the subtree root of the Yew app. Still, events are delivered in their native form, and no synthetic -form is created. This can lead to mismatches between the event you'd expect in html listeners and those showing up in Yew. +form is created. This can lead to mismatches between the event you would expect in HTML listeners and those showing up in Yew. - [`Event::current_target`] points to the Yew subtree root instead of the element the listener is added on. Use [`NodeRef`](../function-components/node-refs.mdx) if you want access to the underlying `HtmlElement`. - [`Event::event_phase`] is always [`Event::CAPTURING_PHASE`]. Internally, the event will behave as if it was in the bubbling phase, the event propagation is replayed and the event [bubbles _up_](#event-bubbling), i.e. event listeners higher up in - the virtual DOM will trigger _after_ event listeners below them. Currently, capturing listeners are not supported by Yew. + the virtual DOM will trigger _after_ event listeners below them. Currently, capturing listeners is not supported by Yew. This also means that events registered by Yew will usually fire before other event listeners. @@ -88,8 +88,8 @@ them here. Calling [`web_sys::Event::target`](https://rustwasm.github.io/wasm-bi on an event returns an optional [`web_sys::EventTarget`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.EventTarget.html) type, which might not seem very useful when you want to know the value of your input element. -In all the approaches below we are going to tackle the same problem, so it's clear where the approach -differs opposed to the problem at hand. +In all the approaches below we are going to tackle the same problem, so it is clear where the approach +differs as opposed to the problem at hand. **The Problem:** @@ -369,7 +369,7 @@ You may want to listen to an event that is not supported by Yew's `html` macro, [supported events listed here](#event-types). In order to add an event listener to one of elements manually we need the help of -[`NodeRef`](../function-components/node-refs.mdx) so that in `use_effect_with_deps` we can add a listener using the +[`NodeRef`](../function-components/node-refs.mdx) so that in `use_effect_with` we can add a listener using the [`web-sys`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/index.html) and [wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/index.html) API. @@ -394,7 +394,8 @@ use yew::prelude::*; fn MyComponent() -> Html { let div_node_ref = use_node_ref(); - use_effect_with_deps( + use_effect_with( + div_node_ref.clone(), { let div_node_ref = div_node_ref.clone(); @@ -425,8 +426,7 @@ fn MyComponent() -> Html { move || drop(custard_listener) } - }, - div_node_ref.clone() + } ); html! { @@ -461,7 +461,8 @@ use gloo::events::EventListener; fn MyComponent() -> Html { let div_node_ref = use_node_ref(); - use_effect_with_deps( + use_effect_with( + div_node_ref.clone(), { let div_node_ref = div_node_ref.clone(); @@ -486,8 +487,7 @@ fn MyComponent() -> Html { move || drop(custard_listener) } - }, - div_node_ref.clone() + } ); html! { @@ -561,7 +561,7 @@ For more information on `EventListener`, see the | `onselect` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | | `onslotchange` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | | `onstalled` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | -| `onsubmit` | [FocusEvent](https://docs.rs/web-sys/latest/web_sys/struct.FocusEvent.html) | +| `onsubmit` | [SubmitEvent](https://docs.rs/web-sys/latest/web_sys/struct.SubmitEvent.html) | | `onsuspend` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | | `ontimeupdate` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | | `ontoggle` | [Event](https://docs.rs/web-sys/latest/web_sys/struct.Event.html) | diff --git a/website/docs/concepts/html/fragments.mdx b/website/docs/concepts/html/fragments.mdx index 81d6672c1ba..21a6ca3388a 100644 --- a/website/docs/concepts/html/fragments.mdx +++ b/website/docs/concepts/html/fragments.mdx @@ -5,7 +5,7 @@ title: 'Fragments' import Tabs from '@theme/Tabs' import TabItem from '@theme/TabItem' -The `html!` macro always requires a single root node. In order to get around this restriction, you +The `html!` macro always requires a single root node. To get around this restriction, you can use an "empty tag" (these are also called "fragments"). diff --git a/website/docs/concepts/html/introduction.mdx b/website/docs/concepts/html/introduction.mdx index 3d323dbfe48..8886778e87d 100644 --- a/website/docs/concepts/html/introduction.mdx +++ b/website/docs/concepts/html/introduction.mdx @@ -9,7 +9,7 @@ import Tabs from '@theme/Tabs' import TabItem from '@theme/TabItem' The `html!` macro allows you to write HTML and SVG code declaratively. It is similar to JSX -(an extension to JavaScript which allows you to write HTML-like code inside of JavaScript). +(an extension to JavaScript that allows you to write HTML-like code inside of JavaScript). **Important notes** @@ -17,7 +17,7 @@ The `html!` macro allows you to write HTML and SVG code declaratively. It is sim [fragments](./fragments.mdx) or [iterators](./../html/lists.mdx)) 2. An empty `html! {}` invocation is valid and will not render anything 3. Literals must always be quoted and wrapped in braces: `html! {

    { "Hello, World" }

    }` -4. The `html!` macro will make all tag names lower case. To use upper case characters (which are required for some SVG elements) use [dynamic tag names](concepts/html/elements.mdx#dynamic-tag-names): `html! { <@{"myTag"}> }` +4. The `html!` macro will make all tag names lowercase. To use upper case characters (which are required for some SVG elements) use [dynamic tag names](concepts/html/elements.mdx#dynamic-tag-names): `html! { <@{"myTag"}> }` :::note The `html!` macro can reach the default recursion limit of the compiler. If you encounter compilation errors, @@ -151,7 +151,45 @@ HTML code. At the moment the lints are mostly accessibility-related. If you have ideas for lints, please feel free to [chime in on this issue](https://github.com/yewstack/yew/issues/1334). -## Special properties +## Specifying attributes and properties + +Attributes are set on elements in the same way as in normal HTML: + +```rust +use yew::prelude::*; + +let value = "something"; +html! {
    }; +``` + +Properties are specified with `~` before the element name: + +```rust +use yew::prelude::*; + +html! { }; +``` + +:::tip + +The braces around the value can be ommited if the value is a literal. + +::: + +:::note What classifies as a literal + +Literals are all valid [literal expressions](https://doc.rust-lang.org/reference/expressions/literal-expr.html) +in Rust. Note that [negative numbers are **not** literals](https://users.rust-lang.org/t/why-are-negative-value-literals-expressions/43333) +and thus must be encosed in curly-braces `{-6}` + +::: + +:::note Component properties +Component properites are passed as Rust objects and are different from the element attributes/properties described here. +Read more about them at [Component Properties](../function-components/properties.mdx) +::: + +### Special properties There are special properties which don't directly influence the DOM but instead act as instructions to Yew's virtual DOM. Currently, there are two such special props: `ref` and `key`. diff --git a/website/docs/concepts/html/lists.mdx b/website/docs/concepts/html/lists.mdx index 0b327494b7e..6dca5da6c48 100644 --- a/website/docs/concepts/html/lists.mdx +++ b/website/docs/concepts/html/lists.mdx @@ -51,11 +51,11 @@ html! { ## Keyed lists A keyed list is an optimized list that has keys on **all** children. -`key` is a special prop provided by Yew which gives an html element or component a unique identifier -which is used for optimization purposes inside Yew. +`key` is a special prop provided by Yew that gives an HTML element or component a unique identifier +that is used for optimization purposes inside Yew. :::caution -Key has to be unique only in each list, in constrast to the global uniqueness of html `id`s. It must not depend on the order of the list. +Key has to be unique only in each list, in contrast to the global uniqueness of HTML `id`s. It must not depend on the order of the list. ::: It is always recommended to add keys to lists. @@ -81,7 +81,7 @@ html! { ### Performance increases -We have [Keyed list](https://github.com/yewstack/yew/tree/master/examples/keyed_list) example that lets you test the performance improvements, but here is rough rundown: +We have [Keyed list](https://github.com/yewstack/yew/tree/master/examples/keyed_list) example that lets you test the performance improvements, but here is a rough rundown: 1. Go to [Keyed list](https://github.com/yewstack/yew/tree/master/examples/keyed_list) hosted demo 2. Add 500 elements. @@ -92,14 +92,14 @@ We have [Keyed list](https://github.com/yewstack/yew/tree/master/examples/keyed_ 7. Reverse the list. 8. Look at "The last rendering took Xms" (At the time of writing this it was ~30ms) -So just at the time of writing this, for 500 components its a x2 increase of speed. +So just at the time of writing this, for 500 components it is a 2x increase of speed. ### Detailed explanation -Usually you just need a key on every list item when you iterate and the order of data can change. +Usually, you just need a key on every list item when you iterate and the order of data can change. It's used to speed up the reconciliation process when re-rendering the list. -Without keys, lets assume you iterate through `["bob","sam","rob"]`, ending up with the html: +Without keys, assume you iterate through `["bob", "sam", "rob"]`, ending up with the HTML: ```html
    My name is Bob
    @@ -107,16 +107,16 @@ Without keys, lets assume you iterate through `["bob","sam","rob"]`, ending up w
    My name is rob
    ``` -Then on the next render, if your list changed to `["bob","rob"]`, yew could delete +Then on the next render, if your list changed to `["bob", "rob"]`, yew could delete the element with id="rob" and update id="sam" to be id="rob" -If you had added a key to each element, the initial html would be the same, but after -the render with the modified list, `["bob","rob"]`, yew would just delete the second -html element and leave the rest untouched since it can use the keys to associate them. +If you had added a key to each element, the initial HTML would be the same, but after +the render with the modified list, `["bob", "rob"]`, yew would just delete the second +HTML element and leave the rest untouched since it can use the keys to associate them. If you ever encounter a bug/"feature" where you switch from one component to another but both have a div as the highest rendered element. -Yew reuses the rendered html div in those cases as an optimization. -If you need that div to be recreated instead of reused, then you can add different keys and they wont be reused +Yew reuses the rendered HTML div in those cases as an optimization. +If you need that div to be recreated instead of reused, then you can add different keys and they will not be reused. ## Further reading diff --git a/website/docs/concepts/html/literals-and-expressions.mdx b/website/docs/concepts/html/literals-and-expressions.mdx index 69b6e6f7a02..df1fa4dc3e6 100644 --- a/website/docs/concepts/html/literals-and-expressions.mdx +++ b/website/docs/concepts/html/literals-and-expressions.mdx @@ -9,7 +9,7 @@ If expressions resolve to types that implement `Display`, they will be converted String literals create `Text` nodes, which are treated as strings by the browser. Hence, even if the expression contains a ` + + + +``` + +## アプリを動かす! + +[`wasm-pack`](https://rustwasm.github.io/docs/wasm-pack/)を使うのがアプリを動かすのに推奨される方法です。 +まだ`wasm-pack`をインストールしていない場合、`cargo install wasm-pack`でインストールして開発サーバーを動かしてみましょう: + +```bash +wasm-pack build --target web --out-name wasm --out-dir ./static +``` + +`wasm-pack`はコンパイルされた WebAssembly と JavaScript ラッパーをまとめたものを`./static`ディレクトリに作り、 +アプリの WebAssembly バイナリを読み込んで動かします。 + +そして、`./static`以下で好きなサーバーをファイルをサーブしてみましょう。 +例えば: + +```bash +cargo +nightly install miniserve +miniserve ./static --index index.html +``` diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/getting-started/examples.mdx b/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/getting-started/examples.mdx new file mode 100644 index 00000000000..cab88429afa --- /dev/null +++ b/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/getting-started/examples.mdx @@ -0,0 +1,14 @@ +--- +title: Examples +--- + +Yew のリポジトリは[例](https://github.com/yewstack/yew/tree/v0.17/examples)がたくさんあります +\(メンテナンス状況は様々\)。 +様々なフレームワークの機能の使い方を知るのにはそれらの例に取り組むのを勧めます。 +プルリクエストや Issue はウェルカムです。 + +- [**Todo アプリ** ](https://github.com/yewstack/yew/tree/v0.17/examples/todomvc) +- [**カスタムコンポーネント**](https://github.com/yewstack/yew/tree/v0.17/examples/custom_components) +- [**マルチスレッド\(エージェント\)**](https://github.com/yewstack/yew/tree/v0.17/examples/multi_thread) +- [**タイマーサービス**](https://github.com/yewstack/yew/tree/v0.17/examples/timer) +- [**ネストしたコンポーネント**](https://github.com/yewstack/yew/tree/v0.16.0/examples/nested_list) diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/css.mdx b/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/css.mdx new file mode 100644 index 00000000000..a688301db50 --- /dev/null +++ b/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/css.mdx @@ -0,0 +1,18 @@ +--- +title: CSS +--- + +# CSS + +<TODO> + +統合的な CSS サポートについての提案はこちらにあります: [https://github.com/yewstack/yew/issues/533](https://github.com/yewstack/yew/issues/533) + +## スタイルフレームワーク: + +今のところ、コミュニティメンバーは以下のスタイルフレームワークを開発しています。 + +- [yew_styles](https://github.com/spielrs/yew_styles) - JavaScript に依存しない Yew のスタイルフレームワーク +- [yew-mdc](https://github.com/Follpvosten/yew-mdc) - マテリアルデザインのコンポーネント +- [muicss-yew](https://github.com/AlephAlpha/muicss-yew) - MUI の CSS コンポーネント +- [Yewtify](https://github.com/yewstack/yewtify) – Yew で Vuetify フレームワークで提供されている機能の実装 diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/debugging.mdx b/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/debugging.mdx new file mode 100644 index 00000000000..f7c2a88bf1c --- /dev/null +++ b/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/debugging.mdx @@ -0,0 +1,58 @@ +--- +title: Debugging +--- + +# デバッグ + +## パニック + +Rust シンボルで良いスタックトレースをするには +[`console_error_panic`](https://github.com/rustwasm/console_error_panic_hook)クレートを使用してください。 +注意として、`cargo-web`でビルドされたものとは互換性がありません。 + +## コンソールでのログ + +一般的に、Wasm の Web アプリはブラウザの API と連携することができ、`console.log`の API も例外ではありません。 +いつくかの選択肢があります: + +### [`wasm-logger`](https://crates.io/crates/wasm-logger) + +このクレートは Rust の`log`クレートと親和性があります。 + +```rust +// セットアップ +fn main() { + wasm_logger::init(wasm_logger::Config::default()); +} + +// 使用方法 +log::info!("Update: {:?}", msg); +``` + +### [`ConsoleService`](https://docs.rs/yew/latest/yew/services/console/struct.ConsoleService.html) + +このサービスは Yew に含まれており、`"services"`の機能が有効化されている場合は利用可能です。 + +```rust +// 使用方法 +ConsoleService::info(format!("Update: {:?}", msg).as_ref()); +``` + +## ソースマップ + +今のところは Rust/Wasm の Web アプリにはソースマップへの第一級のサポートがありません。 +もちろん、これは変更される可能性があります。これが当てはまらない場合、または進捗が見られる場合は、変更を提案してください! + +### 最新情報 + +\[2019 年 12 月\] [Chrome DevTools update](https://developers.google.com/web/updates/2019/12/webassembly#the_future) + +> やらなければいけないことがまだたくさんあります。例えばツール側では Emscripten\(Binaryen\)と wasm-pack\(wasm-bindgen\)がそれらが実行する変換に関する DWARF 情報の更新をまだサポートしていません。 + +\[2020\] [Rust Wasm デバッグガイド](https://rustwasm.github.io/book/reference/debugging.html#using-a-debugger) + +> 残念なことに、WebAssembly のデバッグの物語はまだ未成熟です。ほとんどの Unix のシステムでは[DWARF](http://dwarfstd.org/)は実行中のプログラムをソースレベルで検査するためにデバッガに必要な情報をエンコードするために使用されます。Windows には同様の情報をエンコードする代替形式があります。現在、WebAssembly に相当するものはありません。 + +\[2019\] [Rust Wasm ロードマップ](https://rustwasm.github.io/rfcs/007-2019-roadmap.html#debugging) + +> デバッグはトリッキーです。なぜなら、多くの話はこの活動チームの手の届かないところにあり、WebAssembly の標準化団体とブラウザ開発者ツールを実装している人たちの両方に依存しているからです。 diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/roadmap.mdx b/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/roadmap.mdx new file mode 100644 index 00000000000..80c0ad9d29d --- /dev/null +++ b/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/roadmap.mdx @@ -0,0 +1,45 @@ +--- +title: Roadmap +description: The planned feature roadmap for the Yew framework +--- + +# ロードマップ + +## 優先順位 + +フレームワークの今後の機能やフォーカスの優先順位は、コミュニティによって決定されます。2020 年の春には、プロジェクトの方向性についてのフィードバックを集めるために開発者アンケートが行われました。その概要は [Yew Wiki](https://github.com/yewstack/yew/wiki/Dev-Survey-%5BSpring-2020%5D) で見ることができます。 + +:::note +主要な取り組みの状況は、Yew の Github の[Project board](https://github.com/yewstack/yew/projects)で確認できます。 +::: + +## 焦点 + +1. Top Requested Features +2. Production Readiness +3. Documentation +4. Pain Points + +### Top Requested Features + +1. [関数型コンポーネント](https://github.com/yewstack/yew/projects/3) +2. [Component ライブラリ](https://github.com/yewstack/yew/projects/4) +3. より良い状態管理 +4. [サーバーサイドでのレンダリング](https://github.com/yewstack/yew/projects/5) + +### Production Readiness + +- テストカバレッジの向上 +- バイナリサイズ +- [ベンチマークのパフォーマンス](https://github.com/yewstack/yew/issues/5) + +### Documentation + +- チュートリアルを作る +- プロジェクトのセットアップをシンプルにする + +### Pain Points + +- [Component のボイラープレート](https://github.com/yewstack/yew/issues/830) +- Fetch API +- [エージェント](https://github.com/yewstack/yew/projects/6) diff --git a/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/testing.mdx b/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/testing.mdx new file mode 100644 index 00000000000..d6f4a678591 --- /dev/null +++ b/website/i18n/ja/docusaurus-plugin-content-docs/version-0.20/more/testing.mdx @@ -0,0 +1,12 @@ +--- +title: Testing apps +description: Testing your app +--- + +# Testing + +<TODO> + +## wasm_bindgen_test + +Rust Wasm ワーキンググループは wasm_bindgen_test というフレームワークをメンテナンスしており、組み込みの #[test] プロシージャルマクロの動作と同様の方法でブラウザでテストを実行することができます。詳細は、[Rust Wasm 活動グループのドキュメント](https://rustwasm.github.io/docs/wasm-bindgen/wasm-bindgen-test/index.html)に記載されています。 diff --git a/website/i18n/ja/docusaurus-theme-classic/navbar.json b/website/i18n/ja/docusaurus-theme-classic/navbar.json index 9c30ac27e75..6defadb84cf 100644 --- a/website/i18n/ja/docusaurus-theme-classic/navbar.json +++ b/website/i18n/ja/docusaurus-theme-classic/navbar.json @@ -30,5 +30,9 @@ "item.label.Playground": { "message": "Playground", "description": "Navbar item with label Playground" + }, + "logo.alt": { + "message": "Yew Logo", + "description": "The alt text of navbar logo" } } diff --git a/website/i18n/zh-Hans/code.json b/website/i18n/zh-Hans/code.json index 091856d4547..ec2caa96f3a 100644 --- a/website/i18n/zh-Hans/code.json +++ b/website/i18n/zh-Hans/code.json @@ -5,7 +5,7 @@ }, "theme.ErrorPageContent.tryAgain": { "message": "Try again", - "description": "The label of the button to try again when the page crashed" + "description": "The label of the button to try again rendering when the React error boundary captures an error" }, "theme.NotFound.title": { "message": "页面找不到啦", @@ -255,5 +255,45 @@ "theme.SearchPage.noResultsText": { "message": "没有找到任何文档", "description": "The paragraph for empty search result" + }, + "theme.admonition.note": { + "message": "备注", + "description": "The default label used for the Note admonition (:::note)" + }, + "theme.admonition.tip": { + "message": "提示", + "description": "The default label used for the Tip admonition (:::tip)" + }, + "theme.admonition.danger": { + "message": "危险", + "description": "The default label used for the Danger admonition (:::danger)" + }, + "theme.admonition.info": { + "message": "信息", + "description": "The default label used for the Info admonition (:::info)" + }, + "theme.admonition.caution": { + "message": "警告", + "description": "The default label used for the Caution admonition (:::caution)" + }, + "theme.tags.tagsPageTitle": { + "message": "标签", + "description": "The title of the tag list page" + }, + "theme.NavBar.navAriaLabel": { + "message": "Main", + "description": "The ARIA label for the main navigation" + }, + "theme.docs.sidebar.navAriaLabel": { + "message": "Docs sidebar", + "description": "The ARIA label for the sidebar navigation" + }, + "theme.docs.sidebar.closeSidebarButtonAriaLabel": { + "message": "关闭导航栏", + "description": "The ARIA label for close button of mobile sidebar" + }, + "theme.docs.sidebar.toggleSidebarButtonAriaLabel": { + "message": "切换导航栏", + "description": "The ARIA label for hamburger menu button of mobile navigation" } } diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/advanced-topics/optimizations.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/advanced-topics/optimizations.mdx index 321c71ae2cb..448206c3c0e 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/advanced-topics/optimizations.mdx +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/advanced-topics/optimizations.mdx @@ -62,18 +62,6 @@ fn change(&mut self, props: Self::Properties) -> ShouldRender { 你不仅限在 `change` 函数中使用它。通常,在 `update` 函数中执行此操作也是有意义的,尽管性能提升在那里不太明显。 -## wee_alloc - -[wee_alloc](https://github.com/rustwasm/wee_alloc) 是一个比 Rust 二进制文件中通常使用的分配器还小得多的微型分配器。用这个分配器来替代默认的分配器将使 Wasm 文件体积更小,但会牺牲速度和内存开销。 - -对比不包含默认分配器换取的体积大小,牺牲的速度和内存开销是微不足道的。较小的文件体积意味着你的页面将加载更快,因此通常建议使用此分配器而不是默认分配器,除非你的应用程序会执行一些繁重的内存分配任务。 - -```rust -// 将 `wee_alloc` 作为全局分配器 -#[global_allocator] -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; -``` - ## RC 为了避免在重新渲染时为了创建 props 而克隆大块数据,我们可以使用智能指针来只克隆指针。如果在 props 和子组件中使用 `Rc<_>` 而不是普通未装箱的值,则可以延迟克隆直到需要修改子组件中的数据为止,在该组件中可以使用 `Rc::make_mut` 来对要更改数据进行克隆和获取可变引用。通过在要修改前不进行克隆,子组件可以在几乎没有性能成本的情况下拒绝与它们在 `Component::change` 中拥有状态的 props 相同的 props,这与数据本身需要先复制到父级 props 结构体中,然后在子级中进行比较和拒绝的情况相反。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20.json b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20.json new file mode 100644 index 00000000000..0016c41f6e3 --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20.json @@ -0,0 +1,86 @@ +{ + "version.label": { + "message": "0.20", + "description": "The label for version 0.20" + }, + "sidebar.docs.category.Getting Started": { + "message": "Getting Started", + "description": "The label for category Getting Started in sidebar docs" + }, + "sidebar.docs.category.Concepts": { + "message": "Concepts", + "description": "The label for category Concepts in sidebar docs" + }, + "sidebar.docs.category.Concepts.link.generated-index.title": { + "message": "Yew concepts", + "description": "The generated-index page title for category Concepts in sidebar docs" + }, + "sidebar.docs.category.Concepts.link.generated-index.description": { + "message": "Learn about the important Yew concepts!", + "description": "The generated-index page description for category Concepts in sidebar docs" + }, + "sidebar.docs.category.Using Basic Web Technologies In Yew": { + "message": "Using Basic Web Technologies In Yew", + "description": "The label for category Using Basic Web Technologies In Yew in sidebar docs" + }, + "sidebar.docs.category.Using Basic Web Technologies In Yew.link.generated-index.title": { + "message": "Yew's Take on Basic Web Technologies", + "description": "The generated-index page title for category Using Basic Web Technologies In Yew in sidebar docs" + }, + "sidebar.docs.category.Using Basic Web Technologies In Yew.link.generated-index.description": { + "message": "Yew centrally operates on the idea of keeping everything that a reusable piece of UI may needin one place - rust files, while also keeping the underlying technology accessible where necessary. Explore further to fully grasp what we mean by these statements:", + "description": "The generated-index page description for category Using Basic Web Technologies In Yew in sidebar docs" + }, + "sidebar.docs.category.Components": { + "message": "Components", + "description": "The label for category Components in sidebar docs" + }, + "sidebar.docs.category.Hooks": { + "message": "Hooks", + "description": "The label for category Hooks in sidebar docs" + }, + "sidebar.docs.category.HTML": { + "message": "HTML", + "description": "The label for category HTML in sidebar docs" + }, + "sidebar.docs.category.Advanced topics": { + "message": "Advanced topics", + "description": "The label for category Advanced topics in sidebar docs" + }, + "sidebar.docs.category.Advanced topics.link.generated-index.title": { + "message": "Advanced topics", + "description": "The generated-index page title for category Advanced topics in sidebar docs" + }, + "sidebar.docs.category.Advanced topics.link.generated-index.description": { + "message": "Learn about the advanced topics and inner workings of Yew!", + "description": "The generated-index page description for category Advanced topics in sidebar docs" + }, + "sidebar.docs.category.Struct Components": { + "message": "Struct Components", + "description": "The label for category Struct Components in sidebar docs" + }, + "sidebar.docs.category.More": { + "message": "More", + "description": "The label for category More in sidebar docs" + }, + "sidebar.docs.category.More.link.generated-index.title": { + "message": "Miscellaneous", + "description": "The generated-index page title for category More in sidebar docs" + }, + "sidebar.docs.category.Migration guides": { + "message": "Migration guides", + "description": "The label for category Migration guides in sidebar docs" + }, + "sidebar.docs.category.yew": { + "message": "yew", + "description": "The label for category yew in sidebar docs" + }, + "sidebar.docs.category.yew-agent": { + "message": "yew-agent", + "description": "The label for category yew-agent in sidebar docs" + }, + "sidebar.docs.category.yew-router": { + "message": "yew-router", + "description": "The label for category yew-router in sidebar docs" + } +} diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/how-it-works.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/how-it-works.mdx new file mode 100644 index 00000000000..c7645fe767c --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/how-it-works.mdx @@ -0,0 +1,7 @@ +--- +description: 有关框架的底层细节 +--- + +# 底层库的内部细节 + +组件生命周期状态机,虚拟 dom diff 算法。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/optimizations.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/optimizations.mdx new file mode 100644 index 00000000000..448206c3c0e --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/optimizations.mdx @@ -0,0 +1,93 @@ +--- +description: 加速你的应用程序 +--- + +# 性能优化与最佳实践 + +## neq_assign + +当组件从它的父组件接收 props 时,`change` 方法将被调用。除了允许你更新组件的状态,还允许你返回一个布尔类型的值 `ShouldRender` 来指示组件是否应该响应 props 的更改而重新渲染自身。 + +重新渲染的开销很大,你应该尽量避免。一个通用的法则是,你只应该在 props 实际更改时重新渲染。以下代码块展示了此法则,如果 props 和先前的 props 不同,则返回 `true`: + +```rust +fn change(&mut self, props: Self::Properties) -> ShouldRender { + if self.props != &props { + *self.props = props; + true + } else { + false + } +} +``` + +但是我们可以更进一步!对于任何实现了 `PartialEq` 的项,可以使用一个 trait 和一个 blanket implementation 将这六行样板代码减少到一行。 + +{% code title="neq\_assign.rs" %} + +```rust +pub trait NeqAssign { + fn neq_assign(&mut self, new: Self) -> ShouldRender; +} +impl NeqAssign for T { + fn neq_assign(&mut self, new: T) -> ShouldRender { + if self != &new { + *self = new; + true + } else { + false + } + } +} + +// ... +fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.props.neq_assign(props) +} +``` + +{% endcode %} + +该 trait 称为 `NeqAssign` 是因为如果目标值和新值不相等,它将赋为新值。 + +这比简单的实现还要短: + +```rust +// 不要这样做,除非你无法避免。 +fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.props = props; + true +} +``` + +你不仅限在 `change` 函数中使用它。通常,在 `update` 函数中执行此操作也是有意义的,尽管性能提升在那里不太明显。 + +## RC + +为了避免在重新渲染时为了创建 props 而克隆大块数据,我们可以使用智能指针来只克隆指针。如果在 props 和子组件中使用 `Rc<_>` 而不是普通未装箱的值,则可以延迟克隆直到需要修改子组件中的数据为止,在该组件中可以使用 `Rc::make_mut` 来对要更改数据进行克隆和获取可变引用。通过在要修改前不进行克隆,子组件可以在几乎没有性能成本的情况下拒绝与它们在 `Component::change` 中拥有状态的 props 相同的 props,这与数据本身需要先复制到父级 props 结构体中,然后在子级中进行比较和拒绝的情况相反。 + +对于不是 `Copy` 类型的数据,这种优化是最有用的。如果你能轻松地拷贝数据,那么将其放入智能指针中可能是不值得的。对于可以包含大量数据的结构,例如 `Vec`,`HashMap` 和 `String`,这种优化应该是值得的。 + +如果子组件从不更新组件的值,则这种优化效果最好,如果父组件很少更新组件的值,则效果更好。这使得 `Rc<_>s` 是包装纯组件属性值的不错选择。 + +## 视图函数 + +出于代码可读性的原因,将 `html!` 各个部分的代码迁移到他们自己的函数中通常是有意义的,这样就可以避免在深层嵌套的 HTML 中出现代码块向右偏移。 + +## 纯组件 / 函数式组件 + +纯组件是不会修改它们状态的组件,它们仅展示内容和向普通可变组件传递消息。它们与视图函数不同之处在于他们可以使用组件语法(``)而不是表达式语法(`{some_view_function()}`)来在 `html!` 宏中使用,并且根据它们的实现,它们可以被记忆化 - 使用前面提到的 `neq_assign` 逻辑来防止因为相同的 props 而重新渲染。 + +Yew 没有原生支持纯组件或者函数式组件,但是可以通过外部库获取它们。 + +函数式组件尚不存在,但是从理论上来讲,可以通过使用 proc 宏和标注函数生成纯组件。 + +## Keyed DOM nodes when they arrive + +## 使用 Cargo Workspaces 进行编译速度优化 + +可以说,使用 Yew 的最大缺点是编译时间长。编译时间似乎与 `html!` 宏块中的代码量相关。对于较小的项目,这通常不是什么大问题,但是对于跨多个页面的 web 应用程序,将代码拆分为多个 crates 以最大程度地减少编译器要做的工作通常是有意义的。 + +你应该尝试让主 crate 处理路由和页面选择,将所有公用的代码移动到另一个 crate,然后为每一个页面创建一个不同的 crate,其中每个页面可能是一个不同的组件,或者只是一个产生 `Html` 的大函数。在最好的情况下,你将从重新构建所有代码到只重新构建主 crate 和一个页面的 crate。在最糟糕的情况下,当你在“公共” crate 中编辑内容时,你将回到起点:编译所有依赖此公用 crate 的代码,这可能就是除此之外的所有代码。 + +如果你的主 crate 过于庞大,或者你想在深层嵌套的页面(例如,在另一个页面顶部渲染的页面)中快速迭代,则可以使用一个示例 crate 创建一个更简单的主页面实现并在之上渲染你正在开发的组件。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/callbacks.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/callbacks.mdx new file mode 100644 index 00000000000..15b9f0961bb --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/callbacks.mdx @@ -0,0 +1,27 @@ +--- +description: ComponentLink 和 Callbacks. +--- + +# 回调(Callbacks) + +组件“link”是一种机制,通过该机制,组件可以注册回调并自行更新。 + +## ComponentLink API + +### callback + +注册一个回调,该回调将在执行时将消息发送到组件的更新机制。在内部,它将使用提供的闭包返回的消息调用 `send_self`。提供 `Fn(IN) -> Vec`,返回 `Callback`。 + +### send_message + +当前循环结束后立即向组件发送消息,导致另一个更新循环启动。 + +### send_message_batch + +注册一个回调,该回调在执行时立即发送一批消息。如果其中任何一个消息将导致组件重新渲染,那么组件会在该批次所有消息被处理后重新渲染。提供 `Fn(IN) -> COMP::Message`,返回 `Callback`。 + +## Callbacks + +Callbacks 用于与 Yew 中的 services,agents 和父组件进行通信。它们仅仅是个 `Fn`,并由 `Rc` 包裹以允许被克隆。 + +它们有一个 `emit` 函数,该函数将它的 `` 类型作为参数并将其转换为目标所期望的消息。如果一个回调从父组件中通过 props 提供给子组件,则子组件可以在其 `update` 生命周期钩子中对该回调调用 `emit`,以将消息发送回父组件。在 `html!` 宏内被提供作为 props 的闭包或函数会自动转换为 Callbacks。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/lifecycle.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/lifecycle.mdx new file mode 100644 index 00000000000..563663ad1e6 --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/lifecycle.mdx @@ -0,0 +1,170 @@ +--- +description: 组件及其生命周期钩子 +--- + +# 组件(Components) + +## 组件是什么? + +组件是 Yew 的基石。它们管理自己的状态,并可以渲染为 DOM。组件是通过实现描述组件生命周期的 `Component` trait 来创建的。 + +## 生命周期 + +:::note +`为我们的文档做出贡献:`[添加组件的生命周期图示](https://github.com/yewstack/docs/issues/22) +::: + +## 生命周期方法 + +### Create + +当一个组件被创建时,它会从其父组件以及一个 `ComponentLink` 接收属性(properties)。属性(properties)可用于初始化组件的状态,“link”可用于注册回调或向组件发送消息。 + +通常将 props 和 link 存储在组件的结构体中,如下所示: + +```rust +pub struct MyComponent { + props: Props, + link: ComponentLink, +} + +impl Component for MyComponent { + type Properties = Props; + // ... + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + MyComponent { props, link } + } + + // ... +} +``` + +### View + +组件在 `view()` 方法中声明它的布局。Yew 提供了 `html!` 宏来声明 HTML 和 SVG 节点和它们的监听器及其子组件。这个宏的行为很像 React 中的 JSX,但是使用的是 Rust 表达式而不是 JavaScript。 + +```rust +impl Component for MyComponent { + // ... + + fn view(&self) -> Html { + let onclick = self.link.callback(|_| Msg::Click); + html! { + + } + } +} +``` + +有关用法的详细信息,请查看 [`html!` 宏指南](concepts/html/introduction.mdx)] + +### Mounted + +`mounted()` 组件生命周期方法调用是在 `view()` 被处理并且 Yew 已经把组件挂载到 DOM 上之后,浏览器刷新页面之前。组件通常希望实现此方法以执行只能在组件渲染元素之后才能执行的操作。如果你想在做出一些更改后重新渲染组件,返回 `true` 就可以了。 + +```rust +use stdweb::web::html_element::InputElement; +use stdweb::web::IHtmlElement; +use yew::prelude::*; + +pub struct MyComponent { + node_ref: NodeRef, +} + +impl Component for MyComponent { + // ... + + fn view(&self) -> Html { + html! { + + } + } + + fn mounted(&mut self) -> ShouldRender { + if let Some(input) = self.node_ref.cast::() { + input.focus(); + } + false + } +} +``` + +:::note +请注意,此生命周期方法不要求必须实现,默认情况下不会执行任何操作。 +::: + +### Update + +组件是动态的,可以注册以接收异步信息。`update()` 生命周期方法对于每个消息都会被调用。这使得组件可以根据消息的内容来更新自身,并决定是否需要重新渲染自己。消息可以由 HTML 元素监听器触发,或者由子组件,Agents,Services 或 Futures 发送。 + +`update()` 可能看起来像下面这个例子: + +```rust +pub enum Msg { + SetInputEnabled(bool) +} + +impl Component for MyComponent { + type Message = Msg; + + // ... + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::SetInputEnabled(enabled) => { + if self.input_enabled != enabled { + self.input_enabled = enabled; + true // 重新渲染 + } else { + false + } + } + } + } +} +``` + +### Change + +组件可能被其父节点重新渲染。发生这种情况时,它们可以接收新的属性(properties)并选择重新渲染。这种设计通过更改属性(properties)来促进父子组件之间的通信。你不是必须实现 `change()`,但是如果想在组件被创建后通过 props 来更新组件,则可能要这么做。 + +一个原始的实现可能看起来像: + +```rust +impl Component for MyComponent { + // ... + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.props = props; + true // 当提供了新的 props 将始终重新渲染。 + } +} +``` + +### Destroy + +组件从 DOM 上被卸载后,Yew 调用 `destroy()` 生命周期方法来支持任何必要的清理操作。这个方法是可选的,默认情况下不执行任何操作。 + +## 关联类型 + +`Component` trait 有两个关联类型:`Message` 和 `Properties`。 + +```rust +impl Component for MyComponent { + type Message = Msg; + type Properties = Props; + + // ... +} +``` + +`Message` 表示组件可以处理以触发某些副作用的各种消息。例如,你可能有一条 `Click` 消息,该消息触发 API 请求或者切换 UI 组件的外观。通常的做法是在组件模块中创建一个叫做 `Msg` 的枚举并将其用作组件中的消息类型。通常将“message”缩写为“msg”。 + +```rust +enum Msg { + Click, +} +``` + +`Properties` 表示从父级传递到组件的信息。此类型必须实现 `Properties` trait(通常通过派生),并且可以指定某些属性(properties)是必需的还是可选的。创建和更新组件时使用此类型。通常的做法是在组件模块中创建一个叫做 `Props` 的结构体并将其用作组件的 `Properties` 类型。通常将“properties”缩写为“props”。由于 props 是从父组件传递下来的,因此应用程序的根组件通常有一个类型为 `()` 的 `Properties`。如果你希望为根组件指定属性(properties),请使用 `App::mount_with_props` 方法。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/properties.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/properties.mdx new file mode 100644 index 00000000000..f07a81fa927 --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/properties.mdx @@ -0,0 +1,72 @@ +--- +description: 父组件到子组件的通信 +--- + +# 属性(Properties) + +如“组件(Components)”页面所述,Properties 用于父级到子组件的通信。 + +## 派生宏 + +不要尝试自己去实现 `Properties`,而是通过使用 `#[derive(Properties)]` 来派生它。 + +### 必需属性 + +默认情况下,实现了 `Properties` 的结构体中的字段是必需的。当缺少了该字段并且在 `html!` 宏中创建了组件时,将返回编译错误。对于具有可选属性的字段,使用 `#[prop_or_default]` 来使用该类型的默认值。要指定一个值,请使用 `#[prop_or_else(value)]`,其中 value 是该属性的默认值。例如,要将一个布尔值的默认值设置为 `true`,请使用属性 `#[prop_or_else(true)]`。可选属性通常使用 `Option`,其默认值为 `None`。 + +### PartialEq + +如果可以的话,在你的 props 上派生 `PartialEq` 通常是很有意义的。这使用了一个**性能优化与最佳实践**部分解释了的技巧,可以更轻松地避免重新渲染。 + +## Properties 的内存/速度开销 + +记住组件的 `view` 函数签名: + +```rust +fn view(&self) -> Html +``` + +你对组件的状态取了一个引用,并用来创建 `Html`。但是 properties 是有所有权的值(owned values)。这意味着为了创造它们并且将它们传递给子组件,我们需要获取 `view` 函数里提供的引用的所有权。这是在将引用传递给组件时隐式克隆引用完成的,以获得构成其 props 的有所有权的值。 + +这意味着每个组件都有从其父级传递来的状态的独特副本,而且,每当你重新渲染一个组件时,该重新渲染组件的所有子组件的 props 都将被克隆。 + +这意味着如果你将 _大量_ 数据作为 props(大小为 10 KB 的字符串)向下传递,则可能需要考虑将子组件转换为在父级运行返回 `Html` 的函数,因为这样就不会被强制克隆你的数据。 + +另外,如果你不需要修改作为 props 传递的大数据,而只需要显示它,则可以将其包装在 `Rc` 中,以便仅克隆一个引用计数的指针,而不是数据本身。 + +## 示例 + +```rust +pub struct LinkColor { + Blue, + Red, + Green, + Black, + Purple, +} + +impl Default for LinkColor { + fn default() -> Self { + // 除非另有说明,否则链接的颜色将为蓝色 + LinkColor::Blue + } +} + +#[derive(Properties, PartialEq)] +pub struct LinkProps { + /// 链接必须有一个目标地址 + href: String, + /// 如果链接文本很大,这将使得复制字符串开销更小 + /// 除非有性能问题,否则通常不建议这么做 + text: Rc, + /// 链接的颜色 + #[prop_or_default] + color: LinkColor, + /// 如果为 None,则 view 函数将不指定大小 + #[prop_or_default] + size: Option + /// 当 view 函数没有指定 active,其默认为 true + #[prop_or_else(true)] + active: bool, +} +``` diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/refs.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/refs.mdx new file mode 100644 index 00000000000..75e9e1d9fd9 --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/advanced-topics/struct-components/refs.mdx @@ -0,0 +1,23 @@ +--- +title: Refs +description: 超出界限的 DOM 访问 +--- + +`ref` 关键词可被用在任何 HTML 元素或组件内部以获得该项所附加到的 DOM 元素。这可被用于在 `view` 生命周期方法之外来对 DOM 进行更改。 + +这对于获取 canvas 元素或者滚动到页面的不同部分是有用的。 + +语法如下: + +```rust +// 在 create 中 +self.node_ref = NodeRef::default(); + +// 在 view 中 +html! { +
    +} + +// 在 update 中 +let has_attributes = self.node_ref.cast::().unwrap().has_attributes(); +``` diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/agents.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/agents.mdx new file mode 100644 index 00000000000..725a13b5fc8 --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/agents.mdx @@ -0,0 +1,40 @@ +--- +title: Agents +description: Yew 的 Actor 系统 +--- + +Agents 和 Angular 的 [Services](https://angular.io/guide/architecture-services) 相似(但没有依赖注入),给 Yew 提供了 [Actor 模型](https://en.wikipedia.org/wiki/Actor_model)。Agents 可以用于在组件之间路由消息,而与它们在组件层次结构中的位置无关,或者可以用于协调全局状态,或者可以用于从主 UI 线程上卸载计算密集型任务,或者在不同的标签页间通信(在未来)。 + +Agents 使用 [web-workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) 同时运行来实现并发。 + +## 生命周期 + +![Agent lifecycle](https://user-images.githubusercontent.com/42674621/79125224-b6481d80-7d95-11ea-8e6a-ab9b52d1d8ac.png) + +## Agents 的类型 + +#### Reaches + +- Job - 在 UI 线程上为每个新的 Bridge 生成一个新的 Agent。这对于将与浏览器通信的共享但独立的行为移出组件是很有用的。(待验证)任务完成后,Agent 将消失。 +- Context - Bridges 将生成或连接到 UI 线程上的 agent。这可用于在组件和其它 Agents 之间协调状态。当没有 Bridge 连接到该 Agent 时,Agent 将消失。 +- Private - 与 Job 相同,但运行在自己的 web worker 中。 +- Public - 与 Context 相同,但运行在自己的 web worker 中。 +- Global \(WIP\) + +## Agent 通信 + +### Bridges + +Bridges 将连接到一个 Agent 并且允许双向通信。 + +### Dispatchers + +Dispatchers 和 Bridges 类似,但是他们只能发送消息给 Agents。 + +## 开销 + +Agents 通过使用二进制码 bincode 序列化其消息来进行通信。因此,存在比仅调用函数相比更高的性能消耗。除非计算成本或者在任意组件间协调的需求超过消息传递的成本,否则你应该尽可能地在函数中包含你的应用逻辑。 + +## Further reading + +- The [web_worker_fib](https://github.com/yewstack/yew/tree/master/examples/web_worker_fib) example shows how components can use agents to communicate with each other. diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/function-components/hooks/custom-hooks.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/function-components/hooks/custom-hooks.mdx new file mode 100644 index 00000000000..f872d01c0f3 --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/function-components/hooks/custom-hooks.mdx @@ -0,0 +1,121 @@ +--- +title: 自定义钩子(Custom Hooks) +description: 定义你自己的 Hooks +--- + +## 定义自定义钩子 + +组件中与状态有关的逻辑可以通过创建自定义 Hooks 提取到函数中。 + +假设我们有一个组件,它订阅了一个代理(agent)并且会显示发送给它的消息。 + +```rust +#[function_component(ShowMessages)] +pub fn show_messages() -> Html { + let (state, set_state) = use_state(|| vec![]); + + { + let mut state = Rc::clone(&state); + use_effect(move || { + let producer = EventBus::bridge(Callback::from(move |msg| { + let mut messages = (*state).clone(); + messages.push(msg); + set_state(messages) + })); + + || drop(producer) + }); + } + + let output = state.iter().map(|it| html! {

    { it }

    }); + html! {
    { for output }
    } +} +``` + +这段代码有一个问题:逻辑不能被另一个组件重用。如果我们构建另一个跟踪消息的组件,我们可以将逻辑移动到自定义钩子中,而不是复制代码。 + +我们将首先创建一个名为`use_subscribe`的新函数。 `use_`前缀通常表示此函数是一个钩子。这个函数将不接受任何参数并返回`Rc>>` 。 + +```rust +fn use_subscribe() -> Rc>> { + // ... +} +``` + +钩子的逻辑在`use_hook`的回调中。 `use_hook`指的是自定义 Hook 的处理函数。它接受 2 个参数: `hook_runner`和`initial_state_producer` 。 + +`hook_runner`中包含了所有钩子的逻辑,它的回调的返回值又会被`use_hook`返回。 `hook_runner`需要 2 个参数:分别是对钩子和`hook_callback`它们两个的内部状态的可变引用。 而`hook_callback`同样也要 2 个参数:一个回调和一个 bool,回调接受`internal_state` ,也就是对内部状态实例的可变引用,并且会调执行实际的更改,还会返回表示`ShouldRender`的布尔值,第二个参数 bool 的用处是指示它是否在组件渲染后运行。`use_hook`的第二个参数`initial_state_producer`接受用于创建内部状态实例的回调。这里说的内部状态指的是一个实现了`Hook` trait 的结构体。 + +现在让我们为`use_subscribe`钩子创建状态(state struct)。 + +```rust +/// `use_subscribe` internal state +struct UseSubscribeState { + /// holds all the messages received + pub messages: Rc>>, +} + +impl Hook for UseSubscribeState {} +``` + +接下来我们为`use_subscribe`添加实际逻辑。 + +```rust +fn use_subscribe() -> Rc>> { + use_hook( + // hook's handler. all the logic goes in here + |state: &mut UseSubscribeState, hook_callback| { + // calling other Hooks inside a hook + use_effect(move || { + let producer = EventBus::bridge(Callback::from(move |msg| { + hook_callback( + // where the mutations of state are performed + |state| { + (*state.messages).borrow_mut().deref_mut().push(msg); + true // should re-render + }, false // run post-render + ) + })); + + || drop(producer) + }); + + // return from hook + state.messages.clone() + }, + // initial state producer + || UseSubscribeState { messages: Rc::new(RefCell::new(vec![])) }, + ) +} +``` + +现在我们可以使用自定义钩子了: + +```rust +#[function_component(ShowMessages)] +pub fn show_messages() -> Html { + let state = use_subscribe(); + let output = state.borrow().deref().into_iter().map(|it| html! {

    { it }

    }); + + html! {
    { for output }
    } +} +``` + +需要特别注意的是创建自定义钩子时`use_hook`不是必须的,它们只是用来包含其他钩子。通常应避免使用`use_hook`。 + +```rust +fn use_subscribe() -> Rc> { + let (state, set_state) = use_state(Vec::new); + + use_effect(move || { + let producer = EventBus::bridge(Callback::from(move |msg| { + let mut messages = (*state).clone(); + messages.push(msg); + set_state(messages) + })); + || drop(producer) + }); + + state +} +``` diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/function-components/introduction.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/function-components/introduction.mdx new file mode 100644 index 00000000000..babad678459 --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/function-components/introduction.mdx @@ -0,0 +1,27 @@ +--- +title: 函数式组件 +sidebar_label: 简介 +description: 介绍函数式组件 +slug: /concepts/function-components +--- + +函数式组件是普通组件的简化版本。它们由一个接收 props 的函数组成,并通过返回`Html`来确定应该呈现什么。基本上,它是一个简化为`view`方法的组件。就其本身而言,这将是相当有限的,因为您只能创建纯组件,而这就是 Hook 大展身手的地方。Hook 允许函数组件无需实现`Component` trait,就可以使用状态(state)和其他 Yew 功能。 + +## 创建函数式组件 + +创建函数式组件的最简单方法是在函数前添加`#[function_component]`属性。 + +```rust +#[function_component(HelloWorld)] +fn hello_world() -> Html { + html! { "Hello world" } +} +``` + +### 更多细节 + +函数式组件由两部分组成。首先, `FunctionProvider` trait 与`Component` trait 差不多,但它只有一个名为`run`方法。之后是`FunctionComponent`结构体,它封装了`FunctionProvider`类型并将其转换为实际的`Component` 。 `#[function_component]`属性本质上只是`FunctionProvider`并将其暴露在`FunctionComponent` 。 + +### 钩子(Hooks) + +钩子(Hooks)就是让您“钩住”组件的状态(state)和/或生命周期并执行操作的函数。 除了 Yew 自带的一些预定义的 Hook。您也可以创建自己的。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/html/components.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/html/components.mdx new file mode 100644 index 00000000000..cb795e0cf21 --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/html/components.mdx @@ -0,0 +1,114 @@ +--- +description: 使用具有层次结构的组件来创建复杂的布局 +--- + +# 组件 + +## 基础 + +任何实现了 `Component` trait 的类型都可被用在 `html!` 宏中: + +```rust +html!{ + <> + // 没有属性 + + + // 具有属性 + + + // 同时提供全套的 props + + +} +``` + +## 嵌套 + +如果组件的 `Properties` 中有 `children` 字段,则可以被传递子组件。 + +{% code title="parent.rs" %} + +```rust +html! { + +

    { "Hi" }

    +
    { "Hello" }
    +
    +} +``` + +{% endcode %} + +{% code title="container.rs" %} + +```rust +pub struct Container(Props); + +#[derive(Properties)] +pub struct Props { + pub children: Children, +} + +impl Component for Container { + type Properties = Props; + + // ... + + fn view(&self) -> Html { + html! { +
    + { self.0.children.clone() } +
    + } + } +} +``` + +{% endcode %} + +## 拥有 Props 的嵌套子组件 + +如果包含组件标注了 children 的类型,则可以访问和更改嵌套组件的属性。在下面的示例中,`List` 组件可以包含 `ListItem` 组件。有关此模式的真实示例,请查看 `yew-router` 的源码。有关更高级的示例,请在 yew 主仓库中查看 `nested-list` 示例代码。 + +{% code title="parent.rs" %} + +```rust +html! { + + + + + +} +``` + +{% endcode %} + +{% code title="list.rs" %} + +```rust +pub struct List(Props); + +#[derive(Properties)] +pub struct Props { + pub children: ChildrenWithProps, +} + +impl Component for List { + type Properties = Props; + + // ... + + fn view(&self) -> Html { + html!{{ + for self.0.children.iter().map(|mut item| { + item.props.value = format!("item-{}", item.props.value); + item + }) + }} + } +} +``` + +{% endcode %} diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/html/elements.mdx b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/html/elements.mdx new file mode 100644 index 00000000000..dc2726fefda --- /dev/null +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/version-0.20/concepts/html/elements.mdx @@ -0,0 +1,264 @@ +--- +description: HTML 和 SVG 元素均受支持 +--- + +# 元素 + +## 标签结构 + +元素标签必须是自闭合的 `<... />`,或是每个标签都有一个对应的闭合标签。 + + + + +```rust +html! { +
    +} +``` + + + +```rust +html! { +
    // <- 缺少闭合标签 +} +``` + + + +```rust +html! { + +} +``` + + + +```rust +html! { + // <- 没有自闭合 +} +``` + + + +:::note +为方便起见,一些 _通常_ 需要闭合标签的元素是被**允许**自闭合的。例如,`html! {
    }` 这样写是有效的。 +::: + +## Children + +轻松创建复杂的嵌套 HTML 和 SVG 布局: + + + + +```rust +html! { +
    +
    +
    + + + + +