Skip to content

Commit

Permalink
Merge pull request #93 from hydrostack/92-upload-multiple-files
Browse files Browse the repository at this point in the history
Support binding multiple files to one field
  • Loading branch information
kjeske authored Oct 1, 2024
2 parents a867345 + f5702a0 commit e0fe889
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 16 deletions.
50 changes: 50 additions & 0 deletions docs/content/features/binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,56 @@ which we can use later when submitting the form.

> NOTE: Make sure the temporary storage is cleared periodically.
### Multiple files

To support multiple files in one field, you can use the `multiple` attribute:

```razor
<!-- AddAttachment.cshtml -->
@model AddAttachment
<div>
<input
asp-for="DocumentFile"
type="file"
multiple
hydro-bind />
</div>
```

Then your component code would change to:


```c#
// AddAttachment.cshtml.cs
public class AddAttachment : HydroComponent
{
[Transient]
public IFormFile[] DocumentFiles { get; set; }

[Required]
public List<string> DocumentIds { get; set; }

public override async Task BindAsync(PropertyPath property, object value)
{
if (property.Name == nameof(DocumentFiles))
{
DocumentIds = [];
var files = (IFormFile[])value;

foreach (var file in files)
{
DocumentIds.Add(await GetStoredTempFileId(file));
}
}
}

// rest of the file same as in the previous example
}
```

## Styling

`.hydro-request` CSS class is toggled on the elements that are currently in the binding process
1 change: 1 addition & 0 deletions docs/content/features/errors-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ app.UseExceptionHandler(b => b.Run(async context =>
{
if (!context.IsHydro())
{
context.Response.Redirect("/Error");
return;
}

Expand Down
34 changes: 26 additions & 8 deletions src/HydroComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public abstract class HydroComponent : ViewComponent
/// Default identifier used to specify place of the page to replace when during location change
/// </summary>
public const string LocationTargetId = "hydro";

/// <summary>
/// Provides list of already accessed component's properties
/// </summary>
Expand Down Expand Up @@ -569,15 +569,33 @@ private async Task BindModel(IFormCollection formCollection)
}
}

foreach (var file in formCollection.Files)
var fileGroups = formCollection.Files.GroupBy(m => m.Name);

foreach (var fileGroup in fileGroups)
{
var setter = PropertyInjector.GetPropertySetter(this, file.Name, file);
var propertyPath = PropertyPath.ExtractPropertyPath(file.Name);
var setter = PropertyInjector.GetPropertySetter(this, fileGroup.Key, null);
var propertyPath = PropertyPath.ExtractPropertyPath(fileGroup.Key);

if (setter != null)
{
setter.Value.Setter(file);
await BindAsync(propertyPath, file);
if (setter.Value.PropertyType.IsArray)
{
var formFiles = fileGroup.ToArray();
setter.Value.Setter(formFiles);
await BindAsync(propertyPath, formFiles);
}
else if (typeof(IEnumerable<IFormFile>).IsAssignableFrom(setter.Value.PropertyType))
{
var formFiles = fileGroup.ToList();
setter.Value.Setter(formFiles);
await BindAsync(propertyPath, formFiles);
}
else if (setter.Value.PropertyType == typeof(IFormFile))
{
var formFile = fileGroup.First();
setter.Value.Setter(formFile);
await BindAsync(propertyPath, formFile);
}
}
else
{
Expand Down Expand Up @@ -685,7 +703,7 @@ private void PopulateDispatchers()

HttpContext.Response.Headers.TryAdd(HydroConsts.ResponseHeaders.Trigger, JsonConvert.SerializeObject(data, JsonSerializerSettings));
}

private void PopulateClientScripts()
{
if (!_clientScripts.Any())
Expand Down Expand Up @@ -1084,4 +1102,4 @@ internal void AddClientScript(string script) =>

private static string Hash(string input) =>
$"W{Convert.ToHexString(MD5.HashData(Encoding.ASCII.GetBytes(input)))}";
}
}
22 changes: 16 additions & 6 deletions src/PropertyInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public static void SetPropertyValue(object target, string propertyPath, object v
propertyInfo.SetValue(currentObject, convertedValue);
}

public static (object Value, Action<object> Setter)? GetPropertySetter(object target, string propertyPath, object value)
public static (object Value, Action<object> Setter, Type PropertyType)? GetPropertySetter(object target, string propertyPath, object value)
{
if (target == null)
{
Expand Down Expand Up @@ -186,7 +186,7 @@ private static (int, string) GetIndexAndCleanedPropertyName(string propName)
return (Convert.ToInt32(iteratorValue), cleanedPropName);
}

private static (object, Action<object>)? SetValueOnObject(object obj, string propName, object valueToSet)
private static (object, Action<object>, Type)? SetValueOnObject(object obj, string propName, object valueToSet)
{
if (obj == null)
{
Expand All @@ -211,10 +211,10 @@ private static (object, Action<object>)? SetValueOnObject(object obj, string pro

var convertedValue = ConvertValue(valueToSet, propertyInfo.PropertyType);
propertyInfo.SetValue(obj, convertedValue);
return (convertedValue, val => propertyInfo.SetValue(obj, val));
return (convertedValue, val => propertyInfo.SetValue(obj, val), propertyInfo.PropertyType);
}

private static (object, Action<object>)? SetIndexedValue(object obj, string propName, object valueToSet)
private static (object, Action<object>, Type)? SetIndexedValue(object obj, string propName, object valueToSet)
{
var (index, cleanedPropName) = GetIndexAndCleanedPropertyName(propName);
var propertyInfo = obj.GetType().GetProperty(cleanedPropName);
Expand All @@ -234,7 +234,7 @@ private static (object, Action<object>)? SetIndexedValue(object obj, string prop
throw new InvalidOperationException("Wrong type");
}

return (convertedValue, val => array.SetValue(val, index));
return (convertedValue, val => array.SetValue(val, index), propertyInfo!.PropertyType);
}

if (typeof(IList).IsAssignableFrom(propertyInfo.PropertyType))
Expand All @@ -244,7 +244,7 @@ private static (object, Action<object>)? SetIndexedValue(object obj, string prop
throw new InvalidOperationException("Wrong type");
}

return (convertedValue, val => list[index] = val);
return (convertedValue, val => list[index] = val, propertyInfo!.PropertyType);
}

throw new InvalidOperationException($"Indexed access for property '{cleanedPropName}' is not supported.");
Expand All @@ -261,6 +261,16 @@ private static object ConvertValue(object valueToConvert, Type destinationType)
{
return null;
}

if (typeof(IFormFile[]).IsAssignableFrom(destinationType) && StringValues.IsNullOrEmpty(stringValues))
{
return Array.Empty<IFormFile>();
}

if (typeof(IEnumerable<IFormFile>).IsAssignableFrom(destinationType) && StringValues.IsNullOrEmpty(stringValues))
{
return new List<IFormFile>();
}

var converter = TypeDescriptor.GetConverter(destinationType!);

Expand Down
6 changes: 4 additions & 2 deletions src/Scripts/hydro.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,11 @@
formData.set(propertyName, el.checked);
} else if (el.tagName === "INPUT" && el.type === 'file') {
if (el.files.length) {
formData.set(propertyName, el.files[0]);
Array.from(el.files).forEach((file) => {
formData.append(propertyName, file);
});
} else {
formData.set(propertyName, new Blob(), '');
formData.set(propertyName, '');
}
} else {
formData.set(propertyName, el.value);
Expand Down

0 comments on commit e0fe889

Please sign in to comment.