Issue #2347469 by mondrake, Wim Leers, larowlan, kattekrab, mnico, tadityar: Rendering forms in AjaxResponses does not attach assets automatically
parent
23e9d7f16e
commit
8ee035826c
|
@ -20,6 +20,7 @@ use Drupal\Component\Utility\SortArray;
|
|||
use Drupal\Component\Utility\String;
|
||||
use Drupal\Component\Utility\Tags;
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Core\Asset\AttachedAssets;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Site\Settings;
|
||||
|
@ -32,6 +33,7 @@ use Drupal\Core\Datetime\DrupalDateTime;
|
|||
use Drupal\Core\Routing\GeneratorNotInitializedException;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Render\Renderer;
|
||||
use Drupal\Core\Session\AnonymousUserSession;
|
||||
|
||||
/**
|
||||
|
@ -852,7 +854,7 @@ function drupal_js_defaults($data = NULL) {
|
|||
* Merges two #attached arrays.
|
||||
*
|
||||
* The values under the 'drupalSettings' key are merged in a special way, to
|
||||
* match the behavior of
|
||||
* match the behavior of:
|
||||
*
|
||||
* @code
|
||||
* jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...)
|
||||
|
@ -889,15 +891,12 @@ function drupal_js_defaults($data = NULL) {
|
|||
*
|
||||
* @return array
|
||||
* The merged #attached array.
|
||||
*
|
||||
* @deprecated To be removed in Drupal 8.0.x. Use
|
||||
* \Drupal\Core\Render\Renderer::mergeAttachments() instead.
|
||||
*/
|
||||
function drupal_merge_attached(array $a, array $b) {
|
||||
// If both #attached arrays contain drupalSettings, then merge them correctly;
|
||||
// adding the same settings multiple times needs to behave idempotently.
|
||||
if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
|
||||
$a['drupalSettings'] = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE);
|
||||
unset($b['drupalSettings']);
|
||||
}
|
||||
return NestedArray::mergeDeep($a, $b);
|
||||
return Renderer::mergeAttachments($a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -34,7 +34,7 @@ class AfterCommand extends InsertCommand {
|
|||
'command' => 'insert',
|
||||
'method' => 'after',
|
||||
'selector' => $this->selector,
|
||||
'data' => $this->html,
|
||||
'data' => $this->getRenderedContent(),
|
||||
'settings' => $this->settings,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
namespace Drupal\Core\Ajax;
|
||||
|
||||
use Drupal\Core\Asset\AttachedAssets;
|
||||
use Drupal\Core\Render\Renderer;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
@ -71,6 +72,15 @@ class AjaxResponse extends JsonResponse {
|
|||
else {
|
||||
$this->commands[] = $command->render();
|
||||
}
|
||||
if ($command instanceof CommandWithAttachedAssetsInterface) {
|
||||
$assets = $command->getAttachedAssets();
|
||||
$attachments = [
|
||||
'library' => $assets->getLibraries(),
|
||||
'drupalSettings' => $assets->getSettings(),
|
||||
];
|
||||
$attachments = Renderer::mergeAttachments($this->attachments, $attachments);
|
||||
$this->setAttachments($attachments);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class AppendCommand extends InsertCommand {
|
|||
'command' => 'insert',
|
||||
'method' => 'append',
|
||||
'selector' => $this->selector,
|
||||
'data' => $this->html,
|
||||
'data' => $this->getRenderedContent(),
|
||||
'settings' => $this->settings,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class BeforeCommand extends InsertCommand {
|
|||
'command' => 'insert',
|
||||
'method' => 'before',
|
||||
'selector' => $this->selector,
|
||||
'data' => $this->html,
|
||||
'data' => $this->getRenderedContent(),
|
||||
'settings' => $this->settings,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Ajax\CommandWithAttachedAssetsInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Ajax;
|
||||
|
||||
/**
|
||||
* Interface for Ajax commands that render content and attach assets.
|
||||
*
|
||||
* All Ajax commands that render HTML should implement these methods
|
||||
* to be able to return attached assets to the calling AjaxResponse object.
|
||||
*
|
||||
* @ingroup ajax
|
||||
*/
|
||||
interface CommandWithAttachedAssetsInterface {
|
||||
|
||||
/**
|
||||
* Gets the attached assets.
|
||||
*
|
||||
* @return \Drupal\Core\Asset\AttachedAssets|null
|
||||
* The attached assets for this command.
|
||||
*/
|
||||
public function getAttachedAssets();
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Ajax\CommandWithAttachedAssetsTrait.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Ajax;
|
||||
|
||||
use Drupal\Core\Asset\AttachedAssets;
|
||||
|
||||
/**
|
||||
* Trait for Ajax commands that render content and attach assets.
|
||||
*
|
||||
* @ingroup ajax
|
||||
*/
|
||||
trait CommandWithAttachedAssetsTrait {
|
||||
|
||||
/**
|
||||
* The attached assets for this Ajax command.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\AttachedAssets
|
||||
*/
|
||||
protected $attachedAssets;
|
||||
|
||||
/**
|
||||
* Processes the content for output.
|
||||
*
|
||||
* If content is a render array, it may contain attached assets to be
|
||||
* processed.
|
||||
*
|
||||
* @return string
|
||||
* HTML rendered content.
|
||||
*/
|
||||
protected function getRenderedContent() {
|
||||
$this->attachedAssets = new AttachedAssets();
|
||||
if (is_array($this->content)) {
|
||||
$html = \Drupal::service('renderer')->render($this->content);
|
||||
$this->attachedAssets = AttachedAssets::createFromRenderArray($this->content);
|
||||
return $html;
|
||||
}
|
||||
else {
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attached assets.
|
||||
*
|
||||
* @return \Drupal\Core\Asset\AttachedAssets|null
|
||||
* The attached assets for this command.
|
||||
*/
|
||||
public function getAttachedAssets() {
|
||||
return $this->attachedAssets;
|
||||
}
|
||||
|
||||
}
|
|
@ -34,7 +34,7 @@ class HtmlCommand extends InsertCommand {
|
|||
'command' => 'insert',
|
||||
'method' => 'html',
|
||||
'selector' => $this->selector,
|
||||
'data' => $this->html,
|
||||
'data' => $this->getRenderedContent(),
|
||||
'settings' => $this->settings,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ use Drupal\Core\Ajax\CommandInterface;
|
|||
*
|
||||
* @ingroup ajax
|
||||
*/
|
||||
class InsertCommand implements CommandInterface {
|
||||
class InsertCommand implements CommandInterface, CommandWithAttachedAssetsInterface {
|
||||
|
||||
use CommandWithAttachedAssetsTrait;
|
||||
|
||||
/**
|
||||
* A CSS selector string.
|
||||
|
@ -34,11 +36,13 @@ class InsertCommand implements CommandInterface {
|
|||
protected $selector;
|
||||
|
||||
/**
|
||||
* The HTML content that will replace the matched element(s).
|
||||
* The content for the matched element(s).
|
||||
*
|
||||
* @var string
|
||||
* Either a render array or an HTML string.
|
||||
*
|
||||
* @var string|array
|
||||
*/
|
||||
protected $html;
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* A settings array to be passed to any any attached JavaScript behavior.
|
||||
|
@ -52,14 +56,15 @@ class InsertCommand implements CommandInterface {
|
|||
*
|
||||
* @param string $selector
|
||||
* A CSS selector.
|
||||
* @param string $html
|
||||
* String of HTML that will replace the matched element(s).
|
||||
* @param string|array $content
|
||||
* The content that will be inserted in the matched element(s), either a
|
||||
* render array or an HTML string.
|
||||
* @param array $settings
|
||||
* An array of JavaScript settings to be passed to any attached behaviors.
|
||||
*/
|
||||
public function __construct($selector, $html, array $settings = NULL) {
|
||||
public function __construct($selector, $content, array $settings = NULL) {
|
||||
$this->selector = $selector;
|
||||
$this->html = $html;
|
||||
$this->content = $content;
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
|
@ -72,7 +77,7 @@ class InsertCommand implements CommandInterface {
|
|||
'command' => 'insert',
|
||||
'method' => NULL,
|
||||
'selector' => $this->selector,
|
||||
'data' => $this->html,
|
||||
'data' => $this->getRenderedContent(),
|
||||
'settings' => $this->settings,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@ use Drupal\Core\Ajax\CommandInterface;
|
|||
*
|
||||
* @ingroup ajax
|
||||
*/
|
||||
class OpenDialogCommand implements CommandInterface {
|
||||
class OpenDialogCommand implements CommandInterface, CommandWithAttachedAssetsInterface {
|
||||
|
||||
use CommandWithAttachedAssetsTrait;
|
||||
|
||||
/**
|
||||
* The selector of the dialog.
|
||||
|
@ -31,11 +33,13 @@ class OpenDialogCommand implements CommandInterface {
|
|||
protected $title;
|
||||
|
||||
/**
|
||||
* HTML content that will placed in the dialog.
|
||||
* The content for the dialog.
|
||||
*
|
||||
* @var string
|
||||
* Either a render array or an HTML string.
|
||||
*
|
||||
* @var string|array
|
||||
*/
|
||||
protected $html;
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* Stores dialog-specific options passed directly to jQuery UI dialogs. Any
|
||||
|
@ -60,8 +64,9 @@ class OpenDialogCommand implements CommandInterface {
|
|||
* The selector of the dialog.
|
||||
* @param string $title
|
||||
* The title of the dialog.
|
||||
* @param string $html
|
||||
* HTML that will be placed in the dialog.
|
||||
* @param string|array $content
|
||||
* The content that will be placed in the dialog, either a render array
|
||||
* or an HTML string.
|
||||
* @param array $dialog_options
|
||||
* (optional) Options to be passed to the dialog implementation. Any
|
||||
* jQuery UI option can be used. See http://api.jqueryui.com/dialog.
|
||||
|
@ -70,10 +75,10 @@ class OpenDialogCommand implements CommandInterface {
|
|||
* on the content of the dialog. If left empty, the settings will be
|
||||
* populated automatically from the current request.
|
||||
*/
|
||||
public function __construct($selector, $title, $html, array $dialog_options = array(), $settings = NULL) {
|
||||
public function __construct($selector, $title, $content, array $dialog_options = array(), $settings = NULL) {
|
||||
$dialog_options += array('title' => $title);
|
||||
$this->selector = $selector;
|
||||
$this->html = $html;
|
||||
$this->content = $content;
|
||||
$this->dialogOptions = $dialog_options;
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
@ -131,7 +136,7 @@ class OpenDialogCommand implements CommandInterface {
|
|||
'command' => 'openDialog',
|
||||
'selector' => $this->selector,
|
||||
'settings' => $this->settings,
|
||||
'data' => $this->html,
|
||||
'data' => $this->getRenderedContent(),
|
||||
'dialogOptions' => $this->dialogOptions,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,8 +25,9 @@ class OpenModalDialogCommand extends OpenDialogCommand {
|
|||
*
|
||||
* @param string $title
|
||||
* The title of the dialog.
|
||||
* @param string $html
|
||||
* HTML that will be placed in the dialog.
|
||||
* @param string|array $content
|
||||
* The content that will be placed in the dialog, either a render array
|
||||
* or an HTML string.
|
||||
* @param array $dialog_options
|
||||
* (optional) Settings to be passed to the dialog implementation. Any
|
||||
* jQuery UI option can be used. See http://api.jqueryui.com/dialog.
|
||||
|
@ -35,8 +36,8 @@ class OpenModalDialogCommand extends OpenDialogCommand {
|
|||
* on the content of the dialog. If left empty, the settings will be
|
||||
* populated automatically from the current request.
|
||||
*/
|
||||
public function __construct($title, $html, array $dialog_options = array(), $settings = NULL) {
|
||||
public function __construct($title, $content, array $dialog_options = array(), $settings = NULL) {
|
||||
$dialog_options['modal'] = TRUE;
|
||||
parent::__construct('#drupal-modal', $title, $html, $dialog_options, $settings);
|
||||
parent::__construct('#drupal-modal', $title, $content, $dialog_options, $settings);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class PrependCommand extends InsertCommand {
|
|||
'command' => 'insert',
|
||||
'method' => 'prepend',
|
||||
'selector' => $this->selector,
|
||||
'data' => $this->html,
|
||||
'data' => $this->getRenderedContent(),
|
||||
'settings' => $this->settings,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ class ReplaceCommand extends InsertCommand {
|
|||
'command' => 'insert',
|
||||
'method' => 'replaceWith',
|
||||
'selector' => $this->selector,
|
||||
'data' => $this->html,
|
||||
'data' => $this->getRenderedContent(),
|
||||
'settings' => $this->settings,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
namespace Drupal\Core\Render;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheContexts;
|
||||
use Drupal\Core\Cache\CacheFactoryInterface;
|
||||
|
@ -582,4 +583,18 @@ class Renderer implements RendererInterface {
|
|||
return $a;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function mergeAttachments(array $a, array $b) {
|
||||
// If both #attached arrays contain drupalSettings, then merge them
|
||||
// correctly; adding the same settings multiple times needs to behave
|
||||
// idempotently.
|
||||
if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
|
||||
$a['drupalSettings'] = NestedArray::mergeDeepArray([$a['drupalSettings'], $b['drupalSettings']], TRUE);
|
||||
unset($b['drupalSettings']);
|
||||
}
|
||||
return NestedArray::mergeDeep($a, $b);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -304,4 +304,48 @@ interface RendererInterface {
|
|||
*/
|
||||
public static function mergeBubbleableMetadata(array $a, array $b);
|
||||
|
||||
/**
|
||||
* Merges two attachments arrays (which live under the '#attached' key).
|
||||
*
|
||||
* The values under the 'drupalSettings' key are merged in a special way, to
|
||||
* match the behavior of:
|
||||
*
|
||||
* @code
|
||||
* jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...)
|
||||
* @endcode
|
||||
*
|
||||
* This means integer indices are preserved just like string indices are,
|
||||
* rather than re-indexed as is common in PHP array merging.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* function module1_page_attachments(&$page) {
|
||||
* $page['a']['#attached']['drupalSettings']['foo'] = ['a', 'b', 'c'];
|
||||
* }
|
||||
* function module2_page_attachments(&$page) {
|
||||
* $page['#attached']['drupalSettings']['foo'] = ['d'];
|
||||
* }
|
||||
* // When the page is rendered after the above code, and the browser runs the
|
||||
* // resulting <SCRIPT> tags, the value of drupalSettings.foo is
|
||||
* // ['d', 'b', 'c'], not ['a', 'b', 'c', 'd'].
|
||||
* @endcode
|
||||
*
|
||||
* By following jQuery.extend() merge logic rather than common PHP array merge
|
||||
* logic, the following are ensured:
|
||||
* - Attaching JavaScript settings is idempotent: attaching the same settings
|
||||
* twice does not change the output sent to the browser.
|
||||
* - If pieces of the page are rendered in separate PHP requests and the
|
||||
* returned settings are merged by JavaScript, the resulting settings are
|
||||
* the same as if rendered in one PHP request and merged by PHP.
|
||||
*
|
||||
* @param array $a
|
||||
* An attachments array.
|
||||
* @param array $b
|
||||
* Another attachments array.
|
||||
*
|
||||
* @return array
|
||||
* The merged attachments array.
|
||||
*/
|
||||
public static function mergeAttachments(array $a, array $b);
|
||||
|
||||
}
|
||||
|
|
|
@ -226,11 +226,11 @@ class EditorImageDialog extends FormBase {
|
|||
|
||||
if ($form_state->getErrors()) {
|
||||
unset($form['#prefix'], $form['#suffix']);
|
||||
$status_messages = array('#theme' => 'status_messages');
|
||||
$output = drupal_render($form);
|
||||
$response->setAttachments($form['#attached']);
|
||||
$output = '<div>' . drupal_render($status_messages) . $output . '</div>';
|
||||
$response->addCommand(new HtmlCommand('#editor-image-dialog-form', $output));
|
||||
$form['status_messages'] = [
|
||||
'#theme' => 'status_messages',
|
||||
'#weight' => -10,
|
||||
];
|
||||
$response->addCommand(new HtmlCommand('#editor-image-dialog-form', $form));
|
||||
}
|
||||
else {
|
||||
$response->addCommand(new EditorDialogSave($form_state->getValues()));
|
||||
|
|
|
@ -85,11 +85,11 @@ class EditorLinkDialog extends FormBase {
|
|||
|
||||
if ($form_state->getErrors()) {
|
||||
unset($form['#prefix'], $form['#suffix']);
|
||||
$status_messages = array('#theme' => 'status_messages');
|
||||
$output = drupal_render($form);
|
||||
$response->setAttachments($form['#attached']);
|
||||
$output = '<div>' . drupal_render($status_messages) . $output . '</div>';
|
||||
$response->addCommand(new HtmlCommand('#editor-link-dialog-form', $output));
|
||||
$form['status_messages'] = [
|
||||
'#theme' => 'status_messages',
|
||||
'#weight' => -10,
|
||||
];
|
||||
$response->addCommand(new HtmlCommand('#editor-link-dialog-form', $form));
|
||||
}
|
||||
else {
|
||||
$response->addCommand(new EditorDialogSave($form_state->getValues()));
|
||||
|
|
|
@ -100,19 +100,17 @@ class AjaxTestDialogForm extends FormBase {
|
|||
$content = ajax_test_dialog_contents();
|
||||
$response = new AjaxResponse();
|
||||
$title = $this->t('AJAX Dialog contents');
|
||||
$html = drupal_render($content);
|
||||
|
||||
// Attach the library necessary for using the Open(Modal)DialogCommand and
|
||||
// set the attachments for this Ajax response.
|
||||
$content['#attached']['library'][] = 'core/drupal.dialog.ajax';
|
||||
$response->setAttachments($content['#attached']);
|
||||
|
||||
if ($is_modal) {
|
||||
$response->addCommand(new OpenModalDialogCommand($title, $html));
|
||||
$response->addCommand(new OpenModalDialogCommand($title, $content));
|
||||
}
|
||||
else {
|
||||
$selector = '#ajax-test-dialog-wrapper-1';
|
||||
$response->addCommand(new OpenDialogCommand($selector, $title, $html));
|
||||
$response->addCommand(new OpenDialogCommand($selector, $title, $content));
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue