I would like to create a form that allows the user to upload a file (a JSON object) and then post the contents of that file to an api.
I would like to see (for now just in a console log) the contents of the file I am about to post before I post it. So the flow should go like this
User sees a blank form with an upload file field
User selects a file from their computer
Once selected, I would like to log the contents of the file to the console
Once form is submitted, I would like to post the file to an endpoint with axios
I am using the useFormik hook because I have used it elsewhere and found it more intuitive than some of Formiks other options, and because I think it will be useful for when I build a few more features, so there may be some redundant lines of code here (e.g. initialValues) but for now I'm just focused on step one - seeing the values in a file before I post them.
It seems like I should be able to upload the file, read the contents of the file, store the contents of the file in a result, then pass that result to the onSubmit callback in the eventual axios.post() request I will make.
But I don't think I'm understanding the fundamentals here.
Should I use new FormData instead of a FileReader?
Should I bother reading the contents of the file before posting?
Here's my current code:
import { useFormik } from 'formik';
export const MyUploadPerfectAssetForm = () => {
const onChange = (event: any) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
// The file's text will be printed here
console.log(e.target?.result)
const result = e.target?.result;
console.log("logging result from reader.onload " + result)
return result;
};
//shows the files values properly
reader.readAsText(file);
}
//redundant for now?
const formik = useFormik({
initialValues: {
name: null,
tags: null,
type: null,
s3URL: null,
thumbnailImageURL: null,
},
onSubmit: (values, result) => {
console.log("logging values" + JSON.stringify(values))
alert(JSON.stringify(values, null, 2));
console.log("logging values from onSubmit " + JSON.stringify(values))
const uploadPerfectAsset = async (perfectAssetValues: any) => {
//there will be an axios post request here,
console.log("upload perfect asset ran")
console.log("testing uploadPerfectAsset with result from onChange file reader result" + JSON.stringify(perfectAssetValues))
}
uploadPerfectAsset(result)
},
});
return (
<div>
<form onSubmit={formik.handleSubmit}>
<div>
<input id="file"
name="test"
type="file"
onChange={onChange}></input>
</div>
<div>
<button type="submit">Submit</button></div>
</form>
</div >
);
}
Thanks
You're almost there mate đ A couple of small changes are needed:
add file to initialState
move onChange function to after the hook useFormik() and add formik.setFieldValue("file", file); to set the value
remove argument perfectAssetValues from function uploadPerfectAsset() - it's unnecessary
export const MyUploadPerfectAssetForm = () => {
const formik = useFormik({
initialValues: {
name: null,
tags: null,
type: null,
s3URL: null,
thumbnailImageURL: null,
file: null
},
onSubmit: (values) => {
console.log("logging values from onSubmit ", values);
const uploadPerfectAsset = async () => {
//there will be an axios post request here,
console.log("upload perfect asset ran");
console.log(
"File info: ",
JSON.stringify(
{
fileName: values.file.name,
type: values.file.type,
size: `${values.file.size} bytes`
},
null,
2
)
);
};
uploadPerfectAsset();
}
});
const onChange = (event) => {
const file = event.target.files[0];
formik.setFieldValue("file", file);
const reader = new FileReader();
// temporarily show file contentx
reader.onload = (e) => {
// The file's text will be printed here
const result = e.target.result;
console.log("logging result from reader.onload " + result);
return result;
};
//shows the files values properly
reader.readAsText(file);
};
return (
<div>
<form onSubmit={formik.handleSubmit}>
<div>
<input id="file" name="test" type="file" onChange={onChange}></input>
</div>
<div>
<button type="submit">Submit</button>
</div>
</form>
</div>
);
};
Live demo
Related
I have a Stencil custom input component with several props and I'm having major problems testing the readonly attribute.
The Stencil class looks like this:
#Component({
tag: 'my-input',
shadow: true,
})
export class Input {
#Prop() readonly: boolean = false;
.
.
render() {
return (
.
.
.
<input readonly={this.readonly}
.
.
What the actual rendered HTML looks like:
[![enter image description here][1]][1]
The interesting part is if I edit the HTML it looks as if the readonly attribute is empty:
<input maxlength="524288" type="text" id="input" placeholder="Enter text" readonly="">
The Playwright test:
test('input is readonly', async ({ page }) => {
const myinput = await page.locator('my-input');
await myinput.evaluate((el) => {
el.setAttribute('readonly', 'true');
});
const input = await page.locator('my-input >> input');
const attrs = await input.evaluate(async (el) => el.getAttributeNames()); // doesnt work
await expect(attrs.includes('readonly')).toBeTruthy(); // doesnt work
await expect(input).toHaveAttribute('readonly', 'true'); // doesnt work
});
If I test the available attributes on the elements sometimes the readonly is not present. WHY is it so inconsistent? âšī¸
[1]: https://i.stack.imgur.com/H5WD0.png
Your example contains a few problems:
test('input is readonly', async ({ page }) => {
// Dont't forget to open the page with the input
// await page.goto('http://localhost:52330/index.html');
// 1. remove await â â 2. provide valid selector
const myinput = page.locator('#input');
await myinput.evaluate((el) => {
el.setAttribute('readonly', 'true');
});
// 3. remove await â â 4. provide valid selector
const input = page.locator('#input');
const attrs = await input.evaluate(async (el) => el.getAttributeNames());
// 5. remove await â
expect(attrs.includes('readonly')).toBeTruthy();
await expect(input).toHaveAttribute('readonly', 'true');
});
Tested it with your HTML code:
<input maxlength="524288" type="text" id="input" placeholder="Enter text" readonly="">
and it works
I am trying to migrate Model-driven-form to Reactive-form.
This is a dynamic form that gets filled according to data from getCommandPacket
this.renderSvc.getCommandPacket - is getting the data from the server, this is the function signature:
Server
..
[HttpGet("[action]")]
public Dictionary<string, string> GetCommandPacket(int ID){
..
}
..
Html
<form>
<div *ngFor="let key of commandKeys">
<span class="ui-float-label">
<textarea [id]="key" [name]="key" pInputTextArea [(ngModel)]="commandPacket[key]" style="width: 40em;"></textarea>
<label [for]="key">{{ key }}</label>
</span>
</div>
<p-button label="Add Field"></p-button>
<button p-button type="submit" icon="fa fa-angle-right" iconPos="right">
<span class="ui-button-text ui-clickable">Re-Submit</span>
</button>
</form>
TS
...
export class CommandPacketDetailsComponent implements OnInit {
#Input() id: number;
myForm: FormGroup;
constructor(private renderSvc: PfServerService, private fb: FormBuilder) {
}
commandPacket: { [key: string]: string; };
commandKeys: string[];
message: string = null;
ngOnInit() {
if (this.id !== 0 && typeof this.id !== "undefined")
this.getCommandPacket(this.id);
else
this.message = "No ID Given for Packet";
}
getCommandPacket(id: number) {
this.renderSvc.getCommandPacket(id).subscribe(data => {
this.commandPacket = data;
this.commandKeys = Object.keys(this.commandPacket);
});
}
...
how can I achieve the same result but in Reactive-form way?
You want to use FormArray. Declare form and within it, declare formArray. Then when you get your data from service, create as many formControls as you have results and add them to FormArray.
you have an example here:
https://angular.io/guide/reactive-forms#use-formarray-to-present-an-array-of-formgroups
form type:
yourForm:FormGroup;
form definition:
this.yourForm = this.fb.group({
yourFormArray: this.fb.array([])
});
make a getter for your formArray:
get yourFormArray(): FormArray {
return this.cpForm.get('commands') as FormArray;
}
and then once you get your data from server:
this.yourFormArray.reset();
this.commandKeys.forEach(val =>
this.yourFormArray.push(this.fb.group({ command: [''] }))
);
that will create as many command (without s) formGroups (having only one input field) as you have keys in your commandKeys result.
PS.
once you set that up you can use patchValue on formArray to fill it with actual values. something like:
this.myFormArray.patchValue(commandKeys.map(key => ({ command: key })));
PS2.
to clear form controls from formarray, you can use function like this:
//Clear formArray
clearItemsFormArray() {
while (this.yourFormArray.length > 0)
this.yourFormArray.removeAt(0);
}
yourFormArray is the one coming from getter.
I have a pretty basic Dropzone in an asp.net core app:
<div class="panel-body " id="dropzone">
<form asp-action="Upload" class="dropzone
needsclick dz-clickable" id="uploader">
<div class="dz-message needsclick">
Drop files here or <a class="btn btn-
primary">click</a> to upload.<br>
</div>
</form>
</div>
I'm trying to set up a params value in the config, because in some situations there is a selectlist where the user chooses a value that must be passed to the MVC controller. The param is always null and I have a feeling it's because of the way I am setting the configuration:
$(document).ready(function () {
Dropzone.options.uploader = {
acceptedFiles: ".csv, .xls, .xlsx",
init: function () {
this.on("error", function (file) {
if (!file.accepted) alert("csv, xls, and xlsx files only. File not uploaded");
this.removeFile(file);
});
},
params: { orgId: $('#orglist').val()}
};
I'm guessing my params value is calculated at doc ready and never again. How I can I pass the value currently selected when the actual file is dragged?
The answer I found was to add a listener to the send function and remove params option, and append a new parameter orgID which is a parameter in my controller action :
Dropzone.options.uploader = {
acceptedFiles: ".csv, .xls, .xlsx",
init: function () {
this.on("error", function (file) {
if (!file.accepted) alert("csv, xls, and xlsx files only. File not uploaded");
this.removeFile(file);
});
this.on("sending", function (file, xhr, formData) {
formData.append("orgId", $('#orglist').val())
})
}
I have somewhat of a complex requirement here (a real head-scratcher)... and I'm not sure on the best way to proceed:
Requirement:
Build a page for managing widgets (CMS content blocks) in MVC5 using AngularJS for the frontend (as per the rest of the admin UI). The problem is that each widget has its own specific set of properties. They all share some properties like Title, IsEnabled, etc.. but an HTML Widget for example will have a BodyContent field and a Slider Widget would have a collection of images, etc..
My first thought was using [UIHint] and Html.EditorFor so that each widget type will have its own markup.. I think that's pretty straightforward, but how could we get the properties from any such arbitrary widget into the AngularJS model?
Example Controller
widgetsApp.controller('widgetController', function ($scope, $http) {
$scope.emptyGuid = '00000000-0000-0000-0000-000000000000';
$scope.id = $scope.emptyGuid;
$scope.title = '';
$scope.order = 0;
$scope.enabled = false;
$scope.widgetType = '';
$scope.zoneId = $scope.emptyGuid;
// etc
// how to get properties of ANY widget type?
Is this even possible? Is there a better solution? Note, I might consider changing the code to use Knockout or some other such framework if it can support my requirements.
Edit
Note that the issue is further complicated because of the fact of needing to then pass such a model back to the server and dealing with it there. In regular MVC controllers, I can use Request.Form to inspect what other values are there, but I'm using Web API and not sure if that's possible there.
Edit 2
Okay, so I think I'm on the right track, but still having issues. Firstly, here's my progress:
I found out about .factory and made a test page like this:
<div ng-app="myApp">
<div ng-controller="controller1">
<button class="btn btn-primary" ng-click="showAllInfo()">Show Info</button>
</div>
<div ng-controller="controller2">
</div>
</div>
<script type="text/javascript">
var myApp = angular.module('myApp', []);
myApp.factory('widgetModel', function () {
return {
id: '00000000-0000-0000-0000-000000000000',
title: '',
order: 0,
enabled: false,
widgetName: '',
widgetType: '',
zoneId: '00000000-0000-0000-0000-000000000000',
displayCondition: '',
widgetValues: '',
pageId: null,
cultureCode: '',
refId: null,
};
});
// This is representative of the main controller
myApp.controller('controller1', function ($scope, widgetModel) {
$scope.emptyGuid = '00000000-0000-0000-0000-000000000000';
$scope.model = widgetModel;
$scope.model.id = $scope.emptyGuid;
$scope.showAllInfo = function () {
alert("id: " + $scope.model.id + ", New Property: " + $scope.model.myNewProperty);
};
});
// This is representative of the details controller (to add properties specific to that particular widget type)
myApp.controller('controller2', function ($scope, widgetModel) {
$scope.model = widgetModel;
$scope.model.myNewProperty = "My Awesome Widget";
});
</script>
The above test works beautifully.. however, when I use this sort of code in my real application it fails to work and the reason I believe is because the second controller is injected into the DOM later on.. here's what's happening:
I have a div as follows
<div ng-bind-html="widgetDetails"></div>
and after loading the other details, I load the html for this as such:
$http.get("/admin/widgets/get-editor-ui/" + $scope.model.id).success(function (json) {
$scope.widgetDetails = $sce.trustAsHtml(json.Content);
});
That works.. I can see my the html controls for my new properties there.. the following snippet is the HTML which is injected into the above div:
<div ng-controller="widgetDetailsController">
<div class="col-sm-12 col-md-12">
<div class="form-group">
#Html.Label("BodyContent", "Body Content", new { #class = "control-label" })
#Html.TextArea("BodyContent", null, new { #class = "form-control", ng_model = "model.bodyContent", ui_tinymce = "tinyMCEOptions_BodyContent" })
</div>
</div>
<button class="btn" ng-click="test()">Test</button>
</div>
<script type="text/javascript">
widgetsApp.controller('widgetDetailsController', function ($scope, $http, widgetModel) {
$scope.model = widgetModel;
$scope.json = angular.fromJson($scope.model.widgetValues);
$scope.model.bodyContent = $scope.json.bodyContent || "";
$scope.test = function () {
alert($scope.model.bodyContent);
};
});
</script>
When I click, the "Test" button, nothing happens...
I tried to load a controller dynamically via the method outlined at this link: http://www.bennadel.com/blog/2553-loading-angularjs-components-after-your-application-has-been-bootstrapped.htm
It doesn't work. To be honest though, I am new to AngularJS and don't really know all the ins out outs of it.. any help would be great.
IF you are just looking to get the properties and their values, then on AngularJS or Javascript side you can just iterate over the object properties to get all the properties defined over the object.
for(var key in obj){
$scope[key]=obj[key];
}
Once on scope you can bind it to the view using ng-model.
This approach would get you the data but metadata about the data such as control to render for property need would not work.
For advance scenarios you should try to send metadata about each properties that can help render it on the view.
If ng-model is setup correctly all data would be send to server.
On the server you can use the dynamic keyword as input parameter to webapi method and there should be a similar method to iterate over the payload using key value pair.
I ended up changing to KnockoutJS, partly because AngularJS ended up being a bit overkill for my needs, but also because it couldn't handle this situation very nicely (or at least there was no obvious and clean way to do it). My KnockoutJS solution is below:
In the main page, I add an html element:
<fieldset id="widget-details"></fieldset>
An example of arbitrary HTML to be injected:
<div id="widget-content" class="col-sm-12 col-md-12">
<div class="form-group">
#Html.Label("BodyContent", "Body Content", new { #class = "control-label" })
#Html.TextArea("BodyContent", null, new { #class = "form-control", data_bind = "wysiwyg: bodyContent, wysiwygConfig: tinyMCEConfig" })
</div>
</div>
<script type="text/javascript">
function updateModel() {
var data = ko.mapping.fromJSON(viewModel.widgetValues());
viewModel.bodyContent = ko.observable("");
if (data && data.BodyContent) {
viewModel.bodyContent(data.BodyContent());
}
viewModel.tinyMCEConfig = {
theme: "modern",
plugins: [
"advlist autolink lists link image charmap print preview hr anchor pagebreak",
"searchreplace wordcount visualblocks visualchars code fullscreen",
"insertdatetime media nonbreaking save table contextmenu directionality",
"emoticons template paste textcolor"
],
toolbar1: "insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image",
toolbar2: "print preview media | forecolor backcolor emoticons",
image_advtab: true,
templates: [
{ title: 'Test template 1', content: 'Test 1' },
{ title: 'Test template 2', content: 'Test 2' }
],
content_css: tinyMCEContentCss
};
};
function onBeforeSave() {
var data = {
BodyContent: viewModel.bodyContent()
};
viewModel.widgetValues(ko.mapping.toJSON(data));
};
</script>
Then in my script for the main page, I use the following:
$.ajax({
url: "/admin/widgets/get-editor-ui/" + self.id(),
type: "GET",
dataType: "json",
async: false
})
.done(function (json) {
var result = $(json.Content);
var content = $(result.filter('#widget-content')[0]);
var details = $('<div>').append(content.clone()).html();
$("#widget-details").html(details);
var scripts = result.filter('script');
scripts.appendTo('body');
// ensure the function exists before calling it...
if (typeof updateModel == 'function') {
updateModel();
var elementToBind = $("#widget-details")[0];
ko.cleanNode(elementToBind);
ko.applyBindings(viewModel, elementToBind);
}
})
.fail(function () {
$.notify("There was an error when retrieving the record.", "error");
});
and when I save, I call this code:
// ensure the function exists before calling it...
if (typeof onBeforeSave == 'function') {
onBeforeSave();
}
Works really well.
I have implemented image upload but can't find a way to display some animated gif image while uploading files. Here what I got so far:
<form method="post" action="/Images/Upload" enctype="multipart/form-data">
<input type="file" multiple name="ImageUploaded">
<input type="submit">
</form>
[HttpPost]
public ActionResult Upload()
{
for (int i = 0; i < Request.Files.Count; i++)
{
HttpPostedFileBase hpf = Request.Files[i] as HttpPostedFileBase;
if (hpf.ContentLength == 0)
continue;
string savedFileNameThumb = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"Content", "Images", "Thumb",
Path.GetFileName(hpf.FileName));
string savedFileName = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"Content", "Images", "Full",
Path.GetFileName(hpf.FileName));
ImageModel.ResizeAndSave(savedFileNameThumb, hpf.FileName, hpf.InputStream, 80, true);
ImageModel.ResizeAndSave(savedFileName, hpf.FileName, hpf.InputStream, int.MaxValue, false);
}
return View();
}
I added now jquery form plugin and it works. Selected images are uploaded I show/hide preloader image.
I just still need to return view or uploaded image to display it after upload finish...
I return view from controller but nothing happens after upload.
$(function () {
$("#Form").ajaxForm({
iframe: true,
dataType: "html",
url: "/Images/Upload",
success: function (result) {
$('#loadingGif').remove();
},
beforeSubmit: function () {
$('<img id="loadingGif" src="../../Content/loader.gif" />').appendTo('body');
},
error: function (response) {
alert(response);
$('#loadingGif').remove();
}
});
});
you can use jQuery to post the form asynchronously and display an animated gif while you wait for the call to return.
$('form').submit(function () {
$(this).before('<img src="loader.gif" alt="Loading..." />');
// code to submit the form
return false;
});
EDIT:
When the view is returned in the success handler, if you e.g. return an <img> tag with the url of the uploaded image, you can use jQuery to display it:
$('body').append(result);