Commit 8ee03582 authored by alexpott's avatar alexpott

Issue #2347469 by mondrake, Wim Leers, larowlan, kattekrab, mnico, tadityar:...

Issue #2347469 by mondrake, Wim Leers, larowlan, kattekrab, mnico, tadityar: Rendering forms in AjaxResponses does not attach assets automatically
parent 23e9d7f1
......@@ -20,6 +20,7 @@
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\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 @@ public function render() {
'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 @@ public function addCommand(CommandInterface $command, $prepend = FALSE) {
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 @@ public function render() {
'command' => 'insert',
'method' => 'append',
'selector' => $this->selector,
'data' => $this->html,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
......
......@@ -34,7 +34,7 @@ public function render() {
'command' => 'insert',
'method' => 'before',
'selector' => $this->selector,
'data' => $this->html,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
......
<?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();
}
<?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 @@ public function render() {
'command' => 'insert',
'method' => 'html',
'selector' => $this->selector,
'data' => $this->html,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
......
......@@ -21,7 +21,9 @@
*
* @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 @@ public function render() {
'command' => 'insert',
'method' => NULL,
'selector' => $this->selector,
'data' => $this->html,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
......
......@@ -14,7 +14,9 @@
*
* @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 @@ public function render() {
'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 @@ public function render() {
'command' => 'insert',
'method' => 'prepend',
'selector' => $this->selector,
'data' => $this->html,
'data' => $this->getRenderedContent(),
'settings' => $this->settings,
);
}
......
......@@ -35,7 +35,7 @@ public function render() {
'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 @@ public static function mergeBubbleableMetadata(array $a, array $b) {
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 @@ public function getCacheableRenderArray(array $elements);
*/
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 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
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 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
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 @@ protected function dialog($is_modal = FALSE) {
$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;
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment