├── .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 |  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 |  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 |
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 |  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 | 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 |