ElemeFE / element

A Vue.js 2.0 UI Toolkit for Web
https://element.eleme.io/
MIT License
54.11k stars 14.64k forks source link

[Feature Request] Custom header for the el-table-column #4909

Closed keevitaja closed 7 years ago

keevitaja commented 7 years ago

Existing Component

Yes

Component Name

el-table-column

Description

Hello!

Could i have a custom template for the column header?

I need to have some filtering using el-input.

Endresult should be similar to

screenshot_2017-05-17_13-13-53

Leopoldthecoder commented 7 years ago

Use render-header

keevitaja commented 7 years ago

Here's a little render-header snippet if anyone else needs this feature. It is bit tricky to use element ui components in the header.

if (this.dataFilters[k].type == 'date-picker-range') {
    result = h('el-date-picker', {
        props: {
            size: 'small',
            type: 'daterange'
        },
        style: 'font-weight: normal',
        on: {
            input(e) {
                if (! e) return

                // bind selected range to datapicker element
                result.componentInstance.$data.currentValue = e

                // v-model
                self.dataFilters[k].value = e
            }
        }
    })
} else {
    result = h('el-input', {
        props: {
            size: 'small'
        },
        style: 'font-weight: normal',
        on: {
            input(e) {
                // v-model
                self.dataFilters[k].value = e
            }
        }
    })
}

return h('div', { style: 'margin-bottom: 10px' }, [
    h('div', null, e.column.label), result
])

Or perhaps there is a better way?

Leopoldthecoder commented 7 years ago

You can use JSX

Sancho66 commented 7 years ago

Hi , I use your example with render-headers , but I don't now how to filter my data table (called usersList) on "firstname" input event.

My table column : <el-table-column sortable render-header="filterFirstname" prop="firstname" label="Prénom" min-width="100"> </el-table-column>

My method:

filterFirstname(h,{column,$index})
            {
                let self = this
                let result = h('el-input', {
                    props: {
                        size: 'small'
                    },
                    style: 'font-weight: normal',
                    on: {
                        input(query) {
                            //TODO : filter rows by query 

                        }
                    }
                })

                return h('div', { style: 'margin-bottom: 10px' }, [
                    h('div', null, column.label), result
                ])
            },

Can you help me ?

LawyerWebber commented 7 years ago

@keevitaja can you paste your full code??

keevitaja commented 7 years ago

Sorry guys... It has been a while i worked on this project, but the full component code is here:

<template>
    <div class="ajax-table">
        <div class="row" v-if="filterable || actions">
            <div class="col-md-3">
                <div class="form-group" v-if="filterable">
                    <el-input v-model="filter"></el-input>
                </div>
            </div>

            <div class="col-md-3 col-md-offset-6">
                <div class="form-group">
                    <el-select v-model="actionsUrl" :placeholder="actionsChoose" v-if="actions">
                        <el-option
                            v-for="item in actions"
                            :label="item.name"
                            :value="item.url"
                            :key="item.id"
                        ></el-option>
                    </el-select>

                    <form style="display:none;" method="POST" ref="actionsForm">
                        <input type="hidden" name="_token" :value="token">

                        <template v-for="row in rows">
                            <input type="hidden" name="items[]" :value="row.id" v-if="row.checked">
                        </template>
                    </form>
                </div>
            </div>
        </div>

        <el-table :data="rows" stripe @selection-change="doSelection">
            <el-table-column type="selection" width="50" v-if="actions"></el-table-column>

            <template v-for="column, k, i in columns">
                <el-table-column :label="column" :width="(k == 'id') ? '100' : 'auto'" :render-header="renderHeader">
                    <template scope="scope"><span v-html="objectPath(rows[scope.$index], k)"></span></template>
                </el-table-column>
            </template>

            <el-table-column v-if="buttons.length > 0" label="" align="right">
                <template scope="scope">
                    <template v-for="button in rows[scope.$index].buttons">
                        <confirmed-destroy classes="el-button el-button--danger el-button--small" :url="button.url" v-if="button.type == 'destroy'">
                            {{ button.label }}
                        </confirmed-destroy>

                        <form
                            style="display: inline;"
                            method="POST"
                            :action="button.url"
                            v-else-if="button.type == 'store' || button.type == 'update'"
                        >
                            <input type="hidden" name="_token" :value="token">
                            <input type="hidden" name="_method" value="PATCH" v-if="button.type == 'update'">

                            <template v-for="value, key in button.payload">
                                <input type="hidden" :name="key" :value="value">
                            </template>

                            <a class="el-button el-button--default el-button--small" :href="button.url" @click.prevent="submit">
                                {{ button.label }}
                            </a>
                        </form>

                        <a class="el-button el-button--default el-button--small" :href="button.url" v-else>{{ button.label }}</a>
                    </template>
                </template>
            </el-table-column>
        </el-table>

        <div class="paginate-table" v-if="pagination.total > dataParams.perpage">
            <el-pagination
                layout="sizes, prev, pager, next"
                :total="pagination.total"
                :page-size="dataParams.perpage"
                :current-page="pagination.current"
                :page-sizes="[10, 20, 30, 40, 100]"
                @current-change="paginate"
                @size-change="updatePageSize"
            ></el-pagination>
        </div>
    </div>
