[{"data":1,"prerenderedAt":3597},["ShallowReactive",2],{"i-lucide:chevron-down":3,"i-lucide:search":8,"i-lucide:sun":10,"i-lucide:moon":12,"i-lucide:arrow-right":14,"i-lucide:menu":16,"i-lucide:mail":18,"i-simple-icons:x":20,"i-simple-icons:linkedin":22,"i-simple-icons:dribbble":25,"i-simple-icons:github":27,"i-lucide:arrow-up-right":29,"blog-\u002Fblog\u002Fen\u002Fjavafx-mysql-student-enrollment":31,"blog-related-\u002Fblog\u002Fen\u002Fjavafx-mysql-student-enrollment":2215,"i-lucide:chevron-right":3581,"i-lucide:calendar":3583,"i-lucide:clock":3585,"i-lucide:link":3587,"i-lucide:blocks":3589,"i-lucide:circle-check":3591,"i-lucide:triangle-alert":3593,"i-lucide:copy":3595},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m6 9l6 6l6-6\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"m21 21l-4.34-4.34\"\u002F>\u003Ccircle cx=\"11\" cy=\"11\" r=\"8\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"4\"\u002F>\u003Cpath d=\"M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7l7 7l-7 7\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":17},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 5h16M4 12h16M4 19h16\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":19},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"m22 7l-8.991 5.727a2 2 0 0 1-2.009 0L2 7\"\u002F>\u003Crect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":21},"\u003Cpath fill=\"currentColor\" d=\"M14.234 10.162L22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299l-.929-1.329L3.076 1.56h3.182l5.965 8.532l.929 1.329l7.754 11.09h-3.182z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":23,"hidden":24},"\u003Cpath fill=\"currentColor\" d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037c-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85c3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.06 2.06 0 0 1-2.063-2.065a2.064 2.064 0 1 1 2.063 2.065m1.782 13.019H3.555V9h3.564zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0z\"\u002F>",true,{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":26},"\u003Cpath fill=\"currentColor\" d=\"M12 24C5.385 24 0 18.615 0 12S5.385 0 12 0s12 5.385 12 12s-5.385 12-12 12m10.12-10.358c-.35-.11-3.17-.953-6.384-.438c1.34 3.684 1.887 6.684 1.992 7.308a10.28 10.28 0 0 0 4.395-6.87zm-6.115 7.808c-.153-.9-.75-4.032-2.19-7.77l-.066.02c-5.79 2.015-7.86 6.025-8.04 6.4a10.16 10.16 0 0 0 6.29 2.166c1.42 0 2.77-.29 4-.814zm-11.62-2.58c.232-.4 3.045-5.055 8.332-6.765q.202-.067.405-.12q-.392-.879-.832-1.74C7.17 11.775 2.206 11.71 1.756 11.7l-.004.312c0 2.633.998 5.037 2.634 6.855zm-2.42-8.955c.46.008 4.683.026 9.477-1.248a66 66 0 0 0-3.8-5.928a10.28 10.28 0 0 0-5.676 7.17zM9.6 2.052c.282.38 2.145 2.914 3.822 6c3.645-1.365 5.19-3.44 5.373-3.702A10.2 10.2 0 0 0 12 1.764c-.825 0-1.63.1-2.4.285zm10.335 3.483c-.218.29-1.935 2.493-5.724 4.04c.24.49.47.985.68 1.486c.08.18.15.36.22.53c3.41-.43 6.8.26 7.14.33c-.02-2.42-.88-4.64-2.31-6.38z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":28},"\u003Cpath fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":30},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M7 7h10v10M7 17L17 7\"\u002F>",{"id":32,"title":33,"author":34,"body":37,"category":2200,"cover":2201,"coverAlt":33,"date":2202,"description":2203,"draft":6,"extension":2204,"featured":6,"lang":2205,"meta":2206,"navigation":24,"path":2207,"readingTime":510,"seo":2208,"seoTitle":2209,"stem":2210,"tags":2211,"__hash__":2214},"blog\u002Fblog\u002Fen\u002Fjavafx-mysql-student-enrollment.md","Build a student enrollment app with JavaFX, Scene Builder and MySQL",{"name":35,"role":36},"UXAtom Team","Engineering",{"type":38,"value":39,"toc":2180},"minimark",[40,49,52,80,83,115,121,126,137,265,276,286,290,305,322,326,337,369,375,421,440,444,453,717,731,735,738,746,842,852,858,865,1719,1722,1762,1772,1778,1881,1896,1900,1903,1990,1997,2001,2004,2122,2125,2129,2132,2162,2165,2169,2176],[41,42,43,44,48],"p",{},"Most first database apps go wrong in the same place. Not in the SQL, and not in the UI, but in the wiring between them: an ",[45,46,47],"code",{},"fx:id"," that doesn't match its field, a module that isn't opened, a table that shows the right number of rows with every cell empty and no error to say why.",[41,50,51],{},"So this tutorial builds something small on purpose. A desktop app that manages student course enrollments, with four features:",[53,54,55,63,69,75],"ol",{},[56,57,58,62],"li",{},[59,60,61],"strong",{},"Enroll a student"," with an ID, name and course, validated first: every field required, no duplicate IDs",[56,64,65,68],{},[59,66,67],{},"List all students"," in a table",[56,70,71,74],{},[59,72,73],{},"Delete a student"," by ID",[56,76,77,74],{},[59,78,79],{},"Search for a student",[41,81,82],{},"Small enough to finish in one sitting, but it touches every seam that matters:",[84,85,86,96,106,112],"ul",{},[56,87,88,89,92,93],{},"Designing a UI visually in ",[59,90,91],{},"Scene Builder"," and connecting it to Java through ",[59,94,95],{},"FXML",[56,97,98,99,102,103],{},"Talking to a database with ",[59,100,101],{},"JDBC"," and ",[45,104,105],{},"PreparedStatement",[56,107,108,109],{},"Showing database rows in a JavaFX ",[45,110,111],{},"TableView",[56,113,114],{},"Validating input before it ever reaches the database",[41,116,117,120],{},[59,118,119],{},"Tools:"," IntelliJ IDEA · Scene Builder · MySQL Workbench · JDK 17+",[122,123,125],"h2",{"id":124},"step-1-create-the-database","Step 1: Create the database",[41,127,128,129,132,133,136],{},"Open ",[59,130,131],{},"MySQL Workbench",", connect to your local instance, open a new query tab (",[45,134,135],{},"Ctrl+T",") and run:",[138,139,144],"pre",{"className":140,"code":141,"language":142,"meta":143,"style":143},"language-sql shiki shiki-themes github-dark-dimmed github-dark-dimmed","CREATE DATABASE enrollment_db;\nUSE enrollment_db;\n\nCREATE TABLE students (\n    student_id    VARCHAR(20)  PRIMARY KEY,\n    student_name  VARCHAR(100) NOT NULL,\n    course        VARCHAR(50)  NOT NULL\n);\n","sql","",[45,145,146,166,175,181,195,220,241,259],{"__ignoreMap":143},[147,148,151,155,158,162],"span",{"class":149,"line":150},"line",1,[147,152,154],{"class":153},"sEYkB","CREATE",[147,156,157],{"class":153}," DATABASE",[147,159,161],{"class":160},"s-wk_"," enrollment_db",[147,163,165],{"class":164},"sM9_K",";\n",[147,167,169,172],{"class":149,"line":168},2,[147,170,171],{"class":153},"USE",[147,173,174],{"class":164}," enrollment_db;\n",[147,176,178],{"class":149,"line":177},3,[147,179,180],{"emptyLinePlaceholder":24},"\n",[147,182,184,186,189,192],{"class":149,"line":183},4,[147,185,154],{"class":153},[147,187,188],{"class":153}," TABLE",[147,190,191],{"class":160}," students",[147,193,194],{"class":164}," (\n",[147,196,198,201,204,207,211,214,217],{"class":149,"line":197},5,[147,199,200],{"class":164},"    student_id    ",[147,202,203],{"class":153},"VARCHAR",[147,205,206],{"class":164},"(",[147,208,210],{"class":209},"sQdni","20",[147,212,213],{"class":164},")  ",[147,215,216],{"class":153},"PRIMARY KEY",[147,218,219],{"class":164},",\n",[147,221,223,226,228,230,233,236,239],{"class":149,"line":222},6,[147,224,225],{"class":164},"    student_name  ",[147,227,203],{"class":153},[147,229,206],{"class":164},[147,231,232],{"class":209},"100",[147,234,235],{"class":164},") ",[147,237,238],{"class":153},"NOT NULL",[147,240,219],{"class":164},[147,242,244,247,249,251,254,256],{"class":149,"line":243},7,[147,245,246],{"class":164},"    course        ",[147,248,203],{"class":153},[147,250,206],{"class":164},[147,252,253],{"class":209},"50",[147,255,213],{"class":164},[147,257,258],{"class":153},"NOT NULL\n",[147,260,262],{"class":149,"line":261},8,[147,263,264],{"class":164},");\n",[41,266,267,268,271,272,275],{},"Refresh the ",[59,269,270],{},"Schemas"," panel and check that ",[45,273,274],{},"enrollment_db → students"," is there.",[41,277,278,279,281,282,285],{},"The ",[45,280,216],{}," on ",[45,283,284],{},"student_id"," matters more than it looks. The app will check for duplicate IDs before inserting, but that check is for a friendly message. The key is what actually guarantees it.",[122,287,289],{"id":288},"step-2-create-the-javafx-project","Step 2: Create the JavaFX project",[41,291,292,293,296,297,300,301,304],{},"In IntelliJ, go to ",[59,294,295],{},"File → New → Project → JavaFX",", choose ",[59,298,299],{},"Maven"," as the build system, and click ",[59,302,303],{},"Create",".",[41,306,307,308,311,312,311,315,102,318,321],{},"IntelliJ generates four files: ",[45,309,310],{},"HelloApplication.java",", ",[45,313,314],{},"HelloController.java",[45,316,317],{},"hello-view.fxml",[45,319,320],{},"module-info.java",". You'll replace the contents of each one below.",[122,323,325],{"id":324},"step-3-add-the-mysql-driver","Step 3: Add the MySQL driver",[41,327,328,329,332,333,336],{},"JDBC is only an interface. To talk to MySQL specifically, you need its driver. Add this to the ",[45,330,331],{},"\u003Cdependencies>"," block in ",[45,334,335],{},"pom.xml",", then reload Maven:",[138,338,342],{"className":339,"code":340,"language":341,"meta":143,"style":143},"language-xml shiki shiki-themes github-dark-dimmed github-dark-dimmed","\u003Cdependency>\n    \u003CgroupId>com.mysql\u003C\u002FgroupId>\n    \u003CartifactId>mysql-connector-j\u003C\u002FartifactId>\n    \u003Cversion>8.4.0\u003C\u002Fversion>\n\u003C\u002Fdependency>\n","xml",[45,343,344,349,354,359,364],{"__ignoreMap":143},[147,345,346],{"class":149,"line":150},[147,347,348],{},"\u003Cdependency>\n",[147,350,351],{"class":149,"line":168},[147,352,353],{},"    \u003CgroupId>com.mysql\u003C\u002FgroupId>\n",[147,355,356],{"class":149,"line":177},[147,357,358],{},"    \u003CartifactId>mysql-connector-j\u003C\u002FartifactId>\n",[147,360,361],{"class":149,"line":183},[147,362,363],{},"    \u003Cversion>8.4.0\u003C\u002Fversion>\n",[147,365,366],{"class":149,"line":197},[147,367,368],{},"\u003C\u002Fdependency>\n",[122,370,372,373],{"id":371},"step-4-update-module-infojava","Step 4: Update ",[45,374,320],{},[138,376,380],{"className":377,"code":378,"language":379,"meta":143,"style":143},"language-java shiki shiki-themes github-dark-dimmed github-dark-dimmed","module com.example.demo {\n    requires javafx.controls;\n    requires javafx.fxml;\n    requires java.sql;                                   \u002F\u002F needed for JDBC\n\n    opens com.example.demo to javafx.fxml, javafx.base;  \u002F\u002F needed for FXML + TableView\n    exports com.example.demo;\n}\n","java",[45,381,382,387,392,397,402,406,411,416],{"__ignoreMap":143},[147,383,384],{"class":149,"line":150},[147,385,386],{},"module com.example.demo {\n",[147,388,389],{"class":149,"line":168},[147,390,391],{},"    requires javafx.controls;\n",[147,393,394],{"class":149,"line":177},[147,395,396],{},"    requires javafx.fxml;\n",[147,398,399],{"class":149,"line":183},[147,400,401],{},"    requires java.sql;                                   \u002F\u002F needed for JDBC\n",[147,403,404],{"class":149,"line":197},[147,405,180],{"emptyLinePlaceholder":24},[147,407,408],{"class":149,"line":222},[147,409,410],{},"    opens com.example.demo to javafx.fxml, javafx.base;  \u002F\u002F needed for FXML + TableView\n",[147,412,413],{"class":149,"line":243},[147,414,415],{},"    exports com.example.demo;\n",[147,417,418],{"class":149,"line":261},[147,419,420],{},"}\n",[422,423,424],"tip",{},[41,425,426,433,434,436,437,439],{},[59,427,428,429,432],{},"Why ",[45,430,431],{},"javafx.base","?"," ",[45,435,111],{}," reads each cell by calling your getters through reflection, and that reflection lives in ",[45,438,431],{},". Leave it out and the table still renders the right number of rows — with every cell empty, and no exception to tell you why.",[122,441,443],{"id":442},"step-5-design-the-ui","Step 5: Design the UI",[41,445,128,446,448,449,452],{},[45,447,317],{}," in Scene Builder (right-click the file → ",[59,450,451],{},"Open In SceneBuilder",") and lay it out visually, or paste this in directly:",[138,454,456],{"className":339,"code":455,"language":341,"meta":143,"style":143},"\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n\u003C?import javafx.geometry.Insets?>\n\u003C?import javafx.scene.control.*?>\n\u003C?import javafx.scene.layout.*?>\n\n\u003CVBox spacing=\"12.0\" xmlns=\"http:\u002F\u002Fjavafx.com\u002Fjavafx\u002F21\" xmlns:fx=\"http:\u002F\u002Fjavafx.com\u002Ffxml\u002F1\"\n      fx:controller=\"com.example.demo.HelloController\">\n   \u003Cpadding>\n      \u003CInsets top=\"15.0\" right=\"15.0\" bottom=\"15.0\" left=\"15.0\" \u002F>\n   \u003C\u002Fpadding>\n   \u003Cchildren>\n      \u003CLabel text=\"Student Course Enrollment System\" style=\"-fx-font-size: 18px; -fx-font-weight: bold;\" \u002F>\n\n      \u003CHBox spacing=\"10.0\">\n         \u003Cchildren>\n            \u003CTextField fx:id=\"idField\" promptText=\"Student ID\" \u002F>\n            \u003CTextField fx:id=\"nameField\" promptText=\"Student Name\" \u002F>\n            \u003CComboBox fx:id=\"courseBox\" prefWidth=\"160.0\" promptText=\"Select Course\" \u002F>\n            \u003CButton onAction=\"#enrollStudent\" text=\"Enroll Student\" \u002F>\n         \u003C\u002Fchildren>\n      \u003C\u002FHBox>\n\n      \u003CHBox spacing=\"10.0\">\n         \u003Cchildren>\n            \u003CButton onAction=\"#viewAll\" text=\"View All Members\" \u002F>\n            \u003CButton onAction=\"#deleteStudent\" text=\"Delete Member\" \u002F>\n         \u003C\u002Fchildren>\n      \u003C\u002FHBox>\n\n      \u003CHBox spacing=\"10.0\">\n         \u003Cchildren>\n            \u003CTextField fx:id=\"searchField\" promptText=\"Search by Student ID\" \u002F>\n            \u003CButton onAction=\"#searchStudent\" text=\"Search Enrollment\" \u002F>\n            \u003CLabel fx:id=\"resultLabel\" \u002F>\n         \u003C\u002Fchildren>\n      \u003C\u002FHBox>\n\n      \u003CTableView fx:id=\"table\" VBox.vgrow=\"ALWAYS\">\n         \u003Ccolumns>\n            \u003CTableColumn fx:id=\"idCol\" prefWidth=\"150.0\" text=\"Student ID\" \u002F>\n            \u003CTableColumn fx:id=\"nameCol\" prefWidth=\"300.0\" text=\"Student Name\" \u002F>\n            \u003CTableColumn fx:id=\"courseCol\" prefWidth=\"200.0\" text=\"Course\" \u002F>\n         \u003C\u002Fcolumns>\n      \u003C\u002FTableView>\n   \u003C\u002Fchildren>\n\u003C\u002FVBox>\n",[45,457,458,463,467,472,477,482,486,491,496,502,508,514,520,526,531,537,543,549,555,561,567,573,579,584,588,593,599,605,610,615,620,625,630,636,642,648,653,658,663,669,675,681,687,693,699,705,711],{"__ignoreMap":143},[147,459,460],{"class":149,"line":150},[147,461,462],{},"\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",[147,464,465],{"class":149,"line":168},[147,466,180],{"emptyLinePlaceholder":24},[147,468,469],{"class":149,"line":177},[147,470,471],{},"\u003C?import javafx.geometry.Insets?>\n",[147,473,474],{"class":149,"line":183},[147,475,476],{},"\u003C?import javafx.scene.control.*?>\n",[147,478,479],{"class":149,"line":197},[147,480,481],{},"\u003C?import javafx.scene.layout.*?>\n",[147,483,484],{"class":149,"line":222},[147,485,180],{"emptyLinePlaceholder":24},[147,487,488],{"class":149,"line":243},[147,489,490],{},"\u003CVBox spacing=\"12.0\" xmlns=\"http:\u002F\u002Fjavafx.com\u002Fjavafx\u002F21\" xmlns:fx=\"http:\u002F\u002Fjavafx.com\u002Ffxml\u002F1\"\n",[147,492,493],{"class":149,"line":261},[147,494,495],{},"      fx:controller=\"com.example.demo.HelloController\">\n",[147,497,499],{"class":149,"line":498},9,[147,500,501],{},"   \u003Cpadding>\n",[147,503,505],{"class":149,"line":504},10,[147,506,507],{},"      \u003CInsets top=\"15.0\" right=\"15.0\" bottom=\"15.0\" left=\"15.0\" \u002F>\n",[147,509,511],{"class":149,"line":510},11,[147,512,513],{},"   \u003C\u002Fpadding>\n",[147,515,517],{"class":149,"line":516},12,[147,518,519],{},"   \u003Cchildren>\n",[147,521,523],{"class":149,"line":522},13,[147,524,525],{},"      \u003CLabel text=\"Student Course Enrollment System\" style=\"-fx-font-size: 18px; -fx-font-weight: bold;\" \u002F>\n",[147,527,529],{"class":149,"line":528},14,[147,530,180],{"emptyLinePlaceholder":24},[147,532,534],{"class":149,"line":533},15,[147,535,536],{},"      \u003CHBox spacing=\"10.0\">\n",[147,538,540],{"class":149,"line":539},16,[147,541,542],{},"         \u003Cchildren>\n",[147,544,546],{"class":149,"line":545},17,[147,547,548],{},"            \u003CTextField fx:id=\"idField\" promptText=\"Student ID\" \u002F>\n",[147,550,552],{"class":149,"line":551},18,[147,553,554],{},"            \u003CTextField fx:id=\"nameField\" promptText=\"Student Name\" \u002F>\n",[147,556,558],{"class":149,"line":557},19,[147,559,560],{},"            \u003CComboBox fx:id=\"courseBox\" prefWidth=\"160.0\" promptText=\"Select Course\" \u002F>\n",[147,562,564],{"class":149,"line":563},20,[147,565,566],{},"            \u003CButton onAction=\"#enrollStudent\" text=\"Enroll Student\" \u002F>\n",[147,568,570],{"class":149,"line":569},21,[147,571,572],{},"         \u003C\u002Fchildren>\n",[147,574,576],{"class":149,"line":575},22,[147,577,578],{},"      \u003C\u002FHBox>\n",[147,580,582],{"class":149,"line":581},23,[147,583,180],{"emptyLinePlaceholder":24},[147,585,586],{"class":149,"line":5},[147,587,536],{},[147,589,591],{"class":149,"line":590},25,[147,592,542],{},[147,594,596],{"class":149,"line":595},26,[147,597,598],{},"            \u003CButton onAction=\"#viewAll\" text=\"View All Members\" \u002F>\n",[147,600,602],{"class":149,"line":601},27,[147,603,604],{},"            \u003CButton onAction=\"#deleteStudent\" text=\"Delete Member\" \u002F>\n",[147,606,608],{"class":149,"line":607},28,[147,609,572],{},[147,611,613],{"class":149,"line":612},29,[147,614,578],{},[147,616,618],{"class":149,"line":617},30,[147,619,180],{"emptyLinePlaceholder":24},[147,621,623],{"class":149,"line":622},31,[147,624,536],{},[147,626,628],{"class":149,"line":627},32,[147,629,542],{},[147,631,633],{"class":149,"line":632},33,[147,634,635],{},"            \u003CTextField fx:id=\"searchField\" promptText=\"Search by Student ID\" \u002F>\n",[147,637,639],{"class":149,"line":638},34,[147,640,641],{},"            \u003CButton onAction=\"#searchStudent\" text=\"Search Enrollment\" \u002F>\n",[147,643,645],{"class":149,"line":644},35,[147,646,647],{},"            \u003CLabel fx:id=\"resultLabel\" \u002F>\n",[147,649,651],{"class":149,"line":650},36,[147,652,572],{},[147,654,656],{"class":149,"line":655},37,[147,657,578],{},[147,659,661],{"class":149,"line":660},38,[147,662,180],{"emptyLinePlaceholder":24},[147,664,666],{"class":149,"line":665},39,[147,667,668],{},"      \u003CTableView fx:id=\"table\" VBox.vgrow=\"ALWAYS\">\n",[147,670,672],{"class":149,"line":671},40,[147,673,674],{},"         \u003Ccolumns>\n",[147,676,678],{"class":149,"line":677},41,[147,679,680],{},"            \u003CTableColumn fx:id=\"idCol\" prefWidth=\"150.0\" text=\"Student ID\" \u002F>\n",[147,682,684],{"class":149,"line":683},42,[147,685,686],{},"            \u003CTableColumn fx:id=\"nameCol\" prefWidth=\"300.0\" text=\"Student Name\" \u002F>\n",[147,688,690],{"class":149,"line":689},43,[147,691,692],{},"            \u003CTableColumn fx:id=\"courseCol\" prefWidth=\"200.0\" text=\"Course\" \u002F>\n",[147,694,696],{"class":149,"line":695},44,[147,697,698],{},"         \u003C\u002Fcolumns>\n",[147,700,702],{"class":149,"line":701},45,[147,703,704],{},"      \u003C\u002FTableView>\n",[147,706,708],{"class":149,"line":707},46,[147,709,710],{},"   \u003C\u002Fchildren>\n",[147,712,714],{"class":149,"line":713},47,[147,715,716],{},"\u003C\u002FVBox>\n",[41,718,719,720,730],{},"One rule holds the whole file together: ",[59,721,722,723,725,726,729],{},"every ",[45,724,47],{}," must match a field name in the controller, and every ",[45,727,728],{},"onAction=\"#method\""," must match a method name."," FXML connects to Java by name alone, so a single typo is enough to break it.",[122,732,734],{"id":733},"step-6-write-the-code","Step 6: Write the code",[41,736,737],{},"Three classes: a model for one row, a controller for all the logic, and the application that starts it.",[739,740,742,743],"h3",{"id":741},"the-model-studentjava","The model: ",[45,744,745],{},"Student.java",[138,747,749],{"className":377,"code":748,"language":379,"meta":143,"style":143},"package com.example.demo;\n\n\u002F\u002F One Student object = one row in the database\npublic class Student {\n    private final String id;\n    private final String name;\n    private final String course;\n\n    public Student(String id, String name, String course) {\n        this.id = id;\n        this.name = name;\n        this.course = course;\n    }\n\n    \u002F\u002F Getter names must match PropertyValueFactory names (\"id\" -> getId)\n    public String getId()     { return id; }\n    public String getName()   { return name; }\n    public String getCourse() { return course; }\n}\n",[45,750,751,756,760,765,770,775,780,785,789,794,799,804,809,814,818,823,828,833,838],{"__ignoreMap":143},[147,752,753],{"class":149,"line":150},[147,754,755],{},"package com.example.demo;\n",[147,757,758],{"class":149,"line":168},[147,759,180],{"emptyLinePlaceholder":24},[147,761,762],{"class":149,"line":177},[147,763,764],{},"\u002F\u002F One Student object = one row in the database\n",[147,766,767],{"class":149,"line":183},[147,768,769],{},"public class Student {\n",[147,771,772],{"class":149,"line":197},[147,773,774],{},"    private final String id;\n",[147,776,777],{"class":149,"line":222},[147,778,779],{},"    private final String name;\n",[147,781,782],{"class":149,"line":243},[147,783,784],{},"    private final String course;\n",[147,786,787],{"class":149,"line":261},[147,788,180],{"emptyLinePlaceholder":24},[147,790,791],{"class":149,"line":498},[147,792,793],{},"    public Student(String id, String name, String course) {\n",[147,795,796],{"class":149,"line":504},[147,797,798],{},"        this.id = id;\n",[147,800,801],{"class":149,"line":510},[147,802,803],{},"        this.name = name;\n",[147,805,806],{"class":149,"line":516},[147,807,808],{},"        this.course = course;\n",[147,810,811],{"class":149,"line":522},[147,812,813],{},"    }\n",[147,815,816],{"class":149,"line":528},[147,817,180],{"emptyLinePlaceholder":24},[147,819,820],{"class":149,"line":533},[147,821,822],{},"    \u002F\u002F Getter names must match PropertyValueFactory names (\"id\" -> getId)\n",[147,824,825],{"class":149,"line":539},[147,826,827],{},"    public String getId()     { return id; }\n",[147,829,830],{"class":149,"line":545},[147,831,832],{},"    public String getName()   { return name; }\n",[147,834,835],{"class":149,"line":551},[147,836,837],{},"    public String getCourse() { return course; }\n",[147,839,840],{"class":149,"line":557},[147,841,420],{},[41,843,844,845,848,849,304],{},"The getter names aren't a style choice. ",[45,846,847],{},"PropertyValueFactory\u003C>(\"id\")"," literally looks for a public method called ",[45,850,851],{},"getId",[739,853,855,856],{"id":854},"the-controller-hellocontrollerjava","The controller: ",[45,857,314],{},[41,859,860,861,864],{},"This is where everything happens. Change ",[45,862,863],{},"PASS"," to your MySQL root password.",[138,866,868],{"className":377,"code":867,"language":379,"meta":143,"style":143},"package com.example.demo;\n\nimport javafx.collections.FXCollections;\nimport javafx.collections.ObservableList;\nimport javafx.fxml.FXML;\nimport javafx.scene.control.*;\nimport javafx.scene.control.cell.PropertyValueFactory;\n\nimport java.sql.*;\n\npublic class HelloController {\n\n    \u002F\u002F ---------- STEP A: Database connection ----------\n    private static final String URL  = \"jdbc:mysql:\u002F\u002Flocalhost:3306\u002Fenrollment_db\";\n    private static final String USER = \"root\";\n    private static final String PASS = \"your_password\";   \u002F\u002F your MySQL root password\n\n    private Connection connect() throws SQLException {\n        return DriverManager.getConnection(URL, USER, PASS);\n    }\n\n    \u002F\u002F ---------- STEP B: fx:id fields (names must match the FXML exactly) ----------\n    @FXML private TextField idField;\n    @FXML private TextField nameField;\n    @FXML private ComboBox\u003CString> courseBox;\n    @FXML private TextField searchField;\n    @FXML private Label resultLabel;\n    @FXML private TableView\u003CStudent> table;\n    @FXML private TableColumn\u003CStudent, String> idCol;\n    @FXML private TableColumn\u003CStudent, String> nameCol;\n    @FXML private TableColumn\u003CStudent, String> courseCol;\n\n    \u002F\u002F Runs automatically once after the FXML is loaded\n    @FXML\n    public void initialize() {\n        courseBox.getItems().addAll(\"Java\", \"Python\", \"Data Structures\");\n        idCol.setCellValueFactory(new PropertyValueFactory\u003C>(\"id\"));\n        nameCol.setCellValueFactory(new PropertyValueFactory\u003C>(\"name\"));\n        courseCol.setCellValueFactory(new PropertyValueFactory\u003C>(\"course\"));\n        viewAll();   \u002F\u002F load data when the app opens\n    }\n\n    \u002F\u002F ---------- STEP C: Requirement 1 — Enroll Student ----------\n    @FXML\n    private void enrollStudent() {\n        String id = idField.getText().trim();\n        String name = nameField.getText().trim();\n        String course = courseBox.getValue();\n\n        \u002F\u002F Validation 1: all fields must be filled\n        if (id.isEmpty() || name.isEmpty() || course == null) {\n            showAlert(\"All fields must be filled!\");\n            return;\n        }\n\n        try (Connection con = connect()) {\n            \u002F\u002F Validation 2: unique Student ID\n            PreparedStatement check = con.prepareStatement(\n                    \"SELECT student_id FROM students WHERE student_id = ?\");\n            check.setString(1, id);\n            if (check.executeQuery().next()) {\n                showAlert(\"Student ID already exists!\");\n                return;\n            }\n\n            PreparedStatement ps = con.prepareStatement(\n                    \"INSERT INTO students (student_id, student_name, course) VALUES (?, ?, ?)\");\n            ps.setString(1, id);\n            ps.setString(2, name);\n            ps.setString(3, course);\n            ps.executeUpdate();\n\n            showAlert(\"Student enrolled successfully!\");\n            idField.clear();\n            nameField.clear();\n            courseBox.setValue(null);\n            viewAll();\n        } catch (SQLException ex) {\n            showAlert(\"Database error: \" + ex.getMessage());\n        }\n    }\n\n    \u002F\u002F ---------- STEP D: Requirement 2 — View All Members ----------\n    @FXML\n    private void viewAll() {\n        ObservableList\u003CStudent> list = FXCollections.observableArrayList();\n        try (Connection con = connect()) {\n            ResultSet rs = con.createStatement().executeQuery(\"SELECT * FROM students\");\n            while (rs.next()) {\n                list.add(new Student(\n                        rs.getString(\"student_id\"),\n                        rs.getString(\"student_name\"),\n                        rs.getString(\"course\")));\n            }\n            table.setItems(list);\n        } catch (SQLException ex) {\n            showAlert(\"Database error: \" + ex.getMessage());\n        }\n    }\n\n    \u002F\u002F ---------- STEP E: Requirement 3 — Delete Member ----------\n    @FXML\n    private void deleteStudent() {\n        String id = idField.getText().trim();\n\n        \u002F\u002F Bonus: if the ID field is empty, use the selected table row\n        Student selected = table.getSelectionModel().getSelectedItem();\n        if (id.isEmpty() && selected != null) id = selected.getId();\n\n        if (id.isEmpty()) {\n            showAlert(\"Enter a Student ID (or select a row) to delete.\");\n            return;\n        }\n        try (Connection con = connect()) {\n            PreparedStatement ps = con.prepareStatement(\n                    \"DELETE FROM students WHERE student_id = ?\");\n            ps.setString(1, id);\n            int rows = ps.executeUpdate();\n            showAlert(rows > 0 ? \"Member deleted!\" : \"No member found with this ID.\");\n            idField.clear();\n            viewAll();\n        } catch (SQLException ex) {\n            showAlert(\"Database error: \" + ex.getMessage());\n        }\n    }\n\n    \u002F\u002F ---------- STEP F: Requirement 4 — Search Enrollment ----------\n    @FXML\n    private void searchStudent() {\n        String id = searchField.getText().trim();\n        if (id.isEmpty()) {\n            showAlert(\"Enter a Student ID to search.\");\n            return;\n        }\n        try (Connection con = connect()) {\n            PreparedStatement ps = con.prepareStatement(\n                    \"SELECT student_name, course FROM students WHERE student_id = ?\");\n            ps.setString(1, id);\n            ResultSet rs = ps.executeQuery();\n            if (rs.next()) {\n                resultLabel.setText(\"Name: \" + rs.getString(\"student_name\")\n                        + \"  |  Course: \" + rs.getString(\"course\"));\n            } else {\n                resultLabel.setText(\"\");\n                showAlert(\"Student ID does not exist!\");\n            }\n        } catch (SQLException ex) {\n            showAlert(\"Database error: \" + ex.getMessage());\n        }\n    }\n\n    \u002F\u002F ---------- Helper ----------\n    private void showAlert(String msg) {\n        Alert a = new Alert(Alert.AlertType.INFORMATION);\n        a.setHeaderText(null);\n        a.setContentText(msg);\n        a.showAndWait();\n    }\n}\n",[45,869,870,874,878,883,888,893,898,903,907,912,916,921,925,930,935,940,945,949,954,959,963,967,972,977,982,987,992,997,1002,1007,1012,1017,1021,1026,1031,1036,1041,1046,1051,1056,1061,1065,1069,1074,1078,1083,1088,1093,1099,1104,1110,1116,1122,1128,1134,1139,1145,1151,1157,1163,1169,1175,1181,1187,1193,1198,1204,1210,1216,1222,1228,1234,1239,1245,1251,1257,1263,1269,1275,1281,1286,1291,1296,1302,1307,1313,1319,1324,1330,1336,1342,1348,1354,1360,1365,1371,1376,1381,1386,1391,1396,1402,1407,1413,1418,1423,1429,1435,1441,1446,1452,1458,1463,1468,1473,1478,1484,1489,1495,1501,1506,1511,1516,1521,1526,1531,1536,1542,1547,1553,1559,1564,1570,1575,1580,1585,1590,1596,1601,1607,1613,1619,1625,1631,1637,1643,1648,1653,1658,1663,1668,1673,1679,1685,1691,1697,1703,1709,1714],{"__ignoreMap":143},[147,871,872],{"class":149,"line":150},[147,873,755],{},[147,875,876],{"class":149,"line":168},[147,877,180],{"emptyLinePlaceholder":24},[147,879,880],{"class":149,"line":177},[147,881,882],{},"import javafx.collections.FXCollections;\n",[147,884,885],{"class":149,"line":183},[147,886,887],{},"import javafx.collections.ObservableList;\n",[147,889,890],{"class":149,"line":197},[147,891,892],{},"import javafx.fxml.FXML;\n",[147,894,895],{"class":149,"line":222},[147,896,897],{},"import javafx.scene.control.*;\n",[147,899,900],{"class":149,"line":243},[147,901,902],{},"import javafx.scene.control.cell.PropertyValueFactory;\n",[147,904,905],{"class":149,"line":261},[147,906,180],{"emptyLinePlaceholder":24},[147,908,909],{"class":149,"line":498},[147,910,911],{},"import java.sql.*;\n",[147,913,914],{"class":149,"line":504},[147,915,180],{"emptyLinePlaceholder":24},[147,917,918],{"class":149,"line":510},[147,919,920],{},"public class HelloController {\n",[147,922,923],{"class":149,"line":516},[147,924,180],{"emptyLinePlaceholder":24},[147,926,927],{"class":149,"line":522},[147,928,929],{},"    \u002F\u002F ---------- STEP A: Database connection ----------\n",[147,931,932],{"class":149,"line":528},[147,933,934],{},"    private static final String URL  = \"jdbc:mysql:\u002F\u002Flocalhost:3306\u002Fenrollment_db\";\n",[147,936,937],{"class":149,"line":533},[147,938,939],{},"    private static final String USER = \"root\";\n",[147,941,942],{"class":149,"line":539},[147,943,944],{},"    private static final String PASS = \"your_password\";   \u002F\u002F your MySQL root password\n",[147,946,947],{"class":149,"line":545},[147,948,180],{"emptyLinePlaceholder":24},[147,950,951],{"class":149,"line":551},[147,952,953],{},"    private Connection connect() throws SQLException {\n",[147,955,956],{"class":149,"line":557},[147,957,958],{},"        return DriverManager.getConnection(URL, USER, PASS);\n",[147,960,961],{"class":149,"line":563},[147,962,813],{},[147,964,965],{"class":149,"line":569},[147,966,180],{"emptyLinePlaceholder":24},[147,968,969],{"class":149,"line":575},[147,970,971],{},"    \u002F\u002F ---------- STEP B: fx:id fields (names must match the FXML exactly) ----------\n",[147,973,974],{"class":149,"line":581},[147,975,976],{},"    @FXML private TextField idField;\n",[147,978,979],{"class":149,"line":5},[147,980,981],{},"    @FXML private TextField nameField;\n",[147,983,984],{"class":149,"line":590},[147,985,986],{},"    @FXML private ComboBox\u003CString> courseBox;\n",[147,988,989],{"class":149,"line":595},[147,990,991],{},"    @FXML private TextField searchField;\n",[147,993,994],{"class":149,"line":601},[147,995,996],{},"    @FXML private Label resultLabel;\n",[147,998,999],{"class":149,"line":607},[147,1000,1001],{},"    @FXML private TableView\u003CStudent> table;\n",[147,1003,1004],{"class":149,"line":612},[147,1005,1006],{},"    @FXML private TableColumn\u003CStudent, String> idCol;\n",[147,1008,1009],{"class":149,"line":617},[147,1010,1011],{},"    @FXML private TableColumn\u003CStudent, String> nameCol;\n",[147,1013,1014],{"class":149,"line":622},[147,1015,1016],{},"    @FXML private TableColumn\u003CStudent, String> courseCol;\n",[147,1018,1019],{"class":149,"line":627},[147,1020,180],{"emptyLinePlaceholder":24},[147,1022,1023],{"class":149,"line":632},[147,1024,1025],{},"    \u002F\u002F Runs automatically once after the FXML is loaded\n",[147,1027,1028],{"class":149,"line":638},[147,1029,1030],{},"    @FXML\n",[147,1032,1033],{"class":149,"line":644},[147,1034,1035],{},"    public void initialize() {\n",[147,1037,1038],{"class":149,"line":650},[147,1039,1040],{},"        courseBox.getItems().addAll(\"Java\", \"Python\", \"Data Structures\");\n",[147,1042,1043],{"class":149,"line":655},[147,1044,1045],{},"        idCol.setCellValueFactory(new PropertyValueFactory\u003C>(\"id\"));\n",[147,1047,1048],{"class":149,"line":660},[147,1049,1050],{},"        nameCol.setCellValueFactory(new PropertyValueFactory\u003C>(\"name\"));\n",[147,1052,1053],{"class":149,"line":665},[147,1054,1055],{},"        courseCol.setCellValueFactory(new PropertyValueFactory\u003C>(\"course\"));\n",[147,1057,1058],{"class":149,"line":671},[147,1059,1060],{},"        viewAll();   \u002F\u002F load data when the app opens\n",[147,1062,1063],{"class":149,"line":677},[147,1064,813],{},[147,1066,1067],{"class":149,"line":683},[147,1068,180],{"emptyLinePlaceholder":24},[147,1070,1071],{"class":149,"line":689},[147,1072,1073],{},"    \u002F\u002F ---------- STEP C: Requirement 1 — Enroll Student ----------\n",[147,1075,1076],{"class":149,"line":695},[147,1077,1030],{},[147,1079,1080],{"class":149,"line":701},[147,1081,1082],{},"    private void enrollStudent() {\n",[147,1084,1085],{"class":149,"line":707},[147,1086,1087],{},"        String id = idField.getText().trim();\n",[147,1089,1090],{"class":149,"line":713},[147,1091,1092],{},"        String name = nameField.getText().trim();\n",[147,1094,1096],{"class":149,"line":1095},48,[147,1097,1098],{},"        String course = courseBox.getValue();\n",[147,1100,1102],{"class":149,"line":1101},49,[147,1103,180],{"emptyLinePlaceholder":24},[147,1105,1107],{"class":149,"line":1106},50,[147,1108,1109],{},"        \u002F\u002F Validation 1: all fields must be filled\n",[147,1111,1113],{"class":149,"line":1112},51,[147,1114,1115],{},"        if (id.isEmpty() || name.isEmpty() || course == null) {\n",[147,1117,1119],{"class":149,"line":1118},52,[147,1120,1121],{},"            showAlert(\"All fields must be filled!\");\n",[147,1123,1125],{"class":149,"line":1124},53,[147,1126,1127],{},"            return;\n",[147,1129,1131],{"class":149,"line":1130},54,[147,1132,1133],{},"        }\n",[147,1135,1137],{"class":149,"line":1136},55,[147,1138,180],{"emptyLinePlaceholder":24},[147,1140,1142],{"class":149,"line":1141},56,[147,1143,1144],{},"        try (Connection con = connect()) {\n",[147,1146,1148],{"class":149,"line":1147},57,[147,1149,1150],{},"            \u002F\u002F Validation 2: unique Student ID\n",[147,1152,1154],{"class":149,"line":1153},58,[147,1155,1156],{},"            PreparedStatement check = con.prepareStatement(\n",[147,1158,1160],{"class":149,"line":1159},59,[147,1161,1162],{},"                    \"SELECT student_id FROM students WHERE student_id = ?\");\n",[147,1164,1166],{"class":149,"line":1165},60,[147,1167,1168],{},"            check.setString(1, id);\n",[147,1170,1172],{"class":149,"line":1171},61,[147,1173,1174],{},"            if (check.executeQuery().next()) {\n",[147,1176,1178],{"class":149,"line":1177},62,[147,1179,1180],{},"                showAlert(\"Student ID already exists!\");\n",[147,1182,1184],{"class":149,"line":1183},63,[147,1185,1186],{},"                return;\n",[147,1188,1190],{"class":149,"line":1189},64,[147,1191,1192],{},"            }\n",[147,1194,1196],{"class":149,"line":1195},65,[147,1197,180],{"emptyLinePlaceholder":24},[147,1199,1201],{"class":149,"line":1200},66,[147,1202,1203],{},"            PreparedStatement ps = con.prepareStatement(\n",[147,1205,1207],{"class":149,"line":1206},67,[147,1208,1209],{},"                    \"INSERT INTO students (student_id, student_name, course) VALUES (?, ?, ?)\");\n",[147,1211,1213],{"class":149,"line":1212},68,[147,1214,1215],{},"            ps.setString(1, id);\n",[147,1217,1219],{"class":149,"line":1218},69,[147,1220,1221],{},"            ps.setString(2, name);\n",[147,1223,1225],{"class":149,"line":1224},70,[147,1226,1227],{},"            ps.setString(3, course);\n",[147,1229,1231],{"class":149,"line":1230},71,[147,1232,1233],{},"            ps.executeUpdate();\n",[147,1235,1237],{"class":149,"line":1236},72,[147,1238,180],{"emptyLinePlaceholder":24},[147,1240,1242],{"class":149,"line":1241},73,[147,1243,1244],{},"            showAlert(\"Student enrolled successfully!\");\n",[147,1246,1248],{"class":149,"line":1247},74,[147,1249,1250],{},"            idField.clear();\n",[147,1252,1254],{"class":149,"line":1253},75,[147,1255,1256],{},"            nameField.clear();\n",[147,1258,1260],{"class":149,"line":1259},76,[147,1261,1262],{},"            courseBox.setValue(null);\n",[147,1264,1266],{"class":149,"line":1265},77,[147,1267,1268],{},"            viewAll();\n",[147,1270,1272],{"class":149,"line":1271},78,[147,1273,1274],{},"        } catch (SQLException ex) {\n",[147,1276,1278],{"class":149,"line":1277},79,[147,1279,1280],{},"            showAlert(\"Database error: \" + ex.getMessage());\n",[147,1282,1284],{"class":149,"line":1283},80,[147,1285,1133],{},[147,1287,1289],{"class":149,"line":1288},81,[147,1290,813],{},[147,1292,1294],{"class":149,"line":1293},82,[147,1295,180],{"emptyLinePlaceholder":24},[147,1297,1299],{"class":149,"line":1298},83,[147,1300,1301],{},"    \u002F\u002F ---------- STEP D: Requirement 2 — View All Members ----------\n",[147,1303,1305],{"class":149,"line":1304},84,[147,1306,1030],{},[147,1308,1310],{"class":149,"line":1309},85,[147,1311,1312],{},"    private void viewAll() {\n",[147,1314,1316],{"class":149,"line":1315},86,[147,1317,1318],{},"        ObservableList\u003CStudent> list = FXCollections.observableArrayList();\n",[147,1320,1322],{"class":149,"line":1321},87,[147,1323,1144],{},[147,1325,1327],{"class":149,"line":1326},88,[147,1328,1329],{},"            ResultSet rs = con.createStatement().executeQuery(\"SELECT * FROM students\");\n",[147,1331,1333],{"class":149,"line":1332},89,[147,1334,1335],{},"            while (rs.next()) {\n",[147,1337,1339],{"class":149,"line":1338},90,[147,1340,1341],{},"                list.add(new Student(\n",[147,1343,1345],{"class":149,"line":1344},91,[147,1346,1347],{},"                        rs.getString(\"student_id\"),\n",[147,1349,1351],{"class":149,"line":1350},92,[147,1352,1353],{},"                        rs.getString(\"student_name\"),\n",[147,1355,1357],{"class":149,"line":1356},93,[147,1358,1359],{},"                        rs.getString(\"course\")));\n",[147,1361,1363],{"class":149,"line":1362},94,[147,1364,1192],{},[147,1366,1368],{"class":149,"line":1367},95,[147,1369,1370],{},"            table.setItems(list);\n",[147,1372,1374],{"class":149,"line":1373},96,[147,1375,1274],{},[147,1377,1379],{"class":149,"line":1378},97,[147,1380,1280],{},[147,1382,1384],{"class":149,"line":1383},98,[147,1385,1133],{},[147,1387,1389],{"class":149,"line":1388},99,[147,1390,813],{},[147,1392,1394],{"class":149,"line":1393},100,[147,1395,180],{"emptyLinePlaceholder":24},[147,1397,1399],{"class":149,"line":1398},101,[147,1400,1401],{},"    \u002F\u002F ---------- STEP E: Requirement 3 — Delete Member ----------\n",[147,1403,1405],{"class":149,"line":1404},102,[147,1406,1030],{},[147,1408,1410],{"class":149,"line":1409},103,[147,1411,1412],{},"    private void deleteStudent() {\n",[147,1414,1416],{"class":149,"line":1415},104,[147,1417,1087],{},[147,1419,1421],{"class":149,"line":1420},105,[147,1422,180],{"emptyLinePlaceholder":24},[147,1424,1426],{"class":149,"line":1425},106,[147,1427,1428],{},"        \u002F\u002F Bonus: if the ID field is empty, use the selected table row\n",[147,1430,1432],{"class":149,"line":1431},107,[147,1433,1434],{},"        Student selected = table.getSelectionModel().getSelectedItem();\n",[147,1436,1438],{"class":149,"line":1437},108,[147,1439,1440],{},"        if (id.isEmpty() && selected != null) id = selected.getId();\n",[147,1442,1444],{"class":149,"line":1443},109,[147,1445,180],{"emptyLinePlaceholder":24},[147,1447,1449],{"class":149,"line":1448},110,[147,1450,1451],{},"        if (id.isEmpty()) {\n",[147,1453,1455],{"class":149,"line":1454},111,[147,1456,1457],{},"            showAlert(\"Enter a Student ID (or select a row) to delete.\");\n",[147,1459,1461],{"class":149,"line":1460},112,[147,1462,1127],{},[147,1464,1466],{"class":149,"line":1465},113,[147,1467,1133],{},[147,1469,1471],{"class":149,"line":1470},114,[147,1472,1144],{},[147,1474,1476],{"class":149,"line":1475},115,[147,1477,1203],{},[147,1479,1481],{"class":149,"line":1480},116,[147,1482,1483],{},"                    \"DELETE FROM students WHERE student_id = ?\");\n",[147,1485,1487],{"class":149,"line":1486},117,[147,1488,1215],{},[147,1490,1492],{"class":149,"line":1491},118,[147,1493,1494],{},"            int rows = ps.executeUpdate();\n",[147,1496,1498],{"class":149,"line":1497},119,[147,1499,1500],{},"            showAlert(rows > 0 ? \"Member deleted!\" : \"No member found with this ID.\");\n",[147,1502,1504],{"class":149,"line":1503},120,[147,1505,1250],{},[147,1507,1509],{"class":149,"line":1508},121,[147,1510,1268],{},[147,1512,1514],{"class":149,"line":1513},122,[147,1515,1274],{},[147,1517,1519],{"class":149,"line":1518},123,[147,1520,1280],{},[147,1522,1524],{"class":149,"line":1523},124,[147,1525,1133],{},[147,1527,1529],{"class":149,"line":1528},125,[147,1530,813],{},[147,1532,1534],{"class":149,"line":1533},126,[147,1535,180],{"emptyLinePlaceholder":24},[147,1537,1539],{"class":149,"line":1538},127,[147,1540,1541],{},"    \u002F\u002F ---------- STEP F: Requirement 4 — Search Enrollment ----------\n",[147,1543,1545],{"class":149,"line":1544},128,[147,1546,1030],{},[147,1548,1550],{"class":149,"line":1549},129,[147,1551,1552],{},"    private void searchStudent() {\n",[147,1554,1556],{"class":149,"line":1555},130,[147,1557,1558],{},"        String id = searchField.getText().trim();\n",[147,1560,1562],{"class":149,"line":1561},131,[147,1563,1451],{},[147,1565,1567],{"class":149,"line":1566},132,[147,1568,1569],{},"            showAlert(\"Enter a Student ID to search.\");\n",[147,1571,1573],{"class":149,"line":1572},133,[147,1574,1127],{},[147,1576,1578],{"class":149,"line":1577},134,[147,1579,1133],{},[147,1581,1583],{"class":149,"line":1582},135,[147,1584,1144],{},[147,1586,1588],{"class":149,"line":1587},136,[147,1589,1203],{},[147,1591,1593],{"class":149,"line":1592},137,[147,1594,1595],{},"                    \"SELECT student_name, course FROM students WHERE student_id = ?\");\n",[147,1597,1599],{"class":149,"line":1598},138,[147,1600,1215],{},[147,1602,1604],{"class":149,"line":1603},139,[147,1605,1606],{},"            ResultSet rs = ps.executeQuery();\n",[147,1608,1610],{"class":149,"line":1609},140,[147,1611,1612],{},"            if (rs.next()) {\n",[147,1614,1616],{"class":149,"line":1615},141,[147,1617,1618],{},"                resultLabel.setText(\"Name: \" + rs.getString(\"student_name\")\n",[147,1620,1622],{"class":149,"line":1621},142,[147,1623,1624],{},"                        + \"  |  Course: \" + rs.getString(\"course\"));\n",[147,1626,1628],{"class":149,"line":1627},143,[147,1629,1630],{},"            } else {\n",[147,1632,1634],{"class":149,"line":1633},144,[147,1635,1636],{},"                resultLabel.setText(\"\");\n",[147,1638,1640],{"class":149,"line":1639},145,[147,1641,1642],{},"                showAlert(\"Student ID does not exist!\");\n",[147,1644,1646],{"class":149,"line":1645},146,[147,1647,1192],{},[147,1649,1651],{"class":149,"line":1650},147,[147,1652,1274],{},[147,1654,1656],{"class":149,"line":1655},148,[147,1657,1280],{},[147,1659,1661],{"class":149,"line":1660},149,[147,1662,1133],{},[147,1664,1666],{"class":149,"line":1665},150,[147,1667,813],{},[147,1669,1671],{"class":149,"line":1670},151,[147,1672,180],{"emptyLinePlaceholder":24},[147,1674,1676],{"class":149,"line":1675},152,[147,1677,1678],{},"    \u002F\u002F ---------- Helper ----------\n",[147,1680,1682],{"class":149,"line":1681},153,[147,1683,1684],{},"    private void showAlert(String msg) {\n",[147,1686,1688],{"class":149,"line":1687},154,[147,1689,1690],{},"        Alert a = new Alert(Alert.AlertType.INFORMATION);\n",[147,1692,1694],{"class":149,"line":1693},155,[147,1695,1696],{},"        a.setHeaderText(null);\n",[147,1698,1700],{"class":149,"line":1699},156,[147,1701,1702],{},"        a.setContentText(msg);\n",[147,1704,1706],{"class":149,"line":1705},157,[147,1707,1708],{},"        a.showAndWait();\n",[147,1710,1712],{"class":149,"line":1711},158,[147,1713,813],{},[147,1715,1717],{"class":149,"line":1716},159,[147,1718,420],{},[41,1720,1721],{},"A few things worth noticing:",[84,1723,1724,1730,1747,1756],{},[56,1725,1726,1729],{},[59,1727,1728],{},"Validation happens in two layers."," Empty fields are caught in Java before a connection is even opened. The duplicate ID needs the database, so it is checked there — with the primary key from step 1 as the backstop.",[56,1731,1732,1738,1739,1742,1743,1746],{},[59,1733,1734,1735,1737],{},"Every query uses ",[45,1736,432],{}," placeholders."," Values are bound with ",[45,1740,1741],{},"setString",", never glued into the SQL string, so a name like ",[45,1744,1745],{},"O'Brien"," works and a malicious one can't rewrite the query.",[56,1748,1749,1755],{},[59,1750,1751,1754],{},[45,1752,1753],{},"try (Connection con = connect())"," closes the connection for you",", along with every statement and result set opened on it, even when something throws.",[56,1757,1758,1761],{},[59,1759,1760],{},"Delete has a quiet convenience:"," leave the ID field empty and it deletes whichever table row is selected.",[1763,1764,1765],"warning",{},[41,1766,1767,1768,1771],{},"A password in source code is fine for a local tutorial and nowhere else. Before this goes near a shared repository, move the credentials out — an environment variable read with ",[45,1769,1770],{},"System.getenv(...)"," is the simplest step up.",[739,1773,1775,1776],{"id":1774},"the-entry-point-helloapplicationjava","The entry point: ",[45,1777,310],{},[138,1779,1781],{"className":377,"code":1780,"language":379,"meta":143,"style":143},"package com.example.demo;\n\nimport javafx.application.Application;\nimport javafx.fxml.FXMLLoader;\nimport javafx.scene.Scene;\nimport javafx.stage.Stage;\n\npublic class HelloApplication extends Application {\n    @Override\n    public void start(Stage stage) throws Exception {\n        FXMLLoader loader = new FXMLLoader(HelloApplication.class.getResource(\"hello-view.fxml\"));\n        Scene scene = new Scene(loader.load(), 800, 500);\n        stage.setTitle(\"Student Course Enrollment System\");\n        stage.setScene(scene);\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch();\n    }\n}\n",[45,1782,1783,1787,1791,1796,1801,1806,1811,1815,1820,1825,1830,1835,1840,1845,1850,1855,1859,1863,1868,1873,1877],{"__ignoreMap":143},[147,1784,1785],{"class":149,"line":150},[147,1786,755],{},[147,1788,1789],{"class":149,"line":168},[147,1790,180],{"emptyLinePlaceholder":24},[147,1792,1793],{"class":149,"line":177},[147,1794,1795],{},"import javafx.application.Application;\n",[147,1797,1798],{"class":149,"line":183},[147,1799,1800],{},"import javafx.fxml.FXMLLoader;\n",[147,1802,1803],{"class":149,"line":197},[147,1804,1805],{},"import javafx.scene.Scene;\n",[147,1807,1808],{"class":149,"line":222},[147,1809,1810],{},"import javafx.stage.Stage;\n",[147,1812,1813],{"class":149,"line":243},[147,1814,180],{"emptyLinePlaceholder":24},[147,1816,1817],{"class":149,"line":261},[147,1818,1819],{},"public class HelloApplication extends Application {\n",[147,1821,1822],{"class":149,"line":498},[147,1823,1824],{},"    @Override\n",[147,1826,1827],{"class":149,"line":504},[147,1828,1829],{},"    public void start(Stage stage) throws Exception {\n",[147,1831,1832],{"class":149,"line":510},[147,1833,1834],{},"        FXMLLoader loader = new FXMLLoader(HelloApplication.class.getResource(\"hello-view.fxml\"));\n",[147,1836,1837],{"class":149,"line":516},[147,1838,1839],{},"        Scene scene = new Scene(loader.load(), 800, 500);\n",[147,1841,1842],{"class":149,"line":522},[147,1843,1844],{},"        stage.setTitle(\"Student Course Enrollment System\");\n",[147,1846,1847],{"class":149,"line":528},[147,1848,1849],{},"        stage.setScene(scene);\n",[147,1851,1852],{"class":149,"line":533},[147,1853,1854],{},"        stage.show();\n",[147,1856,1857],{"class":149,"line":539},[147,1858,813],{},[147,1860,1861],{"class":149,"line":545},[147,1862,180],{"emptyLinePlaceholder":24},[147,1864,1865],{"class":149,"line":551},[147,1866,1867],{},"    public static void main(String[] args) {\n",[147,1869,1870],{"class":149,"line":557},[147,1871,1872],{},"        launch();\n",[147,1874,1875],{"class":149,"line":563},[147,1876,813],{},[147,1878,1879],{"class":149,"line":569},[147,1880,420],{},[41,1882,1883,1884,1887,1888,1891,1892,1895],{},"Click ",[59,1885,1886],{},"Run",". The window opens with any existing students already in the table, because ",[45,1889,1890],{},"initialize()"," calls ",[45,1893,1894],{},"viewAll()"," on startup.",[122,1897,1899],{"id":1898},"try-it-out","Try it out",[41,1901,1902],{},"Walk through these in order. Each one exercises a different branch of the controller:",[1904,1905,1906,1919],"table",{},[1907,1908,1909],"thead",{},[1910,1911,1912,1916],"tr",{},[1913,1914,1915],"th",{},"Try this",[1913,1917,1918],{},"What should happen",[1920,1921,1922,1931,1948,1958,1970,1980],"tbody",{},[1910,1923,1924,1928],{},[1925,1926,1927],"td",{},"Click Enroll with an empty field",[1925,1929,1930],{},"\"All fields must be filled!\"",[1910,1932,1933,1945],{},[1925,1934,1935,1936,311,1939,311,1942],{},"Enroll ",[45,1937,1938],{},"101",[45,1940,1941],{},"Rahim",[45,1943,1944],{},"Java",[1925,1946,1947],{},"Success, and the row appears in the table",[1910,1949,1950,1955],{},[1925,1951,1935,1952,1954],{},[45,1953,1938],{}," again",[1925,1956,1957],{},"\"Student ID already exists!\"",[1910,1959,1960,1965],{},[1925,1961,1962,1963],{},"Search ",[45,1964,1938],{},[1925,1966,1967],{},[45,1968,1969],{},"Name: Rahim | Course: Java",[1910,1971,1972,1977],{},[1925,1973,1962,1974],{},[45,1975,1976],{},"999",[1925,1978,1979],{},"\"Student ID does not exist!\"",[1910,1981,1982,1987],{},[1925,1983,1984,1985],{},"Delete ",[45,1986,1938],{},[1925,1988,1989],{},"\"Member deleted!\" and the row disappears",[41,1991,1992,1993,1996],{},"To confirm the data really reached the database, run ",[45,1994,1995],{},"SELECT * FROM students;"," in Workbench.",[122,1998,2000],{"id":1999},"when-it-doesnt-work","When it doesn't work",[41,2002,2003],{},"Nearly every failure in this app is one of these:",[1904,2005,2006,2016],{},[1907,2007,2008],{},[1910,2009,2010,2013],{},[1913,2011,2012],{},"Error",[1913,2014,2015],{},"Fix",[1920,2017,2018,2035,2045,2057,2071,2089,2106],{},[1910,2019,2020,2025],{},[1925,2021,2022],{},[45,2023,2024],{},"No suitable driver found",[1925,2026,2027,2028,2031,2032,2034],{},"Add ",[45,2029,2030],{},"mysql-connector-j"," to ",[45,2033,335],{}," and reload Maven",[1910,2036,2037,2042],{},[1925,2038,2039],{},[45,2040,2041],{},"Communications link failure",[1925,2043,2044],{},"The MySQL service isn't running. Start it.",[1910,2046,2047,2052],{},[1925,2048,2049],{},[45,2050,2051],{},"Access denied for user 'root'",[1925,2053,2054,2055],{},"Wrong password in ",[45,2056,863],{},[1910,2058,2059,2064],{},[1925,2060,2061],{},[45,2062,2063],{},"Public Key Retrieval is not allowed",[1925,2065,2066,2067,2070],{},"Append ",[45,2068,2069],{},"?allowPublicKeyRetrieval=true&useSSL=false"," to the URL",[1910,2072,2073,2079],{},[1925,2074,2075,2078],{},[45,2076,2077],{},"NullPointerException"," on a control",[1925,2080,2081,2082,2084,2085,2088],{},"An ",[45,2083,47],{}," doesn't match the controller field name, or the ",[45,2086,2087],{},"@FXML"," annotation is missing",[1910,2090,2091,2096],{},[1925,2092,2093],{},[45,2094,2095],{},"LoadException",[1925,2097,2081,2098,2101,2102,2105],{},[45,2099,2100],{},"onAction"," method name doesn't match the controller, or ",[45,2103,2104],{},"fx:controller"," has the wrong package",[1910,2107,2108,2111],{},[1925,2109,2110],{},"Table rows show but the cells are empty",[1925,2112,2113,2114,2117,2118,2121],{},"Getter names don't match, the getters aren't ",[45,2115,2116],{},"public",", or ",[45,2119,2120],{},"opens ... to javafx.base"," is missing",[41,2123,2124],{},"Notice that four of the seven are naming problems, not logic problems. That is the real lesson of FXML: it is wired by strings, so the compiler can't catch a mismatch for you.",[122,2126,2128],{"id":2127},"the-pattern-to-take-away","The pattern to take away",[41,2130,2131],{},"Strip away the UI and every database action in this app is the same four steps:",[138,2133,2135],{"className":377,"code":2134,"language":379,"meta":143,"style":143},"Connection con = connect();                              \u002F\u002F 1. connect\nPreparedStatement ps = con.prepareStatement(\"... ?\");    \u002F\u002F 2. prepare SQL\nps.setString(1, value);                                  \u002F\u002F 3. bind values\nps.executeUpdate();   \u002F\u002F INSERT \u002F DELETE \u002F UPDATE        \u002F\u002F 4. execute\nps.executeQuery();    \u002F\u002F SELECT, which returns a ResultSet\n",[45,2136,2137,2142,2147,2152,2157],{"__ignoreMap":143},[147,2138,2139],{"class":149,"line":150},[147,2140,2141],{},"Connection con = connect();                              \u002F\u002F 1. connect\n",[147,2143,2144],{"class":149,"line":168},[147,2145,2146],{},"PreparedStatement ps = con.prepareStatement(\"... ?\");    \u002F\u002F 2. prepare SQL\n",[147,2148,2149],{"class":149,"line":177},[147,2150,2151],{},"ps.setString(1, value);                                  \u002F\u002F 3. bind values\n",[147,2153,2154],{"class":149,"line":183},[147,2155,2156],{},"ps.executeUpdate();   \u002F\u002F INSERT \u002F DELETE \u002F UPDATE        \u002F\u002F 4. execute\n",[147,2158,2159],{"class":149,"line":197},[147,2160,2161],{},"ps.executeQuery();    \u002F\u002F SELECT, which returns a ResultSet\n",[41,2163,2164],{},"Once that clicks, a library catalog, a contact book or an inventory tracker is the same app with different column names.",[122,2166,2168],{"id":2167},"where-to-go-next","Where to go next",[2170,2171],"checklist",{":items":2172,"doneLabel":2173,"resetLabel":2174,"title":2175},"[{\"label\":\"Add an Update feature\",\"detail\":\"UPDATE students SET student_name = ?, course = ? WHERE student_id = ? — the same four steps, one new button.\"},{\"label\":\"Fill the form from a table row\",\"detail\":\"Listen to table.getSelectionModel().selectedItemProperty() and copy the selected student into the fields, so editing starts with a click.\"},{\"label\":\"Move the database code into a StudentDAO class\",\"detail\":\"The controller should handle the UI; a DAO owns the SQL. It also makes the queries testable without opening a window.\"},{\"label\":\"Style the app with CSS\",\"detail\":\"Add a stylesheet with scene.getStylesheets().add(...) — JavaFX CSS uses -fx- prefixed properties, but the model is the one you already know.\"}]","Done","Reset","Four upgrades, in the order worth doing them",[2177,2178,2179],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}",{"title":143,"searchDepth":177,"depth":177,"links":2181},[2182,2183,2184,2185,2187,2188,2196,2197,2198,2199],{"id":124,"depth":168,"text":125},{"id":288,"depth":168,"text":289},{"id":324,"depth":168,"text":325},{"id":371,"depth":168,"text":2186},"Step 4: Update module-info.java",{"id":442,"depth":168,"text":443},{"id":733,"depth":168,"text":734,"children":2189},[2190,2192,2194],{"id":741,"depth":177,"text":2191},"The model: Student.java",{"id":854,"depth":177,"text":2193},"The controller: HelloController.java",{"id":1774,"depth":177,"text":2195},"The entry point: HelloApplication.java",{"id":1898,"depth":168,"text":1899},{"id":1999,"depth":168,"text":2000},{"id":2127,"depth":168,"text":2128},{"id":2167,"depth":168,"text":2168},"engineering","\u002Fcovers\u002Fblog\u002Fen\u002Fjavafx-mysql-student-enrollment.webp","2026-09-26","A small desktop app that enrolls, lists, searches and deletes students — and teaches the four-step JDBC pattern you will reuse in every data app after it.","md","en",{},"\u002Fblog\u002Fen\u002Fjavafx-mysql-student-enrollment",{"title":33,"description":2203},"Student enrollment app with JavaFX and MySQL","blog\u002Fen\u002Fjavafx-mysql-student-enrollment",[1944,2212,2213,101],"JavaFX","MySQL","8s4W13O8S26XQe5aXgJ3o6p_yo0_7QcIr8I-qCY2uYo",[2216,2699,3223],{"id":2217,"title":2218,"author":2219,"body":2222,"category":2200,"cover":2687,"coverAlt":2687,"date":2688,"description":2689,"draft":6,"extension":2204,"featured":6,"lang":2205,"meta":2690,"navigation":24,"path":2691,"readingTime":261,"seo":2692,"seoTitle":2687,"stem":2693,"tags":2694,"__hash__":2698},"blog\u002Fblog\u002Fen\u002Finteractive-by-exception.md","Interactive by exception",{"name":2220,"role":2221},"Sofia Lindqvist","Staff Frontend Engineer",{"type":38,"value":2223,"toc":2679},[2224,2227,2230,2233,2237,2240,2246,2252,2258,2264,2268,2275,2278,2447,2450,2453,2456,2460,2471,2474,2480,2491,2496,2500,2503,2510,2605,2617,2620,2627,2631,2634,2644,2647,2662,2666,2669,2673,2676],[41,2225,2226],{},"An article is the cheapest thing a framework can render. Headings, paragraphs, a few highlighted code blocks — all of it is produced at build time and served as bytes that no JavaScript needs to touch. The reader gets text on first paint and nothing runs.",[41,2228,2229],{},"Then someone adds a live example, and the article is no longer that. It is now an application with an article in it.",[41,2231,2232],{},"That is often the right trade. This post is full of live examples, and each one is here because the alternative was worse. But it is a trade, and the useful discipline is to make it deliberately: interactive by exception, not by default.",[122,2234,2236],{"id":2235},"what-an-interactive-block-actually-costs","What an interactive block actually costs",[41,2238,2239],{},"Three separate things, and they are worth separating because only one of them is the one people talk about.",[41,2241,2242,2245],{},[59,2243,2244],{},"Transfer."," The component's own code, plus anything it imports. This is the cost everyone measures and the smallest of the three on a site that already ships a framework.",[41,2247,2248,2251],{},[59,2249,2250],{},"Hydration."," The block has to be reconciled on the client before it responds. Until then it is a picture of a control — visible, focusable if you built it from real elements, and inert. A reader who taps during that window gets nothing and has no way to know why.",[41,2253,2254,2257],{},[59,2255,2256],{},"Comprehension."," The one nobody budgets for. A control implies there is something to find by using it. If there isn't — if flipping the toggle just re-renders the same idea in a different colour — the reader has spent attention and got nothing back, and the next control in the article starts with less credit.",[2259,2260,2261],"note",{},[41,2262,2263],{},"Transfer is a number, so it gets a budget in CI. The other two are properties of the design and get argued about in review, if at all. That asymmetry is why articles end up with three sliders and no point.",[122,2265,2267],{"id":2266},"the-test-could-the-prose-have-done-it","The test: could the prose have done it?",[41,2269,2270,2271,2274],{},"Here is the rule I keep coming back to. An interactive block earns its place when the thing it shows is ",[59,2272,2273],{},"combinatorial"," — when the point is not one state but the relationship between many, and enumerating them in prose would take a table nobody reads.",[41,2276,2277],{},"A chart of one dataset is not combinatorial. It is a picture, and a picture is an image file.",[2279,2280,2284,2421],"compare",{"after":2281,"before":2282,"note":2283},"Chart as an image","Chart as a component","Same figure, one data set, no interaction. The second version has no hydration cost and no loading state, because there is nothing to load.",[2285,2286,2287],"template",{"v-slot:before":143},[138,2288,2292],{"className":2289,"code":2290,"language":2291,"meta":143,"style":143},"language-vue shiki shiki-themes github-dark-dimmed github-dark-dimmed","\u003Cscript setup>\nimport { Line } from 'vue-chartjs'\nimport { Chart, registerables } from 'chart.js'\nChart.register(...registerables)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CLine :data=\"data\" :options=\"options\" \u002F>\n\u003C\u002Ftemplate>\n","vue",[45,2293,2294,2309,2324,2336,2352,2361,2365,2373,2413],{"__ignoreMap":143},[147,2295,2296,2299,2303,2306],{"class":149,"line":150},[147,2297,2298],{"class":164},"\u003C",[147,2300,2302],{"class":2301},"sza-u","script",[147,2304,2305],{"class":209}," setup",[147,2307,2308],{"class":164},">\n",[147,2310,2311,2314,2317,2320],{"class":149,"line":168},[147,2312,2313],{"class":153},"import",[147,2315,2316],{"class":164}," { Line } ",[147,2318,2319],{"class":153},"from",[147,2321,2323],{"class":2322},"szYpP"," 'vue-chartjs'\n",[147,2325,2326,2328,2331,2333],{"class":149,"line":177},[147,2327,2313],{"class":153},[147,2329,2330],{"class":164}," { Chart, registerables } ",[147,2332,2319],{"class":153},[147,2334,2335],{"class":2322}," 'chart.js'\n",[147,2337,2338,2341,2344,2346,2349],{"class":149,"line":183},[147,2339,2340],{"class":164},"Chart.",[147,2342,2343],{"class":160},"register",[147,2345,206],{"class":164},[147,2347,2348],{"class":153},"...",[147,2350,2351],{"class":164},"registerables)\n",[147,2353,2354,2357,2359],{"class":149,"line":197},[147,2355,2356],{"class":164},"\u003C\u002F",[147,2358,2302],{"class":2301},[147,2360,2308],{"class":164},[147,2362,2363],{"class":149,"line":222},[147,2364,180],{"emptyLinePlaceholder":24},[147,2366,2367,2369,2371],{"class":149,"line":243},[147,2368,2298],{"class":164},[147,2370,2285],{"class":2301},[147,2372,2308],{"class":164},[147,2374,2375,2378,2381,2384,2387,2390,2393,2395,2397,2399,2402,2404,2406,2408,2410],{"class":149,"line":261},[147,2376,2377],{"class":164},"  \u003C",[147,2379,2380],{"class":2301},"Line",[147,2382,2383],{"class":164}," :",[147,2385,2386],{"class":209},"data",[147,2388,2389],{"class":164},"=",[147,2391,2392],{"class":164},"\"",[147,2394,2386],{"class":164},[147,2396,2392],{"class":164},[147,2398,2383],{"class":164},[147,2400,2401],{"class":209},"options",[147,2403,2389],{"class":164},[147,2405,2392],{"class":164},[147,2407,2401],{"class":164},[147,2409,2392],{"class":164},[147,2411,2412],{"class":164}," \u002F>\n",[147,2414,2415,2417,2419],{"class":149,"line":498},[147,2416,2356],{"class":164},[147,2418,2285],{"class":2301},[147,2420,2308],{"class":164},[2285,2422,2423],{"v-slot:after":143},[138,2424,2427],{"className":2425,"code":2426,"language":2204,"meta":143,"style":143},"language-md shiki shiki-themes github-dark-dimmed github-dark-dimmed","![Bundle size by release](\u002Fcharts\u002Fbundle-by-release.svg)\n",[45,2428,2429],{"__ignoreMap":143},[147,2430,2431,2434,2437,2440,2444],{"class":149,"line":150},[147,2432,2433],{"class":164},"![",[147,2435,2436],{"class":2322},"Bundle size by release",[147,2438,2439],{"class":164},"](",[147,2441,2443],{"class":2442},"sDD3T","\u002Fcharts\u002Fbundle-by-release.svg",[147,2445,2446],{"class":164},")\n",[41,2448,2449],{},"The first version is a reasonable thing to reach for and completely disproportionate to what is being shown. It pulls a charting library into the article's bundle so that a static figure can be drawn on the client instead of at build time, and it introduces a frame in which the reader sees empty space where the chart will be.",[41,2451,2452],{},"The second version renders a figure. That is all the page needed.",[41,2454,2455],{},"Now compare that to a case where the states are the point.",[122,2457,2459],{"id":2458},"when-it-earns-it","When it earns it",[41,2461,2462,2463,2466,2467,2470],{},"A component's prop surface is combinatorial by definition. The argument of a design system is that the variants are a closed set — that ",[45,2464,2465],{},"variant"," has exactly these values, ",[45,2468,2469],{},"size"," exactly these, and anything outside the table is an override rather than a variant. A screenshot is one cell of that table. Prose describing all of them is a paragraph the reader skims.",[41,2472,2473],{},"Letting them walk the table is the argument:",[2475,2476],"playground",{":controls":2477,"component":2478,"label":2479},"[{\"prop\":\"variant\",\"options\":[\"primary\",\"outline\",\"ghost\",\"soft\",\"danger\"]},{\"prop\":\"size\",\"options\":[\"sm\",\"md\",\"lg\"]},{\"prop\":\"loading\",\"options\":[false,true]}]","button","Save changes",[41,2481,2482,2483,2486,2487,2490],{},"That block is the real ",[45,2484,2485],{},"UiButton"," from this codebase, not a reproduction of it, and the snippet underneath is what you would paste into a ",[45,2488,2489],{},".vue"," file to get the state you are looking at. Both of those matter more than the interaction: an example that has drifted from the component it documents is worse than no example, because it is confidently wrong.",[422,2492,2493],{},[41,2494,2495],{},"Render the actual component, never a copy styled to match. A copy is a second implementation with no tests, and it will disagree with the first one within a release or two — usually the release where someone fixes a bug in the real component and not in the article.",[122,2497,2499],{"id":2498},"interactive-does-not-mean-client-only","Interactive does not mean client-only",[41,2501,2502],{},"The most common mistake is not shipping too much JavaScript. It is making the content depend on the JavaScript.",[41,2504,2505,2506,2509],{},"If the hidden pane of a comparison is rendered with ",[45,2507,2508],{},"v-if",", that content does not exist in the served HTML. It is not in the page's find-in-page. It is not in the search index. Someone who reads with JavaScript disabled, or on the connection where the bundle times out, gets an article with a hole in it.",[2279,2511,2514,2561],{"after":2512,"before":2513},"Content behind a style","Content behind a condition",[2285,2515,2516],{"v-slot:before":143},[138,2517,2519],{"className":2289,"code":2518,"language":2291,"meta":143,"style":143},"\u003Csection v-if=\"mode === 'after'\">\n  \u003Cslot name=\"after\" \u002F>\n\u003C\u002Fsection>\n",[45,2520,2521,2548,2553],{"__ignoreMap":143},[147,2522,2523,2525,2528,2531,2533,2535,2538,2541,2544,2546],{"class":149,"line":150},[147,2524,2298],{"class":164},[147,2526,2527],{"class":2301},"section",[147,2529,2530],{"class":153}," v-if",[147,2532,2389],{"class":164},[147,2534,2392],{"class":164},[147,2536,2537],{"class":164},"mode ",[147,2539,2540],{"class":153},"===",[147,2542,2543],{"class":2322}," 'after'",[147,2545,2392],{"class":164},[147,2547,2308],{"class":164},[147,2549,2550],{"class":149,"line":168},[147,2551,2552],{"class":164},"  \u003Cslot name=\"after\" \u002F>\n",[147,2554,2555,2557,2559],{"class":149,"line":177},[147,2556,2356],{"class":164},[147,2558,2527],{"class":2301},[147,2560,2308],{"class":164},[2285,2562,2563],{"v-slot:after":143},[138,2564,2566],{"className":2289,"code":2565,"language":2291,"meta":143,"style":143},"\u003Csection v-show=\"mode !== 'before'\">\n  \u003Cslot name=\"after\" \u002F>\n\u003C\u002Fsection>\n",[45,2567,2568,2593,2597],{"__ignoreMap":143},[147,2569,2570,2572,2574,2577,2579,2581,2583,2586,2589,2591],{"class":149,"line":150},[147,2571,2298],{"class":164},[147,2573,2527],{"class":2301},[147,2575,2576],{"class":209}," v-show",[147,2578,2389],{"class":164},[147,2580,2392],{"class":164},[147,2582,2537],{"class":164},[147,2584,2585],{"class":153},"!==",[147,2587,2588],{"class":2322}," 'before'",[147,2590,2392],{"class":164},[147,2592,2308],{"class":164},[147,2594,2595],{"class":149,"line":168},[147,2596,2552],{"class":164},[147,2598,2599,2601,2603],{"class":149,"line":177},[147,2600,2356],{"class":164},[147,2602,2527],{"class":2301},[147,2604,2308],{"class":164},[41,2606,2607,2608,2612,2613,2616],{},"One character of difference in intent, and a large difference in what the page ",[2609,2610,2611],"em",{},"is",". ",[45,2614,2615],{},"v-show"," leaves both panes in the DOM and toggles their visibility, so the document is complete before hydration and the control is an affordance layered over it rather than a prerequisite for reading it.",[41,2618,2619],{},"The same principle decides what a live example may hold. A quiz answer sitting in the served HTML is readable by anyone who opens devtools — and that is fine, because a self-check in an article is not an assessment. Building an endpoint to hide a value from a reader who is only cheating themselves is ceremony, and it converts a static page into one that cannot be cached.",[2621,2622],"quiz",{":answer":2623,":options":2624,"explanation":2625,"question":2626},"1","[\"The bundle size budget in CI\",\"Find-in-page and the search index, before any of it loads\",\"Keyboard navigation between the panes\",\"The component's own unit tests\"]","Bundle size is a real cost but a measured one, and keyboard behaviour is a separate bug. `v-if` means the content is not in the served HTML at all — so the browser's own find, the crawler, and any reader whose bundle never arrives are all looking at a document with a hole in it.","An interactive block renders its content with `v-if` and hydrates on the client. What is the first thing that breaks?",[122,2628,2630],{"id":2629},"accessibility-is-not-a-later-pass","Accessibility is not a later pass",[41,2632,2633],{},"An interactive block in an article is held to exactly the same standard as one in a product, and it usually gets less scrutiny because it looks like content. Two things go wrong most often.",[41,2635,2636,2637,2640,2641,2643],{},"The first is a control that is a ",[45,2638,2639],{},"div",". It has a click handler and a hover state and no role, no focusability and no keyboard behaviour, and it is invisible to anyone not using a mouse. Building it from a ",[45,2642,2478],{}," costs nothing and gets all three for free.",[41,2645,2646],{},"The second is state communicated only by colour. A tinted row is a result to someone who can see the tint and nothing at all to someone who cannot. If the block's entire purpose is to tell the reader whether they were right, that verdict has to be in text and in the accessibility tree, not in a hue.",[1763,2648,2649],{},[41,2650,2651,2654,2655,2657,2658,2661],{},[45,2652,2653],{},"role=\"checkbox\""," on a ",[45,2656,2478],{}," needs ",[45,2659,2660],{},"aria-checked"," kept in sync, or you have announced a control that never reports its own state. The role is a promise about the properties you will also supply.",[122,2663,2665],{"id":2664},"the-list","The list",[41,2667,2668],{},"Before adding a live example, this is what I check:",[2170,2670],{":items":2671,"title":2672},"[{\"label\":\"The point is combinatorial\",\"detail\":\"Many states, and the relationship between them is the argument. One state is a picture.\"},{\"label\":\"The content is in the served HTML\",\"detail\":\"v-show over v-if. Find-in-page and the crawler see everything before hydration.\"},{\"label\":\"It renders the real component\",\"detail\":\"Not a copy. A copy is a second implementation with no tests.\"},{\"label\":\"Every control is a real control\",\"detail\":\"Built from button or input, reachable by keyboard, with a name.\"},{\"label\":\"State is not carried by colour alone\",\"detail\":\"The verdict is in text and in the accessibility tree.\"},{\"label\":\"It degrades to something readable\",\"detail\":\"With no JavaScript, the block is a plain, complete piece of the article.\"}]","Before an interactive block goes in",[41,2674,2675],{},"None of that is exotic. It is the same bar the rest of the interface is held to — which is the actual point. An article that ships components is shipping software, and the moment we stop calling it content is the moment it starts getting reviewed properly.",[2177,2677,2678],{},"html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .sza-u, html code.shiki .sza-u{--shiki-default:#8DDB8C;--shiki-dark:#8DDB8C}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sDD3T, html code.shiki .sDD3T{--shiki-default:#ADBAC7;--shiki-default-text-decoration:underline;--shiki-dark:#ADBAC7;--shiki-dark-text-decoration:underline}",{"title":143,"searchDepth":177,"depth":177,"links":2680},[2681,2682,2683,2684,2685,2686],{"id":2235,"depth":168,"text":2236},{"id":2266,"depth":168,"text":2267},{"id":2458,"depth":168,"text":2459},{"id":2498,"depth":168,"text":2499},{"id":2629,"depth":168,"text":2630},{"id":2664,"depth":168,"text":2665},null,"2026-09-06","A static article ships no JavaScript for its body. Every live example changes that, so each one should be an argument the prose cannot make on its own.",{},"\u002Fblog\u002Fen\u002Finteractive-by-exception",{"title":2218,"description":2689},"blog\u002Fen\u002Finteractive-by-exception",[2695,2696,2697],"Nuxt","Performance","Content","dc5IXKw_qrTjp3OHte-CmmAoKcUTTtemzxUsOwv5IwA",{"id":2700,"title":2701,"author":2702,"body":2703,"category":2200,"cover":3211,"coverAlt":2701,"date":3212,"description":3213,"draft":6,"extension":2204,"featured":6,"lang":2205,"meta":3214,"navigation":24,"path":3215,"readingTime":222,"seo":3216,"seoTitle":2687,"stem":3217,"tags":3218,"__hash__":3222},"blog\u002Fblog\u002Fen\u002Fcomponent-api-design.md","Your component API is a contract, so write it down",{"name":2220,"role":2221},{"type":38,"value":2704,"toc":3202},[2705,2708,2719,2722,2726,2729,2874,2881,2884,2888,2899,2902,2969,2982,2986,2989,3002,3014,3018,3021,3078,3089,3092,3127,3131,3137,3150,3168,3172,3175,3186,3190,3193,3199],[41,2706,2707],{},"A component library fails in a predictable way. Not all at once — one prop at a time.",[41,2709,2710,2711,2714,2715,2718],{},"Someone needs a slightly different button. They add ",[45,2712,2713],{},"isCompact",". Someone else needs a different one and adds ",[45,2716,2717],{},"variant=\"small\"",". Six months later the component takes fourteen props, four of which interact in ways nobody has documented, and everyone has quietly gone back to writing their own.",[41,2720,2721],{},"The fix is to treat the component's visual API as a contract that is written down in one place and enforced.",[122,2723,2725],{"id":2724},"the-variant-table","The variant table",[41,2727,2728],{},"Every component should declare its full surface as data, not as branching logic scattered through the template.",[138,2730,2734],{"className":2731,"code":2732,"language":2733,"meta":143,"style":143},"language-ts shiki shiki-themes github-dark-dimmed github-dark-dimmed","const button = tv({\n  base: 'inline-flex items-center justify-center font-medium',\n  variants: {\n    variant: {\n      primary: 'bg-primary text-primary-foreground',\n      outline: 'border border-border-strong bg-transparent',\n      ghost: 'text-foreground-secondary hover:bg-surface-hover',\n    },\n    size: {\n      sm: 'h-9 px-3.5 text-sm',\n      md: 'h-11 px-5 text-sm',\n      lg: 'h-12 px-6 text-md',\n    },\n  },\n  defaultVariants: { variant: 'primary', size: 'md' },\n})\n","ts",[45,2735,2736,2753,2763,2768,2773,2783,2793,2803,2808,2813,2823,2833,2843,2847,2852,2869],{"__ignoreMap":143},[147,2737,2738,2741,2744,2747,2750],{"class":149,"line":150},[147,2739,2740],{"class":153},"const",[147,2742,2743],{"class":209}," button",[147,2745,2746],{"class":153}," =",[147,2748,2749],{"class":160}," tv",[147,2751,2752],{"class":164},"({\n",[147,2754,2755,2758,2761],{"class":149,"line":168},[147,2756,2757],{"class":164},"  base: ",[147,2759,2760],{"class":2322},"'inline-flex items-center justify-center font-medium'",[147,2762,219],{"class":164},[147,2764,2765],{"class":149,"line":177},[147,2766,2767],{"class":164},"  variants: {\n",[147,2769,2770],{"class":149,"line":183},[147,2771,2772],{"class":164},"    variant: {\n",[147,2774,2775,2778,2781],{"class":149,"line":197},[147,2776,2777],{"class":164},"      primary: ",[147,2779,2780],{"class":2322},"'bg-primary text-primary-foreground'",[147,2782,219],{"class":164},[147,2784,2785,2788,2791],{"class":149,"line":222},[147,2786,2787],{"class":164},"      outline: ",[147,2789,2790],{"class":2322},"'border border-border-strong bg-transparent'",[147,2792,219],{"class":164},[147,2794,2795,2798,2801],{"class":149,"line":243},[147,2796,2797],{"class":164},"      ghost: ",[147,2799,2800],{"class":2322},"'text-foreground-secondary hover:bg-surface-hover'",[147,2802,219],{"class":164},[147,2804,2805],{"class":149,"line":261},[147,2806,2807],{"class":164},"    },\n",[147,2809,2810],{"class":149,"line":498},[147,2811,2812],{"class":164},"    size: {\n",[147,2814,2815,2818,2821],{"class":149,"line":504},[147,2816,2817],{"class":164},"      sm: ",[147,2819,2820],{"class":2322},"'h-9 px-3.5 text-sm'",[147,2822,219],{"class":164},[147,2824,2825,2828,2831],{"class":149,"line":510},[147,2826,2827],{"class":164},"      md: ",[147,2829,2830],{"class":2322},"'h-11 px-5 text-sm'",[147,2832,219],{"class":164},[147,2834,2835,2838,2841],{"class":149,"line":516},[147,2836,2837],{"class":164},"      lg: ",[147,2839,2840],{"class":2322},"'h-12 px-6 text-md'",[147,2842,219],{"class":164},[147,2844,2845],{"class":149,"line":522},[147,2846,2807],{"class":164},[147,2848,2849],{"class":149,"line":528},[147,2850,2851],{"class":164},"  },\n",[147,2853,2854,2857,2860,2863,2866],{"class":149,"line":533},[147,2855,2856],{"class":164},"  defaultVariants: { variant: ",[147,2858,2859],{"class":2322},"'primary'",[147,2861,2862],{"class":164},", size: ",[147,2864,2865],{"class":2322},"'md'",[147,2867,2868],{"class":164}," },\n",[147,2870,2871],{"class":149,"line":539},[147,2872,2873],{"class":164},"})\n",[41,2875,2876,2877,2880],{},"The value here is not the terseness. It is that the table is exhaustive. If a variant is not in it, it does not exist, and a request for a new one is a visible decision rather than an inline ",[45,2878,2879],{},"class"," override that nobody reviews.",[41,2882,2883],{},"This is the single highest-leverage thing you can do to stop a library from decaying.",[122,2885,2887],{"id":2886},"booleans-are-enums-that-have-not-grown-up-yet","Booleans are enums that have not grown up yet",[41,2889,2890,2892,2893,102,2895,2898],{},[45,2891,2713],{}," seems fine until you need a third density. Then you get ",[45,2894,2713],{},[45,2896,2897],{},"isSpacious"," and the undefined behaviour when both are true.",[41,2900,2901],{},"If a property has any chance of gaining a third value, make it an enum on day one:",[138,2903,2905],{"className":2731,"code":2904,"language":2733,"meta":143,"style":143},"\u002F\u002F Will break\ndefineProps\u003C{ isCompact?: boolean }>()\n\n\u002F\u002F Will not\ndefineProps\u003C{ density?: 'compact' | 'default' | 'comfortable' }>()\n",[45,2906,2907,2913,2933,2937,2942],{"__ignoreMap":143},[147,2908,2909],{"class":149,"line":150},[147,2910,2912],{"class":2911},"s0RzX","\u002F\u002F Will break\n",[147,2914,2915,2918,2921,2924,2927,2930],{"class":149,"line":168},[147,2916,2917],{"class":160},"defineProps",[147,2919,2920],{"class":164},"\u003C{ ",[147,2922,2713],{"class":2923},"siJwO",[147,2925,2926],{"class":153},"?:",[147,2928,2929],{"class":209}," boolean",[147,2931,2932],{"class":164}," }>()\n",[147,2934,2935],{"class":149,"line":177},[147,2936,180],{"emptyLinePlaceholder":24},[147,2938,2939],{"class":149,"line":183},[147,2940,2941],{"class":2911},"\u002F\u002F Will not\n",[147,2943,2944,2946,2948,2951,2953,2956,2959,2962,2964,2967],{"class":149,"line":197},[147,2945,2917],{"class":160},[147,2947,2920],{"class":164},[147,2949,2950],{"class":2923},"density",[147,2952,2926],{"class":153},[147,2954,2955],{"class":2322}," 'compact'",[147,2957,2958],{"class":153}," |",[147,2960,2961],{"class":2322}," 'default'",[147,2963,2958],{"class":153},[147,2965,2966],{"class":2322}," 'comfortable'",[147,2968,2932],{"class":164},[41,2970,2971,2972,311,2975,311,2978,2981],{},"Booleans are correct for genuinely binary states — ",[45,2973,2974],{},"disabled",[45,2976,2977],{},"loading",[45,2979,2980],{},"required",". Everything describing appearance is a candidate for growth.",[122,2983,2985],{"id":2984},"name-props-after-what-they-do","Name props after what they do",[41,2987,2988],{},"Two habits that pay for themselves:",[41,2990,2991,433,2994,2997,2998,3001],{},[59,2992,2993],{},"Name the role, not the implementation.",[45,2995,2996],{},"trailingIcon"," survives a switch from an icon font to inline SVG. ",[45,2999,3000],{},"iconRightSvg"," does not.",[41,3003,3004,3007,3008,3010,3011,3013],{},[59,3005,3006],{},"Match the platform where one exists."," If the DOM calls it ",[45,3009,2974],{},", call it ",[45,3012,2974],{},". Every renamed standard property is a thing every consumer has to learn.",[122,3015,3017],{"id":3016},"slots-for-content-props-for-configuration","Slots for content, props for configuration",[41,3019,3020],{},"The rule that keeps components from growing forever: if it is content, it belongs in a slot; if it changes behaviour or appearance, it is a prop.",[138,3022,3024],{"className":2289,"code":3023,"language":2291,"meta":143,"style":143},"\u003CUiButton variant=\"primary\" size=\"lg\">\n  \u003Ctemplate #leading>\n    \u003CAvatar :src=\"user.avatar\" size=\"xs\" \u002F>\n  \u003C\u002Ftemplate>\n  Continue as {{ user.name }}\n\u003C\u002FUiButton>\n",[45,3025,3026,3050,3055,3060,3065,3070],{"__ignoreMap":143},[147,3027,3028,3030,3032,3035,3037,3040,3043,3045,3048],{"class":149,"line":150},[147,3029,2298],{"class":164},[147,3031,2485],{"class":2301},[147,3033,3034],{"class":209}," variant",[147,3036,2389],{"class":164},[147,3038,3039],{"class":2322},"\"primary\"",[147,3041,3042],{"class":209}," size",[147,3044,2389],{"class":164},[147,3046,3047],{"class":2322},"\"lg\"",[147,3049,2308],{"class":164},[147,3051,3052],{"class":149,"line":168},[147,3053,3054],{"class":164},"  \u003Ctemplate #leading>\n",[147,3056,3057],{"class":149,"line":177},[147,3058,3059],{"class":164},"    \u003CAvatar :src=\"user.avatar\" size=\"xs\" \u002F>\n",[147,3061,3062],{"class":149,"line":183},[147,3063,3064],{"class":164},"  \u003C\u002Ftemplate>\n",[147,3066,3067],{"class":149,"line":197},[147,3068,3069],{"class":164},"  Continue as {{ user.name }}\n",[147,3071,3072,3074,3076],{"class":149,"line":222},[147,3073,2356],{"class":164},[147,3075,2485],{"class":2301},[147,3077,2308],{"class":164},[41,3079,3080,3081,3084,3085,3088],{},"Without the slot, someone eventually asks for an ",[45,3082,3083],{},"avatarSrc"," prop, and then an ",[45,3086,3087],{},"avatarSize"," prop, and the button is now responsible for rendering avatars.",[41,3090,3091],{},"Provide a default inside the slot so the common case stays one line:",[138,3093,3095],{"className":2289,"code":3094,"language":2291,"meta":143,"style":143},"\u003Cslot name=\"leading\">\n  \u003CIcon v-if=\"icon\" :name=\"icon\" \u002F>\n\u003C\u002Fslot>\n",[45,3096,3097,3114,3119],{"__ignoreMap":143},[147,3098,3099,3101,3104,3107,3109,3112],{"class":149,"line":150},[147,3100,2298],{"class":164},[147,3102,3103],{"class":2301},"slot",[147,3105,3106],{"class":209}," name",[147,3108,2389],{"class":164},[147,3110,3111],{"class":2322},"\"leading\"",[147,3113,2308],{"class":164},[147,3115,3116],{"class":149,"line":168},[147,3117,3118],{"class":164},"  \u003CIcon v-if=\"icon\" :name=\"icon\" \u002F>\n",[147,3120,3121,3123,3125],{"class":149,"line":177},[147,3122,2356],{"class":164},[147,3124,3103],{"class":2301},[147,3126,2308],{"class":164},[122,3128,3130],{"id":3129},"let-consumers-override-but-make-it-lose-predictably","Let consumers override, but make it lose predictably",[41,3132,3133,3134,3136],{},"A component that cannot be adjusted gets forked. A component that can be adjusted arbitrarily stops being a system. The middle path is to accept a ",[45,3135,2879],{}," prop, apply it last, and resolve conflicts deterministically:",[138,3138,3140],{"className":2731,"code":3139,"language":2733,"meta":143,"style":143},"cx(base, variantClasses, props.class)\n",[45,3141,3142],{"__ignoreMap":143},[147,3143,3144,3147],{"class":149,"line":150},[147,3145,3146],{"class":160},"cx",[147,3148,3149],{"class":164},"(base, variantClasses, props.class)\n",[41,3151,3152,3153,3156,3157,3159,3160,3163,3164,3167],{},"With ",[45,3154,3155],{},"tailwind-merge"," in ",[45,3158,3146],{},", a consumer passing ",[45,3161,3162],{},"h-14"," beats the variant's ",[45,3165,3166],{},"h-11"," instead of both landing in the class list and the winner depending on Tailwind's internal ordering. Overrides become predictable, which means people use them for genuine one-offs rather than forking.",[122,3169,3171],{"id":3170},"accessibility-belongs-in-the-primitive","Accessibility belongs in the primitive",[41,3173,3174],{},"If focus rings, ARIA wiring and reduced-motion handling live in the consumer, they will be forgotten. Not sometimes — reliably, under deadline.",[41,3176,3177,3178,3181,3182,3185],{},"Put them in the primitive. A ",[45,3179,3180],{},"UiField"," that owns its own id, ",[45,3183,3184],{},"aria-describedby"," and error announcement means no form in the codebase can ship an undescribed input, because the wiring is not the consumer's job.",[122,3187,3189],{"id":3188},"the-test","The test",[41,3191,3192],{},"Ask someone who did not build the library to add a new page with it, and watch without helping.",[41,3194,3195,3196,3198],{},"Every time they open the component's source to work out what a prop does, that is a documentation failure. Every time they write a custom ",[45,3197,2879],{}," to get something the variant table should have offered, that is an API failure. Both are cheap to fix in week one and expensive in month nine.",[2177,3200,3201],{},"html pre.shiki code .sEYkB, html code.shiki .sEYkB{--shiki-default:#F47067;--shiki-dark:#F47067}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html pre.shiki code .s-wk_, html code.shiki .s-wk_{--shiki-default:#DCBDFB;--shiki-dark:#DCBDFB}html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s0RzX, html code.shiki .s0RzX{--shiki-default:#768390;--shiki-dark:#768390}html pre.shiki code .siJwO, html code.shiki .siJwO{--shiki-default:#F69D50;--shiki-dark:#F69D50}html pre.shiki code .sza-u, html code.shiki .sza-u{--shiki-default:#8DDB8C;--shiki-dark:#8DDB8C}",{"title":143,"searchDepth":177,"depth":177,"links":3203},[3204,3205,3206,3207,3208,3209,3210],{"id":2724,"depth":168,"text":2725},{"id":2886,"depth":168,"text":2887},{"id":2984,"depth":168,"text":2985},{"id":3016,"depth":168,"text":3017},{"id":3129,"depth":168,"text":3130},{"id":3170,"depth":168,"text":3171},{"id":3188,"depth":168,"text":3189},"\u002Fcovers\u002Fblog\u002Fen\u002Fcomponent-api-design.webp","2026-04-09","Variant tables, prop naming and the boolean that should have been an enum. Notes from maintaining component libraries other people have to use.",{},"\u002Fblog\u002Fen\u002Fcomponent-api-design",{"title":2701,"description":3213},"blog\u002Fen\u002Fcomponent-api-design",[3219,3220,3221],"Vue","Components","API Design","uzwUIe6urf03H4xOT1YwLUkBmwH7x1Q4GcoW9xYlyXQ",{"id":3224,"title":3225,"author":3226,"body":3227,"category":2200,"cover":3571,"coverAlt":3225,"date":3572,"description":3573,"draft":6,"extension":2204,"featured":6,"lang":2205,"meta":3574,"navigation":24,"path":3575,"readingTime":197,"seo":3576,"seoTitle":2687,"stem":3577,"tags":3578,"__hash__":3580},"blog\u002Fblog\u002Fen\u002Fperformance-budgets-in-ci.md","Performance budgets belong in CI, not in a retrospective",{"name":2220,"role":2221},{"type":38,"value":3228,"toc":3563},[3229,3232,3235,3239,3242,3245,3400,3403,3406,3410,3421,3424,3428,3431,3447,3460,3466,3472,3476,3479,3486,3490,3493,3547,3550,3554,3557,3560],[41,3230,3231],{},"Every team has run a Lighthouse audit, celebrated a 96, and watched it drift to 71 over the following quarter. The audit was not wrong. It was just a photograph of a moving thing.",[41,3233,3234],{},"Performance is not a score you achieve. It is a constraint you enforce, and the only place a constraint holds is in the pipeline.",[122,3236,3238],{"id":3237},"budget-the-bytes-not-the-score","Budget the bytes, not the score",[41,3240,3241],{},"Lighthouse scores are noisy — they move with machine load, network conditions and the phase of the moon. Failing a build on a composite score produces flaky CI and, within two weeks, a team that skips the check.",[41,3243,3244],{},"Budget the things that are deterministic:",[138,3246,3250],{"className":3247,"code":3248,"language":3249,"meta":143,"style":143},"language-json shiki shiki-themes github-dark-dimmed github-dark-dimmed","{\n  \"budgets\": [\n    {\n      \"path\": \"\u002F*\",\n      \"resourceSizes\": [\n        { \"resourceType\": \"script\", \"budget\": 180 },\n        { \"resourceType\": \"stylesheet\", \"budget\": 60 },\n        { \"resourceType\": \"font\", \"budget\": 120 },\n        { \"resourceType\": \"total\", \"budget\": 500 }\n      ]\n    }\n  ]\n}\n","json",[45,3251,3252,3257,3265,3270,3283,3290,3315,3337,3359,3382,3387,3391,3396],{"__ignoreMap":143},[147,3253,3254],{"class":149,"line":150},[147,3255,3256],{"class":164},"{\n",[147,3258,3259,3262],{"class":149,"line":168},[147,3260,3261],{"class":2301},"  \"budgets\"",[147,3263,3264],{"class":164},": [\n",[147,3266,3267],{"class":149,"line":177},[147,3268,3269],{"class":164},"    {\n",[147,3271,3272,3275,3278,3281],{"class":149,"line":183},[147,3273,3274],{"class":2301},"      \"path\"",[147,3276,3277],{"class":164},": ",[147,3279,3280],{"class":2322},"\"\u002F*\"",[147,3282,219],{"class":164},[147,3284,3285,3288],{"class":149,"line":197},[147,3286,3287],{"class":2301},"      \"resourceSizes\"",[147,3289,3264],{"class":164},[147,3291,3292,3295,3298,3300,3303,3305,3308,3310,3313],{"class":149,"line":222},[147,3293,3294],{"class":164},"        { ",[147,3296,3297],{"class":2301},"\"resourceType\"",[147,3299,3277],{"class":164},[147,3301,3302],{"class":2322},"\"script\"",[147,3304,311],{"class":164},[147,3306,3307],{"class":2301},"\"budget\"",[147,3309,3277],{"class":164},[147,3311,3312],{"class":209},"180",[147,3314,2868],{"class":164},[147,3316,3317,3319,3321,3323,3326,3328,3330,3332,3335],{"class":149,"line":243},[147,3318,3294],{"class":164},[147,3320,3297],{"class":2301},[147,3322,3277],{"class":164},[147,3324,3325],{"class":2322},"\"stylesheet\"",[147,3327,311],{"class":164},[147,3329,3307],{"class":2301},[147,3331,3277],{"class":164},[147,3333,3334],{"class":209},"60",[147,3336,2868],{"class":164},[147,3338,3339,3341,3343,3345,3348,3350,3352,3354,3357],{"class":149,"line":261},[147,3340,3294],{"class":164},[147,3342,3297],{"class":2301},[147,3344,3277],{"class":164},[147,3346,3347],{"class":2322},"\"font\"",[147,3349,311],{"class":164},[147,3351,3307],{"class":2301},[147,3353,3277],{"class":164},[147,3355,3356],{"class":209},"120",[147,3358,2868],{"class":164},[147,3360,3361,3363,3365,3367,3370,3372,3374,3376,3379],{"class":149,"line":498},[147,3362,3294],{"class":164},[147,3364,3297],{"class":2301},[147,3366,3277],{"class":164},[147,3368,3369],{"class":2322},"\"total\"",[147,3371,311],{"class":164},[147,3373,3307],{"class":2301},[147,3375,3277],{"class":164},[147,3377,3378],{"class":209},"500",[147,3380,3381],{"class":164}," }\n",[147,3383,3384],{"class":149,"line":504},[147,3385,3386],{"class":164},"      ]\n",[147,3388,3389],{"class":149,"line":510},[147,3390,813],{"class":164},[147,3392,3393],{"class":149,"line":516},[147,3394,3395],{"class":164},"  ]\n",[147,3397,3398],{"class":149,"line":522},[147,3399,420],{"class":164},[41,3401,3402],{},"Bundle size is stable across runs. If the JavaScript budget goes from 178KB to 214KB, something specific happened in this pull request, and the diff is right there.",[41,3404,3405],{},"Keep the Lighthouse run, but report it rather than gate on it. Gate on bytes.",[122,3407,3409],{"id":3408},"attribute-the-regression-in-the-pr","Attribute the regression in the PR",[41,3411,3412,3413,3416,3417,3420],{},"A build that fails with \"bundle too large\" sends someone on a twenty-minute hunt. A build that fails with \"",[45,3414,3415],{},"+34KB"," — ",[45,3418,3419],{},"date-fns"," added to the client bundle\" is fixed in two minutes.",[41,3422,3423],{},"Emit a comparison against the base branch as a PR comment: per-chunk deltas, and the new dependencies responsible. The point is to make the cheapest moment to fix a regression also the most obvious one.",[122,3425,3427],{"id":3426},"the-four-things-that-actually-regress","The four things that actually regress",[41,3429,3430],{},"Across the front-ends we have built, budget failures cluster:",[41,3432,3433,3436,3437,2612,3440,3443,3444,3446],{},[59,3434,3435],{},"A date or utility library reaching the client."," Almost always importable server-side only, or replaceable with ",[45,3438,3439],{},"Intl",[45,3441,3442],{},"Intl.DateTimeFormat"," covers the majority of what teams reach for ",[45,3445,3419],{}," to do.",[41,3448,3449,433,3452,3455,3456,3459],{},[59,3450,3451],{},"An icon set imported wholesale.",[45,3453,3454],{},"import * as Icons"," pulls in thousands. Bundle the icons you use explicitly — and remember that dynamically named icons (",[45,3457,3458],{},":name=\"item.icon\"",") are invisible to static scanners, so they need listing by hand.",[41,3461,3462,3465],{},[59,3463,3464],{},"Fonts."," Self-host, subset to the ranges you serve, preload only the weights used above the fold. Four weights of two families at full Latin coverage is 300KB nobody asked for.",[41,3467,3468,3471],{},[59,3469,3470],{},"Images without dimensions."," Not a byte problem, a layout-shift problem, and the one most likely to make a fast site feel slow. Width and height on every image, always.",[122,3473,3475],{"id":3474},"measure-the-field-not-the-lab","Measure the field, not the lab",[41,3477,3478],{},"Lab metrics tell you whether a change helped. Field metrics tell you whether your users are having a good time. They disagree more often than people expect, because your laptop is not a mid-range Android on a train.",[41,3480,3481,3482,3485],{},"Ship ",[45,3483,3484],{},"web-vitals",", send p75 to your analytics, and put LCP, INP and CLS on a dashboard someone looks at weekly. When lab and field diverge, the field is right.",[122,3487,3489],{"id":3488},"reasonable-starting-numbers","Reasonable starting numbers",[41,3491,3492],{},"For a content-led marketing site, these are achievable without heroics:",[1904,3494,3495,3505],{},[1907,3496,3497],{},[1910,3498,3499,3502],{},[1913,3500,3501],{},"Metric",[1913,3503,3504],{},"Target (p75, field)",[1920,3506,3507,3515,3523,3531,3539],{},[1910,3508,3509,3512],{},[1925,3510,3511],{},"LCP",[1925,3513,3514],{},"\u003C 1.5s",[1910,3516,3517,3520],{},[1925,3518,3519],{},"INP",[1925,3521,3522],{},"\u003C 150ms",[1910,3524,3525,3528],{},[1925,3526,3527],{},"CLS",[1925,3529,3530],{},"\u003C 0.05",[1910,3532,3533,3536],{},[1925,3534,3535],{},"JS transferred",[1925,3537,3538],{},"\u003C 180KB",[1910,3540,3541,3544],{},[1925,3542,3543],{},"Total transferred",[1925,3545,3546],{},"\u003C 500KB",[41,3548,3549],{},"An application will need looser numbers. Set them deliberately rather than inheriting them, and write down why.",[122,3551,3553],{"id":3552},"the-part-that-is-cultural","The part that is cultural",[41,3555,3556],{},"The technical setup takes an afternoon. Keeping it takes agreement that a budget failure blocks a merge like a failing test does.",[41,3558,3559],{},"Without that, the check becomes advisory, advisory becomes ignored, and you are back to running an audit every quarter and wondering where the score went.",[2177,3561,3562],{},"html pre.shiki code .sM9_K, html code.shiki .sM9_K{--shiki-default:#ADBAC7;--shiki-dark:#ADBAC7}html pre.shiki code .sza-u, html code.shiki .sza-u{--shiki-default:#8DDB8C;--shiki-dark:#8DDB8C}html pre.shiki code .szYpP, html code.shiki .szYpP{--shiki-default:#96D0FF;--shiki-dark:#96D0FF}html pre.shiki code .sQdni, html code.shiki .sQdni{--shiki-default:#6CB6FF;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":143,"searchDepth":177,"depth":177,"links":3564},[3565,3566,3567,3568,3569,3570],{"id":3237,"depth":168,"text":3238},{"id":3408,"depth":168,"text":3409},{"id":3426,"depth":168,"text":3427},{"id":3474,"depth":168,"text":3475},{"id":3488,"depth":168,"text":3489},{"id":3552,"depth":168,"text":3553},"\u002Fcovers\u002Fblog\u002Fen\u002Fperformance-budgets-in-ci.webp","2026-01-22","A Lighthouse score is a snapshot. A budget that fails the build is a constraint. Here is the setup we ship with every front-end.",{},"\u002Fblog\u002Fen\u002Fperformance-budgets-in-ci",{"title":3225,"description":3573},"blog\u002Fen\u002Fperformance-budgets-in-ci",[2696,3579,2695],"CI","AQvLcIOE4sC23Ymk_TQXiYRa1TryKQgAcQK6B4JUfCg",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":3582},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m9 18l6-6l-6-6\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":3584},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M8 2v3m8-3v3\"\u002F>\u003Crect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"\u002F>\u003Cpath d=\"M3 9h18\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":3586},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"M12 6v6l4 2\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":3588},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"\u002F>\u003Cpath d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":3590},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M10 22V7a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5a1 1 0 0 0-1-1H2\"\u002F>\u003Crect width=\"8\" height=\"8\" x=\"14\" y=\"2\" rx=\"1\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":3592},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"m9 12l2 2l4-4\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":3594},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m21.73 18l-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3M12 9v4m0 4h.01\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":3596},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Crect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"\u002F>\u003Cpath d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"\u002F>\u003C\u002Fg>",1791742736868]