├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Attributes.php ├── BaseElement.php ├── Elements ├── A.php ├── Attributes │ ├── Autocomplete.php │ ├── Autofocus.php │ ├── Disabled.php │ ├── MinMaxLength.php │ ├── Name.php │ ├── Placeholder.php │ ├── ReadonlyTrait.php │ ├── Required.php │ ├── Target.php │ ├── Type.php │ └── Value.php ├── Button.php ├── Div.php ├── Element.php ├── Fieldset.php ├── File.php ├── Form.php ├── I.php ├── Img.php ├── Input.php ├── Label.php ├── Legend.php ├── Optgroup.php ├── Option.php ├── P.php ├── Select.php ├── Span.php └── Textarea.php ├── Exceptions ├── InvalidChild.php ├── InvalidHtml.php └── MissingTag.php ├── Facades └── Html.php ├── Helpers └── Arr.php ├── Html.php ├── HtmlElement.php ├── HtmlServiceProvider.php ├── Selectable.php └── helpers.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-html` will be documented in this file. 4 | 5 | ## 3.12.0 - 2025-03-21 6 | 7 | ### What's Changed 8 | 9 | * add disabled option to file input by @it-can in https://github.com/spatie/laravel-html/pull/253 10 | 11 | ### New Contributors 12 | 13 | * @it-can made their first contribution in https://github.com/spatie/laravel-html/pull/253 14 | 15 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.11.3...3.12.0 16 | 17 | ## 3.11.3 - 2025-02-17 18 | 19 | ### What's Changed 20 | 21 | * Laravel 12.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-html/pull/251 22 | 23 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.11.2...3.11.3 24 | 25 | ## 3.11.2 - 2025-02-05 26 | 27 | ### What's Changed 28 | 29 | * Radio buttons with value 0 are incorrectly marked as checked by @acarpio89 in https://github.com/spatie/laravel-html/pull/249 30 | 31 | ### New Contributors 32 | 33 | * @acarpio89 made their first contribution in https://github.com/spatie/laravel-html/pull/249 34 | 35 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.11.1...3.11.2 36 | 37 | ## 3.11.1 - 2024-10-18 38 | 39 | ### What's Changed 40 | 41 | * fix: Passing null to parameter #1 ($string) of type string is deprecated by @francoism90 in https://github.com/spatie/laravel-html/pull/244 42 | 43 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.11.0...3.11.1 44 | 45 | ## 3.11.0 - 2024-07-16 46 | 47 | ### What's Changed 48 | 49 | * Add some attributes by @francoism90 in https://github.com/spatie/laravel-html/pull/239 50 | 51 | ### New Contributors 52 | 53 | * @francoism90 made their first contribution in https://github.com/spatie/laravel-html/pull/239 54 | 55 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.10.1...3.11.0 56 | 57 | ## 3.10.1 - 2024-07-15 58 | 59 | ### What's Changed 60 | 61 | * Fix value omitted when input created with no name by @raveren in https://github.com/spatie/laravel-html/pull/235 62 | * Fix value omitted when input created with no name by @raveren in https://github.com/spatie/laravel-html/pull/236 63 | * Fix for Select and model's relationships by @nikosv in https://github.com/spatie/laravel-html/pull/237 64 | 65 | ### New Contributors 66 | 67 | * @nikosv made their first contribution in https://github.com/spatie/laravel-html/pull/237 68 | 69 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.10.0...3.10.1 70 | 71 | ## 3.10.0 - 2024-07-03 72 | 73 | ### What's Changed 74 | 75 | * Update docs for name attribute by @bskl in https://github.com/spatie/laravel-html/pull/225 76 | * Add Conditionable trait: now `->when()` helper is available on all elements by @raveren in https://github.com/spatie/laravel-html/pull/234 77 | 78 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.9.0...3.10.0 79 | 80 | ## 3.9.0 - 2024-04-25 81 | 82 | ### What's Changed 83 | 84 | * Add use statement by @bskl in https://github.com/spatie/laravel-html/pull/222 85 | * Add aria helper method by @bskl in https://github.com/spatie/laravel-html/pull/226 86 | 87 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.8.0...3.9.0 88 | 89 | ## 3.8.0 - 2024-04-24 90 | 91 | ### What's Changed 92 | 93 | * Add autocomplete attribute helper to the form element by @raveren in https://github.com/spatie/laravel-html/pull/221 94 | * Added support for Htmlable contents in BaseElement by @hemmesdev in https://github.com/spatie/laravel-html/pull/215 95 | * Register Service Provider in Laravel 11 by @gqrdev in https://github.com/spatie/laravel-html/pull/224 96 | * Add name attribute to form element by @bskl in https://github.com/spatie/laravel-html/pull/223 97 | 98 | ### New Contributors 99 | 100 | * @hemmesdev made their first contribution in https://github.com/spatie/laravel-html/pull/215 101 | * @gqrdev made their first contribution in https://github.com/spatie/laravel-html/pull/224 102 | 103 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.7.0...3.8.0 104 | 105 | ## 3.7.0 - 2024-03-23 106 | 107 | ### What's Changed 108 | 109 | * Fix return value in docs in element-methods.md by @raveren in https://github.com/spatie/laravel-html/pull/218 110 | * Add autocomplete attribute helper to input, select and textarea by @raveren in https://github.com/spatie/laravel-html/pull/219 111 | * Fix link with version in documentation by @fey in https://github.com/spatie/laravel-html/pull/217 112 | 113 | ### New Contributors 114 | 115 | * @raveren made their first contribution in https://github.com/spatie/laravel-html/pull/218 116 | * @fey made their first contribution in https://github.com/spatie/laravel-html/pull/217 117 | 118 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.6.0...3.7.0 119 | 120 | ## 3.6.0 - 2024-03-08 121 | 122 | ### What's Changed 123 | 124 | * Laravel 11.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-html/pull/214 125 | 126 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.5.0...3.6.0 127 | 128 | ## 3.4.0 - 2024-01-05 129 | 130 | ### What's Changed 131 | 132 | * Fix docblock to solve phpstan errors when passing an array to html()->div() by @SanderMuller in https://github.com/spatie/laravel-html/pull/210 133 | * Documentation on how to extend the package by @azamtav in https://github.com/spatie/laravel-html/pull/204 134 | 135 | ### New Contributors 136 | 137 | * @SanderMuller made their first contribution in https://github.com/spatie/laravel-html/pull/210 138 | * @azamtav made their first contribution in https://github.com/spatie/laravel-html/pull/204 139 | 140 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.3.0...3.4.0 141 | 142 | ## 3.3.0 - 2023-10-24 143 | 144 | ### What's Changed 145 | 146 | - Add documentation for new FormElement::route() method by @miken32 in https://github.com/spatie/laravel-html/pull/190 147 | - Update `.gitattributes` by @totoprayogo1916 in https://github.com/spatie/laravel-html/pull/194 148 | - Correction to docs re: readonly vs isReadonly by @sgilberg in https://github.com/spatie/laravel-html/pull/195 149 | - Get value from model with casts php native enum by @bskl in https://github.com/spatie/laravel-html/pull/203 150 | 151 | ### New Contributors 152 | 153 | - @totoprayogo1916 made their first contribution in https://github.com/spatie/laravel-html/pull/194 154 | - @sgilberg made their first contribution in https://github.com/spatie/laravel-html/pull/195 155 | - @bskl made their first contribution in https://github.com/spatie/laravel-html/pull/203 156 | 157 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.2.2...3.3.0 158 | 159 | ## 3.2.2 - 2023-07-20 160 | 161 | ### What's Changed 162 | 163 | - Allow setting a form action to a route by @miken32 in https://github.com/spatie/laravel-html/pull/189 164 | 165 | ### New Contributors 166 | 167 | - @miken32 made their first contribution in https://github.com/spatie/laravel-html/pull/189 168 | 169 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.2.1...3.2.2 170 | 171 | ## 3.2.1 - 2023-01-24 172 | 173 | ### What's Changed 174 | 175 | - Convert all tests to Pest by @alexmanase in https://github.com/spatie/laravel-html/pull/183 176 | - Laravel 10.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-html/pull/184 177 | 178 | ### New Contributors 179 | 180 | - @alexmanase made their first contribution in https://github.com/spatie/laravel-html/pull/183 181 | - @laravel-shift made their first contribution in https://github.com/spatie/laravel-html/pull/184 182 | 183 | **Full Changelog**: https://github.com/spatie/laravel-html/compare/3.2.0...3.2.1 184 | 185 | ## 3.2.0 - 2022-12-27 186 | 187 | - Add `P` class to render paragraphs 188 | 189 | ## 3.1.0 - 2022-01-14 190 | 191 | - Allow Laravel 9 192 | 193 | ## 3.0.0 - 2021-11-17 194 | 195 | - Add compatiblity with PHP 8.1. The only breaking change with v2 is that `readonly` has been renamed to `isReadonly`. 196 | 197 | ## 2.30.0 - 2022-07-09 198 | 199 | - Better support for numeric values in attributes 200 | 201 | ## 2.29.0 - 2021-02-09 202 | 203 | - Add `target` attribute method to links and button 204 | 205 | ## 2.28.1 - 2020-11-30 206 | 207 | - add support for PHP 8 208 | 209 | ## 2.28.0 - 2020-09-30 210 | 211 | - add the disabled method to all elements that support the attribute (#165) 212 | 213 | ## 2.27.0 - 2020-09-09 214 | 215 | - Add support for Laravel 8 216 | 217 | ## 2.26.0 - 2020-04-20 218 | 219 | - Internal refactor to normalize availabel attribute methods 220 | 221 | ## 2.25.0 - 2020-03-02 222 | 223 | - add Laravel 7 support 224 | 225 | ## 2.24.0 - 2019-09-04 226 | 227 | - Added number input 228 | 229 | ## 2.23.0 - 2019-09-04 230 | 231 | - Laravel 6 support 232 | - Better handling for `0` values in inputs 233 | - Add `range` for range inputs 234 | - Format date and time values 235 | 236 | ## 2.22.1 - 2019-07-16 237 | 238 | - Prevent password fields to be filled 239 | 240 | ## 2.22.0 - 2019-04-26 241 | 242 | - Changed the `value` parameter in `data` to an optional parameter 243 | 244 | ## 2.21.0 - 2019-02-27 245 | 246 | - Added Laravel 5.8 support 247 | - Dropped PHP 7.0 support 248 | - Dropped Laravel 5.4 support 249 | - Dropped PHPUnit 6 support 250 | 251 | ## 2.20.1 - 2019-02-01 252 | 253 | - use `Arr::` and `Str::` functions 254 | 255 | ## 2.20.0 - 2019-01-18 256 | 257 | - Added `unless` method and magic `__call` handler (e.g. `$input->valueUnless(false, 5)`) 258 | - Added `size` attribute method to `Input` 259 | - Added `name` attribute method to `Button` 260 | - Fixed checkbox value repopulation after request 261 | 262 | ## 2.19.9 - 2019-01-10 263 | 264 | - Improve default of `tel` link 265 | 266 | ## 2.19.8 - 2018-09-04 267 | 268 | - Add support for Laravel 5.7 269 | 270 | ## 2.19.7 - 2018-04-30 271 | 272 | - Allow radio input check "0" value 273 | 274 | ## 2.19.6 - 2018-04-30 275 | 276 | - Correctly prefill form array attributes from the model 277 | 278 | ## 2.19.5 - 2018-04-04 279 | 280 | - Allow `null` children 281 | 282 | ## 2.19.4 - 2018-03-28 283 | 284 | - Revert comparison function change in `2.19.2` 285 | 286 | ## 2.19.2 - 2018-03-26 287 | 288 | - Fixed comparison function for selected options in `Select` 289 | 290 | ## 2.19.1 - 2018-03-23 291 | 292 | - Fixed `Html::radio` auto-generated id's & checked behaviour 293 | 294 | ## 2.19.0 - 2018-03-09 295 | 296 | - Changed `Input::require` to accept a boolean value 297 | 298 | ## 2.18.0 - 2018-03-02 299 | 300 | - Added `I` element class and `Html::i` factory method 301 | 302 | ## 2.17.0 - 2018-02-28 303 | 304 | - Added `Html::value` function that's a public method for `old` 305 | 306 | ## 2.16.0 - 2018-02-26 307 | 308 | - Added `Img` element class and `Html::img` factory method 309 | 310 | ## 2.15.1 - 2018-02-26 311 | 312 | - Removed `id` from CSRF fields 313 | 314 | ## 2.15.0 - 2018-02-23 315 | 316 | - Added `Input::date` and `Input::time` 317 | 318 | ## 2.14.0 - 2018-02-22 319 | 320 | - Added `Input::disabled` 321 | 322 | ## 2.13.1 - 2018-02-20 323 | 324 | - Added `Form::novalidate` 325 | 326 | ## 2.12.1 - 2018-02-08 327 | 328 | - Fixed Laravel 5.6 compatibility 329 | 330 | ## 2.12.0 - 2018-02-08 331 | 332 | - Added Laravel 5.6 compatibility 333 | - Fixed an issue with checkbox values 334 | 335 | ## 2.11.0 - 2018-02-02 336 | 337 | - Add `readonly` method to input 338 | 339 | ## 2.10.3 - 2018-01-09 340 | 341 | - Fix `__call` when using macros 342 | 343 | ## 2.10.2 - 2017-12-28 344 | 345 | - `Htmlable` elements can now be used in the `html()` method 346 | - Array notation is now implicitly converted to dot notation in `old` (e.g. `foo[1] -> foo.1`) 347 | 348 | ## 2.10.1 - 2017-12-18 349 | 350 | - Fixed old values containing `0` 351 | 352 | ## 2.10.0 - 2017-11-08 353 | 354 | - Added `required` method to `Select` 355 | 356 | ## 2.9.0 - 2017-10-20 357 | 358 | - Added `required` method to `Textarea` 359 | 360 | ## 2.8.2 - 2017-10-13 361 | 362 | - Fixed a bug with values that are a `"0"` string 363 | 364 | ## 2.8.1 - 2017-10-12 365 | 366 | - Fixed a bug with values that are a `"0"` string 367 | 368 | ## 2.8.0 - 2017-10-12 369 | 370 | - Added a magic `__call` method that responds to methods ending with `If`, so any method can be called with a condition as it's first argument. The method will only be applied if the condition is truthy. 371 | 372 | ## 2.7.0 - 2017-10-11 373 | 374 | - Added `BaseElement::data` for data attributes 375 | 376 | ## 2.6.0 - 2017-10-11 377 | 378 | - Added `BaseElement::setChildren` to replace all children 379 | - Fixed a bug that didn't select options in optgroups when applying a value 380 | 381 | ## 2.5.0 - 2017-10-11 382 | 383 | - Added `BaseElement::style` for setting the style attribute (with a string or an associative array) 384 | - Added `Html::reset` for form reset buttons 385 | 386 | ## 2.4.1 - 2017-09-07 387 | 388 | - Nothing changed, but `2.2.0` was accidentally tagged as `2.4.0`. This release contains the actual latest version at the time of writing. 389 | 390 | ## 2.3.0 - 2017-09-04 391 | 392 | - Added `checked` and `unchecked` methods to `Input` 393 | 394 | ## 2.2.0 - 2017-08-29 395 | 396 | - Added `Optgroup` element 397 | - Added the ability to create optgroups in `Options` by passing an array of groups with options 398 | 399 | ## 2.1.0 - 2017-08-24 400 | 401 | - Added `Html::file` and a `File` element for file inputs 402 | 403 | ## 2.0.2 - 2017-07-14 404 | 405 | - Fixed an issue that stripped square brackets from element attributes 406 | 407 | ## 2.0.1 - 2017-06-28 408 | 409 | - Fixed the `Html` facade 410 | 411 | ## 2.0.0 - 2017-06-13 412 | 413 | - Minimum requirements have been reduced to PHP 7.0 414 | - Added a `html()` helper function that returns an instance of `Html` 415 | - Added `Macroable` to all elements and `Html` 416 | - Loosened type hints in method signatures for flexibility 417 | - Added `Html::multiselect` method 418 | - Added `Select::multiple` method 419 | 420 | ## 1.5.0 - 2017-05-19 421 | 422 | - Added `class` method to `Html` 423 | 424 | ## 1.4.0 - 2017-05-16 425 | 426 | - Added a `placeholder` method to `Textarea` 427 | 428 | ## 1.3.1 - 2017-05-09 429 | 430 | - Added an empty `value` to `Select::placeholder` 431 | 432 | ## 1.3.0 - 2017-05-08 433 | 434 | - Added a `placeholder` method to `Select` for default empty values 435 | 436 | ## 1.2.0 - 2017-04-28 437 | 438 | - Added a `Html` facade 439 | 440 | ## 1.1.1 - 2017-04-27 441 | 442 | - Fixed an issue where html was escaped when you didn't want it to do that, like in buttons and links 443 | 444 | ## 1.1.0 - 2017-04-19 445 | 446 | - Added `Html::radio` 447 | - Fixed an issue that set the wrong `value` for a checkbox created with `Html::checkbox` 448 | - Fixed a case sensitivity issue with the `Textarea` class 449 | 450 | ## 1.0.0 - 2017-03-31 451 | 452 | - Initial release 453 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | Logo for laravel-html 6 | 7 | 8 | 9 |

Painless HTML generation

10 | 11 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-html.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-html) 12 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 13 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-html.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-html) 14 | 15 |
16 | 17 | This package helps you generate HTML using a clean, simple and easy to read API. All elements can be dynamically generated and put together. The HTML builder helps you generate dynamically assigned form elements based on your selected model, the session or a default value. 18 | 19 | ## Support us 20 | 21 | [](https://spatie.be/github-ad-click/laravel-html) 22 | 23 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 24 | 25 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 26 | 27 | ## Postcardware 28 | 29 | You're free to use this package (it's [MIT-licensed](LICENSE.md)), but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 30 | 31 | Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. 32 | 33 | All postcards are published [on our website](https://spatie.be/en/opensource/postcards). 34 | 35 | ## Installation 36 | 37 | You can install the package via composer: 38 | 39 | ``` bash 40 | composer require spatie/laravel-html 41 | ``` 42 | 43 | And optionally register an alias for the facade. 44 | 45 | ```php 46 | // config/app.php 47 | 'aliases' => [ 48 | ... 49 | 'Html' => Spatie\Html\Facades\Html::class, 50 | ]; 51 | ``` 52 | 53 | ## Usage 54 | 55 | ### Concepts 56 | 57 | Elements—classes under the `Spatie\Html\Elements` namespace—are generally created via a `Spatie\Html\Html` builder instance. 58 | 59 | ```php 60 | html()->span()->text('Hello world!'); 61 | ``` 62 | 63 | Element attributes and contents are modified via with fluent methods which return a new instance. This means element instances are immutable. 64 | 65 | ```php 66 | $icon = html()->span()->class('fa'); 67 | 68 | $icon->class('fa-eye'); // '' 69 | $icon->class('fa-eye-slash'); // '' 70 | ``` 71 | 72 | Element classes don't have any knowledge of the outside world. Any coupling to other concepts, like requests and sessions, should happen in the builder class, not on the element classes. 73 | 74 | By convention, we assume that builder methods will modify values to our advantage (like pulling old values from the session on a failed form request), and element methods will be deterministic. 75 | 76 | ```php 77 | // This will try to resolve an initial value, and fall back to 'hello@example.com' 78 | $email = html()->email('email', 'hello@example.com'); 79 | 80 | // This will always have 'hello@example.com' as it's value 81 | $email = html()->email('email')->value('hello@example.com'); 82 | ``` 83 | 84 | ## Upgrading 85 | 86 | ### From v1 to v2 87 | 88 | Version 2 was created because the typehints in version 1 was holding the package back in some cases (like multiple select which requires an array of values instead of a string which was assumed). 89 | 90 | Luckily, bumping the version number in `composer.json` and running `composer update` should be non-breaking. Here are some caveats to look out for: 91 | 92 | - The package now ships with a `html()` function by default, which returns an instance of the `Html` builder class. If you've defined your own method, you'll need to remove it. 93 | - Various type hints have been removed throughout the package, if you've extended a class to override its methods, you'll need to update them accordingly (everything still behaves the same!) 94 | 95 | ## Changelog 96 | 97 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 98 | 99 | ## Testing 100 | 101 | ```bash 102 | $ composer test 103 | ``` 104 | 105 | ## Contributing 106 | 107 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 108 | 109 | ## Security 110 | 111 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 112 | 113 | ## Credits 114 | 115 | - [Sebastian De Deyne](https://github.com/sebastiandedeyne) 116 | - [Freek Van der Herten](https://github.com/freekmurze) 117 | - [All Contributors](../../contributors) 118 | 119 | ## About Spatie 120 | Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 121 | 122 | ## License 123 | 124 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 125 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-html", 3 | "description": "A fluent html builder", 4 | "keywords": [ 5 | "spatie", 6 | "html" 7 | ], 8 | "homepage": "https://github.com/spatie/laravel-html", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Sebastian De Deyne", 13 | "email": "sebastian@spatie.be", 14 | "homepage": "https://spatie.be", 15 | "role": "Developer" 16 | }, 17 | { 18 | "name": "Freek Van der Herten", 19 | "email": "freek@spatie.be", 20 | "homepage": "https://spatie.be", 21 | "role": "Developer" 22 | } 23 | ], 24 | "require": { 25 | "php": "^8.2", 26 | "illuminate/http": "^10.0|^11.0|^12.0", 27 | "illuminate/support": "^10.0|^11.0|^12.0" 28 | }, 29 | "require-dev": { 30 | "mockery/mockery": "^1.3", 31 | "orchestra/testbench": "^8.0|^9.0|^10.0", 32 | "pestphp/pest": "^2.34|^3.7" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Spatie\\Html\\": "src" 37 | }, 38 | "files": [ 39 | "src/helpers.php" 40 | ] 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Spatie\\Html\\Test\\": "tests" 45 | } 46 | }, 47 | "scripts": { 48 | "test": "vendor/bin/pest" 49 | }, 50 | "config": { 51 | "sort-packages": true, 52 | "allow-plugins": { 53 | "pestphp/pest-plugin": true 54 | } 55 | }, 56 | "extra": { 57 | "laravel": { 58 | "providers": [ 59 | "Spatie\\Html\\HtmlServiceProvider" 60 | ], 61 | "aliases": { 62 | "Html": "Spatie\\Html\\Facades\\Html" 63 | } 64 | } 65 | }, 66 | "minimum-stability": "dev", 67 | "prefer-stable": true 68 | } 69 | -------------------------------------------------------------------------------- /src/Attributes.php: -------------------------------------------------------------------------------- 1 | $value) { 23 | if ($attribute === 'class') { 24 | $this->addClass($value); 25 | 26 | continue; 27 | } 28 | 29 | if (is_int($attribute)) { 30 | $attribute = $value; 31 | $value = ''; 32 | } 33 | 34 | $this->setAttribute($attribute, (string) $value); 35 | } 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * @param string $attribute 42 | * @param string|null $value 43 | * 44 | * @return $this 45 | */ 46 | public function setAttribute($attribute, $value = null) 47 | { 48 | if ($attribute === 'class') { 49 | $this->addClass($value); 50 | 51 | return $this; 52 | } 53 | 54 | $this->attributes[$attribute] = $value; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @param string $attribute 61 | * 62 | * @return $this 63 | */ 64 | public function forgetAttribute($attribute) 65 | { 66 | if ($attribute === 'class') { 67 | $this->classes = []; 68 | 69 | return $this; 70 | } 71 | 72 | if (isset($this->attributes[$attribute])) { 73 | unset($this->attributes[$attribute]); 74 | } 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * @param string $attribute 81 | * @param mixed $fallback 82 | * 83 | * @return mixed 84 | */ 85 | public function getAttribute($attribute, $fallback = null) 86 | { 87 | if ($attribute === 'class') { 88 | return implode(' ', $this->classes); 89 | } 90 | 91 | return $this->attributes[$attribute] ?? $fallback; 92 | } 93 | 94 | public function hasAttribute(string $attribute): bool 95 | { 96 | return array_key_exists($attribute, $this->attributes); 97 | } 98 | 99 | /** 100 | * @param string|iterable $class 101 | */ 102 | public function addClass($class) 103 | { 104 | if (is_string($class)) { 105 | $class = explode(' ', $class); 106 | } 107 | 108 | $class = Arr::getToggledValues($class); 109 | 110 | $this->classes = array_unique( 111 | array_merge($this->classes, $class) 112 | ); 113 | } 114 | 115 | /** 116 | * @return bool 117 | */ 118 | public function isEmpty() 119 | { 120 | return empty($this->attributes) && empty($this->classes); 121 | } 122 | 123 | /** 124 | * @return array 125 | */ 126 | public function toArray() 127 | { 128 | if (empty($this->classes)) { 129 | return $this->attributes; 130 | } 131 | 132 | return array_merge(['class' => implode(' ', $this->classes)], $this->attributes); 133 | } 134 | 135 | /** 136 | * @return string 137 | */ 138 | public function render() 139 | { 140 | if ($this->isEmpty()) { 141 | return ''; 142 | } 143 | 144 | $attributeStrings = []; 145 | 146 | foreach ($this->toArray() as $attribute => $value) { 147 | if ($value === '') { 148 | $attributeStrings[] = $attribute; 149 | 150 | continue; 151 | } 152 | 153 | $value = htmlentities($value, ENT_QUOTES, 'UTF-8', false); 154 | 155 | $attributeStrings[] = "{$attribute}=\"{$value}\""; 156 | } 157 | 158 | return implode(' ', $attributeStrings); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/BaseElement.php: -------------------------------------------------------------------------------- 1 | tag)) { 36 | throw MissingTag::onClass(static::class); 37 | } 38 | 39 | $this->attributes = new Attributes(); 40 | $this->children = new Collection(); 41 | } 42 | 43 | public static function create() 44 | { 45 | return new static(); 46 | } 47 | 48 | /** 49 | * @param string $attribute 50 | * @param string|null $value 51 | * 52 | * @return static 53 | */ 54 | public function attribute($attribute, $value = null) 55 | { 56 | $element = clone $this; 57 | 58 | $element->attributes->setAttribute($attribute, (string) $value); 59 | 60 | return $element; 61 | } 62 | 63 | /** 64 | * @param iterable $attributes 65 | * 66 | * @return static 67 | */ 68 | public function attributes($attributes) 69 | { 70 | $element = clone $this; 71 | 72 | $element->attributes->setAttributes($attributes); 73 | 74 | return $element; 75 | } 76 | 77 | /** 78 | * @param string $attribute 79 | * 80 | * @return static 81 | */ 82 | public function forgetAttribute($attribute) 83 | { 84 | $element = clone $this; 85 | 86 | $element->attributes->forgetAttribute($attribute); 87 | 88 | return $element; 89 | } 90 | 91 | /** 92 | * @param string $attribute 93 | * @param mixed $fallback 94 | * 95 | * @return mixed 96 | */ 97 | public function getAttribute($attribute, $fallback = null) 98 | { 99 | return $this->attributes->getAttribute($attribute, $fallback); 100 | } 101 | 102 | /** 103 | * @param string $attribute 104 | * 105 | * @return bool 106 | */ 107 | public function hasAttribute($attribute) 108 | { 109 | return $this->attributes->hasAttribute($attribute); 110 | } 111 | 112 | /** 113 | * @param iterable|string $class 114 | * 115 | * @return static 116 | */ 117 | public function class($class) 118 | { 119 | return $this->addClass($class); 120 | } 121 | 122 | /** 123 | * Alias for `class`. 124 | * 125 | * @param iterable|string $class 126 | * 127 | * @return static 128 | */ 129 | public function addClass($class) 130 | { 131 | $element = clone $this; 132 | 133 | $element->attributes->addClass($class); 134 | 135 | return $element; 136 | } 137 | 138 | /** 139 | * @param string $id 140 | * 141 | * @return static 142 | */ 143 | public function id($id) 144 | { 145 | return $this->attribute('id', $id); 146 | } 147 | 148 | /** 149 | * @param array|string|null $style 150 | * 151 | * @return static 152 | */ 153 | public function style($style) 154 | { 155 | if (is_array($style)) { 156 | $style = implode('; ', array_map(function ($value, $attribute) { 157 | return "{$attribute}: {$value}"; 158 | }, $style, array_keys($style))); 159 | } 160 | 161 | return $this->attribute('style', $style); 162 | } 163 | 164 | /** 165 | * @param string $name 166 | * @param string $value 167 | * 168 | * @return static 169 | */ 170 | public function data($name, $value = null) 171 | { 172 | return $this->attribute("data-{$name}", $value); 173 | } 174 | 175 | /** 176 | * @param string $attribute 177 | * @param string|null $value 178 | * 179 | * @return static 180 | */ 181 | public function aria($attribute, $value = null) 182 | { 183 | return $this->attribute("aria-{$attribute}", $value); 184 | } 185 | 186 | /** 187 | * @param \Spatie\Html\HtmlElement|string|iterable|int|float|null $children 188 | * @param callable|null $mapper 189 | * 190 | * @return static 191 | */ 192 | public function addChildren($children, $mapper = null) 193 | { 194 | if (is_null($children)) { 195 | return $this; 196 | } 197 | 198 | $children = $this->parseChildren($children, $mapper); 199 | 200 | $element = clone $this; 201 | 202 | $element->children = $element->children->merge($children); 203 | 204 | return $element; 205 | } 206 | 207 | /** 208 | * Alias for `addChildren`. 209 | * 210 | * @param \Spatie\Html\HtmlElement|string|iterable|int|float|null $children 211 | * @param callable|null $mapper 212 | * 213 | * @return static 214 | */ 215 | public function addChild($child, $mapper = null) 216 | { 217 | return $this->addChildren($child, $mapper); 218 | } 219 | 220 | /** 221 | * Alias for `addChildren`. 222 | * 223 | * @param \Spatie\Html\HtmlElement|string|iterable|int|float|null $children 224 | * @param callable|null $mapper 225 | * 226 | * @return static 227 | */ 228 | public function child($child, $mapper = null) 229 | { 230 | return $this->addChildren($child, $mapper); 231 | } 232 | 233 | /** 234 | * Alias for `addChildren`. 235 | * 236 | * @param \Spatie\Html\HtmlElement|string|iterable|int|float|null $children 237 | * @param callable|null $mapper 238 | * 239 | * @return static 240 | */ 241 | public function children($children, $mapper = null) 242 | { 243 | return $this->addChildren($children, $mapper); 244 | } 245 | 246 | /** 247 | * Replace all children with an array of elements. 248 | * 249 | * @param \Spatie\Html\HtmlElement[] $children 250 | * @param callable|null $mapper 251 | * 252 | * @return static 253 | */ 254 | public function setChildren($children, $mapper = null) 255 | { 256 | $element = clone $this; 257 | 258 | $element->children = new Collection(); 259 | 260 | return $element->addChildren($children, $mapper); 261 | } 262 | 263 | /** 264 | * @param \Spatie\Html\HtmlElement|string|iterable|int|float|null $children 265 | * @param callable|null $mapper 266 | * 267 | * @return static 268 | */ 269 | public function prependChildren($children, $mapper = null) 270 | { 271 | $children = $this->parseChildren($children, $mapper); 272 | 273 | $element = clone $this; 274 | 275 | $element->children = $children->merge($element->children); 276 | 277 | return $element; 278 | } 279 | 280 | /** 281 | * Alias for `prependChildren`. 282 | * 283 | * @param \Spatie\Html\HtmlElement|string|iterable|int|float|null $children 284 | * @param callable|null $mapper 285 | * 286 | * @return static 287 | */ 288 | public function prependChild($children, $mapper = null) 289 | { 290 | return $this->prependChildren($children, $mapper); 291 | } 292 | 293 | /** 294 | * @param string|null $text 295 | * 296 | * @return static 297 | */ 298 | public function text($text) 299 | { 300 | return $this->html(htmlentities($text ?? '', ENT_QUOTES, 'UTF-8', false)); 301 | } 302 | 303 | /** 304 | * @param string|null $html 305 | * 306 | * @return static 307 | */ 308 | public function html($html) 309 | { 310 | if ($this->isVoidElement()) { 311 | throw new InvalidHtml("Can't set inner contents on `{$this->tag}` because it's a void element"); 312 | } 313 | 314 | return $this->setChildren($html); 315 | } 316 | 317 | /** 318 | * Conditionally transform the element. Note that since elements are 319 | * immutable, you'll need to return a new instance from the callback. 320 | * 321 | * @param bool $condition 322 | * @param \Closure $callback 323 | * 324 | * @return mixed 325 | */ 326 | public function if(bool $condition, \Closure $callback) 327 | { 328 | return $condition ? $callback($this) : $this; 329 | } 330 | 331 | /** 332 | * Conditionally transform the element. Note that since elements are 333 | * immutable, you'll need to return a new instance from the callback. 334 | * 335 | * @param bool $condition 336 | * @param \Closure $callback 337 | * 338 | * @return mixed 339 | */ 340 | public function unless(bool $condition, \Closure $callback) 341 | { 342 | return $this->if(! $condition, $callback); 343 | } 344 | 345 | /** 346 | * Conditionally transform the element. Note that since elements are 347 | * immutable, you'll need to return a new instance from the callback. 348 | * 349 | * @param mixed $value 350 | * @param \Closure $callback 351 | * 352 | * @return mixed 353 | */ 354 | public function ifNotNull($value, \Closure $callback) 355 | { 356 | return ! is_null($value) ? $callback($this) : $this; 357 | } 358 | 359 | /** 360 | * @return \Illuminate\Contracts\Support\Htmlable 361 | */ 362 | public function open() 363 | { 364 | $tag = $this->attributes->isEmpty() 365 | ? '<' . $this->tag . '>' 366 | : "<{$this->tag} {$this->attributes->render()}>"; 367 | 368 | $children = $this->children->map(function ($child): string { 369 | if ($child instanceof HtmlElement) { 370 | return $child->render(); 371 | } 372 | 373 | if (is_null($child)) { 374 | return ''; 375 | } 376 | 377 | if (is_string($child) || is_numeric($child)) { 378 | return $child; 379 | } 380 | 381 | throw InvalidChild::childMustBeAnHtmlElementOrAString(); 382 | })->implode(''); 383 | 384 | return new HtmlString($tag . $children); 385 | } 386 | 387 | /** 388 | * @return \Illuminate\Contracts\Support\Htmlable 389 | */ 390 | public function close() 391 | { 392 | return new HtmlString( 393 | $this->isVoidElement() 394 | ? '' 395 | : "tag}>" 396 | ); 397 | } 398 | 399 | /** 400 | * @return \Illuminate\Contracts\Support\Htmlable 401 | */ 402 | public function render() 403 | { 404 | return new HtmlString( 405 | $this->open() . $this->close() 406 | ); 407 | } 408 | 409 | public function isVoidElement(): bool 410 | { 411 | return in_array($this->tag, [ 412 | 'area', 'base', 'br', 'col', 'embed', 'hr', 413 | 'img', 'input', 'keygen', 'link', 'menuitem', 414 | 'meta', 'param', 'source', 'track', 'wbr', 415 | ]); 416 | } 417 | 418 | /** 419 | * Dynamically handle calls to the class. 420 | * Check for methods finishing by If or fallback to Macroable. 421 | * 422 | * @param string $name 423 | * @param array $arguments 424 | * @return mixed 425 | * 426 | * @throws BadMethodCallException 427 | */ 428 | public function __call($name, $arguments) 429 | { 430 | if (Str::endsWith($name, $conditions = ['If', 'Unless', 'IfNotNull'])) { 431 | foreach ($conditions as $condition) { 432 | if (! method_exists($this, $method = str_replace($condition, '', $name))) { 433 | continue; 434 | } 435 | 436 | return $this->callConditionalMethod($condition, $method, $arguments); 437 | } 438 | } 439 | 440 | return $this->__macro_call($name, $arguments); 441 | } 442 | 443 | protected function callConditionalMethod($type, $method, array $arguments) 444 | { 445 | $value = array_shift($arguments); 446 | $callback = function () use ($method, $arguments) { 447 | return $this->{$method}(...$arguments); 448 | }; 449 | 450 | switch ($type) { 451 | case 'If': 452 | return $this->if((bool) $value, $callback); 453 | case 'Unless': 454 | return $this->unless((bool) $value, $callback); 455 | case 'IfNotNull': 456 | return $this->ifNotNull($value, $callback); 457 | default: 458 | return $this; 459 | } 460 | } 461 | 462 | public function __clone() 463 | { 464 | $this->attributes = clone $this->attributes; 465 | $this->children = clone $this->children; 466 | } 467 | 468 | public function __toString(): string 469 | { 470 | return $this->render(); 471 | } 472 | 473 | public function toHtml(): string 474 | { 475 | return $this->render(); 476 | } 477 | 478 | protected function parseChildren($children, $mapper = null): Collection 479 | { 480 | if ($children instanceof HtmlElement) { 481 | $children = [$children]; 482 | } elseif ($children instanceof Htmlable) { 483 | $children = $children->toHtml(); 484 | } 485 | 486 | $children = Collection::make($children); 487 | 488 | if ($mapper) { 489 | $children = $children->map($mapper); 490 | } 491 | 492 | $this->guardAgainstInvalidChildren($children); 493 | 494 | return $children; 495 | } 496 | 497 | protected function guardAgainstInvalidChildren(Collection $children) 498 | { 499 | foreach ($children as $child) { 500 | if ($child instanceof HtmlElement || is_null($child) || is_string($child) || is_numeric($child)) { 501 | continue; 502 | } 503 | 504 | throw InvalidChild::childMustBeAnHtmlElementOrAString(); 505 | } 506 | } 507 | } 508 | -------------------------------------------------------------------------------- /src/Elements/A.php: -------------------------------------------------------------------------------- 1 | attribute('href', $href); 22 | } 23 | 24 | /** 25 | * @param string|null $route 26 | * @param mixed $params 27 | * 28 | * @return static 29 | */ 30 | public function route($route, ...$params) 31 | { 32 | return $this->href(route($route, ...$params)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Elements/Attributes/Autocomplete.php: -------------------------------------------------------------------------------- 1 | attribute('autocomplete', $value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Elements/Attributes/Autofocus.php: -------------------------------------------------------------------------------- 1 | attribute('autofocus') 16 | : $this->forgetAttribute('autofocus'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Elements/Attributes/Disabled.php: -------------------------------------------------------------------------------- 1 | attribute('disabled', 'disabled') 16 | : $this->forgetAttribute('disabled'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Elements/Attributes/MinMaxLength.php: -------------------------------------------------------------------------------- 1 | attribute('minlength', $minlength); 15 | } 16 | 17 | /** 18 | * @param int $maxlength 19 | * 20 | * @return static 21 | */ 22 | public function maxlength(int $maxlength) 23 | { 24 | return $this->attribute('maxlength', $maxlength); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Elements/Attributes/Name.php: -------------------------------------------------------------------------------- 1 | attribute('name', $name); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Elements/Attributes/Placeholder.php: -------------------------------------------------------------------------------- 1 | attribute('placeholder', $placeholder); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Elements/Attributes/ReadonlyTrait.php: -------------------------------------------------------------------------------- 1 | attribute('readonly') 16 | : $this->forgetAttribute('readonly'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Elements/Attributes/Required.php: -------------------------------------------------------------------------------- 1 | attribute('required') 16 | : $this->forgetAttribute('required'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Elements/Attributes/Target.php: -------------------------------------------------------------------------------- 1 | attribute('target', $target); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Elements/Attributes/Type.php: -------------------------------------------------------------------------------- 1 | attribute('type', $type); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Elements/Attributes/Value.php: -------------------------------------------------------------------------------- 1 | attribute('value', $value); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Elements/Button.php: -------------------------------------------------------------------------------- 1 | newInstanceWithoutConstructor(); 20 | 21 | $element->tag = $tag; 22 | $element->attributes = new Attributes(); 23 | $element->children = new Collection(); 24 | 25 | return $element; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Elements/Fieldset.php: -------------------------------------------------------------------------------- 1 | prependChild( 22 | Legend::create()->text($contents) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Elements/File.php: -------------------------------------------------------------------------------- 1 | attributes->setAttribute('type', 'file'); 29 | } 30 | 31 | /** 32 | * @param string|null $name 33 | * 34 | * @return static 35 | */ 36 | public function accept($type) 37 | { 38 | return $this->attribute('accept', $type); 39 | } 40 | 41 | /** 42 | * @return static 43 | */ 44 | public function acceptAudio() 45 | { 46 | return $this->attribute('accept', self::ACCEPT_AUDIO); 47 | } 48 | 49 | /** 50 | * @return static 51 | */ 52 | public function acceptVideo() 53 | { 54 | return $this->attribute('accept', self::ACCEPT_VIDEO); 55 | } 56 | 57 | /** 58 | * @return static 59 | */ 60 | public function acceptImage() 61 | { 62 | return $this->attribute('accept', self::ACCEPT_IMAGE); 63 | } 64 | 65 | /** 66 | * @return static 67 | */ 68 | public function multiple() 69 | { 70 | return $this->attribute('multiple'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Elements/Form.php: -------------------------------------------------------------------------------- 1 | attribute('action', $action); 26 | } 27 | 28 | /** 29 | * @param string|null $route 30 | * @param mixed $params 31 | * 32 | * @return static 33 | */ 34 | public function route($route, ...$params) 35 | { 36 | return $this->action(route($route, ...$params)); 37 | } 38 | 39 | /** 40 | * @param string|null $method 41 | * 42 | * @return static 43 | */ 44 | public function method($method) 45 | { 46 | return $this->attribute('method', $method); 47 | } 48 | 49 | /** 50 | * @param bool $novalidate 51 | * 52 | * @return static 53 | */ 54 | public function novalidate($novalidate = true) 55 | { 56 | return $novalidate 57 | ? $this->attribute('novalidate') 58 | : $this->forgetAttribute('novalidate'); 59 | } 60 | 61 | /** 62 | * @return static 63 | */ 64 | public function acceptsFiles() 65 | { 66 | return $this->attribute('enctype', 'multipart/form-data'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Elements/I.php: -------------------------------------------------------------------------------- 1 | attribute('alt', $alt); 19 | } 20 | 21 | /** 22 | * @param string|null $src 23 | * 24 | * @return static 25 | */ 26 | public function src($src) 27 | { 28 | return $this->attribute('src', $src); 29 | } 30 | 31 | /** 32 | * @param string|null $srcset 33 | * 34 | * @return static 35 | */ 36 | public function srcset($srcset) 37 | { 38 | return $this->attribute('srcset', $srcset); 39 | } 40 | 41 | /** 42 | * @param string|null $loading 43 | * 44 | * @return static 45 | */ 46 | public function loading($loading) 47 | { 48 | return $this->attribute('loading', $loading); 49 | } 50 | 51 | /** 52 | * @param string|null $crossorigin 53 | * 54 | * @return static 55 | */ 56 | public function crossorigin($crossorigin) 57 | { 58 | return $this->attribute('crossorigin', $crossorigin); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Elements/Input.php: -------------------------------------------------------------------------------- 1 | checked(false); 38 | } 39 | 40 | /** 41 | * @param bool $checked 42 | * 43 | * @return static 44 | */ 45 | public function checked($checked = true) 46 | { 47 | return $checked 48 | ? $this->attribute('checked', 'checked') 49 | : $this->forgetAttribute('checked'); 50 | } 51 | 52 | /** 53 | * @param string|null $size 54 | * 55 | * @return static 56 | */ 57 | public function size($size) 58 | { 59 | return $this->attribute('size', $size); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Elements/Label.php: -------------------------------------------------------------------------------- 1 | attribute('for', $for); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Elements/Legend.php: -------------------------------------------------------------------------------- 1 | attribute('label', $label); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Elements/Option.php: -------------------------------------------------------------------------------- 1 | attribute('selected', 'selected'); 24 | } 25 | 26 | /** 27 | * @param bool $condition 28 | * 29 | * @return static 30 | */ 31 | public function selectedIf($condition) 32 | { 33 | return $condition ? 34 | $this->selected() : 35 | $this->unselected(); 36 | } 37 | 38 | /** 39 | * @return static 40 | */ 41 | public function unselected() 42 | { 43 | return $this->forgetAttribute('selected'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Elements/P.php: -------------------------------------------------------------------------------- 1 | attribute('multiple'); 42 | 43 | $name = $element->getAttribute('name'); 44 | 45 | if ($name && ! Str::endsWith($name, '[]')) { 46 | $element = $element->name($name.'[]'); 47 | } 48 | 49 | $element->applyValueToOptions(); 50 | 51 | return $element; 52 | } 53 | 54 | /** 55 | * @param iterable $options 56 | * 57 | * @return static 58 | */ 59 | public function options($options) 60 | { 61 | return $this->addChildren($options, function ($text, $value) { 62 | if (is_array($text) || $text instanceof Collection) { 63 | return $this->optgroup($value, $text); 64 | } 65 | 66 | return Option::create() 67 | ->value($value) 68 | ->text($text) 69 | ->selectedIf($value === $this->value); 70 | }); 71 | } 72 | 73 | /** 74 | * @param string $label 75 | * @param iterable $options 76 | * 77 | * @return static 78 | */ 79 | public function optgroup($label, $options) 80 | { 81 | return Optgroup::create() 82 | ->label($label) 83 | ->addChildren($options, function ($text, $value) { 84 | return Option::create() 85 | ->value($value) 86 | ->text($text) 87 | ->selectedIf($value === $this->value); 88 | }); 89 | } 90 | 91 | /** 92 | * @param string|null $text 93 | * 94 | * @return static 95 | */ 96 | public function placeholder($text) 97 | { 98 | return $this->prependChild( 99 | Option::create() 100 | ->value(null) 101 | ->text($text) 102 | ->selectedIf(! $this->hasSelection()) 103 | ); 104 | } 105 | 106 | /** 107 | * @param string|iterable $value 108 | * 109 | * @return static 110 | */ 111 | public function value($value = null) 112 | { 113 | $element = clone $this; 114 | 115 | $element->value = $value; 116 | 117 | $element->applyValueToOptions(); 118 | 119 | return $element; 120 | } 121 | 122 | protected function hasSelection() 123 | { 124 | return $this->children->contains->hasAttribute('selected'); 125 | } 126 | 127 | protected function applyValueToOptions() 128 | { 129 | $value = $this->value instanceof \Illuminate\Support\Collection 130 | ? $this->value 131 | : Collection::make($this->value); 132 | 133 | if (! $this->hasAttribute('multiple')) { 134 | $value = $value->take(1); 135 | } 136 | 137 | $this->children = $this->applyValueToElements($value, $this->children); 138 | } 139 | 140 | protected function applyValueToElements($value, Collection $children) 141 | { 142 | return $children->map(function ($child) use ($value) { 143 | if ($child instanceof Optgroup) { 144 | return $child->setChildren($this->applyValueToElements($value, $child->children)); 145 | } 146 | 147 | if ($child instanceof Selectable) { 148 | return $child->selectedIf($value->contains($child->getAttribute('value'))); 149 | } 150 | 151 | return $child; 152 | }); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Elements/Span.php: -------------------------------------------------------------------------------- 1 | html($value); 37 | } 38 | 39 | /** 40 | * @param int $rows 41 | * 42 | * @return static 43 | */ 44 | public function rows(int $rows) 45 | { 46 | return $this->attribute('rows', $rows); 47 | } 48 | 49 | /** 50 | * @param int $cols 51 | * 52 | * @return static 53 | */ 54 | public function cols(int $cols) 55 | { 56 | return $this->attribute('cols', $cols); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidChild.php: -------------------------------------------------------------------------------- 1 | true, 'bar' => false, 'baz']) 17 | * // => ['foo', 'baz'] 18 | * 19 | * @param mixed $map 20 | * 21 | * @return array 22 | */ 23 | public static function getToggledValues($map) 24 | { 25 | return Collection::make($map)->map(function ($condition, $value) { 26 | if (is_numeric($value)) { 27 | return $condition; 28 | } 29 | 30 | return $condition ? $value : null; 31 | })->filter()->toArray(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Html.php: -------------------------------------------------------------------------------- 1 | request = $request; 49 | } 50 | 51 | /** 52 | * @param string|null $href 53 | * @param string|null $text 54 | * 55 | * @return \Spatie\Html\Elements\A 56 | */ 57 | public function a($href = null, $contents = null) 58 | { 59 | return A::create() 60 | ->attributeIf($href, 'href', $href) 61 | ->html($contents); 62 | } 63 | 64 | /** 65 | * @param string|null $href 66 | * @param string|null $text 67 | * 68 | * @return \Spatie\Html\Elements\I 69 | */ 70 | public function i($contents = null) 71 | { 72 | return I::create() 73 | ->html($contents); 74 | } 75 | 76 | /** 77 | * @param \Spatie\Html\HtmlElement|string|null $contents 78 | * 79 | * @return \Spatie\Html\Elements\P 80 | */ 81 | public function p($contents = null) 82 | { 83 | return P::create() 84 | ->html($contents); 85 | } 86 | 87 | /** 88 | * @param string|null $type 89 | * @param string|null $text 90 | * 91 | * @return \Spatie\Html\Elements\Button 92 | */ 93 | public function button($contents = null, $type = null, $name = null) 94 | { 95 | return Button::create() 96 | ->attributeIf($type, 'type', $type) 97 | ->attributeIf($name, 'name', $this->fieldName($name)) 98 | ->html($contents); 99 | } 100 | 101 | /** 102 | * @param \Illuminate\Support\Collection|iterable|string $classes 103 | * 104 | * @return \Illuminate\Contracts\Support\Htmlable 105 | */ 106 | public function class($classes): Htmlable 107 | { 108 | if ($classes instanceof Collection) { 109 | $classes = $classes->toArray(); 110 | } 111 | 112 | $attributes = new Attributes(); 113 | $attributes->addClass($classes); 114 | 115 | return new HtmlString( 116 | $attributes->render() 117 | ); 118 | } 119 | 120 | /** 121 | * @param string|null $name 122 | * @param bool $checked 123 | * @param string|null $value 124 | * 125 | * @return \Spatie\Html\Elements\Input 126 | */ 127 | public function checkbox($name = null, $checked = null, $value = '1') 128 | { 129 | return $this->input('checkbox', $name, $value) 130 | ->attributeIf(! is_null($value), 'value', $value) 131 | ->attributeIf((bool) $this->old($name, $checked), 'checked'); 132 | } 133 | 134 | /** 135 | * @param \Spatie\Html\HtmlElement|string|iterable|int|float|null $contents 136 | * 137 | * @return \Spatie\Html\Elements\Div 138 | */ 139 | public function div($contents = null) 140 | { 141 | return Div::create()->children($contents); 142 | } 143 | 144 | /** 145 | * @param string|null $name 146 | * @param string|null $value 147 | * 148 | * @return \Spatie\Html\Elements\Input 149 | */ 150 | public function email($name = null, $value = null) 151 | { 152 | return $this->input('email', $name, $value); 153 | } 154 | 155 | /** 156 | * @param string|null $name 157 | * @param string|null $value 158 | * 159 | * @return \Spatie\Html\Elements\Input 160 | */ 161 | public function search($name = null, $value = null) 162 | { 163 | return $this->input('search', $name, $value); 164 | } 165 | 166 | /** 167 | * @param string|null $name 168 | * @param string|null $value 169 | * @param bool $format 170 | * 171 | * @return \Spatie\Html\Elements\Input 172 | */ 173 | public function date($name = '', $value = null, $format = true) 174 | { 175 | $element = $this->input('date', $name, $value); 176 | 177 | if (! $format || empty($element->getAttribute('value'))) { 178 | return $element; 179 | } 180 | 181 | return $element->value($this->formatDateTime($element->getAttribute('value'), self::HTML_DATE_FORMAT)); 182 | } 183 | 184 | /** 185 | * @param string|null $name 186 | * @param string|null $value 187 | * @param bool $format 188 | * 189 | * @return \Spatie\Html\Elements\Input 190 | */ 191 | public function datetime($name = '', $value = null, $format = true) 192 | { 193 | $element = $this->input('datetime-local', $name, $value); 194 | 195 | if (! $format || empty($element->getAttribute('value'))) { 196 | return $element; 197 | } 198 | 199 | return $element->value($this->formatDateTime( 200 | $element->getAttribute('value'), 201 | self::HTML_DATE_FORMAT.'\T'.self::HTML_TIME_FORMAT 202 | )); 203 | } 204 | 205 | /** 206 | * @param string|null $name 207 | * @param string|null $value 208 | * @param string|null $min 209 | * @param string|null $max 210 | * @param string|null $step 211 | * 212 | * @return \Spatie\Html\Elements\Input 213 | */ 214 | public function range($name = '', $value = null, $min = null, $max = null, $step = null) 215 | { 216 | return $this->input('range', $name, $value) 217 | ->attributeIfNotNull($min, 'min', $min) 218 | ->attributeIfNotNull($max, 'max', $max) 219 | ->attributeIfNotNull($step, 'step', $step); 220 | } 221 | 222 | /** 223 | * @param string|null $name 224 | * @param string|null $value 225 | * @param bool $format 226 | * 227 | * @return \Spatie\Html\Elements\Input 228 | */ 229 | public function time($name = '', $value = null, $format = true) 230 | { 231 | $element = $this->input('time', $name, $value); 232 | 233 | if (! $format || empty($element->getAttribute('value'))) { 234 | return $element; 235 | } 236 | 237 | return $element->value($this->formatDateTime($element->getAttribute('value'), self::HTML_TIME_FORMAT)); 238 | } 239 | 240 | /** 241 | * @param string $tag 242 | * 243 | * @return \Spatie\Html\Elements\Element 244 | */ 245 | public function element($tag) 246 | { 247 | return Element::withTag($tag); 248 | } 249 | 250 | /** 251 | * @param string|null $type 252 | * @param string|null $name 253 | * @param string|null $value 254 | * 255 | * @return \Spatie\Html\Elements\Input 256 | */ 257 | public function input($type = null, $name = null, $value = null) 258 | { 259 | $hasValue = ! is_null($value) || ($type !== 'password' && ! is_null($this->old($name, $value))); 260 | 261 | return Input::create() 262 | ->attributeIf($type, 'type', $type) 263 | ->attributeIf($name, 'name', $this->fieldName($name)) 264 | ->attributeIf($name, 'id', $this->fieldName($name)) 265 | ->attributeIf($hasValue, 'value', $this->old($name, $value)); 266 | } 267 | 268 | /** 269 | * @param \Spatie\Html\HtmlElement|string|null $legend 270 | * 271 | * @return \Spatie\Html\Elements\Fieldset 272 | */ 273 | public function fieldset($legend = null) 274 | { 275 | return $legend 276 | ? Fieldset::create()->legend($legend) 277 | : Fieldset::create(); 278 | } 279 | 280 | /** 281 | * @param string $method 282 | * @param string|null $action 283 | * 284 | * @return \Spatie\Html\Elements\Form 285 | */ 286 | public function form($method = 'POST', $action = null) 287 | { 288 | $method = strtoupper($method); 289 | $form = Form::create(); 290 | 291 | // If Laravel needs to spoof the form's method, we'll append a hidden 292 | // field containing the actual method 293 | if (in_array($method, ['DELETE', 'PATCH', 'PUT'])) { 294 | $form = $form->addChild($this->hidden('_method')->value($method)); 295 | } 296 | 297 | // On any other method that get, the form needs a CSRF token 298 | if ($method !== 'GET') { 299 | $form = $form->addChild($this->token()); 300 | } 301 | 302 | return $form 303 | ->method($method === 'GET' ? 'GET' : 'POST') 304 | ->attributeIf($action, 'action', $action); 305 | } 306 | 307 | /** 308 | * @param string|null $name 309 | * @param string|null $value 310 | * 311 | * @return \Spatie\Html\Elements\Input 312 | */ 313 | public function hidden($name = null, $value = null) 314 | { 315 | return $this->input('hidden', $name, $value); 316 | } 317 | 318 | /** 319 | * @param string|null $src 320 | * @param string|null $alt 321 | * 322 | * @return \Spatie\Html\Elements\Img 323 | */ 324 | public function img($src = null, $alt = null) 325 | { 326 | return Img::create() 327 | ->attributeIf($src, 'src', $src) 328 | ->attributeIf($alt, 'alt', $alt); 329 | } 330 | 331 | /** 332 | * @param \Spatie\Html\HtmlElement|iterable|string|null $contents 333 | * @param string|null $for 334 | * 335 | * @return \Spatie\Html\Elements\Label 336 | */ 337 | public function label($contents = null, $for = null) 338 | { 339 | return Label::create() 340 | ->attributeIf($for, 'for', $this->fieldName($for)) 341 | ->children($contents); 342 | } 343 | 344 | /** 345 | * @param \Spatie\Html\HtmlElement|string|null $contents 346 | * 347 | * @return \Spatie\Html\Elements\Legend 348 | */ 349 | public function legend($contents = null) 350 | { 351 | return Legend::create()->html($contents); 352 | } 353 | 354 | /** 355 | * @param string $email 356 | * @param string|null $text 357 | * 358 | * @return \Spatie\Html\Elements\A 359 | */ 360 | public function mailto($email, $text = null) 361 | { 362 | return $this->a('mailto:'.$email, $text ?: $email); 363 | } 364 | 365 | /** 366 | * @param string|null $name 367 | * @param iterable $options 368 | * @param string|iterable|null $value 369 | * 370 | * @return \Spatie\Html\Elements\Select 371 | */ 372 | public function multiselect($name = null, $options = [], $value = null) 373 | { 374 | return Select::create() 375 | ->attributeIf($name, 'name', $this->fieldName($name)) 376 | ->attributeIf($name, 'id', $this->fieldName($name)) 377 | ->options($options) 378 | ->value($name ? $this->old($name, $value) : $value) 379 | ->multiple(); 380 | } 381 | 382 | /** 383 | * @param string|null $name 384 | * @param string|null $value 385 | * @param string|null $min 386 | * @param string|null $max 387 | * @param string|null $step 388 | * 389 | * @return \Spatie\Html\Elements\Input 390 | */ 391 | public function number($name = null, $value = null, $min = null, $max = null, $step = null) 392 | { 393 | return $this->input('number', $name, $value) 394 | ->attributeIfNotNull($min, 'min', $min) 395 | ->attributeIfNotNull($max, 'max', $max) 396 | ->attributeIfNotNull($step, 'step', $step); 397 | } 398 | 399 | /** 400 | * @param string|null $text 401 | * @param string|null $value 402 | * @param bool $selected 403 | * 404 | * @return \Spatie\Html\Elements\Option 405 | */ 406 | public function option($text = null, $value = null, $selected = false) 407 | { 408 | return Option::create() 409 | ->text($text) 410 | ->value($value) 411 | ->selectedIf($selected); 412 | } 413 | 414 | /** 415 | * @param string|null $value 416 | * 417 | * @return \Spatie\Html\Elements\Input 418 | */ 419 | public function password($name = null) 420 | { 421 | return $this->input('password', $name); 422 | } 423 | 424 | /** 425 | * @param string|null $name 426 | * @param bool $checked 427 | * @param string|null $value 428 | * 429 | * @return \Spatie\Html\Elements\Input 430 | */ 431 | public function radio($name = null, $checked = null, $value = null) 432 | { 433 | return $this->input('radio', $name, $value) 434 | ->attributeIf($name, 'id', $value === null ? $name : ($name.'_'.Str::slug($value))) 435 | ->attributeIf(! is_null($value), 'value', $value) 436 | ->attributeIf((! is_null($value) && $this->old($name) === $value) || $checked, 'checked'); 437 | } 438 | 439 | /** 440 | * @param string|null $name 441 | * @param iterable $options 442 | * @param string|iterable|null $value 443 | * 444 | * @return \Spatie\Html\Elements\Select 445 | */ 446 | public function select($name = null, $options = [], $value = null) 447 | { 448 | return Select::create() 449 | ->attributeIf($name, 'name', $this->fieldName($name)) 450 | ->attributeIf($name, 'id', $this->fieldName($name)) 451 | ->options($options) 452 | ->value($name ? $this->old($name, $value) : $value); 453 | } 454 | 455 | /** 456 | * @param \Spatie\Html\HtmlElement|string|null $contents 457 | * 458 | * @return \Spatie\Html\Elements\Span 459 | */ 460 | public function span($contents = null) 461 | { 462 | return Span::create()->children($contents); 463 | } 464 | 465 | /** 466 | * @param string|null $text 467 | * 468 | * @return \Spatie\Html\Elements\Button 469 | */ 470 | public function submit($text = null) 471 | { 472 | return $this->button($text, 'submit'); 473 | } 474 | 475 | /** 476 | * @param string|null $text 477 | * 478 | * @return \Spatie\Html\Elements\Button 479 | */ 480 | public function reset($text = null) 481 | { 482 | return $this->button($text, 'reset'); 483 | } 484 | 485 | /** 486 | * @param string $number 487 | * @param string|null $text 488 | * 489 | * @return \Spatie\Html\Elements\A 490 | */ 491 | public function tel($number, $text = null) 492 | { 493 | return $this->a('tel:'.$number, $text ?: $number); 494 | } 495 | 496 | /** 497 | * @param string|null $name 498 | * @param string|null $value 499 | * 500 | * @return \Spatie\Html\Elements\Input 501 | */ 502 | public function text($name = null, $value = null) 503 | { 504 | return $this->input('text', $name, $value); 505 | } 506 | 507 | /** 508 | * @param string|null $name 509 | * 510 | * @return \Spatie\Html\Elements\File 511 | */ 512 | public function file($name = null) 513 | { 514 | return File::create() 515 | ->attributeIf($name, 'name', $this->fieldName($name)) 516 | ->attributeIf($name, 'id', $this->fieldName($name)); 517 | } 518 | 519 | /** 520 | * @param string|null $name 521 | * @param string|null $value 522 | * 523 | * @return \Spatie\Html\Elements\Textarea 524 | */ 525 | public function textarea($name = null, $value = null) 526 | { 527 | return Textarea::create() 528 | ->attributeIf($name, 'name', $this->fieldName($name)) 529 | ->attributeIf($name, 'id', $this->fieldName($name)) 530 | ->value($this->old($name, $value)); 531 | } 532 | 533 | /** 534 | * @return \Spatie\Html\Elements\Input 535 | */ 536 | public function token() 537 | { 538 | return $this 539 | ->hidden() 540 | ->name('_token') 541 | ->value($this->request->session()->token()); 542 | } 543 | 544 | /** 545 | * @param \ArrayAccess|array $model 546 | * 547 | * @return $this 548 | */ 549 | public function model($model) 550 | { 551 | $this->model = $model; 552 | 553 | return $this; 554 | } 555 | 556 | /** 557 | * @param \ArrayAccess|array $model 558 | * @param string|null $method 559 | * @param string|null $action 560 | * 561 | * @return \Spatie\Html\Elements\Form 562 | */ 563 | public function modelForm($model, $method = 'POST', $action = null): Form 564 | { 565 | $this->model($model); 566 | 567 | return $this->form($method, $action); 568 | } 569 | 570 | /** 571 | * @return $this 572 | */ 573 | public function endModel() 574 | { 575 | $this->model = null; 576 | 577 | return $this; 578 | } 579 | 580 | /** 581 | * @return \Illuminate\Contracts\Support\Htmlable 582 | */ 583 | public function closeModelForm(): Htmlable 584 | { 585 | $this->endModel(); 586 | 587 | return $this->form()->close(); 588 | } 589 | 590 | /** 591 | * @param string $name 592 | * @param mixed $value 593 | * 594 | * @return mixed 595 | */ 596 | protected function old($name, $value = null) 597 | { 598 | if (empty($name)) { 599 | return $value; 600 | } 601 | 602 | // Convert array format (sth[1]) to dot notation (sth.1) 603 | $name = preg_replace('/\[(.+)\]/U', '.$1', $name); 604 | 605 | // If there's no default value provided, the html builder currently 606 | // has a model assigned and there aren't old input items, 607 | // try to retrieve a value from the model. 608 | if (is_null($value) && $this->model && empty($this->request->old())) { 609 | $value = ($value = data_get($this->model, $name)) instanceof UnitEnum 610 | ? $this->getEnumValue($value) 611 | : $value; 612 | } 613 | 614 | return $this->request->old($name, $value); 615 | } 616 | 617 | /** 618 | * Retrieve the value from the current session or assigned model. This is 619 | * a public alias for `old`. 620 | * 621 | * @param string $name 622 | * @param mixed $value 623 | * 624 | * @return mixed 625 | */ 626 | public function value($name, $default = null) 627 | { 628 | return $this->old($name, $default); 629 | } 630 | 631 | /** 632 | * @param string $name 633 | * 634 | * @return string 635 | */ 636 | protected function fieldName($name) 637 | { 638 | return $name; 639 | } 640 | 641 | protected function ensureModelIsAvailable() 642 | { 643 | if (empty($this->model)) { 644 | throw new Exception('Method requires a model to be set on the html builder'); 645 | } 646 | } 647 | 648 | /** 649 | * @param string $value 650 | * @param string $format DateTime formatting string supported by date_format() 651 | * @return string 652 | */ 653 | protected function formatDateTime($value, $format) 654 | { 655 | if (empty($value)) { 656 | return $value; 657 | } 658 | 659 | try { 660 | $date = new DateTimeImmutable($value); 661 | 662 | return $date->format($format); 663 | } catch (Exception $e) { 664 | return $value; 665 | } 666 | } 667 | 668 | /** 669 | * Get the value from the given enum. 670 | * 671 | * @param \UnitEnum|\BackedEnum $value 672 | * @return string|int 673 | */ 674 | protected function getEnumValue($value) 675 | { 676 | return $value instanceof BackedEnum 677 | ? $value->value 678 | : $value->name; 679 | } 680 | } 681 | -------------------------------------------------------------------------------- /src/HtmlElement.php: -------------------------------------------------------------------------------- 1 | app->singleton(Html::class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Selectable.php: -------------------------------------------------------------------------------- 1 |