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

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?

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

No branches or pull requests

3 participants