Open kylemilloy opened 6 years ago
For anyone else looking to solve this issue I hacked it by doing the following...this guide specifically deals with setting an "order" attribute on the pivot table but you could likely turn it into anything.
You'll need a custom controller to handle the pivot attributes. Create a controller and extend the VoyagerBaseController. From here we're going to overwrite the insertUpdateData
method. Most of this remains the same but near the very end where we run a foreach
call on the $multi_select
variable we're going to modify some stuff here. If you're looking to order items, do the following:
// ... insertUpdateData
foreach ($multi_select as $sync_data) {
$attrs = [];
for ($i = 0; $i < $count; $i++) {
$pivot[$i] = ['order' => $i];
}
//
$data->belongsToMany($sync_data['model'], $sync_data['table'])->sync(
array_combine($sync_data['content'], $attrs)
);
}
// ...
See Laravel documentation here for the Laravel sync command and what it expects
On the model you need to include the relationship data for later so add a ->withPivot($column_name)
call to the end of your belongsToMany
call.
You'll need to overwrite your edit-add view for the slug of the model you're updating. Ex: Updating the "Artist" model? Then create a view at resources/views/vendor/voyager/artists/edit-add.blade.php
and copy everything from the base edit-add view. See the doc here. If you're trying to add order you'll also need to make a custom relationship partial at resources/views/vendor/voyager/formfields/relationship-w-pivot.blade.php
. Copy the contents of the base voyager relationship.blade.php view. In the custom edit-add view you made change the include relationship call to point at your custom one.
This is very specific to setting an order and likely won't need to be done. In the edit-add partial you made attach jQuery UI in the javascripts section and add the following at the bottom...feel free to clean this up but I hacked it up with the following:
$('.select2-selection__rendered').sortable({
stop: function(evt, ui) {
$parent = $(evt.target).parents('.form-group');
var select = $parent.find('select');
var option_array = $(evt.target).parents('.form-group').find('.select2-selection__choice').get();
$.each(option_array, function(i, el) {
var id = $(el).attr('id');
var option = select.find('option[value="' + id + '"]')[0];
select.prepend(option);
});
}
});
This will make it so the belongsToMany field will be sortable and when it sorts it it will update the order of the select options. Doing this allows us to avoid passing more data to the backend and instead just rely on the order of the options in the select input to tell Laravel what order to assign.
For ordering we need to persist the order that they appear. Skip down to the <select multiple>
portion (around line 150) and here you will see a call to @php
where we assign the $selected_values
variable. Everything should remain the same but I modified it a bit to fetch additional pivot info...and then sort by this pivot data. I changed the @php
section to be the following:
$selected_values = isset($dataTypeContent)
? $dataTypeContent->belongsToMany($options->model, $options->pivot_table)->withPivot('order')->get()
: array();
$relationshipOptions = app($options->model)->all()->merge($selected_values)->sortByDesc('pivot.order');
$selected_values = $selected_values->pluck($options->key)->toArray();
$count
where the $count comes from and $pivot array where we used it. Thanks.
From the rest of the insertUpdateData method on the parent class.
Is there any new/built-in way to do that?
@lablushah I update $count
as count($sync_data['content'])
and then worked for me.
@kylemilloy Do you have full example in repository?
Here's an exemple with a custom text pivot field : We have a Model Sale: we can sale some elements to a user and we want to save the price of the sale in the pivot table.
In the relationship details we add the name of the pivot field and the displayed name wanted (here the name is displayed in french):
{
"pivot_datas": [
[
"price",
"Prix (CHF)"
]
]
}
You'll need a custom controller to handle the pivot attributes. Create a controller and extend the VoyagerBaseController. From here we're going to overwrite the insertUpdateData method.
//... insertUpdateData
//
//add pivotDatas in $multi_select[]
if ($row->type == 'relationship' && $row->details->type == 'belongsToMany') {
// Only if select_multiple is working with a relationship
$multi_select[] = [
'model' => $row->details->model,
'content' => $content,
'table' => $row->details->pivot_table,
'foreignPivotKey' => $row->details->foreign_pivot_key ?? null,
'relatedPivotKey' => $row->details->related_pivot_key ?? null,
'parentKey' => $row->details->parent_key ?? null,
'relatedKey' => $row->details->key,
'pivotDatas' => $row->details->pivot_datas ?? null,
'FieldName' => $row->field,
];
} else {
$data->{$row->field} = $content;
}
// ...
foreach ($multi_select as $sync_data) {
//if there is pivot data parameters
if(isset($sync_data['pivotDatas']) && isset($sync_data['content'])){
$pivot_tab = [];
//we are looking for pivot data value
foreach($sync_data['content'] as $key => $content_to_sync_with){
$pivot_rows = [];
foreach($sync_data['pivotDatas'] as $pivot_data){
//name of the column in the pivot table
$name_of_pivot = $pivot_data[0];
//form field name
$name_of_formField = $sync_data['FieldName']."-".$name_of_pivot;
//value of this form field name
$value = $request->$name_of_formField[$key];
$pivot_rows[$name_of_pivot] = $value;
}
array_push($pivot_tab, $pivot_rows);
}
//combination of the datas to sync and datas to put in the columns of the pivot table
$sync = array_combine($sync_data['content'], $pivot_tab);
}else{
$sync = $sync_data['content'];
}
$data->belongsToMany(
$sync_data['model'],
$sync_data['table'],
$sync_data['foreignPivotKey'],
$sync_data['relatedPivotKey'],
$sync_data['parentKey'],
$sync_data['relatedKey']
)->sync($sync);
}
You have to create a new file in ressources/views/vendor/voyager/formfields/relationshipwithpivot.blade.php:
@php
$relationshipField = $row->field;
$options->pivot_extra_fields = [];
foreach($options->pivot_datas as $pivot_datas){
array_push($options->pivot_extra_fields, $pivot_datas[0]);
}
$selected_values = isset($dataTypeContent) ? $dataTypeContent->belongsToMany($options->model, $options->pivot_table, $options->foreign_pivot_key ?? null, $options->related_pivot_key ?? null, $options->parent_key ?? null, $options->key)->withPivot($options->pivot_extra_fields)->get()->map(function ($item, $key) use ($options) {
$array_pivot = [];
foreach($options->pivot_extra_fields as $pivot_col_name){
array_push($array_pivot, $item->pivot->$pivot_col_name);
}
return array('id' => $item->{$options->key}, 'extra_pivot' => $array_pivot);
})->all() : array();
$relationshipOptions = app($options->model)->all();
$selected_values = old($relationshipField, $selected_values);
if(count($selected_values) == null){
$selected_values[0] = 0;
}
@endphp
@foreach($selected_values as $cpt => $selected_value)
<div class="panel-body" id="{{ $relationshipField }}" style="background-color:#f2f2f2">
<div id="{{ $relationshipField.'-num'.$cpt }}">
<label class="control-label" for="name">{{ $row->getTranslatedAttribute('display_name') }}</label>
<select
class="form-control"
onchange="hideAlreadySelectedOptions(this)"
name="{{ $relationshipField }}[]"
data-get-items-route="{{route('voyager.' . $dataType->slug.'.relation')}}"
data-get-items-field="{{$row->field}}"
@if(!is_null($dataTypeContent->getKey())) data-id="{{$dataTypeContent->getKey()}}" @endif
data-method="{{ !is_null($dataTypeContent->getKey()) ? 'edit' : 'add' }}"
@if(isset($options->taggable) && $options->taggable === 'on')
data-route="{{ route('voyager.'.\Illuminate\Support\Str::slug($options->table).'.store') }}"
data-label="{{$options->label}}"
data-error-message="{{__('voyager::bread.error_tagging')}}"
@endif
>
@if(!$row->required)
<option value="">{{__('voyager::generic.none')}}</option>
@endif
@foreach($relationshipOptions as $relationshipOption)
<option value="{{ $relationshipOption->{$options->key} }}" @if($relationshipOption->{$options->key} == $selected_value['id']) selected="selected" @endif>{{ $relationshipOption->{$options->label} }}</option>
@endforeach
</select>
@foreach($row->details->pivot_datas as $key => $pivot_datas)
<label for="{{ $relationshipField.'-'.$pivot_datas[0] }}">{{ $pivot_datas[1] }}</label>
<input type="text" class="form-control pivot" id="{{ $relationshipField.'-'.$pivot_datas[0] }}" name="{{ $relationshipField.'-'.$pivot_datas[0].'[]' }}" placeholder="{{ $pivot_datas[1] }}"
value="{{$selected_value['extra_pivot'][$key]}}">
@endforeach
<hr style="height:1px;border-width:0;color:gray;background-color:gray">
</div>
@if(count($relationshipOptions) > 1)
<a class="addEntry" style="cursor: pointer;" data-relationshipField="{{ $relationshipField }}" data-nbOfRelationshipOptions="{{ count($relationshipOptions) }}"><span class="voyager-plus">Ajouter une entrée</span></a>
<a class="delEntry" style="cursor: pointer; display: none;" data-relationshipField="{{ $relationshipField }}" data-nbOfRelationshipOptions="{{ count($relationshipOptions) }}"><span class="voyager-trash">Supprimer une entrée</span></a>
@endif
</div>
@endforeach
We change the way to display the relationship type field:
//...
@if (isset($row->details->view))
@include($row->details->view, ['row' => $row, 'dataType' => $dataType, 'dataTypeContent' => $dataTypeContent, 'content' => $dataTypeContent->{$row->field}, 'action' => ($edit ? 'edit' : 'add'), 'view' => ($edit ? 'edit' : 'add'), 'options' => $row->details])
@elseif ($row->type == 'relationship')
@if(isset($row->details->pivot_datas) && $row->details->pivot_datas != null)
@include('voyager::formfields.relationshipwithpivot', ['options' => $row->details])
@else
@include('voyager::formfields.relationship', ['options' => $row->details])
@endif
@else
{!! app('voyager')->formField($row, $dataType, $dataTypeContent) !!}
@endif
//.....
in the javascript section of the edit-add.blade.php file:
<script>
function hideAlreadySelectedOptions(selectObject){
//When an option has been selected in a list, we do not want this option to be displayed in other lists.
var name_of_select = selectObject.name;
console.log(name_of_select);
var options_to_hide_array = [];
//Looking for every select element
$("select[name='"+name_of_select+"']").each(function(){
//The option selected has to be hide in each select
var option_to_hide = $(this).val();
if(option_to_hide != ""){
options_to_hide_array.push(option_to_hide);
}
//for now we show all options (for updating)
$("option", this).each(function(index, element){
$(element).show();
});
});
console.log(options_to_hide_array);
//we are looking for every select again
$("select[name='"+name_of_select+"']").each(function(){
var option_selected = $(this).val();
$("option", this).each(function(index, element){
//if the option is in the array of the options to be hide AND the option is not selected in the current select...
if(options_to_hide_array.includes(element.value) && element.value != option_selected){
//...we hide this option
$(element).hide();
}
});
});
}
$('.addEntry').click(function(){
// get the last DIV which ID starts with ^= id (the $relationshipField)
var id = $( this ).attr("data-relationshipField");
var $div = $('div[id^="'+id+'"]:last');
// Read the Number from that DIV's ID
// And increment that number by 1
var num = parseInt( $div.prop("id").split('-num')[1] ) +1;
var $newdiv = $div.clone().prop('id', id+'-num'+num );
$div.after($newdiv);
$newdiv.hide();
$newdiv.show("fast");
//retrieve all pivot field within the new div cloned and delete their value
var div_cloned = '#'+id+'-num'+num;
var allPivotFields = $(div_cloned).find('.pivot').map(function() {
this.value = "";
}).get();
//we don't want to select the same option twice
$("select[name='"+id+"[]']").each(function(){
var option_to_delete = $(this).val();
$(div_cloned + " > select[name='"+id+"[]'] > option[value='"+option_to_delete+"']").hide();
});
var max_of_options = parseInt($( this ).attr("data-nbOfRelationshipOptions"));
//if the nb of entries is bigger or equal to the maximum options available
if(num+1 >= max_of_options){
$(this).hide();
}
//show the button "delEntry"
$parent_div = $(this).closest("div");
$parent_div.find(".delEntry").show();
});
$('.delEntry').click(function(){
// get the last DIV which ID starts with ^= id (the $relationshipField)
var id = $( this ).attr("data-relationshipField");
var $div = $('div[id^="'+id+'"]:last');
var num = parseInt( $div.prop("id").split('-num')[1] );
$div.fadeOut(500, function(){
$(this).remove();
//we update the list of option off all select
var last_select = $parent_div.find("select[name='"+id+"[]']").get(0);
hideAlreadySelectedOptions(last_select);
});
if(num <= 1){
$(this).hide();
}
//show the button "addEntry"
$parent_div = $(this).closest("div");
$parent_div.find(".addEntry").show();
});
</script>
For exemple:
public function materials()
{
return $this->belongsToMany('App\Material')
->withPivot([
'price',
]);
}
I know it's not perfect, but it's a base to be able to manage custom pivot fields!
I know it's not perfect, but it's a base to be able to manage custom pivot fields!
Thank you, it works!
Hi,
Are there any plans to implement this feature within voyager? The example above is a great start however it doesn't seems to allow me to add additional pivot table rows and has various other bugs such as array access issues.
Thanks in advance
Hi @phillmorgan28 Yes I think you can add an additional pivot table rows (you have to modify the "relationship options" in your bread panel and to be sure that the name correspond to your rows name in your database). And yes there's some bugs... and honestly, I think that the code must to be cleaned before to implement this future! I'm not a professional! ;-) but here's a new version of the file relationshipwithpivot.blade.php. It should work!
ressources/views/vendor/voyager/formfields/relationshipwithpivot.blade.php:
@php
$relationshipField = $row->field;
$options->pivot_extra_fields = [];
foreach($options->pivot_datas as $pivot_datas){
array_push($options->pivot_extra_fields, $pivot_datas[0]);
}
$selected_values = isset($dataTypeContent) ? $dataTypeContent->belongsToMany($options->model, $options->pivot_table, $options->foreign_pivot_key ?? null, $options->related_pivot_key ?? null, $options->parent_key ?? null, $options->key)->withPivot($options->pivot_extra_fields)->get()->map(function ($item, $key) use ($options) {
$array_pivot = [];
foreach($options->pivot_extra_fields as $pivot_col_name){
array_push($array_pivot, $item->pivot->$pivot_col_name);
}
return array('id' => $item->{$options->key}, 'extra_pivot' => $array_pivot);
})->all() : array();
$relationshipOptions = app($options->model)->all();
$selected_values = old($relationshipField, $selected_values);
if(count($selected_values) == null){
$selected_values[0] = 0;
}
@endphp
@foreach($selected_values as $cpt => $selected_value)
<div class="panel-body" id="{{ $relationshipField }}" style="background-color:#f2f2f2">
<div id="{{ $relationshipField.'-num'.$cpt }}">
<label class="control-label" for="name">{{ $row->getTranslatedAttribute('display_name') }}</label>
<select
class="form-control"
onchange="hideAlreadySelectedOptions(this)"
name="{{ $relationshipField }}[]"
data-get-items-route="{{route('voyager.' . $dataType->slug.'.relation')}}"
data-get-items-field="{{$row->field}}"
@if(!is_null($dataTypeContent->getKey())) data-id="{{$dataTypeContent->getKey()}}" @endif
data-method="{{ !is_null($dataTypeContent->getKey()) ? 'edit' : 'add' }}"
@if(isset($options->taggable) && $options->taggable === 'on')
data-route="{{ route('voyager.'.\Illuminate\Support\Str::slug($options->table).'.store') }}"
data-label="{{$options->label}}"
data-error-message="{{__('voyager::bread.error_tagging')}}"
@endif
>
@if(!$row->required)
<option value="">{{__('voyager::generic.none')}}</option>
@endif
@foreach($relationshipOptions as $relationshipOption)
<option value="{{ $relationshipOption->{$options->key} }}" @if(isset($selected_value['id']) && $relationshipOption->{$options->key} == $selected_value['id']) selected="selected" @endif>{{ $relationshipOption->{$options->label} }}</option>
@endforeach
</select>
@foreach($row->details->pivot_datas as $key => $pivot_datas)
<label for="{{ $relationshipField.'-'.$pivot_datas[0] }}">{{ $pivot_datas[1] }}</label>
<input type="text" class="form-control pivot" id="{{ $relationshipField.'-'.$pivot_datas[0] }}" name="{{ $relationshipField.'-'.$pivot_datas[0].'[]' }}" placeholder="{{ $pivot_datas[1] }}"
value="@if(isset($selected_value['extra_pivot'])) {{$selected_value['extra_pivot'][$key]}} @endif">
@endforeach
<hr style="height:1px;border-width:0;color:gray;background-color:gray">
</div>
@if(count($relationshipOptions) > 1)
<a class="addEntry" style="cursor: pointer;" data-relationshipField="{{ $relationshipField }}" data-nbOfRelationshipOptions="{{ count($relationshipOptions) }}"><span class="voyager-plus">Ajouter une entrée</span></a>
<a class="delEntry" style="cursor: pointer; display: none;" data-relationshipField="{{ $relationshipField }}" data-nbOfRelationshipOptions="{{ count($relationshipOptions) }}"><span class="voyager-trash">Supprimer une entrée</span></a>
@endif
</div>
@endforeach
hi @g-trema
Your code works great and I thank you very much for it but there are some bugs in the display of adding and deleting an entry when editing. Can you review this?
I have some many-to-many relationships that I'd like to include pivot data with. I don't suppose there's a way to get/set pivot table data within the Voyager UI short of making BREAD for the pivot table?
The use case is: A project belongs to many services. But I don't want to show them in the order they were made. Instead, want to have an "order" column that a user can set to display them as required.
I am assuming I'll need to build something bespoke here...how can I create a custom data type for including pivot data? What part of Voyager would I need to augment?