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

dependsOn for siblings #61

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,37 @@ php artisan vendor:publish --provider="Outl1ne\NovaSimpleRepeatable\SimpleRepeat

You can then edit the strings to your liking.

## DependsOn for siblings

``dependsOn()`` function is made available for fields within the `SimpleRepeatable`. To make the feature available, be sure
to use ``DependsOnSiblings`` trait on the resource you want to have `dependsOn()` available for sibling fields.

Key on which the field depends on is constructed out of two parts: ``{parent attribute}.{child attribute}``. This key
will be made available to fetch within the function via ``$request->get('parent.child')``.

Example:

```php
SimpleRepeatable::make('Adding', 'parent', [
// Depending on 1 field
Text::make('Child'),
Text::make('Dependent Child')
->dependsOn('parent.child', function ($field, NovaRequest $request, FormData $formData) {
$attribute = $request->get('parent.child');
}),

// Depending on multiple fields
Text::make('Second Child'),
Text::make('Third Child'),
Text::make('Really Dependent Child')
->dependsOn(['parent.second_child', 'parent.third_child'], function ($field, NovaRequest $request, FormData $formData) {
$attribute1 = $request->get('parent.second_child');
$attribute2 = $request->get('parent.third_child');
}),

])
```

## Credits

- [Tarvo Reinpalu](https://github.com/tarpsvo)
Expand Down
2 changes: 1 addition & 1 deletion dist/js/entry.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions resources/js/components/FormField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
:unique-id="getUniqueId(field, rowField)"
class="o1-mr-3"
:style="{ maxWidth: rowField.nsrWidth || null }"
:resource-name="this.resourceName"
/>
</div>

Expand Down
32 changes: 32 additions & 0 deletions resources/js/mixins/HandlesRepeatable.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default {

return {
...field,
dependsOn: this.transformDependsOn(field.dependsOn, rowIndex),
originalAttribute: field.attribute,
validationKey: uniqueAttribute,
attribute: uniqueAttribute,
Expand All @@ -63,6 +64,37 @@ export default {
});
},

transformDependsOn(obj, rowIndex) {
if (!obj || typeof obj !== 'object') {
return obj;
}

let transformedObj = {};

Object.keys(obj).forEach(key => {
let transformedKey = this.transformDependsOnString(key, rowIndex);
transformedObj[transformedKey] = obj[key];
});

return transformedObj;
},

// Take backend (dot) format of dependsOn and format for frontend (triple dash)
transformDependsOnString(inputString, rowIndex) {
if (!inputString) {
return null;
}

if (!inputString.includes('.')) {
return inputString;
}

let transformedString = inputString.replace(/\./g, '---');
transformedString += `---${rowIndex}`;

return transformedString;
},

getFieldLocales(field) {
let localeKeys = Object.keys(field.translatable.locales);

Expand Down
157 changes: 157 additions & 0 deletions src/App/Traits/DependsOnSiblings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

namespace Outl1ne\NovaSimpleRepeatable\App\Traits;

use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Laravel\Nova\Fields\Field;
use Laravel\Nova\Http\Controllers\CreationFieldSyncController;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Resource;
use Outl1ne\NovaSimpleRepeatable\SimpleRepeatable;

trait DependsOnSiblings
{
protected string $frontendSeparator = '---';

// When to append the merging logic
protected array $callerClasses = [
CreationFieldSyncController::class
];

/**
* @throws Exception
*/
public function creationFields(NovaRequest $request)
{
$creationFields = parent::creationFields($request);

return $this->shouldAppendSiblings()
? $creationFields->merge($this->loadSiblings($request))
: $creationFields;
}

/**
* We should dynamically load fields within SimpleRepeatable only when the request is coming
* from certain endpoints/controllers, because we don't want to change the way plain
* fields() method within a resource functions.
*
* @return string|null
*/
protected function shouldAppendSiblings(): ?string
{
$backtrace = debug_backtrace();
$classes = Arr::pluck($backtrace, 'class');

foreach ($classes as $class) {
if (in_array($class, $this->callerClasses)) {
return true;
}
}

return false;
}

/**
* @throws Exception
*/
protected function loadSiblings(NovaRequest $request): array
{
$siblings = [];
// If triggered within SimpleRepeatable, this will come in xxx---xxx---0 format
$affectedField = $request->get('field');
$elementNumber = Str::afterLast($affectedField, '-');

$triggerFields = $this->getTriggerFields($request, $affectedField);

// Prevent entering if dependsOn is triggered by outer fields
if (!str_contains($affectedField, $this->frontendSeparator)) {
return $siblings;
}

/** @var Resource $this */
foreach ($this->fields($request) as $parentField) {
if (get_class($parentField) !== SimpleRepeatable::class) {
continue;
}

$parentAttribute = $parentField->attribute;

/** @var Field $childField */
foreach ($parentField->getFields()->all() as $childField) {
// Need to change original attribute to the frontend dashed equivalent so Nova knows which
// field will be affected by changing its dependant
$childField->attribute = "$parentAttribute---{$childField->attribute}---$elementNumber";
$siblings[] = $childField;

$this->setAffected($childField, $affectedField, $request);
$this->setTrigger($childField, $triggerFields, $parentAttribute, $request);
}
}

return $siblings;
}

protected function getTriggerFields(NovaRequest $request, mixed $affectedField): array
{
// Fetching input without query string parameters
$input = $request->json()->all();

// Filter out the specific field
$filteredInputs = array_filter($input, function ($value, $key) use ($affectedField) {
return $key !== $affectedField;
}, ARRAY_FILTER_USE_BOTH);

return array_keys($filteredInputs);
}

protected function extractFieldAttribute(string $field): string
{
return Str::betweenFirst($field, $this->frontendSeparator, $this->frontendSeparator);
}

/**
* Need to replace original component in the query since we changed the original attribute
* as we can't change it afterward without overriding Nova routes & controllers which
* seemed like an unnecessary complexity.
*
* @param Field $childField
* @param mixed $affectedField
* @param NovaRequest $request
* @return void
*/
protected function setAffected(Field $childField, string $affectedField, NovaRequest $request): void
{
if ($childField->attribute === $affectedField) {
$request->query->set('component', $childField->dependentComponentKey());
}
}

/**
* When we encounter a trigger field, we need to make sure to extract value it is providing for the
* dependent field, and attach it to the request in the same format as expected on the backend
*
* @param Field $childField
* @param array $triggerFields
* @param string $parentAttribute
* @param NovaRequest $request
* @return void
*/
protected function setTrigger(Field $childField, array $triggerFields, string $parentAttribute, NovaRequest $request): void
{
if (!in_array($childField->attribute, $triggerFields)) {
return;
}

$triggerAttribute = $this->extractFieldAttribute($childField->attribute);

// Prepare dot-notation backend key to be available through request
$setAttribute = "$parentAttribute.$triggerAttribute";
$value = $request->get($childField->attribute);

\Log::info("Setting $setAttribute to $value");

$request->query->set($setAttribute, $value);
}
}
5 changes: 5 additions & 0 deletions src/SimpleRepeatable.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public function fields($fields = [])
$this->fields = FieldCollection::make($fields);
}

public function getFields()
{
return $this->fields;
}

public function minRows($minRows = null)
{
return $this->withMeta(['minRows' => $minRows]);
Expand Down