├── .changeset ├── README.md └── config.json ├── .eslintrc.json ├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .ladle ├── config.mjs ├── head.html └── vite-plugin-node.js ├── .lintstagedrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── examples ├── __tests__ │ ├── basic.spec.ts │ ├── custom-prop.spec.ts │ ├── live-code-only.spec.ts │ ├── modal.spec.ts │ ├── state-hook.spec.ts │ ├── theming.spec.ts │ ├── typescript.spec.ts │ └── view.spec.ts ├── advanced.stories.tsx ├── basic.tsx ├── const.ts ├── custom-prop.tsx ├── layout │ └── index.tsx ├── live-code-only.tsx ├── modal.tsx ├── showcase-components │ ├── button.tsx │ ├── input.tsx │ ├── modal.tsx │ ├── rating.tsx │ └── theme-provider.tsx ├── state-hook.tsx ├── test.stories.tsx ├── theming.tsx ├── typescript.tsx ├── use-view.stories.tsx ├── view.stories.tsx └── view.tsx ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── src ├── __tests__ │ ├── ast.test.ts │ ├── code-generator.test.ts │ └── utils.test.ts ├── actions.ts ├── ast.ts ├── code-generator.ts ├── const.ts ├── index.ts ├── light-theme.ts ├── reducer.ts ├── snippets │ ├── __tests__ │ │ └── vscode-snippet.test.ts │ └── vscode-snippet.ts ├── types.ts ├── ui │ ├── action-buttons.tsx │ ├── compiler.tsx │ ├── editor.tsx │ ├── error.tsx │ ├── knob.tsx │ ├── knobs.tsx │ ├── placeholder.tsx │ └── view.tsx ├── use-view.ts └── utils.ts ├── tsconfig.es.json ├── tsconfig.json ├── tsconfig.lib.json └── vite.config.js /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "uber/react-view" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:react/recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:prettier/recommended" 7 | ], 8 | "rules": { 9 | "@typescript-eslint/no-explicit-any": "off", 10 | "@typescript-eslint/ban-types": "off", 11 | "@typescript-eslint/ban-ts-comment": "off", 12 | "@typescript-eslint/explicit-module-boundary-types": "off", 13 | "react/prop-types": "off", 14 | "react/react-in-jsx-scope": "off" 15 | }, 16 | "settings": { 17 | "react": { 18 | "version": "detect" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Learn how to add code owners here: 2 | # https://help.github.com/en/articles/about-code-owners 3 | * @chasestarr @tajo 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test 12 | runs-on: ${{ matrix.os }} 13 | timeout-minutes: 15 14 | env: 15 | CI: true 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | node-version: [18.x, 20.x] 20 | steps: 21 | - name: Check out code 22 | uses: actions/checkout@v3 23 | with: 24 | fetch-depth: 2 25 | 26 | - uses: pnpm/action-setup@v2.2.4 27 | with: 28 | version: 8.7.1 29 | 30 | - name: Setup Node.js environment 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | cache: "pnpm" 35 | 36 | - name: Install dependencies 37 | run: pnpm install --frozen-lockfile 38 | 39 | - name: Install playwright 40 | run: pnpm exec playwright install --with-deps 41 | 42 | - name: Eslint 43 | run: pnpm lint 44 | 45 | - name: Typescript 46 | run: pnpm typecheck 47 | 48 | - name: Build 49 | run: pnpm build 50 | 51 | - name: Unit tests 52 | run: pnpm test 53 | 54 | - name: E2e tests 55 | run: pnpm test:e2e 56 | 57 | - uses: actions/upload-artifact@v3 58 | if: always() 59 | with: 60 | name: playwright-report 61 | path: playwright-report/ 62 | retention-days: 30 63 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | id-token: write 18 | steps: 19 | - name: Checkout Repo 20 | uses: actions/checkout@v3 21 | with: 22 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 23 | fetch-depth: 0 24 | 25 | - uses: pnpm/action-setup@v2.2.4 26 | with: 27 | version: 8.7.1 28 | 29 | - name: Setup Node.js environment 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: 20.5 33 | cache: "pnpm" 34 | 35 | - name: Install dependencies 36 | run: pnpm install --frozen-lockfile 37 | 38 | - name: Build 39 | run: pnpm build 40 | 41 | - name: Creating .npmrc 42 | run: | 43 | cat << EOF > "$HOME/.npmrc" 44 | email=vojtech+ladle@miksu.cz 45 | //registry.npmjs.org/:_authToken=$NPM_TOKEN 46 | EOF 47 | env: 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | 50 | - name: Create Release Pull Request or Publish to npm 51 | id: changesets 52 | uses: changesets/action@v1 53 | with: 54 | publish: pnpm changeset publish 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GIT_DEPLOY_KEY }} 57 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.log 4 | .vscode 5 | .idea 6 | dist 7 | build 8 | compiled 9 | docs 10 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm exec lint-staged 5 | -------------------------------------------------------------------------------- /.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('@ladle/react').UserConfig} */ 2 | export default { 3 | stories: "examples/**/*.stories.{js,jsx,ts,tsx,mdx}", 4 | }; 5 | -------------------------------------------------------------------------------- /.ladle/head.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.ladle/vite-plugin-node.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | import { builtinModules } from "node:module"; 4 | import nodeLibsBrowser from "node-libs-browser"; 5 | 6 | function NodeBuiltinsPolyfillPlugin() { 7 | return { 8 | name: "vite:node-builtins-polyfill", 9 | config() { 10 | const aliasEntries = []; 11 | for (let moduleName of builtinModules) { 12 | const polyfillPath = nodeLibsBrowser[moduleName]; 13 | if (polyfillPath) { 14 | aliasEntries.push({ 15 | // eslint-disable-next-line 16 | find: new RegExp(`^${moduleName}\/?$`), // handle "string_decoder/" import 17 | replacement: polyfillPath, 18 | }); 19 | } 20 | } 21 | 22 | return { 23 | resolve: { 24 | alias: aliasEntries, 25 | }, 26 | }; 27 | }, 28 | }; 29 | } 30 | 31 | export default NodeBuiltinsPolyfillPlugin; 32 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,tsx,css,scss,postcss,md,json}": [ 3 | "prettier --write --plugin-search-dir=.", 4 | "prettier --check --plugin-search-dir=." 5 | ], 6 | "*.{js,ts,tsx}": "eslint" 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | pnpm-workspace.yaml 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # react-view 2 | 3 | ## 3.0.1 4 | 5 | ### Patch Changes 6 | 7 | - [#105](https://github.com/uber/react-view/pull/105) [`9a6fd36`](https://github.com/uber/react-view/commit/9a6fd361380e6540edffe1e4068084a8b87c269a) Thanks [@taifen](https://github.com/taifen)! - Updated react-tiny-popover to use React 17 and 18 8 | 9 | ## 3.0.0 10 | 11 | ### Major Changes 12 | 13 | - [#102](https://github.com/uber/react-view/pull/102) [`89350f4`](https://github.com/uber/react-view/commit/89350f40c745aca38ef78e58efd8c4b6191b7788) Thanks [@tajo](https://github.com/tajo)! - No breaking changes really, just bumping all dependencies and modernizing the tooling. Live code that has export default gets wrapped by an additional IIEF, so you can have other variables defined at the root scope. 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at vojtech@uber.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to React View 2 | 3 | 1. Clone the repo locally and run yarn to install dependencies from npm. We use [volta](https://volta.sh/). 4 | 5 | ```sh 6 | git clone https://github.com/uber/react-view 7 | cd react-view 8 | pnpm install 9 | ``` 10 | 11 | 2. You can test your changes inside of the Ladle dev server by running: 12 | 13 | ```sh 14 | pnpm ladle serve 15 | ``` 16 | 17 | 3. When done, run all unit tests, e2e tests, typescript check and eslint via: 18 | 19 | ```sh 20 | pnpm typecheck 21 | pnpm lint 22 | pnpm test 23 | 24 | pnpm exec playwright install 25 | pnpm test:e2e:dev 26 | ``` 27 | 28 | All features and bug fixes should be covered by unit or e2e tests. 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Uber Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /examples/__tests__/basic.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import { urls } from "../const"; 8 | 9 | import { test, expect } from "@playwright/test"; 10 | 11 | test.describe("Basic knobs", () => { 12 | test.beforeEach(async ({ page }) => { 13 | await page.goto(urls.basic); 14 | await page.waitForSelector("[data-storyloaded]"); 15 | await page.click('[data-testid="rv-reset"]'); 16 | }); 17 | test("should select size compact, update component and input", async ({ 18 | page, 19 | }) => { 20 | const codeOutput = `import * as React from "react"; 21 | import { Button, SIZE } from "your-button-component"; 22 | 23 | export default () => { 24 | return ( 25 | 31 | ); 32 | }`; 33 | 34 | await page.click("#size_compact"); 35 | const fontSize = await page.$eval( 36 | "#example-btn", 37 | (e) => (e as any).style["font-size"], 38 | ); 39 | expect(fontSize).toBe("14px"); 40 | const editorTextarea = await page.$('[data-testid="rv-editor"] textarea'); 41 | const text = await page.evaluate((el: any) => el.value, editorTextarea); 42 | expect(text).toBe(codeOutput); 43 | }); 44 | 45 | test("should check disabled, update component and input", async ({ 46 | page, 47 | }) => { 48 | const codeOutput = `import * as React from "react"; 49 | import { Button } from "your-button-component"; 50 | 51 | export default () => { 52 | return ( 53 | 56 | ); 57 | }`; 58 | await page.click("#disabled"); 59 | const isDisabled = await page.$eval( 60 | "#example-btn", 61 | (e) => (e as any).disabled, 62 | ); 63 | expect(isDisabled).toBeTruthy(); 64 | const editorTextarea = await page.$('[data-testid="rv-editor"] textarea'); 65 | const text = await page.evaluate((el: any) => el.value, editorTextarea); 66 | expect(text).toBe(codeOutput); 67 | }); 68 | 69 | test("should change the children knob, update component and code", async ({ 70 | page, 71 | }) => { 72 | const childrenPropValue = "e2etest"; 73 | const codeOutput = `import * as React from "react"; 74 | import { Button } from "your-button-component"; 75 | 76 | export default () => { 77 | return ( 78 | 81 | ); 82 | }`; 83 | const textareaSelector = '[data-testid="rv-knob-children"] textarea'; 84 | await page.waitForSelector(textareaSelector); 85 | await page.fill(textareaSelector, childrenPropValue); 86 | await page.waitForTimeout(300); // waiting for debounce 87 | const exampleBtn = await page.$("#example-btn"); 88 | await expect(exampleBtn!.textContent()).resolves.toBe(childrenPropValue); 89 | const editorTextarea = await page.$('[data-testid="rv-editor"] textarea'); 90 | const text = await page.evaluate((el: any) => el.value, editorTextarea); 91 | expect(text).toBe(codeOutput); 92 | }); 93 | 94 | test("should change the onClick knob, update component and code", async ({ 95 | page, 96 | }) => { 97 | const onClickPropValue = `() => {document.querySelector('h1').innerText = "foo"}`; 98 | const codeOutput = `import * as React from "react"; 99 | import { Button } from "your-button-component"; 100 | 101 | export default () => { 102 | return ( 103 | 110 | ); 111 | }`; 112 | await page 113 | .locator('[data-testid="rv-knob-onClick"] textarea') 114 | .fill(onClickPropValue); 115 | await page.waitForTimeout(300); // waiting for debounce 116 | await page.click("#example-btn"); 117 | const text = await page.evaluate(() => { 118 | const h1 = document.querySelector("h1"); 119 | return h1 ? h1.innerText : ""; 120 | }); 121 | expect(text).toBe("foo"); 122 | const editorTextarea = await page.$('[data-testid="rv-editor"] textarea'); 123 | const editorText = await page.evaluate( 124 | (el: any) => el.value, 125 | editorTextarea, 126 | ); 127 | expect(editorText).toBe(codeOutput); 128 | }); 129 | }); 130 | 131 | test.describe("Basic actions", () => { 132 | test.beforeEach(async ({ page }) => { 133 | await page.goto(urls.basic); 134 | await page.waitForSelector("[data-storyloaded]"); 135 | await page.click('[data-testid="rv-reset"]'); 136 | }); 137 | 138 | test("should format the code snippet", async ({ page }) => { 139 | const formattedCode = `import * as React from "react"; 140 | import { Button } from "your-button-component"; 141 | 142 | export default () => { 143 | return ( 144 | 145 | ); 146 | }`; 147 | const messyCode = ` import * as React from "react"; 148 | import { Button } from "your-button-component"; 149 | 150 | export default () => { 151 | return ( 152 | 154 | ); 155 | }`; 156 | await page.locator('[data-testid="rv-editor"] textarea').fill(messyCode); 157 | // for (let i = 0; i < 232; i++) { 158 | // await page.keyboard.press("Delete"); 159 | // } 160 | // await page.keyboard.type(messyCode); 161 | await page.waitForTimeout(300); // waiting for debounce 162 | await page.click('[data-testid="rv-format"]'); 163 | const editorTextarea = await page.$('[data-testid="rv-editor"] textarea'); 164 | const text = await page.evaluate((el: any) => el.value, editorTextarea); 165 | expect(text).toBe(formattedCode); 166 | }); 167 | }); 168 | 169 | test.describe("Basic editor", () => { 170 | test.beforeEach(async ({ page }) => { 171 | await page.goto(urls.basic); 172 | await page.waitForSelector("[data-storyloaded]"); 173 | await page.click('[data-testid="rv-reset"]'); 174 | }); 175 | 176 | test("should edit the code and update the knob and component", async ({ 177 | page, 178 | }) => { 179 | const newCode = `import * as React from "react"; 180 | import { Button } from "your-button-component"; 181 | 182 | export default () => { 183 | return ( 184 | 185 | ); 186 | }`; 187 | await page.locator('[data-testid="rv-editor"] textarea').fill(newCode); 188 | await page.waitForTimeout(300); // waiting for debounce 189 | const isButtonDisabled = await page.$eval( 190 | "#example-btn", 191 | (e) => (e as any).disabled, 192 | ); 193 | expect(isButtonDisabled).toBeTruthy(); 194 | const isDisabledChecked = await page.$eval( 195 | "#disabled", 196 | (el) => (el as any).checked, 197 | ); 198 | expect(isDisabledChecked).toBeTruthy(); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /examples/__tests__/custom-prop.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { test, expect } from "@playwright/test"; 9 | import { urls } from "../const"; 10 | 11 | test.describe("Basic knobs", () => { 12 | test.beforeEach(async ({ page }) => { 13 | await page.goto(urls.customProps); 14 | await page.waitForSelector("[data-storyloaded]"); 15 | await page.click('[data-testid="rv-reset"]'); 16 | }); 17 | 18 | test("should output initial code", async ({ page }) => { 19 | const codeOutput = `import * as React from "react"; 20 | import { Rating } from "your-rating-component"; 21 | 22 | export default () => { 23 | const [value, setValue] = React.useState(3); 24 | return ( 25 | setValue(value)} 28 | /> 29 | ); 30 | }`; 31 | const editorTextarea = await page.$('[data-testid="rv-editor"] textarea'); 32 | const text = await page.evaluate((el: any) => el.value, editorTextarea); 33 | expect(text).toBe(codeOutput); 34 | }); 35 | 36 | test("should select 4 hearts and update the slider and code", async ({ 37 | page, 38 | }) => { 39 | const codeOutput = `import * as React from "react"; 40 | import { Rating } from "your-rating-component"; 41 | 42 | export default () => { 43 | const [value, setValue] = React.useState(4); 44 | return ( 45 | setValue(value)} 48 | /> 49 | ); 50 | }`; 51 | await page.click("#heart-4"); 52 | await page.waitForTimeout(300); // debounce time 53 | const inputValue = await page.$eval("input", (e) => (e as any).value); 54 | expect(inputValue).toBe("4"); 55 | const editorTextarea = await page.$('[data-testid="rv-editor"] textarea'); 56 | const text = await page.evaluate((el: any) => el.value, editorTextarea); 57 | expect(text).toBe(codeOutput); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /examples/__tests__/live-code-only.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import { test, expect } from "@playwright/test"; 8 | import { urls } from "../const"; 9 | 10 | test.describe("Live Code Only", () => { 11 | test("should compile the code and render component", async ({ page }) => { 12 | await page.goto(urls.liveCodeOnly); 13 | await page.waitForSelector("[data-storyloaded]"); 14 | const inputCode = ``; 15 | await page.locator("textarea").first().fill(inputCode); 16 | await page.waitForTimeout(300); // waiting for debounce 17 | const button = await page.$("button"); 18 | expect(await button!.textContent()).toBe("Hey"); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /examples/__tests__/modal.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import { test, expect } from "@playwright/test"; 8 | import { urls } from "../const"; 9 | 10 | test.describe("Modal", () => { 11 | test("open, close and open the modal", async ({ page }) => { 12 | await page.goto(urls.modal); 13 | await page.waitForSelector("[data-storyloaded]"); 14 | await (await page.$("#show"))?.click(); 15 | await page.waitForTimeout(300); // waiting for debounce 16 | expect((await page.$("#close-modal")) !== null).toBeTruthy(); 17 | (await page.$("#close-modal"))?.click(); 18 | await page.waitForTimeout(500); // waiting for debounce 19 | expect((await page.$("#close-modal")) !== null).toBeFalsy(); 20 | await (await page.$("#show"))?.click(); 21 | await page.waitForTimeout(300); // waiting for debounce 22 | expect((await page.$("#close-modal")) !== null).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/__tests__/state-hook.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import { test, expect } from "@playwright/test"; 8 | import { urls } from "../const"; 9 | 10 | test.describe("State hook", () => { 11 | test.beforeEach(async ({ page }) => { 12 | await page.goto(urls.stateHook); 13 | await page.waitForSelector("[data-storyloaded]"); 14 | await page.click('[data-testid="rv-reset"]'); 15 | }); 16 | 17 | test("should update the input and sync the knob and code", async ({ 18 | page, 19 | }) => { 20 | const codeOutput = `import * as React from "react"; 21 | import { Input } from "your-input-component"; 22 | 23 | export default () => { 24 | const [value, setValue] = React.useState("HelloFoo"); 25 | return ( 26 | setValue(e.target.value)} 29 | /> 30 | ); 31 | }`; 32 | 33 | await page.locator("#example-input").fill("HelloFoo"); 34 | await page.waitForTimeout(300); // waiting for debounce 35 | 36 | const valueKnob = await page.$('[data-testid="rv-knob-value"] textarea'); 37 | const valueText = await page.evaluate((el: any) => el.value, valueKnob); 38 | expect(valueText).toBe("HelloFoo"); 39 | 40 | const editorTextarea = await page.$('[data-testid="rv-editor"] textarea'); 41 | const text = await page.evaluate((el: any) => el.value, editorTextarea); 42 | expect(text).toBe(codeOutput); 43 | }); 44 | 45 | test("should update the value knob and sync with component and code", async ({ 46 | page, 47 | }) => { 48 | const codeOutput = `import * as React from "react"; 49 | import { Input } from "your-input-component"; 50 | 51 | export default () => { 52 | const [value, setValue] = React.useState("HelloFoo"); 53 | return ( 54 | setValue(e.target.value)} 57 | /> 58 | ); 59 | }`; 60 | 61 | await page 62 | .locator('[data-testid="rv-knob-value"] textarea') 63 | .fill("HelloFoo"); 64 | await page.waitForTimeout(300); // waiting for debounce 65 | 66 | const input = await page.$("#example-input"); 67 | const inputValue = await page.evaluate((el: any) => el.value, input); 68 | expect(inputValue).toBe("HelloFoo"); 69 | 70 | const editorTextarea = await page.$('[data-testid="rv-editor"] textarea'); 71 | const text = await page.evaluate((el: any) => el.value, editorTextarea); 72 | expect(text).toBe(codeOutput); 73 | }); 74 | 75 | test("should respect the default boolean value, uncheck editable and update component and input", async ({ 76 | page, 77 | }) => { 78 | const initialCode = `import * as React from "react"; 79 | import { Input } from "your-input-component"; 80 | 81 | export default () => { 82 | const [value, setValue] = React.useState("Hello"); 83 | return ( 84 | setValue(e.target.value)} 87 | /> 88 | ); 89 | }`; 90 | const resultCode = `import * as React from "react"; 91 | import { Input } from "your-input-component"; 92 | 93 | export default () => { 94 | const [value, setValue] = React.useState("Hello"); 95 | return ( 96 | setValue(e.target.value)} 99 | editable={false} 100 | /> 101 | ); 102 | }`; 103 | const initialEditor = await page.evaluate( 104 | (el: any) => el.value, 105 | await page.$('[data-testid="rv-editor"] textarea'), 106 | ); 107 | expect(initialEditor).toBe(initialCode); 108 | 109 | await page.click("#editable"); 110 | const isDisabled = await page.$eval( 111 | "#example-input", 112 | (e: any) => (e as any).disabled, 113 | ); 114 | expect(isDisabled).toBeTruthy(); 115 | const resultEditor = await page.evaluate( 116 | (el: any) => el.value, 117 | await page.$('[data-testid="rv-editor"] textarea'), 118 | ); 119 | expect(resultEditor).toBe(resultCode); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /examples/__tests__/theming.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import { test, expect } from "@playwright/test"; 8 | import { urls } from "../const"; 9 | 10 | const initialCode = `import * as React from "react"; 11 | import { Button } from "your-button-component"; 12 | 13 | export default () => { 14 | return ; 15 | }`; 16 | 17 | test.describe("Theming", () => { 18 | test.beforeEach(async ({ page }) => { 19 | await page.goto(urls.theming); 20 | await page.waitForSelector("[data-storyloaded]"); 21 | }); 22 | 23 | test("should change the theme, add provider and update the component", async ({ 24 | page, 25 | }) => { 26 | await page.click('[data-testid="rv-reset"]'); 27 | const hotpinkCode = `import * as React from "react"; 28 | import { Button } from "your-button-component"; 29 | import { ThemeProvider } from "your-component-library"; 30 | 31 | export default () => { 32 | return ( 33 | 38 | 39 | 40 | ); 41 | }`; 42 | const initialEditor = await page.evaluate( 43 | (el: any) => el.value, 44 | await page.$('[data-testid="rv-editor"] textarea'), 45 | ); 46 | expect(initialEditor).toBe(initialCode); 47 | await page.locator('[data-testid="background"] textarea').fill("hotpink"); 48 | await page.waitForTimeout(600); // waiting for debounce 49 | 50 | const exampleBtn = await page.$("#example-btn"); 51 | expect(await exampleBtn!.evaluate((e: any) => e.style["background"])).toBe( 52 | "hotpink", 53 | ); 54 | const editorTextarea = await page.$('[data-testid="rv-editor"] textarea'); 55 | const text = await page.evaluate((el: any) => el.value, editorTextarea); 56 | expect(text).toBe(hotpinkCode); 57 | }); 58 | 59 | test("should reset provider values and get the initial state of code and component", async ({ 60 | page, 61 | }) => { 62 | await page.click('[data-testid="rv-reset"]'); 63 | const editor = await page.evaluate( 64 | (el: any) => el.value, 65 | await page.$('[data-testid="rv-editor"] textarea'), 66 | ); 67 | expect(editor).toBe(initialCode); 68 | const background = await page.$eval( 69 | "#example-btn", 70 | (e) => (e as any).style["background"], 71 | ); 72 | expect(background).toBe("rgb(39, 110, 241)"); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /examples/__tests__/typescript.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import { test, expect } from "@playwright/test"; 8 | import { urls } from "../const"; 9 | 10 | test.describe("Typescript", () => { 11 | test("should compile the code and render component", async ({ page }) => { 12 | await page.goto(urls.liveCodeOnly); 13 | await page.waitForSelector("[data-storyloaded]"); 14 | const inputCode = `() => { 15 | const num1: number = 13; 16 | const num2: number = 4; 17 | return num1 * num2; 18 | }`; 19 | 20 | await page.locator("textarea").first().fill(inputCode); 21 | await page.waitForTimeout(300); // waiting for debounce 22 | expect(await page.textContent("body")).toContain("52"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/__tests__/view.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import { test, expect } from "@playwright/test"; 8 | import { urls } from "../const"; 9 | 10 | test.describe("View", () => { 11 | test.beforeEach(async ({ page }) => { 12 | await page.goto(urls.view); 13 | await page.waitForSelector("[data-storyloaded]"); 14 | }); 15 | 16 | test('should render the button with "Hello" label', async ({ page }) => { 17 | expect(await page.textContent("button")).toContain("Hello"); 18 | }); 19 | 20 | test("should generate the correct code snippet", async ({ page }) => { 21 | const codeOutput = `import * as React from "react"; 22 | import { Button } from "your-button-component"; 23 | 24 | export default () => { 25 | return ( 26 | 27 | ); 28 | }`; 29 | const text = await page.evaluate( 30 | (el: any) => el.value, 31 | await page.$('[data-testid="rv-editor"] textarea'), 32 | ); 33 | expect(text).toBe(codeOutput); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /examples/advanced.stories.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | import Theming from "./theming"; 9 | import CustomProp from "./custom-prop"; 10 | 11 | export default { 12 | title: "Advanced", 13 | }; 14 | 15 | export const customProp = () => { 16 | return ; 17 | }; 18 | 19 | export const theming = () => { 20 | return ; 21 | }; 22 | -------------------------------------------------------------------------------- /examples/basic.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | import { Layout, H1, H2, P, Code, Inline } from "./layout/"; 9 | import { Button, SIZE } from "./showcase-components/button"; 10 | 11 | import { 12 | useView, 13 | Compiler, 14 | Knobs, 15 | Editor, 16 | Error, 17 | ActionButtons, 18 | Placeholder, 19 | PropTypes, 20 | } from "../src"; 21 | 22 | const Basic = () => { 23 | const params = useView({ 24 | componentName: "Button", 25 | props: { 26 | children: { 27 | value: "Hello", 28 | type: PropTypes.ReactNode, 29 | description: `Visible label.`, 30 | }, 31 | size: { 32 | value: "SIZE.default", 33 | defaultValue: "SIZE.default", 34 | options: SIZE, 35 | type: PropTypes.Enum, 36 | description: "Defines the size of the button.", 37 | imports: { 38 | "your-button-component": { 39 | named: ["SIZE"], 40 | }, 41 | }, 42 | }, 43 | onClick: { 44 | value: '() => alert("click")', 45 | type: PropTypes.Function, 46 | description: `Function called when button is clicked.`, 47 | }, 48 | disabled: { 49 | value: false, 50 | type: PropTypes.Boolean, 51 | description: "Indicates that the button is disabled", 52 | }, 53 | }, 54 | scope: { 55 | Button, 56 | SIZE, 57 | }, 58 | imports: { 59 | "your-button-component": { 60 | named: ["Button"], 61 | }, 62 | }, 63 | }); 64 | 65 | return ( 66 | 67 |

