Repository: sveltejs/svelte (JavaScript)
Date: 2017-12-30 15:39:36+00:00
Task type: Feature

Issue:
Option to prevent two-way binding?
A lot of people have internalized the FUD around two-way binding to the extent that they're turned off Svelte altogether, notwithstanding the fact that it's optional and explicitly opt-in. I wonder if the compiler should have an option that prevents bindings from being used, so the 'we lost our second round of financing because a junior dev added a `bind:` directive' scenario doesn't arise


Issue discussion:
I would welcome this, have never like two-way binding.
I like two-way binding when I want it. Here's a scenario that I often run into:

I have an edit form. I want it to edit a copy, and only commit the edits when the user saves. I want 2-way binding on that copy, but it shouldn't update the original (in some global array somewhere).

The way I like to do this is to pass the original into my form component, and in my form component's initialize method, I create a copy of the data.

It would be nice to be able to access the passed-in values in a components data method:

```js
import UserCard from './user_card.html';
export default {
  data(params) {
    return {
      tmp: copyObject(params.user)
    }
  },
  components: {UserCard}
}
```

This would be really handy.

As a followup to my last comment, this seems to work pretty well for getting a `tmp` property that is a copy of a the user object:

```js
    computed: {
      tmp: user => {
        return Object.assign({}, user);
      }
    },
```
When I use Redux with Ractive, I disable two-way binding to avoid accidentally mutating state.  If/when I hook up Redux to Svelte, I imagine I'd want the same option.
Is this literally requesting an option to disable the `bind:` directive?
yep

Solution patch:
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -81,6 +81,7 @@ The Svelte compiler optionally takes a second argument, an object of configurati
 | `hydratable` | `true`, `false` | Whether to support hydration on the compiled component. | `false` |
 | `customElement` | `true`, `false`, `{ tag, props }` | Whether to compile this component to a custom element. If `tag`/`props` are passed, compiles to a custom element and overrides the values exported by the component. | `false` |
 | `cascade` | `true`, `false` | Whether to cascade all of the component's styles to child components. If `false`, only selectors wrapped in `:global(...)` and keyframe IDs beginning with `-global-` are cascaded. | `true` |
+| `bind` | `boolean` | If `false`, disallows `bind:` directives | `true` |
 | | | |
 | `shared` | `true`, `false`, `string` | Whether to import various helpers from a shared external library. When you have a project with multiple components, this reduces the overall size of your JavaScript bundle, at the expense of having immediately-usable component. You can pass a string of the module path to use, or `true` will import from `'svelte/shared.js'`. | `false` |
 | `legacy` | `true`, `false` | Ensures compatibility with very old browsers, at the cost of some extra code. | `false` |
diff --git a/src/parse/index.ts b/src/parse/index.ts
--- a/src/parse/index.ts
+++ b/src/parse/index.ts
@@ -22,6 +22,7 @@ class ParseError extends CompileError {
 
 interface ParserOptions {
 	filename?: string;
+	bind?: boolean;
 }
 
 type ParserState = (parser: Parser) => (ParserState | void);
@@ -38,6 +39,8 @@ export class Parser {
 	js: Node;
 	metaTags: {};
 
+	allowBindings: boolean;
+
 	constructor(template: string, options: ParserOptions) {
 		if (typeof template !== 'string') {
 			throw new TypeError('Template must be a string');
@@ -46,6 +49,8 @@ export class Parser {
 		this.template = template.replace(/\s+$/, '');
 		this.filename = options.filename;
 
+		this.allowBindings = options.bind !== false;
+
 		this.index = 0;
 		this.stack = [];
 		this.metaTags = {};
diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts
--- a/src/parse/state/tag.ts
+++ b/src/parse/state/tag.ts
@@ -182,6 +182,10 @@ export default function tag(parser: Parser) {
 
 	let attribute;
 	while ((attribute = readAttribute(parser, uniqueNames))) {
+		if (attribute.type === 'Binding' && !parser.allowBindings) {
+			parser.error(`Two-way binding is disabled`, attribute.start);
+		}
+
 		element.attributes.push(attribute);
 		parser.allowWhitespace();
 	}


Test patch:
diff --git a/test/parser/index.js b/test/parser/index.js
--- a/test/parser/index.js
+++ b/test/parser/index.js
@@ -1,6 +1,6 @@
 import assert from 'assert';
 import fs from 'fs';
-import { svelte } from '../helpers.js';
+import { svelte, tryToLoadJson } from '../helpers.js';
 
 describe('parse', () => {
 	fs.readdirSync('test/parser/samples').forEach(dir => {
@@ -20,8 +20,10 @@ describe('parse', () => {
 				.readFileSync(`test/parser/samples/${dir}/input.html`, 'utf-8')
 				.replace(/\s+$/, '');
 
+			const options = tryToLoadJson(`test/parser/samples/${dir}/options.json`) || {};
+
 			try {
-				const actual = svelte.parse(input);
+				const actual = svelte.parse(input, options);
 				fs.writeFileSync(
 					`test/parser/samples/${dir}/_actual.json`,
 					JSON.stringify(actual, null, '\t')
diff --git a/test/parser/samples/error-binding-disabled/error.json b/test/parser/samples/error-binding-disabled/error.json
new file mode 100644
--- /dev/null
+++ b/test/parser/samples/error-binding-disabled/error.json
@@ -0,0 +1,8 @@
+{
+	"message": "Two-way binding is disabled",
+	"loc": {
+		"line": 1,
+		"column": 7
+	},
+	"pos": 7
+}
\ No newline at end of file
diff --git a/test/parser/samples/error-binding-disabled/input.html b/test/parser/samples/error-binding-disabled/input.html
new file mode 100644
--- /dev/null
+++ b/test/parser/samples/error-binding-disabled/input.html
@@ -0,0 +1 @@
+<input bind:value='name'>
diff --git a/test/parser/samples/error-binding-disabled/options.json b/test/parser/samples/error-binding-disabled/options.json
new file mode 100644
--- /dev/null
+++ b/test/parser/samples/error-binding-disabled/options.json
@@ -0,0 +1,3 @@
+{
+	"bind": false
+}
\ No newline at end of file