Symfony geavanceerde formulieren

Inleiding

In deze tutorial maken we een formulier met een collectie van subformulieren. We gebruiken hiervoor een webshop scenario waarin we een entity Product hebben. Aan de producten willen we echter ook opties kunnen toevoegen zoals bijvoorbeeld een kleur of een maat. Ieder product kan echter nul, één of meerder opties hebben met ieder twee of meer keuzes. Deze opties en hun keuzes moeten in de backend aangemaakt en gewijzigd kunnen worden en dus komen de gegevens uit de database. Als laatste zullen we nog laten zien hoe we wat extra attributen aan formuliervelden kunnen toevoegen en hoe we hier gebruik van maken in javascript.

De entities

Maak een Product, een ProductOption en een ProductOptionChoice Entity. Plaats de entities in de AppBundle\Entity map. Als je Symfony net gedownload hebt zul je de Entity map misschien zelf moeten aanmaken. Merk op dat Product een many-to-many relatie heeft met ProductOption. een product kan dus meerdere opties hebben en een optie kan ook aan meerdere producten toegevoegd worden. ProductOptionChoice heeft een many-to-one relatie met ProductOption. Een optie kan dus meerdere keuzes hebben. Hieronder staan ze:

<?php
# AppBundle\Entity\Product.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Product
 *
 * @ORM\Table(name="products")
 * @ORM\Entity
 */
class Product
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255)
     */
    private $name;

    /**
     * @var string
     *
     * @ORM\Column(name="price", type="decimal", precision=10, scale=2)
     */
    private $price = .0;

    /**
     * @ORM\ManyToMany(targetEntity="ProductOption")
     * @ORM\JoinTable(name="products_options",
     *      joinColumns={@ORM\JoinColumn(name="product_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="option_id", referencedColumnName="id")}
     *      )
     */
    private $options;


    public function __construct()
    {
        $this->options = new ArrayCollection();
    }
}
<?php
# AppBundle\Entity\ProductOption.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * ProductOption
 *
 * @ORM\Table(name="product_options")
 * @ORM\Entity
 */
class ProductOption
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=64)
     */
    private $name = '';

    /**
     * @var string
     *
     * @ORM\Column(name="optionLabel", type="string", length=64, nullable=true)
     */
    private $optionLabel;

    /**
     * @ORM\OneToMany(targetEntity="ProductOptionChoice", mappedBy="option", cascade={"persist", "remove"})
     */
    private $choices;

    
    public function __construct()
    {
        $this->choices = new ArrayCollection();
    }
    
    public function __toString()
    {
        return $this->name;
    }
}
<?php
# AppBundle\Entity\ProductOptionChoice.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * ProductOptionChoice
 *
 * @ORM\Table(name="product_option_choices")
 * @ORM\Entity
 */
class ProductOptionChoice
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=64)
     */
    private $name = '';

    /**
     * @var string
     *
     * @ORM\Column(name="price", type="decimal", precision=10, scale=2)
     */
    private $price = .0;
    
    /**
     * @ORM\ManyToOne(targetEntity="ProductOption", inversedBy="choices")
     * @ORM\JoinColumn(name="option_id", referencedColumnName="id")
     */
    private $option;

    
    public function __toString()
    {
        return $this->name;
    }
}

Nu moeten we nog even zorgen dat de getters en setters aan de entities toegevoegd worden en dat de database tabellen aangemaakt en ingericht worden:

bin/console doctrine:generate:entities AppBundle
bin/console doctrine:schema:update --force
Zorg dat de database al aangemaakt is of maak deze aan met het commando bin/console doctrine:database:create

Als laatste brengen we nog een wijziging aan de addChoice() method van de ProductOption entity zodat als er nieuwe keuzes aan een optie toegevoegd worden het option_id veld automatisch ingevuld wordt.

# AppBundle\Entity\ProductOption.php

    public function addChoice(\AppBundle\Entity\ProductOptionChoice $choice)
    {
        $this->choices[] = $choice;
        $choice->setOption($this); # voeg deze regel toe

        return $this;
    }

Testdata

Met een lege database valt er weinig te testen dus zullen we voor wat data moeten zorgen. Je kunt er voor kiezen om Bijvoorbeeld Sonata AdminBundle te installeren en Admin classes toe te voegen om de entities mee te bewerken of je kunt zelf een CRUD controller schrijven. Dit is echter niet het doel van deze tutorial en dus heb ik een Action aan de DefaultController toegevoegd die wat testdata aanmaakt.

