Skip to content

feat(PdfReader): add setUrl method#815

Merged
ArgoZhang merged 2 commits intomasterfrom
feat-pdf
Dec 13, 2025
Merged

feat(PdfReader): add setUrl method#815
ArgoZhang merged 2 commits intomasterfrom
feat-pdf

Conversation

@ArgoZhang
Copy link
Copy Markdown
Member

@ArgoZhang ArgoZhang commented Dec 13, 2025

Link issues

fixes #814

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Add support for dynamically changing the PDF source in PdfReader and centralize PDF disposal and toolbar cleanup.

New Features:

  • Introduce a setUrl method in the PdfReader JS interop to update the displayed PDF by URL.
  • Add a setData method to switch the PDF source to in-memory data instead of a URL.

Enhancements:

  • Refactor PDF loading to use a shared loadPdf helper with state stored in the Data map.
  • Centralize PDF disposal logic, including observers, event handlers, and DOM cleanup, into a reusable disposePdf function and use it from dispose and when changing sources.
  • Reset the toolbar and viewer state whenever a new PDF is loaded.

Copilot AI review requested due to automatic review settings December 13, 2025 03:26
@bb-auto bb-auto Bot added the enhancement New feature or request label Dec 13, 2025
@bb-auto bb-auto Bot added this to the v9.2.0 milestone Dec 13, 2025
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Dec 13, 2025

Reviewer's Guide

Adds dynamic URL/data switching support to PdfReader by introducing setUrl/setData JS interop APIs, centralizing PDF load/disposal logic, and wiring the Blazor component to call setUrl when the Url parameter changes.

Sequence diagram for PdfReader Url change using setUrl

sequenceDiagram
    actor User
    participant Blazor as PdfReaderComponent
    participant JSRuntime
    participant JsModule as PdfReader_razor_js
    participant DataStore as Data
    participant PdfContext as PdfObject

    User->>Blazor: Update Url parameter
    Blazor->>Blazor: OnAfterRenderAsync detects Url changed
    Blazor->>JSRuntime: InvokeVoidAsync setUrl(Id, Url)
    JSRuntime->>JsModule: setUrl(id, url)

    JsModule->>DataStore: get(id)
    DataStore-->>JsModule: PdfObject
    JsModule->>JsModule: disposePdf(PdfObject)

    alt url is falsy
        JsModule-->>JSRuntime: return
    else url is truthy
        JsModule->>JsModule: options = PdfObject.options
        JsModule->>JsModule: options.url = url
        JsModule->>JsModule: options.data = null
        JsModule->>JsModule: loadPdf(PdfObject)
        JsModule->>DataStore: set(id, PdfObject with pdfViewer, loadingTask, observer)
    end
Loading

Updated class diagram for PdfReader JS interop and PDF context

classDiagram
    class PdfReaderComponent {
        string Id
        string Url
        int CurrentPage
        string _url
        int _currentPage
        OnAfterRenderAsync(firstRender bool) Task
    }

    class JsPdfReaderModule {
        init(id string, invoke any, options any) Promise
        setUrl(id string, url string) Promise
        setData(id string, data any) Promise
        setScaleValue(id string, value number) void
        resetThumbnails(id string) void
        loadPdf(pdf PdfContext) Promise
        disposePdf(pdf PdfContext) void
        loadMetadata(el HTMLElement, pdfViewer any, metadata any) void
        addToolbarEventHandlers(el HTMLElement, pdfViewer any, invoke any, options any) void
        removeToolbarEventHandlers(el HTMLElement) void
        resetToolbarView(el HTMLElement, pdfViewer any) void
        printPdf(url string) void
        dispose(id string) void
    }

    class PdfContext {
        HTMLElement el
        any invoke
        any options
        any pdfViewer
        any observer
        any loadingTask
    }

    class DataStore {
        +set(id string, pdf PdfContext) void
        +get(id string) PdfContext
        +remove(id string) void
    }

    PdfReaderComponent ..> JsPdfReaderModule : JS interop
    JsPdfReaderModule o--> PdfContext : manages
    JsPdfReaderModule ..> DataStore : stores PdfContext
    DataStore o--> PdfContext : holds instances

    note for JsPdfReaderModule "Represents PdfReader.razor.js exported functions"
    note for PdfReaderComponent "C# Blazor component calling setUrl when Url changes"
Loading

File-Level Changes

