Skip to content

Commit

Permalink
Merge pull request #14212 from craftcms/feature/cms-1177-map-element-…
Browse files Browse the repository at this point in the history
…errors-to-field-containers-with-data-error-key

Feature/cms 1177 map element errors to field containers with data error key
  • Loading branch information
brandonkelly authored Jan 31, 2024
2 parents 8541471 + a99d57f commit 7838c9b
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 76 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- Fixed a bug where element thumbnails weren’t getting loaded within newly-added cards in Matrix and Addresses fields.
- Fixed a bug where inline Matrix block tabs weren’t scaling up for text-only zoom levels. ([#14213](https://github.com/craftcms/cms/pull/14213))
- Fixed a bug where required custom fields within inline-editable Matrix blocks weren’t getting validated.
- Fixed a bug where validation summaries weren’t always linking to the offending field. ([#14212](https://github.com/craftcms/cms/pull/14212))

## 5.0.0-alpha.9 - 2024-01-29

Expand Down
3 changes: 3 additions & 0 deletions src/fieldlayoutelements/BaseField.php
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,9 @@ public function formHtml(?ElementInterface $element = null, bool $static = false
'translatable' => $this->translatable($element, $static),
'translationDescription' => $this->translationDescription($element, $static),
'errors' => !$static ? $this->errors($element) : [],
'data' => [
'error-key' => $this->name ?? $this->attribute(),
],
]);
}

Expand Down
2 changes: 1 addition & 1 deletion src/fields/Matrix.php
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,7 @@ private function validateEntries(ElementInterface $element): void
}

if (!$entry->validate()) {
$element->addModelErrors($entry, "$this->handle[$i]");
$element->addModelErrors($entry, "$this->handle[$entry->uid]");
$allEntriesValidate = false;
}
}
Expand Down
25 changes: 24 additions & 1 deletion src/helpers/Cp.php
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,8 @@ public static function fieldHtml(string|callable $input, array $config = []): st
$fieldId = $config['fieldId'] ?? "$id-field";
$label = $config['fieldLabel'] ?? $config['label'] ?? null;

$data = $config['data'] ?? [];

if ($label === '__blank__') {
$label = null;
}
Expand Down Expand Up @@ -1360,7 +1362,7 @@ public static function fieldHtml(string|callable $input, array $config = []): st
'id' => $fieldId,
'data' => [
'attribute' => $attribute,
],
] + $data,
],
$config['fieldAttributes'] ?? []
)) .
Expand Down Expand Up @@ -2021,6 +2023,9 @@ public static function addressFieldsHtml(Address $address): string
'autocomplete' => $belongsToCurrentUser ? 'address-line1' : 'off',
'required' => isset($requiredFields['addressLine1']),
'errors' => $address->getErrors('addressLine1'),
'data' => [
'error-key' => 'addressLine1',
],
]) .
static::textFieldHtml([
'status' => $address->getAttributeStatus('addressLine2'),
Expand All @@ -2031,6 +2036,9 @@ public static function addressFieldsHtml(Address $address): string
'autocomplete' => $belongsToCurrentUser ? 'address-line2' : 'off',
'required' => isset($requiredFields['addressLine2']),
'errors' => $address->getErrors('addressLine2'),
'data' => [
'error-key' => 'addressLine2',
],
]) .
self::_subdivisionField(
$address,
Expand Down Expand Up @@ -2072,6 +2080,9 @@ public static function addressFieldsHtml(Address $address): string
'autocomplete' => $belongsToCurrentUser ? 'postal-code' : 'off',
'required' => isset($requiredFields['postalCode']),
'errors' => $address->getErrors('postalCode'),
'data' => [
'error-key' => 'postalCode',
],
]) .
static::textFieldHtml([
'fieldClass' => array_filter([
Expand All @@ -2085,6 +2096,9 @@ public static function addressFieldsHtml(Address $address): string
'value' => $address->sortingCode,
'required' => isset($requiredFields['sortingCode']),
'errors' => $address->getErrors('sortingCode'),
'data' => [
'error-key' => 'sortingCode',
],
]);
}

Expand Down Expand Up @@ -2132,6 +2146,9 @@ private static function _subdivisionField(
'id' => $name,
'required' => $required,
'errors' => $errors,
'data' => [
'error-key' => $name,
],
]);
}

