--- name: port-from-bslib description: Comprehensive guide for porting UI components from R's bslib package to py-shiny. Use this skill when: (1) User asks to "port this feature" or "port a component" and mentions bslib or links to a bslib PR (e.g., github.com/rstudio/bslib/pull/...), (2) Porting a new component from bslib to py-shiny, (3) Adding a new input, output, or UI component that exists in bslib, (4) Implementing feature parity with bslib, (5) Working on bslib-related features or components. Covers the complete workflow including understanding source implementation, creating Python equivalents, vendoring assets (SCSS, CSS, JavaScript), creating tests, documentation, and all necessary project files. --- # Porting Components from bslib to py-shiny This guide explains how to port new UI components from the R bslib package to py-shiny. It assumes you're an experienced developer familiar with Python, R, and JavaScript/TypeScript, but may be new to the specifics of these repositories. ## Background The bslib R package serves as the primary development location for new Bootstrap 5 components in the Shiny ecosystem. Components developed in bslib are then ported to py-shiny to maintain feature parity between Shiny for R and Shiny for Python. **Key relationship**: bslib provides both the R implementation and the compiled JavaScript/CSS assets that py-shiny vendors and uses directly. ## Overview of the Porting Process The porting process involves three main phases: 1. **Understanding the source** - Study the bslib implementation (R, TypeScript, SCSS) 2. **Implementing the port** - Create Python equivalents and add client-side bindings 3. **Testing and documentation** - Ensure correctness with unit and end-to-end tests ## Phase 1: Understanding the Source Implementation ### Step 1.1: Locate the bslib PR Find the bslib PR that introduced the feature. The py-shiny PR description should reference it (e.g., `Related https://github.com/rstudio/bslib/pull/...`). **Checklist:** - [ ] Located the bslib PR - [ ] Reviewed the PR description and any design discussions ### Step 1.2: Identify the core source files In the bslib PR, locate these key files: 1. **R implementation**: `R/[feature-name].R` - The main component function(s) 2. **TypeScript bindings**: `srcts/src/components/[featureName].ts` - Client-side behavior 3. **SCSS styles**: `inst/components/scss/[feature_name].scss` - Component styles 4. **Unit tests**: `tests/testthat/test-[feature-name].R` - R unit tests **Note**: Generated files (compiled JS/CSS, documentation) can be ignored - focus on source files. **Checklist:** - [ ] Found the R implementation file(s) - [ ] Found the TypeScript source file(s) - [ ] Found the SCSS source file - [ ] Found the unit test file(s) - [ ] Reviewed how TypeScript is registered in `srcts/src/components/index.ts` ### Step 1.3: Understand the component structure Study the bslib implementation to understand: 1. **API design**: Function signature, parameters, defaults 2. **HTML structure**: The DOM elements created by the R function 3. **Client-side behavior**: How the TypeScript binding handles interactions 4. **Shiny communication**: How the component sends/receives values from the server 5. **Dependencies**: What other components or utilities it relies on **Key patterns to note:** - Is the component an input binding? If so, how does the binding register (typically via a CSS class)? - What markup structure does it generate on the server side? - How does the component integrate with Bootstrap classes? - How are configuration options passed from R to JavaScript -- data attributes, embedded JSON, etc.? - How does the component receive data from the server -- via `sendInputMessage()` or `sendCustomMessage()`? - How does the component receive data from the client -- via `receiveMessage()` or small inputs set with `Shiny.setInputValue()`? **Checklist:** - [ ] Documented the component's API surface - [ ] Understood the HTML structure and CSS classes - [ ] Identified client-side event handlers and state management - [ ] Noted any dependencies on other components ## Phase 2: Implementing the Port ### Step 2.1: Create the Python implementation Create a new file in `shiny/ui/` for the component implementation. **File naming**: Use snake_case matching the R function name: - R: `input_submit_textarea()` → Python: `_input_submit_textarea.py` **Implementation notes:** - Translate R's htmltools to Python's htmltools - Use `@add_example()` decorator for documentation examples (examples are created in a later step) - Follow existing patterns for parameter validation - Use `resolve_id()` for module namespace support - Use `restore_input()` for bookmarking support - Include both the main component function and any `update_*()` functions **Common translations:** - R `tags$div()` → Python `div()` or `tags.div()` - R `!!!args` (splicing) → Python `*args`+`**kwargs` - R `NULL` → Python `None` - R lists → Python dicts or lists as appropriate - R `paste0()` → Python f-strings or `.format()` **Checklist:** - [ ] Created `shiny/ui/_[component_name].py` - [ ] Implemented the main component function with full docstring - [ ] Implemented any `update_*()` functions - [ ] Added parameter validation - [ ] Added support for modules (`resolve_id`) - [ ] Added support for bookmarking (`restore_input`) - [ ] Used `components_dependencies()` for client-side deps ### Step 2.2: Export the new functions Update `shiny/ui/__init__.py` to export the new component: ```python from ._input_submit_textarea import input_submit_textarea, update_submit_textarea __all__ = ( # ... existing exports ... "input_submit_textarea", "update_submit_textarea", ) ``` If the component is suitable for express mode, also export from `shiny/express/ui/__init__.py`. **Checklist:** - [ ] Added imports to `shiny/ui/__init__.py` - [ ] Added to `__all__` tuple in `shiny/ui/__init__.py` - [ ] (If applicable) Exported from `shiny/express/ui/__init__.py` ### Step 2.3: Vendor assets from bslib The TypeScript in bslib is compiled to JavaScript and bundled, and SCSS is compiled to CSS. Py-shiny vendors these compiled assets along with the SCSS source files from bslib. **Process:** 1. Ensure bslib PR is merged and the feature is in the branch referenced in `scripts/_pkg-sources.R` (usually `@main`) 2. If vendoring from a non-default branch or specific commit, update `scripts/_pkg-sources.R` temporarily 3. Run `make upgrade-html-deps` to vendor the latest assets from bslib, shiny, sass, and htmltools **What `make upgrade-html-deps` does:** - Copies SCSS source files from bslib to `shiny/www/shared/sass/bslib/components/scss/` - Updates all theme preset `_04_rules.scss` files to import the new SCSS - Compiles SCSS to CSS for all themes - Vendors compiled JavaScript bundles (`components.min.js`, etc.) - Updates other shared assets from upstream packages **Files updated by this process** (examples): - `shiny/www/shared/sass/bslib/components/scss/[feature_name].scss` (SCSS source) - `shiny/www/shared/sass/preset/*/04_rules.scss` (27 theme preset files with new imports) - `shiny/www/shared/bslib/components/components.min.js` (compiled JavaScript) - `shiny/www/shared/bslib/components/components.min.js.map` (source map) - `shiny/www/shared/bslib/components/components.css` (compiled CSS) - Other theme-specific CSS files **Note**: This is a manual process that's not part of CI. The vendored files are committed to the repository. **Checklist:** - [ ] Verified bslib PR is merged (or adjusted `scripts/_pkg-sources.R` if needed) - [ ] Ran `make upgrade-html-deps` - [ ] Reviewed changes to vendored files (SCSS, CSS, JS) - [ ] Verified new SCSS imports in theme preset files - [ ] Restored `scripts/_pkg-sources.R` if temporarily modified ### Step 2.4: Create API examples Create example applications demonstrating the component's usage. **Location**: `shiny/api-examples/[component_name]/` **Files to create**: - `app-core.py` - Core mode example - `app-express.py` - Express mode example (if applicable) **Example structure**: ```python # app-express.py from shiny.express import input, render, ui ui.input_submit_textarea("text", placeholder="Enter some input...") @render.text def value(): if "text" in input: return f"You entered: {input.text()}" else: return "Submit some input to see it here." ``` **Best practices**: - Keep examples simple and focused - Demonstrate the primary use case - Show server-side value handling patterns - Include any important parameter variations - Re-use examples from bslib where possible - Always include an Express version unless there's a strong reason not to - A human reviewer should test the examples locally **Checklist:** - [ ] Created `shiny/api-examples/[component]/app-core.py` - [ ] Created `shiny/api-examples/[component]/app-express.py` (unless not warranted) - [ ] Tested examples locally ### Step 2.5: Add Playwright controller (if input component) If the component is an input component, create a Playwright controller for end-to-end testing. **Location**: `shiny/playwright/controller/_input_fields.py` (or create new file if needed) **Implementation**: 1. Create a new class inheriting from appropriate mixins 2. Implement required methods: `__init__`, `set`, interaction methods 3. Add expectation methods for testing (e.g., `expect_value`, `expect_placeholder`) 4. Follow existing patterns for locator initialization **Example pattern**: ```python class InputSubmitTextarea( _SetTextM, WidthContainerStyleM, _ExpectTextInputValueM, _ExpectPlaceholderAttrM, _ExpectRowsAttrM, UiWithLabel, ): """Controller for :func:`shiny.ui.input_submit_textarea`.""" loc_button: Locator def __init__(self, page: Page, id: str) -> None: super().__init__( page, id=id, loc=f"textarea#{id}.form-control", ) self.loc_button = self.loc_container.locator(".bslib-submit-textarea-btn") def set(self, value: str, *, submit: bool = False, timeout: Timeout = None) -> None: # Implementation pass ``` **Don't forget to export**: Update `shiny/playwright/controller/__init__.py` to export the new controller class. **Checklist:** - [ ] Created Playwright controller class - [ ] Implemented core interaction methods - [ ] Implemented expectation methods for testing - [ ] Exported from `shiny/playwright/controller/__init__.py` - [ ] Added to `__all__` in the same file ## Phase 3: Testing and Documentation ### Step 3.1: Port unit tests from bslib Port the relevant unit tests from bslib's testthat tests to pytest. **Translation patterns**: - R `test_that()` → Python `def test_[name]():` - R `expect_snapshot()` → Python snapshot testing (if applicable) - R `expect_error()` → Python `with pytest.raises()` **Focus on**: - Parameter validation - Error messages - Edge cases - HTML structure (snapshot tests if helpful) **Note**: Python has better tooling for end-to-end tests, so unit tests here focus on correctness of the Python API and generated markup. **Checklist:** - [ ] Ported relevant unit tests - [ ] Tests pass locally (`pytest tests/`) ### Step 3.2: Create end-to-end Playwright tests Create comprehensive end-to-end tests using Playwright. It is likely that these tests do not exist in the R package, so you will need to use your knowledge of the component and its documented behavior to create them. Collaborate with the human reviewer if there are any uncertainties around expected behavior. **Location**: `tests/playwright/shiny/inputs/[component_name]/` **Files to create**: - `app.py` - Test application with multiple variants - `test_[component_name].py` - Playwright test cases **Test coverage should include**: - Initial state verification - User interactions (typing, clicking, keyboard shortcuts) - Server updates (via `update_*()` functions) - Edge cases (empty values, disabled states, etc.) - Multiple submissions and state changes **Example test structure**: ```python def test_input_submit_textarea_initial_state(page: Page, local_app: ShinyAppProc): page.goto(local_app.url) basic = controller.InputSubmitTextarea(page, "basic") basic.expect_label("Enter text") basic.expect_placeholder("Type something here...") basic.expect_value("Initial value") value_output = controller.OutputCode(page, "basic_value") value_output.expect_value("No value submitted yet") ``` **Best practices**: - Use the Playwright controller for interactions - Test both user interactions and programmatic updates - Use clear, descriptive test names - Verify both input state and output state - Output state can be checked by rendering text outputs in the test app **Checklist:** - [ ] Created test app with multiple component variations - [ ] Created comprehensive test cases - [ ] Tests pass locally (`pytest tests/playwright/`) - [ ] Covered key interaction patterns and edge cases ### Step 3.3: Update CHANGELOG Add an entry to `CHANGELOG.md` under the `[UNRELEASED]` section. **Format**: ```markdown ### New features * Added `input_new_feature()` [description of function, new features and salient details in 1-3 sentences]. (#[PR_NUMBER]) ``` **Checklist:** - [ ] Added CHANGELOG entry under appropriate section - [ ] Entry includes clear description and PR reference ### Step 3.4: Run quality checks Run the various quality checks to ensure your code meets project standards. **Recommended workflow**: 1. `make formatting` - Fix formatting and linting issues automatically 2. `make check-types` - Run type checking (pyright) 3. `make check-tests` - Run the test suite 4. `make playwright` - Run Playwright end-to-end tests (see `Makefile` for alternate make commands to run subsets of tests) 5. `make check` - Comprehensive checks (slower, runs everything) **Common issues**: - Type errors: Ensure proper type hints on all functions - Format errors: Run `make format` to auto-fix - Missing imports: Ensure all new modules are properly imported - Test failures: Debug and fix any failing tests **Checklist:** - [ ] `make format` applied successfully - [ ] `make check-types` passes - [ ] `make check-tests` passes (or at least your new tests pass) - [ ] `make playwright` passes (or at least new end-to-end tests pass) - [ ] Addressed any other linting/quality issues ### Step 3.5: Update API reference configuration Add the new component functions to the quartodoc YAML configuration files so they appear in the generated documentation. **Files to update**: - `docs/_quartodoc-core.yml` - Add component function and update function (if applicable) - `docs/_quartodoc-express.yml` - Add express versions (if applicable) - `docs/_quartodoc-testing.yml` - Add Playwright controller (if applicable) **Where to add entries**: - Find the appropriate section (e.g., "Inputs" for input components, "Update" for update functions) - Add entries in alphabetical order within the section - Use the full module path (e.g., `ui.input_submit_textarea`) **Example additions**: ```yaml # In _quartodoc-core.yml under inputs section: - ui.input_submit_textarea # In _quartodoc-core.yml under update section: - ui.update_submit_textarea # In _quartodoc-express.yml: - express.ui.input_submit_textarea - express.ui.update_submit_textarea # In _quartodoc-testing.yml: - playwright.controller.InputSubmitTextarea ``` **Checklist:** - [ ] Added entries to `docs/_quartodoc-core.yml` - [ ] Added entries to `docs/_quartodoc-express.yml` (if applicable) - [ ] Added entries to `docs/_quartodoc-testing.yml` (if applicable) - [ ] Verified alphabetical ordering within sections ### Step 3.6: Build documentation (final step) Only build documentation at the very end, as it's slow and resource-intensive. **Command**: `make docs` **This will**: - Generate API reference documentation from the quartodoc YAML files - Build example applications - Create the documentation website **Note**: You don't need to run this frequently during development. It's primarily for final verification before PR submission. A human reviewer can handle this entire step, just remind them to run it before merging. **Checklist:** - [ ] `make docs` completes successfully - [ ] Reviewed the generated documentation for your component - [ ] API reference appears correctly in the appropriate sections - [ ] Examples are properly linked ## Phase 4: Final Review and Submission ### Step 4.1: Self-review the changes Before submitting, review all changes: **Python code**: - [ ] Functions have comprehensive docstrings - [ ] Parameter types and defaults match or improve on bslib's API - [ ] Code follows existing project patterns - [ ] Error messages are clear and helpful **Tests**: - [ ] Unit tests cover parameter validation and edge cases - [ ] End-to-end tests cover user workflows - [ ] All tests pass locally **Styles**: - [ ] SCSS is imported in all theme presets - [ ] Styles match the bslib implementation **Documentation**: - [ ] CHANGELOG is updated - [ ] API examples are clear and working - [ ] Docstrings are complete ### Step 4.2: Create the PR Create a pull request with: 1. **Title**: `feat: Add [component_name]` or similar 2. **Description**: - Link to the source bslib PR - Brief description of the component - Any implementation notes or decisions - Example usage **PR description template**: ```markdown Related https://github.com/rstudio/bslib/pull/[NUMBER] Adds `[component_name]`, a new [input/output/UI] component that [brief description]. ## Example Here is a hello world example: [code block with example] ## Implementation notes [Any important notes about the implementation, decisions made, etc. or open questions for reviewers.] ``` **Checklist:** - [ ] PR title follows project conventions - [ ] PR description links to bslib source - [ ] PR description includes example usage - [ ] All quality checks pass in CI --- ## Quick Reference Checklist Use this high-level checklist to track your progress through the porting process: ### Preparation - [ ] Located and reviewed the bslib PR - [ ] Identified all source files (R, TypeScript, SCSS, tests) - [ ] Understood the component's API and behavior ### Implementation - [ ] Created Python implementation in `shiny/ui/_[name].py` - [ ] Exported from `shiny/ui/__init__.py` (and express if applicable) - [ ] Ran `make upgrade-html-deps` to vendor assets from bslib (SCSS, CSS, JavaScript) - [ ] Created API examples (core and express) - [ ] Created Playwright controller (if input component) ### Testing - [ ] Ported unit tests from bslib - [ ] Created comprehensive Playwright tests - [ ] Updated CHANGELOG ### Documentation - [ ] Updated quartodoc YAML files to include new functions - [ ] Ran `make format` - [ ] Passed `make check-types` - [ ] Passed `make check-tests` - [ ] Built docs with `make docs` ### Submission - [ ] Self-reviewed all changes - [ ] Created PR with proper description - [ ] All CI checks passing --- ## Common Patterns and Tips ### Handling component dependencies If the component depends on other components: - Use `components_dependencies()` for bslib components (automatic) - For other dependencies, add them explicitly in the component function ### Testing with different themes The component should work with all Bootstrap themes. The `make upgrade-html-deps` process ensures SCSS is properly imported across all theme presets, but you may want to spot-check a few themes manually during development. ### Parameter naming conventions Python style guide preferences: - Use `snake_case` for parameter names - Match bslib's parameter names when possible - Use `Optional[T]` for user-facing parameters that can be `None` - Use `Literal` rather than `Enum` for parameters with specific allowed values ### Working with htmltools Key patterns: - `Tag` objects are mutable by default, use `copy.copy()` if you need to modify - Use `.add_class()`, `.add_style()`, `.attrs` to add CSS classes, styles, and HTML attributes - Create new tags with `ui.tags` and add attributes and children directly, e.g. `ui.tags.div(class_="fw-bold", ...)` - Use `css()` helper for inline styles - Tag children can be strings, Tags, TagLists, or None --- ## Troubleshooting ### "Tests fail with 'element not found'" - Verify Playwright selectors match the actual HTML structure - Check that the component is properly rendered before interaction - Use `page.wait_for_selector()` if there are timing issues ### "SCSS not taking effect" - Check that `make upgrade-html-deps` was run to vendor and compile SCSS/CSS - Verify the SCSS file exists in `shiny/www/shared/sass/bslib/components/scss/` - Verify the imports were added to preset `_04_rules.scss` files (should be automatic) - Clear browser cache when testing --- ## Additional Resources - **bslib repository**: https://github.com/rstudio/bslib - **py-shiny repository**: https://github.com/posit-dev/py-shiny - **Example PR (bslib)**: https://github.com/rstudio/bslib/pull/1204 - **Example PR (py-shiny)**: https://github.com/posit-dev/py-shiny/pull/2099 For questions or clarifications, consult with the Shiny team or reference previous component ports for patterns.