</template>

<script>
    import regex from './../../utils/regex.js'
    import objectPath from 'object-path'

    export default {
        props: {
            actions: {
                default: false
            },
            url: {
                type: String,
                required: true
            },
            columns: {
                type: Object,
                required: true
            },
            filterable: {
                type: Boolean,
                default: false
            },
            filters: {
                type: Object,
                default: ()=> {
                    return {}
                }
            },
            anchors: {
                type: Object,
                default: ()=> {
                    return {}
                }
            },
            buttons: {
                type: Array,
                default: ()=> {
                    return []
                }
            },
            noresults: {
                type: String,
                default: objectPath.get(window.vars, 'translations.table.noresults', 'script.table.noresults')
            },
            params: {
                type: Object,
                default: ()=> {
                    return {
                        paginate: 10,
                        filters: {},
                        perpage: 10
                    }
                }
            },
            payload: {
                type: Object,
                default: ()=> {
                    return {}
                }
            }
        },
        data() {
            return {
                actionsChoose: objectPath.get(window.vars, 'script.table.actions', 'script.table.actions'),
                actionsUrl: false,
                dataFilters: {},
                filter: '',
                id: 'ajax-table-' + this._uid,
                loaded: false,
                rows: [],
                toggleActions: false,
                token: window.vars.csrf,
                pagination: {
                    current: 0,
                    total: 0
                }
            }
        },
        computed: {
            dataParams() {
                let params = this.params

                params.filters = this.dataFilters

                return params
            }
        },
        methods: {
            applyAnchors(rows) {
                for (let column in this.anchors) {
                    let url = this.anchors[column]
                    let fields = regex(url, /-([a-z_.]+)-/gi)

                    for (let row of rows) {
                        let parsed

                        for (let field of fields) {
                            parsed = url.replace('-' + field + '-', objectPath.get(row, field))
                        }

                        objectPath.set(row, column, `<a href="${parsed}">${objectPath.get(row, column)}</a>`)
                    }
                }
            },
            applyButtons(rows) {
                for (let row of rows) {
                    row.buttons = []

                    for (let button of this.buttons) {
                        let fields = regex(button.url, /-([a-z_.]+)-/gi)
                        let parsed

                        for (let field of fields) {
                            parsed = button.url.replace('-' + field + '-', row[field])
                        }

                        row.buttons.push({
                            url: parsed,
                            label: button.label,
                            type: button.type,
                            payload: button.payload
                        })
                    }
                }
            },
            camel(str) {
                return str.replace(/^([A-Z])|[\s-_](\w)/g, function(match, p1, p2, offset) {
                    if (p2) return p2.toUpperCase()
                    return p1.toLowerCase()
                })
            },
            load(page) {
                this.dataParams.page = page

                let options = {
                    params: this.dataParams
                }

                this.$http(this.url, options).then((res)=> {
                    this.rows = res.data.data.map((o)=> {
                        o.checked = false

                        return o
                    })

                    if (this.rows.length > 0) this.loaded = true

                    this.pagination.current = res.data.current_page
                    this.pagination.total = res.data.total

                    this.applyAnchors(this.rows)
                    this.applyButtons(this.rows)
                }).catch((err)=> console.log(err))
            },
            doSelection(rows) {
                for (let row of this.rows) {
                    row.checked = false
                }

                for (let row of rows) {
                    row.checked = ! row.checked
                }
            },
            paginate(e) {
                this.load(e)
            },
            updatePageSize(e) {
                this.dataParams.perpage = e
                this.pagination.current = 1
                this.load(1)
            },
            renderHeader(h, e) {
                const i = this.actions ? e.$index - 1 : e.$index
                const k = Object.keys(this.columns)[i]
                const self = this

                let result

                if ( ! this.dataFilters[k]) {
                    return e.column.label
                }

                if (this.dataFilters[k].type == 'date-picker-range') {
                    result = h('el-date-picker', {
                        props: {
                            size: 'small',
                            type: 'daterange'
                        },
                        style: 'font-weight: normal',
                        on: {
                            input(e) {
                                if (! e) return
                                result.componentInstance.$data.currentValue = e
                                self.dataFilters[k].value = e
                            }
                        }
                    })
                } else {
                    result = h('el-input', {
                        props: {
                            size: 'small'
                        },
                        style: 'font-weight: normal',
                        on: {
                            input(e) {
                                self.dataFilters[k].value = e
                            }
                        }
                    })
                }

                return h('div', { style: 'margin-bottom: 10px' }, [
                    h('div', null, e.column.label), result
                ])
            },
            seek(page) {
                if (page > 0 && page <= this.paginator.total) {
                    return this.load(page)
                }
            },
            singleFilterAdd(column) {
                this.dataParams.filters[column] = this.$refs['filters_' + column][0].value
                this.load(1)
            },
            dateFilterAdd(column, type) {
                console.log('test')
            },
            objectPath(object, key, def = null) {
                return objectPath.get(object, key, def)
            },
            submit(evt) {
                evt.target.parentElement.submit()
            }
        },
        watch: {
            actionsUrl() {
                if (this.actionsUrl) {
                    let form = this.$refs.actionsForm
                    form.action = this.actionsUrl
                    form.submit()
                }
            },
            filter() {
                this.dataParams.filter = this.filter
                this.load(1)
            },
            toggleActions() {
                this.rows.map((o)=> {
                    o.checked = this.toggleActions
                })
            }
        },
        mounted() {
            this.load(1)

            for (let column in this.filters) {
                let type = this.filters[column]

                this.$set(this.dataFilters, column, {})
                this.$set(this.dataFilters[column], 'value', '')

                this.dataFilters[column].type = type
            }

            this.$watch('dataFilters', ()=> {
                this.load(1)
            }, { deep: true })
        }
    }
