Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
9cd5acd
add test launcher for vscode
BryanCrotaz Dec 8, 2021
bdb5e02
refactor to separate files ready for ProxyChangeset
BryanCrotaz Dec 8, 2021
ffb953e
initial add files
BryanCrotaz Dec 8, 2021
6385273
changes storage finished, validation WIP
BryanCrotaz Dec 9, 2021
6567bf3
Start to implement validation
BryanCrotaz Dec 13, 2021
7365367
fix more tests
BryanCrotaz Dec 15, 2021
4ceac70
fix native setter test
BryanCrotaz Dec 15, 2021
a6b60d0
make debugging in vscode work
BryanCrotaz Dec 15, 2021
79478f3
fix running tests
BryanCrotaz Dec 15, 2021
5760701
fix toString so that equality tests work
BryanCrotaz Dec 15, 2021
918403b
remove test for toString override
BryanCrotaz Dec 15, 2021
cd55f7a
deliver errors in alphabetical order so that tests are predictable
BryanCrotaz Dec 15, 2021
6fdd099
implement skip validation option
BryanCrotaz Dec 15, 2021
eb3afb8
correctly identify changesets without private api
BryanCrotaz Dec 15, 2021
86fe5c1
fix test that relies on toString
BryanCrotaz Dec 15, 2021
1dd23e3
improve empty hash test
BryanCrotaz Dec 15, 2021
7ccc8df
clean up
BryanCrotaz Dec 15, 2021
1e4a96c
fix defineProperty on the proxy
BryanCrotaz Dec 15, 2021
1c09dcc
fix test comparisons that rely on ordering
BryanCrotaz Dec 15, 2021
2d5fcf4
add change storage to array proxy
BryanCrotaz Dec 15, 2021
9080ccf
fix array behaviour
BryanCrotaz Dec 16, 2021
e771328
fix change tracking tests
BryanCrotaz Dec 16, 2021
6b96e28
fix object change detection
BryanCrotaz Dec 16, 2021
34cc3c5
fix save returning async nested save result
BryanCrotaz Dec 16, 2021
688e9fd
fix error message ordering in alphabetical order
BryanCrotaz Dec 16, 2021
c91a56c
fix test for prepare - was dependent on array order
BryanCrotaz Dec 16, 2021
0d130ea
add access tests
BryanCrotaz Dec 16, 2021
cae41c1
implement merge
BryanCrotaz Dec 16, 2021
6526068
remove package lock
BryanCrotaz Dec 17, 2021
fb96ffe
remove old changesets
BryanCrotaz Dec 17, 2021
a879491
remove old changesets
BryanCrotaz Dec 17, 2021
dc2600a
start to refactor for new major
BryanCrotaz Dec 21, 2021
9845ccc
fix async validation test
BryanCrotaz Dec 21, 2021
e306068
Merge branch 'use-proxy-3' into use-proxy-incompatible
BryanCrotaz Dec 21, 2021
293da86
Added structure.drawio.svg
BryanCrotaz Dec 21, 2021
303eb4a
Update structure.drawio.svg
BryanCrotaz Dec 21, 2021
1848918
add structure drawing to readme
BryanCrotaz Dec 21, 2021
6a4cec9
remove svg background
BryanCrotaz Dec 21, 2021
fc1493c
update link to drawing
BryanCrotaz Dec 21, 2021
e1c217c
sanitize svg in readme
BryanCrotaz Dec 21, 2021
3036484
update to new api
BryanCrotaz Dec 21, 2021
792476f
Update structure.drawio.svg
BryanCrotaz Dec 21, 2021
e64b94b
Update structure.drawio.svg
BryanCrotaz Dec 22, 2021
feab59d
clean up public api
BryanCrotaz Dec 22, 2021
7a3fa16
Merge branches 'use-proxy-incompatible' and 'use-proxy-incompatible' …
BryanCrotaz Dec 22, 2021
f391093
fix up file locations and get basics of changeset up
BryanCrotaz Dec 22, 2021
a0c851f
refactoring content & originalContent
BryanCrotaz Dec 23, 2021
c28f1c2
remove @bind
BryanCrotaz Dec 23, 2021
633a1eb
refactor replacing array content into functon
BryanCrotaz Dec 23, 2021
0c8f4e5
tidy up public api
BryanCrotaz Dec 23, 2021
3b2912f
refactor merge onto the changeset
BryanCrotaz Dec 23, 2021
a97587b
fix validate signature on public api
BryanCrotaz Dec 23, 2021
08b2278
fix validation
BryanCrotaz Dec 23, 2021
c6be09e
rename and refactor and clean up
BryanCrotaz Dec 23, 2021
1ca5349
add crude implementation of array proxy handler
BryanCrotaz Dec 23, 2021
891a10a
implement save/restore on snapshot
BryanCrotaz Dec 23, 2021
45d776f
tidy up
BryanCrotaz Dec 24, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"version": "0.2.0",
"configurations":[
{
"type": "node",
"name": "vscode-jest-tests",
"request": "launch",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"program": "${workspaceFolder}/node_modules/.bin/jest",
"cwd": "${workspaceFolder}",
"args": [
"unit",
"--runInBand",
"--watchAll=false"
]
},
{
"type": "node",
"name": "vscode-jest-tests-windows",
"request": "launch",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"program": "${workspaceRoot}/node_modules/jest/bin/jest.js",
"cwd": "${workspaceFolder}",
"args": [
"--runInBand",
"--watchAll=false"
]
}
]
}
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"jest.jestCommandLine": "./node_modules/.bin/jest --runInBand",
"svg.preview.background": "custom"
}
194 changes: 91 additions & 103 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ let changeset = Changeset(user, validatorFn);
user.firstName; // "Michael"
user.lastName; // "Bolton"

