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

fix(date)!: Restore 'format', 'precison', and usage of moment for the date validator #726

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
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
107 changes: 102 additions & 5 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,106 @@

This document is here to show breaking changes when upgrading from v3.x to v4.x.

## Support Latest 2 LTS Releases
## Ember Source Compatibility

As Ember is evolving, we have to be able to keep up. v3.x supported Ember versions as old as 1.11 which not only made this addon difficult to maintain, but also added a lot of bloat. Versions 4 and 5 of this addon will maintain compatibility with Ember v3.28.
As Ember adopts a [scheduled major version bump](https://blog.emberjs.com/evolving-embers-major-version-process), this addon will keep pace and target the last LTS of the previous major version.

| Ember Source | Supported Version |
|:---:|:---:|
|4.x|3.28|
|5.x|4.12|

## Date Validation

The [ember-validators][ev] package provides many of the built-in validators found within this addon. In ember-validators v3, the `'date'` validator was [significantly refactored](https://github.com/offirgolan/ember-validators/pull/100) to remove the dependency on `moment`, and instead use the native `Date` and `Intl.DateTimeFormat` APIs now available in all modern browsers. If you used the `'date'` validator with either the `'format'` or `'precision'` options, or made use of `'now'` as a relationship, you will need to make changes to upgrade.

### Replacing `'now'`

The usage of `'now'` as a relationship reference can be replaced with a native getter:

**Before (3.x, 4.0.0-beta < 13)**
```js
options: {
before: 'now'
}
```

**After (4.x)**

```js
options: {
get before() {
return new Date();
}
}
```

### Validating DateString Input Format

The usage and semantics of the `'format'` option has changed due to the upstream ember-validators addon. and is now expected to be an object that satisfies the [`Intl.DateTimeFormat` API.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options) **If the `'format'` option is provided, all relationship comparisons are performed between strings *in this format.***

It is no longer possible to validate that any string values given to the `'date'` validator are in a particular format. Those who need to perform this check will need to add an additional validator to the field that performs solely this check.

**Before (v3, 4.0.0-beta < 13)**
```js
// TODO: make full validator definition
options: {
format: 'MM-DD-YYYY HH:mm'
}
```

**After (4.x)**
```js
// TODO
```

### Comparison Granularity (`'precision'`)

The date validator no longer accepts a `'precision'` argument. If you need to ignore pieces of a datetime when comparing relationships, you will generally need to use the `'format'` option to convert the validated value and relationships into the appropriate strings.

**Before (3.x, 4.0.0-beta < 13)**
```js
options: {
precision: 'seconds'
}
```

**After (4.x)**
```js
options: {
format: {
hour12: false,
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
}
}
```

It is possible that, if your validation was to compare against `'now'` at a specific precision level, that you can remove the `'precision'` option entirely through the use of a rounding getter:

```js
// 3.x, 4.0.0-beta < 13:
options: {
after: 'now',
precision: 'minute',
}

// 4.x
options: {
get after() {
const now = new Date();
// Round up, so that the value must be different at the 'minutes' level to pass validation.
now.setSeconds(59);
now.setMilliseconds(999);
return now;
}
}
```

As Ember is evolving, we have to be able to keep up. v3.x supported Ember versions as old
as 1.11 which not only made this addon difficult to maintain, but also added a
lot of bloat. Going forward, this addon will target and test against only the 2
latest LTS releases.

## Inline Validator

Expand Down Expand Up @@ -40,3 +134,6 @@ validator('inline', {
}
});
```


[ev]: https://github.com/rwwagner90/ember-validators/
133 changes: 133 additions & 0 deletions addon/validators/date-moment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { isEmpty, isNone } from '@ember/utils';
import moment from 'moment';
import EmberValidator from 'ember-cp-validations/-private/ember-validator';

/**
* @typedef {'now'|string|null|moment.Moment|Date} ReferenceDate
*/

/**
* @typedef {object} DateValidatorOptions
* @property {boolean} [allowNone] If true, skips validation if the value is "none", according to Ember.
* @property {boolean} [allowBlank] If true, skips validation if the value is "empty," according to Ember.
* @property {string} [format] If provided, validates that a string value is in this format.
* @property {ReferenceDate} [before] The value must be before this date.
* @property {ReferenceDate} [onOrBefore] The value must be on or before this date.
* @property {ReferenceDate} [after] The value must be after this date.
* @property {ReferenceDate} [onOrAfter] The value must be on or after this date.
* @property {'year'|'month'|'week'|'day'|'hour'|'minute'|'second'|'millisecond'} [precision] The granularity of the comparison between a reference date and the value. If the difference is less than the precision, the values are considered equivalent.
* @property {string} [errorFormat] The format used to display the reference date in validation errors. Defaults to `MMM Do, YYYY`.
*/

/**
* <i class="fa fa-hand-o-right" aria-hidden="true"></i> [See All Options](#method_validate)
*
* Validate over a date range. Uses [MomentJS](http://momentjs.com/) for date mathematics and calculations.
*
* **Note**: MomentJS must be installed to be able to use this validator. The easiest way to do this is to install [ember-moment](https://github.com/stefanpenner/ember-moment)
*
* ## Examples
*
* If `before`, `onOrBefore`, `after`, or `onOrAfter` is set to **now**, the value given to the validator will be tested against the current date and time.
*
* ```javascript
* validator('date', {
* after: 'now',
* before: '1/1/2020',
* precision: 'day',
* format: 'M/D/YYY',
* errorFormat: 'M/D/YYY'
* })
* ```
*
* @class Date
* @module Validators
* @extends Base
*/
export default class MomentValidator extends EmberValidator {
_evType = 'date';

/**
* A reimplementation of the v2 Date validator from ember-validators, since v3 removed support for most
* desired validation errors.
* @param {any} value A string, null, or moment object
* @param {DateValidatorOptions} options
* @returns {true|string} true if passed, otherwise the error message
*/
validate(value, options) {
if (options.allowNone && isNone(value)) return true;
if (options.allowBlank && isEmpty(value)) return true;

const hasFormat = Boolean(options.format);

// Parse the value. If a format was given, the input must be in that format.
const parsed = hasFormat
? moment(value, options.format, true)
: moment(value);
if (!parsed.isValid()) {
// If a format was given, reparse to determine whether we received something that represented a date-ish.
// If so, then raise a 'wrongFormatError'. Otherwise, raise a date error.
if (
hasFormat &&
(moment(value, options.format).isValid() || moment(value).isValid())
) {
return this.createErrorMessage('wrongDateFormat', value, options);
}
return this.createErrorMessage('date', value, options);
}

// Validate all provided relational requirements are satisfied. If the
// reference value is not currently valid, the validation will fail.
const errorFormat = options.errorFormat || 'MMM Do, YYYY';
const { precision } = options;
const getRelation = (key) => {
if (options[key] === 'now') return moment.utc();
return moment(options[key], options.format, hasFormat);
};
if (typeof options.before !== 'undefined') {
const reference = getRelation('before');

if (!parsed.isBefore(reference, precision)) {
return this.createErrorMessage('before', parsed, {
...options,
before: reference.format(errorFormat),
});
}
}

if (typeof options.onOrBefore !== 'undefined') {
const reference = getRelation('onOrBefore');

if (!parsed.isSameOrBefore(reference, precision)) {
return this.createErrorMessage('onOrBefore', parsed, {
...options,
onOrBefore: reference.format(errorFormat),
});
}
}

if (typeof options.after !== 'undefined') {
const reference = getRelation('after');

if (!parsed.isAfter(reference, precision)) {
return this.createErrorMessage('after', parsed, {
...options,
after: reference.format(errorFormat),
});
}
}

if (typeof options.onOrAfter !== 'undefined') {
const reference = getRelation('onOrAfter');

if (!parsed.isSameOrAfter(reference, precision)) {
return this.createErrorMessage('onOrAfter', parsed, {
...options,
onOrAfter: reference.format(errorFormat),
});
}
}

return true;
}
}
43 changes: 31 additions & 12 deletions addon/validators/date.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
import EmberValidator from 'ember-cp-validations/-private/ember-validator';
import { assert } from '@ember/debug';

/**
* <i class="fa fa-hand-o-right" aria-hidden="true"></i> [See All Options](#method_validate)
*
* Validate over a date range. Uses [MomentJS](http://momentjs.com/) for date mathematics and calculations.
*
* **Note**: MomentJS must be installed to be able to use this validator. The easiest way to do this is to install [ember-moment](https://github.com/stefanpenner/ember-moment)
* Validate over a date range. Uses the native Date and Intl APIs. Will parse strings using `new Date(<string>)`,
* so care must be taken to ensure you use only ISO 8601 strings if you want cross-browser compatibility.
*
* ## Examples
*
* If `before`, `onOrBefore`, `after`, or `onOrAfter` is set to **now**, the value given to the validator will be tested against the current date and time.
* To set `before`, `onOrBefore`, `after`, or `onOrAfter` to the current instant, use a JS getter:
*
* ```javascript
* validator('date', {
* after: 'now',
* before: '1/1/2020',
* precision: 'day',
* format: 'M/D/YYY',
* errorFormat: 'M/D/YYY'
* get after(): { return Date.now(); },
* before: '2020/01/01T00:00:00.0000Z', // Must be ISO or Date!
* })
* ```
*
* @class Date
* @module Validators
* @extends Base
*/
export default EmberValidator.extend({
_evType: 'date',
});
export default class DateValidator extends EmberValidator {
_evType = 'date';

validate(value, options) {
// Help applications recognize no longer supported configurations for the default date validator:
assert(
"the default 'date' validator no longer accepts a string 'format' option",
typeof options.format !== 'string'
);
assert(
"the default 'date' validator does not support a 'precision' option",
!options.precision
);
assert(
"the default 'date' validator does not accept the string 'now'; use a getter instead",
options.before !== 'now' &&
options.onOrBefore !== 'now' &&
options.after !== 'now' &&
options.onOrAfter !== 'now'
);

// Use the date validator supplied by ember-validators:
return super.validate(...arguments);
}
}
4 changes: 3 additions & 1 deletion addon/validators/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ import Messages from 'ember-validators/messages';
* @class Messages
* @module Validators
*/
export default EmberObject.extend(Messages);
export default EmberObject.extend(Messages, {
wrongDateFormat: '{description} must be in the format of {format}',
});
1 change: 1 addition & 0 deletions app/validators/date-moment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-cp-validations/validators/date-moment';
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
"ember-cli-github-pages": "^0.2.0",
"ember-cli-htmlbars": "^6.0.1",
"ember-cli-inject-live-reload": "^2.1.0",
"ember-cli-moment-shim": "^3.3.3",
"ember-cli-sass": "^8.0.1",
"ember-cli-sri": "^2.1.1",
"ember-cli-terser": "^4.0.2",
Expand All @@ -95,6 +94,8 @@
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-qunit": "^7.2.0",
"loader.js": "^4.7.0",
"luxon": "^3.0.4",
"moment": "^2.29.4",
"npm-run-all": "^4.1.5",
"prettier": "^2.5.1",
"qunit": "^2.17.2",
Expand All @@ -105,6 +106,10 @@
"webpack": "^5.65.0",
"yuidoc-ember-theme": "^2.0.1"
},
"peerDependencies": {
"luxon": "^2 || ^3",
"moment": "^2"
},
"engines": {
"node": "12.* || 14.* || >= 16"
},
Expand Down
Loading