I am trying to build a generic dropdown/popover/tooltip system in Vue.js.
The idea is to be able to open nested popover-esque elements, and manage their z layering, focus, re-focus on a child popover closing, etc. The underlying element positioning code may be the likes of Tether or Popper.
The API I want to have is this:
<div id="app">
<div class="page">
<tooltip>
<button>Hover this</button>
<div>Contents of tooltip</div>
</tooltip>
</div>
<popover-container/>
</div>
The end result DOM I want to have is:
<div id="app">
<div class="page">
<button>Hover this</button>
</div>
<div class="popover-container">
<div>Contents of tooltip</div>
</div>
</div>
It is imperative that the <tooltip/> component does not wrap the <button/> element in any additional DOM.
I have tried a number of approaches, none of which work, or at least not how I want them to.
1. Slots
I tried using slots for the <tooltip/> component. This creates a number of problems.
First, the tooltip content is appended right after the button.
Second, since you can't have multiple root nodes in a template, I had to wrap the <button/> and tooltip content in another element, which is a no-no.
I have tried the likes of vue-popper-js, which appends the tooltip content to the body, but it only happens in $nextTick, which means the template has to render with the wrapper span regardless.
2. Virtual DOM
I tried using Vue's render function. While I could render just the button, and using a standalone Vue() bus to send instructions to the <popover-container/> component to render the tooltip content, it is unable to update its content whenever the DOM is supposed to change.
Plus, there doesn't seem to be a documented way to clone VNodes, and since they are readonly, I cannot add event handlers. I hammered this together, but it's silly and wouldn't work the second my <button/> was a little more complex:
render: function (createElement) {
let slotContent = this.$slots.default[0].componentOptions;
return createElement(slotContent.Ctor, {
props: slotContent.propsData,
nativeOn: {
mouseenter: this.onMouseEnter,
...slotContent.listeners
}
}, this.$slots.default);
}
I still use slots in this case, but updates to the tooltip content from the parent component aren't doing anything here. Every time I open the tooltip, it has the old content it was created with.
Anyway, how can I do this? Is it even possible in Vue?
Related
I have been setting up a simple web guide without using any framework, by just using separate Material Design Components. Many of these I had no issues with (I have successfully implemented buttons with ripple effects, cards, tabs...) but I can not get tooltips to work.
Following the documentation, I have tried many things to make them appear, but they do not show.
They are included in my CSS:
#use "#material/tooltip/styles";
Instanciated in my JS:
import {MDCTooltip} from '#material/tooltip';
const tooltip = new MDCTooltip(document.querySelector('.mdc-tooltip'));
And implemented in my HTML as per the example given, first the tooltip is declared in the body:
<div id="tooltip-1" class="mdc-tooltip" role="tooltip" aria-hidden="true">
<div class="mdc-tooltip__surface mdc-tooltip__surface-animation">
TestyMcTest
</div>
</div>
...and then the tooltip is attached to another element in the same body:
<div aria-describedby="tooltip-1" class="mdc-card in-card-margins">
<div>The status of these data updates can be observed in the blabla</div>
</div>
The tooltip div is made invisible (which is expected behaviour), however, nothing shows up when mousing over the linked object (in this instance a card, but I tried with multiple types of containers and I could not get anything to work).
Thank you for any help.
Currently building a web page in Vue, and have hit a bit of an issue parsing and then rendering the <slot>'s child components.
I need to be able to take the slot, parse the components into an array, and then render those components for the end-user.
What I've Tried
I've tried many variations of things, most starting with this: this.$slots.default
This is the last version I tried
let slotComponents = [];
this.$slots.default.forEach(vNode => {
slotComponents.push(vNode);
});
But I've also tried selecting the elements within the vNode and using things like $childeren to select the components. No luck so far.
Potential Issues
The cause could be any number of things, but here is what I thought was going on (in order)
I'm not getting the components into the array properly
I'm not rendering them properly or missed something about how they render
Vue isn't supposed to do this?
Edit - Context
Seems like it would be easier if I gave you the full context of my specific problem.
Goal
To create a dynamic tab component. Should look like this.
// Example of component use
<tab-container>
<tab>
<!-- Tab Content -->
</tab>
<tab>
<!-- Tab Content -->
</tab>
<tab>
<!-- Tab Content -->
</tab>
<trash>
<!-- This one won't show up -->
</trash>
</tab-container>
In order to parse through this content, I needed to get the slot data out.
// Inside the <tabs-container> component
computed: {
tabs: function() {
let tabs = []
this.$slots.default.forEach(vNode => {
tabs.push(vNode);
});
return tabs;
}
}
// Inside the <tabs-container> template
<div>
{{tabs[currentTab]}}
</div>
You shouldn't be using template and computed properties if you want to programmatically render out <tab> inside <tab-container>. {{}} in templates are designed to perform basic operations of JS. The most appropriate way will be to use render function.
Render functions - Vue docs
Here is a working example that takes in few tabs components and shows only active tab component: https://jsfiddle.net/ajitid/eywraw8t/403667/
I am reworking an old app of mine and I am having issues with dom manipulation and basic selections within a vue instance.
Essentially I have information in a database that I load in via ajax.
Each record in the db has 2 sections. The header tab(title, time, date etc) and the body of the record(notes, ideas, etc)
When loaded, the header shows normally to the user but if they want to see what that note contains, they have to click on the header for the bottom to appear.
consider the following html:
<vuejs for loop>
<div v-bind:id='item._id' class="tabW" v-on:click="blueTabClick" >
<div class="blueTabMainColor">
<!-- header stuff here -->
</div>
<div class="notesOpenedW">
<!-- interior informaton here, HIDDEN BY CSS -->
</div>
</div>
<vuejs for loop ender>
This HTML is essentially inside a Vue for/loop directive, and generates however many "tabs(tabW)" as needed based on how much info I have in the DB
All I want the user to do is to be able to click whichever tab(tabW) they want information on, and for the notes show underneath(notesOpenedW).
I stripped my entire app and js and tried to keep it as simple a test as possible and even with the below, I still can't get anything.
here is my JS(JQ):
$(document).ready(function(evt){
$(".blueTabMainColor").click(function(){
$(this).next(".notesOpenedW").fadeToggle();
});
});
With this basic code, when I put it inside a Vue instance, via:
methods: {
blueTabClick: function (evt) {
evt.preventDefault();
$(".blueTabMainColor").click(function(){
//alert("you clicked me");
$(this).next(".notesOpenedW").fadeToggle();
});
}
}
It doesn't work, but if I take it out of the Vue instance, it works just fine.
how can I get this to work? or am I going about it the wrong way?
Vue will not cohabit happily with JQuery. You're $(this) will not work because you're not even in the document at that point, you're in pure js, virtual DOM, another universe. Then, if it did, the event listener you call may not exist. You will need to fundamentally transition this code to Vue if you want it to work, I fear.
You can achieve this by setting a ref on "notesOpenedW".
https://v2.vuejs.org/v2/api/#ref
I would strongly recommend to wrap this behaviour in a dedicated component
That would have the following content :
<div class="tabW" v-on:click="blueTabClick" >
<div class="blueTabMainColor">
<!-- header stuff here -->
</div>
<div class="notesOpenedW" ref="notesToggleDiv">
<!-- interior informaton here, HIDDEN BY CSS -->
</div>
</div>
And the method :
methods: {
blueTabClick: function () {
$(this.$refs.notesToggleDiv).fadeToggle();
}
}
Be aware that when using Vue, manipulating directly the dom is usually a bad idea.
As i showed you, it is possible to use jQuery with Vue if you absolutely need it (or cannot afford to rework more deeply your application).
Edit : Just found this article that i think would help you a lot :
https://www.smashingmagazine.com/2018/02/jquery-vue-javascript/?utm_campaign=Revue%20newsletter&utm_medium=Newsletter&utm_source=Vue.js%20Developers
Considering that you have this situation:
<div class="site-frame">
<div class="auxiliary"></div>
<div class="main" ui-view>
<div class="componentA">
</div>
<div class="componentB" move-to=".auxiliary" breakpoints="1,2,3,4">
<!-- CONTENTS OF componentB -->
</div>
<div class="componentC">
</div>
</div>
</div>
The element .componentB has a directive called move-to which does simply move the contents of this element, collecting them with a jQuery children wildcard selector (like var contents = $('.componentB').find('> *');), when any of those breakpoints, defined on the breakpoints directive (those numbers are the indexes of one array, that keeps the breakpoints measurements) are currently occurring.
When some breakpoint of that directive is active, the DOM change to this:
<div class="site-frame">
<div class="auxiliary">
<!-- CONTENTS OF componentB -->
</div>
<div class="main" ui-view>
<div class="componentA">
</div>
<div class="componentB" move-to=".auxiliary" breakpoints="1,2,3,4">
</div>
<div class="componentC">
</div>
</div>
</div>
This is a responsive mechanism is being used in a static version of one website that I'm dealing with. What I need is to know if there's flaws with the scope inheritance, event broadcasting, state controller, which is being related to the div.main, which is itself a sibling of the div.auxiliary.
Wondering on how angular does it's job, I suppose that the JS logic layer keeps relations between the DOM element being referenced on some scope. Mainly on directive scopes, using link() functions, which are post-link() by nature so DOM manipulation is more secure, because the link was already made.
Keep in mind that I keep references of the .componentB contents inside the link() function, while listening to the scope $destroy event, to clear things up, avoiding memory leaks. Also because this system listens to $window.on('resize') to find out which is the current breakpoint and, during the navigation, it's possible that the contents of my example can be moved between their original container and the auxiliary one.
So, the question is: if I move one element through DOM, even outside it's ui-view parent, it's safe to keep counting on variable updates on data-bindings, inheritances, and so on?
I'm asking this before implementing because of the huge size of the application, and as always on production-wise runnings, there was no room to discuss this before...
Edit 1:
Temporarily, this CodePen has more chances: http://codepen.io/anon/pen/JXPvBE?editors=0010
The code is doing what I need, but I need to test it inside the final app.
While I was developing other tasks, the solution posted on the Edit 1 of the question pointed over a good way to deal with this.
While some doubts about the persistance of the original controller, of the parent element, were somehow a leak on this situation, which means, for example, that other updates on the models had the possibility to not be affected with the changes, until now, no problems were noticed.
Perhaps with other situations it can fail, but for now, it's working well:
http://codepen.io/anon/pen/JXPvBE?editors=0010
On the CodePen, on the beginning of the directive move-to, the compile function, at the pre-link phase, keeps an reference to the element in case, to manipulate it using it's original form, before angular process it's directives too (like ng-repeat), and so we will not be dealing with those angular comments that limit the place of it's directives:
<!-- ng-repeat: x in collection -->
<li class="repeated-element">
...
</li>
<!-- end ng-repeat: x in collection -->
It's is indeed a good way to deal with some responsive issues, were no room left for some functionalities on the SPA and you need to place them in a auxiliary container, that covers the entire page and properly places those elements that need more attention, like the native apps on mobile phones, with side panels.+
I intend to set up the following React Component
<tab-box>
<title>Example</title>
<butons>
<button id="actionID1"></button>
<button id="actionID2"></button>
<button id="actionID3"></button>
</butons>
<content>
<tabList>
<tab id="tab1" label="label1"></tab>
<tab id="tab2" label="label2"></tab>
</tabList>
</content>
</tab-box>
Where I would like to pull out each label attribute for tab to set up a nav bar on top of the actual content.
The question is, how do I pull out the attributes from nested children? Or how can I restructure the component so that I don't have this issue?
Thought1: use a global Store service, children like tab populate the Store and parents may retrieve them when being mounted
UPDATE, Thought2: make labels a prop on tab-box, but I still don't feel quite right..
There are ways to do it, but i doubt it is what you ultimately want. You want the parent to obtain information about the child? This doesn't make sense as you are trying to create "components" and not some code specific to one task.
Think of it this way. The tabs should be using tab component to display its own information. Under what circumstance would the tab know more than its parent? If so then is it even a component? Who is using who?
React is suited for one way data flows from parent to child, so it makes more sense to hold information in tabs component, and pass pieces of info to child tabs.
<tabs>
<button text={tab[0].text}/>
<tab>
write transcluded children here
</tab>
</tabs>
Even this begs the question - why are you using so many components to begin with? Why not use css classes to represent tabs container and individual tabs like a normal person?
If you insist on making a tabs component, at least keep it all in one component, something like
<tabs data={[{title:"tab-title", content: (<p>stuff</p>)}]} />
Also I absolutely do not agree with defining tabs in a model/store. common sense is UI and data should be separate.
As someone else mentioned you can use ref, but i'd reserve that for very limited purposes such as getting a field from a form.