diff --git a/docs/1-essentials/02-views.md b/docs/1-essentials/02-views.md index 3db0f453c..e56dec8b1 100644 --- a/docs/1-essentials/02-views.md +++ b/docs/1-essentials/02-views.md @@ -341,21 +341,128 @@ The idiomatic way of using attributes is to always use `{txt}kebab-case`. ### Fallthrough attributes -When `{html}class` and `{html}style` attributes are used on a view component, they will automatically be added to the root node, or merged with the existing attribute if it already exists. +When `{html}class` and `{html}style` attributes, or `{html}id` is provided on a view component, Tempest will attempt to automatically apply these to the root node within the view component. +:::info +In previous releases (3.8.0 and prior), Tempest would attempt to *merge* these values, however there was no way to prevent this, or customise the behaviour. There was also a bug in applying the attributes, which meant that in many cases it didn't apply at all, resulting in inconsistent behaviour. This has been resolved, but has a new default behaviour, as explained below. +::: + +Assume you have a `button`, like so, with a default set of classes present: ```html x-button.view.php +``` +Now, in your page, you may utilise the element: +```html index.view.php + +``` +As these attributes automatically apply, your button will be converted to this: +```html + +``` +When you use this version of ``: +- `{html}id` will now default to `mybtn_(sequence generated by uniqid)`, +- `{html}style` will not appear automatically, as it was not supplied, +- `{html}class` will have a default, you can of course instead concatenate these strings, or use a CVA utility for smart class merging, or anything you want. + +For example, pass one or more classes: +```html + +``` +And you'll get +```html + +``` + +### Controlling fallthrough attributes with the Apply attribute + +You can also leverage the `ApplyAttribute` to completely control the behaviour, and add further fallthrough attributes, if you wish. When `:apply` is detected on a view component, Tempest will disable all automatic fallthrough attributes, for that instance of the view component. If you are familiar with JS frontend frameworks, this is not dissimilar to a one-way `v-bind` in Vue, or a spread props operator in other languages. + +By default, `$attributes` is an `ImmutableArray` and so we can manipulate it with the methods available on that class. + +:::info +You cannot mix `ApplyAttribute` with automatic fallthrough attributes. Opting to use the `ApplyAttribute` hands you full control of which attributes are applied, which means you then need to declare these. +::: + +#### Excluding specific fallthrough attributes + +To exclude specific attributes from falling through, configure your `button` view component like this: +```html x-button.view.php + ``` +:::info +Why array_flip? In `$attributes` the keys are the attributes, in the array in the example above, the values are the attributes, you could also pass `['class' => 0, 'width' => 1, etc]` without a flip. +::: +Now, when utilising it in your page: +```html index.view.php + +``` +Will result in: +```html + +``` +:::info +Why array_flip? In `$attributes` the keys are the attributes, in the array in the example above, the values are the attributes, you could also pass `['class' => 0, 'width' => 1, etc]` without a flip. +::: +Now, when utilising it in your page: ```html index.view.php - + ``` +Tempest will apply only the specified attributes: +```html + +``` +Now, when utilising it in your page: +```html index.view.php + +``` +Tempest will spread the supplied attributes, and as we also used the `AsAttribute` to convert it to a `{html}a` when `$href` is populated, you will get a hyperlink: +```html +Tempest, the framework that gets out of your way +``` ### Dynamic attributes diff --git a/packages/view/src/Attributes/ApplyAttribute.php b/packages/view/src/Attributes/ApplyAttribute.php new file mode 100644 index 000000000..0b700c434 --- /dev/null +++ b/packages/view/src/Attributes/ApplyAttribute.php @@ -0,0 +1,93 @@ +consumeAttribute(':apply'); + + if ($value === null || trim($value) === '') { + return $element; + } + + if ($element instanceof ViewComponentElement) { + $element->setApplyExpression($value); + + return $element; + } + + $element->addRawAttribute(sprintf( + '', + self::class, + $value, + )); + + return $element; + } + + /** + * Stringifies an ImmutableArray or plain array of attributes into an HTML attribute string. + * + * Rules: + * - boolean true → bare attribute name (e.g. `disabled`) + * - boolean false → omitted + * - null → omitted + * - empty string → omitted + * - int / float → name="value" (cast to string — HTML attributes are always strings) + * - string → name="value" + * - array → name="space-joined values" (via ExpressionAttribute::resolveValue) + * + * Returns 'key="val" key2="val2"' with NO leading space. GenericElement's compile() + * inserts one space before the raw attribute block at compile time, so adding a leading + * space here would produce a double space in the rendered output. + * + * Note: when this returns '' (all attributes omitted), GenericElement's compile-time + * space still appears, producing e.g. `