changeset.set('firstName', 'Jim');
changeset.set('lastName', 'B');
changeset.get('isInvalid'); // true
changeset.get('errors'); // [{ key: 'lastName', validation: 'too short', value: 'B' }]
changeset.set('lastName', 'Bob');
changeset.get('isValid'); // true
changeset.content.firstName = 'Jim';
changeset.content.lastName = 'B';
changeset.isInvalid; // true
changeset.errors; // [{ key: 'lastName', validation: 'too short', value: 'B' }]
changeset.content.lastName = 'Bob';
changeset.isValid; // true

user.firstName; // "Michael"
user.lastName; // "Bolton"
Expand Down Expand Up @@ -53,10 +53,17 @@ In the above example, when the input changes, only the changeset's internal valu

On rollback, all changes are dropped and the underlying Object is left untouched.

## Structure

![Structure drawing](./structure.drawio.svg?sanitize=true "Structure")

## Full API

```js
Changeset(model, lookupValidator(validationMap), validationMap, { skipValidate: boolean, changesetKeys: string[] });
changeset(model); // simplest

changeset(model, lookupValidator(validationMap), validationMap, { skipValidate: boolean, changesetKeys: string[] });

```

- `model` (required)
Expand All @@ -81,14 +88,13 @@ Changeset(model, lookupValidator(validationMap), validationMap, { skipValidate:
+ [`change`](#change)
+ [`errors`](#errors)
+ [`changes`](#changes)
+ [`data`](#data)
+ [`content`](#content)
+ [`originalContent`](#originalcontent)
+ [`isValid`](#isvalid)
+ [`isInvalid`](#isinvalid)
+ [`isPristine`](#ispristine)
+ [`isDirty`](#isdirty)
* Methods
+ [`get`](#get)
+ [`set`](#set)
+ [`prepare`](#prepare)
+ [`execute`](#execute)
+ [`save`](#save)
Expand Down Expand Up @@ -253,15 +259,54 @@ You can use this property to render a list of changes:

**[⬆️ back to top](#api)**

#### `data`
#### `content`

Returns the Object that was wrapped in the changeset.
Returns an Proxy representing `originalContent` plus all the current changes.

You can call `asChangeset` on the proxy to get back to this changeset api. You can do this on any child object in the tree to get a changeset that you can pass
for example to a child component. This is the only property on the proxy that will conflict with your object's API. All other property and methods will be
passed through to your object.

In this way a child component can see its changes and validation errors scoped
only to this proxied object.

```hbs
{{#each this.changeset.content.people as |person|}}
<p>{{person.name}}</p>
<AddressEditor @address={{person.address.asChangeset}}/>
{{/each}}
```

```js
let user = { name: 'Bobby', age: 21, address: { zipCode: '10001' } };
let changeset = Changeset(user);

changeset.get('data'); // user
changeset.content.age = 17;
```

**[⬆️ back to top](#api)**


```js
let user = { name: 'Bobby', age: 21, address: { zipCode: '10001' } };
let changeset = Changeset(user);

changeset.content.age = 17;
```

**[⬆️ back to top](#api)**

#### `originalContent`

Returns the Object that was wrapped in the changeset without any
pending changes applied.

```js
let user = { name: 'Bobby', age: 21, address: { zipCode: '10001' } };
let changeset = Changeset(user);

changeset.content.age = 45;
changeset.originalContent.age; // 21
```

**[⬆️ back to top](#api)**
Expand All @@ -271,7 +316,7 @@ changeset.get('data'); // user
Returns a Boolean value of the changeset's validity.

```js
changeset.get('isValid'); // true
changeset.isValid; // true
```

You can use this property in the template:
Expand All @@ -289,7 +334,7 @@ You can use this property in the template:
Returns a Boolean value of the changeset's (in)validity.

```js
changeset.get('isInvalid'); // true
changeset.isInvalid; // true
```

You can use this property in the template:
Expand All @@ -307,22 +352,22 @@ You can use this property in the template:
Returns a Boolean value of the changeset's state. A pristine changeset is one with no changes.

```js
changeset.get('isPristine'); // true
changeset.isPristine; // true
```

If changes present on the changeset are equal to the content's, this will return `true`. However, note that key/value pairs in the list of changes must all be present and equal on the content, but not necessarily vice versa:

```js
let user = { name: 'Bobby', age: 21, address: { zipCode: '10001' } };

changeset.set('name', 'Bobby');
changeset.get('isPristine'); // true
changeset.content.name = 'Bobby';
changeset.isPristine; // true

changeset.set('address.zipCode', '10001');
changeset.get('isPristine'); // true
changeset.content.address.zipCode = '10001';
changeset.isPristine; // true

changeset.set('foo', 'bar');
changeset.get('isPristine'); // false
changeset.content.foo = 'bar';
changeset.isPristine; // false
```

**[⬆️ back to top](#api)**
Expand All @@ -332,68 +377,11 @@ changeset.get('isPristine'); // false
Returns a Boolean value of the changeset's state. A dirty changeset is one with changes.

```js
changeset.get('isDirty'); // true
changeset.isDirty; // true
```

**[⬆️ back to top](#api)**

#### `get`

Exactly the same semantics as `Ember.get`. This proxies first to the error value, the changed value, and finally to the underlying Object.

```js
changeset.get('firstName'); // "Jim"
changeset.set('firstName', 'Billy'); // "Billy"
changeset.get('firstName'); // "Billy"

changeset.get('address.zipCode'); // "10001"
changeset.set('address.zipCode', '94016'); // "94016"
changeset.get('address.zipCode'); // "94016"
```

You can use and bind this property in the template:

```hbs
{{input value=changeset.firstName}}
```

Note that using `Ember.get` **will not necessarily work if you're expecting an Object**. On the other hand, using `changeset.get` will work just fine:

```js
get(changeset, 'momentObj').format('dddd'); // will error, format is undefined
changeset.get('momentObj').format('dddd'); // => "Friday"
```

This is because `Changeset` wraps an Object with `Ember.ObjectProxy` internally, and overrides `Ember.Object.get` to hide this implementation detail.

Because an Object is wrapped with `Ember.ObjectProxy`, the following (although more verbose) will also work:

```js
get(changeset, 'momentObj.content').format('dddd'); // => "Friday"
```

**[⬆️ back to top](#api)**

#### `set`

Exactly the same semantics as `Ember.set`. This stores the change on the changeset. It is recommended to use `changeset.set(...)` instead of `Ember.set(changeset, ...)`. `Ember.set` will set the property for nested keys on the underlying model.

```js
changeset.set('firstName', 'Milton'); // "Milton"
changeset.set('address.zipCode', '10001'); // "10001"
```

You can use and bind this property in the template:

```hbs
{{input value=changeset.firstName}}
{{input value=changeset.address.country}}
```

Any updates on this value will only store the change on the changeset, even with 2 way binding.

**[⬆️ back to top](#api)**

#### `prepare`

Provides a function to run before emitting changes to the model. The callback function must return a hash in the same shape:
Expand Down Expand Up @@ -524,8 +512,8 @@ Rolls back unsaved changes for the specified property only. All other changes wi
```js
// user = { firstName: "Jim", lastName: "Bob" };
let changeset = Changeset(user);
changeset.set('firstName', 'Jimmy');
changeset.set('lastName', 'Fallon');
changeset.content.firstName = 'Jimmy';
changeset.content.lastName = 'Fallon';
changeset.rollbackProperty('lastName'); // returns changeset
changeset.execute();
user.firstName; // "Jimmy"
Expand Down Expand Up @@ -554,7 +542,7 @@ let validationMap = {
};

let changeset = Changeset(user, validatorFn, validationMap);
changeset.get('isValid'); // true
changeset.isValid; // true

// validate single field; returns Promise
changeset.validate('lastName');
Expand All @@ -564,11 +552,11 @@ changeset.validate('lastName', 'address.zipCode');

// validate all fields; returns Promise
changeset.validate().then(() => {
changeset.get('isInvalid'); // true
changeset.isInvalid; // true

// [{ key: 'lastName', validation: 'too short', value: 'B' },
// { key: 'address.zipCode', validation: 'too short', value: '123' }]
changeset.get('errors');
changeset.errors;
});
```

Expand Down Expand Up @@ -629,16 +617,16 @@ Restores a snapshot of changes and errors to the changeset. This overrides exist
let user = { name: 'Adam', address: { country: 'United States' } };
let changeset = Changeset(user, validatorFn);

changeset.set('name', 'Jim Bob');
changeset.set('address.country', 'North Korea');
changeset.content.name = 'Jim Bob';
changeset.content.address.country = 'North Korea';
let snapshot = changeset.snapshot();

changeset.set('name', 'Poteto');
changeset.set('address.country', 'Australia')
changeset.content.name = 'Poteto';
changeset.content.address.country = 'Australia'

changeset.restore(snapshot);
changeset.get('name'); // "Jim Bob"
changeset.get('address.country'); // "North Korea"
changeset.name; // "Jim Bob"
changeset.address.country; // "North Korea"
```

**[⬆️ back to top](#api)**
Expand All @@ -651,18 +639,18 @@ Unlike `Ecto.Changeset.cast`, `cast` will take an array of allowed keys and remo
let allowed = ['name', 'password', 'address.country'];
let changeset = Changeset(user, validatorFn);

changeset.set('name', 'Jim Bob');
changeset.set('address.country', 'United States');
changeset.content.name = 'Jim Bob';
changeset.content.address.country = 'United States';

changeset.set('unwantedProp', 'foo');
changeset.set('address.unwantedProp', 123);
changeset.get('unwantedProp'); // "foo"
changeset.get('address.unwantedProp'); // 123
changeset.content.unwantedProp = 'foo';
changeset.content.address.unwantedProp = 123;
changeset.unwantedProp; // "foo"
changeset.address.unwantedProp; // 123

changeset.cast(allowed); // returns changeset
changeset.get('unwantedProp'); // undefined
changeset.get('address.country'); // "United States"
changeset.get('another.unwantedProp'); // undefined
changeset.unwantedProp; // undefined
changeset.address.country; // "United States"
changeset.another.unwantedProp; // undefined
```

For example, this method can be used to only allow specified changes through prior to saving. This is especially useful if you also setup a `schema` object for your model (using Ember Data), which can then be exported and used as a list of allowed keys:
Expand Down Expand Up @@ -701,9 +689,9 @@ export default Controller.extend({
Checks to see if async validator for a given key has not resolved. If no key is provided it will check to see if any async validator is running.

```js
changeset.set('lastName', 'Appleseed');
changeset.set('firstName', 'Johnny');
changeset.set('address.city', 'Anchorage');
changeset.content.lastName = 'Appleseed';
changeset.content.firstName = 'Johnny';
changeset.content.address.city = 'Anchorage';
changeset.validate();

changeset.isValidating(); // true if any async validation is still running
Expand Down
Loading