Vue-material: [feature request] autocomplete

Created on 14 Dec 2016  路  12Comments  路  Source: vuematerial/vue-material

Hi,

autocomplete would be nice feature.

https://material.google.com/components/text-fields.html#text-fields-auto-complete-text-field

When would you implement this?

new component request

Most helpful comment

Hi. The autocomplete components it's on my backlog. I didn't prioritize yet, but will come after 0.5.0. Thanks.

All 12 comments

Hi. The autocomplete components it's on my backlog. I didn't prioritize yet, but will come after 0.5.0. Thanks.

@marcosmoura I've done one already. You can assign me to this task and I will try the component I have (because I developed it using version 0.5.2) on the new code base and, once I'm done with it, I'll create a PR.

I did not forget to create the css helper classes.

Oh. That's amazing @pablohpsilva! I would love to see this PR! :D

Soon enough @marcosmoura! LOL
I just have to have some free time to spare. I think I'll have that time tonight.

You guys can check my progress on this branch. When I'm done I'll PR it.

Hey @pablohpsilva, I've reviewed your work. Good stuff, man! How far are you with the job? I need the autocomplete for my project but it makes no sense to build from scratch if you are almost done with it. Cheers

@gazpachu I had to stop working for a few days. Now I'm putting my shit together and I'll finish it by tomorrow (that's the plan at least).

Thank you @gazpachu !!! :)

OK, I need you guys help @gazpachu @marcosmoura @breadlesscode . The issue is: I'mm not getting why the autocomplete is not working like the md-input component. I did try so many ways and my final try can be seen right here on my vue-material fork. The Autocomplete code is below. The use of vue-resource will be replaced with something else in the future. Keep in mind the code below IS NOT the same I was preparing on my vue-material fork. The code below is how I created an autocomplete component way before I had the idea to implement it for vue-material project.

The code below should work right out of the box. All you have to have: [email protected] and [email protected] installed. Like I said (and can be seen), the vue-resource dependency will be removed, since the idea will be to send the fetchFunction through a param. The <style> is is plain CSS.

<template lang="html">
  <div class="Autocomplete__Wrapper">
    <input class="md-input"
      v-model="query"
      type="text"
      autocomplete="off"
      @keydown.down="down"
      @keydown.up="up"
      @keydown.enter="hit"
      @keydown.esc="reset"
      @blur="onBlur"
      @focus="onFocus"
      @input="update"/>
    <md-button class="md-icon-button Autocomplete__IconButton"
      v-if="hasSearchButton"
      @click="callFetch">
      <md-icon>{{searchIcon}}</md-icon>
    </md-button>
    <ul class="md-list md-theme-default Autocomplete__List"
      v-if="items && hasItems">
      <li class="md-list-item md-menu-item md-option Autocomplete__Item"
        :class="activeClass(index)"
        v-for="(item, index) in items"
        @mousedown="hit"
        @mousemove="setActive(index)">
        <md-button @click.native="hit"
          @mousemover="setActive(index)">
          {{item.name || item.nome || item.nomExtensao}}
        </md-button>
      </li>
    </ul>
  </div>
</template>

