Skip to content
New issue

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

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

Already on GitHub? # to your account

Validations with dependentKeys do not work with change-set changes. #25

Open
arenoir opened this issue Mar 29, 2017 · 2 comments
Open

Comments

@arenoir
Copy link

arenoir commented Mar 29, 2017

IMO the best feature of the cp-validations addon is the ability to conditionally validate attributes based on other attribute values. This feature is lost when the validate function is built. I am thinking a separate Object needs to be created with the validations and all changes applied to it within the validate function.

Something like:

export default function createValidatableChangeset(model, validationsMixin) {
    let validatableChangeset = Ember.Object.extend(validationsMixin).create();
    let validationMap = validatableChangeset.get('validations.validatableAttributes').reduce((o, attr) => {
      o[attr] = true;
      return o;
    }, {});

    function validateFn({ key, newValue, oldValue, changes, content }) {
      validatableChangeset.setProperties(changes);
      return validatableChangeset.validateAttribute(key, newValue).then(({ validations }) => {
        return validations.get('isValid') ? true : validations.get('message');
      });
   }

    return new Changeset(model, validateFn, validationMap);
}

@bgentry
Copy link

bgentry commented Mar 24, 2018

I encountered this same need tonight. Following on @arenoir's idea, I was able to come up with something that works (with a minor change to ember-changeset).

I made a ValidationProxy class that I extend with my validation mixin from buildValidations:

// ValidationProxy acts as a proxy between a model and ember-changeset-cs-validations.
const ValidationProxy = EmberObject.extend({
  _content: null,
  _changes: null,

  init() {
    this._super(...arguments);
    this._changes = {};
  },

  unknownProperty(key) {
    if (this._changes.hasOwnProperty(key)) {
      return this._changes[key];
    }
    return get(this, `_content.${key}`);
  },

  setUnknownProperty(key, value) {
    if (key === "_content") {
      this._content = value;
      return;
    }
    this._changes[key] = value;
    this.notifyPropertyChange(key);
  },

  _performRollback() {
    let keysToReset = Object.keys(this._changes);
    this._changes = {};
    keysToReset.forEach(key => this.notifyPropertyChange(key));
  },
});

This proxy is like a mini changeset of its own. It proxies property get calls to its own internal change tracking, or to the underlying model. When a set is made, it tracks the change. Finally, when _performRollback() is called, it resets all tracked changes.

So I might have a validation class like this:

export const ProductValidations = buildValidations({
  name: validator("presence", { presence: true, description: "Name" }),
});

const ProductValidation = ValidationProxy.extend(ProductValidations);
export default ProductValidation;

In order to make this work, I've had to make an alternate changeset creation method which takes two arguments: the model, and an instance of the validations proxy (that's already set up with a reference to the model):

export function createValidatedChangeset(model, validation) {
  let validationMap = validation
    .get("validations.validatableAttributes")
    .reduce((o, attr) => {
      o[attr] = true;
      return o;
    }, {});

  let validateFn = function({ key, newValue }) {
    // set the property immediately on the proxy so it is available to other validations:
    validation.set(key, newValue);
    return validation
      .validateAttribute(key, newValue)
      .then(({ validations }) => {
        return validations.get("isValid") ? true : validations.get("message");
      });
  };
  let cs = new Changeset(model, validateFn, validationMap);
  cs.on("afterRollback", () => validation._performRollback());
  return cs;
}

Now I can set this up with:

    product = {}; // my model object
    // validation instance, created with an owner inside tests:
    let validation = ProductValidation.create(this.owner.ownerInjection(), {
      _content: product,
    });

    // or, inside my form component:
    let validation = getOwner(this)
      .factoryFor(`validation:${validationName}`)
      .create({ _content: model });

    changeset = createValidatedChangeset(product, validation);

Now, I can get the best of both worlds: the full benefits of ember-changeset (especially the ability to validate properties dependent on the proposed/chantged value of other properties), and also all the niceties of ember-cp-validations: full access to the object model, including services.

I'm not sure if there's a super straightforward way to make this into a good general purpose PR. In the mean time I've posted it here to help others.

@ryanto
Copy link

ryanto commented Feb 12, 2019

Thanks @bgentry! This will help get me unstuck.

I know this issue is a little old, are you still using this approach today?

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants