In a fluid template I have a start date and an end date. How do I get the dates in between so I have a list of all dates?
<f:format.date format="%d">{newsItem.datetime}</f:format.date>
<f:format.date format="%d">{newsItem.eventEnd}</f:format.date>
There is no out of the box solution for this but you can write your own viewhelper which takes these two DateTime objects and returns a DatePeriod object which you can conveniently iterate with <f:for> in your template. For every iteration you get a DateTime object again which you can format as usual.
For this you can create your own ViewHelper and in that you need to pass start and end dates and it will return DatePeriod object which you can iterate using for loop (<f:for>) in Fluid template.
You can try something like :
Use ViewHelper in Fluid Template as :
{namespace vh=Vendor\ExtensionKey\ViewHelpers}
<f:for each="{vh:DateRange(startdate:'{starttime}', enddate:'{endtime}')}" as="dates">
<f:format.date format="%d.%m.%Y">{dates}</f:format.date> <br/>
</f:for>
ViewHelper Class :
<?php
namespace Vendor\ExtensionKey\ViewHelpers;
/**
* Date Range ViewHelper
*/
class DateRangeViewHelper extends \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper
{
/**
* #return void
*/
public function initializeArguments()
{
parent::initializeArguments();
$this->registerArgument('startdate', 'string', 'start date', true);
$this->registerArgument('enddate', 'string', 'end date', true);
}
/**
* #return \DatePeriod $dateRange
*/
public function render()
{
$startdate = new \DateTime($this->arguments['startdate']);
$enddate = new \DateTime($this->arguments['enddate']);
$interval = new \DateInterval('P1D'); // 1 Day
$dateRange = new \DatePeriod($startdate, $interval, $enddate);
return $dateRange;
}
}
References :
https://docs.typo3.org/typo3cms/ExtbaseGuide/Fluid/ViewHelper/For.html
http://php.net/manual/en/class.dateperiod.php
Hope this help you!
Related
I upgraded a website from TYPO3 v7 to v9 and now I get the following error:
Undeclared arguments passed to ViewHelper \ViewHelpers\MyViewHelper: value, list. Valid arguments are: [...]
My current ViewHelper looks as follows:
<?php
namespace VP\News\ViewHelpers;
/**
* #package TYPO3
* #subpackage Fluid
*/
class InListViewHelper extends \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper {
/**
* #param mixed $value value
* #param mixed $list list
* #return boolean
*/
public function render($value, $list) {
if (!is_array($list)) {
$list = str_replace(' ', '', $list);
$list = explode(',', $list);
}
return in_array($value, $list);
}
}
Some things have changed between v7 and v9 ViewHelpers in TYPO3 Fluid.
➊ You should extend from the abstract class TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper.
➋ You have to register the arguments you pass to the ViewHelper.
➌ Your ViewHelper looks more like a Condition-ViewHelper than an Abstract-ViewHelper.
The first point is self-explanatory. Simply change the name of the base class (the fully qualified class name). For the second point, you can use an additional method initializeArguments(). For example:
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('value', 'type', 'description');
...
}
You can find an example here.
However, your ViewHelper seems to check a condition ("is this element in a list?", "then...", "else..."). Therefore, it might be better to implement a Condition-ViewHelper.
This type of ViewHelper extends the class TYPO3Fluid\Fluid\Core\ViewHelper\AbstractConditionViewHelper and evaluates a condition using a method verdict() instead of render() or renderStatic().
You can find an example of a simple Condition-ViewHelper here.
In my flux content template configuration section I define an image field like this:
<flux:field.inline.fal label="Image" name="images" maxItems="1" minItems="0" showThumbs="1"/>
In my flux content template main section I debug the output:
<f:for each="{v:resource.record.fal(table: 'tt_content',field: 'images', uid: '{record.uid}')}" as="image" iteration="iterator">
<f:debug>{image}</f:debug>
</f:for>
The debuging output shows an array but what I need is the FAL object of that image I added in the backend.
I googled a lot and found some older posts from 2015. All say it is not possible to get the fal object(!) in flux. Is it still true? Do you know any way?
One solution is to create a custom ViewHelper:
<?php
namespace Cmichael\Typo3projectprovider\ViewHelpers\Content;
/* FalViewHelper
* To get fal object by image uid (respectivly in flux templates)
* Usage example: <cm:content.fal referenceUid="{image.uid}">
* */
class FalViewHelper extends \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper {
/**
* #var bool
*/
protected $escapeOutput = false;
/**
* Initialize arguments.
*
*/
public function initializeArguments() {
$this->registerArgument('referenceUid', 'integer', 'File reference uid', true, 0);
}
/**
* Return file reference
*
* #return \TYPO3\CMS\Core\Resource\FileReference|null
*/
public function render() {
$referenceUid = $this->arguments['referenceUid'];
$fileReferenceData = $GLOBALS['TSFE']->sys_page->checkRecord('sys_file_reference', $referenceUid);
return $fileReferenceData ? \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->getFileReferenceObject($referenceUid) : $referenceUid;
}
}
what is the correct way to dynamically create new Child Elements in a Fluid Form using JavaScript?
Problem:
1:n Relation (Parent/Child) using Extbase ObjectStorages:
When the Parent Fluid Form is called it should be possible to add several childs (incl. properties of course!)
Dirty, partly working, Solution:
I added some JS Code and added the required input elements dynamically.
The "xxx" will be interated for each Child. The data will be correctly stored in the DB.
<input type="text" placeholder="First Name" name="tx_booking[newBooking][accompanyingperson][xxx][firstname]">
However, if an error occurres all child forms disappear and no f3-form-error will be shown. The reason for this, may be the redirect to originalRequest (initial form without child fields).
How can I handle this Problem without dirty tricks?
Please give me shirt hint.
Again, I will answer the question myself!
The following lines are the foulest code ever but it works.
I really want to know how to do this in a correct way. However, the solution is, to get the Arguments from the dynamically added JS Inputs. This is done in the errorAction and will be passed by the forward() to the initial Action, where the Errors should be appear.
I for all think, that must be a better way by using the PropertyMapper and modify the trustedProperties....
Here a short example:
// Error function in Controller
protected function errorAction() {
$referringRequest = $this->request->getReferringRequest();
// Manual added JS Data
if($this->request->hasArgument('newRegistration'))
{
$newRegistration = $this->request->getArgument('newRegistration');
$referringRequest->setArgument('accompanyingperson', $newRegistration['accompanyingperson']);
}
if ($referringRequest !== NULL) {
$originalRequest = clone $this->request;
$this->request->setOriginalRequest($originalRequest);
$this->request->setOriginalRequestMappingResults($this->arguments->getValidationResults());
$this->forward($referringRequest->getControllerActionName(), $referringRequest->getControllerName(), $referringRequest->getControllerExtensionName(), $referringRequest->getArguments());
}
}
// action new in Controller
public function newAction(\***\***\Domain\Model\Registration $newRegistration = NULL) {
if($this->request->hasArgument('accompanyingperson'))
{
$this->view->assign('accPer', $this->request->getArgument('accompanyingperson'));
}
.
.
.
}
//Fluid template of Action New
<f:if condition="{accPer}">
<f:for each="{accPer}" as="ap" key="key" iteration="i">
<f:form.textfield class="form-control" placeholder="First Name" property="accompanyingperson.{key}.firstname"/>
.
.
.
</f:for>
</f:if>
Following is my solution, something like yours.
Models
class Resume extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity {
/**
* #var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<Builder>
* #cascade remove
*/
protected $builders;
}
class Builder extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity {
/**
* #var string
*/
protected $title;
}
Controller
class ResumeController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController {
/**
* #var \Dagou\Resume\Domain\Repository\ResumeRepository
* #inject
*/
protected $resumeRepository;
/**
* #param \Dagou\Resume\Domain\Model\Resume $resume
* #see \Dagou\Resume\Controller\ResumeController::saveAction()
*/
protected function createAction(\Dagou\Resume\Domain\Model\Resume $resume = NULL) {
$this->view->assignMultiple([
'resume' => $resume,
]);
}
protected function initializeCreateAction() {
if (($request = $this->request->getOriginalRequest())) {
$this->request->setArgument('resume', $request->getArgument('resume'));
$propertyMappingConfiguration = $this->arguments->getArgument('resume')->getPropertyMappingConfiguration();
$propertyMappingConfiguration->allowCreationForSubProperty('builders.*');
$propertyMappingConfiguration->allowProperties('builders')
->forProperty('builders')->allowAllProperties()
->forProperty('*')->allowAllProperties();
}
}
protected function initializeSaveAction() {
$propertyMappingConfiguration = $this->arguments->getArgument('resume')->getPropertyMappingConfiguration();
$propertyMappingConfiguration->allowCreationForSubProperty('builders.*');
$propertyMappingConfiguration->allowProperties('builders')
->forProperty('builders')->allowAllProperties()
->forProperty('*')->allowAllProperties();
}
}
/**
* #param \Dagou\Resume\Domain\Model\Resume $resume
*/
protected function saveAction(\Dagou\Resume\Domain\Model\Resume $resume) {
$this->resumeRepository->add($resume);
}
}
Template
<f:form class="form-horizontal" name="resume" action="save" object="{resume}">
<f:if condition="{resume.builders}">
<f:for each="{resume.builders}" as="builder" iteration="builderIteration">
<f:form.textfield class="form-control" property="builders.{builderIteration.index}.header" />
</f:for>
</f:if>
</f:form>
If you have a better one, please let me know. Thanks!
I have an entity Calendar with dateFrom and dateTo properties.
Now in my form I have one hidden input with date formatted like this: 2010-01-01,2011-01-01.
How can I write a data transformer in Symfony2 which will allow me to transform this date to TWO properties?
I think that the transformer himself has nothing to do with the "properties", it just handle transformation from a data structure to another data structure. You just have to handle the new data structure on your code base.
The transformer himself might look like this :
class DateRangeArrayToDateRangeStringTransformer implements DataTransformerInterface
{
/**
* Transforms an array of \DateTime instances to a string of dates.
*
* #param array|null $dates
* #return string
*/
public function transform($dates)
{
if (null === $dates) {
return '';
}
$datesStr = $dates['from']->format('Y-m-d').','.$dates['to']->format('Y-m-d');
return $datesStr;
}
/**
* Transforms a string of dates to an array of \DateTime instances.
*
* #param string $datesStr
* #return array
*/
public function reverseTransform($datesStr)
{
$dates = array();
$datesStrParts = explode(',', $datesStr);
return array(
'from' => new \DateTime($datesStrParts[1]),
'to' => new \DateTime($datesStrParts[2])
);
}
}
You can use the explode function like that :
$dates = explode(",", "2010-01-01,2011-01-01");
echo $dates[0]; // 2010-01-01
echo $dates[1]; // 2011-01-01
Then create two new DateTime.
If it's possible, use 2 hidden fields. Then use a DateTime to String datatransformer on each field. Then your form is logically mapped to your entity.
I solved a similar problem by adding a custom getter/setter to my entity (for example, getDateIntervalString and setDateIntervalString). The getter converts dateTo and dateFrom into the interval string and returns it, and the setter accepts a similarly formatted string and uses it to set dateTo and dateFrom. Then, add the field to the form like this:
$builder->add('dates', 'text', ['property_path' => 'date_interval_string'])
By overriding the property path, your custom getter and setter will be used.
I have 3 entity (Country, Region, City)
namespace ****\****Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
class Country
{
private $id;
private $name;
/**
* #var integer $regions
*
* #ORM\OneToMany(targetEntity="Region", mappedBy="Country")
*/
protected $regions;
//...
}
class Region
{
private $id;
private $name;
/**
* #var integer $country
*
* #Assert\Type(type="****\***Bundle\Entity\Country")
* #ORM\ManyToOne(targetEntity="Country", inversedBy="regions")
* #ORM\JoinColumn(name="country_id", referencedColumnName="id", nullable=false)
*/
private $country;
/**
* #ORM\OneToMany(targetEntity="City", mappedBy="Region")
*/
protected $cities;
}
class City
{
private $id;
private $name;
/**
* #var integer $region
*
* #Assert\Type(type="****\****Bundle\Entity\Region")
* #ORM\ManyToOne(targetEntity="Region", inversedBy="cities")
* #ORM\JoinColumn(name="region_id", referencedColumnName="id", nullable=false)
*/
private $region;
/**
* #ORM\OneToMany(targetEntity="Company", mappedBy="City")
*/
protected $companys;
//...
}
Here is my Form Class for City:
namespace ****\****Bundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('region');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => '****\****Bundle\Entity\City',
));
}
public function getName()
{
return 'city';
}
}
This make a basic HTML5 Form with a textBox for the name and a SelectBox where all the region is available.
My question is what is the best way for adding a first SelectBox that will allow me to select the country in order to filter the second SelectBox and decrease the number of choice of Region?
EventListener ?
The Event Dispatcher Component ?
NO, EventListener and Event dispatcher are for events that happen ON THE SERVER, not on the client side. You need to use Javascript. When one of the select boxes changes, this should fire a javascript function and either make an AJAX call and fill the other select box with the results of that call or use some javascript code to select which options to show on the second box.
Look here for some ideas
As Carlos Granados said you have basically two options:
Create a separate Symfony actions that takes a country as parameter and returns a list of associated regions in XML or JSON format. (You can use Symfony\Component\HttpFoundation\JsonResponseto send a JSON response, however, there is no corresponding XmlResponse class). Use jQuery (or any other JS library or even plain Javascript) to send a request to the server whenever a user changes the currently selected item. In the callback (when you retrieved the response in the Javascript) you can update the region select box. You may find the documentation of jQuery Ajax interesting.
You store a list of all countries and its associated regions in your HTML code (you can use JSON or generate a native Javascript array) and whenever a user changes the value of the country select box you just have to replace the list of regions in the regions select box.
The second method has a heavier load on the initial loading of the form since all countries and its associated regions must be loaded (from the database or a text file or wherever you store them) and rendered in a format that can be easily read by JS.
The first method however must send a request every time a user selects another country. Additionally you have to implement another action.
I'm doing this myself on a form.
I change a field (a product) and the units in which the quantity can be measured are updated.
I am using a macro with parameters to adapt it more easily.
The macro :
{% macro javascript_filter_unit(event, selector) %}
<script>
$(function(){
$('#usersection')
.on('{{ event }}', '{{ selector }}', function(e){
e.preventDefault();
if (!$(this).val()) return;
$.ajax({
$parent: $(this).closest('.child_collection'),
url: $(this).attr('data-url'),
type: "get",
dataType: "json",
data: {'id' : $(this).val(), 'repo': $(this).attr('data-repo'), parameter: $(this).attr('data-parameter')},
success: function (result) {
if (result['success'])
{
var units = result['units'];
this.$parent.find('.unit').eq(0).html(units);
}
}
});
})
});
</script>
{% endmacro %}
The ajax returns an array : array('success' => $value, 'units' => $html). You use the $html code and put it in place of the select you want to change.
Of course the javascript code of the ajax call need to be modfied to match your fields.
You call the macro like you would normally do:
{% import ':Model/Macros:_macros.html.twig' as macros %}
{{ macros.javascript_filter_unit('change', '.unitTrigger') }}
So I have two arguments : the event, often a change of a select. and a selector, the one whose change triggers the ajax call.
I hope that helps.
As Carlos Granados said, you have to use client-side programming:Javascript.
I run also into the same problem as yours and came out with a solution that worked perfectly for me, here is the snippet on codepen, you can get inspired from (Cascade Ajax Selects)
//-------------------------------SELECT CASCADING-------------------------//
var currentCities=[];
// This is a demo API key that can only be used for a short period of time, and will be unavailable soon. You should rather request your API key (free) from http://battuta.medunes.net/
var BATTUTA_KEY="00000000000000000000000000000000"
// Populate country select box from battuta API
url="http://battuta.medunes.net/api/country/all/?key="+BATTUTA_KEY+"&callback=?";
$.getJSON(url,function(countries)
{
console.log(countries);
$('#country').material_select();
//loop through countries..
$.each(countries,function(key,country)
{
$("<option></option>")
.attr("value",country.code)
.append(country.name)
.appendTo($("#country"));
});
// trigger "change" to fire the #state section update process
$("#country").material_select('update');
$("#country").trigger("change");
});
$("#country").on("change",function()
{
countryCode=$("#country").val();
// Populate country select box from battuta API
url="http://battuta.medunes.net/api/region/"
+countryCode
+"/all/?key="+BATTUTA_KEY+"&callback=?";
$.getJSON(url,function(regions)
{
$("#region option").remove();
//loop through regions..
$.each(regions,function(key,region)
{
$("<option></option>")
.attr("value",region.region)
.append(region.region)
.appendTo($("#region"));
});
// trigger "change" to fire the #state section update process
$("#region").material_select('update');
$("#region").trigger("change");
});
});
$("#region").on("change",function()
{
// Populate country select box from battuta API
countryCode=$("#country").val();
region=$("#region").val();
url="http://battuta.medunes.net/api/city/"
+countryCode
+"/search/?region="
+region
+"&key="
+BATTUTA_KEY
+"&callback=?";
$.getJSON(url,function(cities)
{
currentCities=cities;
var i=0;
$("#city option").remove();
//loop through regions..
$.each(cities,function(key,city)
{
$("<option></option>")
.attr("value",i++)
.append(city.city)
.appendTo($("#city"));
});
// trigger "change" to fire the #state section update process
$("#city").material_select('update');
$("#city").trigger("change");
});
});
$("#city").on("change",function()
{
currentIndex=$("#city").val();
currentCity=currentCities[currentIndex];
city=currentCity.city;
region=currentCity.region;
country=currentCity.country;
lat=currentCity.latitude;
lng=currentCity.longitude;
$("#location").html('<i class="fa fa-map-marker"></i> <strong> '+city+"/"+region+"</strong>("+lat+","+lng+")");
});
//-------------------------------END OF SELECT CASCADING-------------------------//