I'm using Jquery-File-Upload for my upload page, and I'm having a problem to add extra upload fields.
I'm following that page: https://github.com/blueimp/jQuery-File-Upload/wiki/How-to-submit-additional-form-data
It works well for 1 file submission.
However, with multiple files submission we are starting to see issues, because files are uploaded 1 file / POST (singleFileUploads: true).
The code I'm using as reference is the following:
<script id="template-upload" type="text/x-tmpl">
{% for (var i=0, file; file=o.files[i]; i++) { %}
<tr class="template-upload fade">
<!-- ... -->
<td class="title"><label>Title: <input name="title[]" required></label></td>
<!-- ... -->
</tr>
{% } %}
</script>
If you submit that with 2 files, then you get 2 POSTS:
1/
$_REQUEST:
(
title:
(
[0] -> Title1
[1] -> Title2
)
)
$_FILES:
(
[0] -> ( 'name' => 'file name 1', ... )
)
2/
$_REQUEST:
(
title:
(
[0] -> Title1
[1] -> Title2
)
)
$_FILES:
(
[0] -> ( 'name' => 'file name 2', ... )
)
Then, on php side, the function handle_form_data relies on file index
<?php
// ...
protected function handle_form_data($file, $index) {
// Handle form data, e.g. $_REQUEST['description'][$index]
}
// ...
The problem is that index is always 0, because we're uploading 1 file / post. Now you see that since the $_REQUEST uploads all extra fields from all files (no matter what is it the current file), the index from $_FILES gets de-synchronized from the extra fields array.
Do you know any workaround, except turning singleFileUploads to OFF?
Ok, I will answer myself.
First, we will attribute an id to the files as soon as they are added to the UI. We maintain an incremental index for that:
//global
var current_file_index = 0;
Next, we need to play with fileuploadadd callback to add that index to the files:
$('#fileupload').bind('fileuploadadd', function (e, data) {
for (var i = 0; i < data.files.length; i++) {
data.files[i].index = current_file_index++;
}
});
That index is now accessible when adding the files on UI side. We don't want the custom input to be added to the form, so change the name by an id (so that it will not be submitted). And add the brand new index as part of that ID:
<script id="template-upload" type="text/x-tmpl">
{% for (var i=0, file; file=o.files[i]; i++) { %}
<tr class="template-upload fade">
<!-- ... -->
<td class="title"><label>Title: <input id="title_{%=file.index%}" required></label></td>
<!-- ... -->
</tr>
{% } %}
</script>
Then, when we submit the file(s), we want to add the result of that input to the formData. We don't care about sending to much data, so we basically send the whole files array as JSON string:
$('#fileupload').bind('fileuploadsubmit', function (e, data) {
for (var i = 0; i < data.files.length; i++) {
var title = $("title_ + data.files[i].index.toString()").val();
data.files[i].title = title;
}
data.formData = {
files_data: JSON.stringify(data.files)
}
});
Don't forget the get the data back on server side in $_REQUEST["files_data"], and explode the json that now contains only 1 file's data).
I know, it's too late, but i use easiest way:
<input type="text" name="{%=file.name%}[title]">
<input type="text" name="{%=file.name%}[description]">
Then you get convenient data array for each file by filename, like this:
print_r($_POST);
/* will be:
* Array
* (
* [file1_jpg] => Array
* (
* [title] => title 1
* [description] => description 1
* )
*
* [file2_jpg] => Array
* (
* [title] => title 2
* [description] => description 2
* )
*
* [file3_png] => Array
* (
* [title] => title 3
* [description] => description 3
* )
*
* )
*/
But remember: all dots in $_POST keys will be replaced to underscores! This is normal. Just remember it.
Late to answer as well, but I just got done working through the same issue.
There is a key part of the documentation linked above that the OP didn't implement:
$('#fileupload').bind('fileuploadsubmit', function (e, data) {
var inputs = data.context.find(':input');
if (inputs.filter(function () {
return !this.value && $(this).prop('required');
}).first().focus().length) {
data.context.find('button').prop('disabled', false);
return false;
}
data.formData = inputs.serializeArray();
});
With this added, the "closest" value for the input field is added to the formData array that is submitted with the form.
More details at: https://github.com/blueimp/jQuery-File-Upload/wiki/How-to-submit-additional-form-data#setting-formdata-on-upload-start-for-each-individual-file-upload
Related
I am using TYPO3 10.4.15
My edit view:
f:section name="content">
<h1>Edit Album</h1>
<f:flashMessages />
<f:render partial="FormErrors" />
<f:form id='fNew' action="update" name="album" object="{album}" arguments="{mode:mode, disc:disc}" >
<f:render partial="Album/FormFields" arguments="{album:album, disc:disc}" />
<f:form.submit value="Save" />
</f:form>
</f:section>
</html>
This is the relevant part of the partial formfields.html:
<f:if condition='{disc}'>
<input type='text' name="disc[0][name][]" />
</f:if>
The error_log with the disc structure looks:
Update-Disc: array (
0 =>
array (
'name' => '',
'trackNum' => '1',
'track' =>
array (
0 =>
array (
'title' => '',
'duration' => '0',
'composer' => '',
'texter' => '',
'musicFile' => '',
'imageFile' => '',
),
),
),
)
And this is the "updateAction" part of the controller
/**
* action update
*
* #param \HGA\Album\Domain\Model\Album $album
* #param string $mode
* #param array $disc
* #return string|object|null|void
*/
public function updateAction(\HGA\Album\Domain\Model\Album $album, $mode, $disc)
{
error_log("Update-Disc: " . var_export($disc, true) . " Mode: " . $mode, 0);
if ($mode == 'tracks') {
$this->editAction($album, $mode, $disc);
}
error_log("Update: " . var_export($album, true) . " Mode: " . $mode, 0);
$this->addFlashMessage('The object was updated. Please be aware that this action is publicly accessible unless you implement an access check. See https://docs.typo3.org/typo3cms/extensions/extension_builder/User/Index.html', '', \TYPO3\CMS\Core\Messaging\AbstractMessage::WARNING);
$this->albumRepository->update($album);
$this->redirect('list');
}
If I write something into the text input field and execute submit, I get the error_log you can see above. The value I have typed in the input field is missing. It is only the array, as I have send it to the view.
The mode string will be transmitted correctly, but with the disc array is maybe something wrong!
The disc array is more complex, but I made it simple, because I need to understand how it works in general.
I also need this additional disc array and can not doing it with the album object!
Thanks in advance for your help.
You are ignoring your plugin's namespace in combination with misinterpretation of f:form arguments.
Each field for your plugin has a prefix like tx_hgaalbum... followed by your property's name in square brackets. So the fieldname for disc should look like tx_hgaalbum...[disc]. Have a look into the HTML code and see which names are generated for the other properties.
The second problem is using the arguments in the form-ViewHelper. This will only add the arguments to the action URI of your form. That's why you're getting your initial values for disc.
I need to show a selection of days in an event in the frontend:
in my TCA I set the field like this:
'days' => [
'exclude' => true,
'label' => 'choose weekdays',
'config' => [
'type' => 'check',
'eval' => 'required,unique',
'items' => [
['monday',''],
['thuesday',''],
['wednesday',''],
['thursday',''],
['friday',''],
['saturday',''],
['sunday',''],
],
'cols' => 'inline',
],
],
That stores an integer in the db, but now I have to display the selected days in a fluid template in the frontend.
This is the reference regarding in the TYPO3 documentation which explains that I should check the bit-0 of values ... I've searched a lot but couldn't find anything except this question here on stack overflow, which I cannot get to work.
I strongly recommend not to use the bitmasking feature of the check field. It's rarely worth the overhead to split the values apart again and also is a lot harder to understand for most developers.
Instead you can use a select field, in this case selectCheckBox should serve you well. Given a static list of items you will get a CSV string with the selected values which is a lot easier to split, e.g. in a getter method of an Extbase domain model. If it makes sense you can even use a relation to records instead which is even cleaner but requires additional work.
If you still want to continue with bitmasks this answer may help you.
SOLUTION 1: using Mathias's solution mixed with the one of Dimitri L.
I wanted to give it here as a full solution to this particular question, so add this in the domain model:
/**
* #var int
*/
protected $days;
and then following for all the days:
/**
* Get day 1
*
* #return int
*/
public function getDay1()
{
return $this->days & 0b00000001 ? 1 : 0;
}
/**
* Set day 1
*
* #param int $day1
*/
public function setDay1($day1) {
if ($day1) {
$this->days |= 0b00000001;
} else {
$this->days &= ~0b00000001;
}
}
/**
* And so on for the other 7 days
*/
You can now use it in extbase $object->getDay1() or in fluid {object.day1}
As Mathias stated, it quickly gets very complicated, I preferred this solution since I use it only to display the days an event takes place in a week, and in a calendar so a 0 or 1 solution was just fine.
SOLUTION 2: I ended up using the decimal bitmask value from the database directly in a viewhelper: (solution is adepted for the number of checkboxes used, in my case the 7 weekdays)
use \TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* News extension
*
* #package TYPO3
* #subpackage tx_news
*/
class CoursedaysViewHelper extends \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper
{
/**
* #param string $days (bitmask)
* #return string checked weekdays seperated by /
*/
public function render($days)
{
// render binary, 7 digits, split into array and reverse
$days = decbin($days);
$days = sprintf('%07d', $days);
$days = str_split($days);
$days = array_reverse($days);
foreach($days as $day){
$key = 'days.' . ++$a;
if($day) $coursedays .= LocalizationUtility::translate($key, 'news_ext') . '/';
}
return substr($coursedays, 0, -1);
}
}
A possible solution for multiple checkboxes by evaluating the bitmask
Programmers often want to read some data into a form and then output it as text. There are a few examples of this here.
Sometimes programmers want to display the form data in the same form with multiple checkboxes so that the user can change the data. There are no examples of this and many programmers find it difficult to read the data bit by bit and then output it again.
Here is a working example (in BE and FE):
(Tested with Typo3 9.5.20 and 10.4.9)
In TCA the example of the question:
'days' => [
'exclude' => false,
'label' => 'LLL:EXT:example/Resources/Private/Language/locallang_db.xlf:tx_example_domain_model_week.days',
'config' => [
'type' => 'check',
'items' => [
['monday', ''],
['thuesday', ''],
['wednesday', ''],
['thursday', ''],
['friday', ''],
['saturday', ''],
['sunday', ''],
],
'default' => 0,
]
],
The model:
The type of the property must be integer.
However, getters and setters are arrays because we have a multiple checkbox and this is implemented with an array.
It is important to keep this in mind as it will create a problem that needs to be resolved.
class Week extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
/**
* Days
*
* #var int
*/
protected $days = 0;
/**
* Returns the days
*
* #return array $days
*/
public function getDays()
{
return $this->days;
}
/**
* Sets the days
*
* #param array $days
* #return void
*/
public function setDays($days)
{
$this->days = $days;
}
}
In controller
In the initializeCreateAction and the initializeUpdateAction we solve the problem of the different property types between integer and arrays.
Without this, we receive an error message that the array cannot be converted to an integer.
This code means that Extbase should keep the property type.
In the createAction and the updateAction we branch to the method countBits in CheckboxUtility to add the values of the selected checkboxes.
In the editAction and the updateAction we branch to the method convertDataForMultipleCheckboxes in CheckboxUtility in order to convert the values to be input and output.
/**
* initializeCreateAction
* #return void
*/
public function initializeCreateAction(): void
{
if ($this->arguments->hasArgument('newWeek')) {
$this->arguments->getArgument('newWeek')->getPropertyMappingConfiguration()->setTargetTypeForSubProperty('days', 'array');
}
}
/**
* action create
*
* #param Week $newWeek
* #return void
*/
public function createAction(Week $newWeek)
{
$days = (int)CheckboxUtility::countBits($newWeek->getDays());
$newWeek->setDays($days);
$this->weekRepository->add($newWeek);
$this->redirect('list');
}
/**
* action edit
*
* #param Week $week
* #return void
*/
public function editAction(Week $week)
{
$week->setDays(CheckboxUtility::convertDataForMultipleCheckboxes((int)$week->getDays()));
$this->view->assign('week', $week);
}
/**
* initializeUpdateAction
* #return void
*/
public function initializeUpdateAction(): void
{
if ($this->arguments->hasArgument('week')) {
$this->arguments->getArgument('week')->getPropertyMappingConfiguration()->setTargetTypeForSubProperty('days', 'array');
}
}
/**
* action update
*
* #param Week $week
* #return void
*/
public function updateAction(Week $week)
{
$days = (int)CheckboxUtility::countBits($week->getDays());
$week->setDays($days);
$this->weekRepository->update($week);
$this->redirect('list');
}
In Classes/Utility/CheckboxUtility.php
Read the code. The procedure is described at each point.
In method convertDataForMultipleCheckboxes the basic direction is as follows:
We have an integer value in the database, e.g. 109.
In binary notation: 1011011 (64 + 32 + 0 + 8 + 4 + 0 + 1 = 109)
In the form, this means that the first, third, fourth, sixth and seventh checkboxes are selected.
We read the binary value from left to right, at 1011011 in seven loops.
For example, let's read the first character (from the left) we overwrite the six characters on the right with 0. This results in the binary number 1000000, in decimal notation = 64.
For example, let's read the fourth character (from the left) we overwrite the three characters on the right with 0. This results in the binary number 1000, in decimal notation = 8.
When we have read this, we will get the result 64 + 32 + 0 + 8 + 4 + 0 + 1 because we read from left to right.
Therefore we turn the result around at the end so that each checkbox receives the correct value!
So we get this 1 + 0 + 4 + 8 + 0 + 32 + 64 because the first, third, fourth, sixth and seventh checkboxes are selected.
In method countBits we just add all integer values to one number.
namespace Vendor\Example\Utility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class CheckboxUtility extends GeneralUtility
{
/**
* Convert an integer to binary and then convert each bit back to an integer for use with multiple checkboxes.
*
* #param int $value
* #return array
*/
public static function convertDataForMultipleCheckboxes(int $value): array
{
$bin = decbin($value); // convert dec to bin
$num = strlen($bin); // counts the bits
$res = array();
for ($i = 0; $i < $num; $i++) {
// loop through binary value
if ($bin[$i] !== 0) {
$bin_2 = str_pad($bin[$i], $num - $i, '0'); //pad string
$res[] = bindec($bin_2); // convert that bit to dec and push in array
}
}
return array_reverse($res); // reverse order and return
}
/**
* Adds the values of the checkboxes
*
* #param array $value
* #return int
*/
public static function countBits(array $value): int
{
foreach ($value as $key => $item) {
$res = $res + $item;
}
return $res;
}
}
In Templates or Partials
The argument multiple="1" is important here. This adds an additional dimension to the array of property days. (This can be seen in the website's source code).
It is important that we give the checkbox the correct value according to the binary notation.
When we have read the values from the database, the result is available to us as an array. So we read the additional dimension at the appropriate place (starting with 0) in the same order as the order of the checkboxes. e.g. the seventh value / checkbox: checked = "{week.days.6} == 64"
<f:form.checkbox
id="day_1"
property="days"
value="1"
multiple="1"
checked="{week.days.0} == 1" />
<label for="day_1" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day1" />
</label>
<f:form.checkbox
id="day_2"
property="days"
value="2"
multiple="1"
checked="{week.days.1} == 2" />
<label for="day_2" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day2" />
</label>
<f:form.checkbox
id="day_3"
property="days"
value="4"
multiple="1"
checked="{week.days.2} == 4" />
<label for="day_3" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day3" />
</label>
<f:form.checkbox
id="day_4"
property="days"
value="8"
multiple="1"
checked="{week.days.3} == 8" />
<label for="day_4" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day4" />
</label>
<f:form.checkbox
id="day_5"
property="days"
value="16"
multiple="1"
checked="{week.days.4} == 16" />
<label for="day_5" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day5" />
</label>
<f:form.checkbox
id="day_6"
property="days"
value="32"
multiple="1"
checked="{week.days.5} == 32" />
<label for="day_6" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day6" />
</label>
<f:form.checkbox
id="day_7"
property="days"
value="64"
multiple="1"
checked="{week.days.6} == 64" />
<label for="day_7" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day7" />
</label>
... and now happy coding!
When run I this query ... there is no show action in the links in my list view ... and they do not work anymore:
Controller.php
$uid = 10;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$statement = $queryBuilder
->select('uid', 'pid', 'header')
->from('tt_content')
->where(
$queryBuilder->expr()->eq('uid', $uid)
)
->execute();
while ($row = $statement->fetchAll()) {
$this->view->assign('inet', $row);
}
Console:
<tr id="2">
<td>2</td>
<td class="name">Company Name Inc</td>
</tr>
When I debug it I can see that I render the following array:
array(20 items)
0 => array(3 items)
uid => 1 (integer)
code => '213800' (6 chars)
name => 'Company Name Inc' (16 chars)
How can I get the links to work again?
change
<f:link.action action="show" pageUid="43" arguments="{record:record}">{record.name}</f:link.action>
to
<f:link.action action="show" pageUid="43" arguments="{record:record.uid}">{record.name}</f:link.action>
because you ain't getting Objects anymore but arrays of database rows.
Additionally your listAction is missing the parameters $minUid and $maxUid.
I built form with GET method but when i submit form empty field also pass to url, can i exclude empty field from passing to url ?
For example > when i submit my form url changed to :
?jobTitle=Title&jobCompany=CompanyName&jobGovernorate=&jobLocation=&postingDate=ad
Here in this example jobGovernorate and jobLocation is empty so i want form skip those when i submit the form.
If there's a way to get url like this
?jobTitle=Title&jobCompany=CompanyName&postingDate=ad
Because jobGovernorate and jobLocation is empty
Sorry for poor english, Thank you.
You can use middleware for your problem
class StripEmptyParams
{
public function handle($request, Closure $next)
{
$query = request()->query();
$querycount = count($query);
foreach ($query as $key => $value) {
if ($value == '') {
unset($query[$key]);
}
}
if ($querycount > count($query)) {
$path = url()->current() . (!empty($query) ? '/?' . http_build_query($query) : '');
return redirect()->to($path);
}
return $next($request);
}
}
then call for specific route like code below
Route::get('/search','YourController#search')->middleware(StripEmptyParams::class);
Assuming you have a form as below
<form>
<input type="text" class="url_params" name="jobTitle" value="">
<input type="text" class="url_params" name="jobCompany" value="">
<input type="text" class="url_params" name="jobGovernorate" value="">
<input type="text" class="url_params" name="jobLocation" value="">
<input type="text" class="url_params" name="postingDate" value="">
<input type="submit" name="submit" id="submit">
</form>
<script type="text/javascript">
$(document).ready(function () {
$("#submit").on("click", function(e) {
e.preventDefault();
var url = '{{ url('/') }}?';
var total = $(".url_params").length;
$(".url_params").each(function (index) {
if ($(this).val().trim().length) {
if (index === total - 1) {
url += $(this).attr('name') + '=' + $(this).val();
} else {
url += $(this).attr('name') + '=' + $(this).val() + "&";
}
}
});
window.location.href = url;
});
});
</script>
The above code will generate an URL based on the field value and redirect to the url. So it won't generate a url with the empty field value key.
And having an empty field value shouldn't make a difference as you could check for the url values in the controller using $request->input('key')
Hope this helps!
Go through array like this, you will just check if your array has empty, will not add the key.
$data = array('foo'=>'bar',
'baz'=>'boom',
'cow'=>'milk',
'php'=>'hypertext processor');
echo http_build_query($data) . "\n";
//echo http_build_query($data, '', '&'); // only for use & instead & if needed
I have applied the next middleware on a Laravel 8.x project to solve a related problem. This may be helpful to other ones...
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class StripEmptyParamsFromQueryString
{
/**
* Remove parameters with empty value from a query string.
*
* #param \Illuminate\Http\Request $request
* #param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* #return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
// Get the current query and the number of query parameters.
$query = request()->query();
$queryCount = count($query);
// Strip empty query parameters.
foreach ($query as $param => $value) {
if (! isset($value) || $value == '') {
unset($query[$param]);
}
}
// If there were empty query parameters, redirect to a new url with the
// non empty query parameters. Otherwise keep going with the current
// request.
if ($queryCount > count($query)) {
return redirect()->route($request->route()->getName(), $query);
}
return $next($request);
}
}
Note the middleware should only be applied to specific routes, not to all request. In my particular case I have a resource controller and to apply the middleware only to the index route I have used the next approach inside the resource controller:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Middleware\StripEmptyParamsFromQueryString;
class MyController extends Controller
{
/**
* Instantiate a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware(StripEmptyParamsFromQueryString::class)
->only('index');
}
...
}
i generated a multiple upload form with the former generator tool from https://github.com/Anahkiasen/former.
{{ Former::files('file')->accept('image')->required(); }}
that results in
<input multiple="true" accept="image/*" required="true" id="file[]" type="file" name="file[]">
After I've submit the form Ive figured out that Input::hasFile('file') always returns false whilst Input:has('file') returns true.
What i've tried until now:
Log::info(Input::file('file')); <-- empty
foreach(Input::file('file') as $file) <-- Invalid argument supplied for foreach()
Log::info("test");
if(Input::has('file'))
{
if(is_array(Input::get('file')))
foreach ( Input::get('file') as $file)
{
Log::info($file); <-- returns the filename
$validator = Validator::make( array('file' => $file), array('file' => 'required|image'));
if($validator->fails()) {
...
}
}
}
Of course, the validator always fails cause Input::get('file') does not return a file object. How do I have to modify my code to catch the submited files?
Thanks for the help, the answer from Kestutis made it clear. The common way to define a file form in Laravel is
echo Form::open(array('url' => 'foo/bar', 'files' => true))
This Options sets the proper encryption type with enctype='multipart/form-data'.
With the laravel form builder "Former" you have to open the form with
Former::open_for_files()
After that u can validate the form in the common way.
if(Input::hasFile('files')) {
Log::info(Input::File('files'));
$rules = array(
...
);
if(!array(Input::File('files')))
$rules["files"] = 'required|image';
else
for($i=0;$i<count(Input::File('files'));$i++) {
$rules["files.$i"] = 'required|image';
}
$validation = Validator::make(Input::all(), $rules);
if ($validation->fails())
{
return ...
}
else {
// everything is ok ...
}