├── .nvmrc ├── src ├── lib │ ├── DownloadProject.svelte │ ├── Sidebar.svelte │ ├── ToggleBuilderMode.svelte │ ├── Accordion.svelte │ ├── ClearEditor.svelte │ ├── ToggleCodePanel.svelte │ ├── Dropdown.svelte │ ├── PrismContainer.svelte │ ├── ActionsPanel.svelte │ ├── EditorMenu.svelte │ ├── HtmxTemplates.svelte │ ├── InspectorPanel.svelte │ ├── ExportToCodeSandbox.svelte │ ├── PreviewContainer.svelte │ ├── Editor.svelte │ └── ViewSelector.svelte ├── updateProps.js ├── panels │ ├── RootPanel.svelte │ ├── AnchorPanel.svelte │ ├── ButtonPanel.svelte │ ├── OptionPanel.svelte │ ├── ImgPanel.svelte │ ├── InputPanel.svelte │ ├── DefaultPanel.svelte │ ├── ChildrenPanel.svelte │ └── HtmxPanel.svelte ├── main.js ├── utils │ ├── generateId.js │ ├── import.js │ └── recursive.js ├── icons │ ├── caret-down.svg │ ├── box-arrow-up-right.svg │ ├── copy.svg │ ├── refresh.svg │ ├── trash.svg │ └── code.svg ├── getExpressCode.js ├── defaultProps.js ├── styles.css ├── App.svelte ├── generateCode.js └── stores.js ├── .gitignore ├── vite.config.js ├── index.html ├── public └── templates │ ├── lazy-loading.json │ ├── progress-bar.json │ ├── infinite-scroll.json │ ├── click-to-load.json │ ├── edit-row.json │ ├── delete-row.json │ ├── click-to-edit.json │ ├── active-search.json │ ├── inline-validation.json │ └── bulk-update.json ├── package.json ├── README.md ├── LICENSE ├── jsconfig.json ├── favicon.svg └── pnpm-lock.yaml /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.19.1 2 | -------------------------------------------------------------------------------- /src/lib/DownloadProject.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/Sidebar.svelte: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /src/updateProps.js: -------------------------------------------------------------------------------- 1 | import { editor } from './stores'; 2 | 3 | export default function updateProps(props) { 4 | editor.updateProps(props); 5 | } 6 | -------------------------------------------------------------------------------- /src/panels/RootPanel.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | import './styles.css'; 3 | 4 | const app = new App({ 5 | target: document.getElementById('app') 6 | }) 7 | 8 | export default app 9 | -------------------------------------------------------------------------------- /src/utils/generateId.js: -------------------------------------------------------------------------------- 1 | export const generateId = () => { 2 | return `comp-${( 3 | Date.now().toString(36) + 4 | Math.random() 5 | .toString(36) 6 | .substr(2, 5) 7 | ).toUpperCase()}` 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/ToggleBuilderMode.svelte: -------------------------------------------------------------------------------- 1 | 3 |
4 | 7 |
8 | 14 | -------------------------------------------------------------------------------- /src/icons/caret-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | import { svelteSVG } from "rollup-plugin-svelte-svg"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | svelte(), 9 | svelteSVG({ 10 | svgo: {}, 11 | enforce: "pre", 12 | }), 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /src/lib/Accordion.svelte: -------------------------------------------------------------------------------- 1 | 5 |
6 | {heading} 7 | 8 |
9 | 10 | 21 | -------------------------------------------------------------------------------- /src/lib/ClearEditor.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 |
12 | 13 | 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | HTMX Playground 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/getExpressCode.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | 3 | const code = ` 4 | const express = require('express') 5 | const app = express() 6 | const port = 3000 7 | app.set('view engine', 'ejs') 8 | 9 | app.get('/contacts/1/edit', (req, res) => { 10 | res.render('editcontact'); 11 | }) 12 | 13 | app.get('/', (req, res) => { 14 | res.render('index') 15 | }) 16 | 17 | app.listen(port, () => { 18 | console.log(\`Example app listening on port \${port}\`) 19 | }) 20 | 21 | `; 22 | 23 | return code; 24 | } 25 | -------------------------------------------------------------------------------- /src/icons/box-arrow-up-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/defaultProps.js: -------------------------------------------------------------------------------- 1 | const defaultProps = { 2 | div: {}, 3 | button: { children: "Hello" }, 4 | form: {}, 5 | label: { 6 | children: "MyLabel:", 7 | }, 8 | input: {}, 9 | select: { 10 | options: [ 11 | { value: "Audi", label: "Audi" }, 12 | { value: "Ferrari", label: "Ferrari" }, 13 | { value: "BMW", label: "BMW" }, 14 | ], 15 | }, 16 | span: { 17 | children: "My span", 18 | }, 19 | table: {}, 20 | thead: {}, 21 | tbody: {}, 22 | th: {}, 23 | tr: {}, 24 | td: {}, 25 | }; 26 | 27 | export default defaultProps; 28 | -------------------------------------------------------------------------------- /public/templates/lazy-loading.json: -------------------------------------------------------------------------------- 1 | {"selectedId":"comp-L2Y92P63VQOJF","components":{"root":{"id":"root","parent":"root","type":"root","children":["comp-L2Y92JR83G45M"],"props":{}},"comp-L2Y92JR83G45M":{"id":"comp-L2Y92JR83G45M","props":{"hx-get":"/graph","hx-trigger":"load"},"children":["comp-L2Y92P63VQOJF"],"type":"div","parent":"root","rootParentType":"root"},"comp-L2Y92P63VQOJF":{"id":"comp-L2Y92P63VQOJF","props":{"alt":"Result Loading...","class":"htmx-indicator","src":"/img/bars.svg"},"children":[],"type":"img","parent":"comp-L2Y92JR83G45M","rootParentType":"img"}},"builderMode":true,"showCode":false} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "htmx-playground", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "rollup-plugin-svelte-svg": "^1.0.0-beta.6", 12 | "vite": "^2.9.5" 13 | }, 14 | "dependencies": { 15 | "@sveltejs/vite-plugin-svelte": "^1.0.0-next.41", 16 | "browser-nativefs": "^0.12.0", 17 | "codesandbox": "^2.2.3", 18 | "immer": "^9.0.12", 19 | "lodash": "^4.17.21", 20 | "prettier": "^2.6.2", 21 | "prismjs": "^1.28.0", 22 | "sortablejs": "^1.15.0", 23 | "svelte": "^3.47.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/templates/progress-bar.json: -------------------------------------------------------------------------------- 1 | {"selectedId":"comp-L2Y9X5Q71N4RP","components":{"root":{"id":"root","parent":"root","type":"root","children":["comp-L2Y9WXY928M50"],"props":{}},"comp-L2Y9WXY928M50":{"id":"comp-L2Y9WXY928M50","props":{"hx-target":"this","hx-swap":"outerHTML"},"children":["comp-L2Y9X2CEB036U","comp-L2Y9X5Q71N4RP"],"type":"div","parent":"root","rootParentType":"root"},"comp-L2Y9X2CEB036U":{"id":"comp-L2Y9X2CEB036U","props":{"children":"Start Progress"},"children":[],"type":"h3","parent":"comp-L2Y9WXY928M50","rootParentType":"h3"},"comp-L2Y9X5Q71N4RP":{"id":"comp-L2Y9X5Q71N4RP","props":{"children":"Start Job","class":"btn","hx-post":"/start"},"children":[],"type":"button","parent":"comp-L2Y9WXY928M50","rootParentType":"button"}},"builderMode":true,"showCode":false} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # htmx-playground 2 | 3 | An advanced Drag-n-Drop editor for building HTML with support for [HTMX](https://htmx.org) attributes. 4 | 5 | ![Demo](https://www.youtube.com/watch?v=s3O4xLW7qgA) 6 | 7 | ## Features 8 | - Drag and drop HTML tags 9 | - Support for minimal HTML attributes like id, class, children, etc., 10 | - Support all HTMX attributes 11 | - View the generated HTML markup code 12 | - Can export the current HTML to a Code Sandbox project 13 | - Some built in examples of HTMX 14 | - Ability to import and export the design as JSON 15 | - Reset the editor at any point of time. 16 | 17 | 18 | ## More coming soon 19 | - Ability to add hyperscript "_" attributes 20 | - Ability to build/edit Multiple views/designs simultaneously 21 | - Preview mode in Editor itself 22 | -------------------------------------------------------------------------------- /src/lib/ToggleCodePanel.svelte: -------------------------------------------------------------------------------- 1 | 24 |
25 | 28 |
29 | 35 | -------------------------------------------------------------------------------- /public/templates/infinite-scroll.json: -------------------------------------------------------------------------------- 1 | {"selectedId":"comp-L2Y9M7N3E3AJV","components":{"root":{"id":"root","parent":"root","type":"root","children":["comp-L2Y9M7N3E3AJV"],"props":{}},"comp-L2Y9M7N3E3AJV":{"id":"comp-L2Y9M7N3E3AJV","props":{"hx-get":"/contacts/?page=2","hx-trigger":"revealed","hx-swap":"outerHTML"},"children":["comp-L2Y9M9LTAYNDE","comp-L2Y9MBC6NIOP9","comp-L2Y9MBOA0SDVO"],"type":"tr","parent":"root","rootParentType":"root"},"comp-L2Y9M9LTAYNDE":{"id":"comp-L2Y9M9LTAYNDE","props":{"children":"Agent Smith"},"children":[],"type":"td","parent":"comp-L2Y9M7N3E3AJV","rootParentType":"td"},"comp-L2Y9MBC6NIOP9":{"id":"comp-L2Y9MBC6NIOP9","props":{"children":"void29@null.org"},"children":[],"type":"td","parent":"comp-L2Y9M7N3E3AJV","rootParentType":"td"},"comp-L2Y9MBOA0SDVO":{"id":"comp-L2Y9MBOA0SDVO","props":{"children":"12345678"},"children":[],"type":"td","parent":"comp-L2Y9M7N3E3AJV","rootParentType":"td"}},"builderMode":true,"showCode":false} -------------------------------------------------------------------------------- /src/panels/AnchorPanel.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | {#each attrs as attr} 22 | 23 | handleInput(attr.name, ev.target.value)} /> 24 | {/each} 25 | 26 | 43 | -------------------------------------------------------------------------------- /src/panels/ButtonPanel.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | {#each attrs as attr} 22 | 23 | handleInput(attr.name, ev.target.value)} /> 24 | {/each} 25 | 26 | 43 | -------------------------------------------------------------------------------- /src/panels/OptionPanel.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | {#each attrs as attr} 22 | 23 | handleInput(attr.name, ev.target.value)} /> 24 | {/each} 25 | 26 | 43 | -------------------------------------------------------------------------------- /src/panels/ImgPanel.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | {#each attrs as attr} 31 | 32 | handleInput(attr.name, ev.target.value)} /> 33 | {/each} 34 | 35 | 52 | -------------------------------------------------------------------------------- /src/panels/InputPanel.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | {#each attrs as attr} 31 | 32 | handleInput(attr.name, ev.target.value)} /> 33 | {/each} 34 | 35 | 52 | -------------------------------------------------------------------------------- /src/utils/import.js: -------------------------------------------------------------------------------- 1 | import { fileOpen, fileSave } from "browser-nativefs"; 2 | 3 | export async function loadFromJSON() { 4 | const blob = await fileOpen({ 5 | extensions: [".json"], 6 | mimeTypes: ["application/json"], 7 | }); 8 | 9 | const contents = await new Promise((resolve) => { 10 | const reader = new FileReader(); 11 | reader.readAsText(blob, "utf8"); 12 | reader.onloadend = () => { 13 | if (reader.readyState === FileReader.DONE) { 14 | resolve(reader.result); 15 | } 16 | }; 17 | }); 18 | 19 | try { 20 | return JSON.parse(contents); 21 | } catch (error) { 22 | console.error(error); 23 | } 24 | } 25 | 26 | export async function saveAsJSON(components) { 27 | const serialized = JSON.stringify(components); 28 | const name = prompt("Please enter a file name (without extension)"); 29 | 30 | if (name) { 31 | await fileSave( 32 | new Blob([serialized], { type: "application/json" }), 33 | { 34 | fileName: `${name}.json`, 35 | description: "Crayons Playground file", 36 | }, 37 | window.handle 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/Dropdown.svelte: -------------------------------------------------------------------------------- 1 | 5 | 13 | 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Rajasegar Chandran 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. -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "esnext", 5 | "module": "esnext", 6 | /** 7 | * svelte-preprocess cannot figure out whether you have 8 | * a value or a type, so tell TypeScript to enforce using 9 | * `import type` instead of `import` for Types. 10 | */ 11 | "importsNotUsedAsValues": "error", 12 | "isolatedModules": true, 13 | "resolveJsonModule": true, 14 | /** 15 | * To have warnings / errors of the Svelte compiler at the 16 | * correct position, enable source maps by default. 17 | */ 18 | "sourceMap": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "baseUrl": ".", 23 | /** 24 | * Typecheck JS in `.svelte` and `.js` files by default. 25 | * Disable this if you'd like to use dynamic types. 26 | */ 27 | "checkJs": true 28 | }, 29 | /** 30 | * Use global.d.ts instead of compilerOptions.types 31 | * to avoid limiting type declarations. 32 | */ 33 | "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] 34 | } 35 | -------------------------------------------------------------------------------- /public/templates/click-to-load.json: -------------------------------------------------------------------------------- 1 | {"selectedId":"comp-L2Y8SLLTR5JYD","components":{"root":{"id":"root","parent":"root","type":"root","children":["comp-L2Y8RKK1CJDDF"],"props":{}},"comp-L2Y8RKK1CJDDF":{"id":"comp-L2Y8RKK1CJDDF","props":{"id":"replaceMe"},"children":["comp-L2Y8SAZLI5K07"],"type":"tr","parent":"root","rootParentType":"root"},"comp-L2Y8SAZLI5K07":{"id":"comp-L2Y8SAZLI5K07","props":{},"children":["comp-L2Y8SF1FWBH2T"],"type":"td","parent":"comp-L2Y8RKK1CJDDF","rootParentType":"td"},"comp-L2Y8SF1FWBH2T":{"id":"comp-L2Y8SF1FWBH2T","props":{"children":"Hello","class":"btn","hx-get":"/contacts/?page=2","hx-target":"#replaceMe","hx-swap":"outerHTML"},"children":["comp-L2Y8SILA68C7F","comp-L2Y8SLLTR5JYD"],"type":"button","parent":"comp-L2Y8SAZLI5K07","rootParentType":"button"},"comp-L2Y8SILA68C7F":{"id":"comp-L2Y8SILA68C7F","props":{"children":"Load More Agents"},"children":[],"type":"span","parent":"comp-L2Y8SF1FWBH2T","rootParentType":"span"},"comp-L2Y8SLLTR5JYD":{"id":"comp-L2Y8SLLTR5JYD","props":{"src":"/img/bars.svg","class":"htmx-indicator"},"children":[],"type":"img","parent":"comp-L2Y8SF1FWBH2T","rootParentType":"img"}},"builderMode":true,"showCode":false} -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --elephant: #183247; 3 | --smoke: #f3f5f7; 4 | --milk: #fff; 5 | --smoke10: #f7f9fa; 6 | --smoke700: #475867; 7 | --smoke600: #576c7d; 8 | --jungle: #00a886; 9 | --azure: #2c5cc5; 10 | --persimmon: #e43538; 11 | --casablanca: #e86f25; 12 | --sidebar-bg: #264966; 13 | } 14 | 15 | * { 16 | margin: 0; 17 | padding: 0; 18 | line-height: 1; 19 | } 20 | 21 | body { 22 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | 28 | 29 | #code-panel { 30 | overflow-y: auto; 31 | overflow-x: hidden; 32 | position: relative; 33 | } 34 | 35 | pre { 36 | padding: 2em; 37 | } 38 | 39 | 40 | 41 | .gutter { 42 | background-color: var(--elephant); 43 | background-repeat: no-repeat; 44 | background-position: 50%; 45 | } 46 | 47 | .gutter.gutter-horizontal { 48 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); 49 | cursor: col-resize; 50 | } 51 | 52 | 53 | .dropbtn svg { 54 | vertical-align: text-bottom; 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/PrismContainer.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | {#await codePromise} 24 |

Loading...

25 | {:then code} 26 |
27 | 28 |
29 |
30 | {@html Prism.highlight(code, Prism.languages[language])} 31 |
32 | {/await} 33 | 34 | 35 | 36 | 37 | 38 | 59 | -------------------------------------------------------------------------------- /public/templates/edit-row.json: -------------------------------------------------------------------------------- 1 | {"selectedId":"root","components":{"root":{"id":"root","parent":"root","type":"root","children":["comp-L2Y8WGPP3U0VW"],"props":{}},"comp-L2Y8WGPP3U0VW":{"id":"comp-L2Y8WGPP3U0VW","props":{"class":"table delete-row-example"},"children":["comp-L2Y8WKTVVX1LN","comp-L2Y8XMF9VQN3R"],"type":"table","parent":"root","rootParentType":"root"},"comp-L2Y8WKTVVX1LN":{"id":"comp-L2Y8WKTVVX1LN","props":{},"children":["comp-L2Y8WNJMLDIC3"],"type":"thead","parent":"comp-L2Y8WGPP3U0VW","rootParentType":"thead"},"comp-L2Y8WNJMLDIC3":{"id":"comp-L2Y8WNJMLDIC3","props":{},"children":["comp-L2Y8WSWLHJS70","comp-L2Y8WWU7HW552","comp-L2Y8WY18DURFZ"],"type":"tr","parent":"comp-L2Y8WKTVVX1LN","rootParentType":"tr"},"comp-L2Y8WSWLHJS70":{"id":"comp-L2Y8WSWLHJS70","props":{"children":"Name"},"children":[],"type":"th","parent":"comp-L2Y8WNJMLDIC3","rootParentType":"th"},"comp-L2Y8WWU7HW552":{"id":"comp-L2Y8WWU7HW552","props":{"children":"Email"},"children":[],"type":"th","parent":"comp-L2Y8WNJMLDIC3","rootParentType":"th"},"comp-L2Y8WY18DURFZ":{"id":"comp-L2Y8WY18DURFZ","props":{"children":""},"children":[],"type":"th","parent":"comp-L2Y8WNJMLDIC3","rootParentType":"th"},"comp-L2Y8XMF9VQN3R":{"id":"comp-L2Y8XMF9VQN3R","props":{"hx-confirm":"","hx-target":"closest tr","hx-swap":"outerHTML"},"children":[],"type":"tbody","parent":"comp-L2Y8WGPP3U0VW","rootParentType":"tbody"}},"builderMode":true,"showCode":false} -------------------------------------------------------------------------------- /src/lib/ActionsPanel.svelte: -------------------------------------------------------------------------------- 1 | 25 |
26 | 29 | 32 | 35 | 38 |
39 | 40 | 54 | -------------------------------------------------------------------------------- /src/panels/DefaultPanel.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | handleInput('id', ev.target.value)} /> 23 | 24 | handleInput('class', ev.target.value)} /> 25 | 26 |