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

Ajax for select2 #166

Closed
maesklaas opened this issue Oct 5, 2016 · 16 comments
Closed

Ajax for select2 #166

maesklaas opened this issue Oct 5, 2016 · 16 comments

Comments

@maesklaas
Copy link
Contributor

Hi,

I really like your package. At the moment i have a CrudController in which i have an 1-n relation with an select2 field. But my edit and crete forms loading time is quite large because of the fact that the data loaded in the select2 is quite large. Is it possible to use the ajax functionality of select2 and load data in depending on typed text?

Thanks!

@OwenMelbz
Copy link
Contributor

Hi @maesklaas

This is a pretty good idea! We've actually had to do something like this in the past as well, I'm not sure if its possible currently with the select2 fields, however if you're in urgent need of the feature, you can always copy the existing select2.blade.php(might be named slightly differently) into your app/resources/views/vendor/backpack/crud/fields/select2_ajax.blade.php and modify it for your own needs, You'll need to setup your own ajax endpoint route and controller method to do this

@maesklaas
Copy link
Contributor Author

maesklaas commented Oct 6, 2016

I did give it a try and it seams to work quit nice! Probably there are still some improvements possible. One thing that isn't working yet is when i have multiple of these ajax selects it just uses the first one. At the moment only that one will be inserted. I can get it out of the @if ($crud->checkIfFieldIsFirstOfItsType($field, $fields)) but i also need to do something to initialize the corresponding select. Any ideas for this?

By the way an underscore in the blade file of the custom field is not working.

My code is below for anyone who can use it.

Controller functions

public function getCities()
    {
        $q = Input::get("q");
        $cities = \App\Models\City::where('name', 'like', '%'.$q.'%')->paginate();
        return \Response::json($cities);
    }

    public function getCity($id)
    {
        $city = \App\Models\City::findOrFail($id);
        return \Response::json($city);
    }

field init

$this->crud->addField([
            // 1-n relationship
            'label' => "End", // Table column heading
            'type' => "select2Ajax",
            'name' => 'city_id', // the column that contains the ID of that connected entity;
            'entity' => 'city', // the method that defines the relationship in your Model
            'attribute' => "name", // foreign key attribute that is shown to user
            'model' => "App\Models\City", // foreign key model
            'datasource' => url("api/cities"), // url to controller search function (with /{id} should return model)
            'placeholder' => "Select a city", // placeholder for the select
            'minimumInputLength' => 2, // minimum characters to type before querying results
        ]);

select2Ajax.blade.php

<!-- select2 Ajax -->
<div @include('crud::inc.field_wrapper_attributes') >
    <label>{!! $field['label'] !!}</label>
    <?php $entity_model = $crud->model; ?>
    <input type="hidden"
    name="{{ $field['name'] }}" value="{{ $field['value'] }}"
    @include('crud::inc.field_attributes', ['default_class' =>  'form-control select2Ajax'])
    >

    {{-- HINT --}}
    @if (isset($field['hint']))
        <p class="help-block">{!! $field['hint'] !!}</p>
    @endif
</div>