Change Details Files
Refactor PdfReader initialization to store PDF state and support loading from either URL or raw data.
  • Store { el, invoke, options } in the Data map during init instead of storing viewer/observer immediately.
  • Branch initialization to call setUrl when options.url is present or setData when loading from options.data.
  • Change loadPdf to accept a single pdf state object and extract el/invoke/options from it.
src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js
Introduce setUrl and setData JS APIs to reload PDFs and update options in place.
  • Add setUrl(id, url) to dispose existing PDF resources, update options.url, clear options.data, and reload via loadPdf.
  • Add setData(id, data) to dispose existing PDF resources, clear options.url, set options.data, and reload via loadPdf.
  • Ensure both methods no-op when called with falsy url/data values.
src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js
Centralize PDF viewer lifecycle management, including cleanup of observers, toolbar events, and DOM elements.
  • Extend loadPdf to save pdfViewer, loadingTask, and observer back onto the pdf state and reset the toolbar view before wiring events.
  • Add disposePdf helper to disconnect observers, destroy loadingTask, remove toolbar event handlers, and clear viewer/thumbnails DOM nodes.
  • Add removeToolbarEventHandlers to encapsulate all toolbar-related EventHandler.off calls and iframe/close-button cleanup.
  • Update dispose(id) to fetch the pdf state from Data and delegate cleanup to disposePdf.
src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js
Wire Blazor PdfReader component to use the new setUrl JS interop method when the Url parameter changes.
  • In OnAfterRenderAsync, replace InvokeInitAsync on Url changes with InvokeVoidAsync("setUrl", Id, _url) while retaining existing current-page handling logic.
src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#814 Add a JavaScript setUrl method to the PdfReader client script that can update the PDF source without reinitializing the whole component.
#814 Update the PdfReader .razor.cs component to call the new setUrl JavaScript method when the Url parameter changes instead of re-running the full initialization.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ArgoZhang ArgoZhang merged commit 268a13b into master Dec 13, 2025
3 of 4 checks passed
@ArgoZhang ArgoZhang deleted the feat-pdf branch December 13, 2025 03:26
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • Both setUrl/setData and dispose call disposePdf(pdf) without checking whether Data.get(id) returned a valid object, so it would be safer to add a null/undefined guard in disposePdf (or at the call sites) to avoid runtime errors when an unknown or already-disposed id is passed.
  • In removeToolbarEventHandlers, document.querySelector('.bb-view-print-iframe') operates at the document level and may affect other instances; consider scoping this to el (or an instance-specific container) to avoid cross-instance interference.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Both `setUrl`/`setData` and `dispose` call `disposePdf(pdf)` without checking whether `Data.get(id)` returned a valid object, so it would be safer to add a null/undefined guard in `disposePdf` (or at the call sites) to avoid runtime errors when an unknown or already-disposed id is passed.
- In `removeToolbarEventHandlers`, `document.querySelector('.bb-view-print-iframe')` operates at the document level and may affect other instances; consider scoping this to `el` (or an instance-specific container) to avoid cross-instance interference.

## Individual Comments

### Comment 1
<location> `src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js:21-24` </location>
<code_context>
-    if (options.url === null) {
+    Data.set(id, { el, invoke, options });
+
+    if (options.url) {
+        setUrl(id, options.url);
+    }
+    else {
+        setData(id, options.data);
+    }
</code_context>

<issue_to_address>
**issue (bug_risk):** The async `setUrl`/`setData` calls in `init` are not awaited, changing initialization ordering semantics.

Previously `init` only resolved after `loadPdf` completed, so callers could treat it as “viewer is ready.” Now `init` returns as soon as it fires `setUrl`/`setData`, which may run asynchronously, changing that contract and potentially introducing race conditions. To keep the old behavior, `await setUrl(...)` / `await setData(...)`; if the change is intentional, clarify the async semantics in the API (e.g., via naming or docs).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a setUrl method to the PdfReader component to enable dynamic URL changes without requiring full component reinitialization. The implementation refactors the initialization logic to separate data storage from PDF loading, allowing the URL or data to be updated independently after the component is mounted.

Key changes:

  • Introduces setUrl and setData export functions in JavaScript for dynamic PDF content updates
  • Refactors init to store component state first, then delegate to setUrl/setData for loading
  • Extracts cleanup logic into disposePdf and removeToolbarEventHandlers helper functions for reuse

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.

File Description
PdfReader.razor.js Adds setUrl/setData methods, refactors init and loadPdf to support dynamic updates, extracts cleanup logic into disposePdf and removeToolbarEventHandlers
PdfReader.razor.cs Updates URL change detection to call setUrl instead of reinitializing the entire component
Comments suppressed due to low confidence (10)

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.cs:206

  • Equality checks on floating point values can yield unexpected results.
        if (_currentRotation != CurrentRotation)

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js:179

  • Unused variable title.
    const title = el.querySelector('.bb-view-pdf-dialog-title');

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js:180

  • Unused variable author.
    const author = el.querySelector('.bb-view-pdf-dialog-author');

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js:181

  • Unused variable subject.
    const subject = el.querySelector('.bb-view-pdf-dialog-subject');

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js:182

  • Unused variable keywords.
    const keywords = el.querySelector('.bb-view-pdf-dialog-keywords');

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js:186

  • Unused variable modified.
    const modified = el.querySelector('.bb-view-pdf-dialog-modified');

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js:206

  • Unused variable webview.
    const webview = el.querySelector('.bb-view-pdf-dialog-webview');

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js:670

  • The initial value of v is unused, since it is always overwritten.
    let v = 100;

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js:489

  • Avoid automated semicolon insertion (95% of all statements in the enclosing function have an explicit semicolon).
    })