<script type="text/javascript">
  import { util } from 'vue';

  // getClosestVueParent from https://vuematerial.github.io
  const getClosestVueParent = (parent, cssClass) => {
    if (!parent || !parent.$el) {
      return false;
    }

    if (parent._uid === 0) { // eslint-disable-line no-underscore-dangle
      return false;
    }

    if (parent.$el.classList.contains(cssClass)) {
      return parent;
    }

    return getClosestVueParent(parent.$parent, cssClass);
  };

  export default {
    name: 'MdTypeahead',
    props: {
      url: {
        type: String,
        default() {
          return 'https://typeahead-js-twitter-api-proxy.herokuapp.com/demo/search';
        },
      },
      queryParam: {
        type: String,
        default() { return 'q'; },
      },
      minChars: {
        type: Number,
        default() { return 3; },
      },
      fetchFunction: {
        type: Function,
      },
      hasSearchButton: {
        type: Boolean,
        default() { return false; },
      },
      searchIcon: {
        type: String,
        default() { return 'search'; },
      },
      limit: {
        type: Number,
        default() { return 0; },
      },
    },
    data() {
      return {
        items: [],
        query: '',
        current: -1,
        loading: false,
        selectFirst: false,
        selected: null,
      };
    },
    computed: {
      hasItems() {
        return this.items.length > 0
      },
      isEmpty() {
        return !this.query
      },
      isDirty() {
        return !!this.query
      },
      parentContainer() {
        if (this.$parent) {
          return getClosestVueParent(this.$parent, 'md-input-container');
        }
        util.warn('You need to use this component wrapped in a \'md-input-container\' component. Material Design guide.', this);
      },
      hasSelected() {
        return !!(this.selected && this.query.length);
      },
      selectedText() {
        return this.selected ?
          (this.selected.name || this.selected.nome || this.selected.nomExtensao || this.selected.title) :
          '';
      },
    },
    watch: {
      query(value) {
        this.setParentValue(value);
      },
    },
    methods: {
      update() {
        if (!this.query) {
          return this.reset();
        }
        if (this.minChars && this.query.length < this.minChars) {
          return;
        }
        this.loading = true;
        this.callFetch();
      },
      callFetch() {
        if (this.fetchFunction) {
          this.fetchFunction().then((response) => this.handleRequest(response));
          return;
        }
        this.fetch().then((response) => this.handleRequest(response));
      },
      fetch() {
        if (!this.$http) {
          return util.warn('You need to install the `vue-resource` plugin', this);
        }
        if (!this.url) {
          return util.warn('You need to set the `src` property', this);
        }
        const src = this.queryParam
          ? this.url
          : this.url + this.query;
        const params = this.queryParam
          ? Object.assign({ [this.queryParam]: this.query }, this.data)
          : this.data;
        return this.$http.get(src, { params });
      },
      handleRequest(response) {
        if (this.query) {
          let data = (response.data.hasOwnProperty('length')) ?
            response.data :
            response.data.resposta;

          data = this.prepareResponseData ?
            this.prepareResponseData(data) :
            data;
          this.items = this.limit ?
            data.slice(0, this.limit) :
            data;
          this.current = -1;
          this.loading = false;
          if (this.selectFirst) {
            this.down();
          }
        }
      },
      reset() {
        this.items = [];
        this.loading = false;
      },
      setActive (index) {
        this.current = index;
      },
      activeClass (index) {
        return {
          active: this.current === index
        };
      },
      hit() {
        if (this.current !== -1) {
          this.onHit(this.items[this.current]);
        }
      },
      up() {
        if (this.current - 1 < 0) {
          this.current = this.items.length - 1;
          return;
        }
        this.current -= 1;
      },
      down() {
        if ((this.current + 1) >= this.items.length) {
          this.current = 0;
          return;
        }
        this.current += 1;
      },
      onHit(hit) {
        this.selected = hit;
        this.query = this.selectedText;
        this.$emit('TYPEAHEAD_SELECTED', hit);
        this.reset();
      },
      onFocus() {
        this.parentContainer.isFocused = true;
      },
      onBlur() {
        setTimeout(() => {
          this.parentContainer.isFocused = false;
          this.reset();
        }, 1E2);
      },
      setParentValue(query) {
        this.parentContainer.setValue(query || this.$el.query);
      },
    }
  }
</script>

<style lang="css" scoped>
  .Autocomplete__Wrapper {}
  .md-list.Autocomplete__List {
    position: absolute;
    width: 100%;
    z-index: 10;
    max-height: 200px;
    overflow-y: scroll;
  }
  .Autocomplete__Item.active {
    background-color: rgba(153, 153, 153, 0.2);
    text-decoration: none;
  }
  .Autocomplete__Item.active .md-button.md-theme-default {
    background-color: transparent;
  }
  .Autocomplete__IconButton {
    position: absolute;
    right: 0;
    top: 12px;
  }
  .Autocomplete__IconButton .md-icon {
    color: rgba(0,0,0,0.7);
  }
</style>

@pablohpsilva Hi

Thanks for your code!! I think that these framework is wonderfull..

I have a error with your code @pablohpsilva , when I try use like as component, it generate a error Cannot create property 'isFocused' on boolean 'false'
I'm new in Vue, I'm start 6 days ago and I don't know what util can do, then I don't know why this.parentContainer.isFocused when parentContainer is a function.
Thanks

I'm sorry, I resolved the problem with <md-input-container> before template. :D

Here is the PR #644 :D

Closing this issue as our focus is on the new 1.0.0 version.

https://vuematerial.io/components/autocomplete

Was this page helpful?
0 / 5 - 0 ratings

Related issues

andreujuanc picture andreujuanc  路  3Comments

delueg picture delueg  路  3Comments

capttrousers picture capttrousers  路  3Comments

alexMugen picture alexMugen  路  3Comments

tridcatij picture tridcatij  路  3Comments