├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Controller └── AjaxEntityController.php ├── DependencyInjection ├── Configuration.php └── ZenstruckFormExtension.php ├── Form ├── AjaxEntityManager.php ├── DataTransformer │ └── AjaxEntityTransformer.php ├── Extension │ ├── GroupTypeExtension.php │ ├── HelpTypeExtension.php │ └── ThemeTypeExtension.php ├── GroupedFormView.php └── Type │ ├── AjaxEntityType.php │ └── TunnelEntityType.php ├── README.md ├── Resources ├── config │ ├── ajax_entity_controller.xml │ ├── ajax_entity_routing.xml │ ├── ajax_entity_type.xml │ ├── group_type.xml │ ├── help_type.xml │ ├── theme_type.xml │ └── tunnel_entity_type.xml ├── meta │ └── LICENSE ├── public │ └── js │ │ └── helper.js └── views │ └── Twitter │ ├── form_bootstrap3_layout.html.twig │ ├── form_bootstrap_layout.html.twig │ └── grouped_form.html.twig ├── Tests ├── Fixtures │ └── App │ │ ├── FormTestBundle │ │ ├── Entity │ │ │ └── Author.php │ │ └── FormTestBundle.php │ │ ├── TestKernel.php │ │ └── config │ │ ├── default.yml │ │ └── routing.yml ├── Form │ ├── AjaxEntityManagerTest.php │ ├── DataTransformer │ │ └── AjaxEntityTransformerTest.php │ ├── GroupedFormViewTest.php │ └── Type │ │ ├── AjaxEntityTypeTest.php │ │ └── TunnelEntityTypeTest.php ├── Functional │ └── WebTestCase.php └── bootstrap.php ├── ZenstruckFormBundle.php ├── composer.json └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | 9 | env: 10 | - SYMFONY_VERSION=2.1.* 11 | - SYMFONY_VERSION=2.2.* 12 | - SYMFONY_VERSION=2.3.* 13 | - SYMFONY_VERSION=2.4.* 14 | - SYMFONY_VERSION=2.5.* 15 | - SYMFONY_VERSION=2.6.* 16 | 17 | before_script: 18 | - composer require --no-update symfony/symfony:${SYMFONY_VERSION} 19 | - composer install --dev --prefer-source 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.5.1 2 | 3 | - 718b285 switch from ZenstruckSlugifyBundle to CocurSlugifyBundle (Kevin Bond) 4 | - ad323de added changelog (Kevin Bond) 5 | 6 | ## v1.5.0 7 | 8 | - 5eb8da6 Update README.md (doughayward0) 9 | - 0fe4ead Update README.md (doughayward0) 10 | - e0c7b72 Update README.md (doughayward0) 11 | - 1390ee8 Update README.md (doughayward0) 12 | - 863d860 Update README.md (doughayward0) 13 | - 76d791e Update README.md (doughayward0) 14 | - 62fec46 Update README.md (doughayward0) 15 | - be32a3f Add default value for extra parameter for BC (Maxime AILLOUD) 16 | - f5d5b54 Adding documentation for the new field option (Maxime AILLOUD) 17 | - 953d303 Allow the use of extra data for ajax request when using the ajax entity type field form (Maxime AILLOUD) 18 | - 3b88dbc Pass RouterInterface instead of Router itself (Guillaume Bornot) 19 | 20 | ## v1.4.1 21 | 22 | - 29686c0 cs fix (Kevin Bond) 23 | 24 | ## v1.4.0 25 | 26 | - 3f3b1c4 set travis to test symfony 2.4 (Kevin Bond) 27 | - 0aa54ea Simplify the way errors are displayed. (ngodfraind) 28 | - 1ab1392 Fixing form error pluralization. (ngodfraind) 29 | - f2944d0 Correct use of label_width (Maxime AILLOUD) 30 | - 57b4313 Remove useless space (Maxime AILLOUD) 31 | - 4306e7a Adding type form select field to have them the from-control class (Maxime AILLOUD) 32 | 33 | ## v1.3.0 34 | 35 | - ac19769 adjusted formatting (Kevin Bond) 36 | - c5971be added theme options (Kevin Bond) 37 | - bd74aa5 Make the bootstrap3 form theme extend of symfony form_div_layout instead of botstrap 2 one + update for avoiding checkbox to get the form-control class (Maxime AILLOUD) 38 | - bdfeef5 added bootstrap3 form layout (Kevin Bond) 39 | - 7558d83 use depreciated method to be compatible with symfony 2.2 (Kevin Bond) 40 | - 0691171 fixed failing tests (Kevin Bond) 41 | - caeecee try to use property for displaying entity text (Kevin Bond) 42 | - d8d21f8 Bad quote on example (Maxime AILLOUD) 43 | - e3fa2c7 add a collection check (Kevin Bond) 44 | - 95e51eb https://github.com/kbond/ZenstruckFormBundle/issues/8 (Alexander Janssen) 45 | - 15aef11 Update for symfony2.3 (Alexander Janssen) 46 | - 1c33f21 added a currency widget (closes #6) (Kevin Bond) 47 | - 0f12c76 Symfony 2.3 (Alexander Janssen) 48 | - b859278 Subforms children can have it's own group too. (Alexander Janssen) 49 | - 36d5708 allow access to wrapped form's variables (Kevin Bond) 50 | - 1fbd2aa removed form error message (Kevin Bond) 51 | - b4ec722 fixed form errors block (Kevin Bond) 52 | - 79833cd remove "add" button when `allow_add` is false (Kevin Bond) 53 | - e1686dc [BC break] made compatible with symfony 2.3... (Kevin Bond) 54 | - 4420705 made test forward compatible (Kevin Bond) 55 | - 8c1c555 fixed typo (Kevin Bond) 56 | - 8d158b3 added note about Entity::__toString() (Kevin Bond) 57 | - 7a201ac added ability to order form groups (Kevin Bond) 58 | - 8014300 added GroupedFormView tests (Kevin Bond) 59 | - 8f9c866 added ability to add custom data to GroupedFormView (Kevin Bond) 60 | - 31466c1 updated readme [ci skip] (Kevin Bond) 61 | - 92878e8 added blocks to grouped_form template to allow overriding (Kevin Bond) 62 | - 6c5c52e use --prefer-source for travis composer install (Kevin Bond) 63 | - fcc80df fixed failing test (Kevin Bond) 64 | - 3a69849 added branch alias (Kevin Bond) 65 | - e2e95df added --no-interaction to travis config composer install (Kevin Bond) 66 | - aa0249e require zend-crypt in dev (Kevin Bond) 67 | - 9a7d9d8 made zendframework/zend-crypt a soft requirement (Kevin Bond) 68 | 69 | ## v1.2.2 70 | 71 | - c988182 added formatting for radio/checkboxes (Kevin Bond) 72 | 73 | ## v1.2.1 74 | 75 | - a168c87 made zenstruck/slugify-bundle a soft requirement (Kevin Bond) 76 | 77 | ## v1.2.0 78 | 79 | - 4109b9b require zenstruck/slugify-bundle (Kevin Bond) 80 | 81 | ## v1.1.0 82 | 83 | - 7d940f8 added grouped form (Kevin Bond) 84 | - 97f5265 use view transformer instead of model transformer (Kevin Bond) 85 | - 30ac1de use delete method (Kevin Bond) 86 | - 1bcc056 added method-post-confirm to helper.js (Kevin Bond) 87 | - c2faf23 build with symfony 2.2 (Kevin Bond) 88 | - 3051998 ensure future embedded forms have select2 enabled (Kevin Bond) 89 | - 9d685ab trim prototype string (Kevin Bond) 90 | - 07722cd ensured zend framework 2.1.1 is not used - autoload bug (Kevin Bond) 91 | - 9ec39a1 used ZendCrypt for encryption/decryption (Kevin Bond) 92 | - dcdc660 edit demo url (Kevin Bond) 93 | - eeb1289 added demo link (Kevin Bond) 94 | - 4a6381a updated description (Kevin Bond) 95 | 96 | ## v1.0.1 97 | 98 | - eba3da0 added encryption with mcrypt (Kevin Bond) 99 | - 16aca37 fixed tests (Kevin Bond) 100 | - c0e58de refactored (Kevin Bond) 101 | -------------------------------------------------------------------------------- /Controller/AjaxEntityController.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class AjaxEntityController 14 | { 15 | protected $manager; 16 | 17 | public function __construct(AjaxEntityManager $manager) 18 | { 19 | $this->manager = $manager; 20 | } 21 | 22 | public function findAction(Request $request) 23 | { 24 | if (!$request->isXmlHttpRequest()) { 25 | throw new NotFoundHttpException('Must be ajax request'); 26 | } 27 | 28 | $property = $request->request->get('property'); 29 | $method = $request->request->get('method'); 30 | $entity = $request->request->get('entity'); 31 | $query = $request->request->get('q'); 32 | $extra = $request->request->get('extra'); 33 | 34 | $results = array(); 35 | 36 | if ($query) { 37 | if ($property) { 38 | $results = $this->manager->findEntitiesByProperty($entity, $property, $query); 39 | } elseif ($method) { 40 | $results = $this->manager->findEntitiesByMethod($entity, $method, $query, $extra); 41 | } 42 | } 43 | 44 | return new JsonResponse($results); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Configuration implements ConfigurationInterface 12 | { 13 | public function getConfigTreeBuilder() 14 | { 15 | $treeBuilder = new TreeBuilder(); 16 | $rootNode = $treeBuilder->root('zenstruck_form'); 17 | 18 | $rootNode 19 | ->children() 20 | ->arrayNode('form_types') 21 | ->addDefaultsIfNotSet() 22 | ->children() 23 | ->booleanNode('help')->defaultFalse()->end() 24 | ->booleanNode('group')->defaultFalse()->end() 25 | ->booleanNode('theme')->defaultFalse()->end() 26 | ->booleanNode('tunnel_entity')->defaultFalse()->end() 27 | ->booleanNode('ajax_entity')->defaultFalse()->end() 28 | ->booleanNode('ajax_entity_controller')->defaultFalse()->end() 29 | ->end() 30 | ->end() 31 | ->variableNode('theme_options')->defaultValue(array())->end() 32 | ->end() 33 | ; 34 | 35 | return $treeBuilder; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DependencyInjection/ZenstruckFormExtension.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ZenstruckFormExtension extends Extension 15 | { 16 | public function load(array $configs, ContainerBuilder $container) 17 | { 18 | $processor = new Processor(); 19 | $configuration = new Configuration(); 20 | $config = $processor->processConfiguration($configuration, $configs); 21 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 22 | 23 | if ($config['form_types']['help']) { 24 | $loader->load('help_type.xml'); 25 | } 26 | 27 | if ($config['form_types']['group']) { 28 | $bundles = $container->getParameter('kernel.bundles'); 29 | if (!isset($bundles['ZenstruckSlugifyBundle']) && !isset($bundles['CocurSlugifyBundle'])) { 30 | throw new \Exception('ZenstruckSlugifyBundle or CocurSlugifyBundle must be installed in order to use the "group" type.'); 31 | } 32 | $loader->load('group_type.xml'); 33 | } 34 | 35 | if ($config['form_types']['theme']) { 36 | $container->setParameter('zenstruck_form.theme_options', $config['theme_options']); 37 | $loader->load('theme_type.xml'); 38 | } 39 | 40 | if ($config['form_types']['ajax_entity']) { 41 | $loader->load('ajax_entity_type.xml'); 42 | } 43 | 44 | if ($config['form_types']['ajax_entity_controller']) { 45 | if (!class_exists('\Zend\Crypt\BlockCipher')) { 46 | throw new \Exception('zendframework/zend-crypt must be installed to use the ajax_entity_controller feature.'); 47 | } 48 | 49 | $loader->load('ajax_entity_controller.xml'); 50 | } 51 | 52 | if ($config['form_types']['tunnel_entity']) { 53 | $loader->load('tunnel_entity_type.xml'); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Form/AjaxEntityManager.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class AjaxEntityManager 12 | { 13 | protected $registry; 14 | protected $secret; 15 | 16 | public function __construct(ManagerRegistry $registry, $secret) 17 | { 18 | $this->registry = $registry; 19 | $this->secret = $secret; 20 | } 21 | 22 | public function findEntitiesByMethod($entity, $method, $query, $extra = array()) 23 | { 24 | $className = $this->decriptString($entity); 25 | $method = $this->decriptString($method); 26 | 27 | try { 28 | $repo = $this->registry->getRepository($className); 29 | } catch (\ErrorException $e) { 30 | throw new \InvalidArgumentException('Entity does not exist'); 31 | } 32 | 33 | if (!method_exists($repo, $method)) { 34 | throw new \InvalidArgumentException(sprintf( 35 | 'The method "%s" for "%s" does not exist.', 36 | $method, 37 | get_class($repo) 38 | )); 39 | } 40 | 41 | return $repo->$method($query, $extra); 42 | } 43 | 44 | public function findEntitiesByProperty($entity, $property, $query) 45 | { 46 | $className = $this->decriptString($entity); 47 | $property = $this->decriptString($property); 48 | 49 | $sql = "SELECT e.id, e.$property AS text FROM $className e WHERE e.$property LIKE :query"; 50 | 51 | $em = $this->registry->getManager(); 52 | $dqlQuery = $em->createQuery($sql); 53 | $dqlQuery->setParameter('query', '%'.$query.'%'); 54 | $dqlQuery->setMaxResults(10); 55 | 56 | return $dqlQuery->getResult(); 57 | } 58 | 59 | public function encriptString($string) 60 | { 61 | return $this->getBlockCipher()->encrypt($string); 62 | } 63 | 64 | public function decriptString($string) 65 | { 66 | return $this->getBlockCipher()->decrypt($string); 67 | } 68 | 69 | /** 70 | * @return \Zend\Crypt\BlockCipher 71 | */ 72 | protected function getBlockCipher() 73 | { 74 | $blockCipher = BlockCipher::factory('mcrypt', array('algo' => 'aes')); 75 | $blockCipher->setKey($this->secret); 76 | 77 | return $blockCipher; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Form/DataTransformer/AjaxEntityTransformer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class AjaxEntityTransformer implements DataTransformerInterface 16 | { 17 | protected $repo; 18 | protected $multiple; 19 | protected $property; 20 | 21 | public function __construct(ManagerRegistry $registry, $class, $multiple, $property) 22 | { 23 | $this->repo = $registry->getManager()->getRepository($class); 24 | $this->multiple = $multiple; 25 | $this->property = $property; 26 | } 27 | 28 | public function transform($value) 29 | { 30 | if (is_array($value) || $value instanceof Collection) { 31 | $ret = array(); 32 | 33 | foreach ($value as $entity) { 34 | $ret[] = array( 35 | 'id' => $entity->getId(), 36 | 'text' => $this->getText($entity) 37 | ); 38 | } 39 | 40 | return $ret; 41 | } 42 | 43 | if (is_object($value)) { 44 | return array( 45 | 'id' => $value->getId(), 46 | 'text' => $this->getText($value) 47 | ); 48 | } 49 | 50 | return null; 51 | } 52 | 53 | public function reverseTransform($value) 54 | { 55 | if (!$value) { 56 | return $this->multiple ? array() : null; 57 | } 58 | 59 | if ($this->multiple) { 60 | $ids = explode(',', $value); 61 | $ids = array_unique($ids); 62 | 63 | $qb = $this->repo->createQueryBuilder('entity'); 64 | $qb->where('entity.id IN (:ids)') 65 | ->setParameter('ids', $ids) 66 | ; 67 | 68 | return new ArrayCollection($qb->getQuery()->execute()); 69 | } 70 | 71 | $entity = $this->repo->find($value); 72 | 73 | if (!$entity) { 74 | throw new TransformationFailedException(sprintf( 75 | 'Entity "%s" with id "%s" does not exist.', 76 | $this->repo->getClassName(), 77 | $value 78 | )); 79 | } 80 | 81 | return $entity; 82 | } 83 | 84 | protected function getText($object) 85 | { 86 | if (!$this->property || !class_exists('Symfony\Component\PropertyAccess\PropertyAccess')) { 87 | return (string) $object; 88 | } 89 | 90 | $accessor = PropertyAccess::getPropertyAccessor(); 91 | 92 | return $accessor->getValue($object, $this->property); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Form/Extension/GroupTypeExtension.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class GroupTypeExtension extends AbstractTypeExtension 15 | { 16 | public function buildForm(FormBuilderInterface $builder, array $options) 17 | { 18 | $builder->setAttribute('group', $options['group']); 19 | } 20 | 21 | public function buildView(FormView $view, FormInterface $form, array $options) 22 | { 23 | $view->vars['group'] = $form->getConfig()->getAttribute('group'); 24 | } 25 | 26 | public function setDefaultOptions(OptionsResolverInterface $resolver) 27 | { 28 | $resolver->setDefaults(array( 29 | 'group' => null, 30 | )); 31 | } 32 | 33 | public function getExtendedType() 34 | { 35 | return 'form'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Form/Extension/HelpTypeExtension.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class HelpTypeExtension extends AbstractTypeExtension 15 | { 16 | public function buildForm(FormBuilderInterface $builder, array $options) 17 | { 18 | $builder->setAttribute('help', $options['help']); 19 | } 20 | 21 | public function buildView(FormView $view, FormInterface $form, array $options) 22 | { 23 | $view->vars['help'] = $form->getConfig()->getAttribute('help'); 24 | } 25 | 26 | public function setDefaultOptions(OptionsResolverInterface $resolver) 27 | { 28 | $resolver->setDefaults(array( 29 | 'help' => null, 30 | )); 31 | } 32 | 33 | public function getExtendedType() 34 | { 35 | return 'form'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Form/Extension/ThemeTypeExtension.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ThemeTypeExtension extends AbstractTypeExtension 15 | { 16 | protected $options; 17 | 18 | public function __construct(array $options) 19 | { 20 | $this->options = $options; 21 | } 22 | 23 | public function buildForm(FormBuilderInterface $builder, array $options) 24 | { 25 | $builder->setAttribute('theme_options', array_merge($this->options, $options['theme_options'])); 26 | } 27 | 28 | public function buildView(FormView $view, FormInterface $form, array $options) 29 | { 30 | $view->vars['theme_options'] = $form->getConfig()->getAttribute('theme_options'); 31 | } 32 | 33 | public function setDefaultOptions(OptionsResolverInterface $resolver) 34 | { 35 | $resolver->setDefaults(array( 36 | 'theme_options' => $this->options, 37 | )); 38 | } 39 | 40 | public function getExtendedType() 41 | { 42 | return 'form'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Form/GroupedFormView.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class GroupedFormView 12 | { 13 | protected $groups = array(); 14 | protected $data = array(); 15 | protected $form; 16 | 17 | /** 18 | * @param Form|FormView $form 19 | * @param string $defaultGroup 20 | * @param array $order 21 | */ 22 | public function __construct($form, $defaultGroup = 'Default', $order = array()) 23 | { 24 | if ($form instanceof Form) { 25 | $form = $form->createView(); 26 | } 27 | 28 | $this->form = $form; 29 | 30 | // use custom order 31 | foreach ($order as $item) { 32 | $this->groups[$item] = array(); 33 | } 34 | 35 | // if no order is set, make default first group 36 | if (empty($this->groups)) { 37 | $this->groups[$defaultGroup] = array(); 38 | } 39 | 40 | // add fields to groups 41 | $this->setGroupsFromForm($this->form->children, $defaultGroup); 42 | // filter empty groups 43 | $this->groups = array_filter($this->groups, function ($fields) { 44 | return count($fields); 45 | }); 46 | 47 | } 48 | 49 | public function getForm() 50 | { 51 | return $this->form; 52 | } 53 | 54 | public function isValid($group = null) 55 | { 56 | if (!$group) { 57 | return $this->form->vars['valid']; 58 | } 59 | 60 | $valid = true; 61 | 62 | foreach ($this->groups[$group] as $field) { 63 | if (!$field->vars['valid']) { 64 | $valid = false; 65 | } 66 | } 67 | 68 | return $valid; 69 | } 70 | 71 | public function getGroupNames() 72 | { 73 | return array_keys($this->groups); 74 | } 75 | 76 | public function getGroups() 77 | { 78 | return $this->groups; 79 | } 80 | 81 | public function setData($name, $value) 82 | { 83 | $this->data[$name] = $value; 84 | } 85 | 86 | public function getData($name, $default = null) 87 | { 88 | if (!$this->hasData($name)) { 89 | return $default; 90 | } 91 | 92 | return $this->data[$name]; 93 | } 94 | 95 | public function hasData($name) 96 | { 97 | return array_key_exists($name, $this->data); 98 | } 99 | 100 | public function getVars() 101 | { 102 | return $this->form->vars; 103 | } 104 | 105 | public function setGroupsFromForm($form, $defaultGroup) 106 | { 107 | foreach ($form as $field) { 108 | if ($field->count() && 3 >= count($field->vars['block_prefixes']) && !in_array('collection', $field->vars['block_prefixes'])) { 109 | $this->setGroupsFromForm($field->children, $defaultGroup); 110 | } else { 111 | if ($field->vars['group']) { 112 | $group = $field->vars['group']; 113 | } elseif ($field->parent && $field->parent->vars['group']) { 114 | $group = $field->parent->vars['group']; 115 | } else { 116 | $group = $defaultGroup; 117 | } 118 | $this->groups[$group][] = $field; 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Form/Type/AjaxEntityType.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class AjaxEntityType extends AbstractType 23 | { 24 | protected $registry; 25 | protected $router; 26 | protected $manager; 27 | 28 | public function __construct(ManagerRegistry $registry, RouterInterface $router, AjaxEntityManager $manager = null) 29 | { 30 | $this->registry = $registry; 31 | $this->router = $router; 32 | $this->manager = $manager; 33 | } 34 | 35 | public function buildForm(FormBuilderInterface $builder, array $options) 36 | { 37 | $transformer = new AjaxEntityTransformer( 38 | $this->registry, 39 | $options['class'], 40 | $options['multiple'], 41 | $options['property'] 42 | ); 43 | 44 | $builder->addViewTransformer($transformer); 45 | } 46 | 47 | public function buildView(FormView $view, FormInterface $form, array $options) 48 | { 49 | $value = $view->vars['value']; 50 | $url = $options['url']; 51 | $useController = $options['use_controller']; 52 | $multiple = $options['multiple']; 53 | 54 | if ($value) { 55 | if ($multiple) { 56 | // build id string 57 | $ids = array(); 58 | foreach ($value as $entity) { 59 | $ids[] = $entity['id']; 60 | } 61 | $view->vars['value'] = implode(',', $ids); 62 | } else { 63 | $view->vars['value'] = $value['id']; 64 | } 65 | 66 | $view->vars['attr']['data-initial'] = json_encode($value); 67 | } 68 | 69 | if ($useController || $url) { 70 | $class = 'zenstruck-ajax-entity'; 71 | 72 | if (isset($view->vars['attr']['class'])) { 73 | $class = $view->vars['attr']['class'] . ' ' . $class; 74 | } 75 | 76 | $view->vars['attr']['class'] = $class . ($multiple ? ' multiple' : ''); 77 | 78 | if ($useController) { 79 | if (null === $this->manager) { 80 | throw new MissingOptionsException('Config "zenstruck_form.form_types.ajax_entity_controller" option must be enabled when "use_controller" is true.'); 81 | } 82 | 83 | if (!$options['property'] && !$options['repo_method']) { 84 | throw new MissingOptionsException('Either a property or method option must be set.'); 85 | } 86 | 87 | if ($options['repo_method']) { 88 | $view->vars['attr']['data-method'] = $this->manager->encriptString($options['repo_method']); 89 | } else { 90 | $view->vars['attr']['data-property'] = $this->manager->encriptString($options['property']); 91 | } 92 | 93 | $view->vars['attr']['data-entity'] = $this->manager->encriptString($options['class']); 94 | $url = $this->router->generate('zenstruck_ajax_entity'); 95 | } 96 | 97 | $view->vars['attr']['data-ajax-url'] = $url; 98 | } 99 | 100 | $view->vars['attr']['data-placeholder'] = $options['placeholder']; 101 | 102 | $view->vars['attr']['data-minimum-input-length'] = $options['minimum_input_length']; 103 | 104 | $extraData = $options['extra_data']; 105 | 106 | $serializer = new Serializer(array(), array(new JsonEncoder())); 107 | $view->vars['attr']['data-extra-data'] = $serializer->serialize($extraData, 'json'); 108 | } 109 | 110 | public function setDefaultOptions(OptionsResolverInterface $resolver) 111 | { 112 | $resolver->setRequired(array('class')); 113 | $resolver->setDefaults(array( 114 | 'placeholder' => 'Choose an option', 115 | 'use_controller' => false, 116 | 'url' => null, 117 | 'repo_method' => null, 118 | 'property' => null, 119 | 'multiple' => false, 120 | 'minimum_input_length' => 3, 121 | 'extra_data' => array() 122 | )); 123 | } 124 | 125 | public function getParent() 126 | { 127 | return 'text'; 128 | } 129 | 130 | public function getName() 131 | { 132 | return 'zenstruck_ajax_entity'; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Form/Type/TunnelEntityType.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class TunnelEntityType extends AbstractType 17 | { 18 | protected $registry; 19 | protected $manager; 20 | 21 | public function __construct(ManagerRegistry $registry) 22 | { 23 | $this->registry = $registry; 24 | } 25 | 26 | public function buildForm(FormBuilderInterface $builder, array $options) 27 | { 28 | $transformer = new AjaxEntityTransformer($this->registry, $options['class'], false, null); 29 | $builder->addViewTransformer($transformer); 30 | } 31 | 32 | public function buildView(FormView $view, FormInterface $form, array $options) 33 | { 34 | $value = $view->vars['value']; 35 | 36 | if ($value) { 37 | $view->vars['value'] = $value['id']; 38 | $view->vars['title'] = $value['text']; 39 | } else { 40 | $view->vars['title'] = ''; 41 | } 42 | 43 | $view->vars['attr']['class'] = 'zenstruck-tunnel-id'; 44 | $view->vars['button_text'] = $options['button_text']; 45 | $view->vars['callback'] = $options['callback']; 46 | } 47 | 48 | public function setDefaultOptions(OptionsResolverInterface $resolver) 49 | { 50 | $resolver->setRequired(array('class')); 51 | $resolver->setDefaults(array( 52 | 'button_text' => 'Select...', 53 | 'callback' => null 54 | )); 55 | } 56 | 57 | public function getParent() 58 | { 59 | return 'text'; 60 | } 61 | 62 | public function getName() 63 | { 64 | return 'zenstruck_tunnel_entity'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZenstruckFormBundle 2 | 3 | Provides Twitter Bootstrap form theme, useful FormType Extensions and javascript helpers. 4 | 5 | [View Example Source Code](https://github.com/kbond/sandbox) 6 | 7 | ## Installation 8 | 9 | 1. Add to your `composer.json`: 10 | 11 | ``` 12 | composer require zenstruck/form-bundle:~1.4 13 | ``` 14 | 15 | 2. *Optional* If using the `ajax_entity_controller` feature, add `zendframework/zend-crypt` to your `composer.json`: 16 | 17 | ``` 18 | composer require zendframework/zend-crypt:~2.0,!=2.1.1 19 | ``` 20 | 21 | **Note:** Version 2.1.1 of `zend-crypt` does not have it's autoloader configured correctly. 22 | 23 | 3. *Optional* If using the Grouped form feature, add 24 | [cocur/slugify](https://github.com/cocur/slugify#symfony2) to your `composer.json` 25 | 26 | ``` 27 | composer require cocur/slugify:~0.8 28 | ``` 29 | 30 | 4. Register the bundle with Symfony2: 31 | 32 | ```php 33 | // app/AppKernel.php 34 | 35 | public function registerBundles() 36 | { 37 | $bundles = array( 38 | // ... 39 | new Zenstruck\Bundle\FormBundle\ZenstruckFormBundle(), 40 | 41 | // enable if you want to use the grouped form 42 | // new Cocur\Slugify\Bridge\Symfony\CocurSlugifyBundle() 43 | ); 44 | // ... 45 | } 46 | ``` 47 | 5. If using 'Select2', be sure to download the required files from http://ivaynberg.github.io/select2/ and include the files in your template. 48 | 49 | ``` 50 | //base.html.twig Example 51 | 52 | //... 53 | {% block stylesheets %} 54 | 55 | 56 | //... 57 | {% block javascripts %} 58 | 59 | 60 | 61 | ``` 62 | 63 | 64 | ## Twitter Bootstrap form layout 65 | 66 | To use, do one of the following: 67 | 68 | - Add for a single template: 69 | 70 | ```jinja 71 | {# for bootstrap 2.x #} 72 | {% form_theme form 'ZenstruckFormBundle:Twitter:form_bootstrap_layout.html.twig' %} 73 | 74 | {# for bootstrap 3.x #} 75 | {% form_theme form 'ZenstruckFormBundle:Twitter:form_bootstrap3_layout.html.twig' %} 76 | ``` 77 | 78 | - Add globally in your `config.yml`: 79 | 80 | ```yaml 81 | twig: 82 | form: 83 | resources: 84 | # for bootstrap 2.x 85 | - 'ZenstruckFormBundle:Twitter:form_bootstrap_layout.html.twig' 86 | 87 | # for bootstrap 3.x 88 | - 'ZenstruckFormBundle:Twitter:form_bootstrap3_layout.html.twig' 89 | ``` 90 | 91 | ## FormType Extensions 92 | 93 | ### AjaxEntityType 94 | 95 | ![AjaxEntityType screenshot](https://lh3.googleusercontent.com/-qH5_q34yrjc/URvBEa_eydI/AAAAAAAAKEY/Yywbz7A2OqA/s384/ajax-entity.jpg) 96 | 97 | Creates a `1-m` or `m-m` entity association field. This type simply creates a hidden field that takes 98 | an either 1 or multiple comma separated entity ids. **Note:** Ensure the entity has `__toString()` defined. 99 | 100 | Enable in your `config.yml` (disabled by default): 101 | 102 | ```yaml 103 | zenstruck_form: 104 | form_types: 105 | ajax_entity: true 106 | ``` 107 | 108 | There are several ways to use this type: 109 | 110 | 1. Default - creates a hidden field type. It is up to the user to add functionality. 111 | 112 | ```php 113 | use Symfony\Component\Form\AbstractType; 114 | use Symfony\Component\Form\FormBuilderInterface; 115 | 116 | class MyFormType extends AbstractType 117 | { 118 | public function buildForm(FormBuilderInterface $builder, array $options) 119 | { 120 | $builder 121 | ->add('name', 'zenstruck_ajax_entity', array( 122 | 'class' => 'AppBundle:MyEntity' // ensure MyEntity::__toString() is defined 123 | )) 124 | ; 125 | } 126 | 127 | // ... 128 | } 129 | ``` 130 | 131 | 2. Select2 with built in entity finder (`zendframework/zend-crypt` required): 132 | 133 | Enable the controller in your `config.yml` (disabled by default): 134 | 135 | ```yaml 136 | zenstruck_form: 137 | form_types: 138 | ajax_entity_controller: true 139 | ``` 140 | 141 | Add the route to your `routing.yml`: 142 | 143 | ```yaml 144 | zenstruck_form: 145 | resource: "@ZenstruckFormBundle/Resources/config/ajax_entity_routing.xml" 146 | ``` 147 | 148 | Add to your form type: 149 | 150 | ```php 151 | use Symfony\Component\Form\AbstractType; 152 | use Symfony\Component\Form\FormBuilderInterface; 153 | 154 | class MyFormType extends AbstractType 155 | { 156 | public function buildForm(FormBuilderInterface $builder, array $options) 157 | { 158 | $builder 159 | ->add('name', 'zenstruck_ajax_entity', array( 160 | 'class' => 'AppBundle:MyEntity', // ensure MyEntity::__toString() is defined 161 | 'use_controller' => true, 162 | 'property' => 'name', // the entity property to search by 163 | // 'repo_method' => 'findActive' // for using a custom repository method 164 | // 'extra_data' => array() // for adding extra data in the ajax request (only applicable when using repo_method) 165 | )) 166 | ; 167 | } 168 | 169 | // ... 170 | } 171 | ``` 172 | 173 | **Note:** The URL is dynamically generated for each entity but is encrypted with the application's `secret` for 174 | security purposes. 175 | 176 | 3. Select2 with custom URL. This will create a Select2 widget for this field. 177 | 178 | ```php 179 | use Symfony\Component\Form\AbstractType; 180 | use Symfony\Component\Form\FormBuilderInterface; 181 | 182 | class MyFormType extends AbstractType 183 | { 184 | public function buildForm(FormBuilderInterface $builder, array $options) 185 | { 186 | $builder 187 | ->add('name', 'zenstruck_ajax_entity', array( 188 | 'class' => 'AppBundle:MyEntity', // ensure MyEntity::__toString() is defined 189 | 'url' => '/myentity/find' 190 | )) 191 | ; 192 | } 193 | 194 | // ... 195 | } 196 | ``` 197 | 198 | The url endpoint receives the search string as a `q` request parameter and must return a json encoded array. 199 | Here is an example: 200 | 201 | ```json 202 | [ 203 | {"id":2004,"text":"dolorem"}, 204 | {"id":2008,"text":"inventore"} 205 | ] 206 | ``` 207 | 208 | #### FormType options 209 | 210 | * `class`: The entity the field represents. *Required.* 211 | * `url`: The url that Select2 will send search queries to 212 | * `property`: The entity property to search by (Overrides `url`) 213 | * `method`: The custom repository method to call for searches (Overrides `property`) 214 | * `placeholder`: The Select2 placeholder text. Default: *Choose an option* 215 | * `multiple`: Whether this is allows for multiple values. Default: *false* 216 | * `use_controller`: Whether to use the bundled controller or not (``). Default: *false* 217 | * `repo_method`: For using a custom repository method. Default: *null* 218 | * `extra_data`: For adding extra data in the ajax request (only applicable when using repo_method). Default *array()* 219 | 220 | #### Select2 Javascript Helper 221 | 222 | Enables the [Select2](http://ivaynberg.github.com/select2/) widget for `AjaxEntityType`. Requires 223 | [Select2](https://github.com/ivaynberg/select2/tags). 224 | 225 | Enable with `ZenstruckFormHelper.initSelect2Helper()` 226 | 227 | ### TunnelEntityType 228 | 229 | ![TunnelEntityType screenshot](https://lh3.googleusercontent.com/-G4TtaRInANM/URvBEjb541I/AAAAAAAAKEc/tPOlE47Yj_s/s423/entity-tunnel.jpg) 230 | 231 | Creates an entity association field with a select button. A javascript callback for the select button may be defined. 232 | Can be used for opening a dialog to choose an entity. 233 | 234 | 1. Enable in your `config.yml` (disabled by default): 235 | 236 | ```yaml 237 | zenstruck_form: 238 | form_types: 239 | tunnel_entity: true 240 | ``` 241 | 242 | 2. Add help option to your form fields 243 | 244 | ```php 245 | use Symfony\Component\Form\AbstractType; 246 | use Symfony\Component\Form\FormBuilderInterface; 247 | 248 | class MyFormType extends AbstractType 249 | { 250 | public function buildForm(FormBuilderInterface $builder, array $options) 251 | { 252 | $builder 253 | ->add('name', 'zenstruck_tunnel_entity', array( 254 | 'class' => 'AppBundle:MyEntity', 255 | 'callback' => 'MyApp.selectMyEntity', 256 | 'required' => false 257 | )) 258 | ; 259 | } 260 | 261 | // ... 262 | } 263 | ``` 264 | 265 | The widget html generated by the above example is as follows: 266 | 267 | ```html 268 |
269 | 270 | {{ title }} 271 | 272 | Select... 273 |
274 | ``` 275 | 276 | Your javascript can hook into the clear button and select button. Here are the useful classes: 277 | 278 | * `.zenstruck-tunnel-id`: id of the selected entity 279 | * `.zenstruck-tunnel-title`: title of the selected entity 280 | * `.zenstruck-tunnel-clear`: button that clears the title/id (only available if `required` is `false`) 281 | * `.zenstruck-tunnel-select`: button that initiates the entity selection 282 | 283 | #### FormType options 284 | 285 | * `class`: The entity the field represents. *Required.* 286 | * `callback`: The javascript callback 287 | * `button_text`: The text for the select button. Default: *Select...* 288 | 289 | #### Tunnel Javascript Helper 290 | 291 | Adds events to the clear and select buttons. The select button calls the `callback` defined in the type options. 292 | The callback receives the following parameters: 293 | 294 | - `id`: the id of the currently selected entity (if any) 295 | - `element`: the hidden input element 296 | 297 | Enable with `ZenstruckFormHelper.initTunnelHelper()` 298 | 299 | ### HelpType 300 | 301 | Allow you to add help messages to your form fields. 302 | 303 | 1. Enable in your `config.yml` (disabled by default): 304 | 305 | ```yaml 306 | zenstruck_form: 307 | form_types: 308 | help: true 309 | ``` 310 | 311 | 2. Add help option to your form fields 312 | 313 | ```php 314 | use Symfony\Component\Form\AbstractType; 315 | use Symfony\Component\Form\FormBuilderInterface; 316 | 317 | class MyFormType extends AbstractType 318 | { 319 | public function buildForm(FormBuilderInterface $builder, array $options) 320 | { 321 | $builder 322 | ->add('name', 'text', array( 323 | 'help' => 'Your full name' 324 | )) 325 | ; 326 | } 327 | 328 | // ... 329 | } 330 | ``` 331 | 332 | ### Group Type 333 | 334 | ![Group Type screenshot](https://lh6.googleusercontent.com/-LpFkVgg71nc/UWbRMFGn8cI/AAAAAAAAKFc/XBtlO4b4Uok/s433/form-group.jpg) 335 | 336 | This type allows you group large forms into tabs. 337 | 338 | 1. Enable in your `config.yml` (disabled by default): 339 | 340 | ```yaml 341 | zenstruck_form: 342 | form_types: 343 | group: true 344 | ``` 345 | 346 | 2. Add help option to your form fields 347 | 348 | ```php 349 | use Symfony\Component\Form\AbstractType; 350 | use Symfony\Component\Form\FormBuilderInterface; 351 | 352 | class MyFormType extends AbstractType 353 | { 354 | public function buildForm(FormBuilderInterface $builder, array $options) 355 | { 356 | $builder 357 | ->add('name', 'text', array( 358 | 'group' => 'Foo' 359 | )) 360 | ->add('name', 'text', array( 361 | 'group' => 'Bar' 362 | )) 363 | ; 364 | } 365 | 366 | // ... 367 | } 368 | ``` 369 | 370 | **Note:** fields without a group will be in the first, default tab. 371 | 372 | 3. When creating your form view in your controller, wrap it with `Zenstruck\Bundle\FormBundle\Form\GroupedFormView` 373 | 374 | ```php 375 | class MyController extends Controller 376 | { 377 | public function newAction(Request $request) 378 | { 379 | // ... 380 | return array( 381 | 'grouped_form' => new \Zenstruck\Bundle\FormBundle\Form\GroupedFormView($form->createView()) 382 | ); 383 | } 384 | } 385 | ``` 386 | 387 | **Note:** to name your default tab to something other than *Default*, pass it as the second parameter 388 | to the `GroupedFormView` constructor. 389 | 390 | 4. In your template, include `grouped_form.html.twig` to render the form. 391 | 392 | ```html+jinja 393 |
394 | {% include 'ZenstruckFormBundle:Twitter:grouped_form.html.twig' with { 'grouped_form': grouped_form } %} 395 |
396 | ``` 397 | 398 | **Note:** to use the wrapped form, use `grouped_form.form` 399 | 400 | #### Add custom data to `GroupedFormView` 401 | 402 | ```php 403 | // .. 404 | $groupedForm = new \Zenstruck\Bundle\FormBundle\Form\GroupedFormView($form->createView()); 405 | $groupedForm->setData('foo', 'bar'); 406 | ``` 407 | 408 | In your template: 409 | 410 | ```html+jinja 411 | {# ... #} 412 | {{ grouped_form.data('foo') }} {# returns bar #} 413 | ``` 414 | 415 | #### Custom group order 416 | 417 | ```php 418 | $groupedForm = new GroupedFormView($form->createView(), 'Default', array( 419 | 'Bar', 'Foo', 'Default' 420 | )); 421 | ``` 422 | 423 | ### Theme Type 424 | 425 | Allow you to add theme options to your form fields. The `theme_options` variable will be 426 | available in your form theme. *The bootstrap3 theme currently utilizes.* 427 | 428 | 1. Enable in your `config.yml` (disabled by default): 429 | 430 | ```yaml 431 | zenstruck_form: 432 | form_types: 433 | theme: true 434 | ``` 435 | 436 | 2. Set default theme options in your `config.yml` 437 | 438 | ```yaml 439 | zenstruck_form: 440 | theme_options: 441 | control_width: col-md-4 442 | label_width: col-md-2 443 | # ... 444 | ``` 445 | 446 | 3. Set theme options on a field in your form 447 | 448 | ```php 449 | use Symfony\Component\Form\AbstractType; 450 | use Symfony\Component\Form\FormBuilderInterface; 451 | 452 | class MyFormType extends AbstractType 453 | { 454 | public function buildForm(FormBuilderInterface $builder, array $options) 455 | { 456 | $builder 457 | ->add('name', 'text', array( 458 | 'theme_options' => array('control_width' => 'col-md-6') 459 | )) 460 | ; 461 | } 462 | 463 | // ... 464 | } 465 | ``` 466 | 467 | ## Miscellaneous Javascript helpers 468 | 469 | This bundle comes with a set of useful javascript helpers. To enable, add the following javascipt file (or add to your 470 | assetic javascripts): 471 | 472 | ```html+jinja 473 | 474 | ``` 475 | 476 | Initialize all helpers with: 477 | 478 | ```js 479 | $(function() { 480 | ZenstruckFormHelper.initialize(); 481 | }); 482 | ``` 483 | 484 | ### PostLinkHelper 485 | 486 | Allows a standard `` tag to become a method="POST" link. Add the class `method-post`, `method-post-confirm` 487 | or `method-delete` to an `` tag for it's href value to become a POST link. 488 | 489 | - `method-post`: standard post link (no confirmation) 490 | - `method-post-confirm`: `method-post` with a confirmation dialog that is customizable via the `data-message` attribute 491 | - `method-delete`: cross browser compatible DELETE link with a "Are you sure you want to delete?" confirmation dialog 492 | 493 | Enable with `ZenstruckFormHelper.initPostLinkHelper()` 494 | 495 | ### FormCollectionHelper 496 | 497 | Adds Symfony2 form collection 'add' and 'delete' button functionality. See the 498 | [Symfony2 docs](http://symfony.com/doc/current/cookbook/form/form_collections.html). This works out of the box when 499 | using the `form_bootstrap_layout.html.twig` form layout provided by this bundle. 500 | 501 | **Note:** Do not add the javascript provided in the [Symfony2 cookbook article](http://symfony.com/doc/current/cookbook/form/form_collections.html) 502 | 503 | ## 504 | 505 | Enable with `ZenstruckFormHelper.initFormCollectionHelper()` 506 | 507 | ## Full default config 508 | 509 | ```yaml 510 | zenstruck_form: 511 | form_types: 512 | help: false 513 | group: false 514 | tunnel_entity: false 515 | ajax_entity: false 516 | ajax_entity_controller: false 517 | ``` 518 | -------------------------------------------------------------------------------- /Resources/config/ajax_entity_controller.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Zenstruck\Bundle\FormBundle\Form\AjaxEntityManager 9 | Zenstruck\Bundle\FormBundle\Controller\AjaxEntityController 10 | 11 | 12 | 13 | 14 | 15 | %kernel.secret% 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Resources/config/ajax_entity_routing.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | zenstruck_form.ajax_entity_controller:findAction 9 | POST 10 | 11 | 12 | -------------------------------------------------------------------------------- /Resources/config/ajax_entity_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Zenstruck\Bundle\FormBundle\Form\Type\AjaxEntityType 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Resources/config/group_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Zenstruck\Bundle\FormBundle\Form\Extension\GroupTypeExtension 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Resources/config/help_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Zenstruck\Bundle\FormBundle\Form\Extension\HelpTypeExtension 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Resources/config/theme_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Zenstruck\Bundle\FormBundle\Form\Extension\ThemeTypeExtension 9 | 10 | 11 | 12 | 13 | %zenstruck_form.theme_options% 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Resources/config/tunnel_entity_type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Zenstruck\Bundle\FormBundle\Form\Type\TunnelEntityType 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Kevin Bond 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Resources/public/js/helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper functions 3 | * 4 | * @author Kevin Bond 5 | */ 6 | var ZenstruckFormHelper = { 7 | /** 8 | * Allows a standard tag to become a method="POST" link 9 | * 10 | * Add the class "method-post" or "method-delete" to an tag for it's href value to become a POST link. 11 | * Use the "method-delete" class to generate a confirmation dialog 12 | */ 13 | initPostLinkHelper: function() { 14 | $('a.method-post,a.method-delete,a.method-post-confirm').on('click', function(e) { 15 | var isDelete = $(this).hasClass('method-delete'); 16 | 17 | e.preventDefault(); 18 | 19 | 20 | //check if delete method - show confirmation if is 21 | if (isDelete) { 22 | if (!confirm("Are you sure you want to delete?")) { 23 | return; 24 | } 25 | } 26 | 27 | if ($(this).hasClass('method-post-confirm')) { 28 | var message = $(this).data('message'); 29 | 30 | if (!confirm(message ? message : 'Are you sure?')) { 31 | return; 32 | } 33 | } 34 | 35 | // create form 36 | var $form = $('
').attr('method', 'POST').attr('action', $(this).attr('href')); 37 | 38 | // use delete method if delete link 39 | if (isDelete) { 40 | $form.append(''); 41 | } 42 | 43 | // append and submit 44 | $form.appendTo($('body')); 45 | $form.submit(); 46 | }); 47 | }, 48 | 49 | /** 50 | * Adds Symfony2 form collection add and delete button functionality 51 | */ 52 | initFormCollectionHelper: function() { 53 | // form collection remove button 54 | $('.form-collection').on('click', '.form-collection-element a.remove', function(e) { 55 | e.preventDefault(); 56 | $(this).parents('.form-collection-element').remove(); 57 | }); 58 | 59 | // form collection prototype creation 60 | $('.form-collection-add').on('click', function(e) { 61 | e.preventDefault(); 62 | 63 | var $this = $(this); 64 | var $container = $this.siblings('div[data-prototype]').first(); 65 | var count = $('.form-collection-element', $container).length; 66 | var prototype = $container.data('prototype'); 67 | 68 | // set count 69 | prototype = prototype.replace(/__name__/g, count); 70 | 71 | // create dom element 72 | var $newWidget = $(prototype.trim()); 73 | 74 | $container.children('.form-collection').removeClass('hide').append($newWidget); 75 | 76 | $newWidget.find('.zenstruck-ajax-entity').each(function() { 77 | ZenstruckFormHelper._select2($(this)); 78 | }); 79 | }); 80 | }, 81 | 82 | /** 83 | * Initializes the AjaxEntity Select2 widget 84 | */ 85 | initSelect2Helper: function() { 86 | 87 | $('.zenstruck-ajax-entity').each(function() { 88 | ZenstruckFormHelper._select2($(this)); 89 | }); 90 | }, 91 | 92 | _select2: function($element) { 93 | if(!jQuery().select2) { 94 | return; 95 | } 96 | 97 | var required = $element.attr('required'); 98 | var multiple = $element.hasClass('multiple'); 99 | var method = $element.data('method'); 100 | var property = $element.data('property'); 101 | var entity = $element.data('entity'); 102 | var minimumInputLength = $element.data('minimum-input-length'); 103 | var extraData = $element.data('extra-data'); 104 | // Add icon in result items template 105 | var formatResult = function(result, container, query, escapeMarkup) { 106 | var markup=[]; 107 | if (result.icon) { 108 | markup.push(result.icon+" "); 109 | } 110 | window.Select2.util.markMatch(result.text, query.term, markup, escapeMarkup); 111 | 112 | return markup.join(""); 113 | }; 114 | 115 | var options = { 116 | minimumInputLength: minimumInputLength, 117 | allowClear: !required, 118 | multiple: multiple, 119 | formatResult: formatResult, 120 | placeholder: function(element) { 121 | return $(element).data('placeholder'); 122 | }, 123 | initSelection : function (element, callback) { 124 | var initialData = $(element).data('initial'); 125 | 126 | if (initialData) { 127 | callback(initialData); 128 | } 129 | }, 130 | ajax: { 131 | dataType: 'json', 132 | type: 'post', 133 | data: function (term) { 134 | return { 135 | q: term, 136 | entity: entity, 137 | property: property, 138 | method: method, 139 | extra: extraData 140 | } 141 | }, 142 | results: function (data) { 143 | return { results: data } 144 | } 145 | } 146 | }; 147 | 148 | $element.select2(options); 149 | 150 | if (multiple) { 151 | $element.on('change', function(e) { 152 | if (e.removed) { 153 | var re = new RegExp(e.removed.id, 'g'); 154 | $element.val($element.val().replace(re, '')); 155 | } 156 | }); 157 | } 158 | }, 159 | 160 | initTunnelHelper: function() { 161 | $('.zenstruck-tunnel-select[data-callback]').click(function(e) { 162 | var $this = $(this); 163 | 164 | // create full function name (see http://stackoverflow.com/questions/9228292/javascript-callback-from-form-attribute) 165 | var callback = $this.data('callback'); 166 | var parts = callback.split('.'); 167 | 168 | callback = window; 169 | 170 | $(parts).each(function(){ 171 | callback = callback[this]; 172 | }); 173 | 174 | if (typeof callback === 'function') { 175 | var $element = $this.siblings('.zenstruck-tunnel-id'); 176 | var id = $element.val(); 177 | 178 | callback(id, $element); 179 | } 180 | 181 | e.preventDefault(); 182 | }); 183 | 184 | $('.zenstruck-tunnel-clear').click(function(e) { 185 | $(this) 186 | .siblings('.zenstruck-tunnel-id').val('') 187 | .siblings('.zenstruck-tunnel-title').html('') 188 | ; 189 | 190 | e.preventDefault(); 191 | }); 192 | }, 193 | 194 | initialize: function() { 195 | this.initFormCollectionHelper(); 196 | this.initPostLinkHelper(); 197 | this.initSelect2Helper(); 198 | this.initTunnelHelper(); 199 | } 200 | }; 201 | -------------------------------------------------------------------------------- /Resources/views/Twitter/form_bootstrap3_layout.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'form_div_layout.html.twig' %} 2 | 3 | {% block widget_attributes %} 4 | {% if type is defined and type != 'hidden' %} 5 | {% set attr = attr|merge({'class': (attr.class|default('') ~ ' form-control')|trim}) %} 6 | {% endif %} 7 | 8 | {{ parent() }} 9 | {% endblock widget_attributes %} 10 | 11 | {% block form_row %} 12 | {% spaceless %} 13 |
14 | {{ form_label(form, label|default(null)) }} 15 | {% set ctrl_width = theme_options.control_width is defined ? theme_options.control_width : 'col-md-10' %} 16 |
17 | {{ form_widget(form) }} 18 | {{ form_errors(form) }} 19 | {% if help is defined and help %} 20 |
{{ help }}
21 | {% endif %} 22 |
23 |
24 | {% endspaceless %} 25 | {% endblock form_row %} 26 | 27 | {% block money_widget %} 28 | {% spaceless %} 29 | {% set currency = money_pattern|replace({'{{ widget }}': ''})|trim %} 30 | 31 | {% if currency %} 32 |
33 | {{ currency }} 34 | {{ block('form_widget_simple') }} 35 |
36 | {% else %} 37 | {{ money_pattern|replace({ '{{ widget }}': block('form_widget_simple') })|raw }} 38 | {% endif %} 39 | {% endspaceless %} 40 | {% endblock money_widget %} 41 | 42 | {% block checkbox_widget %} 43 | {% if 'choice' not in form.parent.vars.block_prefixes %} 44 | {% set attr = attr|merge({'class': (attr.class|default('') ~ ' control-standalone')|trim}) %} 45 | {% endif %} 46 | {{ parent() }} 47 | {% endblock checkbox_widget %} 48 | 49 | {% block choice_widget_expanded %} 50 | {% spaceless %} 51 |
52 | {% for child in form %} 53 | {{ form_label(child) }} 54 | {% endfor %} 55 |
56 | {% endspaceless %} 57 | {% endblock choice_widget_expanded %} 58 | 59 | {% block choice_widget_collapsed %} 60 | {% set type = 'select' %} 61 | {{ parent() }} 62 | {% endblock choice_widget_collapsed %} 63 | 64 | {% block form_label %} 65 | {% spaceless %} 66 | {% set is_multichoice_widget = (checked is defined) and ('choice' in form.parent.vars.block_prefixes) %} 67 | 68 | {% if is_multichoice_widget %} 69 | {% set label_attr = label_attr|merge({'class': 'radio' in block_prefixes ? 'radio' : 'checkbox'}) %} 70 | {% else %} 71 | {% set lbl_width = theme_options.label_width is defined ? theme_options.label_width : 'col-md-2' %} 72 | {% set label_attr = label_attr|merge({'class': 'control-label ' ~ lbl_width}) %} 73 | {% endif %} 74 | {% if not compound %} 75 | {% set label_attr = label_attr|merge({'for': id}) %} 76 | {% endif %} 77 | {% if required %} 78 | {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} 79 | {% endif %} 80 | {% if label is empty %} 81 | {% set label = name|humanize %} 82 | {% endif %} 83 | 84 | {{ label|trans({}, translation_domain) }} 85 | {% if is_multichoice_widget %} 86 | {{ form_widget(form) }} 87 | {% endif %} 88 | 89 | {% endspaceless %} 90 | {% endblock form_label %} 91 | 92 | {% block collection_widget %} 93 | {% spaceless %} 94 | {% if prototype is defined %} 95 | {% set attr = attr|merge({'data-prototype': block('collection_prototype') }) %} 96 | {% endif %} 97 |
98 | {{ form_errors(form) }} 99 |
100 | {% for rows in form %} 101 | {{ _self.collection_field(rows, allow_delete) }} 102 | {% endfor %} 103 |
104 | {{ form_rest(form) }} 105 |
106 | {% if allow_add %} 107 |
Add 108 | {% endif %} 109 | {% endspaceless %} 110 | {% endblock collection_widget %} 111 | 112 | {% block collection_prototype %} 113 | {{ _self.collection_field(prototype, allow_delete) }} 114 | {% endblock collection_prototype %} 115 | 116 | {% macro collection_field(rows, allow_delete) %} 117 | {% spaceless %} 118 |
119 | {% for row in rows %} 120 | {{ form_row(row) }} 121 | {% endfor %} 122 | {% if allow_delete %} 123 |
124 |
125 | Delete 126 |
127 |
128 | {% endif %} 129 |
130 | {% endspaceless %} 131 | {% endmacro %} 132 | 133 | {% block form_errors %} 134 | {% spaceless %} 135 | {% if errors|length > 0 %} 136 | 137 | {% for error in errors %} 138 | {{ error.message }} 139 | {% endfor %} 140 | 141 | {% endif %} 142 | {% endspaceless %} 143 | {% endblock form_errors %} 144 | 145 | {% block zenstruck_ajax_entity_widget %} 146 | {% spaceless %} 147 | {% set type = 'hidden' %} 148 | 149 | {% endspaceless %} 150 | {% endblock %} 151 | 152 | {% block zenstruck_tunnel_entity_widget %} 153 | {% spaceless %} 154 | 155 |
156 | 157 | 158 | {% if not required %} 159 | 160 | {% endif %} 161 | {{ button_text|trans }} 162 | 163 |
164 | {% endspaceless %} 165 | {% endblock %} 166 | 167 | {% block textarea_widget -%} 168 | {% set type = 'textarea' %} 169 | {{ parent() }} 170 | {%- endblock textarea_widget %} -------------------------------------------------------------------------------- /Resources/views/Twitter/form_bootstrap_layout.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'form_div_layout.html.twig' %} 2 | 3 | {% block form_row %} 4 | {% spaceless %} 5 |
6 | {{ form_label(form, label|default(null)) }} 7 |
8 | {{ form_widget(form) }} 9 | {{ form_errors(form) }} 10 | {% if help is defined and help %} 11 |
{{ help }}
12 | {% endif %} 13 |
14 |
15 | {% endspaceless %} 16 | {% endblock form_row %} 17 | 18 | {% block money_widget %} 19 | {% spaceless %} 20 | {% set currency = money_pattern|replace({'{{ widget }}': ''})|trim %} 21 | 22 | {% if currency %} 23 |
24 | {{ currency }} 25 | {{ block('form_widget_simple') }} 26 |
27 | {% else %} 28 | {{ money_pattern|replace({ '{{ widget }}': block('form_widget_simple') })|raw }} 29 | {% endif %} 30 | {% endspaceless %} 31 | {% endblock money_widget %} 32 | 33 | {% block checkbox_widget %} 34 | {% if 'choice' not in form.parent.vars.block_prefixes %} 35 | {% set attr = attr|merge({'class': (attr.class|default('') ~ ' control-standalone')|trim}) %} 36 | {% endif %} 37 | {{ parent() }} 38 | {% endblock checkbox_widget %} 39 | 40 | {% block choice_widget_expanded %} 41 | {% spaceless %} 42 |
43 | {% for child in form %} 44 | {{ form_label(child) }} 45 | {% endfor %} 46 |
47 | {% endspaceless %} 48 | {% endblock choice_widget_expanded %} 49 | 50 | {% block form_label %} 51 | {% spaceless %} 52 | {% set is_multichoice_widget = (checked is defined) and ('choice' in form.parent.vars.block_prefixes) %} 53 | 54 | {% if is_multichoice_widget %} 55 | {% set label_attr = label_attr|merge({'class': 'radio' in block_prefixes ? 'radio' : 'checkbox'}) %} 56 | {% else %} 57 | {% set label_attr = label_attr|merge({'class': 'control-label'}) %} 58 | {% endif %} 59 | {% if not compound %} 60 | {% set label_attr = label_attr|merge({'for': id}) %} 61 | {% endif %} 62 | {% if required %} 63 | {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} 64 | {% endif %} 65 | {% if label is empty %} 66 | {% set label = name|humanize %} 67 | {% endif %} 68 | 69 | {{ label|trans({}, translation_domain) }} 70 | {% if is_multichoice_widget %} 71 | {{ form_widget(form) }} 72 | {% endif %} 73 | 74 | {% endspaceless %} 75 | {% endblock form_label %} 76 | 77 | {% block collection_widget %} 78 | {% spaceless %} 79 | {% if prototype is defined %} 80 | {% set attr = attr|merge({'data-prototype': block('collection_prototype') }) %} 81 | {% endif %} 82 |
83 | {{ form_errors(form) }} 84 |
85 | {% for rows in form %} 86 | {{ _self.collection_field(rows, allow_delete) }} 87 | {% endfor %} 88 |
89 | {{ form_rest(form) }} 90 |
91 | {% if allow_add %} 92 | Add 93 | {% endif %} 94 | {% endspaceless %} 95 | {% endblock collection_widget %} 96 | 97 | {% block collection_prototype %} 98 | {{ _self.collection_field(prototype, allow_delete) }} 99 | {% endblock collection_prototype %} 100 | 101 | {% macro collection_field(rows, allow_delete) %} 102 | {% spaceless %} 103 |
104 | {% for row in rows %} 105 | {{ form_row(row) }} 106 | {% endfor %} 107 | {% if allow_delete %} 108 |
109 | Delete 110 |
111 | {% endif %} 112 |
113 | {% endspaceless %} 114 | {% endmacro %} 115 | 116 | {% block form_errors %} 117 | {% spaceless %} 118 | {% if errors|length > 0 %} 119 | 120 | {% for error in errors %} 121 | {{ error.message }} 122 | {% endfor %} 123 | 124 | {% endif %} 125 | {% endspaceless %} 126 | {% endblock form_errors %} 127 | 128 | {% block zenstruck_ajax_entity_widget %} 129 | {% spaceless %} 130 | 131 | {% endspaceless %} 132 | {% endblock %} 133 | 134 | {% block zenstruck_tunnel_entity_widget %} 135 | {% spaceless %} 136 |
137 | 138 | {{ title }} 139 | {% if not required %} 140 | 141 | {% endif %} 142 | {{ button_text|trans }} 143 |
144 | {% endspaceless %} 145 | {% endblock %} 146 | -------------------------------------------------------------------------------- /Resources/views/Twitter/grouped_form.html.twig: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | {% block tabs_content %} 13 | {% for name, group in grouped_form.groups %} 14 |
15 | {% for field in group %} 16 | {{ form_row(field) }} 17 | {% endfor %} 18 |
19 | {% endfor %} 20 | {% endblock %} 21 |
22 | -------------------------------------------------------------------------------- /Tests/Fixtures/App/FormTestBundle/Entity/Author.php: -------------------------------------------------------------------------------- 1 | getName(); 29 | } 30 | 31 | public function getId() 32 | { 33 | return $this->id; 34 | } 35 | 36 | public function setName($name) 37 | { 38 | $this->name = $name; 39 | 40 | return $this; 41 | } 42 | 43 | public function getName() 44 | { 45 | return $this->name; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/Fixtures/App/FormTestBundle/FormTestBundle.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class FormTestBundle extends Bundle 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /Tests/Fixtures/App/TestKernel.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | class TestKernel extends Kernel 10 | { 11 | public function registerBundles() 12 | { 13 | $bundles = array( 14 | new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), 15 | new \Symfony\Bundle\TwigBundle\TwigBundle(), 16 | new \Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), 17 | new \Zenstruck\Bundle\FormBundle\ZenstruckFormBundle(), 18 | new \Cocur\Slugify\Bridge\Symfony\CocurSlugifyBundle(), 19 | new \Zenstruck\Bundle\FormBundle\Tests\Fixtures\App\FormTestBundle\FormTestBundle() 20 | ); 21 | 22 | return $bundles; 23 | } 24 | 25 | public function registerContainerConfiguration(LoaderInterface $loader) 26 | { 27 | $loader->load(__DIR__.'/config/' . $this->environment . '.yml'); 28 | } 29 | 30 | public function getRootDir() 31 | { 32 | return __DIR__; 33 | } 34 | 35 | public function getCacheDir() 36 | { 37 | return sys_get_temp_dir().'/'.Kernel::VERSION.'/cache/'.$this->environment; 38 | } 39 | 40 | public function getLogDir() 41 | { 42 | return sys_get_temp_dir().'/'.Kernel::VERSION.'/logs'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/Fixtures/App/config/default.yml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: test 3 | test: ~ 4 | session: 5 | storage_id: session.storage.filesystem 6 | form: true 7 | csrf_protection: false 8 | router: { resource: "%kernel.root_dir%/config/routing.yml" } 9 | templating: { engines: ['twig'] } 10 | validation: 11 | enabled: true 12 | enable_annotations: true 13 | 14 | zenstruck_form: 15 | form_types: 16 | group: true 17 | 18 | twig: 19 | debug: %kernel.debug% 20 | strict_variables: %kernel.debug% 21 | form: 22 | resources: 23 | - 'ZenstruckFormBundle:Twitter:form_bootstrap_layout.html.twig' 24 | 25 | doctrine: 26 | dbal: 27 | driver: pdo_sqlite 28 | path: %kernel.cache_dir%/db.sqlite 29 | charset: UTF8 30 | orm: 31 | auto_generate_proxy_classes: true 32 | auto_mapping: true 33 | -------------------------------------------------------------------------------- /Tests/Fixtures/App/config/routing.yml: -------------------------------------------------------------------------------- 1 | zenstruck_form: 2 | resource: "@ZenstruckFormBundle/Resources/config/ajax_entity_routing.xml" 3 | -------------------------------------------------------------------------------- /Tests/Form/AjaxEntityManagerTest.php: -------------------------------------------------------------------------------- 1 | getMock('Doctrine\Common\Persistence\ManagerRegistry'); 12 | 13 | $manager = new AjaxEntityManager($registry, '1234'); 14 | 15 | $this->assertEquals('FooBar', $manager->decriptString($manager->encriptString('FooBar'))); 16 | $this->assertNotEquals('FooBar', $manager->encriptString('FooBar')); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/Form/DataTransformer/AjaxEntityTransformerTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class AjaxEntityTransformerTest extends WebTestCase 14 | { 15 | public function testSingleTransform() 16 | { 17 | $client = $this->prepareEnvironment(); 18 | $registry = $client->getContainer()->get('doctrine'); 19 | $transformer = new AjaxEntityTransformer($registry, 'FormTestBundle:Author', false, 'name'); 20 | 21 | $author = $registry->getRepository('FormTestBundle:Author')->find(1); 22 | $transformedValue = $transformer->transform($author); 23 | 24 | $this->assertTrue(is_array($transformedValue)); 25 | $this->assertEquals('1', $transformedValue['id']); 26 | $this->assertEquals('Kevin', $transformedValue['text']); 27 | } 28 | 29 | public function testSingleReverseTransform() 30 | { 31 | $client = $this->prepareEnvironment(); 32 | $registry = $client->getContainer()->get('doctrine'); 33 | $transformer = new AjaxEntityTransformer($registry, 'FormTestBundle:Author', false, 'name'); 34 | 35 | $transformedValue = $transformer->reverseTransform(1); 36 | 37 | $this->assertTrue($transformedValue instanceof Author); 38 | $this->assertEquals(1, $transformedValue->getId()); 39 | $this->assertEquals('Kevin', $transformedValue->getName()); 40 | 41 | $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException'); 42 | $transformedValue = $transformer->reverseTransform(3); 43 | } 44 | 45 | public function testMultipleTransform() 46 | { 47 | $client = $this->prepareEnvironment(); 48 | $registry = $client->getContainer()->get('doctrine'); 49 | $transformer = new AjaxEntityTransformer($registry, 'FormTestBundle:Author', true, 'name'); 50 | 51 | $authors = $registry->getRepository('FormTestBundle:Author')->findAll(); 52 | $transformedValue = $transformer->transform($authors); 53 | 54 | $this->assertTrue(is_array($transformedValue)); 55 | $this->assertEquals('1', $transformedValue[0]['id']); 56 | $this->assertEquals('Kevin', $transformedValue[0]['text']); 57 | } 58 | 59 | public function testMultipleReverseTransform() 60 | { 61 | $client = $this->prepareEnvironment(); 62 | $registry = $client->getContainer()->get('doctrine'); 63 | $transformer = new AjaxEntityTransformer($registry, 'FormTestBundle:Author', true, 'name'); 64 | 65 | $transformedValue = $transformer->reverseTransform("1,2"); 66 | 67 | $this->assertTrue($transformedValue instanceof ArrayCollection); 68 | $this->assertEquals(1, $transformedValue->first()->getId()); 69 | $this->assertEquals('Kevin', $transformedValue->first()->getName()); 70 | $this->assertEquals(2, $transformedValue->get(1)->getId()); 71 | $this->assertEquals('James', $transformedValue->get(1)->getName()); 72 | } 73 | 74 | public function testNullTransform() 75 | { 76 | $client = $this->prepareEnvironment(); 77 | $registry = $client->getContainer()->get('doctrine'); 78 | $transformer = new AjaxEntityTransformer($registry, 'FormTestBundle:Author', false, 'name'); 79 | 80 | $this->assertNull($transformer->transform(null)); 81 | $this->assertNull($transformer->reverseTransform(null)); 82 | 83 | $client = $this->prepareEnvironment(); 84 | $registry = $client->getContainer()->get('doctrine'); 85 | $transformer = new AjaxEntityTransformer($registry, 'FormTestBundle:Author', true, 'name'); 86 | 87 | $this->assertNull($transformer->transform(null)); 88 | $this->assertTrue(is_array($transformer->reverseTransform(null))); 89 | $this->assertEmpty($transformer->reverseTransform(null)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/Form/GroupedFormViewTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class GroupedFormViewTest extends WebTestCase 15 | { 16 | public function testOrder() 17 | { 18 | $client = $this->prepareEnvironment(); 19 | $formBuilder = $client->getContainer()->get('form.factory')->createBuilder(); 20 | 21 | $form = $formBuilder 22 | ->add('name', 'text', array('group' => 'first')) 23 | ->add('address', 'text', array('group' => 'second')) 24 | ->add('notes', 'text') 25 | ->add('posts', 'text', array('group' => 'third')) 26 | ->getForm() 27 | ; 28 | 29 | $groupedForm = new GroupedFormView($form->createView()); 30 | $this->assertEquals(array('Default', 'first', 'second', 'third'), $groupedForm->getGroupNames()); 31 | 32 | $groupedForm = new GroupedFormView($form->createView(), 'Default', array('third')); 33 | $this->assertEquals(array('third', 'first', 'second', 'Default'), $groupedForm->getGroupNames()); 34 | 35 | $groupedForm = new GroupedFormView($form->createView(), 'Default', array('third', 'second')); 36 | $this->assertEquals(array('third', 'second', 'first', 'Default'), $groupedForm->getGroupNames()); 37 | 38 | $groupedForm = new GroupedFormView($form->createView(), 'Default', array('foo', 'third', 'second', 'bar')); 39 | $this->assertEquals(array('third', 'second', 'first', 'Default'), $groupedForm->getGroupNames()); 40 | } 41 | 42 | public function testValid() 43 | { 44 | $collectionConstraint = new Collection(array( 45 | 'name' => new NotBlank(), 46 | 'address' => new NotBlank(), 47 | 'notes' => new NotBlank(), 48 | 'posts' => new NotBlank(), 49 | )); 50 | 51 | $client = $this->prepareEnvironment(); 52 | $formBuilder = $client->getContainer()->get('form.factory')->createBuilder('form', null, array( 53 | 'constraints' => $collectionConstraint 54 | ) 55 | ); 56 | 57 | $form = $formBuilder 58 | ->add('name', 'text', array('group' => 'first')) 59 | ->add('address', 'text', array('group' => 'second')) 60 | ->add('notes', 'text') 61 | ->add('posts', 'text', array('group' => 'third')) 62 | ->getForm() 63 | ; 64 | 65 | $data = array( 66 | 'name' => 'Kevin', 67 | 'address' => 'Canada', 68 | 'notes' => 'Foo', 69 | ); 70 | 71 | $form->bind($data); 72 | 73 | $groupedForm = new GroupedFormView($form->createView()); 74 | 75 | $this->assertFalse($groupedForm->isValid()); 76 | $this->assertTrue($groupedForm->isValid('first')); 77 | $this->assertTrue($groupedForm->isValid('second')); 78 | $this->assertTrue($groupedForm->isValid('Default')); 79 | $this->assertFalse($groupedForm->isValid('third')); 80 | 81 | $data['posts'] = 'Bar'; 82 | 83 | $form = $formBuilder 84 | ->add('name', 'text', array('group' => 'first')) 85 | ->add('address', 'text', array('group' => 'second')) 86 | ->add('notes', 'text') 87 | ->add('posts', 'text', array('group' => 'third')) 88 | ->getForm() 89 | ; 90 | $form->bind($data); 91 | 92 | $groupedForm = new GroupedFormView($form->createView()); 93 | 94 | $this->assertTrue($groupedForm->isValid()); 95 | } 96 | 97 | public function testGroups() 98 | { 99 | $client = $this->prepareEnvironment(); 100 | $formBuilder = $client->getContainer()->get('form.factory')->createBuilder(); 101 | 102 | $form = $formBuilder 103 | ->add('name', 'text', array('group' => 'first')) 104 | ->add('address', 'text', array('group' => 'second')) 105 | ->add('notes', 'text') 106 | ->add('posts', 'text', array('group' => 'third')) 107 | ->getForm() 108 | ; 109 | 110 | $groupedForm = new GroupedFormView($form->createView()); 111 | 112 | $this->assertEquals(4, count($groupedForm->getGroups())); 113 | $this->assertArrayHasKey('first', $groupedForm->getGroups()); 114 | $this->assertArrayHasKey('second', $groupedForm->getGroups()); 115 | $this->assertArrayHasKey('third', $groupedForm->getGroups()); 116 | $this->assertArrayHasKey('Default', $groupedForm->getGroups()); 117 | } 118 | 119 | public function testSetData() 120 | { 121 | $client = $this->prepareEnvironment(); 122 | $formBuilder = $client->getContainer()->get('form.factory')->createBuilder(); 123 | 124 | $form = $formBuilder 125 | ->add('name', 'text') 126 | ->add('address', 'text') 127 | ->add('notes', 'text') 128 | ->add('posts', 'text') 129 | ->getForm() 130 | ; 131 | 132 | $groupedForm = new GroupedFormView($form->createView()); 133 | $groupedForm->setData('foo', 'bar'); 134 | 135 | $this->assertEquals('bar', $groupedForm->getData('foo')); 136 | $this->assertEquals('baz', $groupedForm->getData('bar', 'baz')); 137 | } 138 | 139 | public function testGroupedFormViewDefault() 140 | { 141 | $client = $this->prepareEnvironment(); 142 | $formBuilder = $client->getContainer()->get('form.factory')->createBuilder(); 143 | 144 | $form = $formBuilder 145 | ->add('name', 'text') 146 | ->add('address', 'text') 147 | ->add('notes', 'text') 148 | ->add('posts', 'text') 149 | ->getForm() 150 | ; 151 | 152 | $groupedForm = new GroupedFormView($form->createView()); 153 | 154 | $this->assertEquals(1, count($groupedForm->getGroups())); 155 | $this->assertArrayHasKey('Default', $groupedForm->getGroups()); 156 | 157 | $groups = $groupedForm->getGroups(); 158 | $this->assertEquals(4, count($groups['Default'])); 159 | 160 | $groupedForm = new GroupedFormView($form, 'Main'); 161 | 162 | $this->assertArrayHasKey('Main', $groupedForm->getGroups()); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Tests/Form/Type/AjaxEntityTypeTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class AjaxEntityTypeTest extends WebTestCase 16 | { 17 | public function testDefault() 18 | { 19 | $client = $this->prepareEnvironment(); 20 | $formView = $this->createFormView( 21 | $client, array('class' => 'FormTestBundle:Author') 22 | ); 23 | 24 | $this->assertTrue($formView instanceof FormView); 25 | $this->assertFalse(isset($formView->vars['attr']['data-ajax-url'])); 26 | $this->assertFalse(isset($formView->vars['attr']['data-entity'])); 27 | $this->assertFalse(isset($formView->vars['attr']['data-property'])); 28 | $this->assertFalse(isset($formView->vars['attr']['data-method'])); 29 | } 30 | 31 | public function testCustomUrl() 32 | { 33 | $client = $this->prepareEnvironment(); 34 | $formView = $this->createFormView( 35 | $client, array( 36 | 'class' => 'FormTestBundle:Author', 37 | 'url' => '/foo/bar' 38 | ) 39 | ); 40 | 41 | $this->assertTrue($formView instanceof FormView); 42 | $this->assertEquals('/foo/bar', $formView->vars['attr']['data-ajax-url']); 43 | $this->assertFalse(isset($formView->vars['attr']['data-entity'])); 44 | $this->assertFalse(isset($formView->vars['attr']['data-property'])); 45 | $this->assertFalse(isset($formView->vars['attr']['data-method'])); 46 | } 47 | 48 | public function testAutoUrl() 49 | { 50 | $client = $this->prepareEnvironment(); 51 | $manager = $this->createManager($client); 52 | $formView = $this->createFormView( 53 | $client, 54 | array( 55 | 'class' => 'FormTestBundle:Author', 56 | 'property' => 'name', 57 | 'use_controller' => true, 58 | 'url' => '/foo/bar' 59 | ), 60 | true 61 | ); 62 | 63 | $this->assertTrue($formView instanceof FormView); 64 | $this->assertEquals('/_entity_find', $formView->vars['attr']['data-ajax-url']); 65 | $this->assertTrue(isset($formView->vars['attr']['data-entity'])); 66 | $this->assertTrue(isset($formView->vars['attr']['data-property'])); 67 | $this->assertEquals('FormTestBundle:Author', $manager->decriptString($formView->vars['attr']['data-entity'])); 68 | $this->assertEquals('name', $manager->decriptString($formView->vars['attr']['data-property'])); 69 | 70 | $formView = $this->createFormView( 71 | $client, 72 | array( 73 | 'class' => 'FormTestBundle:Author', 74 | 'property' => 'name', 75 | 'repo_method' => 'findActive', 76 | 'use_controller' => true, 77 | 'url' => '/foo/bar' 78 | ), 79 | true 80 | ); 81 | 82 | $this->assertTrue(isset($formView->vars['attr']['data-entity'])); 83 | $this->assertTrue(isset($formView->vars['attr']['data-method'])); 84 | $this->assertFalse(isset($formView->vars['attr']['data-property'])); 85 | $this->assertEquals('FormTestBundle:Author', $manager->decriptString($formView->vars['attr']['data-entity'])); 86 | $this->assertEquals('findActive', $manager->decriptString($formView->vars['attr']['data-method'])); 87 | 88 | $this->setExpectedException('Symfony\Component\OptionsResolver\Exception\MissingOptionsException'); 89 | 90 | $this->createFormView( 91 | $client, 92 | array( 93 | 'class' => 'FormTestBundle:Author', 94 | 'property' => 'name', 95 | 'use_controller' => true, 96 | 'url' => '/foo/bar' 97 | ), 98 | false 99 | ); 100 | } 101 | 102 | /** 103 | * @param \Symfony\Bundle\FrameworkBundle\Client $client 104 | * @param array $formOptions 105 | * @param bool $controllerEnabled 106 | * 107 | * @return \Symfony\Component\Form\FormView 108 | */ 109 | protected function createFormView(Client $client, array $formOptions, $useManager = true) 110 | { 111 | $registry = $client->getContainer()->get('doctrine'); 112 | $router = $client->getContainer()->get('router'); 113 | 114 | if ($useManager) { 115 | $manager = new AjaxEntityManager($registry, '1234'); 116 | } else { 117 | $manager = null; 118 | } 119 | 120 | /** @var $form Form */ 121 | $form = $client->getContainer()->get('form.factory')->create( 122 | new AjaxEntityType($registry, $router, $manager), 123 | null, 124 | $formOptions 125 | ); 126 | 127 | return $form->createView(); 128 | } 129 | 130 | /** 131 | * @param \Symfony\Bundle\FrameworkBundle\Client $client 132 | * @param bool $controllerEnabled 133 | * 134 | * @return \Zenstruck\Bundle\FormBundle\Form\AjaxEntityManager 135 | */ 136 | protected function createManager(Client $client, $controllerEnabled = true) 137 | { 138 | $registry = $client->getContainer()->get('doctrine'); 139 | 140 | return new AjaxEntityManager($registry, '1234', $controllerEnabled); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Tests/Form/Type/TunnelEntityTypeTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class TunnelEntityTypeTest extends WebTestCase 13 | { 14 | public function testCreation() 15 | { 16 | $client = $this->prepareEnvironment(); 17 | 18 | /** @var $formView FormView */ 19 | $formView = $client->getContainer()->get('form.factory')->create( 20 | new TunnelEntityType($client->getContainer()->get('doctrine')), 21 | null, 22 | array( 23 | 'class' => 'FormTestBundle:Author', 24 | 'callback' => 'MyApp.FindEntity' 25 | ) 26 | )->createView(); 27 | 28 | $this->assertTrue($formView instanceof FormView); 29 | $this->assertEquals('Select...', $formView->vars['button_text']); 30 | $this->assertEquals('MyApp.FindEntity', $formView->vars['callback']); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/Functional/WebTestCase.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class WebTestCase extends BaseWebTestCase 13 | { 14 | /** @var \Doctrine\ORM\EntityManager */ 15 | protected $em; 16 | 17 | protected function prepareEnvironment($environment = 'default') 18 | { 19 | $client = parent::createClient(array('environment' => $environment)); 20 | 21 | $application = new Application($client->getKernel()); 22 | $application->setAutoExit(false); 23 | $this->runConsole($application, "doctrine:database:drop", array("--force" => true)); 24 | $this->runConsole($application, "doctrine:database:create"); 25 | $this->runConsole($application, "doctrine:schema:create"); 26 | 27 | $this->em = $client->getContainer()->get('doctrine.orm.default_entity_manager'); 28 | $this->addTestData(); 29 | 30 | return $client; 31 | } 32 | 33 | protected function runConsole(Application $application, $command, array $options = array()) 34 | { 35 | $options["-e"] = "test"; 36 | $options["-q"] = null; 37 | $options = array_merge($options, array('command' => $command)); 38 | 39 | return $application->run(new \Symfony\Component\Console\Input\ArrayInput($options)); 40 | } 41 | 42 | protected function addTestData() 43 | { 44 | // empty db 45 | $this->em->createQuery('DELETE FormTestBundle:Author') 46 | ->execute() 47 | ; 48 | 49 | $entities[0] = new Author(); 50 | $entities[0]->setName('Kevin'); 51 | $entities[1] = new Author(); 52 | $entities[1]->setName('James'); 53 | 54 | foreach ($entities as $entity) { 55 | $this->em->persist($entity); 56 | } 57 | 58 | $this->em->flush(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ZenstruckFormBundle extends Bundle 11 | { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zenstruck/form-bundle", 3 | "description": "Provides Twitter Bootstrap form theme, a help type extension, Ajax/Tunnel/Select2 entity form types and javascript helpers", 4 | "keywords": ["form"], 5 | "homepage": "http://zenstruck.com/project/ZenstruckFormBundle", 6 | "type": "symfony-bundle", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Kevin Bond", 11 | "email": "kevinbond@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "symfony/symfony": "~2.1" 16 | }, 17 | "require-dev": { 18 | "doctrine/doctrine-bundle": "*", 19 | "doctrine/orm": "*", 20 | "cocur/slugify": "~0.8", 21 | "zendframework/zend-crypt": "~2.0,!=2.1.1" 22 | }, 23 | "suggest": { 24 | "cocur/slugify": "CocurSlugifyBundle is required for the grouped form feature", 25 | "zenstruck/slugify-bundle": "If not using CocurSlugifyBundle, then this is required for the grouped form feature", 26 | "zendframework/zend-crypt": "Required when using the ajax entity type 'use_controller' option. Version: ~2.0,!=2.1.1" 27 | }, 28 | "autoload": { 29 | "psr-0": { "Zenstruck\\Bundle\\FormBundle": "" } 30 | }, 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "1.x-dev" 34 | } 35 | }, 36 | "target-dir": "Zenstruck/Bundle/FormBundle" 37 | } 38 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./Tests/ 21 | 22 | 23 | 24 | 25 | 26 | ./ 27 | 28 | ./Tests 29 | 30 | 31 | 32 | --------------------------------------------------------------------------------