3 namespace Drupal\ctools\Wizard;
5 use Drupal\Core\Ajax\AjaxResponse;
6 use Drupal\Core\Ajax\CloseModalDialogCommand;
7 use Drupal\Core\DependencyInjection\ClassResolverInterface;
8 use Drupal\Core\Form\FormBase;
9 use Drupal\Core\Form\FormBuilderInterface;
10 use Drupal\Core\Form\FormInterface;
11 use Drupal\Core\Form\FormStateInterface;
12 use Drupal\Core\Routing\RouteMatchInterface;
14 use Drupal\ctools\Ajax\OpenModalWizardCommand;
15 use Drupal\ctools\Event\WizardEvent;
16 use Drupal\user\SharedTempStoreFactory;
17 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
20 * The base class for all form wizard.
22 abstract class FormWizardBase extends FormBase implements FormWizardInterface {
25 * Tempstore Factory for keeping track of values in each step of the wizard.
27 * @var \Drupal\user\SharedTempStoreFactory
34 * @var \Drupal\Core\Form\FormBuilderInterface
41 * @var \Drupal\Core\DependencyInjection\ClassResolverInterface;
43 protected $classResolver;
46 * The event dispatcher.
48 * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
50 protected $dispatcher;
53 * The shared temp store factory collection name.
57 protected $tempstore_id;
60 * The SharedTempStore key for our current wizard values.
64 protected $machine_name;
67 * The current active step of the wizard.
74 * @param \Drupal\user\SharedTempStoreFactory $tempstore
75 * Tempstore Factory for keeping track of values in each step of the
77 * @param \Drupal\Core\Form\FormBuilderInterface $builder
79 * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
81 * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
82 * The event dispatcher.
83 * @param $tempstore_id
84 * The shared temp store factory collection name.
85 * @param null $machine_name
86 * The SharedTempStore key for our current wizard values.
88 * The current active step of the wizard.
90 public function __construct(SharedTempStoreFactory $tempstore, FormBuilderInterface $builder, ClassResolverInterface $class_resolver, EventDispatcherInterface $event_dispatcher, RouteMatchInterface $route_match, $tempstore_id, $machine_name = NULL, $step = NULL) {
91 $this->tempstore = $tempstore;
92 $this->builder = $builder;
93 $this->classResolver = $class_resolver;
94 $this->dispatcher = $event_dispatcher;
95 $this->routeMatch = $route_match;
96 $this->tempstore_id = $tempstore_id;
97 $this->machine_name = $machine_name;
104 public static function getParameters() {
106 'tempstore' => \Drupal::service('user.shared_tempstore'),
107 'builder' => \Drupal::service('form_builder'),
108 'class_resolver' => \Drupal::service('class_resolver'),
109 'event_dispatcher' => \Drupal::service('event_dispatcher'),
116 public function initValues() {
118 $event = new WizardEvent($this, $values);
119 $this->dispatcher->dispatch(FormWizardInterface::LOAD_VALUES, $event);
120 return $event->getValues();
126 public function getTempstoreId() {
127 return $this->tempstore_id;
133 public function getTempstore() {
134 return $this->tempstore->get($this->getTempstoreId());
140 public function getMachineName() {
141 return $this->machine_name;
147 public function getStep($cached_values) {
149 $operations = $this->getOperations($cached_values);
150 $steps = array_keys($operations);
151 $this->step = reset($steps);
159 public function getOperation($cached_values) {
160 $operations = $this->getOperations($cached_values);
161 $step = $this->getStep($cached_values);
162 if (!empty($operations[$step])) {
163 return $operations[$step];
165 $operation = reset($operations);
170 * The translated text of the "Next" button's text.
174 public function getNextOp() {
175 return $this->t('Next');
181 public function getNextParameters($cached_values) {
182 // Get the steps by key.
183 $operations = $this->getOperations($cached_values);
184 $steps = array_keys($operations);
185 // Get the steps after the current step.
186 $after = array_slice($operations, array_search($this->getStep($cached_values), $steps) + 1);
187 // Get the steps after the current step by key.
188 $after_keys = array_keys($after);
189 $step = reset($after_keys);
191 $keys = array_keys($operations);
195 'machine_name' => $this->getMachineName(),
204 public function getPreviousParameters($cached_values) {
205 $operations = $this->getOperations($cached_values);
206 $step = $this->getStep($cached_values);
208 // Get the steps by key.
209 $steps = array_keys($operations);
210 // Get the steps before the current step.
211 $before = array_slice($operations, 0, array_search($step, $steps));
212 // Get the steps before the current step by key.
213 $before = array_keys($before);
214 // Reverse the steps for easy access to the next step.
215 $before_steps = array_reverse($before);
216 $step = reset($before_steps);
218 'machine_name' => $this->getMachineName(),
227 public function getFormId() {
228 if (!$this->getMachineName() || !$this->getTempstore()->get($this->getMachineName())) {
229 $cached_values = $this->initValues();
232 $cached_values = $this->getTempstore()->get($this->getMachineName());
234 $operation = $this->getOperation($cached_values);
235 /* @var $operation \Drupal\Core\Form\FormInterface */
236 $operation = $this->classResolver->getInstanceFromDefinition($operation['form']);
237 return $operation->getFormId();
243 public function buildForm(array $form, FormStateInterface $form_state) {
244 $cached_values = $form_state->getTemporaryValue('wizard');
245 // Get the current form operation.
246 $operation = $this->getOperation($cached_values);
247 $form = $this->customizeForm($form, $form_state);
248 /* @var $formClass \Drupal\Core\Form\FormInterface */
249 $formClass = $this->classResolver->getInstanceFromDefinition($operation['form']);
250 // Pass include any custom values for this operation.
251 if (!empty($operation['values'])) {
252 $cached_values = array_merge($cached_values, $operation['values']);
253 $form_state->setTemporaryValue('wizard', $cached_values);
256 $form = $formClass->buildForm($form, $form_state);
257 if (isset($operation['title'])) {
258 $form['#title'] = $operation['title'];
260 $form['actions'] = $this->actions($formClass, $form_state);
267 public function validateForm(array &$form, FormStateInterface $form_state) {}
272 public function submitForm(array &$form, FormStateInterface $form_state) {
273 // Only perform this logic if we're moving to the next page. This prevents
274 // the loss of cached values on ajax submissions.
275 if ((string)$form_state->getValue('op') == (string)$this->getNextOp()) {
276 $cached_values = $form_state->getTemporaryValue('wizard');
277 if ($form_state->hasValue('label')) {
278 $cached_values['label'] = $form_state->getValue('label');
280 if ($form_state->hasValue('id')) {
281 $cached_values['id'] = $form_state->getValue('id');
283 if (is_null($this->machine_name) && !empty($cached_values['id'])) {
284 $this->machine_name = $cached_values['id'];
286 $this->getTempstore()->set($this->getMachineName(), $cached_values);
287 if (!$form_state->get('ajax')) {
288 $form_state->setRedirect($this->getRouteName(), $this->getNextParameters($cached_values));
296 public function populateCachedValues(array &$form, FormStateInterface $form_state) {
297 $cached_values = $this->getTempstore()->get($this->getMachineName());
298 if (!$cached_values) {
299 $cached_values = $form_state->getTemporaryValue('wizard');
300 if (!$cached_values) {
301 $cached_values = $this->initValues();
302 $form_state->setTemporaryValue('wizard', $cached_values);
310 public function previous(array &$form, FormStateInterface $form_state) {
311 $cached_values = $form_state->getTemporaryValue('wizard');
312 $form_state->setRedirect($this->getRouteName(), $this->getPreviousParameters($cached_values));
318 public function finish(array &$form, FormStateInterface $form_state) {
319 $this->getTempstore()->delete($this->getMachineName());
323 * Helper function for generating default form elements.
326 * @param \Drupal\Core\Form\FormStateInterface $form_state
330 protected function customizeForm(array $form, FormStateInterface $form_state) {
331 // Setup the step rendering theme element.
333 '#theme' => ['ctools_wizard_trail'],
335 '#cached_values' => $form_state->getTemporaryValue('wizard'),
337 // @todo properly inject the renderer.
338 $form['#prefix'] = \Drupal::service('renderer')->render($prefix);
343 * Generates action elements for navigating between the operation steps.
345 * @param \Drupal\Core\Form\FormInterface $form_object
346 * The current operation form.
347 * @param \Drupal\Core\Form\FormStateInterface $form_state
348 * The current form state.
352 protected function actions(FormInterface $form_object, FormStateInterface $form_state) {
353 $cached_values = $form_state->getTemporaryValue('wizard');
354 $operations = $this->getOperations($cached_values);
355 $step = $this->getStep($cached_values);
356 $operation = $operations[$step];
358 $steps = array_keys($operations);
359 // Slice to find the operations that occur before the current operation.
360 $before = array_slice($operations, 0, array_search($step, $steps));
361 // Slice to find the operations that occur after the current operation.
362 $after = array_slice($operations, array_search($step, $steps) + 1);
367 '#value' => $this->t('Next'),
368 '#button_type' => 'primary',
370 '::populateCachedValues',
371 [$form_object, 'validateForm'],
374 [$form_object, 'submitForm'],
379 // Add any submit or validate functions for the step and the global ones.
380 if (isset($operation['validate'])) {
381 $actions['submit']['#validate'] = array_merge($actions['submit']['#validate'], $operation['validate']);
383 $actions['submit']['#validate'][] = '::validateForm';
384 if (isset($operation['submit'])) {
385 $actions['submit']['#submit'] = array_merge($actions['submit']['#submit'], $operation['submit']);
387 $actions['submit']['#submit'][] = '::submitForm';
389 if ($form_state->get('ajax')) {
390 // Ajax submissions need to submit to the current step, not "next".
391 $parameters = $this->getNextParameters($cached_values);
392 $parameters['step'] = $this->getStep($cached_values);
393 $actions['submit']['#ajax'] = [
394 'callback' => '::ajaxSubmit',
395 'url' => Url::fromRoute($this->getRouteName(), $parameters),
396 'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]],
400 // If there are steps before this one, label the button "previous"
401 // otherwise do not display a button.
403 $actions['previous'] = array(
405 '#value' => $this->t('Previous'),
406 '#validate' => array(
407 array($this, 'populateCachedValues'),
410 array($this, 'previous'),
412 '#limit_validation_errors' => array(),
415 if ($form_state->get('ajax')) {
416 // Ajax submissions need to submit to the current step, not "previous".
417 $parameters = $this->getPreviousParameters($cached_values);
418 $parameters['step'] = $this->getStep($cached_values);
419 $actions['previous']['#ajax'] = [
420 'callback' => '::ajaxPrevious',
421 'url' => Url::fromRoute($this->getRouteName(), $parameters),
422 'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]],
427 // If there are not steps after this one, label the button "Finish".
429 $actions['submit']['#value'] = $this->t('Finish');
430 $actions['submit']['#submit'][] = array($this, 'finish');
431 if ($form_state->get('ajax')) {
432 $actions['submit']['#ajax']['callback'] = [$this, 'ajaxFinish'];
439 public function ajaxSubmit(array $form, FormStateInterface $form_state) {
440 $cached_values = $form_state->getTemporaryValue('wizard');
441 $response = new AjaxResponse();
442 $parameters = $this->getNextParameters($cached_values);
443 $response->addCommand(new OpenModalWizardCommand($this, $this->getTempstoreId(), $parameters));
447 public function ajaxPrevious(array $form, FormStateInterface $form_state) {
448 $cached_values = $form_state->getTemporaryValue('wizard');
449 $response = new AjaxResponse();
450 $parameters = $this->getPreviousParameters($cached_values);
451 $response->addCommand(new OpenModalWizardCommand($this, $this->getTempstoreId(), $parameters));
455 public function ajaxFinish(array $form, FormStateInterface $form_state) {
456 $response = new AjaxResponse();
457 $response->addCommand(new CloseModalDialogCommand());
461 public function getRouteName() {
462 return $this->routeMatch->getRouteName();