</script>
cag2050 commented 6 years ago

This is demo for table header add el-popover(use vue-data-tables plugin), web address:https://github.com/cag2050/vue_tables_xlsx_demo/blob/master/src/views/useDataTablesComp.vue

chlab commented 6 years ago

Just as an example, for reference. To render the following column header:

<div class="phase-header">
  <div class="phase-header__title">Phase 1</div>
  <span class="phase-header__details">some details</span>
</div>

You need the following code:

<template>
  <el-table-column
    label="Phase 1"
    :render-header="renderPhaseHeader"
  />
</template>
<script>
  ...
  methods: {
    renderPhaseHeader (h, { column }) {
      return h('div', { 'class': 'phase-header' }, [
        h('div', { 'class': 'phase-header__title' }, column.label),
        h('span', { 'class': 'phase-header__details' }, 'some details')
      ])
    }
  }
</script>
ivila commented 6 years ago

I think this might be cleaner

first, make athother component base on el-table-column

vue.component('my-table-column', {
    props: {
        columnKey: String,
        // others
    },
    methods: {
        renderHeader(h, {column, store}){
            this.column = column
            this.store = store
            let header = h('span', [column.label, h('span', {class: 'el-table__column-filter-trigger'}, [this.$scopedSlots.filtering(this.updateChange)]) ])
            return header
        },
        updateChange(newVal){
            this.store.commit('filterChange', { column: this.column, values: newVal})
            this.store.updateAllSelected()
            this.column.filteredValue = newVal
        }
    },
    render(h){
        return (
            <el-table-column
                column-key={ this.columnKey }
                scopedSlots={ this.$scopedSlots }
                render-header={ this.$scopedSlots.filterSlot ?this.renderHeader : null}>
            </el-table-column> 
        )
    }
})