Basic example of useView

68 |

69 | This is our main{" "} 70 | hook based API. 71 | React View strictly separates the UI components from everything else so 72 | you can completely customize every aspect of the playground. If you want 73 | to start as quickly as possible, try the{" "} 74 | View component instead. 75 |

76 | 81 | 82 | 83 | 84 | 85 | 86 |

87 | This is a basic example that demonstrates all basic features of React 88 | View. At the top, you can see the rendered component, followed by 89 | the middle section with knobs that lets you explore all component 90 | props, the edittable code snippet and finally some{" "} 91 | action buttons. 92 |

93 |

Usage

94 | 95 | {`import * as React from 'react'; 96 | import {Button, SIZE} from 'your-button-component'; 97 | 98 | import { 99 | useView, 100 | Compiler, 101 | Knobs, 102 | Editor, 103 | Error, 104 | ActionButtons, 105 | Placeholder, 106 | PropTypes, 107 | } from 'react-view'; 108 | 109 | const Basic = () => { 110 | const params = useView({ 111 | componentName: 'Button', 112 | props: { 113 | children: { 114 | value: 'Hello', 115 | type: PropTypes.ReactNode, 116 | description: 'Visible label.', 117 | }, 118 | size: { 119 | value: 'SIZE.default', 120 | defaultValue: 'SIZE.default', 121 | options: SIZE, 122 | type: PropTypes.Enum, 123 | description: 'Defines the size of the button.', 124 | imports: { 125 | 'your-button-component': { 126 | named: ['SIZE'], 127 | }, 128 | }, 129 | }, 130 | onClick: { 131 | value: '() => alert("click")', 132 | type: PropTypes.Function, 133 | description: 'Function called when button is clicked.', 134 | }, 135 | disabled: { 136 | value: false, 137 | type: PropTypes.Boolean, 138 | description: 'Indicates that the button is disabled', 139 | }, 140 | }, 141 | scope: { 142 | Button, 143 | SIZE, 144 | }, 145 | imports: { 146 | 'your-button-component': { 147 | named: ['Button'], 148 | }, 149 | }, 150 | }); 151 | 152 | return ( 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | ); 162 | } 163 | `} 164 | 165 |

