|
| 1 | +# Twig Components |
| 2 | + |
| 3 | +**EXPERIMENTAL** This component is currently experimental and is |
| 4 | +likely to change, or even change drastically. |
| 5 | + |
| 6 | +Twig components give you the power to bind an object to a template, making |
| 7 | +it easier to render and re-use small template "units" - like an "alert", |
| 8 | +markup for a modal, or a category sidebar: |
| 9 | + |
| 10 | +Every component consists of (1) a class: |
| 11 | + |
| 12 | +```php |
| 13 | +// src/Components/AlertComponent.php |
| 14 | +namespace App\Components; |
| 15 | + |
| 16 | +use Symfony\UX\TwigComponent\ComponentInterface; |
| 17 | + |
| 18 | +class AlertComponent implements ComponentInterface |
| 19 | +{ |
| 20 | + public string $type = 'success'; |
| 21 | + public string $message; |
| 22 | + |
| 23 | + public static function getComponentName(): string |
| 24 | + { |
| 25 | + return 'alert'; |
| 26 | + } |
| 27 | +} |
| 28 | +``` |
| 29 | + |
| 30 | +And (2) a corresponding template: |
| 31 | + |
| 32 | +```twig |
| 33 | +{# templates/components/alert.html.twig #} |
| 34 | +<div class="alert alert-{{ this.type }}"> |
| 35 | + {{ this.message }} |
| 36 | +</div> |
| 37 | +``` |
| 38 | + |
| 39 | +Done! Now render it wherever you want: |
| 40 | + |
| 41 | +```twig |
| 42 | +{{ component('alert', { message: 'Hello Twig Components!' }) }} |
| 43 | +``` |
| 44 | + |
| 45 | +Enjoy your new component! |
| 46 | + |
| 47 | + |
| 48 | + |
| 49 | +This brings the familiar "component" system from client-side frameworks |
| 50 | +into Symfony. Combine this with [Live Components](../LiveComponent), |
| 51 | +to create an interactive frontend with automatic, Ajax-powered rendering. |
| 52 | + |
| 53 | +## Installation |
| 54 | + |
| 55 | +Let's get this thing installed! Run: |
| 56 | + |
| 57 | +``` |
| 58 | +composer require symfony/ux-twig-component |
| 59 | +``` |
| 60 | + |
| 61 | +That's it! We're ready to go! |
| 62 | + |
| 63 | +## Creating a Basic Component |
| 64 | + |
| 65 | +Let's create a reusable "alert" element that we can use to show |
| 66 | +success or error messages across our site. Step 1 is always to create |
| 67 | +a component that implements `ComponentInterface`. Let's start as simple |
| 68 | +as possible: |
| 69 | + |
| 70 | +```php |
| 71 | +// src/Components/AlertComponent.php |
| 72 | +namespace App\Components; |
| 73 | + |
| 74 | +use Symfony\UX\TwigComponent\ComponentInterface; |
| 75 | + |
| 76 | +class AlertComponent implements ComponentInterface |
| 77 | +{ |
| 78 | + public static function getComponentName(): string |
| 79 | + { |
| 80 | + return 'alert'; |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +Step 2 is to create a template for this component. Templates live |
| 86 | +in `templates/components/{Component Name}.html.twig`, where |
| 87 | +`{Component Name}` is whatever you return from the `getComponentName()` |
| 88 | +method: |
| 89 | + |
| 90 | +```twig |
| 91 | +{# templates/components/alert.html.twig #} |
| 92 | +<div class="alert alert-success"> |
| 93 | + Success! You've created a Twig component! |
| 94 | +</div> |
| 95 | +``` |
| 96 | + |
| 97 | +This isn't very interesting yet... since the message is hardcoded |
| 98 | +into the template. But it's enough! Celebrate by rendering your |
| 99 | +component from any other Twig template: |
| 100 | + |
| 101 | +```twig |
| 102 | +{{ component('alert') }} |
| 103 | +``` |
| 104 | + |
| 105 | +Done! You've just rendered your first Twig Component! Take a moment |
| 106 | +to fist pump - then come back! |
| 107 | + |
| 108 | +## Passing Data into your Component |
| 109 | + |
| 110 | +Good start: but this isn't very interesting yet! To make our |
| 111 | +`alert` component reusable, we need to make the message and |
| 112 | +type (e.g. `success`, `danger`, etc) configurable. To do |
| 113 | +that, create a public property for each: |
| 114 | + |
| 115 | +```diff |
| 116 | +// src/Components/AlertComponent.php |
| 117 | +// ... |
| 118 | + |
| 119 | +class AlertComponent implements ComponentInterface |
| 120 | +{ |
| 121 | ++ public string $message; |
| 122 | + |
| 123 | ++ public string $type = 'success'; |
| 124 | + |
| 125 | + // ... |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +In the template, the `AlertComponent` instance is available via |
| 130 | +the `this` variable. Use it to render the two new properties: |
| 131 | + |
| 132 | +```twig |
| 133 | +<div class="alert alert-{{ this.type }}"> |
| 134 | + {{ this.message }} |
| 135 | +</div> |
| 136 | +``` |
| 137 | + |
| 138 | +How can we populate the `message` and `type` properties? By passing them |
| 139 | +as a 2nd argument to the `component()` function when rendering: |
| 140 | + |
| 141 | +```twig |
| 142 | +{{ component('alert', { message: 'Successfully created!' }) }} |
| 143 | +
|
| 144 | +{{ component('alert', { |
| 145 | + type: 'danger', |
| 146 | + message: 'Danger Will Robinson!' |
| 147 | +}) }} |
| 148 | +``` |
| 149 | + |
| 150 | +Behind the scenes, a new `AlertComponent` will be instantiated and |
| 151 | +the `message` key (and `type` if passed) will be set onto the `$message` |
| 152 | +property of the object. Then, the component is rendered! If a |
| 153 | +property has a setter method (e.g. `setMessage()`), that will |
| 154 | +be called instead of setting the property directly. |
| 155 | + |
| 156 | +### The mount() Method |
| 157 | + |
| 158 | +If, for some reason, you don't want an option to the `component()` |
| 159 | +function to be set directly onto a property, you can, instead, create |
| 160 | +a `mount()` method in your component: |
| 161 | + |
| 162 | +```php |
| 163 | +// src/Components/AlertComponent.php |
| 164 | +// ... |
| 165 | + |
| 166 | +class AlertComponent implements ComponentInterface |
| 167 | +{ |
| 168 | + public string $message; |
| 169 | + public string $type = 'success'; |
| 170 | + |
| 171 | + public function mount(bool $isSuccess = true) |
| 172 | + { |
| 173 | + $this->type = $isSuccess ? 'success' : 'danger'; |
| 174 | + } |
| 175 | + |
| 176 | + // ... |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +The `mount()` method is called just one time immediately after your |
| 181 | +component is instantiated. Because the method has an `$isSuccess` |
| 182 | +argument, we can pass an `isSuccess` option when rendering the |
| 183 | +component: |
| 184 | + |
| 185 | +```twig |
| 186 | +{{ component('alert', { |
| 187 | + isSuccess: false, |
| 188 | + message: 'Danger Will Robinson!' |
| 189 | +}) }} |
| 190 | +``` |
| 191 | + |
| 192 | +If an option name matches an argument name in `mount()`, the |
| 193 | +option is passed as that argument and the component system |
| 194 | +will _not_ try to set it directly on a property. |
| 195 | + |
| 196 | +## Fetching Services |
| 197 | + |
| 198 | +Let's create a more complex example: a "featured products" component. |
| 199 | +You _could_ choose to pass an array of Product objects into the |
| 200 | +`component()` function and set those on a `$products` property. But |
| 201 | +instead, let's allow the component to do the work of executing the query. |
| 202 | + |
| 203 | +How? Components are _services_, which means autowiring |
| 204 | +works like normal. This example assumes you have a `Product` |
| 205 | +Doctrine entity and `ProductRepository`: |
| 206 | + |
| 207 | +```php |
| 208 | +// src/Components/FeaturedProductsComponent.php |
| 209 | +namespace App\Components; |
| 210 | + |
| 211 | +use App\Repository\ProductRepository; |
| 212 | +use Symfony\UX\TwigComponent\ComponentInterface; |
| 213 | + |
| 214 | +class FeaturedProductsComponent implements ComponentInterface |
| 215 | +{ |
| 216 | + private ProductRepository $productRepository; |
| 217 | + |
| 218 | + public function __construct(ProductRepository $productRepository) |
| 219 | + { |
| 220 | + $this->productRepository = $productRepository; |
| 221 | + } |
| 222 | + |
| 223 | + public function getProducts(): array |
| 224 | + { |
| 225 | + // an example method that returns an array of Products |
| 226 | + return $this->productRepository->findFeatured(); |
| 227 | + } |
| 228 | + |
| 229 | + public static function getComponentName() : string |
| 230 | + { |
| 231 | + return 'featured_products'; |
| 232 | + } |
| 233 | +} |
| 234 | +``` |
| 235 | + |
| 236 | +In the template, the `getProducts()` method can be accessed via |
| 237 | +`this.products`: |
| 238 | + |
| 239 | +```twig |
| 240 | +{# templates/components/featured_products.html.twig #} |
| 241 | +
|
| 242 | +<div> |
| 243 | + <h3>Featured Products</h3> |
| 244 | +
|
| 245 | + {% for product in this.products %} |
| 246 | + ... |
| 247 | + {% endfor %} |
| 248 | +</div> |
| 249 | +``` |
| 250 | + |
| 251 | +And because this component doesn't have any public properties that |
| 252 | +we need to populate, you can render it with: |
| 253 | + |
| 254 | +```twig |
| 255 | +{{ component('featured_products') }} |
| 256 | +``` |
| 257 | + |
| 258 | +**NOTE** |
| 259 | +Because components are services, normal dependency injection |
| 260 | +can be used. However, each component service is registered with |
| 261 | +`shared: false`. That means that you can safely render the same |
| 262 | +component multiple times with different data because each |
| 263 | +component will be an independent instance. |
| 264 | + |
| 265 | +### Computed Properties |
| 266 | + |
| 267 | +In the previous example, instead of querying for the featured products |
| 268 | +immediately (e.g. in `__construct()`), we created a `getProducts()` |
| 269 | +method and called that from the template via `this.products`. |
| 270 | + |
| 271 | +This was done because, as a general rule, you should make your components |
| 272 | +as _lazy_ as possible and store only the information you need on its |
| 273 | +properties (this also helps if you convert your component to a |
| 274 | +[live component](../LiveComponent)) later. With this setup, the |
| 275 | +query is only executed if and when the `getProducts()` method |
| 276 | +is actually called. This is very similar to the idea of |
| 277 | +"computed properties" in frameworks like [Vue](https://v3.vuejs.org/guide/computed.html). |
| 278 | + |
| 279 | +But there's no magic with the `getProducts()` method: if you |
| 280 | +call `this.products` multiple times in your template, the query |
| 281 | +would be executed multiple times. |
| 282 | + |
| 283 | +To make your `getProducts()` method act like a true computed property |
| 284 | +(where its value is only evaluated the first time you call the |
| 285 | +method), you can store its result on a private property: |
| 286 | + |
| 287 | +```diff |
| 288 | +// src/Components/FeaturedProductsComponent.php |
| 289 | +namespace App\Components; |
| 290 | +// ... |
| 291 | + |
| 292 | +class FeaturedProductsComponent implements ComponentInterface |
| 293 | +{ |
| 294 | + private ProductRepository $productRepository; |
| 295 | + |
| 296 | ++ private ?array $products = null; |
| 297 | + |
| 298 | + // ... |
| 299 | + |
| 300 | + public function getProducts(): array |
| 301 | + { |
| 302 | ++ if ($this->products === null) { |
| 303 | ++ $this->products = $this->productRepository->findFeatured(); |
| 304 | ++ } |
| 305 | + |
| 306 | +- return $this->productRepository->findFeatured(); |
| 307 | ++ return $this->products; |
| 308 | + } |
| 309 | +} |
| 310 | +``` |
| 311 | + |
| 312 | +## Contributing |
| 313 | + |
| 314 | +Interested in contributing? Visit the main source for this repository: |
| 315 | +https://github.com/symfony/ux/tree/main/src/TwigComponent. |
| 316 | + |
| 317 | +Have fun! |
0 commit comments