Expand All @@ -2146,6 +2163,9 @@ private static function _subdivisionField(
'required' => $required,
'errors' => $address->getErrors($name),
'autocomplete' => $autocomplete,
'data' => [
'error-key' => $name,
],
]);
}

Expand All @@ -2160,6 +2180,9 @@ private static function _subdivisionField(
'value' => $value,
'required' => $required,
'errors' => $address->getErrors($name),
'data' => [
'error-key' => $name,
],
]);
}

Expand Down
16 changes: 15 additions & 1 deletion src/models/FieldLayout.php
Original file line number Diff line number Diff line change
Expand Up @@ -827,10 +827,24 @@ public function createForm(?ElementInterface $element = null, bool $static = fal
}, $namespace);

if ($html) {
$errorKey = null;
// if error key prefix was set on the FieldLayoutForm - use it
if ($form->errorKeyPrefix) {
$tagAttributes = Html::parseTagAttributes($html);
// if we already have an error-key for this field, prefix it
if (isset($tagAttributes['data']['error-key'])) {
$errorKey = $form->errorKeyPrefix . '.' . $tagAttributes['data']['error-key'];
} else {
// otherwise let's construct it
/** @phpstan-ignore-next-line */
$errorKey = $form->errorKeyPrefix . '.' . ($layoutElement->name ?? $layoutElement->attribute());
}
}

$html = Html::modifyTagAttributes($html, [
'data' => [
'layout-element' => $isConditional ? $layoutElement->uid : true,
],
] + ($errorKey ? ['error-key' => $errorKey] : []),
]);

$layoutElements[] = [$layoutElement, $isConditional, $html];
Expand Down
6 changes: 6 additions & 0 deletions src/models/FieldLayoutForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ class FieldLayoutForm extends Model
*/
public ?string $tabIdPrefix = null;

/**
* @var string|null The prefix that should be used for the data-error-key attribute
* @since 5.0.0
*/
public ?string $errorKeyPrefix = null;

