niiknow / vue-datatables-net

Vue jQuery DataTables.net wrapper component
https://niiknow.github.io/vue-datatables-net/
MIT License
171 stars 58 forks source link

How to fixed options is undefined? #51

Closed yurenlimbu closed 2 years ago

yurenlimbu commented 4 years ago

I just copied code as example and setup but not working as expected.

My Datatable component

  <div
    :class="classes"
  >
    <table
      v-once
      :id="tableId"
      ref="table"
      :class="className"
      cellpadding="0"
    >
      <thead>
        <tr>
          <th
            v-for="(field, i) in options.columns"
            :key="i"
            :class="field.className"
          >
            <slot
              :name="'HEAD_'+field.name"
              :field="field"
              :i="i"
            >
              <div v-html="field.title" />
            </slot>
          </th>
        </tr>
      </thead>
    </table>
  </div>
</template>

<script>
let myUniqueId = 1

export default {
  name: 'DataTable',
  props: {
    /**
     * The table id
     *
     * @type String
     */
    id: {
      type: String,
      default: null
    },
    /**
     * Set the container classes.
     *
     * @type String
     */
    containerClassName: {
      type: String,
      default: 'table-responsive d-print-inline'
    },
    /**
     * Set the table classes you wish to use, default with bootstrap4
     * but you can override with: themeforest, foundation, etc..
     *
     * @type String
     */
    className: {
      type: String,
      default: 'table table-striped table-bordered nowrap w-100'
    },
    /**
     * the options object: https://datatables.net/manual/options
     *
     * @type Object
     */
    opts: {
      type: Object
    },
    /**
     * List all fields to be converted to opts columns
     *
     * @type Object
     */
    fields: {
      type: Object
    },
    /**
     * Pass in DataTables.Net jQuery to resolve any conflict from
     * multiple jQuery loaded in the browser
     *
     * @type Object
     */
    jquery: {
      type: Object
    },
    /**
     * Pass in Vue to resolve any conflict from multiple loaded
     *
     * @type Object
     */
    vue: {
      type: Object
    },
    /**
     * The select-checkbox column index (start at 1)
     * Current implementation require datatables.net-select
     *
     * @type Number
     */
    selectCheckbox: {
      type: Number
    },
    /**
     * Provide custom local data loading.  Warning: this option has not been
     * thoroughly tested.  Please use ajax and serverSide instead.
     *
     * @type Function
     */
    dataLoader: {
      type: Function
    },
    /**
     * true to hide the footer of the table
     *
     * @type Boolean
     */
    hideFooter: {
      type: Boolean
    },
    /**
     * The details column configuration of master/details.
     *
     * @type {Object}
     */
    details: {
      type: Object
    }
  },
data() {
    // initialize defaults
    return {
      tableId: null,
      options: {
        dom: "tr<'row vdtnet-footer'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'pl>>",
        columns: [],
        language: {
          infoFiltered: ''
        },
        lengthMenu: [ [15, 100, 500, 1000, -1], [15, 100, 500, 1000, 'All'] ],
        pageLength: 15,
        buttons: []  // remove any button defaults
      },
      dataTable: null,
      vdtnet: this,
    }
  },
  computed: {
    jq() {
      return this.jquery || window.jQuery
    },
    myVue() {
      return this.vue || window.Vue
    },
    classes() {
      const that  = this
      let classes = `${that.containerClassName} vdtnet-container`
      if (this.hideFooter) {
        classes += ' hide-footer'
      }

      return classes
    }
  },
  created() {
    const that = this
    const jq   = that.jq

    that.tableId = that.id || `vdtnetable${myUniqueId++}`

    // allow user to override default options
    if (that.opts) {
      that.options = jq.extend({}, that.options, that.opts)
    }
  },
  mounted() {
    const that   = this
    const jq     = that.jq
    const $el    = jq(that.$refs.table)
    const orders = []

    let startCol = 0
    let icol     = 0

    // if fields are passed in, generate column definition
    // from our custom fields schema
    if (that.fields) {
      const fields = that.fields
      let cols     = that.options.columns

      for (let k in fields) {
        const field = fields[k]
        field.name  = field.name || k

        // disable search and sort for local field
        if (field.isLocal) {
          field.searchable = false
          field.sortable  = false
        }

        // generate
        let col = {
          title:      field.label || field.name,
          data:       field.data || field.name,
          width:      field.width,
          name:       field.name,
          className:  field.className,
          index:      field.index || (icol + 1)
        }

        if (field.width) {
          col.width = field.width
        }

        if (field.hasOwnProperty('defaultContent')) {
          col.defaultContent = field.defaultContent
        }

        if (field.hasOwnProperty('sortable')) {
          col.orderable = field.sortable
        }

        if (field.hasOwnProperty('visible')) {
          col.visible = field.visible
        }

        if (field.hasOwnProperty('searchable')) {
          col.searchable = field.searchable
        }

        if (field.hasOwnProperty('editField')) {
            col.editField = field.editField
        }

        if (field.template || that.$scopedSlots[field.name]) {
          field.render = that.compileTemplate(field, that.$scopedSlots[field.name])
        }

        if (field.render) {
          if (!field.render.templated) {
            let myRender = field.render
            field.render = function() {
              return myRender.apply(that, Array.prototype.slice.call(arguments))
            }
          }

          col.render = field.render
        }

        // console.log(col)
        cols.push(col)

        if (field.defaultOrder) {
          orders.push([icol, field.defaultOrder])
        }

        icol++
      }

      // sort columns
      cols = cols.sort((a, b) => a.index - b.index)
    }

    // apply orders calculated from above
    that.options.order = that.options.order || orders

    if (that.selectCheckbox) {
      that.selectCheckbox = that.selectCheckbox || 1

      // create checkbox column
      const col = {
        orderable: false,
        searchable: false,
        name: '_select_checkbox',
        className: 'select-checkbox d-print-none',
        data: null,
        defaultContent: '',
        title: '<input type="checkbox" class="select-all-checkbox d-print-none">',
        index: (that.selectCheckbox - 1)
      }
      that.options.columns.splice(that.selectCheckbox - 1, 0, col)

      // console.log(that.options.columns)
      that.options.select = that.options.select || { style: 'os', selector: 'td.select-checkbox' }

      if (that.selectCheckbox === 1) {
        startCol++
      }
    }

    // handle master details
    if (that.details) {
      that.details.index = that.details.index || 1

      // create details column
      const col = {
        orderable: false,
        searchable: false,
        name: '_details_control',
        className: 'details-control d-print-none',
        data: null,
        defaultContent: that.details.icons || '<span class="details-plus" title="Show details">+</span><span class="details-minus" title="Hide details">-</span>',
        index: (that.details.index - 1)
      }
      that.options.columns.splice(that.details.index - 1, 0, col)

      if (that.details.index === 1) {
        startCol++
      }
    }

    if (startCol > 0) {
      if (that.options.order) {
        that.options.order.forEach((v) => {
          v[0] += startCol
        })
      } else {
        that.options.order = [[startCol, 'asc']]
      }
    }

    // handle local data loader
    if (that.dataLoader) {
      delete that.options.ajax
      that.options.serverSide = false
    }

    // you can access and update the that.options and $el here before we create the DataTable
    that.$emit('table-creating', that, $el)

    that.dataTable = $el.DataTable(that.options)
    if (that.selectCheckbox) {
      // handle select all checkbox
      $el.on('click', 'th input.select-all-checkbox', (e) => {
        if(jq(e.target).is(':checked')) {
          that.dataTable.rows().select()
        } else {
          that.dataTable.rows().deselect()
        }
      })

      // handle individual row select events
      that.dataTable.on('select deselect', (e, dt, type, indexes) => {
        const $input = $el.find('th input.select-all-checkbox')
        if (that.dataTable.rows({
            selected: true
          }).count() !== that.dataTable.rows().count()) {
          jq('th.select-checkbox').removeClass('selected')
          $input.attr('checked', false)
        } else {
          jq('th.select-checkbox').addClass('selected')
          $input.attr('checked', true)
        }

        // type is select/deselect so event become row-select or row-deselect
        that.$emit('row-' + e.type, {
          dataTable: that.dataTable,
          e: e,
          dt: dt,
          type: type,
          indexes: indexes
        })

        // to get data, see const examples below
        // const rows = event.dataTable.rows( event.indexes )
        // const data = rows.data()
      })
    }

    // wire up edit, delete, and/or action buttons
    $el.on('click', '[data-action]', (e) => {
      e.preventDefault()
      e.stopPropagation()
      let target = jq(e.target)
      let action  = target.attr('data-action')
      while(!action) {
        // don't let it propagate outside of container
        if (target.hasClass('vdtnet-container') ||
          target.prop('tagName') === 'table') {
          // no action, simply exit
          return
        }
        target = target.parent()
        action = target.attr('data-action')
      }

      // only emit if there is action
      if (action) {
        // detect if row action
        let tr = target.closest('tr')
        if (tr) {
          if (tr.attr('role') !== 'row') {
            tr = tr.prev()
          }
          const row  = that.dataTable.row(tr)
          const data = row.data()
          that.$emit(action, data, row, tr, target)
        } else {
          // not a row click, must be other kind of action
          // such as bulk, csv, pdf, etc...
          that.$emit(action, null, null, null, target)
        }
      }
    })

    // handle master/details
    if (that.details) {
      // default to render function
      let renderFunc = that.details.render

      // must be string template
      if (that.details.template || that.$scopedSlots['_details']) {
        renderFunc = that.compileTemplate(that.details, that.$scopedSlots['_details'])
      } else if (renderFunc) {
        renderFunc = function() {
          return that.details.render.apply(that, Array.prototype.slice.call(arguments))
        }
      }

      // handle master/details
      // Add event listener for opening and closing details
      $el.on('click', 'td.details-control', (e) => {
        e.preventDefault()
        e.stopPropagation()
        const target = jq(e.target)
        let tr       = target.closest('tr')
        if (tr.attr('role') !== 'row') {
          tr = tr.prev()
        }
        const row = that.dataTable.row( tr )
        if ( row.child.isShown() ) {
          // This row is already open - close it
          row.child.hide()
          tr.removeClass('master')
        }
        else {
          // Open this row
          const data = row.data()
          row.child( renderFunc(data, 'child', row, tr) ).show()
          tr.addClass('master')
        }
      })
    }

    that.$emit('table-created', that)

    // finally, load data
    if (that.dataLoader) {
      that.reload()
    }
  },
  beforeDestroy() {
    const that = this
    if (that.dataTable) {
      that.dataTable.destroy(true)
    }

    that.dataTable = null
  },
  methods: {
    /**
     * Vue.compile a template string and return the compiled function
     *
     * @param  {Object} object with template property
     * @param  {Object} the slot
     * @return {Function}          the compiled template function
     */
    compileTemplate(field, slot) {
      const that = this
      const jq   = that.jq
      const vue  = that.myVue
      const res  = vue.compile(`<div>${field.template || ''}</div>`)

      const renderFunc = (data, type, row, meta) => {
        let myRender = res.render

        if (slot) {
          myRender = (createElement) => {
            return createElement('div', [
              slot({
                data: data,
                type: type,
                row: row,
                meta: meta,
                vdtnet: that,
                def: field,
                comp: that.$parent
              })
            ])
          }
        }

        const comp = new vue({
          data: {
            data: data,
            type: type,
            row: row,
            meta: meta,
            vdtnet: that,
            def: field,
            comp: that.$parent
          },
          render: myRender,
          staticRenderFns: res.staticRenderFns
        }).$mount()
        return jq(comp.$el).html()
      }

      renderFunc.templated = true

      return renderFunc
    },
    /**
     * Set table data array that was loaded from somewhere else
     * This method allow for local setting of data; though, it
     * is recommended to use ajax instead of this.
     *
     * @param {Array} data   the array of data
     * @return {Object}            the component
     */
    setTableData(data) {
      const that = this
      if (data.constructor === Array) {
        that.dataTable.clear().rows.add(data)
        that.dataTable.draw(false)
        that.dataTable.columns.adjust()
      }
      return that
    },
    /**
     * pass through reload method
     *
     * @param  {Boolean}  resetPaging true to reset current page position
     * @return {Object}            the component
     */
    reload(resetPaging = false) {
      const that = this
      if (that.dataLoader) {
        // manual data loading
        that.dataLoader((data) => {
          if (data && !data.data) {
            data = { data: data }
          }
          that.setTableData( data.data )

          that.$emit('reloaded', data, that)
        })
      } else {
        that.dataTable.ajax.reload( (data) => { that.$emit('reloaded', data, that) } , resetPaging )
      }

      return that
    },
    search(value) {
      const that = this
      that.dataTable.search( value ).draw()

      return that
    },
    setPageLength(value) {
      const that = this
      that.dataTable.page.len( value )

      return that.reload()
    },
    getServerParams() {
      if (this.dataLoader) {
        return {}
      }

      return this.dataTable.ajax.params()
    }
  }
}
</script>