166 | useView expects a configuration describing your component and 167 | returns a data-structure that nicely fits into multiple UI components 168 | such as Compiler, Error, Knobs, Editor and Action Buttons. That gives 169 | you the maximum flexibility since you can swap any of these components 170 | for your own. 171 |

172 |

173 | 174 | Note that you never have to specify the code snippet since the code is 175 | auto-generated based on the rest of useView configuration and internal 176 | state. 177 | 178 |

179 |

180 | The biggest part of configuration is a list of props. 181 | You also have to explicitly define the scope (in this 182 | case, importing the Button and passing it through). On the other hand, 183 | the imports setting is completely optional. The imports 184 | appear at the top of auto-generated code. That can be nice for your 185 | users since they will be always able to copy paste a fully working 186 | example.{" "} 187 |

188 |
189 | ); 190 | }; 191 | 192 | export default Basic; 193 | -------------------------------------------------------------------------------- /examples/const.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | export const urls = { 8 | basic: "/?mode=preview&story=useview--basic", 9 | stateHook: "/?mode=preview&story=useview--state-hook", 10 | liveCodeOnly: "/?mode=preview&story=useview--live-code-only", 11 | typescript: "/?mode=preview&story=useview--typescript", 12 | view: "/?mode=preview&story=view--view", 13 | customProps: "/?mode=preview&story=advanced--custom-prop", 14 | theming: "/?mode=preview&story=advanced--theming", 15 | modal: "/?mode=preview&story=tests--modal", 16 | }; 17 | -------------------------------------------------------------------------------- /examples/custom-prop.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | import template from "@babel/template"; 9 | 10 | import { Layout, H1, P, Inline } from "./layout/"; 11 | import { Rating } from "./showcase-components/rating"; 12 | 13 | import { 14 | useView, 15 | Compiler, 16 | Knobs, 17 | Editor, 18 | Error, 19 | ActionButtons, 20 | Placeholder, 21 | PropTypes, 22 | useValueDebounce, 23 | } from "../src"; 24 | 25 | export const customProps = { 26 | value: { 27 | // define how to convert value into an AST tree 28 | generate: (value: number) => { 29 | return (template.ast(String(value), { plugins: ["jsx"] }) as any) 30 | .expression; 31 | }, 32 | // define how to convert the JSX attribute value into value 33 | parse: (code: string) => { 34 | return parseInt(code, 10); 35 | }, 36 | }, 37 | }; 38 | 39 | // custom knob component 40 | const Slider: React.FC<{ 41 | value: number; 42 | set: (val: number, propName: string) => void; 43 | }> = ({ value, set }) => { 44 | // debouncing the knob value so it's always interactive 45 | const [rangeValue, setRangeValue] = useValueDebounce(value, (val) => 46 | set(val, "value"), 47 | ); 48 | return ( 49 | 50 | 78 | 79 | ); 80 | }; 81 | 82 | const StateHook = () => { 83 | const params = useView({ 84 | componentName: "Rating", 85 | props: { 86 | value: { 87 | value: 3, 88 | // mark the prop as type custom so it's not processed by react-view 89 | type: PropTypes.Custom, 90 | description: `Rating value.`, 91 | stateful: true, 92 | }, 93 | onChange: { 94 | value: "value => setValue(value)", 95 | type: PropTypes.Function, 96 | description: `Function called when rating value is changed.`, 97 | propHook: { 98 | what: "value", 99 | into: "value", 100 | }, 101 | }, 102 | }, 103 | scope: { 104 | Rating, 105 | }, 106 | imports: { 107 | "your-rating-component": { 108 | named: ["Rating"], 109 | }, 110 | }, 111 | customProps, 112 | }); 113 | 114 | return ( 115 | 116 |

Custom Props and Knobs

117 |

118 | React View supports many basic prop types out of the box. 119 | Obviously, any prop value can always be edited through an input (or a 120 | tiny code editor). That is no different than writing an actual code. 121 | Boring. 122 |

123 |

124 | 125 | However, many prop types can be more accessible with a specialized UI 126 | 127 | . For example, boolean is always translated into a 128 | checkbox and enum into an input radio or select (if we 129 | have too many options). Those are much nicer and faster to use than 130 | inputs. 131 |

132 |

133 | But what if you want to add something custom that we do not support yet? 134 | There is a customProp API that you can use. You can 135 | control both the knob and also the internal representation of the value. 136 | In this example, we add a pretty simple custom knob. We want to 137 | represent the value with an input slider. 138 |

139 | 144 | 145 | 149 | 150 | 151 | 152 | 153 |

154 | However, you can go much further. For example, our Base Web component 155 | library has this concept of{" "} 156 | 157 | overrides 158 | 159 | . It is a fairly complicated prop that exists on each component and lets 160 | you to customize every aspect of our components. So we have created a 161 | whole sub-playground to just better control the value of this single 162 | prop. Check the{" "} 163 | 164 | Style Overrides tab on the Button page 165 | 166 | .{" "} 167 |

168 |

169 | This is an advanced and very flexible API. For example, you have 170 | to be familiar with the concept of{" "} 171 | AST to 172 | use it. Check the source code of this page or main README for more 173 | details. We will add more docs over time. 174 |

175 |
176 | ); 177 | }; 178 | 179 | export default StateHook; 180 | -------------------------------------------------------------------------------- /examples/layout/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | import { Highlight } from "prism-react-renderer"; 9 | import lightTheme from "../../src/light-theme"; 10 | 11 | export const Layout: React.FC<{ children: React.ReactNode }> = ({ 12 | children, 13 | }) => ( 14 |
21 | {children} 22 |
23 | ); 24 | 25 | export const H1: React.FC<{ children: React.ReactNode }> = ({ children }) => ( 26 |

{children}

27 | ); 28 | 29 | export const H2: React.FC<{ children: React.ReactNode }> = ({ children }) => ( 30 |

{children}

31 | ); 32 | 33 | export const H3: React.FC<{ children: React.ReactNode }> = ({ children }) => ( 34 |

{children}

35 | ); 36 | 37 | export const CompilerBox: React.FC<{ children: React.ReactNode }> = ({ 38 | children, 39 | }) =>
{children}
; 40 | 41 | export const P: React.FC<{ children: React.ReactNode }> = ({ children }) => ( 42 |

49 | {children} 50 |

51 | ); 52 | 53 | export const Code: React.FC<{ children: string }> = ({ children }) => ( 54 |
61 | 66 | {({ style, tokens, getLineProps, getTokenProps }) => ( 67 |
68 |           {tokens.map((line, i) => (
69 |             
70 | {line.map((token, key) => ( 71 | 72 | ))} 73 |
74 | ))} 75 |
76 | )} 77 |
78 |
79 | ); 80 | 81 | export const Inline: React.FC<{ children: string }> = ({ children }) => ( 82 | 92 | {children} 93 | 94 | ); 95 | -------------------------------------------------------------------------------- /examples/live-code-only.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | import { Layout, H1, H2, P, Code, CompilerBox, Inline } from "./layout/"; 9 | import { Button, SIZE } from "./showcase-components/button"; 10 | 11 | import { useView, Compiler, Editor, Error, ActionButtons } from "../src/"; 12 | 13 | const initialCode = `export default () => { 14 | return ( 15 | 16 | ); 17 | }`; 18 | 19 | const initialCodeEl = ``; 20 | const initialCodeSum = `2 + 5`; 21 | 22 | const CodeOnly = () => { 23 | const params = useView({ initialCode, scope: { Button, SIZE } }); 24 | const paramsEl = useView({ 25 | initialCode: initialCodeEl, 26 | scope: { Button, SIZE }, 27 | }); 28 | const paramsSum = useView({ initialCode: initialCodeSum }); 29 | return ( 30 | 31 |

Live Code Editor

32 |

33 | The useView hook can be also used as a live editor only (no prop knobs 34 | or code generation). In this mode, it is very similar to{" "} 35 | react-live.{" "} 36 |

37 | 38 | 39 | 40 | 41 | 42 |

43 | You can create your UI or re-use components from react-view (Editor, 44 | Error...). Optionally you can also add the action buttons: 45 |

46 | 47 |

48 | This time you do not need to configure a list of props. There are 49 | no knobs. However, since no code is auto-generated, you should probably 50 | set the intialCode so the user sees something besides 51 | an empty box. 52 |

53 |

Usage

54 | 55 | {`import { 56 | useView, 57 | Compiler, 58 | Editor, 59 | Error, 60 | ActionButtons 61 | } from 'react-view'; 62 | 63 | export default () => { 64 | const params = useView({ 65 | initialCode: '', 66 | scope: {Button: ({children}) => }, 67 | onUpdate: console.log 68 | }); 69 | 70 | return ( 71 | 72 | 73 | 74 | 75 | 76 | 77 | ); 78 | }`} 79 | 80 |

81 | Note: All import statements in the editor are always taken out 82 | before compilation. They do not do anything. Our compiler does 83 | not understand modules (we do not have a bundler in our flow). So feel 84 | free to add them if beneficial for your users. All dependencies need to 85 | be passed through the scope prop (React is included 86 | automatically). 87 |

88 |

Accepted Code

89 |

90 | The compiler can also handle a React element or class (but we do 91 | not really use those anymore, do we?). 92 |

93 | 94 | 95 | 96 | 97 | 98 |

99 | ...or pretty much anything that{" "} 100 | 101 | could be executed after the return statement of JS 102 | function. 103 | 104 |

105 | 106 | 107 | 108 | 109 | 110 |
111 | ); 112 | }; 113 | 114 | export default CodeOnly; 115 | -------------------------------------------------------------------------------- /examples/modal.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import * as React from "react"; 9 | import { View, PropTypes } from "../src/index"; 10 | import { Layout, H1, P } from "./layout/"; 11 | import Modal from "./showcase-components/modal"; 12 | 13 | const ModalExample = () => ( 14 | 15 |

Modal example

16 |

17 | This is story was created for an e2e test. Reproduces this{" "} 18 | bug report. 19 |

20 | setShow(false)", 30 | type: PropTypes.Function, 31 | description: "Function called when button is clicked.", 32 | propHook: { 33 | what: "false", 34 | into: "show", 35 | }, 36 | }, 37 | show: { 38 | value: false, 39 | type: PropTypes.Boolean, 40 | description: "Indicates that the modal is visible", 41 | stateful: true, 42 | }, 43 | }} 44 | scope={{ 45 | Modal, 46 | }} 47 | imports={{ 48 | "your-modal-component": { 49 | default: "Modal", 50 | }, 51 | }} 52 | /> 53 |
54 | ); 55 | export default ModalExample; 56 | -------------------------------------------------------------------------------- /examples/showcase-components/button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | import { ThemeContext } from "./theme-provider"; 9 | 10 | export const SIZE = { 11 | default: "default", 12 | compact: "compact", 13 | large: "large", 14 | }; 15 | 16 | type TButtonProps = { 17 | children: React.ReactNode; 18 | onClick: (e: any) => void; 19 | size: keyof typeof SIZE; 20 | disabled: boolean; 21 | }; 22 | 23 | export const Button: React.FC = ({ 24 | children, 25 | onClick, 26 | size, 27 | disabled, 28 | }) => { 29 | const colors = React.useContext(ThemeContext); 30 | const getSizeStyle = (size: keyof typeof SIZE) => { 31 | switch (size) { 32 | case SIZE.compact: 33 | return { 34 | padding: "8px", 35 | fontSize: "14px", 36 | }; 37 | case SIZE.large: 38 | return { 39 | padding: "18px", 40 | fontSize: "20px", 41 | }; 42 | default: 43 | return { 44 | padding: "12px", 45 | fontSize: "16px", 46 | }; 47 | } 48 | }; 49 | const btnStyle = { 50 | ...getSizeStyle(size), 51 | background: disabled ? "#CCC" : colors.background, 52 | margin: "0px", 53 | color: disabled ? "#000" : colors.text, 54 | borderRadius: "5px", 55 | borderWidth: "2px", 56 | borderStyle: "solid", 57 | borderColor: disabled ? "#CCC" : colors.background, 58 | }; 59 | return ( 60 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /examples/showcase-components/input.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | 9 | export const SIZE = { 10 | default: "default", 11 | compact: "compact", 12 | large: "large", 13 | }; 14 | 15 | type TInputProps = { 16 | value: string; 17 | onChange: (e: any) => void; 18 | size: keyof typeof SIZE; 19 | disabled: boolean; 20 | editable: boolean; 21 | }; 22 | 23 | export const Input: React.FC = ({ 24 | value, 25 | onChange, 26 | size, 27 | disabled, 28 | editable, 29 | }) => { 30 | const getSizeStyle = (size: keyof typeof SIZE) => { 31 | switch (size) { 32 | case SIZE.compact: 33 | return { 34 | padding: "8px", 35 | fontSize: "14px", 36 | }; 37 | case SIZE.large: 38 | return { 39 | padding: "18px", 40 | fontSize: "20px", 41 | }; 42 | default: 43 | return { 44 | padding: "12px", 45 | fontSize: "16px", 46 | }; 47 | } 48 | }; 49 | const inputStyle = { 50 | ...getSizeStyle(size), 51 | background: disabled ? "#BBB" : "#FFF", 52 | color: "#000", 53 | borderRadius: "5px", 54 | borderWidth: "2px", 55 | borderStyle: "solid", 56 | borderColor: "#000", 57 | }; 58 | return ( 59 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /examples/showcase-components/modal.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import * as React from "react"; 9 | 10 | const Modal: React.FC<{ 11 | handleClose: () => void; 12 | show: boolean; 13 | children: React.ReactNode; 14 | }> = ({ handleClose, show, children }) => { 15 | if (!show) return null; 16 | return ( 17 |
28 |
45 |

{children}

46 | 49 |
50 |
51 | ); 52 | }; 53 | 54 | export default Modal; 55 | -------------------------------------------------------------------------------- /examples/showcase-components/rating.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | 9 | type TRatingProps = { 10 | value: number; 11 | onChange: (e: any) => void; 12 | }; 13 | 14 | type THeartProps = { 15 | active: boolean; 16 | setHovered: (num: number) => void; 17 | index: number; 18 | onClick: () => void; 19 | }; 20 | 21 | const Heart: React.FC = ({ 22 | active, 23 | setHovered, 24 | index, 25 | onClick, 26 | }) => { 27 | const ref = React.useRef(null); 28 | 29 | const handleMouseOver = () => setHovered(index); 30 | const handleMouseOut = () => setHovered(0); 31 | 32 | React.useEffect(() => { 33 | const node = ref.current as any; 34 | if (node) { 35 | node.addEventListener("mouseover", handleMouseOver); 36 | node.addEventListener("mouseout", handleMouseOut); 37 | return () => { 38 | node.removeEventListener("mouseover", handleMouseOver); 39 | node.removeEventListener("mouseout", handleMouseOut); 40 | }; 41 | } 42 | return undefined; 43 | }, [ref.current]); 44 | 45 | return ( 46 | 65 | ); 66 | }; 67 | export const Rating: React.FC = ({ value, onChange }) => { 68 | const [hovered, setHovered] = React.useState(0); 69 | return ( 70 |
    75 | {[...Array(5).keys()].map((index) => ( 76 | index : hovered > index} 79 | index={index + 1} 80 | setHovered={setHovered} 81 | onClick={() => onChange(index + 1)} 82 | /> 83 | ))} 84 |
85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /examples/showcase-components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | 9 | export const defaultTheme = { 10 | background: "#276EF1", 11 | text: "#FFF", 12 | }; 13 | 14 | export const ThemeContext = React.createContext(defaultTheme); 15 | 16 | type TThemeProviderProps = { 17 | children: React.ReactNode; 18 | colors?: { 19 | background?: string; 20 | text?: string; 21 | }; 22 | }; 23 | 24 | const ThemeProvider: React.FC = ({ children, colors }) => { 25 | return ( 26 | 29 | {children} 30 | 31 | ); 32 | }; 33 | 34 | export default ThemeProvider; 35 | -------------------------------------------------------------------------------- /examples/state-hook.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | import { Layout, H1, H2, P, Code, Inline } from "./layout/"; 9 | import { Input, SIZE } from "./showcase-components/input"; 10 | 11 | import { 12 | useView, 13 | Compiler, 14 | Knobs, 15 | Editor, 16 | Error, 17 | ActionButtons, 18 | Placeholder, 19 | PropTypes, 20 | } from "../src"; 21 | 22 | const StateHook = () => { 23 | const params = useView({ 24 | componentName: "Input", 25 | props: { 26 | value: { 27 | value: "Hello", 28 | type: PropTypes.String, 29 | description: `Input value.`, 30 | stateful: true, 31 | }, 32 | size: { 33 | value: "SIZE.default", 34 | defaultValue: "SIZE.default", 35 | options: SIZE, 36 | type: PropTypes.Enum, 37 | description: "Defines the size of the button.", 38 | imports: { 39 | "your-input-component": { 40 | named: ["SIZE"], 41 | }, 42 | }, 43 | }, 44 | onChange: { 45 | value: "e => setValue(e.target.value)", 46 | type: PropTypes.Function, 47 | description: `Function called when input value is changed.`, 48 | propHook: { 49 | what: "e.target.value", 50 | into: "value", 51 | }, 52 | }, 53 | editable: { 54 | value: true, 55 | defaultValue: true, 56 | type: PropTypes.Boolean, 57 | description: "Indicates that the input is editable.", 58 | }, 59 | }, 60 | scope: { 61 | Input, 62 | SIZE, 63 | }, 64 | imports: { 65 | "your-input-component": { 66 | named: ["Input"], 67 | }, 68 | }, 69 | }); 70 | 71 | return ( 72 | 73 |

State Hook

74 |

75 | Not all components are as simple as buttons. The most of React 76 | components have some sort of state. For example, inputs have the{" "} 77 | value state. By default, React View treats everything as{" "} 78 | 79 | controlled components 80 | 81 | . So when you specify the list of props in useView, you will get the 82 | output like this: 83 |

84 | {` 85 | `} 86 |

87 | And that works. You can still update the value by changing the value 88 | knob or editing the code directly.{" "} 89 | 90 | However, you would not be able to interact with the component itself 91 | {" "} 92 | since the value is hard-coded - the component is controlled. The code 93 | above is also not very realistic. How often do we create non editable 94 | inputs? 95 |

96 |

97 | Fortunately, React View has special{" "} 98 | 99 | propHook 100 | {" "} 101 | and{" "} 102 | 103 | stateful 104 | {" "} 105 | settings so you can achieve full interactivity: 106 |

107 | 112 | 113 | 114 | 115 | 116 | 117 |

118 | The example above has its own internal value state (using{" "} 119 | 120 | React.useState 121 | 122 | ) and the value knob is now translated into its initial internal state. 123 | Now you can interact with the component itself and{" "} 124 | everything is still synchronized. Moreover, the code snippet now 125 | also better demonstrates the real-world usage. 126 |

127 |

Usage

128 | {`import * as React from 'react'; 129 | import {Input, SIZE} from 'your-input-component'; 130 | 131 | import { 132 | useView, 133 | Compiler, 134 | Knobs, 135 | Editor, 136 | Error, 137 | ActionButtons, 138 | Placeholder, 139 | PropTypes, 140 | } from 'react-view'; 141 | 142 | const StateHook = () => { 143 | const params = useView({ 144 | componentName: 'Input', 145 | props: { 146 | value: { 147 | value: 'Hello', 148 | type: PropTypes.String, 149 | description: 'Input value.', 150 | stateful: true, 151 | }, 152 | size: { 153 | value: 'SIZE.default', 154 | defaultValue: 'SIZE.default', 155 | options: SIZE, 156 | type: PropTypes.Enum, 157 | description: 'Defines the size of the button.', 158 | imports: { 159 | 'your-input-component': { 160 | named: ['SIZE'], 161 | }, 162 | }, 163 | }, 164 | onChange: { 165 | value: 'e => setValue(e.target.value)', 166 | type: PropTypes.Function, 167 | description: 'Function called when input value is changed.', 168 | propHook: { 169 | what: 'e.target.value', 170 | into: 'value', 171 | }, 172 | }, 173 | editable: { 174 | value: true, 175 | defaultValue: true, 176 | type: PropTypes.Boolean, 177 | description: 'Indicates that the input is editable.', 178 | }, 179 | }, 180 | scope: { 181 | Input, 182 | SIZE, 183 | }, 184 | imports: { 185 | 'your-input-component': { 186 | named: ['Input'], 187 | }, 188 | }, 189 | }); 190 | 191 | return ( 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | ); 201 | };`} 202 |

203 | There are just two changes that we have to make compared to the{" "} 204 | basic example. First, we have 205 | to detach the value prop into an internal state. We simply add the{" "} 206 | 207 | stateful flag 208 | 209 | : 210 |

211 | {`useView({ 212 | props: { 213 | value: { 214 | value: 'Hello', 215 | type: PropTypes.String, 216 | stateful: true, 217 | }, 218 | /* ... */ 219 | } 220 | }) 221 | `} 222 |

223 | At this point,{" "} 224 | the value is detached and rendered input is fully interactive. 225 | However, the changes are not synchronized with the rest of the 226 | playground. We need to give React View a slight hint:{" "} 227 |

228 | 229 | {`onChange: { 230 | value: 'e => setValue(e.target.value)', 231 | type: PropTypes.Function, 232 | propHook: { 233 | what: 'e.target.value', 234 | into: 'value', 235 | }, 236 | } 237 | `} 238 | 239 |

240 | We have added the{" "} 241 | 242 | propHook.what 243 | {" "} 244 | and{" "} 245 | 246 | propHook.into 247 | {" "} 248 | in the onChange prop. We are telling React View{" "} 249 | what value it should use and into what 250 | stateful prop it should go. Note that this setting also depends on the 251 | initial value of onChange prop since React View 252 | secretly adds an instrumentation call into the body of{" "} 253 | e > setValue(e.target.value) function. 254 |

255 |

defaultValue

256 |

257 | Props can have a defaultValue. That is useful for an{" "} 258 | enum so the code generator knows when to skip the 259 | default option. Sometimes you can also have a boolean{" "} 260 | prop that treats undefined the opposite way to{" "} 261 | false. In the example above, this inverted behavior is 262 | demonstrated with the prop editable: 263 |

264 | {`editable: { 265 | value: true, 266 | defaultValue: true, 267 | type: PropTypes.Boolean, 268 | description: 'Indicates that the input is editable.', 269 | }`} 270 |
271 | ); 272 | }; 273 | 274 | export default StateHook; 275 | -------------------------------------------------------------------------------- /examples/test.stories.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | import Modal from "./modal"; 9 | 10 | export default { 11 | title: "Tests", 12 | }; 13 | 14 | export const modal = () => { 15 | return ; 16 | }; 17 | -------------------------------------------------------------------------------- /examples/theming.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Uber Technologies, Inc. 3 | 4 | This source code is licensed under the MIT license found in the 5 | LICENSE file in the root directory of this source tree. 6 | */ 7 | import * as React from "react"; 8 | import * as t from "@babel/types"; 9 | import traverse from "@babel/traverse"; 10 | import { Layout, H1, H3, P, Inline } from "./layout/"; 11 | import { Button, SIZE } from "./showcase-components/button"; 12 | import ThemeProvider, { 13 | defaultTheme, 14 | } from "./showcase-components/theme-provider"; 15 | 16 | import { 17 | useView, 18 | Compiler, 19 | Knobs, 20 | Editor, 21 | Error, 22 | ActionButtons, 23 | Placeholder, 24 | PropTypes, 25 | getAstJsxElement, 26 | useValueDebounce, 27 | } from "../src"; 28 | 29 | type TTheme = typeof defaultTheme; 30 | type TThemeKeys = keyof TTheme; 31 | type TProviderValue = Partial | undefined; 32 | type TThemeEditorProps = { 33 | theme: TTheme; 34 | set: (value: Partial | undefined) => void; 35 | }; 36 | type TColorInputProps = { 37 | themeKey: TThemeKeys; 38 | globalColor: string; 39 | globalSet: (color: string) => void; 40 | }; 41 | 42 | export const getActiveTheme = ( 43 | values: { [key: string]: string }, 44 | initialValues: { [key: string]: string }, 45 | ) => { 46 | const activeValues: { [key: string]: string } = {}; 47 | Object.keys(initialValues).forEach((key) => { 48 | activeValues[key] = initialValues[key]; 49 | if (values && values[key]) { 50 | activeValues[key] = values[key]; 51 | } 52 | }); 53 | return activeValues; 54 | }; 55 | 56 | export const getThemeDiff = ( 57 | values: { [key: string]: string }, 58 | initialValues: { [key: string]: string }, 59 | ) => { 60 | const diff: { [key: string]: string } = {}; 61 | Object.keys(values).forEach((key) => { 62 | if ( 63 | initialValues[key] && 64 | values[key] && 65 | initialValues[key] !== values[key] 66 | ) { 67 | diff[key] = values[key]; 68 | } 69 | }); 70 | return diff; 71 | }; 72 | 73 | const ColorInput: React.FC = ({ 74 | themeKey, 75 | globalSet, 76 | globalColor, 77 | }) => { 78 | const [color, setColor] = useValueDebounce(globalColor, globalSet); 79 | return ( 80 |