3

I have field collection. The problem is that one field is depended on select list.

When I choose type "job" from select list then company field should be required for that collection. Otherwise company field should be completely omitted in validation.

Screenshot

Update:

I tried to add my own constraint validator but with no effect and I don't have errors on that.

function job_market_user_entity_bundle_field_info_alter(
  &$fields,
  \Drupal\Core\Entity\EntityTypeInterface $entity_type,
  $bundle
) {
  if ($entity_type->id() == 'field_collection_item') {
    /* @var Drupal\field\Entity\FieldConfig $company_field */
    if ($bundle === 'field_jmu_working_experience') {
      $company_field = $fields['field_jmu_company'];
      if ($company_field) {
        $company_field->addConstraint('JobTypeConstraint');
      }
    }
  }
}

Constraint

namespace Drupal\job_market_user\Plugin\Validation\Constraint;

use Drupal\Core\Annotation\Translation;
use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
use Drupal\Core\Validation\Annotation\Constraint;
/**
 * @Constraint(
 *   id = "JobTypeConstraint",
 *   label = @Translation("Job Type", context = "Validation"),
 * )
 */
class JobTypeConstraint extends CompositeConstraintBase {

  /**
   * @var string
   */
  public $message = 'Company field is required for Job working experience';

  /**
   * {@inheritdoc}
   */
  public function coversFields() {
    return ['field_jmu_type', 'field_jmu_company'];
  }
}