then, using it just change the tag, add the slot, and binding the value change event.

<my-table-column column-key="key">
    <template slot-scope="scope">
        <span>{{ scope.row }}</span>
    </template>
    <template slot="filterSlot" slot-scope="whatever">
        <el-input @input="whatever"></el-input>
    </template>
</my-table-column>
jagannathprasad commented 6 years ago

How to render header with an el-select with some options. E.g. a select list like below. <el-select v-model="status_filter" size="mini" clearable @change="getList" placeholder="Select">

Verron commented 6 years ago

@jagannathprasad Here's an example of me using el-select in an header. Hope it helps.


                        <el-table-column prop="type" label="Type"
                                         :render-header="renderSelectHeader"
                        >
        data() {
            return {
                dialog: {
                    visible: false,
                    loading: false,
                },
                expense: {
                    date: null,
                    items: [],
                    new_item: { date: null, type: null }
                },
                expenses: [],
                expenses_ref: null,
                expense_types: [{
                    value: 'food', name: 'Food',
                }]
            };
        },
            renderSelectHeader(createElement, { column, store }) {
                let element = createElement('div', { class: 'el-table-header-resource' }, [
                    createElement('el-select', {
                        props: { placeholder: column.label, value: this.expense.new_item[column.property]},
                        on: {
                            input: (value) => {
                                this.expense.new_item[column.property] = value;
                            }
                        }
                    }, this.expense_types.map((item) => {
                        return createElement('el-option', {props: { key: item.value, value: item.value, label: item.name}});
                    }))
                ]);

                element.fnScopeId = this.$options._scopeId;
                element.fnContext = this;

                return element;
            },
calvinxie668 commented 6 years ago

hi guys,i want to do the same with render-header,but there's a little difference that i use reusable components as tag. now,here comes problem ,i want to callback function in the reusable components with ref,but it doesn't work. the codes as follow: ` handleRenderHeader (h,{column,$index}) { let self = this; let item = column.label; const source = this.filterSettingMap.get(column.property); const data = {
on: { input(val) { self.filters[column.property] = val; },

    },
    props: {
      source: source,
      value: self.filters[column.property],
    },
    ref: 'filterValue_'+source.id,
  }
  return (
    <span>{item}
    <FilterValue  {...data}></FilterValue>
    </span>

    )
},`

`watch:{ filters: { handler(form) { console.log(this.$refs); window.setTimeout(() => { const obj = {} for (let key in form) { const map = this.filterSettingMap.get(key) const value = form[key]

        if (value === '' || value.length === 0 || (value.length === 2 && value[0] === '' && value[1] === '')) {
          obj[key] = false
        }else {
          const name = map['name']
          const str = 'filterValue_' + key;
          // this.$refs have no filterValue_... instance
          const label = this.$refs[str][0].getLabel();
            obj[key] = { name, key, label, value }
        }
      }
      this.fillListFilter(obj)
    }, 0)
  },
  deep: true,
}   

},`

kife-design commented 6 years ago

I'm using this as well. I can filter per column, however, i also support sorting per column. The probblem is that when i click the textfield inside the header, the sorting is triggered also.

Tried to prevent bubbling inside the click event of the textfield but without luck. Any ideas on this?

renderHeader: function (h, {column, $index}) { var self = this; return h('span', {class: 'header'}, [ h('span', {class: 'header-label'}, column.label), h('el-input', { props: { size: 'mini' }, style: 'font-weight: debouncedebnormal; padding: 0', on: { input: function (e) { e.preventPropagation(); } } }) ]) },

calvinxie668 commented 6 years ago

@kife-design you can try to bind the pevent event in nativeOn object ,just like
...` nativeOn: { click(e){ e.stopPropagation(); } }

`