Re: [csswg-drafts] Declarative custom functions (#7490)

Amazing discussion so far! It's exciting to see and keep up with all the changes coming.

I've been giving quite a bit of thought to custom CSS function defs this past week before @tabatkins informed me yesterday that this thread already existed (thanks btw Tab!).

It's awesome to see that a lot of the same thoughts and considerations I had have already been voiced and taken into account here.

A few things I'd add which I'm hoping stir continued fruitful discussion—

### TL;DR (aka Table of Contents)

1. Arg names, arg syntaxes, and function return syntax before def block `{ }`
2. `@return` rule instead of `result` value
3. Intermediate value declarations
4. Intermediate logic
5. Recursion
6. Alternative invocation syntax (passing args by param/prop name vs. standard order)

## 1. Arg names, arg syntaxes, and function return syntax before def block `{ }`

As @johannesodland pointed out, it could be advantageous to include the arg names and syntaxes in the opening line declaring and naming the function. I also think it would help to define the return syntax there unless that can be done implicitly, in which case it can be omitted altogether.

```css
@custom-function --function-name(--arg "<number>") "<string>" { /* def block */ }
                                           │            │
                       /* arg syntax */ ───┘            └─── /* return syntax */
```

## 2. `@return` rule instead of `result` value

The justification for using a `@return` rule instead of a `result` property lies in functions being intrinsically logical. Using `@return` allows for there to be multiple `@return` rules within a single function definition, thereby allowing a function to "short-circuit" and return early if certain conditions are met employing an `@if` statement (already being spec'd, more commonly known thus far as `@when`).

```css
@custom-function --return-same(--arg "*") "*" {
  @return var(--arg);
}
```

_** See point no. 4 below for more thoughts and notes on the use case for logical expressions inside functions defs_

## 3. Intermediate value declarations

Most/all of the examples I see here contain all of the function's logic within the `result` property. This _could_ be limiting, and by allowing custom properties to be exposed and manipulated within the function def, I think we could add a lot of logic-oriented value to CSS functions.

```css
@custom-function --func-name(--arg "<string>") "<string>" {
  --prefix: "Title: ";
  @return var(--prefix) var(--arg);
}
```

## 4. Intermediate logic

In addition to adding/modifying property values within the function definition, I also see it as vital that we can do so conditionally. This both allows to set values intermediately and return conditionally, employing a short-circuit pattern.

**Logically intermediate values**

```css
@custom-function --func-name(--arg "<string>") "<string>" {
  @if media(width < 500px) {
    --prefix: "(";
    --suffix: ")";
  }
  @return var(--prefix, "") var(--arg) var(--suffix, "");
}
```

**Logically intermediate `@return`**

```css
@custom-function --show-conditionally(--arg "<string>", --min-screen: "<length>": 500px) "<string>" {
  @if media(width < var(--min-screen)) {
    @return "This data is intended for screen sizes above " var(--min-screen);
  }
  @return var(--arg);
}
```

There are simpler ways to express this function—no doubt—but it suffices as a simple example, I think.

## 5. Recursion

Many of the points here can be expressed in an example demonstrating recursion, which I think to be a key example of the importance of logic use within a CSS function def.

```css
@custom-function --repeat(--string "<string>", --times "<number>", --delimiter "<string>": "") "<string>" {
  /* @if should be able to handle mathematical comparisons */
  @if (var(--times) > 0) {
    --string: var(--string) var(--delimiter) --repeat(var(--string), var(--times) - 1, var(--delimiter));
  }
  @return var(--string);
}
```

And breaking down a couple of vital points in that recursive example:

```css
--string: var(--string) var(--delimiter) --repeat(var(--string), var(--times) - 1, var(--delimiter));
                                            │                                 │
                /* same function name */ ───┘ /* math in arg (w/o calc) */ ───┘

@return var(--string);
    │
    └─── /* the returned value of each recursive iteration would pass its
            value back up to the --string property from which it was called */
```

## 6. Alternative invocation syntax (passing args by param/prop name vs. standard order)

One more consideration I had is that in some cases, you may create a function with default values for all parameters and only need to change one property whenever the function is invoked, with no priority to one param over another. In this case, it would also help to support a syntax where the desired arg values could be passed into the function by prop name instead of in their typical insertion order.

Here is an example of how that might look, using the same recursion example from above:

```css
@custom-function --repeat(--string "<string>": "hello world", --times "<number>": 2, --delimiter "<string>": "") "<string>" {
  @if (var(--times) > 0) {
    --string: var(--string) var(--delimiter) --repeat(var(--string), var(--times) - 1, var(--delimiter));
  }
  @return var(--string);
}

selector {
  /* to use all default param values */
  --value: --repeat();

  /* to pass args in traditional insertion order */
  --value: --repeat("hello john");

  /* to pass args by prop name, to only set/override particular args,
    without having to repeat and reference default values */
  --value: --repeat({ --times: 5 });

  /* or in a more prettified fashion */
  --value: --repeat({
    --times: 5;
    --delimiter: " ";
  });
}
```

This (the final pattern above) is a pattern I don't think we've seen in CSS yet, but I think it makes sense to add, especially in the case of functions. You'd essentially be passing a style block—or a definitions block, rather—of custom properties, whose names must match params used by the function. I don't think there'd need to be any checks/errors if a name is used which does exist as a param in the function, and that would be ignored.

It works almost identically to destructuring in JavaScript, though I think this syntax to be even simpler as it's fairly familiar to a standard CSS block.

This also begs the question—in my mind at least—if there'd be any value in adding a new type to CSS altogether for definition blocks like this and allowing custom properties to be set to them.

<details>
<summary>Expand this to see a shallow dive into the unrelated topic/idea of a definition-block value type ✨🤷🏻‍♂️</summary>
<br />
It's not necessary even for this use case, though it could take this suggestion even a step further and support future development of features like mixins (I think some everybody's still noodling that one) and maybe even an `@extend` rule.

That would mean the above example would be equivalent to this:

```css
selector {
  --default-arg-overrides: {
    --times: 5;
    --delimiter: " ";
  };
  --value: --repeat(var(--default-arg-overrides));
}
```

I do see a lot of value in this too, but I'm sure that's a rabbit hole for another discussion, as that would also then beg a syntax for allowing "spreading", though CSS already naturally supports overriding previously set values of the same property, for example:

```css
selector {
  color: red;
  color: blue; /* <-- blue will be used */
}
```

With that in mind, perhaps spreading could look like declaring the variable as its own single prop/value pair in place of styles:

```css
selector {
  --default-arg-overrides: {
    --times: 5;
    --delimiter: " ";
  };
  --value: --repeat({
    var(--default-arg-overrides);
    --times: 10;
  });
}
```

In terms of use with `@extend`, rather than requiring a new placeholder selector syntax similar to that of Sass's `%`, we could more simply extend a variable that contains a definition block. For example:

```css
:root {
  --button-styles: {
    padding: 1rem 1.5rem;
    background: pink;
    border: 2px solid black;
    color: black;
  };
}
selector {
  @extend var(--button-styles); /* with or without `var(` ... `)` wrapping the custom property name */
}
```

This wouldn't take away from being able to extend existing class names also.

```css
selector {
  @extend .primary-button */
}
```

It also would not suffice to replace mixins entirely, as I believe mixins to be far more sophisticated in nature, likely requiring much of the same logical depth I'm pushing for here.
</details>

-- 
GitHub Notification of comment by brandonmcconnell
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/7490#issuecomment-1256880496 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Saturday, 24 September 2022 06:35:39 UTC