Validator (I added violation just to check if it's invoked - no effect yet)

namespace Drupal\job_market_user\Plugin\Validation\Constraint;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/**
 *
 * Supports validating company depended on job type
 */
class JobTypeConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {

  /**
   * Validator 2.5 and upwards compatible execution context.
   *
   * @var \Symfony\Component\Validator\Context\ExecutionContextInterface
   */
  protected $context;

  /**
   * {@inheritdoc}
   */
  public function validate($entity, Constraint $constraint) {
    $this->context->buildViolation($constraint->message)
      ->atPath('field_jmu_company')
      ->addViolation();
    return;
  }

  /**
   * Instantiates a new instance of this class.
   *
   * This is a factory method that returns a new instance of this class. The
   * factory should pass any needed dependencies into the constructor of this
   * class, but not the container itself. Every call to this method must return
   * a new instance of this class; that is, it may not implement a singleton.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The service container this instance should use.
   *
   * @return static
   */
  public static function create(ContainerInterface $container) {
    return new static($container->get('entity_type.manager')->getStorage('user'));
  }
}
4

You can use the States API to make a field visually required, visible/hidden and so on.

One simple example from https://randyfay.com/states

  $form['tests_taken'] = array(
    '#type' => 'checkboxes',
    '#options' => drupal_map_assoc(array(t('SAT'), t('ACT'))),
    '#title' => t('What standardized tests did you take?'),
    '#states' => array(
      'visible' => array(   // action to take.
        ':input[name=student_type]' => array('value' => t('High School')),
      ),
    ),
  );

There are UI modules to configure this, like https://www.drupal.org/project/conditional_fields, but I think none is ported to D8 yet. So you have to form alter it in.

Also, keep in mind that this is UI only. You still need to implement corresponding server-side validation.

4
  • Thanks for answer I've already used states but this covers half of the problem. Field company is hidden on change but this has no impact on validation and I still can't save the collection. I'm trying to write my own plugin for constraint validation. It's pretty similar to CommentNameConstraint from drupal core. If this succeed I'll post the answer :) Aug 23 '16 at 7:11
  • Well, you should be more specific in your question then and write what you already did. But yes, you can not make a conditional field required in field/form API but have to write your own validation.
    – Berdir
    Aug 23 '16 at 7:18
  • Could you check my update now. I have problem with custom validator. It's not invoked and I don't know why? Aug 23 '16 at 8:17
  • I found that constraint is added correctly and when I use $entity->validate in hook_entity_presave I exactly see list of violations. Do you know how to pass those messages to form and prevent node being saved? Aug 23 '16 at 10:04
1

Afterwords for some reason adding constraint in field collection haven't work for me. Maybe this is an issue of field collection module. I found ugly solution that cover my needs.

The first part is adding states for each collection item. They are responsible for showing and hiding company field depends of the job type value. States use jQuery so content is dynamically changed without page reload (nice!).

The second part is adding form validation hook $form['#validate'][].

function job_market_user_form_alter(
  &$form,
  \Drupal\Core\Form\FormStateInterface &$form_state,
  $form_id
) {
  if ($form_id === 'user_working_experience_form') {
    foreach ($form['field_jmu_working_experience']['widget'] as $key => $value) {
      if (isset($form['field_jmu_working_experience']['widget'][$key])) {
        $widget = &$form['field_jmu_working_experience']['widget'][$key];
        if (isset($widget['field_jmu_type'])) {
          $states_when_job_is_selected = array(
            'visible' => array(
              ':input[name="field_jmu_working_experience[' . $key . '][field_jmu_type]"]' => array('value' => 'job'),
            )
          );

          if (isset($widget['field_jmu_company'])) {
            $widget['field_jmu_company']['#states'] = $states_when_job_is_selected;
          }

          $form['#validate'][] = '_form_user_working_experience_validate';
        }
      }
    }
  }

  return $form;
}

In validation:

Company field validation only checks if this field is required. So for ($type !== 'job') I have to usnet require error.

Few weird things going on there which I'll try to explain:

1st I have to cast $collection_item to an array $collection_item_array = (array) $collection_item;. When I use var_dump on this it returns that this variable is an array but when I try to get array element it throws an error

Cannot use object of type Drupal\Core\StringTranslation\TranslatableMarkup as array

Hmm Smells like really bad coding practice because this variable is reference and it type change in other function.

2nd $form_state don't have an method like $form_state->removeError($error) so I have to copy all errors, clear them using $form_state->clearErrors(); unset what I have to unset and set errors. The one more thing about this is you can't get errors and set errors like this:

// Types are not compatible
$errors = $form_state->getErrors();
$form_state->setErrors($errors);

3rd $company_field_name is not name I expected. if you copy name from html it'll look like this:

field_jmu_working_experience[' . $key . '][field_jmu_company][0][target_id]

this is not correct for $form_state->setErrorByName($name, $message = '')

Name should have that structure:

field_jmu_working_experience][' . $key . '][field_jmu_company][0][target_id. This is explained here

Finally Validation function looks like this.

function _form_user_working_experience_validate(
  &$form,
  \Drupal\Core\Form\FormStateInterface &$form_state
) {
  $collection_items = $form_state->getValue('field_jmu_working_experience');
  foreach ($collection_items as $key => $collection_item) {
    $collection_item_array = (array) $collection_item;
    $type_field = isset($collection_item_array['field_jmu_type']) ? $collection_item_array['field_jmu_type'] : [];
    $company_field_name = 'field_jmu_working_experience][' . $key . '][field_jmu_company][0][target_id';

    if (!empty($type_field)) {
      $type = $type_field[0]['value'];

      if ($type !== 'job') {
        $errors = $form_state->getErrors();
        $form_state->clearErrors();

        if (isset($errors[$company_field_name])) {
          //unset required
          unset($errors[$company_field_name]);
        }
        /* @var Drupal\Core\StringTranslation\TranslatableMarkup $error*/
        foreach ($errors as $err_key => $error) {
          $form_state->setErrorByName($err_key, $error->render());
        }
      }
    }
  }
}

I hope in the future all issues were be resolved and we'll be able to use Constraints.

0

You can actually use the states API to do what you want without all of that workaround.

put this in your array:

  '#states' => array(
    'enabled' => array(
      array(':input[name="Country"]' => array('value' => 'US')),
    ),
    'visible' => array(
      array(':input[name="Country"]' => array('value' => 'US')),
    ),

    'required' => array(
      array(':input[name="Country"]' => array('value' => 'US')),
    ),
  ),

This makes the current selection required only if USA was chosen.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.