{{-- ########################################## --}}
{{-- Extra CSS and JS for this particular field --}}
{{-- If a field type is shown multiple times on a form, the CSS and JS will only be loaded once --}}
@if ($crud->checkIfFieldIsFirstOfItsType($field, $fields))

    {{-- FIELD CSS - will be loaded in the after_styles section --}}
    @push('crud_fields_styles')
    <!-- include select2 css-->
    <link href="{{ asset('vendor/backpack/select2/select2.css') }}" rel="stylesheet" type="text/css" />
    <link href="{{ asset('vendor/backpack/select2/select2-bootstrap-dick.css') }}" rel="stylesheet" type="text/css" />
    @endpush

    {{-- FIELD JS - will be loaded in the after_scripts section --}}
    @push('crud_fields_scripts')
    <!-- include select2 js-->
    <script src="{{ asset('vendor/backpack/select2/select2.js') }}"></script>
    <script>
        jQuery(document).ready(function($) {
            // trigger select2 for each untriggered select2 box
            $('.select2Ajax').each(function (i, obj) {
                if (!$(obj).data("select2"))
                {
                    $(obj).select2({
                        placeholder: "{{ $field['placeholder'] }}",
                        minimumInputLength: "{{ $field['minimumInputLength'] }}",
                        ajax: {
                            url: "{{ $field['datasource'] }}",
                            dataType: 'json',
                            quietMillis: 250,
                            data: function (term, page) {
                                return {
                                    q: term, // search term
                                    page: page
                                };
                            },
                            results: function (data, params) {
                                params.page = params.page || 1;

                                return {
                                    results: $.map(data.data, function (item) {
                                        textField = "{{$field['attribute']}}";
                                        return {
                                            text: item[textField],
                                            id: item["id"]
                                        }
                                    }),
                                    more: data.current_page < data.last_page
                                };
                            },
                            cache: true
                        },
                        initSelection: function(element, callback) {
                            // the input tag has a value attribute preloaded that points to a preselected repository's id
                            // this function resolves that id attribute to an object that select2 can render
                            // using its formatResult renderer - that way the repository name is shown preselected
                            if ("{{ $field['value'] }}" !== "") {
                                $.ajax("{{ $field['datasource'] }}" + '/' + "{{$field['value']}}", {
                                    dataType: "json"
                                }).done(function(data) {
                                    textField = "{{$field['attribute']}}";
                                    callback({ text: data[textField], id: data["id"] });
                                });
                            }
                        },
                    });
                }
            });
        });
    </script>
    @endpush

@endif
{{-- End of Extra CSS and JS --}}
{{-- ########################################## --}}

@tabacitu
Copy link
Member

tabacitu commented Oct 7, 2016

Hi @maesklaas ,

OMG - this is exactly what I was going to suggest! I do think it's the best way to achieve this and it would be a useful addition. Especially since we now have AJAX datatables - tables with millions of entries are a possibility.

  1. I'd call this select2_ajax.blade.php and include it as a standard field in the core and in the documentation. @OwenMelbz , do you agree? See any reason why we shouldn't?

  2. For saving multiple entries you need to take a look / do the same thing with select2_multiple. Basically the difference would be in the definition:

$this->crud->addField([ // 1-n relationship with AJAX
            'label' => "End", // Table column heading
-           'type' => "select2Ajax",
+           'type' => "select2_multiple_ajax", // or maybe select2_ajax_multiple would be better?
            'name' => 'city_id', // the column that contains the ID of that connected entity;
            'entity' => 'city', // the method that defines the relationship in your Model
            'attribute' => "name", // foreign key attribute that is shown to user
            'model' => "App\Models\City", // foreign key model
            'datasource' => url("api/cities"), // url to controller search function (with /{id} should return model)
            'placeholder' => "Select a city", // placeholder for the select
            'minimumInputLength' => 2, // minimum characters to type before querying results
+           'pivot' => true,
        ]);

and in the <select> (it needs the "multiple" attribute). Here's a snapshot of the difference between select2 and select2_multiple, maybe it will help.

  1. Question: in the blade file you're only using the getCities() method, right? That's what the api/cities/ route is pointing to? I want to also suggest that method in the documentation, to make it easier for people to implement.

  2. Where should we recommend that the user places the getCities() method? On the current EntityCrudController on on the CityCrudController, if he has one?

  3. I say we should think hard about the names here before including in the core, no way back afterwards. I think we only need to change the casing, to keep it consistent between fields. Your thoughts?

$this->crud->addField([
            // 1-n relationship
            'label' => "End", // Table column heading
-            'type' => "select2Ajax",
+            'type' => "select2_ajax",
            'name' => 'city_id', // the column that contains the ID of that connected entity;
            'entity' => 'city', // the method that defines the relationship in your Model
            'attribute' => "name", // foreign key attribute that is shown to user
            'model' => "App\Models\City", // foreign key model
-           'datasource' => url("api/cities"), // url to controller search function (with /{id} should return model)
+           'data_source' => url("api/cities"), // url to controller search function (with /{id} should return model)
            'placeholder' => "Select a city", // placeholder for the select
-           'minimumInputLength' => 2, // minimum characters to type before querying results
+           'minimum_input_length' => 2, // minimum characters to type before querying results

        ]);
  1. Would you like to create a fork & pull request to make this official? The benefit is that you'd also appear on Github as a contributor to Backpack. And I love it when that number grows :-) If you don't have the time, I or @OwenMelbz can take care of including it.

That being said - thank you for this. I really think it's a useful new feature.

Cheers

@maesklaas
Copy link
Contributor Author

Hi @tabacitu

Great that you like my try. I had an error when i used the select2_ajax name, but i just retried that and it fine now. So i will make a pull request somewhere next week. And i will add the multiple version as well, select2_ajax_multiple seems to be the best fit i believe. I agree with the name changes in 5. I am actually using the getCity request as well in the ajax call in the initSelection where i use the route api/cities/{id} to get the current model for the edit. Maybe i should create an other property in the field definition such that the route doesn't have to be similar to the data_source route, what do you think?

Before i will make a pull request there is one other issue that should be solved. When you have a form in which i include multiple of these select2_ajax fields, all of these fields will use the javascript code of the first one due to the
@if ($crud->checkIfFieldIsFirstOfItsType($field, $fields))
So getting the specific javascript code outside this if is definitely one step to take. But besides that the jquery part $('.select2Ajax') has to be changed such that it is specific for every select, any suggestions?

@tabacitu
Copy link
Member

tabacitu commented Oct 7, 2016

Hi @maesklaas

Hmm... I guess we can merge the city methods then, make the $id parameter optional and return:

  • an item, if an $id was passed
  • a number of items, if "q" was passed

That would make it seem easier to implement. I think. Not super-sure, though :-)

About the JS: I guess we can place the custom JS outside the @if ($crud->checkIfFieldIsFirstOfItsType($field, $fields)) and trigger the javascript on a custom ID, instead of class, something like $("$select2_ajax_{{ $field->name }}") since $field->name is unique anyway.

@tabacitu
Copy link
Member

tabacitu commented Oct 7, 2016

Please check out my previous comment on github, not email. I just edited it.

I'm not usually a premature commenter, but happens to all guys at one point, I guess... :-))

@maesklaas
Copy link
Contributor Author

The JS does seem the way to go, i'm not sure about merging the methods either. It might also be possible that users want to reuse methods they already have for this, especially when they have an api, and by merging them we force them to create new methods.

@OwenMelbz
Copy link
Contributor

@maesklaas how's this coming?

@ghost
Copy link

ghost commented Nov 20, 2016

FWIW I had a play with this yesterday....I implemented it as

public function autocomplete($table, $column)
....

Route::get('search/{table}/{column}', 'SearchController@autocomplete');

@maesklaas
Copy link
Contributor Author

maesklaas commented Nov 20, 2016

@OwenMelbz I have been quite busy for some time and didn't find the time to make a pull request yet. I will try to do it in the next couple of weeks. I've you want to add it faster i perfectly fine if you add it yourself.

@maesklaas
Copy link
Contributor Author

@OwenMelbz @tabacitu I just created a pull request ;)

@OwenMelbz
Copy link
Contributor

awesome :) I'm sure @tabacitu will be able to give some feedback once he's got time to see if it can go into a future release!

@AurelDragut
Copy link

AurelDragut commented Nov 29, 2016

what would you say about this?

public function getCities()
    {
        $q = Input::get("q");
        $district = \App\Models\District::where('name', $q)->first();
        $cities = $district->cities();
        return \Response::json($cities);
    }

along with a 1-n relationship and q being a select field containing all the district objects added

@OwenMelbz
Copy link
Contributor

I'm just going to close this up as the original issue has a PR for it, and its been added to the new consolidation list #285

@vermaboys
Copy link

@tabacitu
Copy link
Member

Update: we've had a select2_from_ajax field for a while now.

This was referenced Apr 2, 2020
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

5 participants