Giter Site home page Giter Site logo

ember-changeset's Introduction



ember-changeset


Download count all time GitHub Actions Build Status npm version Ember Observer Score

ember install ember-changeset

Watch a free video intro presented by EmberScreencasts

Updates

We have released v3.0.0. See the CHANGELOG here. This requires Ember >= 3.13 as the use of @tracked will help us monitor and propagate changes to the UI layer. If your app is < 3.13 or you need to support IE11, then you can install the 2.0 series ember install [email protected].

Support for IE11 was dropped with the v3.0.0 release given our ubiquitous use of Proxy.

The base library for this addon is validated-changeset. As a result, this functionality is available outside of Ember as well!

Philosophy

The idea behind a changeset is simple: it represents a set of valid changes to be applied onto any Object (Ember.Object, DS.Model, POJOs, etc). Each change is tested against an optional validation, and if valid, the change is stored and applied when executed.

Assuming a Data Down, Actions Up (DDAU) approach, a changeset is more appropriate compared to implicit 2 way bindings. Other validation libraries only validate a property after it is set on an Object, which means that your Object can enter an invalid state.

ember-changeset only allows valid changes to be set, so your Objects will never become invalid (assuming you have 100% validation coverage). Additionally, this addon is designed to be un-opinionated about your choice of form and/or validation library, so you can easily integrate it into an existing solution.

The simplest way to incorporate validations is to use ember-changeset-validations, a companion addon to this one. It has a simple mental model, and there are no Observers or CPs involved – just pure functions.

See also the plugins section for addons that extend ember-changeset.

tl;dr

import { Changeset } from 'ember-changeset';

let dummyValidations = {
  firstName(newValue) {
    return !!newValue;
  },
};

function validatorFn({ key, newValue, oldValue, changes, content }) {
  let validator = get(dummyValidations, key);

  if (typeof validator === 'function') {
    return validator(newValue, oldValue, changes, content);
  }
}