My App.vue

<data-table ref="table" :fields="fields" :opts="options" :select-checkbox="1" :details="details"
          @edit="doAlertEdit" @delete="doAlertDelete" @reloaded="doAfterReload" @table-creating="doCreating"
          @table-created="doCreated">
          <template slot="HEAD__details_control">
            <b>Show Details</b>
          </template>
          <template slot="address2" slot-scope="ctx">
            <span>{{ ctx.data.city }}, {{ ctx.comp.formatCode(ctx.data.zipcode) }}</span>
          </template>
          <template slot="_details" slot-scope="ctx">
            <strong>I'm a child for {{ ctx.data.id }} yall</strong>
          </template>
        </data-table>
<script
  import DataTable from '@/components/DataTable.vue'
import 'datatables.net-bs4'

// below you should only import what you need
// Example: import buttons and plugins
import 'datatables.net-buttons/js/dataTables.buttons.js'
import 'datatables.net-buttons/js/buttons.html5.js'
import 'datatables.net-buttons/js/buttons.print.js'

// import the rest for your specific theme
import 'datatables.net-buttons-bs4'
import 'datatables.net-select-bs4'

import 'datatables.net-select-bs4/css/select.bootstrap4.min.css'
import 'datatables.net-buttons-bs4/css/buttons.bootstrap4.min.css'

  export default {
    layout: "backend",
    components: {
      DataTable
    },
    data() {
      const vm = this
      return {
        options: {
          ajax: {
            url: 'https://jsonplaceholder.typicode.com/users',
            dataSrc: (json) => {
              return json
            }
          },
          buttons: ['copy', 'csv', 'print'],
          dom: "Btr<'row vdtnet-footer'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'pl>>",
          responsive: false,
          processing: true,
          searching: true,
          searchDelay: 1500,
          destroy: true,
          ordering: true,
          lengthChange: true,
          serverSide: true,
          fixedHeader: true,
          saveState: true
        },
        fields: {
          id: {
            label: 'ID',
            sortable: true
          },
          actions: {
            isLocal: true,
            label: 'Actions',
            defaultContent: '<a href="javascript:void(0);" data-action="edit" class="btn btn-primary btn-sm"><i class="mdi mdi-square-edit-outline"></i> Edit</a>' +
              '<span data-action="delete" class="btn btn-danger btn-sm"><i class="mdi mdi-delete"></i> Delete</span>'
          },
          name: {
            label: 'Name',
            sortable: true,
            searchable: true,
            defaultOrder: 'desc'
          },
          username: {
            label: 'Username',
            sortable: false,
            searchable: true
          },
          email: {
            label: 'Email'
          },
          address1: {
            label: 'Address1',
            data: 'address',
            template: '{{ data.street }}, {{ data.suite }}'
          },
          address2: {
            label: 'Address2',
            data: 'address'
          },
          phone: {
            label: 'Phone'
          },
          website: {
            label: 'Website',
            render: (data) => {
              return `https://${ data }`
            }
          }
        },
        quickSearch: '',
        details: {}
      }
    },
    methods: {
      doLoadTable(cb) {
        $.getJSON('https://jsonplaceholder.typicode.com/users', function (data) {
          cb(data)
        })
      },
      doAlertEdit(data) {
        window.alert(`row edit click for item ID: ${data.id}`)
      },
      doAlertDelete(data, row, tr, target) {
        window.alert(`deleting item ID: ${data.id}`)
        // row.remove() doesn't work when serverside is enabled
        // so we fake it with dom remove
        tr.remove()
        const table = this.$refs.table
        setTimeout(() => {
          // simulate extra long running ajax
          table.reload()
        }, 1500)
      },
      doAfterReload(data, table) {
        window.alert('data reloaded')
      },
      doCreating(comp, el) {
        console.log('creating')
      },
      doCreated(comp) {
        console.log('created')
      },
      doSearch() {
        this.$refs.table.search(this.quickSearch)
      },
      doExport(type) {
        const parms = this.$refs.table.getServerParams()
        parms.export = type
        window.alert('GET /api/v1/export?' + jQuery.param(parms))
      },
      formatCode(zipcode) {
        return zipcode.split('-')[0]
      }
    },
  }