<?php
# AppBundle\Controller\DefaultController.php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

use AppBundle\Entity\Product;
use AppBundle\Entity\ProductOption;
use AppBundle\Entity\ProductOptionChoice;


class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        return $this->render('default/index.html.twig');
    }
    
    /**
     * @Route("/testdata", name="testdata")
     */
    public function testdataAction()
    {
        $em = $this->getDoctrine()->getManager();
        
        // Maak een optie om de kleur van een T-shirt te kiezen
        $option1 = new ProductOption();
        $option1->setName('T-shirt Kleur');
        $option1->setOptionLabel('Kleur');
        
        $optionChoice = new ProductOptionChoice();
        $optionChoice->setName('Rood');
        $option1->addChoice($optionChoice);
        
        $optionChoice = new ProductOptionChoice();
        $optionChoice->setName('Wit');
        $option1->addChoice($optionChoice);
        
        $optionChoice = new ProductOptionChoice();
        $optionChoice->setName('Zilver');
        $optionChoice->setPrice(5.0);
        $option1->addChoice($optionChoice);
        
        $em->persist($option1);
        
       // Maak een optie om de maat van een T-shirt te kiezen
        $option2 = new ProductOption();
        $option2->setName('T-shirt maat');
        $option2->setOptionLabel('Maat');
        
        $optionChoice = new ProductOptionChoice();
        $optionChoice->setName('Small');
        $option2->addChoice($optionChoice);
        
        $optionChoice = new ProductOptionChoice();
        $optionChoice->setName('Medium');
        $option2->addChoice($optionChoice);
        
        $optionChoice = new ProductOptionChoice();
        $optionChoice->setName('Large');
        $option2->addChoice($optionChoice);
        
        $optionChoice = new ProductOptionChoice();
        $optionChoice->setName('Extra large');
        $optionChoice->setPrice(2.50);
        $option2->addChoice($optionChoice);
        
        $em->persist($option2);
        
       // Maak een product T-shirt aan
        $product = new Product();
        $product->setName('Super Cool Symfony T-shirt');
        $product->setPrice(20.0);
        $product->addOption($option1);
        $product->addOption($option2);
        $em->persist($product);
        
       // Maak een product Baseball cap aan
        $product = new Product();
        $product->setName('Super Cool Symfony Baseball Cap');
        $product->setPrice(14.5);
        $product->addOption($option1);
        $em->persist($product);
        
       // Maak een product Tea cup aan
        $product = new Product();
        $product->setName('Symfony Tea Cup');
        $product->setPrice(3.95);
        $em->persist($product);
        
        $em->flush();

        return new Response('Testdata is aangemaakt.');
    }
}

Winkelwagen

Nu we een aantal producten hebben met wat opties wordt het tijd om na te denken over de inrichting van de shopping-cart ofwel de winkelwagen. Een winkelwagen bestaat eigenlijk uit een opsomming van producten die in de winkelwagen geplaatst zijn. Omdat ik niet te veel in wil gaan op de werking van een winkelwagen zal ik me beperken tot een nieuwe entity CartItem die één product vertegenwoordigt dat zogenaamd in de winkelwagen geplaatst is. De properties zijn aantal, product, en eventuele opties. Deze opties vormen een tweede nieuwe entity CartOption omdat we nooit weten hoeveel opties een product heeft.

<?php
# AppBundle\Entity\CartItem.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\CartOption;

/**
 * CartItem
 *
 * @ORM\Table(name="cart_items")
 * @ORM\Entity
 */
class CartItem
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var int
     *
     * @ORM\Column(name="amount", type="integer")
     */
    private $amount = 1;

    /**
     * @ORM\ManyToOne(targetEntity="Product")
     * @ORM\JoinColumn(name="product_id", referencedColumnName="id")
     */
    private $product;

    /**
     * @ORM\OneToMany(targetEntity="CartOption", mappedBy="cartItem", cascade={"persist", "remove"}))
     */
    private $options;


    public function __construct(\AppBundle\Entity\Product $product)
    {
        $this->options = new ArrayCollection();
        $this->product = $product;
        
        /*
         * Voeg net zoveel CartOption enitities toe aan de $options collection
         * als het aantal ProductOptions dat $product heeft.
         */
        foreach ($product->getOptions() as $option) {
            $this->addOption(new CartOption($option));
        }
    }
}

