Repository: sveltejs/svelte (JavaScript)
Date: 2017-08-29 12:39:42+00:00
Task type: Feature

Issue:
Pass options through to `oncreate`
This has come up ([most recently](http://stackoverflow.com/questions/43740119/access-target-from-a-instantiated-component)) once or twice in discussion — there are some situations where it would be helpful to be able to access the instantiation options in `oncreate` (though I don't think it's something we should encourage, it's a useful escape hatch).
Allow "listeners" to be passed into constructor
Use case: I like to create my "app" component, then use `app.on()` API to bind event handlers on component events. Example:

```
var app = new App({
    target: document.querySelector('main'),
    data: someData
})

// Example listener outside the component hierarchy.
app.on('deleteUser', (event) => {
    // etc...
})
```

Problem: In a nested component's `oncreate` method I might call `this.fire('foo')` but it's too early to use `app.on()` with and I miss that event.

So... what if I could pass in a map of event names -> functions to the "app" component's constructor? Or is there a better approach?


Issue discussion:
I admire the simplicity and power of svelte but did wonder about the lack of lifecycle events. Unless I'm misunderstanding this wouldn't a beforeCreate event to go with onCreate/afterCreate event be simpler? (likewise for destroy). Am assuming target node would be accessible in the updated onCreate/afterCreate.
What would you do in a `beforeCreate` that you wouldn't do in `oncreate`? (Genuine question.)
Had thought it might be the place to grab some data or prep work but it does seem to be redundant.  Coming from Vue,  had thought that with mounted and other LC events that additional svelte events may be needed. Ignore this please Rich and it's refreshing to keep things minimal. Don't think beforeCreate is used much in Vue after all!
`./Nested.html` here is an example of something I am continually desiring, accomplished with hackery
https://svelte.technology/repl?version=1.30.0&gist=77ab59c28b56d44f6fb25cb750af9a20

And here's a less magical, but still hack-ugly version that rolls its own `on:create` https://svelte.technology/repl?version=1.30.0&gist=45b4a22ac71d357492994af0efc6837e

Can we have this?
```html
<Counter ref:counter/>
<Component arguments="{ counter: this.refs.counter }"/>
```

```html
<!-- Component.html -->
<button on:click="counter.increment()">increment</button>

<script>
export default {
  oncreate ({ counter }) {
    this.counter = counter
  }
}
```
This makes me think of a use case for adding a `.mount` method or something.
I can think of three possible solutions off the top of my head, each with downsides:

### initialisation option

Per the initial proposal:

```js
app = new App({
  target,
  on: {
    foo() {
      console.log('foo');
    }
  }
});
```

One weakness of this approach — though in practical terms, perhaps it doesn't matter — is that there's no equivalent of this:

```js
app.on('foo', () => console.log('foo 1'));
app.on('foo', () => console.log('foo 2'));
```

Each event can only have one listener.

The other downside is that it necessitates adding generated code to every component:

```js
if (options.on) {
  Object.keys(options.on).forEach(eventName => {
    this.on(eventName, options.on[eventName]);
  });
}
```

Not a lot of code, but we've been fairly careful about adding *any* non-optional code unless there's no other way. So let's see if there's another way...


### Queued events

Some event systems allow handlers to receive events that have already happened. Not a huge fan of this approach, it's confusing.


### `.mount(target)` method

This could work. Not much would need to change, and as a bonus you'd gain the ability to control where the component was inserted:

```diff
function App(options) {
  // ...
-  if ( options.target ) {
-    this._fragment.create();
-    this.mount(options.target, null );
-  }
+  if (options.target) this.mount(options.target, options.anchor);
}

// ...

+App.prototype.mount = function mount(target, anchor) {
+  this._fragment.create();
+  this._fragment.mount(target, anchor);
+};
```

The only thing I worry about here is API creep. Do we need to provide an equivalent `unmount` method? What's the behaviour if you repeatedly call `mount`? Does it add confusion about whether you're 'supposed' to use it (instead of specifying `target`), given that the only real use case is the one outlined here (not that you'd guess that from the API itself)?

Right now we have a very compact core API — three methods for dealing with data (`set`, `get` and `observe`, two for events (`on`, `fire`) and one for lifecycle (`destroy`). That's nice and learnable. Am hesitant to add to that unless we really have to. Which brings me to...

## Secret option number 4

If initialisation options were passed through to `oncreate` unmolested, then you could add whatever you wanted:

```js
app = new App({
  target,
  potato: () => {
    console.log('potato');
  }
});
```

```html
<!-- App.html -->
<script>
  export default {
    oncreate(options) {
      options.potato();
    }
  };
</script>
```

This has the advantage of enabling things beyond this use case, including things we haven't thought of yet. And it's as simple as doing this in the generated code:

```diff
-var oncreate = template.oncreate.bind( this );
+var oncreate = template.oncreate.bind( this, options );
```

The disadvantage is that the component needs to be designed with that in mind — the `oncreate` handler needs to know that it might have to call the `potato` function. But for any situation in which an event was getting called inside `oncreate`, that's probably true anyway.

Would that solve your problem @ScottMaclure?

Related issue: #550
Secret option 4 is something that I thought of and that I almost suggested as a reasonable way to do this, without having too much niche API - _but_ from what I could tell, the `oncreate` methods of children components were called before that of the parent component, so any emitted events would still be emitted before the listeners were attached.
> from what I could tell, the oncreate methods of children components were called before that of the parent component

Wouldn't it only really matter for the top-level component though? You don't have an opportunity to attach custom properties to the options for child components.

Relatedly, I've wondered about special-casing `on:create` to solve exactly that problem:

```html
<Widget on:create='doSomething()'/>

<script>
  export default {
    methods: {
      doSomething() {
        // just need to figure out whether this is called before the hook or after...
        console.log('this would be tricky to do any other way');
      }
    }
  }
</script>
Regarding option 4 (and I think what Conduitry said), doesn't the order of execution mean app's oncreate is called **after** the app's event handler for the nested component's event? E.g: https://svelte.technology/repl?version=1.30.0&gist=615fc3182e53f9d49c312e70376b91f9

So, in the event handler method you can't call options.potato? Or am I missing something?

Also, can someone explain a bit more about how the "mount" approach would work, in regards to my use case?

If it helps, I could add more info here about what I'm trying to achieve in my app's design, to see if there's a better approach. Or I could post it on SO as a separate question?
> Or I could post it on SO as a separate question?

@ScottMaclure it sounds like we're ultimately talking about changes to Svelte itself, so this is an appropriate place to have the discussion.

I misunderstood the point about order of execution with nested components — thanks for the demo. What if `options` was attached to the component?

```diff
methods: {
  requestData: () => {
-    console.log('app requestData, options.potato() needed here?')
+    this.options.potato();
  }
}
```

Would that give us the necessary escape hatch?
Yes, I think it would!

Background to this use case: I've rolled my own routing, which is data-driven (e.g. app.set({ route: blah }).
My "app" component has if/else statements to control what "page" component gets rendered. 
On a particular "page" component, `oncreate` fires a request up and out for its data, to be lazily loaded and `set` back in/down through the components.
I have some "store.js" equivalent modules that fetch data, outside of the components, loosely coupled thanks to usage of "app.on/set" API.

One use case breaks the design - when you refresh the browser with the URL pointing to this particular page - the app hasn't been created yet so the listeners aren't defined at that point.

I've written 2 demo apps in Svelte so far, following similar principles, and bumped into the same problem in both, so at least from my limited experience it sounds like a worthy issue to address.

The key technical design idea was to avoid injecting the store.js or other objects into the component hierarchy.... so `this.options.potato()` isn't terrible because I could give it some semantic name - I'm assuming that inside functions like that I'll be able to call `app.set` and my use case will work.

There are other approaches I could take for my app without needing a change to Svelte - for example, must my app actually wait until I navigate to that "page" component to lazily load data? What if I just start loading the data in the background, no matter what the page? Then I can just write a promise and call app.set, and the "page" component inits in a loading state. But would that scale if I had 50 similar pages in my app?

Rich, your HN demo in REPL has a similar feature with the async hashchange function, the only difference is I pulled logic like that out of the components completely.

Edit: here's an example nested component:
https://github.com/ScottMaclure/svelte-demo/blob/master/src/components/EditUser.html#L76




Solution patch:
diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts
--- a/src/generators/dom/index.ts
+++ b/src/generators/dom/index.ts
@@ -151,9 +151,9 @@ export default function dom(
 	// TODO deprecate component.teardown()
 	builder.addBlock(deindent`
 		function ${name} ( options ) {
-			options = options || {};
 			${options.dev &&
-				`if ( !options.target && !options._root ) throw new Error( "'target' is a required option" );`}
+				`if ( !options || (!options.target && !options._root) ) throw new Error( "'target' is a required option" );`}
+			this.options = options;
 			${generator.usesRefs && `this.refs = {};`}
 			this._state = ${templateProperties.data
 				? `@assign( @template.data(), options.data )`


Test patch:
diff --git a/test/js/samples/collapses-text-around-comments/expected-bundle.js b/test/js/samples/collapses-text-around-comments/expected-bundle.js
--- a/test/js/samples/collapses-text-around-comments/expected-bundle.js
+++ b/test/js/samples/collapses-text-around-comments/expected-bundle.js
@@ -217,7 +217,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = assign( template.data(), options.data );
 
 	this._observers = {
diff --git a/test/js/samples/collapses-text-around-comments/expected.js b/test/js/samples/collapses-text-around-comments/expected.js
--- a/test/js/samples/collapses-text-around-comments/expected.js
+++ b/test/js/samples/collapses-text-around-comments/expected.js
@@ -53,7 +53,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = assign( template.data(), options.data );
 
 	this._observers = {
diff --git a/test/js/samples/computed-collapsed-if/expected-bundle.js b/test/js/samples/computed-collapsed-if/expected-bundle.js
--- a/test/js/samples/computed-collapsed-if/expected-bundle.js
+++ b/test/js/samples/computed-collapsed-if/expected-bundle.js
@@ -165,7 +165,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 	this._recompute( {}, this._state, {}, true );
 
diff --git a/test/js/samples/computed-collapsed-if/expected.js b/test/js/samples/computed-collapsed-if/expected.js
--- a/test/js/samples/computed-collapsed-if/expected.js
+++ b/test/js/samples/computed-collapsed-if/expected.js
@@ -25,7 +25,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 	this._recompute( {}, this._state, {}, true );
 
diff --git a/test/js/samples/css-media-query/expected-bundle.js b/test/js/samples/css-media-query/expected-bundle.js
--- a/test/js/samples/css-media-query/expected-bundle.js
+++ b/test/js/samples/css-media-query/expected-bundle.js
@@ -199,7 +199,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/css-media-query/expected.js b/test/js/samples/css-media-query/expected.js
--- a/test/js/samples/css-media-query/expected.js
+++ b/test/js/samples/css-media-query/expected.js
@@ -39,7 +39,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/each-block-changed-check/expected-bundle.js b/test/js/samples/each-block-changed-check/expected-bundle.js
--- a/test/js/samples/each-block-changed-check/expected-bundle.js
+++ b/test/js/samples/each-block-changed-check/expected-bundle.js
@@ -313,7 +313,7 @@ function create_each_block ( state, each_block_value, comment, i, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js
--- a/test/js/samples/each-block-changed-check/expected.js
+++ b/test/js/samples/each-block-changed-check/expected.js
@@ -140,7 +140,7 @@ function create_each_block ( state, each_block_value, comment, i, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/event-handlers-custom/expected-bundle.js b/test/js/samples/event-handlers-custom/expected-bundle.js
--- a/test/js/samples/event-handlers-custom/expected-bundle.js
+++ b/test/js/samples/event-handlers-custom/expected-bundle.js
@@ -210,7 +210,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/event-handlers-custom/expected.js b/test/js/samples/event-handlers-custom/expected.js
--- a/test/js/samples/event-handlers-custom/expected.js
+++ b/test/js/samples/event-handlers-custom/expected.js
@@ -50,7 +50,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/if-block-no-update/expected-bundle.js b/test/js/samples/if-block-no-update/expected-bundle.js
--- a/test/js/samples/if-block-no-update/expected-bundle.js
+++ b/test/js/samples/if-block-no-update/expected-bundle.js
@@ -252,7 +252,7 @@ function select_block_type ( state ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/if-block-no-update/expected.js b/test/js/samples/if-block-no-update/expected.js
--- a/test/js/samples/if-block-no-update/expected.js
+++ b/test/js/samples/if-block-no-update/expected.js
@@ -88,7 +88,7 @@ function select_block_type ( state ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/if-block-simple/expected-bundle.js b/test/js/samples/if-block-simple/expected-bundle.js
--- a/test/js/samples/if-block-simple/expected-bundle.js
+++ b/test/js/samples/if-block-simple/expected-bundle.js
@@ -228,7 +228,7 @@ function create_if_block ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/if-block-simple/expected.js b/test/js/samples/if-block-simple/expected.js
--- a/test/js/samples/if-block-simple/expected.js
+++ b/test/js/samples/if-block-simple/expected.js
@@ -64,7 +64,7 @@ function create_if_block ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/legacy-input-type/expected-bundle.js b/test/js/samples/legacy-input-type/expected-bundle.js
--- a/test/js/samples/legacy-input-type/expected-bundle.js
+++ b/test/js/samples/legacy-input-type/expected-bundle.js
@@ -186,7 +186,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/legacy-input-type/expected.js b/test/js/samples/legacy-input-type/expected.js
--- a/test/js/samples/legacy-input-type/expected.js
+++ b/test/js/samples/legacy-input-type/expected.js
@@ -28,7 +28,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/non-imported-component/expected-bundle.js b/test/js/samples/non-imported-component/expected-bundle.js
--- a/test/js/samples/non-imported-component/expected-bundle.js
+++ b/test/js/samples/non-imported-component/expected-bundle.js
@@ -202,7 +202,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/non-imported-component/expected.js b/test/js/samples/non-imported-component/expected.js
--- a/test/js/samples/non-imported-component/expected.js
+++ b/test/js/samples/non-imported-component/expected.js
@@ -50,7 +50,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
--- a/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
+++ b/test/js/samples/onrender-onteardown-rewritten/expected-bundle.js
@@ -164,7 +164,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/onrender-onteardown-rewritten/expected.js b/test/js/samples/onrender-onteardown-rewritten/expected.js
--- a/test/js/samples/onrender-onteardown-rewritten/expected.js
+++ b/test/js/samples/onrender-onteardown-rewritten/expected.js
@@ -24,7 +24,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/setup-method/expected-bundle.js b/test/js/samples/setup-method/expected-bundle.js
--- a/test/js/samples/setup-method/expected-bundle.js
+++ b/test/js/samples/setup-method/expected-bundle.js
@@ -175,7 +175,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/setup-method/expected.js b/test/js/samples/setup-method/expected.js
--- a/test/js/samples/setup-method/expected.js
+++ b/test/js/samples/setup-method/expected.js
@@ -35,7 +35,7 @@ function create_main_fragment ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/use-elements-as-anchors/expected-bundle.js b/test/js/samples/use-elements-as-anchors/expected-bundle.js
--- a/test/js/samples/use-elements-as-anchors/expected-bundle.js
+++ b/test/js/samples/use-elements-as-anchors/expected-bundle.js
@@ -412,7 +412,7 @@ function create_if_block_4 ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/js/samples/use-elements-as-anchors/expected.js b/test/js/samples/use-elements-as-anchors/expected.js
--- a/test/js/samples/use-elements-as-anchors/expected.js
+++ b/test/js/samples/use-elements-as-anchors/expected.js
@@ -248,7 +248,7 @@ function create_if_block_4 ( state, component ) {
 }
 
 function SvelteComponent ( options ) {
-	options = options || {};
+	this.options = options;
 	this._state = options.data || {};
 
 	this._observers = {
diff --git a/test/runtime/index.js b/test/runtime/index.js
--- a/test/runtime/index.js
+++ b/test/runtime/index.js
@@ -81,6 +81,12 @@ describe("runtime", () => {
 						code.slice(0, startIndex).split('\n').map(x => spaces(x.length)).join('\n') +
 						code.slice(startIndex).replace(/export default .+/, "");
 					acorn.parse(es5, { ecmaVersion: 5 });
+
+					if (/Object\.assign/.test(es5)) {
+						throw new Error(
+							"cannot use Object.assign in generated code, as it is not supported everywhere"
+						);
+					}
 				} catch (err) {
 					failed.add(dir);
 					showOutput(cwd, { shared }); // eslint-disable-line no-console
@@ -133,11 +139,6 @@ describe("runtime", () => {
 					throw err;
 				}
 
-				let usedObjectAssign = false;
-				Object.assign = () => {
-					usedObjectAssign = true;
-				};
-
 				global.window = window;
 
 				// Put the constructor on window for testing
@@ -151,13 +152,13 @@ describe("runtime", () => {
 					warnings.push(warning);
 				};
 
-				const component = new SvelteComponent({
+				const options = Object.assign({}, {
 					target,
 					hydrate,
 					data: config.data
-				});
+				}, config.options || {});
 
-				Object.assign = Object_assign;
+				const component = new SvelteComponent(options);
 
 				console.warn = warn;
 
@@ -183,15 +184,7 @@ describe("runtime", () => {
 					component.destroy();
 					assert.equal(target.innerHTML, "");
 				}
-
-				if (usedObjectAssign) {
-					throw new Error(
-						"cannot use Object.assign in generated code, as it is not supported everywhere"
-					);
-				}
 			} catch (err) {
-				Object.assign = Object_assign;
-
 				if (config.error && !unintendedError) {
 					config.error(assert, err);
 				} else {
diff --git a/test/runtime/samples/options/_config.js b/test/runtime/samples/options/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/options/_config.js
@@ -0,0 +1,12 @@
+export default {
+	'skip-ssr': true,
+	html: `<p>from this.options</p>`,
+
+	options: {
+		text: 'from this.options'
+	},
+
+	test(assert, component) {
+		assert.equal(component.options.text, 'from this.options');
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/options/main.html b/test/runtime/samples/options/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/options/main.html
@@ -0,0 +1,11 @@
+<p>{{text}}</p>
+
+<script>
+	export default {
+		oncreate() {
+			this.set({
+				text: this.options.text
+			});
+		}
+	};
+</script>
\ No newline at end of file
diff --git a/test/runtime/samples/pass-no-options/_config.js b/test/runtime/samples/pass-no-options/_config.js
deleted file mode 100644
--- a/test/runtime/samples/pass-no-options/_config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export default {
-	html: '<h1>Just some static HTML</h1>',
-
-	test ( assert, component, target, window ) {
-		const newComp = new window.SvelteComponent();
-		assert.equal(newComp instanceof window.SvelteComponent, true);
-	}
-};
diff --git a/test/runtime/samples/pass-no-options/main.html b/test/runtime/samples/pass-no-options/main.html
deleted file mode 100644
--- a/test/runtime/samples/pass-no-options/main.html
+++ /dev/null
@@ -1 +0,0 @@
-<h1>Just some static HTML</h1>