</script>

Browser error

_vm.options is undefined

An error occurred while rendering the page. Check developer tools console for details.

Console Error

[Vue warn]: Property or method "classes" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.

[Vue warn]: Property or method "className" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.

[Vue warn]: Property or method "tableId" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.

[Vue warn]: Property or method "options" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
dima-bzz commented 4 years ago

Did you show the code exactly as you have it?

yurenlimbu commented 4 years ago

@dima-bzz Yes! My package.json

{
  "name": "example",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "test": "jest"
  },
  "dependencies": {
    "autoprefixer": "^9.8.6",
    "axios": "^0.21.0",
    "core-js": "^3.6.5",
    "nuxt": "^2.14.6",
    "postcss": "^7.0.35",
    "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.1",
    "vuex": "^3.5.1"
  },
  "devDependencies": {
    "@babel/core": "^7.12.9",
    "@babel/plugin-syntax-dynamic-import": "^7.8.3",
    "@babel/preset-env": "^7.12.7",
    "@nuxtjs/tailwindcss": "^3.2.0",
    "@vue/test-utils": "^1.1.0",
    "@xiaobu12361/vue-table-export": "^1.0.28",
    "babel-core": "7.0.0-bridge.0",
    "babel-eslint": "^10.1.0",
    "babel-jest": "^26.5.0",
    "babel-loader": "^8.2.1",
    "datatables.net-bs4": "^1.10.22",
    "datatables.net-buttons-bs4": "^1.6.2",
    "datatables.net-responsive": "^2.2.5",
    "datatables.net-responsive-bs4": "^2.2.5",
    "datatables.net-select-bs4": "^1.3.1",
    "jest": "^26.5.0",
    "jquery": "^3.5.1",
    "vue-csv-downloader": "^1.0.3",
    "vue-datatables-net": "^1.2.8",
    "vue-good-table": "^2.21.1",
    "vue-jest": "^3.0.4",
    "vue-template-compiler": "^2.6.12"
  }
}

