3 namespace Drupal\views_ui\Form\Ajax;
5 use Drupal\Component\Utility\Html;
6 use Drupal\Core\Ajax\AjaxResponse;
7 use Drupal\Core\Ajax\CloseModalDialogCommand;
8 use Drupal\Core\Ajax\OpenModalDialogCommand;
9 use Drupal\Core\Form\FormBase;
10 use Drupal\Core\Form\FormState;
11 use Drupal\Core\Form\FormStateInterface;
12 use Drupal\Core\Render\BubbleableMetadata;
13 use Drupal\Core\Render\RenderContext;
14 use Drupal\views\Ajax\HighlightCommand;
15 use Drupal\views\Ajax\ReplaceTitleCommand;
16 use Drupal\views\Ajax\ShowButtonsCommand;
17 use Drupal\views\Ajax\TriggerPreviewCommand;
18 use Drupal\views\ViewEntityInterface;
19 use Drupal\views_ui\Ajax\SetFormCommand;
20 use Symfony\Component\HttpFoundation\RedirectResponse;
23 * Provides a base class for Views UI AJAX forms.
25 abstract class ViewsFormBase extends FormBase implements ViewsFormInterface {
28 * The ID of the item this form is manipulating.
35 * The type of item this form is manipulating.
42 * Sets the ID for this form.
45 * The ID of the item this form is manipulating.
47 protected function setID($id) {
54 * Sets the type for this form.
57 * The type of the item this form is manipulating.
59 protected function setType($type) {
68 public function getFormState(ViewEntityInterface $view, $display_id, $js) {
69 // $js may already have been converted to a Boolean.
70 $ajax = is_string($js) ? $js === 'ajax' : $js;
71 return (new FormState())
72 ->set('form_id', $this->getFormId())
73 ->set('form_key', $this->getFormKey())
75 ->set('display_id', $display_id)
77 ->set('type', $this->type)
78 ->set('id', $this->id)
80 ->addBuildInfo('callback_object', $this);
86 public function getForm(ViewEntityInterface $view, $display_id, $js) {
87 $form_state = $this->getFormState($view, $display_id, $js);
88 $view = $form_state->get('view');
89 $key = $form_state->get('form_key');
91 // @todo Remove the need for this.
92 \Drupal::moduleHandler()->loadInclude('views_ui', 'inc', 'admin');
94 // Reset the cache of IDs. Drupal rather aggressively prevents ID
95 // duplication but this causes it to remember IDs that are no longer even
99 // check to see if this is the top form of the stack. If it is, pop
100 // it off; if it isn't, the user clicked somewhere else and the stack is
102 if (!empty($view->stack)) {
103 $identifier = implode('-', array_filter([$key, $view->id(), $display_id, $form_state->get('type'), $form_state->get('id')]));
104 // Retrieve the first form from the stack without changing the integer keys,
105 // as they're being used for the "2 of 3" progress indicator.
107 list($key, $top) = each($view->stack);
108 unset($view->stack[$key]);
110 if (array_shift($top) != $identifier) {
115 // Automatically remove the form cache if it is set and the key does
116 // not match. This way navigating away from the form without hitting
118 if (isset($view->form_cache) && $view->form_cache['key'] != $key) {
119 unset($view->form_cache);
122 $form_class = get_class($form_state->getFormObject());
123 $response = $this->ajaxFormWrapper($form_class, $form_state);
125 // If the form has not been submitted, or was not set for rerendering, stop.
126 if (!$form_state->isSubmitted() || $form_state->get('rerender')) {
130 // Sometimes we need to re-generate the form for multi-step type operations.
131 if (!empty($view->stack)) {
132 $stack = $view->stack;
133 $top = array_shift($stack);
135 // Build the new form state for the next form in the stack.
136 $reflection = new \ReflectionClass($view::$forms[$top[1]]);
137 /** @var $form_state \Drupal\Core\Form\FormStateInterface */
138 $form_state = $reflection->newInstanceArgs(array_slice($top, 3, 2))->getFormState($view, $top[2], $form_state->get('ajax'));
139 $form_class = get_class($form_state->getFormObject());
141 $form_state->setUserInput([]);
142 $form_url = views_ui_build_form_url($form_state);
143 if (!$form_state->get('ajax')) {
144 return new RedirectResponse($form_url->setAbsolute()->toString());
146 $form_state->set('url', $form_url);
147 $response = $this->ajaxFormWrapper($form_class, $form_state);
149 elseif (!$form_state->get('ajax')) {
150 // if nothing on the stack, non-js forms just go back to the main view editor.
151 $display_id = $form_state->get('display_id');
152 return new RedirectResponse($this->url('entity.view.edit_display_form', ['view' => $view->id(), 'display_id' => $display_id], ['absolute' => TRUE]));
155 $response = new AjaxResponse();
156 $response->addCommand(new CloseModalDialogCommand());
157 $response->addCommand(new ShowButtonsCommand(!empty($view->changed)));
158 $response->addCommand(new TriggerPreviewCommand());
159 if ($page_title = $form_state->get('page_title')) {
160 $response->addCommand(new ReplaceTitleCommand($page_title));
163 // If this form was for view-wide changes, there's no need to regenerate
164 // the display section of the form.
165 if ($display_id !== '') {
166 \Drupal::entityManager()->getFormObject('view', 'edit')->rebuildCurrentTab($view, $response, $display_id);
173 * Wrapper for handling AJAX forms.
175 * Wrapper around \Drupal\Core\Form\FormBuilderInterface::buildForm() to
176 * handle some AJAX stuff automatically.
177 * This makes some assumptions about the client.
179 * @param \Drupal\Core\Form\FormInterface|string $form_class
180 * The value must be one of the following:
181 * - The name of a class that implements \Drupal\Core\Form\FormInterface.
182 * - An instance of a class that implements \Drupal\Core\Form\FormInterface.
183 * @param \Drupal\Core\Form\FormStateInterface $form_state
184 * The current state of the form.
186 * @return \Drupal\Core\Ajax\AjaxResponse|string|array
187 * Returns one of three possible values:
188 * - A \Drupal\Core\Ajax\AjaxResponse object.
189 * - The rendered form, as a string.
190 * - A render array with the title in #title and the rendered form in the
193 protected function ajaxFormWrapper($form_class, FormStateInterface &$form_state) {
194 /** @var \Drupal\Core\Render\RendererInterface $renderer */
195 $renderer = \Drupal::service('renderer');
197 // This won't override settings already in.
198 if (!$form_state->has('rerender')) {
199 $form_state->set('rerender', FALSE);
201 $ajax = $form_state->get('ajax');
202 // Do not overwrite if the redirect has been disabled.
203 if (!$form_state->isRedirectDisabled()) {
204 $form_state->disableRedirect($ajax);
206 $form_state->disableCache();
208 // Builds the form in a render context in order to ensure that cacheable
209 // metadata is bubbled up.
210 $render_context = new RenderContext();
211 $callable = function () use ($form_class, &$form_state) {
212 return \Drupal::formBuilder()->buildForm($form_class, $form_state);
214 $form = $renderer->executeInRenderContext($render_context, $callable);
216 if (!$render_context->isEmpty()) {
217 BubbleableMetadata::createFromRenderArray($form)
218 ->merge($render_context->pop())
221 $output = $renderer->renderRoot($form);
223 // These forms have the title built in, so set the title here:
224 $title = $form_state->get('title') ?: '';
226 if ($ajax && (!$form_state->isExecuted() || $form_state->get('rerender'))) {
227 // If the form didn't execute and we're using ajax, build up an
228 // Ajax command list to execute.
229 $response = new AjaxResponse();
231 // Attach the library necessary for using the OpenModalDialogCommand and
232 // set the attachments for this Ajax response.
233 $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
234 $response->setAttachments($form['#attached']);
237 $status_messages = ['#type' => 'status_messages'];
238 if ($messages = $renderer->renderRoot($status_messages)) {
239 $display = '<div class="views-messages">' . $messages . '</div>';
244 'dialogClass' => 'views-ui-dialog js-views-ui-dialog',
248 $response->addCommand(new OpenModalDialogCommand($title, $display, $options));
250 // Views provides its own custom handling of AJAX form submissions.
251 // Usually this happens at the same path, but custom paths may be
252 // specified in $form_state.
253 $form_url = $form_state->has('url') ? $form_state->get('url')->toString() : $this->url('<current>');
254 $response->addCommand(new SetFormCommand($form_url));
256 if ($section = $form_state->get('#section')) {
257 $response->addCommand(new HighlightCommand('.' . Html::cleanCssIdentifier($section)));
263 return $title ? ['#title' => $title, '#markup' => $output] : $output;
269 public function validateForm(array &$form, FormStateInterface $form_state) {
275 public function submitForm(array &$form, FormStateInterface $form_state) {