Nick G
Nick G

Reputation: 1973

Can you pass an element to a function within the template in Vue?

I'm trying to calculate and set an element's max-height style programmatically based on the number of children it has. I have to do this on four separate elements, each with a different number of children, so I can't just create a single computed property. I already have the logic to calculate the max-height in the function, but I'm unable to pass an element from the template into a function.

I've tried the following solutions with no luck:

  1. <div ref="div1" :style="{ maxHeight: getMaxHeight($refs.div1) }"></div>
    This didn't work because $refs is not yet defined at the time I'm passing it into the function.

  2. Trying to pass this or $event.target to getMaxHeight(). This didn't work either because this doesn't refer to the current element, and there was no event since I'm not in a v-on event handler.

The only other solution I can think of is creating four computed properties that each call getMaxHeight() with the $ref, but if I can handle it from a single function called with different params, it would be easier to maintain. If possible, I would like to pass the element itself from the template. Does anyone know of a way to do this, or a more elegant approach to solving this problem?

Upvotes: 2

Views: 5621

Answers (3)

Alec1017
Alec1017

Reputation: 119

Making a custom directive that operates directly on the div element would probably be your best shot. You could create a directive component like:

export default {
  name: 'maxheight',
  bind(el) {
    const numberOfChildren = el.children.length;

    // rest of your max height logic here

    el.style.maxHeight = '100px';
  }
}

Then just make sure to import the directive in the file you plan on using it, and add it to your div element:

<div ref="div1" maxheight></div>

Upvotes: 1

Nick G
Nick G

Reputation: 1973

I ended up creating a directive like was suggested. It tries to expand/compress when:

  • It's clicked
  • Its classes change
  • The element or its children update


Vue component:

<button @click="toggleAccordion($event.currentTarget.nextElementSibling)"></button>
<div @click="toggleAccordion($event.currentTarget)" v-accordion-toggle>
    <myComponent v-for="data in dataList" :data="data"></myComponent>
</div>

.....

private toggleAccordion(elem: HTMLElement): void {
    elem.classList.toggle("expanded");
}


Directive: Accordion.ts

const expandable = (el: HTMLElement) => el.style.maxHeight = (el.classList.contains("expanded") ?
    [...el.children].map(c => c.scrollHeight).reduce((h1, h2) => h1 + h2) : "0") + "px";

Vue.directive("accordion-toggle", {
    bind: (el: HTMLElement, binding: any, vnode: any) => {
        el.onclick = ($event: any) => {
            expandable($event.currentTarget) ; // When the element is clicked
        };

        // If the classes on the elem change, like another button adding .expanded class
        const observer = new MutationObserver(() => expandable(el));        
        observer.observe(el, {
            attributes: true,
            attributeFilter: ["class"],
        });
    },
    componentUpdated: (el: HTMLElement) => {
        expandable(el); // When the component (or its children) update
    }
});

Upvotes: 2

Philip M&#252;ller
Philip M&#252;ller

Reputation: 141

A cheap trick I learned with Vue is that if you require anything in the template that isnt loaded when the template is mounted is to just put a template with a v-if on it:

<template v-if="$refs">
   <div ref="div1" :style="{ maxHeight: getMaxHeight($refs.div1) }"></div>
</template>

around it. This might look dirty at first, but the thing is, it does the job without loads of extra code and time spend and prevents the errors.

Also, a small improvement in code length on your expandable-function:

const expandable = el => el.style.maxHeight = 
    ( el.classList.contains('expanded') ?  
        el.children.map(c=>c.scrollHeight).reduce((h1,h2)=>h1+h2)
        : 0 ) + 'px';

Upvotes: 2

Related Questions