src/components/BootstrapBlazor.PdfReader/PdfReader.razor.js:593

  • Avoid automated semicolon insertion (91% of all statements in the enclosing function have an explicit semicolon).
    delete el.widths

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if (!i.classList.contains("pdfViewer")) {
i.remove();
}
})
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon at the end of the statement. While JavaScript allows this due to automatic semicolon insertion, it's a best practice to include semicolons for consistency with the rest of the codebase.

Suggested change
})
});

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +25
setUrl(id, options.url);
}
else {
setData(id, options.data);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setUrl function is called without await in the init function, but setUrl is an async function that performs important operations like loadPdf. This means the init function may complete before the PDF is fully loaded, which could cause race conditions or unexpected behavior. Either await the call or ensure the function doesn't need to complete before init returns.

Suggested change
setUrl(id, options.url);
}
else {
setData(id, options.data);
await setUrl(id, options.url);
}
else {
await setData(id, options.data);

Copilot uses AI. Check for mistakes.
setUrl(id, options.url);
}
else {
setData(id, options.data);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setData function is called without await in the init function. Similar to the setUrl issue, this could cause race conditions or unexpected behavior.

Suggested change
setData(id, options.data);
await setData(id, options.data);

Copilot uses AI. Check for mistakes.
}

export async function setUrl(id, url) {
const pdf = Data.get(id);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setUrl function doesn't check if the pdf object exists before calling disposePdf. If Data.get(id) returns undefined or null (e.g., if init was never called or the component was already disposed), this will cause disposePdf to receive an invalid value, potentially leading to runtime errors when trying to destructure properties.

Suggested change
const pdf = Data.get(id);
const pdf = Data.get(id);
if (!pdf) {
return;
}

Copilot uses AI. Check for mistakes.

export async function setData(id, data) {
const pdf = Data.get(id);
disposePdf(pdf);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setData function doesn't check if the pdf object exists before calling disposePdf. If Data.get(id) returns undefined or null, this will cause similar issues as in setUrl.

Suggested change
disposePdf(pdf);
if (pdf) {
disposePdf(pdf);
}

Copilot uses AI. Check for mistakes.
pdf.observer = setObserver(el);
}

const disposePdf = pdf => {
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disposePdf function doesn't check if the pdf parameter is null or undefined before destructuring. If called with an invalid value, this will cause a runtime error.

Suggested change
const disposePdf = pdf => {
const disposePdf = pdf => {
if (!pdf) {
return;
}

Copilot uses AI. Check for mistakes.
EventHandler.off(closeButton, "click");
}
}
disposePdf(pdf);
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dispose function doesn't check if the pdf object exists before calling disposePdf. If Data.get(id) returns undefined, disposePdf will be called with an invalid value.

Suggested change
disposePdf(pdf);
if (pdf) {
disposePdf(pdf);
}

Copilot uses AI. Check for mistakes.
}

const resetToolbarView = (el, pdfViewer) => {
const scaleEl = el.querySelector(".bb-view-scale-input");
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable scaleEl.

Suggested change
const scaleEl = el.querySelector(".bb-view-scale-input");

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(PdfReader): add setUrl method

2 participants