Omdat het gedrag van een CartItem afhankelijk is van de Product entity heb ik er voor gekozen om het Product aan de constructor mee te geven en direct een array van CartOptions aan te maken aan de hand van de options van het Product.

<?php
# AppBundle\Entity\CartOption.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\ProductOption;

/**
 * CartOptions
 *
 * @ORM\Table(name="cart_options")
 * @ORM\Entity
 */
class CartOption
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="CartItem", inversedBy="options")
     * @ORM\JoinColumn(name="cartitem_id", referencedColumnName="id")
     */
    private $cartItem;
    
    /**
     * @ORM\ManyToOne(targetEntity="ProductOption")
     * @ORM\JoinColumn(name="option_id", referencedColumnName="id")
     */
    private $productOption;
    
    /**
     * @ORM\ManyToOne(targetEntity="ProductOptionChoice")
     * @ORM\JoinColumn(name="choice_id", referencedColumnName="id")
     */
    private $productOptionChoice;

    
    public function __construct(ProductOption $productOption)
    {
        $this->productOption = $productOption;
    }
}

En ook nu moeten we weer de getters en setters aan de entities toegevoegen en de database updaten:

bin/console doctrine:generate:entities AppBundle
bin/console doctrine:schema:update --force

Ook nu moeten we nog een wijziging aan de addOption() method van de CartItem entity aanbrengen:

# AppBundle\Entity\CartItem.php

    /**
     * Add option
     *
     * @param \AppBundle\Entity\CartOption $option
     *
     * @return CartItem
     */
    public function addOption(\AppBundle\Entity\CartOption $option)
    {
        $this->options[] = $option;
        $option->setCartItem($this); # voeg deze regel toe

        return $this;
    }

Product pagina

We zijn zo ver om een product pagina te maken met daarop een formulier waarmee een product aan de winkelwagen toegevoegd kan worden. Om niet te moeilijk te beginnen zullen we eerst de opties achterwege laten. Maak een formType met onderstaand commando:

bin/console doctrine:generate:form AppBundle:CartItem

Pas de buildForm method een klein beetje aan zoals op onderstaand voorbeeld.

# AppBundle\Form\CartItemType.php

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('amount', null, array('label' => 'Aantal'));

        // opties komen hier
        
        $builder->add('In Winkelwagen', SubmitType::class);
    }

En plaats een nieuw use statement voor de SubmitType bij de andere use statements

use Symfony\Component\Form\Extension\Core\Type\SubmitType;

Vervolgens voegen we een showProductAction toe aan onze DefaultController en maken we een nieuwe Twig template:

# AppBundle\Controller\DefaultController.php

    /**
     * @Route("/products/show/{productId}", name="product_show")
     * @Method({"GET", "POST"})
     */
    public function showProductAction(Request $request, $productId)
    {
        $em = $this->getDoctrine()->getManager();
        
        $product = $em->getRepository('AppBundle:Product')->find($productId);
        
        if(null === $product) {
            throw $this->createNotFoundException('Dit product bestaat niet.');
        }
        
        $cartItem = new Cartitem($product);

        $form = $this->createForm('AppBundle\Form\CartItemType', $cartItem);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $em->persist($cartItem);
            $em->flush();

            $this->addFlash(
                'notice',
                'Uw bestelling is toegevoed aan de winkelwagen'
            );

            return $this->redirectToRoute('product_show', array('productId' => $productId));
        }

        return $this->render('default/show-product.html.twig', array(
            'product' => $product,
            'form' => $form->createView(),
        ));
    }

En plaats drie nieuwe use statements

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use AppBundle\Entity\CartItem;
use AppBundle\Entity\CartOption;

En de show-product.html.twig template:

