diff --git a/README.md b/README.md
index d4d7b07a..a2f2aa15 100644
--- a/README.md
+++ b/README.md
@@ -161,6 +161,26 @@ Then include the entry in your template:
Adjust the import path to your application structure.
+### JavaScript Namespace
+
+The library registers its constraints and its view transformers in a single
+global object named after the `Svaroh\JsFormValidatorBundle` PHP namespace:
+
+```js
+Svaroh.constraints // constraints, keyed by class name without the separators
+Svaroh.transformers // view transformers, keyed the same way
+```
+
+Register your own classes there, as shown in
+[custom constraints](src/Resources/doc/3_7.md) and
+[custom data transformers](src/Resources/doc/3_8.md). The object is created by
+the library, so run your registrations after its script is loaded.
+
+Every bundled constraint and transformer is still exported under its own global
+name, such as `window.SymfonyComponentValidatorConstraintsNotBlank`, and a class
+your application defines as a global is still picked up. Both are deprecated and
+will be dropped in a future release.
+
### Render Bundle Config And Form Models
After the scripts are loaded, render the generated config and queued form models:
diff --git a/src/Resources/doc/3_7.md b/src/Resources/doc/3_7.md
index b6bf405f..32b6c4e9 100644
--- a/src/Resources/doc/3_7.md
+++ b/src/Resources/doc/3_7.md
@@ -1,8 +1,8 @@
### 3.7 Custom constraints
If you have your own Symfony constraint, implement the matching constraint on
-the JavaScript side too. The JavaScript constructor name should match the full
-PHP class name with namespace separators removed.
+the JavaScript side too. Register it in `Svaroh.constraints` under the full PHP
+class name with the namespace separators removed.
For example, a PHP constraint:
@@ -51,7 +51,7 @@ requires this JavaScript constraint:
```html
+```
+
+`Svaroh` is the namespace of the library, as described in the README.
+Constraints declared as a global function of the same name still work:
+
+```html
+
```
+
+This form is deprecated and support for it will be dropped in a future release,
+so prefer the namespace for new code. Note that a global cannot replace a
+constraint the bundle ships: the namespace is looked up first.
diff --git a/src/Resources/doc/3_8.md b/src/Resources/doc/3_8.md
index ae5ff32d..7701e832 100644
--- a/src/Resources/doc/3_8.md
+++ b/src/Resources/doc/3_8.md
@@ -4,11 +4,12 @@ If you use a custom view transformer, implement the matching transformer on the
JavaScript side so validation receives the same normalized value.
For a PHP view transformer such as `App\Form\DataTransformer\MyTransformer`,
-create a JavaScript constructor with the namespace separators removed:
+register a JavaScript constructor in `Svaroh.transformers` under the class name
+with the namespace separators removed:
```html
```
+
+`Svaroh` is the namespace of the library, as described in the README.
+Transformers declared as a global function of the same name still work, but that
+form is deprecated and support for it will be dropped in a future release.
diff --git a/src/Resources/public/js/SvarohJsFormValidator.js b/src/Resources/public/js/SvarohJsFormValidator.js
index 5dc6edc2..8e4adc08 100644
--- a/src/Resources/public/js/SvarohJsFormValidator.js
+++ b/src/Resources/public/js/SvarohJsFormValidator.js
@@ -1,3 +1,4 @@
+import Svaroh from './namespace.js';
import './constraints';
import './transformers';
@@ -1278,6 +1279,29 @@ var SvarohJsFormValidator = new function () {
return value;
};
+ /**
+ * Resolves the constraint or the transformer the PHP side refers to by its
+ * class name with the namespace separators removed.
+ *
+ * The classes shipped by the bundle are registered in the Svaroh namespace.
+ * The ones an application defines for its own constraints and transformers
+ * are looked up in the global scope as before, see doc/3_7.md and
+ * doc/3_8.md, so that a global keeps working when the namespace holds no
+ * class of that name.
+ *
+ * @param {Object} registry
+ * @param {String} className
+ *
+ * @returns {Function|undefined}
+ */
+ this.resolveClass = function (registry, className) {
+ if (Object.prototype.hasOwnProperty.call(registry, className)) {
+ return registry[className];
+ }
+
+ return window[className];
+ };
+
/**
* @param {Object} list
*/
@@ -1285,10 +1309,11 @@ var SvarohJsFormValidator = new function () {
var constraints = [];
for (var name in list) {
var className = name.replace(/\\/g, '');
- if (undefined !== window[className]) {
+ var constraintClass = this.resolveClass(Svaroh.constraints, className);
+ if (undefined !== constraintClass) {
var i = list[name].length;
while (i--) {
- var constraint = new window[className]();
+ var constraint = new constraintClass();
for (var param in list[name][i]) {
constraint[param] = list[name][i][param];
}
@@ -1314,8 +1339,9 @@ var SvarohJsFormValidator = new function () {
var i = list.length;
while (i--) {
var className = String(list[i]['name']).replace(/\\/g, '');
- if (undefined !== window[className]) {
- var transformer = new window[className]();
+ var transformerClass = this.resolveClass(Svaroh.transformers, className);
+ if (undefined !== transformerClass) {
+ var transformer = new transformerClass();
for (var propName in list[i]) {
transformer[propName] = list[i][propName];
}
diff --git a/src/Resources/public/js/constraints/globals.test.js b/src/Resources/public/js/constraints/globals.test.js
index d6c61dab..e6e26580 100644
--- a/src/Resources/public/js/constraints/globals.test.js
+++ b/src/Resources/public/js/constraints/globals.test.js
@@ -37,11 +37,22 @@ const constraintGlobals = [
'SymfonyComponentValidatorConstraintsValid',
];
-// The validator instantiates constraints as window[className], so a constraint
-// that is not exported to the global scope is silently skipped at runtime.
+// The validator instantiates constraints as Svaroh.constraints[className], so a
+// constraint that is not registered in the namespace is silently skipped at
+// runtime.
test.each(constraintGlobals)(
- 'window.%s is registered',
+ 'Svaroh.constraints.%s is registered',
+ (name) => {
+ expect(typeof window.Svaroh.constraints[name]).toBe('function');
+ },
+);
+
+// The global name of every constraint is kept as a deprecated alias: user code
+// may define custom constraints and Callback handlers against it.
+test.each(constraintGlobals)(
+ 'window.%s is a deprecated alias of the namespaced constraint',
(name) => {
expect(typeof window[name]).toBe('function');
+ expect(window[name]).toBe(window.Svaroh.constraints[name]);
},
);
diff --git a/src/Resources/public/js/constraints/index.js b/src/Resources/public/js/constraints/index.js
index 4584b987..a9d2ba58 100644
--- a/src/Resources/public/js/constraints/index.js
+++ b/src/Resources/public/js/constraints/index.js
@@ -1,31 +1,76 @@
-import './Blank.js';
-import './Callback.js';
-import './Choice.js';
-import './Count.js';
-import './Date.js';
-import './DateTime.js';
-import './Email.js';
-import './EqualTo.js';
-import './File.js';
-import './GreaterThan.js';
-import './GreaterThanOrEqual.js';
-import './IdenticalTo.js';
-import './Ip.js';
-import './IsFalse.js';
-import './IsNull.js';
-import './IsTrue.js';
-import './Length.js';
-import './LessThan.js';
-import './LessThanOrEqual.js';
-import './Luhn.js';
-import './NotBlank.js';
-import './NotEqualTo.js';
-import './NotIdenticalTo.js';
-import './NotNull.js';
-import './Range.js';
-import './Regex.js';
-import './Time.js';
-import './Type.js';
-import './UniqueEntity.js';
-import './Url.js';
-import './Valid.js';
+import Svaroh from '../namespace.js';
+
+import SymfonyComponentValidatorConstraintsBlank from './Blank.js';
+import SymfonyComponentValidatorConstraintsCallback from './Callback.js';
+import SymfonyComponentValidatorConstraintsChoice from './Choice.js';
+import SymfonyComponentValidatorConstraintsCount from './Count.js';
+import SymfonyComponentValidatorConstraintsDate from './Date.js';
+import SymfonyComponentValidatorConstraintsDateTime from './DateTime.js';
+import SymfonyComponentValidatorConstraintsEmail from './Email.js';
+import SymfonyComponentValidatorConstraintsEqualTo from './EqualTo.js';
+import SymfonyComponentValidatorConstraintsFile from './File.js';
+import SymfonyComponentValidatorConstraintsGreaterThan from './GreaterThan.js';
+import SymfonyComponentValidatorConstraintsGreaterThanOrEqual from './GreaterThanOrEqual.js';
+import SymfonyComponentValidatorConstraintsIdenticalTo from './IdenticalTo.js';
+import SymfonyComponentValidatorConstraintsIp from './Ip.js';
+import SymfonyComponentValidatorConstraintsIsFalse from './IsFalse.js';
+import SymfonyComponentValidatorConstraintsIsNull from './IsNull.js';
+import SymfonyComponentValidatorConstraintsIsTrue from './IsTrue.js';
+import SymfonyComponentValidatorConstraintsLength from './Length.js';
+import SymfonyComponentValidatorConstraintsLessThan from './LessThan.js';
+import SymfonyComponentValidatorConstraintsLessThanOrEqual from './LessThanOrEqual.js';
+import SymfonyComponentValidatorConstraintsLuhn from './Luhn.js';
+import SymfonyComponentValidatorConstraintsNotBlank from './NotBlank.js';
+import SymfonyComponentValidatorConstraintsNotEqualTo from './NotEqualTo.js';
+import SymfonyComponentValidatorConstraintsNotIdenticalTo from './NotIdenticalTo.js';
+import SymfonyComponentValidatorConstraintsNotNull from './NotNull.js';
+import SymfonyComponentValidatorConstraintsRange from './Range.js';
+import SymfonyComponentValidatorConstraintsRegex from './Regex.js';
+import SymfonyComponentValidatorConstraintsTime from './Time.js';
+import SymfonyComponentValidatorConstraintsType from './Type.js';
+import SvarohJsFormValidatorBundleFormConstraintUniqueEntity from './UniqueEntity.js';
+import SymfonyComponentValidatorConstraintsUrl from './Url.js';
+import SymfonyComponentValidatorConstraintsValid from './Valid.js';
+
+// The constraints are keyed by the name the PHP side serialises them under:
+// their class name with the namespace separators removed. Every module also
+// still assigns that name on "window" as a deprecated alias.
+Object.assign(Svaroh.constraints, {
+ SymfonyComponentValidatorConstraintsBlank,
+ SymfonyComponentValidatorConstraintsCallback,
+ SymfonyComponentValidatorConstraintsChoice,
+ SymfonyComponentValidatorConstraintsCount,
+ SymfonyComponentValidatorConstraintsDate,
+ SymfonyComponentValidatorConstraintsDateTime,
+ SymfonyComponentValidatorConstraintsEmail,
+ SymfonyComponentValidatorConstraintsEqualTo,
+ SymfonyComponentValidatorConstraintsFile,
+ SymfonyComponentValidatorConstraintsGreaterThan,
+ SymfonyComponentValidatorConstraintsGreaterThanOrEqual,
+ SymfonyComponentValidatorConstraintsIdenticalTo,
+ SymfonyComponentValidatorConstraintsIp,
+ SymfonyComponentValidatorConstraintsIsFalse,
+ SymfonyComponentValidatorConstraintsIsNull,
+ SymfonyComponentValidatorConstraintsIsTrue,
+ SymfonyComponentValidatorConstraintsLength,
+ SymfonyComponentValidatorConstraintsLessThan,
+ SymfonyComponentValidatorConstraintsLessThanOrEqual,
+ SymfonyComponentValidatorConstraintsLuhn,
+ SymfonyComponentValidatorConstraintsNotBlank,
+ SymfonyComponentValidatorConstraintsNotEqualTo,
+ SymfonyComponentValidatorConstraintsNotIdenticalTo,
+ SymfonyComponentValidatorConstraintsNotNull,
+ SymfonyComponentValidatorConstraintsRange,
+ SymfonyComponentValidatorConstraintsRegex,
+ SymfonyComponentValidatorConstraintsTime,
+ SymfonyComponentValidatorConstraintsType,
+ SvarohJsFormValidatorBundleFormConstraintUniqueEntity,
+ SymfonyComponentValidatorConstraintsUrl,
+ SymfonyComponentValidatorConstraintsValid,
+
+ // The names the False, Null and True constraints had before Symfony 2.7
+ // renamed them, they are still reachable as class aliases in PHP
+ SymfonyComponentValidatorConstraintsFalse: SymfonyComponentValidatorConstraintsIsFalse,
+ SymfonyComponentValidatorConstraintsNull: SymfonyComponentValidatorConstraintsIsNull,
+ SymfonyComponentValidatorConstraintsTrue: SymfonyComponentValidatorConstraintsIsTrue,
+});
diff --git a/src/Resources/public/js/namespace.js b/src/Resources/public/js/namespace.js
new file mode 100644
index 00000000..c33eb6e1
--- /dev/null
+++ b/src/Resources/public/js/namespace.js
@@ -0,0 +1,32 @@
+/**
+ * The namespace the library registers its constraints and transformers in.
+ *
+ * The PHP side serialises the class name of every constraint and of every view
+ * transformer into the form model, and the browser side instantiates them by
+ * that name with the namespace separators removed. Each of those names used to
+ * be a separate property of "window", which is what the "Add namespace in JS
+ * lib" issue objects to.
+ *
+ * The name mirrors the "Svaroh\JsFormValidatorBundle" PHP namespace and the
+ * "Svaroh" prefix the library already uses for its own globals. The object is
+ * merged into whatever is on "window.Svaroh" already, so an application that
+ * owns a "Svaroh" object of its own keeps it.
+ *
+ * An application registers its own constraint like this:
+ *
+ * Svaroh.constraints.AppValidatorConstraintsContainsAlphanumeric = function () {
+ * // ...
+ * };
+ *
+ * Constraints and transformers that are only defined as globals keep working:
+ * the lookup falls back to the global scope, see
+ * SvarohJsFormValidator.resolveClass().
+ */
+var Svaroh = window.Svaroh || {};
+
+Svaroh.constraints = Svaroh.constraints || {};
+Svaroh.transformers = Svaroh.transformers || {};
+
+window.Svaroh = Svaroh;
+
+export default Svaroh;
diff --git a/src/Resources/public/js/namespace.test.js b/src/Resources/public/js/namespace.test.js
new file mode 100644
index 00000000..9bbfa504
--- /dev/null
+++ b/src/Resources/public/js/namespace.test.js
@@ -0,0 +1,131 @@
+import './SvarohJsFormValidator';
+
+const validator = window.SvarohJsFormValidator;
+
+describe('the Svaroh namespace', () => {
+ test('is the single global the library registers its classes in', () => {
+ expect(typeof window.Svaroh).toBe('object');
+ expect(typeof window.Svaroh.constraints).toBe('object');
+ expect(typeof window.Svaroh.transformers).toBe('object');
+ });
+
+ test('a constraint is instantiated from the namespace, not from the global scope', () => {
+ const constraintClass = window.SymfonyComponentValidatorConstraintsNotBlank;
+ delete window.SymfonyComponentValidatorConstraintsNotBlank;
+
+ try {
+ const constraints = validator.parseConstraints({
+ 'Symfony\\Component\\Validator\\Constraints\\NotBlank': [
+ { message: 'This value should not be blank.' },
+ ],
+ });
+
+ expect(constraints).toHaveLength(1);
+ expect(constraints[0]).toBeInstanceOf(constraintClass);
+ expect(constraints[0].message).toBe('This value should not be blank.');
+ } finally {
+ window.SymfonyComponentValidatorConstraintsNotBlank = constraintClass;
+ }
+ });
+
+ test('a transformer is instantiated from the namespace, not from the global scope', () => {
+ const chainClass = window.SymfonyComponentFormExtensionCoreDataTransformerDataTransformerChain;
+ const valueClass = window.SymfonyComponentFormExtensionCoreDataTransformerChoiceToValueTransformer;
+ delete window.SymfonyComponentFormExtensionCoreDataTransformerDataTransformerChain;
+ delete window.SymfonyComponentFormExtensionCoreDataTransformerChoiceToValueTransformer;
+
+ try {
+ const transformers = validator.parseTransformers([
+ {
+ name: 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\DataTransformerChain',
+ transformers: [
+ {
+ name: 'Symfony\\Component\\Form\\Extension\\Core\\DataTransformer\\ChoiceToValueTransformer',
+ },
+ ],
+ },
+ ]);
+
+ expect(transformers).toHaveLength(1);
+ expect(transformers[0]).toBeInstanceOf(chainClass);
+ expect(transformers[0].transformers).toHaveLength(1);
+ expect(transformers[0].transformers[0]).toBeInstanceOf(valueClass);
+ } finally {
+ window.SymfonyComponentFormExtensionCoreDataTransformerDataTransformerChain = chainClass;
+ window.SymfonyComponentFormExtensionCoreDataTransformerChoiceToValueTransformer = valueClass;
+ }
+ });
+
+ test('an application constraint defined in the global scope is still instantiated', () => {
+ window.AppValidatorConstraintsContainsAlphanumeric = function () {
+ this.message = '';
+
+ this.validate = function () {
+ return [];
+ };
+ };
+
+ try {
+ const constraints = validator.parseConstraints({
+ 'App\\Validator\\Constraints\\ContainsAlphanumeric': [
+ { message: 'It can only contain letters or numbers.' },
+ ],
+ });
+
+ expect(constraints).toHaveLength(1);
+ expect(constraints[0].message).toBe('It can only contain letters or numbers.');
+ } finally {
+ delete window.AppValidatorConstraintsContainsAlphanumeric;
+ }
+ });
+
+ test('an application transformer defined in the global scope is still instantiated', () => {
+ window.AppFormDataTransformerMyTransformer = function () {
+ this.reverseTransform = function (value) {
+ return value;
+ };
+ };
+
+ try {
+ const transformers = validator.parseTransformers([
+ { name: 'App\\Form\\DataTransformer\\MyTransformer', extraOption: 'kept' },
+ ]);
+
+ expect(transformers).toHaveLength(1);
+ expect(transformers[0].extraOption).toBe('kept');
+ } finally {
+ delete window.AppFormDataTransformerMyTransformer;
+ }
+ });
+
+ test('an application class registered in the namespace is instantiated', () => {
+ window.Svaroh.constraints.AppValidatorConstraintsNamespaced = function () {
+ this.message = '';
+
+ this.validate = function () {
+ return [];
+ };
+ };
+
+ try {
+ const constraints = validator.parseConstraints({
+ 'App\\Validator\\Constraints\\Namespaced': [{ message: 'Namespaced.' }],
+ });
+
+ expect(constraints).toHaveLength(1);
+ expect(constraints[0].message).toBe('Namespaced.');
+ } finally {
+ delete window.Svaroh.constraints.AppValidatorConstraintsNamespaced;
+ }
+ });
+
+ test('a name the namespace and the global scope do not know is skipped', () => {
+ expect(validator.parseConstraints({
+ 'App\\Validator\\Constraints\\Unknown': [{ message: 'Unknown.' }],
+ })).toStrictEqual([]);
+
+ expect(validator.parseTransformers([
+ { name: 'App\\Form\\DataTransformer\\Unknown' },
+ ])).toStrictEqual([]);
+ });
+});
diff --git a/src/Resources/public/js/transformers/globals.test.js b/src/Resources/public/js/transformers/globals.test.js
index c686eb20..5be4a043 100644
--- a/src/Resources/public/js/transformers/globals.test.js
+++ b/src/Resources/public/js/transformers/globals.test.js
@@ -18,12 +18,21 @@ const transformerGlobals = [
namespace + 'ValueToDuplicatesTransformer',
];
-// parseTransformers() instantiates a transformer as window[className] and skips
-// it without a word when the global is undefined, which is how the localized
-// number types went unvalidated in the first place.
+// parseTransformers() instantiates a transformer as Svaroh.transformers[className]
+// and skips it without a word when the namespace holds no such name, which is how
+// the localized number types went unvalidated in the first place.
test.each(transformerGlobals)(
- 'window.%s is registered',
+ 'Svaroh.transformers.%s is registered',
+ (name) => {
+ expect(typeof window.Svaroh.transformers[name]).toBe('function');
+ },
+);
+
+// The global name of every transformer is kept as a deprecated alias.
+test.each(transformerGlobals)(
+ 'window.%s is a deprecated alias of the namespaced transformer',
(name) => {
expect(typeof window[name]).toBe('function');
+ expect(window[name]).toBe(window.Svaroh.transformers[name]);
},
);
diff --git a/src/Resources/public/js/transformers/index.js b/src/Resources/public/js/transformers/index.js
index 81e33138..3b3ddae4 100644
--- a/src/Resources/public/js/transformers/index.js
+++ b/src/Resources/public/js/transformers/index.js
@@ -1,13 +1,34 @@
-import './ArrayToParts.js';
-import './BooleanToString.js';
-import './ChoicesToBooleanArray.js';
-import './ChoicesToValues.js';
-import './ChoiceToBooleanArray.js';
-import './ChoiceToValue.js';
-import './DataTransformerChain.js';
-import './DateTimeToArray.js';
-import './IntegerToLocalizedString.js';
-import './MoneyToLocalizedString.js';
-import './NumberToLocalizedString.js';
-import './PercentToLocalizedString.js';
-import './ValueToDuplicates.js';
\ No newline at end of file
+import Svaroh from '../namespace.js';
+
+import SymfonyComponentFormExtensionCoreDataTransformerArrayToPartsTransformer from './ArrayToParts.js';
+import SymfonyComponentFormExtensionCoreDataTransformerBooleanToStringTransformer from './BooleanToString.js';
+import SymfonyComponentFormExtensionCoreDataTransformerChoicesToBooleanArrayTransformer from './ChoicesToBooleanArray.js';
+import SymfonyComponentFormExtensionCoreDataTransformerChoicesToValuesTransformer from './ChoicesToValues.js';
+import SymfonyComponentFormExtensionCoreDataTransformerChoiceToBooleanArrayTransformer from './ChoiceToBooleanArray.js';
+import SymfonyComponentFormExtensionCoreDataTransformerChoiceToValueTransformer from './ChoiceToValue.js';
+import SymfonyComponentFormExtensionCoreDataTransformerDataTransformerChain from './DataTransformerChain.js';
+import SymfonyComponentFormExtensionCoreDataTransformerDateTimeToArrayTransformer from './DateTimeToArray.js';
+import SymfonyComponentFormExtensionCoreDataTransformerIntegerToLocalizedStringTransformer from './IntegerToLocalizedString.js';
+import SymfonyComponentFormExtensionCoreDataTransformerMoneyToLocalizedStringTransformer from './MoneyToLocalizedString.js';
+import SymfonyComponentFormExtensionCoreDataTransformerNumberToLocalizedStringTransformer from './NumberToLocalizedString.js';
+import SymfonyComponentFormExtensionCoreDataTransformerPercentToLocalizedStringTransformer from './PercentToLocalizedString.js';
+import SymfonyComponentFormExtensionCoreDataTransformerValueToDuplicatesTransformer from './ValueToDuplicates.js';
+
+// The transformers are keyed by the name the PHP side serialises them under:
+// their class name with the namespace separators removed. Every module also
+// still assigns that name on "window" as a deprecated alias.
+Object.assign(Svaroh.transformers, {
+ SymfonyComponentFormExtensionCoreDataTransformerArrayToPartsTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerBooleanToStringTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerChoicesToBooleanArrayTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerChoicesToValuesTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerChoiceToBooleanArrayTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerChoiceToValueTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerDataTransformerChain,
+ SymfonyComponentFormExtensionCoreDataTransformerDateTimeToArrayTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerIntegerToLocalizedStringTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerMoneyToLocalizedStringTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerNumberToLocalizedStringTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerPercentToLocalizedStringTransformer,
+ SymfonyComponentFormExtensionCoreDataTransformerValueToDuplicatesTransformer,
+});