My first Symfony 5 attempt and I am trying to use Google Charts for the first time. I am using CMENGoogleChartsBundle which provides a Twig extension and PHP objects to display the Charts.
I want to display different Charts which should be accessible through jQuery UI Tabs I also have a table with all the data being displayed. When now clicking on a Tab with a country for example I want the Graph and table updated. I tried to display all the data directly in my twig template (fruitsoverview.html.twig), but when clicking on a tab I would get the page rendered again just below the tabs, plus I would lose any possible set up of my search filter, if the whole page would be re-loaded. I then read that you can create a view just with the content which needs updating so I have done that and in my controller I now have:
if ($request->isXmlHttpRequest())
{
return $this->render('fruits/chart.html.twig', [
'searchFilter' => $searchFilter->createView(),
'fruitCounts' => $fruitCounts,
'barchart' => $barchart
]);
}else{
return $this->render('fruits/fruitsoverview.html.twig', [
'searchFilter' => $searchFilter->createView(),
'fruitCounts' => $fruitCounts,
'barchart' => $barchart
]);
}}
This solved the problem with the rendering, but the chart is not shown. The chart data is available in the twig view but the Graph only gets shown on the default Tab. I do get the table displayed with the correct data. What could be wrong or better question, how is it supposed to be set up correctly. I am sure I have not understood the concept of Symfony yet and probably not of the jQuery ui Tabs. Any help would be highly appreciated. Thank you very much in advance.
UPDATE
I have now updated my code and got partly working besides the barchart. So the twig variables for the table are updating with the new content after the AJAX request, but the barchart does not change. I have done
{{ dump(barchart) }} in the chart.html.twig and I do get the updated data inside of that as well, but the chart is not re-drawn. How can I achieve that?
controller
if ($request->isXmlHttpRequest())
{
$response = new JsonResponse();
$response->setStatusCode(200);
return new JsonResponse([
'html' => $this->renderView('fruits/chart.html.twig', [
'fruitCounts' => $fruitCounts,
'barchart' => $barchart
])
]);
In my main template I have the below code and Javascript. $("div#client-loop-container").html(data.html); seems to update the twig variables in my chart.twig template.
<div id="client-loop-container">
{% include 'chart.html.twig' %}
</div>
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
<script type="text/javascript">
$( function() {
$('#country-tabs a').on('click', function (e) {
e.preventDefault()
$(this).tab('show');
var $this = $(this),
loadurl = $this.attr('href');
var form = $('form');
var jsonData = $.ajax({
url: loadurl,
type: 'POST',
data: form.serializeArray(),
dataType: 'json',
success: function(data, status) {
$("div#client-loop-container").html(data.html);
}
}).responseText;
});
chart.html.twig
<div id="client-loop-container">
<div class="w-100" id="div_chart"></div>
<table class="table">
<tr>
<th>Date</th>
{% for fruitcount in fruitcounts %}
<th>{{ fruitcount }} </th>
{% endfor %}
</tr>
</table>
</div>
{% block javascripts %}
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
{{ gc_draw(barchart, 'div_chart') }}
</script>
{% endblock %}
Here's a possible solution. The code below uses a single <div> for displaying charts but you might be able to adapt to a tab & id="..." solution. The approach uses jQuery.getScript() to run chart-specific javascript. A ChartService uses the cmen/google-charts-bundle to generate the javascript. The script is supplied to the displaying template by a Controller response. There are six different charts that can be displayed. The page displays charts in pseudo-carousel style.
You might ask why go to all this trouble when it's possible to draw all six charts once and use javascript to hide and show one chart at a time. The answer is that charts 2 - n will usually not have all of its options displayed. I developed this solution so that the vertical axis would always show values at grid lines.
ChartService (with chart definitions eliminated for brevity):
namespace App\Service;
use App\Entity\RV;
use App\Entity\Summary;
use CMEN\GoogleChartsBundle\GoogleCharts\Options\VAxis;
use CMEN\GoogleChartsBundle\GoogleCharts\Charts\Histogram;
use CMEN\GoogleChartsBundle\GoogleCharts\Charts\LineChart;
use CMEN\GoogleChartsBundle\Twig\GoogleChartsExtension;
use Doctrine\ORM\EntityManagerInterface;
class ChartService
{
private $em;
private $gce;
public function __construct(EntityManagerInterface $em, GoogleChartsExtension $gce)
{
$this->em = $em;
$this->gce = $gce;
}
public function buildChart($chartType, $class, $subtype = null)
{
switch ($chartType) {
case 'line':
return $this->lineChart($class, $subtype);
break;
case 'histogram':
return $this->histoChart($class);
default:
break;
}
}
private function lineChart($class, $type)
{
...
return $chart;
}
private function histoChart($class)
{
...
return $histo;
}
public function getChartJs($chartSpecs, $location)
{
$chartType = $chartSpecs['type'] ?? null;
$class = $chartSpecs['class'] ?? null;
$subtype = $chartSpecs['subtype'] ?? null;
$chart = $this->buildChart($chartType, $class, $subtype);
$js = $this->getStart($chart, $location);
$end = $this->getEnd($chart, $location);
$part3 = substr($end, 0, strlen($end) - 1);
return $js . $end;
}
private function getStart($chart, $location)
{
return $this->gce->gcStart($chart, $location);
}
private function getEnd($chart, $location)
{
return $this->gce->gcEnd($chart, $location);
}
}
chartSwitch.js
$(document).ready(function () {
var i = $('#currentChart').attr('data-chart');
chartSwitch(i);
$('#chartNext').on('click', function () {
i++;
if (6 === i) {
i = 0;
}
chartSwitch(i);
});
$('#chartPrevious').on('click', function () {
i--;
if (-1 === i) {
i = 5;
}
chartSwitch(i);
});
function chartSwitch(i) {
$('#currentChart').attr('data-chart', i);
var url = "/js/" + i;
$.getScript(url);
}
});
Controller method for /js/ + i
/**
* #Route("/js/{which}", name="js")
*/
public function returnChartJs(ChartService $chart, $which)
{
$available = [
['type' => 'line', 'class' => 'C', 'subtype' => 'Price'],
['type' => 'line', 'class' => 'C', 'subtype' => 'Count'],
['type' => 'line', 'class' => 'B+', 'subtype' => 'Price'],
['type' => 'line', 'class' => 'B+', 'subtype' => 'Count'],
['type' => 'histogram', 'class' => 'C'],
['type' => 'histogram', 'class' => 'B+'],
];
$js = $chart->getChartJs($available[$which], 'chartA');
$response = new Response($js);
return $response;
}
Relevant pieces from displaying template:
<div class="col-9 text-center">
<div class="row">
<div class="col-3"></div>
<div class="col-3">
<span id="currentChart" data-chart="0"></span>
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item">
<a id="chartPrevious" class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">« Chart</span>
<span class="sr-only">Previous</span>
</a>
</li>
<li class="page-item">
<a id="chartNext" class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">Chart »</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
<div class="row">
<div class="col-12">
<div id="chartA"></div>
</div>
</div>
</div>
...
{% block javascripts %}
{{ parent() }}
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
{{ encore_entry_script_tags('charts') }}
{% endblock %}
Related
I'm trying to save a pdf with Snappy after executing a service. The pdf is created but dont display the css and the chart as it should. Of course the page got no problem to display everything.
Here my knp_snappy.yaml :
knp_snappy:
pdf:
enabled: true
binary: '%env(WKHTMLTOPDF_PATH)% --enable-local-file-access --enable-javascript '
options:
debug-javascript: true
enable-javascript: true
javascript-delay: 200
no-stop-slow-scripts: true
user-style-sheet: 'public/feuilleCalcul.css'
image:
enabled: true
binary: '%env(WKHTMLTOIMAGE_PATH)% --enable-local-file-access --enable-javascript '
options: []
Here my service pdfGenerator.php :
class pdfGenerator
{
public function __construct(Environment $twig, Pdf $pdf)
{
$this->twig = $twig;
$this->pdf = $pdf;
}
public function generatePdf($file)
{
$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');
$snappy->setOption('enable-local-file-access', true);
$snappy->setOption('toc', true);
$snappy->setOption('enable-javascript', true);
$snappy->setOption('debug-javascript', true);
$snappy->setOption('javascript-delay', 200);
$snappy->setOption('enable-local-file-access', true);
$snappy->setOption('no-stop-slow-scripts', true);
$snappy = $this->pdf->generateFromHtml($file, 'myproject/somepath/public/asset/Pdf/test.pdf');
return ;
}
}
and here the controller call :
$chart = $graphGenerator->generateCalculGraph($graphData,$arrayToDisplay,$dataSpecificForm);
$file = $this->render('custom/chartTest.html.twig', [
'chart' => $chart,
'graphData' => $graphData,
'arrayToDisplay' => $arrayToDisplay,
'specificData' => $dataSpecificForm,
]);
$pdfGenerator->generatePdf($file);
And the last one : twig template :
{% extends 'base.html.twig' %}
{% block body %}
<link href="{{ asset('feuilleCalcul.css') }}" rel="stylesheet"/>
<center>
<img class="fit-picture"
src="/asset/image.png"
alt="trench logo" width="25%" height="auto" >
</center>
<div class=page1>
<center>
<div class=topBox>
<p>
<strong>Um :</strong> {{specificData.dataSpecificForm.Um}} kV
</br>
<strong>Uac : </strong>{{specificData.dataSpecificForm.Uac}} kV
</br>
<strong>Ud : </strong>{{graphData.ud}} kV
</p>
</div>
etc..
</div>
{{ render_chart(chart) }}
I dont know what i'm doing wrong and as you can see i tried some options every way i found. So can someone explain what is going on ? :)
Q: If the value in one dropdown changed, how to reload the values for the second dropdown?
When user change the category dropdown, then I want to show the subcategory in the dropdown
Frontend: VueJs (v3)
Server Side Rendering: IneratiaJs
Backend: Laravel (v9)
VueComponent
const props = defineProps({
errors: Object,
categories: Object,
subcategories: Object,
})
const form = useForm({
category_id: '',
subcategory_id: '',
name: '',
price: '',
discount: '',
image: 'sample',
description: ''
});
let getSubcategory = (event) => {
if(event.target.value !== "") {
Inertia.reload({
'category_id': event.target.value
},
{ only: ['subcategories'],
onSuccess: page => {
alert();
console.log('onSuccess');
console.log(props.categories);
console.log(props.subcategories);
console.log(page);
}
}
);
}
}
const submit = () => {
form.post(route('store.subcategory'), {
onFinish: () => form.reset(),
});
};
Vue Template
<template>
<Head title="Add Product" />
<BreezeAuthenticatedLayout>
<template #header>
<form #submit.prevent="submit">
<div class="mt-4">
<BreezeLabel for="category_id" value="Category Name" />
<select #change="getSubcategory" v-model="form.category_id" id="category_id" class="block mt-1 w-full">
<option value="">Select Category</option>
<option v-for="category in categories" :value="category.id">{{ category.name }}</option>
</select>
<div v-if="errors.category_id" class="text-red-400">
{{ errors.category_id }}
</div>
</div>
<div class="mt-4">
<BreezeLabel for="subcategory_id" value="Subcategory Name" />
<select v-model="form.subcategory_id" id="subcategory_id" class="block mt-1 w-full">
<option value="">Select Sategory</option>
<option v-for="subcategory in subcategories" :value="subcategory.id">{{ subcategory.name }}</option>
</select>
<div v-if="errors.subcategory_id" class="text-red-400">
{{ errors.subcategory_id }}
</div>
</div>
</form>
</BreezeAuthenticatedLayout>
</template>
Laravel Route: routes/web.php
Route::get('/create/product/{category_id?}', [ProductController::class, 'create'])->name('create.product'); //Form: Create Product
Product Controller: ProductController.php
public function create($category_id = null)
{
return Inertia::render('Product/Create', [
//I want Evaluated immediately on Page Load.
'categories' => $categories = Category::all(),
//Want Lazy load here.
'subcategories' => function(){
if(!empty($category_id)){
$category = Category::find($category_id);
$subcategories = $category->subcategories()->get();
}
},
]);
}
After fixing Create method in Product Controller and script setup it's working
public function create($category_id = null)
{
return Inertia::render('Product/Create', [
// ALWAYS included on first visit - OPTIONALLY included on partial reloads - ALWAYS evaluated
'categories' => Category::has('subcategories')->get(),
// NEVER included on first visit - OPTIONALLY included on partial reloads - ONLY evaluated when needed
'subcategories' => Inertia::lazy(fn () =>
Subcategory::with('category')->where('category_id', '=', $category_id)->get()
),
]);
}
Vue SCRIPT: I was sending wrong parameters. Then I saw correct way of partial relaod on official site
let getSubcategory = (event) => {
if(event.target.value !== "") {
Inertia.visit(
route('create.product', {
category_id: event.target.value
}),{
only: ['subcategories'],
preserveState: true,
preserveScroll: true,
}
);
}
}
I have a list.html.twig file in which I have included another template file like:
<div class="panel-body">
{{ include('default/partials/groupSettings.html.twig') }}
</div>
And in my controller function following is given:
public function settingsListAction()
{
$settingsGroup = $this->getDoctrine()->getRepository('AppBundle:J1SettingGroup')->findAll();
return $this->render('default/settingsList.html.twig', array('settingsGroup' => $settingsGroup));
$settingsList = $this->getDoctrine()->getRepository('AppBundle:J1Setting')->findAll();
return $this->render('default/partials/groupSettings.html.twig', array('settingsList' => $settingsList));
}
But it only loading the first template and not the second.
includes should be written like so :
{{ include('[BundleName]:[directory_with_your_template]:templatename.html.twig', { 'settingsGroup': settingsGroup }) }}
and in your controller you render only the parent template and pass the params you need to the child template
public function settingsListAction()
{
$settingsGroup = $this->getDoctrine()->getRepository('AppBundle:J1SettingGroup')->findAll();
$settingsList = $this->getDoctrine()->getRepository('AppBundle:J1Setting')->findAll();
return $this->render('default/settingsList.html.twig', array('settingsGroup' => $settingsGroup, 'settingsList' => $settingsList));
}
I load my image (blob data ) in my GETer Entity
When I just return ($this->foto) in my GETer I see :Resource id #284 on the screen
When I change my GETer like this : return stream_get_contents($this->foto);
I see these : ���JFIF��� ( ,,,,,,,, ( and more )
In my Controller a call the index.html.twig to show all my entities
/**
* Lists all Producten entities.
*
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('CustomCMSBundle:Producten')->findAll();
return $this->render('CustomCMSBundle:Producten:index.html.twig', array(
'entities' => $entities,
));
}
Now in my views ( index.html.twig ) I like to show the picture
{% for entity in entities %}
<tr>
<td>
<img src="{{ entity.foto}}" alt="" width="80" height="80" />
</td>
<td>
{{ entity.foto }}
</td>
<td>
<ul>
<li>
show
</li>
<li>
edit
</li>
</ul>
</td>
</tr>
{% endfor %}
But I don't see the picture ?
Can anyone help me?
You are using <img src="(raw image)"> instead of <img src="(image's url)">
A quick solution is to encode your image in base64 and embed it.
Controller
$images = array();
foreach ($entities as $key => $entity) {
$images[$key] = base64_encode(stream_get_contents($entity->getFoto()));
}
// ...
return $this->render('CustomCMSBundle:Producten:index.html.twig', array(
'entities' => $entities,
'images' => $images,
));
View
{% for key, entity in entities %}
{# ... #}
<img alt="Embedded Image" src="data:image/png;base64,{{ images[key] }}" />
{# ... #}
{% endfor %}
in your entity write your image getter like this:
public function getFoto()
{
return imagecreatefromstring($this->foto);
}
and use it instead of the object "foto" property.
php doc for the function: http://php.net/manual/de/function.imagecreatefromstring.php
A more direct way, without extra work in the controller:
In the Entity Class
/**
* #ORM\Column(name="photo", type="blob", nullable=true)
*/
private $photo;
private $rawPhoto;
public function displayPhoto()
{
if(null === $this->rawPhoto) {
$this->rawPhoto = "data:image/png;base64," . base64_encode(stream_get_contents($this->getPhoto()));
}
return $this->rawPhoto;
}
In the view
<img src="{{ entity.displayPhoto }}">
EDIT
Thanks to #b.enoit.be answer to my question here, I could improve this code so the image can be displayed more than once.
As it is said before you must use base64 method, but for a better performance and usability, the correct option is creating a custom twig filter (Twig extension) as described here .
<?php
namespace Your\Namespace;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class TwigExtensions extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('base64', [$this, 'twig_base64_filter']),
];
}
function twig_base64_filter($source)
{ if($source!=null) {
return base64_encode(stream_get_contents($source));
}
return '';
}
}
In your template:
<img src="data:image/png;base64,{{ entity.photo | base64 }}">
I'm trying to create a form which will add a new text box every time the 'Add new box' link got clicked.
I read through the following example.
http://symfony.com/doc/current/reference/forms/types/collection.html
Basically I was following the example from the book. But when the page is rendered and I click on the link nothing happens.
Any thoughts?
Thanks.
This is my controller.
public function createAction() {
$formBuilder = $this->createFormBuilder();
$formBuilder->add('emails', 'collection', array(
// each item in the array will be an "email" field
'type' => 'email',
'prototype' => true,
'allow_add' => true,
// these options are passed to each "email" type
'options' => array(
'required' => false,
'attr' => array('class' => 'email-box')
),
));
$form = $formBuilder->getForm();
return $this->render('AcmeRecordBundle:Form:create.html.twig', array(
'form' => $form->createView(),
));
}
This is the view.
<form action="..." method="POST" {{ form_enctype(form) }}>
{# store the prototype on the data-prototype attribute #}
<ul id="email-fields-list" data-prototype="{{ form_widget(form.emails.get('prototype')) | e }}">
{% for emailField in form.emails %}
<li>
{{ form_errors(emailField) }}
{{ form_widget(emailField) }}
</li>
{% endfor %}
</ul>
Add another email
</form>
<script type="text/javascript">
// keep track of how many email fields have been rendered
var emailCount = '{{ form.emails | length }}';
jQuery(document).ready(function() {
jQuery('#add-another-email').click(function() {
var emailList = jQuery('#email-fields-list');
// grab the prototype template
var newWidget = emailList.attr('data-prototype');
// replace the "$$name$$" used in the id and name of the prototype
// with a number that's unique to our emails
// end name attribute looks like name="contact[emails][2]"
newWidget = newWidget.replace(/\$\$name\$\$/g, emailCount);
emailCount++;
// create a new list element and add it to our list
var newLi = jQuery('<li></li>').html(newWidget);
newLi.appendTo(jQuery('#email-fields-list'));
return false;
});
})
</script>
This problem can be solved by referring to the following link.
https://github.com/beberlei/AcmePizzaBundle
Here you will find the same functionality being implemented.
I've been through this too.
Answer and examples given to this question and the other question I found did not answer my problem either.
Here is how I did it, in some generic manner.
In generic, I mean, Any collection that I add to the form just need to follow the Form template loop (in a macro, for example) and that's all!
Using which convention
HTML is from Twitter Bootstrap 2.0.x
Javascript code is already in a $(document).ready();
Following Symfony 2.0.x tutorial
Using MopaBootstrapBundle
Form Type class
class OrderForm extends AbstractType
{
// ...
public function buildForm(FormBuilder $builder, array $options)
{
// ...
$builder
->add('sharingusers', 'collection', array(
'type' => new UserForm(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required'=> false
));
// ...
}
}
JavaScript
/* In the functions section out of document ready */
/**
* Add a new row in a form Collection
*
* Difference from source is that I use Bootstrap convention
* to get the part we are interrested in, the input tag itself and not
* create a new .collection-field block inside the original.
*
* Source: http://symfony.com/doc/current/cookbook/form/form_collections.html
*/
function addTagForm(collectionHolder, newBtn) {
var prototype = collectionHolder.attr('data-prototype');
var p = prototype.replace(/\$\$name\$\$/g, collectionHolder.children().length);
var newFormFromPrototype = $(p);
var buildup = newFormFromPrototype.find(".controls input");
var collectionField = $('<div class="collection-field"></div>').append(buildup);
newBtn.before(collectionField);
}
/* ********** */
$(document).ready(function(){
/* other initializations */
/**
* Form collection behavior
*
* Inspired, but refactored to be re-usable from Source defined below
*
* Source: http://symfony.com/doc/current/cookbook/form/form_collections.html
*/
var formCollectionObj = $('form .behavior-collection');
if(formCollectionObj.length >= 1){
console.log('run.js: document ready "form .behavior-collection" applied on '+formCollectionObj.length+' elements');
var addTagLink = $('<i class="icon-plus-sign"></i> Add');
var newBtn = $('<div class="collection-add"></div>').append(addTagLink);
formCollectionObj.append(newBtn);
addTagLink.on('click', function(e) {
e.preventDefault();
addTagForm(formCollectionObj, newBtn);
});
}
/* other initializations */
});
The form template
Trick here is that I would have had used the original {{ form_widget(form }} but I needed to add some specific to the view form and I could not make it shorter.
And I tried to edit only the targeted field and found out it was a bit complex
Here is how I did it:
{# All form elements prior to the targeted field #}
<div class="control-collection control-group">
<label class="control-label">{{ form_label(form.sharingusers) }}</label>
<div class="controls behavior-collection" data-prototype="{{ form_widget(form.sharingusers.get('prototype'))|escape }}">
{% for user in form.sharingusers %}
{{ form_row(user) }}
{% endfor %}
</div>
</div>
{{ form_rest(form) }}