Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add master to nested keys feature #167

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 6,
sourceType: 'module'
},
extends: 'eslint:recommended',
env: {
browser: true
},
rules: {
}
};
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# See https://help.github.com/ignore-files/ for more about ignoring files.

# compiled output
/dist
Expand All @@ -13,5 +13,5 @@
/connect.lock
/coverage/*
/libpeerconnection.log
npm-debug.log
npm-debug.log*
testem.log
32 changes: 0 additions & 32 deletions .jshintrc

This file was deleted.

2 changes: 1 addition & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
.editorconfig
.ember-cli
.gitignore
.jshintrc
.eslintrc.js
.watchmanconfig
.travis.yml
bower.json
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2016 Lauren Elizabeth Tan
Copyright (c) 2017 Lauren Elizabeth Tan

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Expand Down
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@ 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.

## Disabling Automatic Validation

The default behavior of `Changeset` is to automatically validate a field when it is set. Automatic validation can be disabled by passing `skipValidate` as on option when creating a changeset.

```js
let changeset = new Changeset(model, validatorFn, validationMap, { skipValidate: true });
```

```hbs
{{#with (changeset model (action "validate") skipValidate=true) as |changeset|}}
...
{{/with}}
```

Be sure to call `validate()` on the `changeset` before saving or committing changes.

## API

* Properties
Expand All @@ -139,6 +155,10 @@ On rollback, all changes are dropped and the underlying Object is left untouched
+ [`snapshot`](#snapshot)
+ [`restore`](#restore)
+ [`cast`](#cast)
+ [`isValidating`](#isvalidating)
* Events
+ [`beforeValidation`](#beforevalidation)
+ [`afterValidation`](#aftervalidation)

#### `error`

Expand Down Expand Up @@ -533,6 +553,63 @@ export default Controller.extend({

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

#### `isValidating`

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.validate('lastName');
changeset.isValidating('lastName'); // would return true if lastName validation is async and still running
changeset.validate().then(() => {
changeset.isValidating('lastName'); // false since validations are complete
});
```

```js
changeset.set('lastName', 'Appleseed');
changeset.set('firstName', 'Johnny');
changeset.validate();
changeset.isValidating(); // returns true if any async validation is still running
changeset.isValidating('lastName'); // returns true if lastName validation is async and still running
changeset.validate().then(() => {
changeset.isValidating(); // returns false since validations are complete
});
```

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

#### `beforeValidation`

This event is triggered after isValidating is set to true for a key, but before the validation is complete.

```js
changeset.on('beforeValidation', key => {
console.log(`${key} is validating...`);
});
changeset.validate();
changeset.isValidating(); // true
// console output: lastName is validating...
```

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

#### `afterValidation`

This event is triggered after async validations are complete and isValidating is set to false for a key.

```js
changeset.on('afterValidation', key => {
console.log(`${key} has completed validating`);
});
changeset.validate().then(() => {
changeset.isValidating(); // false
// console output: lastName has completed validating
});
```

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

## Validation signature

To use with your favorite validation library, you should create a custom `validator` action to be passed into the changeset:
Expand Down
6 changes: 3 additions & 3 deletions addon/helpers/changeset.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import isPromise from 'ember-changeset/utils/is-promise';

const { Helper: { helper } } = Ember;

export function changeset([obj, validations]) {
export function changeset([obj, validations], options = {}) {
if (isChangeset(obj)) {
return obj;
}

if (isPromise(obj)) {
return obj.then((resolved) => new Changeset(resolved, validations));
return obj.then((resolved) => new Changeset(resolved, validations, {}, options));
}

return new Changeset(obj, validations);
return new Changeset(obj, validations, {}, options);
}

export default helper(changeset);
80 changes: 76 additions & 4 deletions addon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ const {
Object: EmberObject,
RSVP: { all, resolve },
computed: { not, readOnly },
Evented,
A: emberArray,
assert,
get,
isArray,
isEmpty,
isEqual,
isNone,
isPresent,
Expand All @@ -31,23 +33,31 @@ const CHANGES = '_changes';
const ERRORS = '_errors';
const VALIDATOR = '_validator';
const RELAY_CACHE = '_relayCache';
const OPTIONS = '_options';
const RUNNING_VALIDATIONS = '_runningValidations';
const BEFORE_VALIDATION_EVENT = 'beforeValidation';
const AFTER_VALIDATION_EVENT = 'afterValidation';

function defaultValidatorFn() {
return true;
}

const defaultOptions = { skipValidate: false };

/**
* Creates new changesets.
*
* @uses Ember.Evented
* @param {Object} obj
* @param {Function} validateFn
* @param {Object} validationMap
* @param {Object} options
* @return {Ember.Object}
*/
export function changeset(obj, validateFn = defaultValidatorFn, validationMap = {}) {
export function changeset(obj, validateFn = defaultValidatorFn, validationMap = {}, options = {}) {
assert('Underlying object for changeset is missing', isPresent(obj));

return EmberObject.extend({
return EmberObject.extend(Evented, {
/**
* Internal descriptor for changeset identification
*
Expand All @@ -74,6 +84,8 @@ export function changeset(obj, validateFn = defaultValidatorFn, validationMap =
this[ERRORS] = {};
this[RELAY_CACHE] = {};
this[VALIDATOR] = validateFn;
this[OPTIONS] = pureAssign(defaultOptions, options);
this[RUNNING_VALIDATIONS] = {};
},

/**
Expand All @@ -96,6 +108,13 @@ export function changeset(obj, validateFn = defaultValidatorFn, validationMap =
* @return {Any}
*/
setUnknownProperty(key, value) {
let changesetOptions = get(this, OPTIONS);
let skipValidate = get(changesetOptions, 'skipValidate');

if (skipValidate) {
return this._setProperty(true, { key, value });
}

return this.validateAndSet(key, value);
},

Expand All @@ -106,7 +125,8 @@ export function changeset(obj, validateFn = defaultValidatorFn, validationMap =
* @return {String}
*/
toString() {
return `changeset:${get(this, CONTENT).toString()}`;
let normalisedContent = pureAssign(get(this, CONTENT), {});
return `changeset:${normalisedContent.toString()}`;
},

/**
Expand Down Expand Up @@ -286,6 +306,22 @@ export function changeset(obj, validateFn = defaultValidatorFn, validationMap =
return resolve(this.validateAndSet(key, this.valueFor(key)));
},

/**
* 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.
*
* @public
* @param {String|Undefined} key
* @return {boolean}
*/
isValidating(key) {
let runningValidations = get(this, RUNNING_VALIDATIONS);
let ks = emberArray(keys(runningValidations));
if (key) { return ks.includes(key); }

return !isEmpty(ks);
},

/**
* Manually add an error to the changeset. If there is an existing error or
* change for `key`, it will be overwritten.
Expand Down Expand Up @@ -409,11 +445,17 @@ export function changeset(obj, validateFn = defaultValidatorFn, validationMap =
let validation = this._validate(key, value, oldValue);

if (isPromise(validation)) {
this._setIsValidating(key, true);
this.trigger(BEFORE_VALIDATION_EVENT, key);
return validation.then((resolvedValidation) => {
this._setIsValidating(key, false);
this.trigger(AFTER_VALIDATION_EVENT, key);
return this._setProperty(resolvedValidation, { key, value, oldValue });
});
}

this.trigger(BEFORE_VALIDATION_EVENT, key);
this.trigger(AFTER_VALIDATION_EVENT, key);
return this._setProperty(validation, { key, value, oldValue });
},

Expand Down Expand Up @@ -500,17 +542,47 @@ export function changeset(obj, validateFn = defaultValidatorFn, validationMap =
} else if (obj.hasOwnProperty(key)) {
delete changes[key];
}

this.notifyPropertyChange(CHANGES);
this.notifyPropertyChange(root);

let errors = get(this, ERRORS);
if (errors['__ember_meta__'] && errors['__ember_meta__']['values']) {
delete errors['__ember_meta__']['values'][key];
set(this, ERRORS, errors);
}

return value;
}

return this.addError(key, { value, validation });
},

/**
* TODO
* Updates the cache that stores the number of running validations
* for a given key.
*
* @private
* @param {String} key
* @param {Boolean} value
*/
_setIsValidating(key, value) {
let runningValidations = get(this, RUNNING_VALIDATIONS);
let count = get(runningValidations, key) || 0;

if (value) {
set(runningValidations, key, count + 1);
} else {
if (count === 1) {
delete runningValidations[key];
} else {
set(runningValidations, key, count - 1);
}
}
},

/**
* Value for change or the original value.
*
* @private
* @param {String} key
Expand Down
8 changes: 0 additions & 8 deletions bower.json

This file was deleted.

1 change: 0 additions & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ dependencies:
- npm install -g bower
post:
- npm install
- bower install
test:
override:
- npm test
Loading