After I remove node_modules and reinstall it. Now I got an error in the browser

vue is undefined

An error occurred while rendering the page. Check developer tools console for details.

And in console error

TypeError: vue is undefined
    compileTemplate DataTable.vue:480
    mounted DataTable.vue:244
    VueJS 11
    NuxtJS 4
    Babel 18
    NuxtJS 2
    Babel 12
    NuxtJS 11
client.js:96
    _callee$ NuxtJS
    Babel 8
    VueJS 13
    NuxtJS 4
    Babel 18
    NuxtJS 2
    Babel 12
    NuxtJS 11

In DataTable.vue line no 244 field.render = that.compileTemplate(field, that.$scopedSlots[field.name])

In DataTable.vue line no 480 const res = vue.compile(`<div>${field.template || ''}</div>`)

dima-bzz commented 4 years ago

@yurenlimbu Does everything else work?

yurenlimbu commented 4 years ago

@dima-bzz Yes! Only not working as data-table.

dima-bzz commented 4 years ago

@yurenlimbu Try replacing the code from my branch

yurenlimbu commented 4 years ago

@dima-bzz as your suggestion I use code from you branch. Now I got this error

$el.DataTable is not a function

An error occurred while rendering the page. Check developer tools console for details.

And in console

TypeError: orders is undefined
    created DataTable.vue:316
    VueJS 8
client.js:96
creating DatatableTest.vue:172
TypeError: $el.DataTable is not a function
    mounted DataTable.vue:418
    VueJS 11
    NuxtJS 4
    Babel 18
    NuxtJS 2
    Babel 12
    NuxtJS 11
dima-bzz commented 4 years ago

@yurenlimbu Did you declare jquery?

yurenlimbu commented 4 years ago

@dima-bzz yes!

jquery
dima-bzz commented 4 years ago

@yurenlimbu Look at index.html

dima-bzz commented 4 years ago

@yurenlimbu I'm not familiar with nuxt. But I can assume that jquery is not available or the correct sequence for connecting libraries is not correct

noogen commented 2 years ago

Stale issue.