/**
* Returns the tab menu config.
*
Expand Down
2 changes: 1 addition & 1 deletion src/templates/_components/fieldtypes/Matrix/block.twig
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
{% endif %}

{% namespace baseInputName %}
{% set form = entry.getFieldLayout().createForm(entry, static) %}
{% set form = entry.getFieldLayout().createForm(entry, static, {'errorKeyPrefix' : "#{name}[#{entry.uid}]"}) %}
{% set tabs = form.getTabMenu() %}
{% endnamespace %}

Expand Down
2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/cp.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/cp.js.map

Large diffs are not rendered by default.

25 changes: 24 additions & 1 deletion src/web/assets/cp/src/js/CpScreenSlideout.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,10 +562,33 @@ Craft.CpScreenSlideout = Craft.Slideout.extend(
this.clearErrors();

Object.entries(errors).forEach(([name, fieldErrors]) => {
const $field = this.$container.find(`[data-attribute="${name}"]`);
const $field = this.$container.find(`[data-error-key="${name}"]`);
if ($field) {
Craft.ui.addErrorsToField($field, fieldErrors);
this.fieldsWithErrors.push($field);

// mark the tab as having errors
let fieldTabAnchors = Craft.ui.findTabAnchorForField(
$field,
this.$container
);

if (fieldTabAnchors.length > 0) {
for (let i = 0; i < fieldTabAnchors.length; i++) {
let $fieldTabAnchor = $(fieldTabAnchors[i]);

if ($fieldTabAnchor.hasClass('error') == false) {
$fieldTabAnchor.addClass('error');
$fieldTabAnchor
.find('.tab-label')
.append(
'<span data-icon="alert">' +
'<span class="visually-hidden">This tab contains errors</span>\n' +
'</span>'
);
}
}
}
}
});
},
Expand Down
98 changes: 29 additions & 69 deletions src/web/assets/cp/src/js/UI.js
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,7 @@ Craft.ui = {
$body.prev('.error-summary').remove();
},

setFocusOnErrorSummary: function ($body, namespace = '') {
setFocusOnErrorSummary: function ($body) {
const errorSummaryContainer = $body.find('.error-summary');
if (errorSummaryContainer.length > 0) {
errorSummaryContainer.trigger('focus');
Expand All @@ -1296,63 +1296,21 @@ Craft.ui = {
errorSummaryContainer.find('a').on('click', (ev) => {
if ($(ev.currentTarget).hasClass('cross-site-validate') == false) {
ev.preventDefault();
this.anchorSummaryErrorToField(ev.currentTarget, $body, namespace);
this.anchorSummaryErrorToField(ev.currentTarget, $body);
}
});
}
},

findErrorsContainerByErrorKey: function ($body, fieldErrorKey, namespace) {
namespace = this._getPreppedNamespace(namespace);

// get the field handle from error key
const errorKeyParts = fieldErrorKey.split(/[\[\]\.]/).filter((n) => n);

// define regex for searching for errors list for given field
let regex;

if (typeof errorKeyParts[0] !== 'undefined') {
if (typeof errorKeyParts[2] === 'undefined') {
regex = new RegExp(`^${namespace}fields-${errorKeyParts[0]}.*-errors`);
} else {
regex = new RegExp(`^${namespace}fields-${errorKeyParts[0]}.*-`);

let subpartsCount = Math.ceil(errorKeyParts.length / 2) - 1;
let j = 0;
for (let i = 0; i < subpartsCount; i++) {
j = j + 2;
let regexPart;
if (i == subpartsCount - 1) {
regexPart = new RegExp(`fields-${errorKeyParts[j]}-errors`);
} else {
regexPart = new RegExp(`fields-${errorKeyParts[j]}.*-`);
}
regex = new RegExp(regex.source + regexPart.source);
}
}
}

// find errors list for given error from summary
let errorsElement;
if (regex) {
errorsElement = $body.find('ul.errors').filter(function () {
return this.id.match(regex);
});

if (
errorsElement.length > 1 &&
typeof errorKeyParts[errorKeyParts.length - 2] !== 'undefined'
) {
errorsElement = errorsElement[errorKeyParts[errorKeyParts.length - 2]];
} else {
errorsElement = errorsElement[0];
}
}
findErrorsContainerByErrorKey: function ($body, fieldErrorKey) {
let errorsElement = $body
.find(`[data-error-key="${fieldErrorKey}"]`)
.find('ul.errors');

return $(errorsElement);
},

anchorSummaryErrorToField: function (error, $body, namespace) {
anchorSummaryErrorToField: function (error, $body) {
const fieldErrorKey = $(error).attr('data-field-error-key');

if (!fieldErrorKey) {
Expand All @@ -1361,20 +1319,23 @@ Craft.ui = {

const $fieldErrorsContainer = this.findErrorsContainerByErrorKey(
$body,
fieldErrorKey,
namespace
fieldErrorKey
);

if ($fieldErrorsContainer) {
// check if we need to switch tabs first
const $fieldTabAnchor = this.findTabAnchorForField(
const fieldTabAnchors = this.findTabAnchorForField(
$fieldErrorsContainer,
$body,
namespace
$body
);

if ($fieldTabAnchor && $fieldTabAnchor.attr('aria-selected') == 'false') {
$fieldTabAnchor.click();
if (fieldTabAnchors.length > 0) {
for (let i = 0; i < fieldTabAnchors.length; i++) {
let $tabAnchor = $(fieldTabAnchors[i]);
if ($tabAnchor.attr('aria-selected') == 'false') {
$tabAnchor.click();
}
}
}

// check if the parents are collapsed - if yes, expand
Expand Down Expand Up @@ -1406,17 +1367,20 @@ Craft.ui = {
}
},

findTabAnchorForField: function ($container, $body, namespace) {
namespace = this._getPreppedNamespace(namespace);

const fieldTabDiv = $container.parents(
`div[id^=${namespace}tab][role="tabpanel"]`
findTabAnchorForField: function ($container, $body) {
const fieldTabDivs = $container.parents(
`div[data-id^=tab][role="tabpanel"]`
);
const fieldTabAnchor = $body
.find('[role="tablist"]')
.find('a[href="#' + fieldTabDiv.attr('id') + '"]');

return $(fieldTabAnchor);
let fieldTabAnchors = [];
fieldTabDivs.each((i, tabDiv) => {
let tabAnchor = $body
.find('[role="tablist"]')
.find('a[href="#' + $(tabDiv).attr('id') + '"]');
fieldTabAnchors.push(tabAnchor);
});

return fieldTabAnchors;
},

getAutofocusValue: function (autofocus) {
Expand All @@ -1426,8 +1390,4 @@ Craft.ui = {
getDisabledValue: function (disabled) {
return disabled ? 'disabled' : null;
},

_getPreppedNamespace: function (namespace) {
return namespace !== '' ? (namespace += '-') : namespace;
},
};

0 comments on commit 7838c9b

Please sign in to comment.