let changeset = Changeset(user, validatorFn);
user.get('firstName'); // "Michael"
user.get('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

user.get('firstName'); // "Michael"
user.get('lastName'); // "Bolton"

changeset.save(); // sets and saves valid changes on the user
user.get('firstName'); // "Jim"
user.get('lastName'); // "Bob"

Usage

First, create a new Changeset using the changeset helper or through JavaScript via a factory function:

{{! application/template.hbs}}
{{#with (changeset model this.validate) as |changesetObj|}}
  <DummyForm
      @changeset={{changesetObj}}
      @submit={{this.submit}}
      @rollback={{this.rollback}} />
{{/with}}
import Component from '@ember/component';
import { Changeset } from 'ember-changeset';

export default FormComponent extends Component {
  init(...args) {
    super.init(...args)

    let validator = this.validate;
    this.changeset = Changeset(this.model, validator);
  }
}

The helper receives any Object (including DS.Model, Ember.Object, or even POJOs) and an optional validator action. If a validator is passed into the helper, the changeset will attempt to call that function when a value changes.

// application/controller.js
import Controller from '@ember/controller';
import { action } from '@ember/object';

export default class FormController extends Controller {
  @action
  submit(changeset) {
    return changeset.save();
  }

  @action
  rollback(changeset) {
    return changeset.rollback();
  }

  @action
  validate({ key, newValue, oldValue, changes, content }) {
    // lookup a validator function on your favorite validation library
    // and return a Boolean
  }
}

Then, in your favorite form library, simply pass in the changeset in place of the original model.

{{! dummy-form/template.hbs}}
<form>
  <Input @value={{changeset.firstName}} />
  <Input @value={{changeset.lastName}} />

  <button {{on "click" this.submit changeset}}>Submit</button>
  <button {{on "click" this.rollback changeset}}>Cancel</button>
</form>

In the above example, when the input changes, only the changeset's internal values are updated. When the submit button is clicked, the changes are only executed if all changes are valid.

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

Extending the base ember-changeset class

import { EmberChangeset, Changeset } from 'ember-changeset';

class MyChangeset extends EmberChangeset {
  save() {
    super.save(...arguments);
    // do stuff
  }
}

let changeset = Changeset(user, validatorFn, validationMap, { changeset: MyChangeset });

Changeset template helpers

ember-changeset overrides set and get in order to handle deeply nested setters. mut is simply an alias for Ember.set(changeset, ...), thus we provide a changeset-set template helper if you are dealing with nested setters.

changeset-get is necessary for nested getters to easily retrieve leaf keys without error. Ember's templating layer will ask us for the first key it comes across as it traverses down the object (user.firstName). We keep track of the changes, but to also keep track of unchanged values and properly merge them in the changeset is difficult. If you are only accessing keys in an object that is only one level deep, you do not need this helper.

<form>
  <input
    id="first-name"
    type="text"
    value={{changeset-get changeset "person.firstName"}}
    {{on "change" (fn this.updateFirstName changeset)}}>
</form>

Limiting which keys dirty the changeset

In order to limit the changes made to your changeset and it's associated isDirty state, you can pass in a list of changesetKeys.

let changeset = Changeset(model, validatorFn, validationMap, { changesetKeys: ['name'] });

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 an option when creating a changeset.

let changeset = Changeset(model, validatorFn, validationMap, { skipValidate: true });
{{#with (changeset model this.validate skipValidate=true) as |changesetObj|}}
  ...
{{/with}}

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

Types

import Component from '@glimmer/component';
import { BufferedChangeset } from 'ember-changeset/types';
import { Changeset } from 'ember-changeset';

interface Args {
  user: {
    name: string;
    age: number;
  };
}

export default class Foo extends Component<Args> {
  changeset: BufferedChangeset;

  constructor(owner, args) {
    super(owner, args);
    this.changeset = Changeset(args.user);
  }
}

Other available types include the following. Please put in a PR if you need more types or access directly in validated-changeset!

import { ValidationResult, ValidatorMapFunc, ValidatorAction } from 'ember-changeset/types';

Alternative Changeset

Enabled in 4.1.0. Experimental and subject to changes until 5.0.

We now ship a ValidatedChangeset that is a proposed new API we would like to introduce and see if it jives with users. The goal of this new feature is to remove confusing APIs and externalize validations.

  • ✂️ save
  • ✂️ cast
  • ✂️ merge
  • errors are required to be added to the Changeset manually after validate
  • validate takes a callback with the sum of changes and original content to be applied to your externalized validation. In user land you will call changeset.validate((changes) => yupSchema.validate(changes))
import Component from '@glimmer/component';
import { ValidatedChangeset } from 'ember-changeset';
import { action, get } from '@ember/object';
import { object, string } from 'yup';

class Foo {
  user = {
    name: 'someone',
    email: '[email protected]',
  };
}

const FormSchema = object({
  cid: string().required(),
  user: object({
    name: string().required(),
    email: string().email(),
  })
});

export default class ValidatedForm extends Component {
  constructor() {
    super(...arguments);

    this.model = new Foo();
    this.changeset = ValidatedChangeset(this.model);
  }

  @action
  async setChangesetProperty(path, evt) {
    this.changeset.set(path, evt.target.value);
    try {
      await this.changeset.validate((changes) => FormSchema.validate(changes));
      this.changeset.removeError(path);
    } catch (e) {
      this.changeset.addError(e.path, { value: this.changeset.get(e.path), validation: e.message });
    }
  }

  @action
  async submitForm(changeset, event) {
    event.preventDefault();

    changeset.execute();
    await this.model.save();
  }
}

API

error

Returns the error object.

{
  firstName: {
    value: 'Jim',
    validation: 'First name must be greater than 7 characters'
  }
}

Note that keys can be arbitrarily nested:

{
  address: {
    zipCode: {
      value: '123',
      validation: 'Zip code must have 5 digits'
    }
  }
}

You can use this property to locate a single error:

{{#if changeset.error.firstName}}
  <p>{{changeset.error.firstName.validation}}</p>
{{/if}}

{{#if changeset.error.address.zipCode}}
  <p>{{changeset.error.address.zipCode.validation}}</p>
{{/if}}

⬆️ back to top

change

Returns the change object.

{
  firstName: 'Jim';
}

Note that keys can be arbitrarily nested:

{
  address: {
    zipCode: '10001';
  }
}

You can use this property to locate a single change:

{{changeset.change.firstName}}
{{changeset.change.address.zipCode}}

⬆️ back to top

errors

Returns an array of errors. If your validate function returns a non-boolean value, it is added here as the validation property.

[
  {
    key: 'firstName',
    value: 'Jim',
    validation: 'First name must be greater than 7 characters',
  },
  {
    key: 'address.zipCode',
    value: '123',
    validation: 'Zip code must have 5 digits',
  },
];

You can use this property to render a list of errors:

{{#if changeset.isInvalid}}
  <p>There were errors in your form:</p>
  <ul>
    {{#each changeset.errors as |error|}}
      <li>{{error.key}}: {{error.validation}}</li>
    {{/each}}
  </ul>
{{/if}}

⬆️ back to top

changes

Returns an array of changes to be executed. Only valid changes will be stored on this property.

[
  {
    key: 'firstName',
    value: 'Jim',
  },
  {
    key: 'address.zipCode',
    value: 10001,
  },
];

You can use this property to render a list of changes:

<ul>
  {{#each changeset.changes as |change|}}
    <li>{{change.key}}: {{change.value}}</li>
  {{/each}}
</ul>

⬆️ back to top

data

Returns the Object that was wrapped in the changeset.

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

changeset.get('data'); // user

⬆️ back to top

pendingData

Returns object with changes applied to original data without mutating original data object. Unlike execute(), pendingData shows resulting object even if validation failed. Original data or changeset won't be modified.

Note: Currently, it only works with POJOs. Refer to execute for a way to apply changes onto ember-data models.

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

changeset.set('name', 'Zoe');

changeset.get('pendingData'); // { name: 'Zoe', age: 21, address: { zipCode: '10001' } }

⬆️ back to top

isValid

Returns a Boolean value of the changeset's validity.

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

You can use this property in the template:

{{#if changeset.isValid}}
  <p>Good job!</p>
{{/if}}

⬆️ back to top

isInvalid

Returns a Boolean value of the changeset's (in)validity.

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

You can use this property in the template:

{{#if changeset.isInvalid}}
  <p>There were one or more errors in your form</p>
{{/if}}

⬆️ back to top

isPristine

Returns a Boolean value of the changeset's state. A pristine changeset is one with no changes.

changeset.get('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:

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

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

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

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

⬆️ back to top

isDirty

Returns a Boolean value of the changeset's state. A dirty changeset is one with changes.

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

⬆️ back to top

get

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

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:

{{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:

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:

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

⬆️ back to top

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.

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

You can use and bind this property in the template:

{{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

prepare

Provides a function to run before emitting changes to the model. The callback function must return a hash in the same shape:

changeset.prepare((changes) => {
  // changes = { firstName: "Jim", lastName: "Bob", 'address.zipCode': "07030" };
  let modified = {};

  for (let key in changes) {
    let newKey = key.split('.').map(underscore).join('.');
    modified[newKey] = changes[key];
  }

  // don't forget to return, the original changes object is not mutated
  // modified = { first_name: "Jim", last_name: "Bob", 'address.zip_code': "07030" };
  return modified;
}); // returns changeset

The callback function is not validated – if you modify a value, it is your responsibility to ensure that it is valid.

Returns the changeset.

⬆️ back to top

execute

Applies the valid changes to the underlying Object.

changeset.execute(); // returns changeset

Note that executing the changeset will not remove the internal list of changes - instead, you should do so explicitly with rollback or save if that is desired.

⬆️ back to top

unexecute

Undo changes made to underlying Object for changeset. This is often useful if you want to remove changes from underlying Object if save fails.

changeset
  .save()
  .catch(() => {
    // save applies changes to the underlying Object via this.execute(). This may be undesired for your use case.
    dummyChangeset.unexecute();
  })

⬆️ back to top

save

Executes changes, then proxies to the underlying Object's save method, if one exists. If it does, the method can either return a Promise or a non-Promise value. Either way, the changeset's save method will return a promise.

changeset.save(); // returns Promise

The save method will also remove the internal list of changes if the save is successful.

⬆️ back to top

merge

Merges 2 changesets and returns a new changeset with the same underlying content and validator as the origin. Both changesets must point to the same underlying object. For example:

let changesetA = Changeset(user, validatorFn);
let changesetB = Changeset(user, validatorFn);

changesetA.set('firstName', 'Jim');
changesetA.set('address.zipCode', '94016');

changesetB.set('firstName', 'Jimmy');
changesetB.set('lastName', 'Fallon');
changesetB.set('address.zipCode', '10112');

let changesetC = changesetA.merge(changesetB);
changesetC.execute();

user.get('firstName'); // "Jimmy"
user.get('lastName'); // "Fallon"
user.get('address.zipCode'); // "10112"

⬆️ back to top

rollback

Rolls back all unsaved changes and resets all errors.

changeset.rollback(); // returns changeset

⬆️ back to top

rollbackInvalid

Rolls back all invalid unsaved changes and resets all errors. Valid changes will be kept on the changeset.

changeset.rollbackInvalid(); // returns changeset

⬆️ back to top

rollbackProperty

Rolls back unsaved changes for the specified property only. All other changes will be kept on the changeset.

// user = { firstName: "Jim", lastName: "Bob" };
let changeset = Changeset(user);
changeset.set('firstName', 'Jimmy');
changeset.set('lastName', 'Fallon');
changeset.rollbackProperty('lastName'); // returns changeset
changeset.execute();
user.get('firstName'); // "Jimmy"
user.get('lastName'); // "Bob"

⬆️ back to top

validate

Validates all, single or multiple fields on the changeset. This will also validate the property on the underlying object, and is a useful method if you require the changeset to validate immediately on render.

Note: This method requires a validation map to be passed in when the changeset is first instantiated.

user.set('lastName', 'B');
user.set('address.zipCode', '123');

let validationMap = {
  lastName: validateLength({ min: 8 }),

  // specify nested keys with pojo's
  address: {
    zipCode: validateLength({ is: 5 }),
  },
};

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

// validate single field; returns Promise
changeset.validate('lastName');
changeset.validate('address.zipCode');
// multiple keys
changeset.validate('lastName', 'address.zipCode');

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

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

⬆️ back to top

addError

Manually add an error to the changeset.

changeset.addError('email', {
  value: '[email protected]',
  validation: 'Email already taken',
});

changeset.addError('address.zip', {
  value: '123',
  validation: 'Must be 5 digits',
});

// shortcut
changeset.addError('email', 'Email already taken');
changeset.addError('address.zip', 'Must be 5 digits');

Adding an error manually does not require any special setup. The error will be cleared if the value for the key is subsequently set to a valid value. Adding an error will overwrite any existing error or change for key.

If using the shortcut method, the value in the changeset will be used as the value for the error.

⬆️ back to top

pushErrors

Manually push errors to the changeset.

changeset.pushErrors('age', 'Too short', 'Not a valid number', 'Must be greater than 18');
changeset.pushErrors('dogYears.age', 'Too short', 'Not a valid number', 'Must be greater than 2.5');

This is compatible with ember-changeset-validations, and allows you to either add a new error with multiple validation messages or push to an existing array of validation messages.

⬆️ back to top

snapshot

Creates a snapshot of the changeset's errors and changes. This can be used to restore the changeset at a later time.

let snapshot = changeset.snapshot(); // snapshot

⬆️ back to top

restore

Restores a snapshot of changes and errors to the changeset. This overrides existing changes and errors.

let user = { name: 'Adam', address: { country: 'United States' } };
let changeset = Changeset(user, validatorFn);

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

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

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

⬆️ back to top

cast

Unlike Ecto.Changeset.cast, cast will take an array of allowed keys and remove unwanted keys off of the changeset.

let allowed = ['name', 'password', 'address.country'];
let changeset = Changeset(user, validatorFn);

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

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

changeset.cast(allowed); // returns changeset
changeset.get('unwantedProp'); // undefined
changeset.get('address.country'); // "United States"
changeset.get('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:

// models/user.js
export const schema = {
  name: attr('string'),
  password: attr('string'),
};

export default Model.extend(schema);
// controllers/foo.js
import { action } from '@ember/object';
import { schema } from '../models/user';
const { keys } = Object;

export default FooController extends Controller {
  // ...

  @action
  save(changeset) {
    return changeset.cast(keys(schema)).save();
  }
}

⬆️ back to top

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.

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

changeset.isValidating(); // true if any async validation is still running
changeset.isValidating('lastName'); // true if lastName validation is async and still running
changeset.isValidating('address.city'); // true if address.city validation is async and still running

changeset.validate().then(() => {
  changeset.isValidating(); // false since validations are complete
});

⬆️ back to top

beforeValidation

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

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

⬆️ back to top

afterValidation

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

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

⬆️ back to top

afterRollback

This event is triggered after a rollback of the changeset. This can be used for some advanced use cases where it is necessary to separately track all changes that are made to the changeset.

changeset.on('afterRollback', () => {
  console.log('changeset has rolled back');
});
changeset.rollback();
// console output: changeset has rolled back

⬆️ back to top

Validation signature

To use with your favorite validation library, you should create a custom validator action to be passed into the changeset:

// application/controller.js
import Controller from '@ember/controller';
import { action } from '@ember/object';

export default class FormController extends Controller {
  @action
  validate({ key, newValue, oldValue, changes, content }) {
    // lookup a validator function on your favorite validation library
    // should return a Boolean
  }
}
{{! application/template.hbs}}
<DummyForm @changeset={{changeset model this.validate}} />

Your action will receive a single POJO containing the key, newValue, oldValue, a one way reference to changes, and the original object content.

Handling Server Errors

When you run changeset.save(), under the hood this executes the changeset, and then runs the save method on your original content object, passing its return value back to you. You are then free to use this result to add additional errors to the changeset via the addError method, if applicable.

For example, if you are using an Ember Data model in your route, saving the changeset will save the model. If the save rejects, Ember Data will add errors to the model for you. To copy the model errors over to your changeset, add a handler like this:

changeset
  .save()
  .then(() => {
    /* ... */
  })
  .catch(() => {
    get(this, 'model.errors').forEach(({ attribute, message }) => {
      changeset.addError(attribute, message);
    });
  });

Detecting Changesets

If you're uncertain whether or not you're dealing with a Changeset, you can use the isChangeset util.

import { isChangeset } from 'validated-changeset';

if (isChangeset(model)) {
  model.execute();
  // other changeset-specific code...
}

Plugins

Tips and Tricks

  • General Input Helper with ember-concurrency
export default Component.extend({
  classNameBindings: ['hasError:validated-input--error'],

  _checkValidity: task(function* (changeset, valuePath, value) {
    yield timeout(150);

    let snapshot = changeset.snapshot();

    // valuePath is the property on the changeset, e.g. firstName
    changeset.set(valuePath, value);

    if (!changeset.get(`error.${valuePath}`)) {
      set(this, 'hasError', false);
    } else {
      // if error, restore changeset so don't show error in template immediately'
      // i.e. wait for onblur action to validate and show error in template
      changeset.restore(snapshot);
    }
  }).restartable(),

  actions: {
    /**
     * @method validateProperty
     * @param {Object} changeset
     * @param {String} valuePath
     * @param {Object} e
     */
    validateProperty(changeset, valuePath, e) {
      changeset.set(valuePath, e.target.value);

      if (changeset.get(`error.${valuePath}`)) {
        set(this, 'hasError', true);
      } else {
        set(this, 'hasError', false);
      }
    },

    /**
     * @method checkValidity
     * @param {Object} changeset
     * @param {Event} e
     */
    checkValidity(changeset, e) {
      get(this, '_checkValidity').perform(changeset, this.valuePath, e.target.value);
    },
  },
});
<input
  type={{type}}
  value={{get model valuePath}}
  {{on "input" (fn this.checkValidity changeset)}}
  {{on "blur" (fn this.validateProperty changeset valuePath)}}
  disabled={{disabled}}
  placeholder={{placeholder}}>

Contributors

We're grateful to these wonderful contributors who've contributed to ember-changeset:

Installation

  • git clone this repository
  • npm install

Running

Running Tests

  • npm test (Runs ember try:testall to test your addon against multiple Ember versions)
  • ember test
  • ember test --serve

Building

  • ember build

For more information on using ember-cli, visit http://ember-cli.com/.

ember-changeset's People

Contributors

andreyfel avatar bartocc avatar bekzod avatar betocantu93 avatar bgentry avatar dustinfarris avatar greenkeeperio-bot avatar homeslicesolutions avatar ivanvanderbyl avatar jeffreybiles avatar jelhan avatar jmbejar avatar jonnedeprez avatar josemarluedke avatar krishandley avatar kslgrd avatar linainu avatar luketheobscure avatar matteodepalo avatar miltonmc avatar mixonic avatar nucleartide avatar oliverlj avatar poteto avatar sergeastapov avatar snewcomer avatar supercoolfrog avatar webark avatar willrax avatar xiphiasuvella avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ember-changeset's Issues

Original/old value of a property of the changeset object

In documentation for change at https://github.com/DockYard/ember-changeset#change it is written:

You can use this property to locate a single change:

{{#if changeset.change.firstName}}
  <p>You changed {{changeset.firstName}} to {{changeset.change.firstName}}!</p>
{{/if}}

But, I seems like {{changeset.change.firstName}} always contains exactly the same value as {{changeset.firstName}}.

I am not surprised to find the new value of firstName in {{changeset.firstName}}. I have no expectations about what to find in {{changeset.change.firstName}}.

Anyway, where do I find the original/old value of a property of the changeset object ?

Question: What is validationMap?

In the method signature for changeset(obj, validateFn = defaultValidatorFn, validationMap = {}), what is the purpose of validationMap and why is it not documented?

value bug when used with ember-cli-materialize

When using with ember-cli-materialize, e.g.:

{{#md-input
    value=changeset.email
    errors=changeset.error.email.validation
}}

The input value is constantly reset to empty. If the errors binding is removed everything works. I dug into both repos and not able to figure out where this is stemming from.

Conditional validations and fields

I have a use case where some form fields are only required if a certain value is selected somewhere else in the form (in this case a checkbox). Upon investigating how validations and changesets work, I can see it's not so trivial to add support for this because not only do you need to not disable the validations, but you also need to ensure that field doesn't get propagated to the model on execute.

Any thoughts on this? Am I missing some not so obvious way to achieve this? Otherwise, I'll have a crack at implementing it soon.

Ember CP Validations + Ember Changeset

Hi there!

I have been working on an addon to integrate ember-cp-validations with ember-changeset. Unfortunately my lack of deep understanding of this library holds me back from releasing it into the wild. I was hoping someone here can do a quick review to make sure Im not doing anything too stupid or something that can be easily broken further down the line. Any help at all is appreciated 😄

➡️ ember-changeset-cp-validations

Building a changeset from 'belongs-to' association (question)

Hi,

I'd like to know if this is posible. I want to create a changeset from a belongs-to association. I have a User model associated to one Location. So, in my template I wrote this:

{{location-form changeset=(changeset model.location)}}

but that doesn't build the changeset properly.

Thanks.

Using ember-changeset with objects containing an array

I want to use ember-changeset for an object containing an array. When I create a changeset based on the following model:

{ "someField": "someValue", "someArray": [ "value1", "value2" ] }

And I do a changeset.set('someArray.0', 'newValue1'), it does not work. Debugging shows that the new value is not registered as a change, but it is applied directly on the original model.

What happens here is that Ember first does a changeset.get('someArray') and then sets the property on the resulting object. On the first get, the unknownProperty handler of ember-changeset returns the original array (as it has not been changed yet). The set is then applied on the original array and not on the changeset.

Can this be fixed? This is probably related to the issues dealing with complex objects #127 and #133.

resolution of changeset.save() isn't very useful

I would expect that when writing
changeset.save().then((result) => { ... })
the result would be the saved underlying entity, but instead it resolves with the changeset itself, which is not very useful, because you already have the changeset in scope.

Support `isValid`, `isDirty`, `isInvalid`, `isPristine`, etc. for individual properties

Something that would be a nice-to-have for Ghost's usage (currently refactoring the validations to use ember-changeset) would be methods (computed properties perhaps?) on the changeset object that allow checking of individual properties for valid/invalid/dirty states.

Based on the current structure this shouldn't be too terribly difficult, but wanted to make sure this is a desired feature before going ahead and creating a PR for it 😄

Error in version 1.1.1

Hello.

This my code:

      const changeset = this.get('changeset');
      const model = this.get('model');
      const validation = this.get('validation');

      return changeset
        .cast(keys(validation))
        .validate()
        .then(() => {
          if (changeset.get('isValid')) {
            return changeset.execute();
          }
        });

I catched an error - You must use Ember.set() to set the_changesproperty.

2016-09-05_15-17-03

This is cast function

2016-09-05_15-25-18

I use:

DEBUG: -------------------------------
DEBUG: Ember             : 2.7.1
DEBUG: Ember Data        : 2.7.0
DEBUG: jQuery            : 2.2.4
DEBUG: Ember Simple Auth : 1.1.0
DEBUG: -------------------------------

Expose errors object

The errors array is useful for iterating through validation messages, but sometimes you want an easy way to find a validation directly. For example,

{{changeset.error.firstName}}

[doc] Best-practise to use with complex objects

I'm using ember-changeset with more complex objects. Let's assume:

this.properties = Ember.A();
this.properties.pushObject({name: 'name', level: 'advanced', type: 'text', title: 'My Name', value: '123'});
this.properties.pushObject({name: 'city', type: 'select', title: 'City', value: 'Brno', options: ['Brno', 'Praha', 'New York']});
this.properties.pushObject({name: 'author', level: 'advanced', type: 'text', title: 'Author', value: null, defaultValue: 'Hugo Hugo'});

what differs from expected

this.properties = { name: 'My Name', city: 'Brno', author: null }

My question is what is the best practise to use more complex objects?

Currently, my solution is to use computed properties to generate expected format from my complex one. It works but I believe there might be a better option.

Snapshot and restore

let snapshot = changeset.snapshot(); // snapshot of changes and errors
localStorage.setItem('user-edit-form', JSON.stringify(snapshot));

// when user returns
changeset.restore(localStorage.getItem('user-edit-form')); // parses if string
  • Could even be used to implement crude undo/redo
  • Needed to be able to rollback

with ember >= 2.7.0 it's impossible to set undefined to an new changesets field

With [email protected] the behavior of Ember.set was changed to not modify the host object if the value is already there.
That means if one does a = {};Ember.set(a, 'foo', undefined);Object.keys(a), a won't have the foo key. ( see emberjs/ember.js#14270 ).

In context of changeset that means that a change that tries to set undefined to the changeset CHANGES, it won't be "tracked" and not set on the CONTENT object on execute().

I'm not sure if this is a problem for changeset. If it is and you think this is a bug/regression in ember, please comment on emberjs/ember.js#14270 .

A possible use-case would be a one-way-select (with promptIsSelectable) that represents a country selector, where blank means all countries.
If the user wants to change an item to all countries, where the country was previously set to a specific country, undefined would be set, which in turn wouldn't be set on the CHANGES object, because previously get(obj, 'country') was already undefined.

property not notified when using a validator

I am getting the bug fixed in #47 when i pass a validator (either as an action or an ember-changeset-validators validation map).

{{#with (changeset model (action "validate") as |changeset|}}
  <h2>{{changeset.email}}</h2>
  <input value={{changeset.email}} oninput={{action (mut changeset.email) value="target.value"}}>
{{/with}}

How to handle validators that return promises

Hi,

Wondering how I can handle validators that return promises.

Trying to create a validator that checks if a name is available by calling some api/name_check service.

https://ember-twiddle.com/d4ec3c55c540d6da68da36ba79250590?fileTreeShown=false&openFiles=validations.user.js%2C

If you type anything in firstName you will notice [object Object] instead of a passing validation.

I noticed there was work done on handling validators that return promises, but I can't see how it actually works.

Thank you

Prepare gives an error

Trying to use prepare, I get an error: vendor.js:29215 Uncaught Error: Assertion Failed: You must use Ember.set() to set the _changes property (of changeset:<project-name@model:load::ember522:1>) to [object Object].

My code below:

var changeset = this.get('changeset');
changeset.prepare((changes) => {
    let modified = JSON.parse(JSON.stringify(changes));
    modified.signature = this.get('currentUser.user.fullName');
    modified.timeIn = moment.utc(modified.date + ' ' + modified.timeIn, 'MMMM DD, YYYY h:mm a').valueOf();
    modified.timeOut = moment.utc(modified.date + ' ' + modified.timeOut, 'MMMM DD, YYYY h:mm a').valueOf();

     return modified;
});

Ember Data models remain dirty after a server error

Hi there!

Love this addon. Actually I love all of your addons, @poteto. Thank you for always providing such elegant solutions to common problems!

One of the main reasons I am using ember-changeset now is to handle rolling back ember data relationships. Currently an ember data model can rollback attributes but not relationships, so this is a nice solution.

However, I have a use case with ember data that I am finding challenging.

  1. Create a changeset based on a DS.Model.
  2. Make some changes to the changeset that will result in a server error, then call save().
  3. When it fails, the underlying model remains in a dirty state.
  4. If the user were to leave the page without officially canceling changes (e.g. by clicking a Cancel button), the model will stay dirty.

The only solution I see today is to reset the changeset on Route.deactivate, Controller.resetController, or Component.willDestroyElement, but that is quite far from the source of the issue. Is there a better solution? How do others handle this today?

I believe an elegant solution would be to have a changeset.reverse() method, which is basically the opposite of execute(). Just as execute() applies the changes to the underlying model without clearing the list of changes, reverse() would reverse the changes to the underlying model without clearing the list of changes. This way, a save action may look something like this:

changeset.save()
  .then(() => { /* ... */ })
  .catch(() => {
    get(this, 'model.errors').forEach(({ attribute, message }) => {
      changeset.addError(attribute, { validation: message });
    });
    changeset.reverse();
  });

This way, the user would not have lost their changes, and the underlying model would be made pristine again. The changeset can be safely discarded without needing to perform a final rollback.

(Ultimately I would love to see a more DDAU approach to Ember Data, where the models are read only from the server (data down), you create a changeset and submit it to the server (actions up), and a successful result will update the model from the returning payload (data down). Let me know if you'd be interested in collaborating on that! 😄 )

addError overwrites existing error for key

Is this intended behavior? If so we should mention in the docs.

Also, as an aside, addError is given an array for validation message from ember-changeset-validations, but is given a string in the docs and test. Is this something we care about and/or will care about in future development?

Skip validation on set

Hi,
It's more if a question rather that the issue but I would like to know if it's (or will be) possible to skip the validation on property set?

If we hook ember-changest with form-for add-on we will get instant feedback on input change. However sometimes there is a case where you want to wait with validation and trigger it on submit. Is this currently doable?

Async validator question

Hi,

Is there a way to check if a changeset has changed since it was last validated?

Ideally setting any prop: changeset.set('prop', 1) will turn changeset.get('isValid') false, until all validators (async here is the issue) resolve.

This would allow us to disable UI until we know for certain that everything is resolved and valid.

Currently, if there is a slow async validator, the changeset will be in isValid true, but in reality, it is still unresolved, and therefor, the user may proceed to submit and then the world falls apart.

Let me know what you think.

Thanks

Validation errors from another changeset

Have you met someone with a problem when using multiple forms? I have a signup and login page and switch between them using the {{link-to}}, if one form generate validation errors (signup-form) and I switch to another (login-form I get validations errors from the previous form after changeset.validation() is executed.

login.hbs

    {{#with (changeset model LoginValidations) as |changeset|}}
      <form {{action 'submit' changeset on='submit'}} >
            {{input id="email" type="email" value=changeset.email }}
            {{input id="password" type="password"  value=changeset.password }}
            <button type="submit" >Log in</button>
      </form>
    {{/with}}
    Not a member? {{#link-to 'signup'}}Sign up{{/link-to}}

signup.hbs:


{{#with (changeset model SignupValidations) as |changeset|}}
    <form {{action 'submit' changeset on='submit'}}>
          {{input id="first_name" type="text" value=changeset.first_name}}
          {{input id="last_name" type="text" value=changeset.last_name }}
          {{input id="email" type="email" value=changeset.email}}
          {{input id="password" type="password"  value=changeset.password}}
          {{input id="password_again" type="password" value=changeset.password_again}}
        <button type="submit"}}>Sign up</button>
    </form>
  {{/with}}
   Already have an account? {{#link-to 'login'}}Log in{{/link-to}}

route for login:

model(){
    return Ember.Object.create({
      email: null,
      password: null
    });
  }

route for signup:

model(){
    return Ember.Object.create({
      first_name: null,
      last_name: null,
      email: null,
      password: null,
      password_again: null
    });
  }

example of "submit" action for login (signup is similar):

submit(changeset) {
      return changeset.validate().then(
      () => {
        if (get(changeset, 'isInvalid')) {
          console.log(changeset.get('errors') --> I get errors from signup changeset, how it is possible?
          return;
        }
    },

addError should set errors as an array

When calling validate with multiple validations on a property, changeset.error.propertName.validations is an array of messages. However, if you call changeset.addError('propertyName', 'message'), then changeset.error.propertName.validations will be a string.

This is inconsistent and makes it unclear how to handle validation messages for a property in the UI.

Restoring original text does not clear presence errors

I'm Chris from zipcar, this is in reference to the comments made on ZC-416.

In when using a form-for textarea bound to a changeset object, if you completely cut the text out, a presence error appears as expected. However, pasting that same text back in does not clear the error. Any other text can be pasted in as long as it is not the original text.

Rollback should rollback all changes, even if invalid

If a cset value is changed from a valid value to an invalid one an error is set within addError but this is not considered a CHANGE. Running rollback() on a cset should revert all changes, even the invalid ones.

Do not register a change when current "model.value" equals current "changeset.value"

I am totally in love with ember-changeset! Bravo!

I have this finding:

Given an empty changeset field in a template, e.g. {input type="text" value=changeset.familyName}}.

Via controller.set('changeset', new Changeset(model)) this changeset is bound to a newly created person record.

Problem: ember-changeset registers a change after entering this empty field and leaving the field untouched.

Enhancement request: Do not do that!

This request probably could be replaced by a more generic enhancement request: Do not make or keep a change registration (in changeset.changes), when current "model.value" equals current "changeset.value".

isValid on init

Hi,

Love this addon, thanks again for putting it together.

Question regarding isValid when you first create the changeset. Is there a way to have the changeset run it's validate function when it is first created?

I would like to be able to create a changeset and be able to test the isValid prop right away in the template.

Notice in this twiddle even though the model is invalid, it shows as valid on init: Twiddle

Thanks

Rolling back hasMany associations

Hi there, thanks for this awesome library.

I may have found a bug with tracking changes to hasMany associations. For example, if I have a Foo model with a users hasMany association, and I update the users prop, there is no change logged in changeset.changes(). In my use case, I noticed this while trying to rollback changes on a Foo model.

Ember Twiddle reproducing the issue: https://ember-twiddle.com/3bb755fcdf978eadfaa498acb5e777ca?openFiles=templates.application.hbs%2C

Please let me know if you need more info or if I'm doing something dumb. Thanks!

How to handle lists of things?

Let's say you have a model A that hasMany some other model B, and your form has a dynamic list of B that are editable all at once. How would you wire that up using ember-changeset? It seems like you'd want to have a changeset per B record as well and have the validation on the list of B be the aggregate of the child validation state, but… that sounds like lots of manual wiring, if it's even possible at all.

Validation of associated models (Question)

Hi,

I am using "ember-changeset" along with "ember-changeset-validation" for client side validation.
Going through the docs, I couldn't find any information regarding how changeset works with ember model association (belongsTo, hasMany).

Currently, I require validation of objects having hasMany relation to my model, Am I supposed to create changeset for every objects in the hasMany association ? This could be a tedious process when the objects are being added/removed dynamically.

Would really like to know the opinion of "ember-changset" regarding this case.

Thanks

Not returning the full promise from Changeset#save causes unhandled rejections when save fails

Spent a while debugging my API failure-case tests today (returning a 500 using mirage) as they were getting unhandled rejections, it eventually seems to be caused by save chaining a then call internally but returning the 'pre-then' promise. This might be due to how RSVP works, or could be something else, but modifying save to return the 'full' promise like:

return savePromise.then((result) => {
  this.rollback();
  return result;
});

Stops the uncaught rejections and gets the tests working again.

It looks like someone else has already had this issue and put in a PR to fix it (#135), wanted to put up a corresponding issue as supporting info for the PR. 👍

Return a new object after applying a changeset

A handy optional feature would be the ability to apply a changeset and have it return a new object, with the changes, (assuming they're valid). This way, we'd have the option to proxy over to the original model, plus the ability to apply changesets immutably and curry the result of changeset executions with other actions/functions.

If you want to point me in the right direction, I'd be happy to work on this myself and open a PR.

@poteto

Changeset is always pristine when invalid

While working on my project I noticed that isPristine returns true when isValid is false. This is definitely not expected. Is there any story behind this? Here is full, working example.

Add ability to save changes to changeset regardless of validation status, and then validate on changeset.execute()

So after building out all of Ghost's new validation functionality using this, I ran into an issue with this library that forced me to re-write a private method (which is bad practice 😁 ). Essentially, I found out that my changeset didn't contain the most recent changes, because only valid changes were actually persisted to the changeset.

This isn't a bug, because it says in the readme:

The idea behind a changeset is simple: it represents a set of valid changes to be applied onto any Object

Here's the specific use-case for why this broke my refactor: the validation flow in Ghost works by validating when the particular input is de-focused, and then all inputs are validated when the form is submitted. After all changes are validated (using the changeset.validate) method, then the changes are persisted to the underlying model.

The problem arises because if the user de-focuses an input without the content of said input being valid, the actual input value doesn't exist in the changeset, resulting in an incorrect error message displayed (e.g. the please enter a value error message other than the invalid format for value message).

After reading through the Ecto Changeset documentation, it seems that the changes are set on the changeset regardless of validation status (it differentiates "casting" from "validation"). I could be reading this wrong though.

I understand that not everyone would necessarily want to use ember-changeset this way, but I think there could be an ability to do both at the same time, based on how the changeset is created initially (maybe a third parameter passed to the changeset constructor that enables/disables this new method of behavior?)

In terms of code changes, it's pretty minimal.

Thoughts?

/cc @poteto

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.