{# app\Resources\views\default\show-product.html.twig #}

{% extends 'base.html.twig' %}

{% block body %}
    {% for message in app.flashes('notice') %}
        <div class="flash-notice">
            {{ message }}
        </div>
    {% endfor %}

    <h1>{{ product.name }}</h1>

    {{ form_start(form) }}
        {{ form_widget(form) }}
    {{ form_end(form) }}
{% endblock %}

Gefeliciteerd, het eerste simpele formulier werkt. U kunt een aantal invullen en op de knop drukken waarna u een bevestiging ziet dat het product in de winkelwagen geplaatst is.

Option formulier

Straks willen we een collection maken van opties. Hiervoor moeten we eerst een formulier maken die één optie laat zien in de vorm van een select/dropdown.

Er is een zeer uitgebreide uitleg over collections beschikbaar op de website van Symfony.

Voeg onderstaande formType toe aan de AppBundle\Form directory:

<?php
# AppBundle\Form\CartOptionType.php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Doctrine\ORM\EntityRepository;


class CartOptionType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $cartOption = $builder->getData();

        $builder->add('productOptionChoice', EntityType::class, array(
            'class' => 'AppBundle:ProductOptionChoice',
            'query_builder' => function (EntityRepository $er) use ($cartOption) {
                return $er->createQueryBuilder('c')
                    ->join('c.option', 'o')
                    ->where('o.id=:id')
                    ->setParameter('id', $cartOption->getProductOption()->getId())
                ;
            },
            'label' => $cartOption->getProductOption()->getOptionLabel(),
            'placeholder' => 'Maak een keuze',
        ));
    }
    
    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\CartOption'
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_cartoption';
    }


}

Dit formulier bestaat uit één formulierveld. We gebruiken Symfony's EntityType voor dit veld zodat we voor de dropdown direct de data uit de database kunnen halen. Aan het formulierveld zijn een aantal opties toegevoegd. We behandelen ze één voor één.

class

Deze optie is verplicht voor de EntityType. Het vertelt de formbuilder waar hij zijn data vandaan moet halen om de <options> mee te vullen. De __toString() method can deze entity zal gebruikt worden voor het weergeven van de options tenzij we anders aangeven met de choice_label optie.

query_builder

Deze optie is optioneel. Echter als we deze optie weglaten zullen alle records uit de ProductOptionChoices tabel gelezen worden. We willen echter alleen de keuzes die gelden voor één bepaalde optie en dus moeten we filteren met een custom query. Omdat we verplicht zijn om een ProductOption entity mee te geven aan de constructor van CartOption en deze in de $productOption property opgeslagen wordt weten we voor welke optie we de keuzes moeten inladen.

label

In plaats van een statische label willen we de label laten zien die we opgeslagen hebben in de database. In de testdata is dat dus Kleur of Maat. We vinden deze waarde weer terug via de getProductOption() method van het onderliggend CartOption object.

placeholder

Deze optie zorgt voor een speciale extra optie in de dropdown. Hierdoor verplichten we de gebruiker eerst een keuze te maken voordat hij het formulier kan indienen.

Symfony collectionType

We zouden nu een controller kunnen schrijven die dit formulier direct gebruikt om één van de dropdowns te tonen en de gebruiker te laten kiezen uit één van de opties. Wat we echter willen is dat we geen, één of meerdere opties laten zien afhankelijk van welke opties aan een bepaald product gekoppeld zijn. Hiervoor gebruiken we de collectionType. Breid de buildForm method van de CartItemType class als volgt uit:

# AppBundle\Form\CartItemType.php

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $cartItem = $builder->getData();
        
        $builder
            ->add('amount', null, array('label' => 'Aantal'))

            ->add('options', CollectionType::class, array(
                'label' => false,
                'entry_type'   => CartOptionType::class,
                'entry_options' => array(
                    'label' => false,
                ),
            ))
        
            ->add('In Winkelwagen', SubmitType::class)
        ;
    }

En voeg twee nieuwe use statements toe:

use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use AppBundle\Form\CartOptionType;

Er ontstaat nu een klein probleem. Als we de productpagina nu opnieuw inladen worden we geconfronteerd met een foutmelding:

Call to a member function getProductOption() on null
Het probleem is dat de formData niet beschikbaar is in het Childformulier:
# AppBundle\Form\CartOptionType.php

class CartOptionType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $cartOption = $builder->getData(); // <-- LEEG !!!

        ...
    }
 
    ...
}
We lossen dat op met behulp van Form Events. We voegen een PRE_SET_DATA event listener toe aan de buildForm method als volgt:
# AppBundle\Form\CartOptionType.php

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            $cartOption = $event->getData();
            $form = $event->getForm();

            $form->add('productOptionChoice', EntityType::class, array(
                'class' => 'AppBundle:ProductOptionChoice',
                'query_builder' => function (EntityRepository $er) use ($cartOption) {
                    return $er->createQueryBuilder('c')
                        ->join('c.option', 'o')
                        ->where('o.id=:id')
                        ->setParameter('id', $cartOption->getProductOption()->getId())
                    ;
                },
                'label' => $cartOption->getProductOption()->getOptionLabel(),
                'placeholder' => 'Maak een keuze',
            ));
        });
    }

