Repository: sveltejs/svelte (JavaScript)
Date: 2019-10-21 19:34:04+00:00
Task type: Feature

Issue:
Allow spring/tweened values to be initially undefined
**Is your feature request related to a problem? Please describe.**
When using springs and tweens to describe values that are set from props, I find myself doing this sort of thing:

```html
<script>
  import { spring } from 'svelte/motion';

  export let big = false;
  
  const size = spring(big ? 100 : 20);
  $: size.set(big ? 100 : 20);
</script>
```

In this situation I want the value of `size` to be static when the component is first created, *until* the prop changes. But it's a nuisance to have to specify `big ? 100 : 20` twice. You could create a function for it...

```js
const get_size = big => big ? 100 : 20;
const size = spring(get_size(big));
$: size.set(get_size(big));
```

...but that's arguably worse.

**Describe the solution you'd like**
If it were possible to call `spring` without an initial value, we could initialise it later:

```js
const size = spring();
$: size.set(big ? 100 : 20);
```

Of course, it would be nice if we could initialise the store right there in the reactive declaration, as happens for non-store values, but I'm not sure what that would look like. Might be cool to be able to do this sort of thing...

```js
$: <spring>$size = big ? 100 : 20;
```

...but it's obviously invalid JS. Maybe worth coming back round to that one day, but for the meantime allowing `undefined`/`null` initial values solves the DRY problem.

**How important is this feature to you?**
I'm encountering this situation a lot in my current project. I think it's important that using motion be as frictionless as possible.


Issue discussion:

Solution patch:
diff --git a/site/content/docs/03-run-time.md b/site/content/docs/03-run-time.md
--- a/site/content/docs/03-run-time.md
+++ b/site/content/docs/03-run-time.md
@@ -433,6 +433,19 @@ Out of the box, Svelte will interpolate between two numbers, two arrays or two o
 
 ---
 
+If the initial value is `undefined` or `null`, the first value change will take effect immediately. This is useful when you have tweened values that are based on props, and don't want any motion when the component first renders.
+
+```js
+const size = tweened(undefined, {
+	duration: 300,
+	easing: cubicOut
+});
+
+$: $size = big ? 100 : 10;
+```
+
+---
+
 The `interpolate` option allows you to tween between *any* arbitrary values. It must be an `(a, b) => t => value` function, where `a` is the starting value, `b` is the target value, `t` is a number between 0 and 1, and `value` is the result. For example, we can use the [d3-interpolate](https://github.com/d3/d3-interpolate) package to smoothly interpolate between two colours.
 
 ```html
@@ -493,6 +506,15 @@ Both `set` and `update` can take a second argument — an object with `hard` or
 </script>
 ```
 
+---
+
+If the initial value is `undefined` or `null`, the first value change will take effect immediately, just as with `tweened` values (see above).
+
+```js
+const size = spring();
+$: $size = big ? 100 : 10;
+```
+
 ### `svelte/transition`
 
 The `svelte/transition` module exports six functions: `fade`, `fly`, `slide`, `scale`, `draw` and `crossfade`. They are for use with svelte [`transitions`](docs#Transitions).
diff --git a/src/runtime/motion/spring.ts b/src/runtime/motion/spring.ts
--- a/src/runtime/motion/spring.ts
+++ b/src/runtime/motion/spring.ts
@@ -65,7 +65,7 @@ interface Spring<T> extends Readable<T>{
 	stiffness: number;
 }
 
-export function spring<T=any>(value: T, opts: SpringOpts = {}): Spring<T> {
+export function spring<T=any>(value?: T, opts: SpringOpts = {}): Spring<T> {
 	const store = writable(value);
 	const { stiffness = 0.15, damping = 0.8, precision = 0.01 } = opts;
 
@@ -84,12 +84,12 @@ export function spring<T=any>(value: T, opts: SpringOpts = {}): Spring<T> {
 		target_value = new_value;
 		const token = current_token = {};
 
-		if (opts.hard || (spring.stiffness >= 1 && spring.damping >= 1)) {
+		if (value == null || opts.hard || (spring.stiffness >= 1 && spring.damping >= 1)) {
 			cancel_task = true; // cancel any running animation
 			last_time = now();
-			last_value = value;
+			last_value = new_value;
 			store.set(value = target_value);
-			return new Promise(f => f()); // fulfil immediately
+			return Promise.resolve();
 		} else if (opts.soft) {
 			const rate = opts.soft === true ? .5 : +opts.soft;
 			inv_mass_recovery_rate = 1 / (rate * 60);
diff --git a/src/runtime/motion/tweened.ts b/src/runtime/motion/tweened.ts
--- a/src/runtime/motion/tweened.ts
+++ b/src/runtime/motion/tweened.ts
@@ -69,13 +69,18 @@ interface Tweened<T> extends Readable<T> {
 	update(updater: Updater<T>, opts: Options<T>): Promise<void>;
 }
 
-export function tweened<T>(value: T, defaults: Options<T> = {}): Tweened<T> {
+export function tweened<T>(value?: T, defaults: Options<T> = {}): Tweened<T> {
 	const store = writable(value);
 
 	let task: Task;
 	let target_value = value;
 
 	function set(new_value: T, opts: Options<T>) {
+		if (value == null) {
+			store.set(value = new_value);
+			return Promise.resolve();
+		}
+
 		target_value = new_value;
 
 		let previous_task = task;


Test patch:
diff --git a/test/motion/index.js b/test/motion/index.js
new file mode 100644
--- /dev/null
+++ b/test/motion/index.js
@@ -0,0 +1,23 @@
+import * as assert from 'assert';
+import { get } from '../../store';
+import { spring, tweened } from '../../motion';
+
+describe('motion', () => {
+	describe('spring', () => {
+		it('handles initially undefined values', () => {
+			const size = spring();
+
+			size.set(100);
+			assert.equal(get(size), 100);
+		});
+	});
+
+	describe('tweened', () => {
+		it('handles initially undefined values', () => {
+			const size = tweened();
+
+			size.set(100);
+			assert.equal(get(size), 100);
+		});
+	});
+});