1

I have a <table-component /> component which I wish I could create rows and columns with data and columns object from the parent.

I have a situation where I need to render an html template to create a clickable link, I make it want to display the link in the Actions column. Vue version used: ^2.5.17.

This is the code from parent.vue:

<table-component :data="category" :column="column" class="bg-red-200 py-4 mt-8"/>

data() {
  return {
    modalMode: '',
    isShowModalEdit: false,
    category: [
      { id: 1, name: 'Jasa Pre-Order', color: '#eb4034' },
      { id: 2, name: 'Jualan', color: '#fcba03' },
      { id: 3, name: 'Jasa Design', color: '#9f34eb' },
    ],
  }
}
// parent.vue
methods: {
  toggleModalEdit(){
    this.isShowModalEdit = !this.isShowModalEdit
    this.modalMode = 'EDIT'
  }
}
// parent.vue
computed: {
            column() {
                return [
                    {
                        dataField: 'name',
                        text: 'Name',
                    },
                    {
                        dataField: 'color',
                        text: 'Category Color',
                        formatter: (cell,row) => {
                            return `
                                <div style="background-color: ${cell};" class="rounded-full h-8 w-8 flex items-center justify-center mr-2"></div>
                                <div class="font-bold text-gray-500">${cell}</div>
                            `
                        },
                        classes: (cell, row, rowIndex, colIndex) => {
                            return 'flex';
                        }
                    },
                    {
                        dataField: 'actions',
                        text: 'Actions',
                        formatter: (cell,row) => {
                            return `
                                <a href="#" @click="${this.toggleModalEdit}" class="text-indigo-600 hover:text-indigo-900">Edit</a>
                            `
                        },
                    },
                ]
            },
}

And this is the sample code from component.vue:

// component.vue
<tbody class="bg-white divide-y divide-gray-200">
  <tr v-for="(row, rowIndex) in data" :key="rowIndex">
    <td v-for="(col, colIndex) in column" :key="col.dataField" :class=" col.classes ? col.classes(row[col.dataField],row,rowIndex,colIndex) : '' " v-html=" col.formatter ? col.formatter(row[col.dataField],row) : row[col.dataField] " class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"></td>
  <tr>
</tbody>
// component.vue
props: {
            data: {
                type: Array,
                required: true
            },
            column: {
                type: Array,
                required: true
            },
}

The result is like this:

enter image description here

but the link in the Actions column does not work as it should, I hope that when clicking this link will run the method of the parent namely toggleModalEdit. And this is what the link looks like when I inspect it:

enter image description here


I am still new to Vue, I am not sure what I did best or not, I hope you guys can help.

1
  • Use the template and slot if you are okay to use with markups. or else use the new Vue to create the vue instances dynamically. Commented Jan 30, 2021 at 14:58

2 Answers 2

2

Your issue is, that the HTML inside the v-html directive is not processed by Vue's template compiler at all.

Because the compiler doesn't compile this HTML, the @click is not interpreted (but simply rendered without triggering any action).

For this to work, you'd need to iterate over all columns and initialize a new component that handles what's inside the cell yourself directly in HTML (and not in some string that's gonna be rendered later on).

I guess that this is enough - if you still need to interpret what's in the string, you may use Vue.compile to interpret the content. But be careful as it's not safe in case there's some malicious code in it - but since the directive by default has no sanitizing at all, I guess that's just the way Vue.js works.

Sign up to request clarification or add additional context in comments.

5 Comments

"for security reasons" I don't think so. It just doesn't make any sense conceptually. From a security standpoint, I can still perform arbitrary XSS, whether the code is first compiled by Vue's compiler or not.
Thanks for pointing that out, I clarified that in my post. Nevertheless, the use of Vue.compile is still regarding security, because attackers get more options using that.
Remember that I can still inject <script>Vue.compile(/* ... whatever ... */)</script> so this doesn't really matter. It's a conceptual concern.
Wow, being used to Angular it's scary that Vue actually doesn't do any sanitizing by default, didn't know that. Then you're probably right about the background. I don't agree in your statement that it doesn't make sense to have something like that, because the question has some use case for it, but that's not the key point of the discussion.
Thanks to @idmean for the answers
0

Thanks to @SimplyComple0x78 for the answer, I marked your suggestions:

For this to work, you'd need to iterate over all columns and initialize a new component that handles what's inside the cell yourself directly in HTML (and not in some string that's gonna be rendered later on).

so I try to create and initialize a new component, I call it element-generator. reference from here. here's the code:

// element-generator.vue
<script>
    export default {
        render: function (createElement) {
            const generatedChildren = (child) => {
                if(!child) return // when child of undefined
                if(typeof child === 'string') return child // when children is String
                return child.map((e,i,a)=>{
                    if(typeof child[i] == 'string'){
                        return child[i]
                    }else{
                        return createElement(
                            child[i].tag,
                            child[i].attributes,
                            generatedChildren(child[i].children) // javascript recursive
                        )
                    }
                })
            }
            return createElement(
                this.formatter.html.tag,
                this.formatter.html.attributes,
                generatedChildren(this.formatter.html.children)
            )
        },
        props: {
            formatter: {
                type: Object,
                required: true
            },
        },
    }
</script>

and I no longer use v-html in component.vue instead I just do a check inside <td> and call element-generator to handle what's inside the cell:

// component.vue
<tbody class="bg-white divide-y divide-gray-200">
    <tr v-for="(row, rowIndex) in data" :key="rowIndex">
        <td v-for="(col, colIndex) in column" :key="col.dataField"
            :class=" col.classes ? col.classes(row[col.dataField],row,rowIndex,colIndex) : '' "
            class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
        >
            <element-generator v-if="col.formatter" :formatter="col.formatter(row[col.dataField],row)"></element-generator>
            <div v-else>{{row[col.dataField]}}</div>
        </td>
    </tr>
</tbody>

and in parent.vue I replaced the String with the Object that will be passed to the element-generator later, it looks like this:

// parent.vue
computed: {
    column() {
        return [
            {
                dataField: 'name',
                text: 'Name',
            },
            {
                dataField: 'color',
                text: 'Category Color',
                formatter: (cell,row) => {
                    return {
                        html: {
                            tag: 'div',
                            attributes: {
                                class: 'flex'
                            },
                            children:[
                                {
                                    tag: 'div',
                                    attributes: {
                                        style: `background-color: ${cell};`,
                                        class: 'rounded-full h-8 w-8 flex items-center justify-center mr-2',
                                    },
                                },
                                {
                                    tag: 'div',
                                    attributes: {
                                        class: 'font-bold text-gray-500',
                                    },
                                    children: cell
                                },
                            ]
                        }
                    }
                },
            },
            {
                dataField: 'actions',
                text: 'Actions',
                formatter: (cell,row) => {
                    return {
                        html: {
                            tag: 'a',
                            attributes: {
                                class: 'text-indigo-600 hover:text-indigo-900',
                                on: {
                                    click: this.toggleModalEdit
                                },
                                attrs: {
                                    href: "#"
                                },
                            },
                            children: 'Edit'
                        },
                    }
                },
            },
        ]
    },
},

then when I inspect it in the browser, the result is like this(this is different from the previous one): enter image description here

and finally what I want to display when Edit is clicked is now displayed: enter image description here


Thank you very much everyone.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.