Extra attributen

So far so good. Alles lijkt te werken. We hadden echter nog een mogelijkheid om bepaalde opties tegen een meerprijs aan te bieden maar zien hiervan nog niets van terug. Het zou leuk zijn als we de totaalprijs konden laten zien van het product waarbij we de meerprijzen van de gekozen opties optellen met behulp van javascript. Voordat dat uberhaupt mogelijk is moeten we de meerprijzen en de prijs van het product ergens in de html kunnen terugvinden. De prijs van het product is simpel in de HTML te zetten. We doen dat door een extra attribuut aan het <form> element toe te voegen:

 {{ form_start(form, {'attr':{'data-price':product.price} }) }}
 
 <!-- Levert het volgende op: -->
 <form name="appbundle_cartitem" method="post" data-price="20.00">

De meerprijzen van de opties kunnen we in de <option> elementen zelf zetten. We doen dit vanuit de CartOption formtype.

# AppBundle\Form\CartOptionType.php

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            $cartOption = $event->getData();
            $form = $event->getForm();

            $form->add('productOptionChoice', EntityType::class, array(
                'class' => 'AppBundle:ProductOptionChoice',
                'query_builder' => function (EntityRepository $er) use ($cartOption) {
                    return $er->createQueryBuilder('c')
                        ->join('c.option', 'o')
                        ->where('o.id=:id')
                        ->setParameter('id', $cartOption->getProductOption()->getId())
                    ;
                },
                'label' => $cartOption->getProductOption()->getOptionLabel(),
                'placeholder' => 'Maak een keuze',
                
                /*
                 * voeg een data-price attribuut toe aan de <option> elementen
                 */
                'choice_attr' => function (\AppBundle\Entity\ProductOptionChoice $choice, $key, $index) {
                    return ['data-price' => (float) $choice->getPrice()];
                },
                
                /*
                 * Voeg een class attibuut toe aan de <select> element
                 */
                'attr' => array(
                    'class' => 'productoption'
                )
                
            ));
        });
    }
    

De template breiden we uit met een product-container, een <span> met daarin de actuele prijs van het product en de nodige javascript.

{# app\Resources\views\default\show-product.html.twig #}

{% extends 'base.html.twig' %}

{% block body %}
    {% for message in app.flashes('notice') %}
        <div class="flash-notice">
            {{ message }}
        </div>
    {% endfor %}

    <div class="product-container">
        <h1>{{ product.name }}</h1>

        &euro; <span class="price">{{ product.price|number_format(2, ',', '.') }}</span> p/s

        {{ form_start(form, {'attr':{'data-price':product.price} }) }}
            {{ form_widget(form) }}
        {{ form_end(form) }}
    </div>
{% endblock %}

{% block javascripts %}
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script>
        /*
         * number_format functie voor javascript https://gist.github.com/rd13/3924792
         */
        String.prototype.number_format = function(d) {
            var n = this;
            var c = isNaN(d = Math.abs(d)) ? 2 : d;
            var s = n < 0 ? "-" : "";
            var i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "", j = (j = i.length) > 3 ? j % 3 : 0;
            return s + (j ? i.substr(0, j) + '.' : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + '.') + (c ? ',' + Math.abs(n - i).toFixed(c).slice(2) : "");
        }     

        $( document ).ready(function() {
            $('.productoption').change(function() {
                var container = $(this).closest('.product-container');
                var form = $(this).closest('form');
                var totalPrice = parseFloat(form.attr('data-price'));
                
                $(form.find('.productoption')).each(function() {
                    var optionPrice = $(this).find(":selected").attr('data-price');
                    
                    if(typeof optionPrice !== typeof undefined && optionPrice !== false) {
                        totalPrice += parseFloat(optionPrice);
                    }                   
                    
                });
                
                container.find('span.price').text(totalPrice.toString().number_format());
            });
        });
    </script>
{% endblock %}

Download

De bestanden kunt u vinden op Github.

Reacties

Om een reactie te plaatsen kun je jezelf eenmalig registeren of je kunt hier inloggen.