Skip to content

Commit 95bd930

Browse files
committed
feat: better vue-forms support for workflows
1 parent 3b3c61b commit 95bd930

File tree

9 files changed

+94
-75
lines changed

9 files changed

+94
-75
lines changed

docs/en/reference/Scripting.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ The following methods are available in addition to the OrchardCore [scripting me
1717
| Function | Description
1818
| -------- | ----------- |
1919
|`requestFormAsJsonObject(): JObject`| Returns a sanitized JObject representation of the `HttpContext.Request.Form` object. Sanitization is performed by Orchard's [sanitizer](https://docs.orchardcore.net/en/dev/docs/reference/core/Sanitizer/). |
20+
| `addError(name: String, errorMessage: String): void` | Adds an error to the input with the specified name. Use the `serverValidationMessage` name to add a global error message to your VueForm. |
21+
| `hasErrors(): Boolean` | Returns true if the error dictionary contains any errors. |
22+
| `validateReCaptcha(recaptchaResponse): Boolean` | Returns true if the recaptchaResponse is valid, false if invalid. |
2023

2124
### Contents
2225

@@ -56,5 +59,4 @@ You can get the values stored in the LocalizedTextPart inside a script.
5659

5760
| Function | Description
5861
| -------- | ----------- |
59-
| `addError(name: String, errorMessage: String): void` | Adds an error to the input with the specified name. Use the `serverValidationMessage` name to add a global error message to your VueForm. |
6062
| `getFormContentItem(): ContentItem` | Only available in the VueForm server side scripts. Returns the current VueForm ContentItem instance. |

docs/en/reference/modules/VueForms.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ VeeValidate.setInteractionMode('passive');
4444

4545
#### OnValidation script
4646

47-
The OnValidation script is used to specify the server side valiadtion script. We are planning to implement components with integrated validation in the future.
47+
The OnValidation script is used to specify the server side validation script. We are planning to implement components with integrated validation in the future.
4848
This module adds some [scripting methods](../Scripting.md#vueforms-module-statcanorchardcorevueforms) to facilitate handling form data and errors.
4949

5050
Here is an example OnValidation script that validates that the name is required.
@@ -65,6 +65,7 @@ addError('serverValidationMessage', 'This is a validation message that is not ti
6565
addError('serverValidationMessage', 'You can add many, they will come as an array via the "form.serverValidationMessage" prop!');
6666
```
6767

68+
All errors are also available on the VueJS component options object's via the `form.responseData.errors` property
6869

6970
#### OnSubmitted script
7071

@@ -173,6 +174,7 @@ You can access these properties in your templates or in the component options ob
173174
| form.successMessage | The success message returned from the server as specified in the [VueForm](#vueform-part) |
174175
| form.submitValidationError | Set to true when a server validation error occus. |
175176
| form.serverValidationMessage | Array of errors set by the server with the `addError('serverValidationMessage', 'Message')` scripting method. |
177+
| form.responseData | The raw response data recieved from the server. Useful if you want to return additional data to the form via a workflow. |
176178
| form.submitError | Set to true when a server error, ajax error or unhandled error occurs. |
177179
| form.serverErrorMessage | An error message set with the ajax error status code and text. Only set when a server errors occur. |
178180

@@ -187,12 +189,21 @@ Although this module does not require a workflow to handle the form submissions,
187189

188190
Please see the [workflow](../Workflows.md#vueforms-statcanorchardcorevueforms) documentation.
189191

192+
If you do use a workflow, you can use the `addError("name", "message");` script to add form errors. You can also use the `HttpRedirect` or `HttpResponse` workflow tasks to redirect or return a custom set of data to the client. All data returned by the HttpResponse task is available via the `form.responseData` object.
193+
190194
## Localization (`StatCan.OrchardCore.VueForms.Localized`)
191195

192196
While you can use Orchard's [LocalizationPart](https://docs.orchardcore.net/en/dev/docs/reference/modules/ContentLocalization/#localizationpart) to localize your forms. We suggest you use the [LocalizedText](LocalizedText.md) feature to implement i18n in your forms. This part is what we weld to your VueForm content type when you enable this feature.
193197

198+
The `[locale]` shortcode is also useful to use in your views to localize simple text fields.
199+
194200
## Examples
195201

196202
The following example forms are provided with the VueForms module as recipes:
197203

198204
- Contact Form
205+
- UserProfile Form
206+
207+
### UserProfile Form
208+
209+
This form allows you to edit a `UserProfile` Content Type with the `CustomUserSettings` stereotype. For this form to work, you must enable the `Users Change Email` feature along with allowing users to update their email in the settings.

src/Modules/StatCan.OrchardCore.Scripting/FormsGlobalMethodsProvider.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,35 @@
77
using OrchardCore.Infrastructure.Html;
88
using OrchardCore.Scripting;
99
using OrchardCore.ReCaptcha.Services;
10+
using OrchardCore.DisplayManagement.ModelBinding;
1011

1112
namespace StatCan.OrchardCore.Scripting
1213
{
1314
public class FormsGlobalMethodsProvider : IGlobalMethodProvider
1415
{
1516
private readonly GlobalMethod _formAsJsonObject;
1617
private readonly GlobalMethod _validateReCaptcha;
18+
private readonly GlobalMethod _setModelError;
19+
private readonly GlobalMethod _hasErrors;
1720

1821
public FormsGlobalMethodsProvider()
1922
{
23+
_setModelError = new GlobalMethod
24+
{
25+
Name = "addError",
26+
Method = serviceProvider => (Action<string, string>)((name, text) => {
27+
var updateModelAccessor = serviceProvider.GetRequiredService<IUpdateModelAccessor>();
28+
updateModelAccessor.ModelUpdater.ModelState.AddModelError(name, text);
29+
})
30+
};
31+
_hasErrors = new GlobalMethod
32+
{
33+
Name = "hasErrors",
34+
Method = serviceProvider => (Func<bool>)(() => {
35+
var updateModelAccessor = serviceProvider.GetRequiredService<IUpdateModelAccessor>();
36+
return updateModelAccessor.ModelUpdater.ModelState.ErrorCount > 0;
37+
})
38+
};
2039
_formAsJsonObject = new GlobalMethod
2140
{
2241
Name = "requestFormAsJsonObject",
@@ -51,7 +70,7 @@ public FormsGlobalMethodsProvider()
5170

5271
public IEnumerable<GlobalMethod> GetMethods()
5372
{
54-
return new[] { _formAsJsonObject, _validateReCaptcha };
73+
return new[] { _formAsJsonObject, _validateReCaptcha, _setModelError, _hasErrors };
5574
}
5675
}
5776
}

src/Modules/StatCan.OrchardCore.VueForms/Assets/vue-forms.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ function initForm(app) {
3434
submitError: false,
3535
submitValidationErrors: false,
3636
serverValidationMessage: undefined,
37-
serverErrorMessage: undefined
37+
serverErrorMessage: undefined,
38+
responseData: undefined
3839
}
3940

4041
Vue.component(app.dataset.name, {
@@ -80,9 +81,11 @@ function initForm(app) {
8081
success: function (data) {
8182

8283
vm.form = {...defaultFormData};
84+
vm.form.responseData = data;
8385
// if there are validation errors on the form, display them.
8486
if (data.validationError) {
85-
// special key for serverErrorMessages
87+
88+
//legacy
8689
if(data.errors['serverValidationMessage'] != null) {
8790
vm.form.serverValidationMessage = data.errors['serverValidationMessage'];
8891
}
@@ -97,15 +100,10 @@ function initForm(app) {
97100
return;
98101
}
99102

100-
//success, set the form success message
101-
if (data.success) {
102-
vm.form.submitSuccess = true;
103-
vm.form.successMessage = data.successMessage;
104-
return;
105-
}
106-
vm.form.submitError = true;
107-
// something went wrong, dev issue
108-
vm.form.serverErrorMessage = "Something wen't wrong. Please report this to your site administrators. Error code: `VueForms.AjaxHandler`";
103+
vm.form.submitSuccess = true;
104+
vm.form.successMessage = data.successMessage;
105+
return;
106+
109107
},
110108
error: function (xhr, statusText) {
111109
vm.form = {...defaultFormData};

src/Modules/StatCan.OrchardCore.VueForms/Controllers/VueFormController.cs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
using StatCan.OrchardCore.VueForms.Workflows;
1818
using StatCan.OrchardCore.Extensions;
1919
using StatCan.OrchardCore.VueForms.Scripting;
20+
using System.Linq;
21+
using OrchardCore.Workflows.Http;
2022

2123
namespace StatCan.OrchardCore.VueForms.Controllers
2224
{
@@ -77,52 +79,73 @@ public async Task<IActionResult> Submit(string formId)
7779
return NotFound();
7880
}
7981

80-
// form validation server side script
81-
var errorsDictionary = new Dictionary<string, List<string>>();
82-
var scriptingProvider = new VueFormMethodsProvider(form, errorsDictionary);
82+
var scriptingProvider = new VueFormMethodsProvider(form);
8383

8484
var script = form.As<VueFormScripts>();
8585
if (!string.IsNullOrEmpty(script?.OnValidation?.Text))
8686
{
8787
_scriptingManager.EvaluateJs(script.OnValidation.Text, scriptingProvider);
8888
}
8989

90-
if (errorsDictionary.Count > 0)
90+
if (ModelState.ErrorCount > 0)
9191
{
92-
return Json(new { validationError = true, errors = errorsDictionary });
92+
return Json(new { validationError = true, errors = GetErrorDictionary() });
9393
}
9494

9595
if (!string.IsNullOrEmpty(script?.OnSubmitted?.Text))
9696
{
9797
_scriptingManager.EvaluateJs(script.OnSubmitted.Text, scriptingProvider);
9898
}
9999

100-
if (errorsDictionary.Count > 0)
100+
if (ModelState.ErrorCount > 0)
101101
{
102-
return Json(new { validationError = true, errors = errorsDictionary });
102+
return Json(new { validationError = true, errors = GetErrorDictionary() });
103103
}
104104

105105
// _workflow manager is null if workflow feature is not enabled
106106
if (_workflowManager != null)
107107
{
108-
//todo: make sure this does not create issues if the workflows has a blocking event
109108
await _workflowManager.TriggerEventAsync(nameof(VueFormSubmittedEvent),
110109
input: new { VueForm = form },
111110
correlationId: form.ContentItemId
112111
);
113112
}
114113

115-
// 302 are equivalent to 301 in this case. No permanent redirect
114+
// workflow added errors, return them here
115+
if (ModelState.ErrorCount > 0)
116+
{
117+
return Json(new { validationError = true, errors = GetErrorDictionary() });
118+
}
119+
120+
// Handle the redirects with ajax requests.
121+
// 302 are equivalent to 301 in this case. No permanent redirect.
122+
// This can come from a scripting method or the HttpRedirect Workflow Task
116123
if(HttpContext.Response.StatusCode == 301 || HttpContext.Response.StatusCode == 302)
117124
{
118125
var returnValue = new { redirect = WebUtility.UrlDecode(HttpContext.Response.Headers["Location"])};
119126
HttpContext.Response.Clear();
120127
return Json(returnValue);
121128
}
129+
130+
// This get's set by either the HttpRedirectTask or HttpResponseTask
131+
if(HttpContext.Items[WorkflowHttpResult.Instance] != null)
132+
{
133+
// Let the HttpResponseTask control the response. This will fail on the client if it's anything other than json
134+
return new EmptyResult();
135+
}
122136
var formSuccessMessage = await _liquidTemplateManager.RenderAsync(formPart.SuccessMessage?.Text, _htmlEncoder);
123137
formSuccessMessage = await _shortcodeService.ProcessAsync(formSuccessMessage);
124138
// everything worked fine. send the success signal to the client
125-
return Json(new { success = true, successMessage = formSuccessMessage });
139+
return Json(new { successMessage = formSuccessMessage });
140+
}
141+
private Dictionary<string, string[]> GetErrorDictionary()
142+
{
143+
return ModelState
144+
.Where(x => x.Value.Errors.Count > 0)
145+
.ToDictionary(
146+
kvp => kvp.Key,
147+
kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
148+
);
126149
}
127150
}
128151
}
Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
22
using System.Collections.Generic;
33
using AngleSharp.Common;
4+
using Microsoft.AspNetCore.Mvc.ModelBinding;
5+
using Microsoft.Extensions.DependencyInjection;
46
using OrchardCore.ContentManagement;
7+
using OrchardCore.DisplayManagement.ModelBinding;
58
using OrchardCore.Scripting;
69

710
namespace StatCan.OrchardCore.VueForms.Scripting
@@ -11,36 +14,11 @@ namespace StatCan.OrchardCore.VueForms.Scripting
1114
/// </summary>
1215
public class VueFormMethodsProvider : IGlobalMethodProvider
1316
{
14-
private readonly GlobalMethod _setModelError;
15-
private readonly GlobalMethod _hasErrors;
17+
1618
private readonly GlobalMethod _getFormContentItem;
1719

18-
public VueFormMethodsProvider(ContentItem form, IDictionary<string, List<string>> errors)
20+
public VueFormMethodsProvider(ContentItem form)
1921
{
20-
_setModelError = new GlobalMethod
21-
{
22-
Name = "addError",
23-
Method = serviceProvider =>
24-
(Action<string, string>)((name, text) =>
25-
{
26-
var errorKey = errors.GetOrDefault(name, new List<string>());
27-
errorKey.Add(text);
28-
if (errors.ContainsKey(name))
29-
{
30-
errors[name] = errorKey;
31-
}
32-
else
33-
{
34-
errors.Add(name, errorKey);
35-
}
36-
}
37-
)
38-
};
39-
_hasErrors = new GlobalMethod
40-
{
41-
Name = "hasErrors",
42-
Method = serviceProvider => (Func<bool>)(() => errors.Count > 0)
43-
};
4422
_getFormContentItem = new GlobalMethod
4523
{
4624
Name = "getFormContentItem",
@@ -50,7 +28,7 @@ public VueFormMethodsProvider(ContentItem form, IDictionary<string, List<string>
5028

5129
public IEnumerable<GlobalMethod> GetMethods()
5230
{
53-
return new[] { _setModelError, _getFormContentItem, _hasErrors };
31+
return new[] { _getFormContentItem };
5432
}
5533
}
5634
}

src/Modules/StatCan.OrchardCore.VueForms/Workflows/VueFormSubmittedEvent.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,8 @@ public override ActivityExecutionResult Resume(WorkflowExecutionContext workflow
6969
private IContent GetForm(WorkflowExecutionContext workflowContext)
7070
{
7171
// get the form from the workflow Input or Properties
72-
var content = workflowContext.Input.GetValue<IContent>(InputKey)
72+
return workflowContext.Input.GetValue<IContent>(InputKey)
7373
?? workflowContext.Properties.GetValue<IContent>(InputKey);
74-
75-
if (content != null)
76-
{
77-
return content;
78-
}
79-
80-
return null;
8174
}
8275
}
8376
}

src/Modules/StatCan.OrchardCore.VueForms/wwwroot/Scripts/vue-forms.js

Lines changed: 9 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)