Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating / editing single matrix blocks on the front end #5717

Closed
samhibberd opened this issue Feb 25, 2020 · 6 comments
Closed

Creating / editing single matrix blocks on the front end #5717

samhibberd opened this issue Feb 25, 2020 · 6 comments

Comments

@samhibberd
Copy link

I'm not sure if this is a feature request, issue, us missing something obvious. We use matrix fields on the front end regularly, but until now have always output all existing blocks, with the option to add new, reorder, delete etc etc. We basically just replicate the cp and it works a dream.

We now have a requirement where we don't want existing blocks output when adding new rows and also want to be able to edit existing rows on their own.

Our preference would be to achieve this with the existing craft controllers where possible:

We have tried posting just new blocks to the standard craft controllers, but this nukes existing rows.

1) Can nuking existing rows be avoided?
2) Is / could this behaviour be possible using the craft controllers?

We have also tested by writing our own matrix block controller, editing works nicely we just pass the blockId, grab the field data, validate and return the response.

For saving new blocks we are passing fieldId typeId & ownerId to create a new block, but new rows are not validating, do blocks in this state have enough info to validate?

3) Is this approach recommended? Would Craft permissions apply when working with Matrix Block this way.
4) Would craft ever consider adding controllers to work directly with matrix block elements?

@Anubarak
Copy link
Contributor

Anubarak commented Feb 25, 2020

Just my opinion on this... I'll take the opportunity to kindly ask for another featuer request/suggestion about this.

First of all, the only way to append/add Matrix block(s) to an existing element, without just saving the block(s) itself but the element and without loosing the rest of them can feel "ugly" (at least I have that feeling)
https://craftcms.stackexchange.com/a/25961

/** @var \craft\fields\Matrix $field */
$field = Craft::$app->getFields()->getFieldByHandle('matrixField');

// get the existing matrixField Value, keep in mind, this is a Query and not an array
$existingMatrixQuery = $element->getFieldValue('matrixField');

// serialize the data in order to get an array like Brandon Kelly
// created in his answer
$serializedMatrix = $field->serializeValue($existingMatrixQuery, $element);

// append the new blocks to the existing array
$serializedMatrix['new1'] = [
    'type' => 'blockTypeHandle',
    'fields' => [
        // the block's custom field values...
    ]
];

$element->setFieldValue('matrixField', $serializedMatrix);

// Save the entry
Craft::$app->elements->saveElement($element);

You need to serialize the existing block elements to an array, then change it / append / modify the existing data and set the field value back. @samhibberd this would be one possible way to modify and add certain blocks, without deleting the rest of them, $serializedMatrix contains all existing blocks. Just dump it an you'll see the data.

So my suggestion would be to to let elements of type MatrixBlock pass in the normalizeFieldValue function. So in your _createBlocksFromSerializedData function check if the value already is a MatrixBlock or really an array.

That would allow us to do the following

/** @var \craft\elements\MatrixBlock[] $existingBlocks */
/** @var \craft\base\Element $element */
$existingBlocks = $element->matrix->all();
foreach ($postData as $key => $newMatrixBlock){
    $existingBlocks['new' . ($key+1)] = [
        // set the properties
    ];
}

$element->setFieldValue($existingBlocks);

@samhibberd As a current "not really secure", but really easy and fast way to let people edit only certain blocks without deleting the rest is to include the rest of them in hidden element tags, so they won't see them, usually they won't edit them but someone with HTML knowledge can modify your HTML and thus delete them anyway.
I personally create a custom controller for that purpose and do the thing mentioned above.

If it's only about saving Matrix Blocks without saving the corresponding element, you can as well only work with the blocks...

$newBlock = new MatrixBlock();
// ... set all required values, as well as ownerId
Craft::$app->getElements()->saveElement($newBlock);

and it will be appended

@brandonkelly
Copy link
Member

As of Craft 3.4 it is now possible to save partial Matrix data, as part of the new delta save feature.

I’ve been meaning to document it, so thanks for the nudge :) https://docs.craftcms.com/v3/matrix-fields.html#saving-matrix-fields-in-entry-forms

The one caveat is that you must submit a sortOrder param that specifies all the block IDs (including any new:X temporary IDs), as that becomes the source of truth on which blocks should persist (or be created), and in which order.

So here’s a simple example of how to create an entry form that just adds one additional text block:

{% if entry is defined %}
    {# Retain existing blocks + sort order #}
    {% for blockId in clone(entry.<FieldHandle>).anyStatus().ids() %}
        {{ hiddenInput('fields[<FieldHandle>][sortOrder][]', blockId) }}
    {% endfor %}
{% endif %}

{# Add a new text block #}
{{ hiddenInput('fields[<FieldHandle>][sortOrder][]', 'new:1') }}

{# Prefix the block's input names with `fields[<FieldHandle>][blocks][new:1]` #}
{% namespace "fields[<FieldHandle>][blocks][new:1]" %}
    {{ hiddenInput('type', 'text') }}
    <textarea name="fields[<TextFieldHandle>]"></textarea>
{% endnamespace %}

That sortOrder param still gives a lot of control to your template, though. If you want the form to only be able to submit the new block data and not have any power over existing blocks, then you will still need a custom controller, though that can be simpler too now.

use yii\web\Response;
use craft\elements\Entry;

public function actionAddBlock(): Response
{
    // ...
    /** @var Entry $entry */
    $sortOrder = (clone $entry->myMatrixField)->anyStatus()->ids();

    $sortOrder[] = 'new:1';
    $newBlock = [
        'type' => 'text',
        'fields' => [
            'myTextField' => Craft::$app->request->getBodyParam('text'),
        ],
    ];

    $entry->setFieldValue('myMatrixField', [
        'sortOrder' => $sortOrder,
        'blocks' => [
            'new:1' => $newBlock,
        ],
    ]);

    // ...
}

@samhibberd
Copy link
Author

This is awesome! Thanks @brandonkelly

@rtrudel
Copy link

rtrudel commented Mar 10, 2020

Is this new delta save feature allow to delete Matrix blocks by removing block ids from the sortOrder cloned var? I mean, this will soft/hard delete MatrixBlock elements or those block will still pointlessly live in my db? The reindex task will do the trick?

As example, this is a correct way to achieve a specific block deletion? :

$sortOrder = (clone $entry->myMatrixField)->anyStatus()->ids();
// [1,2,3]

//remove 2 from my array
foreach (array_keys($sortOrder, 2) as $key) {
  unset($sortOrder[$key]);
}
// [1,3]

$entry->setFieldValue('myMatrixField', [
   'sortOrder' => $sortOrder,
]);

// ...

I want to avoid my db to become a huge orphanage of rows (at least try to keep it clean)...

@brandonkelly
Copy link
Member

@rtrudel Yep, that should work exactly as you’ve typed it.

FYI you could use ArrayHelper::withoutValue() to simplify the removal:

use craft\helpers\ArrayHelper;
// ...
$sortOrder = ArrayHelper::withoutValue($sortOrder, 2);

@rtrudel
Copy link

rtrudel commented Mar 10, 2020

This is excellent! I hope that kind of matrix manipulation snippets will exists somewhere in the doc! For Twig and PHP. Thanks @brandonkelly !

Nice to know about ArrayHelper::withoutValue(), will be handy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants