A modest proposal for a first-class destiny operator equivalent in Svelte components that's also valid JS.
Svelte 2 has a concept of computed properties, which are updated whenever their inputs change. They're powerful, if a little boilerplatey, but there's currently no place for them in Svelte 3.
This means that we have to use functions. Instead of this...
<!-- Svelte 2 -->
<h1>HELLO {NAME}!</h1>
<script>
export default {
data: () => ({ name: 'world' }),
computed: {
NAME: ({ name }) => name.toUpperCase()
}
};
</script>
...we have to do this:
<script>
export let name = 'world';
const NAME = () => name.toUpperCase();
</script>
<h1>HELLO {NAME()}!</h1>
But that has some serious downsides, especially when those functions depend on other function values. In the current.html
example below, x_scale
and y_scale
are invoked twice per point. Each call to x_scale
invokes min_x()
and max_x()
, and similarly for y_scale
. That won't do.
Of course, the compiler could, in some cases, do some kind of automatic memoisation, if it was sufficiently smart. But such an approach would be riddled with caveats, and as a user I'm not sure I'd trust the compiler to do the right thing if I was staring at code that looks as though it's going to result in thousands of unnecessary function calls.
We can't implement the destiny operator, because it's not valid JS — parser, linters, typecheckers etc wouldn't be able to work with it. But there's a piece of syntax in JavaScript that we can use in its place: the labeled statement.
This is valid JS:
let name = 'world';
let NAME;
compute:NAME = name.toUpperCase();
See proposed.html
below for a more complete example. The dependency graph is topologically sorted at compile time, and values that could have changed are recomputed once per update cycle.
Another benefit of this approach is that we could combine multiple computed values into a single block. As an alternative to this...
compute:min_x = Math.min(...points.map(p => p.x));
compute:max_x = Math.max(...points.map(p => p.x));
compute:min_y = Math.min(...points.map(p => p.y));
compute:max_y = Math.max(...points.map(p => p.y));
...we could do this, iterating over points
once instead of mapping it four times:
compute: {
min_x = Infinity; max_x = -Infinity; min_y = Infinity; max_y = -Infinity; // reset
points.forEach(point => {
if (point.x < min_x) min_x = point.x;
if (point.x > max_x) max_x = point.x;
if (point.y < min_y) min_y = point.y;
if (point.y > max_y) max_y = point.y;
});
}
This could maybe work with sources as well — would this make sense?
import { todos } from './sources.js;'
let currentFilter = 'all';
let filteredTodos;
bind:filteredTodos = $todos.filter(currentFilter);