I have 2 lit-element components. The parent content is :
<s-sortablelist>
${this.renderChildren()}
</s-sortablelist>
The renderChildren function is looping over an array property children = [1, 2] and creating <div> elements. The rendered page might be something like :
<s-sortablelist>
<div id="1"></div>
<div id="2"></div>
</s-sortablelist>
The sortablelist component allows the user to reorder the <div> tags using drag and drop. After dnd, the rendered layout might become (2 and 1 are reverted) :
<s-sortablelist>
<div id="2"></div>
<div id="1"></div>
</s-sortablelist>
After the user changed the order, if I change the property children=[3,4], the result is different from my expectations :
I'm expecting to see a new list with the children 3,4
I'm seeing a list with 3,4, and some other elements (1, 2) depending on the dnd operations I made before
So my question is how is it supposed to work ?
If the children array changes, because it is a property, the parent component will render.
I'm expecting also the sotablelist component to be rerendered, but why would I have extra children from a previous render?
You can't mutate the DOM under control of lit-html this much. lit-html places comment nodes into the DOM to keep track of where the dynamic template parts are, and by moving elements around you're breaking the bookkeeping.
The right way to do this is to not move nodes in the drag and drop operation, but right before you would have actually changed the DOM, instead change the data that rendered the DOM. Then lit-html can render the list on the new order, but keep all the comment node and other internal data in sync.
Related
I have a translation system where a user can click on a word to get more information: translation, frequency, and so forth in a popover (using Floating UI). A single page can have up to 500 words or so. The very basic Vue code is as follows:
// parent
<template>
<div class='parser-wrap' v-touch:drag="onDrag">
<template v-for="(word, wid) in sentence">
<word-component
:id="childId(wid)"
:isHovering="isHovering(childId(wid))"
:word="word"></word-component>
</template>
</div>
</template>
//word-component (child)
<template>
<span v-html="output(word)"
#touchstart="doTouchStart"> // move this to parent ?
</span>
</template>
Now there are a couple of issues and approaches. For v-touch:drag using vue3-touch-events there is no option but to put the drag event in the parent rather than the child <word-component> (as there is no other way to detect touch enter).
My question is: what is the best place to put the listener for #touchstart? Putting in the child is the easiest approach but it means 500 listeners - does this impact performance?
On the other hand, if I put in the parent how do I detect which child <word-component> has been touched? At present, I'm using document.elementFromPoint getting the id from the child, then passing whether this matches the childId, and passing the isHovering prop back to the child. This seems janky to say the least :-)
In short, what is the most efficient way to have a touchevent on the parent, but communicate to the child 'you have been touched'. Or is it fine to have hundreds of listeners by putting them into the <word-component>.
(Note: words are not unique so I cannot use a word id, and going to the next page involves grabbing new sentences. In reality, the childId is a combination of 'pagecount-sentencecount-wordcount' but to simplify the above code I have just used childId(w) for illustration purposes).
how would we solve having two nested v-for loops (elements), of which one is solely for getting class attributes and shouldn't display its div element? because it is of course displaying its element in each iteration and causing a break in the layout and displaying duplicated data because of those iterations
DESIRED OUTCOME:
I need to be able to use values from an array, for the column width of the rendered values of a loop, as interpolation in the class attribute along with their corresponding rendered values.
live example: https://stackblitz.com/edit/vitejs-vite-kkzp2g?file=src/components/HelloNestedLoop.vue
Use a <template> tag instead of <div> for the outer element:
<template v-for="...">
<div v-for="...">
</div>
</template>
I am using a knockout foreach (more specifically, template: { foreach: items }) binding to display a list of elements.
I then proceed to take the following actions:
Swap the first and second elements of the observable array. I see the changes reflected on screen, as expected.
Repeat the previous action to revert to the initial state. Again, this works as expected.
Now, swap the first and second DOM elements. I see the changes reflected on screen, as expected.
Repeat the previous action to revert to the initial state. Again, this works as expected.
Even though we have manually tampered with the DOM, we have reverted to exactly the initial state, without invoking knockout during the DOM tampering. This means the state is restored to the last time knockout was aware of it, so it should look to knockout as if nothing ever changed to begin with.
However, if I perform the first action again, that is, swap the first two elements in the array, the changes are not reflected on screen.
Here is a jsfiddle to illustrate the problem: https://jsfiddle.net/k7u5wep9/.
I know that manually tampering with the DOM managed by knockout is a bad idea and that it can lead to undefined behaviour. This is unfortunately unavoidable in my situation due to third party code. What stumps me is that, even after reverting the manual edits to the exact initial state, knockout still does not work as expected.
My question is: what causes this behaviour?
And then, how does one work around it?
Turns out there is nothing magical happening here. The mistake I made was to only consider elements instead of all nodes. The knockout template binding keeps a record of all nodes when reordering, not just elements.
Before manually editing the DOM, the child nodes of the template binding are:
NodeList(6) [text, div, text, text, div, text].
After manually swapping the first two elements using parent.insertBefore(parent.children[1], parent.children[0]), this turns into:
NodeList(6) [text, div, div, text, text, text].
Repeating the action yields:
NodeList(6) [text, div, div, text, text, text].
Although this is identical to the initial state when only referring to elements, it is quite different when referring to all nodes.
The solution now becomes clear. One way to perform a proper manual swap is to replace
parent.insertBefore(parent.children[1], parent.children[0]);
with
let nexts = [parent.children[0].nextSibling, parent.children[1].nextSibling];
parent.insertBefore(parent.children[1], nexts[0]);
parent.insertBefore(parent.children[0], nexts[1]);
as seen in https://jsfiddle.net/k7u5wep9/2/.
Obviously more care has to be taken when there are no text nodes before/after, but the idea remains the same.
By manipulating the DOM, you have broken the binding made.
Do not manipulate directly the DOM. Knockout will not detect the changes made.
If you put a with: items around your foreach, it at least keeps working but requires double click if dom order != array order .. might get you on track atleast, maybe you can re-order the ko-array inside the dom function to keep their 'orders' in sync?
let vm = {
items: ko.observableArray(['item1', 'item2']),
reorder_array() {
vm.items([vm.items()[1], vm.items()[0]]);
},
reorder_dom() {
let parent = document.querySelector('#items');
parent.insertBefore(parent.children[1], parent.children[0]);
vm.reorder_array();
}
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div data-bind="with: items">
<div id="items" data-bind="template: { foreach: $data }">
<div data-bind="text: $data"></div>
</div>
</div>
<button data-bind="click: reorder_array">Reorder array</button>
<button data-bind="click: reorder_dom">Reorder DOM</button>
<div>
Reorder the array twice, then reorder DOM twice. This should work as expected, and end up with the initial state. Then, try to reorder the array again. It should not work. Why?
</div>
I don't know if i'm phrasing this correctly - but essentially what I'm trying basically say is :
If this item has no parent (or is the highest level) then don't display. So my thinking was to try if !$parent (if no parent). Here's what I am looking at -
<div ng-if-"!$parent">{{category.link}} </div>
(this does not work)
It is a recursive list tree (ng-repeats inside ng-repeats) and I basically don't want the top level to have the link attribute shown. I could set a boolean or something to just toggle it off on the most parent level BUT I'm wondering if it's possible to do it in this kind of way. Thanks!
This is a JavaScript/Ajax webpage (also using jQuery).
I have a nested structure I need to display. After displaying a top level element, users can click on it, and see levels below it (dynamically generated).
I don't want to pre-generate everything and hide it with display: none (the page is complex, I'm simplifying for this question) - I want to build the display from the javascript array that was fetched with ajax.
My question:
I have two options:
1: Create a flat array:
[ {id: xx, children: [ xx, xx, .. ] }, ....]
Then for the onclick of an element I get the id from the array, find the children, pull them up from the array and display them. (I guess I'll have to search through the array, since there are no associative arrays in javascript - or make an index.)
2: Create a nested array:
{ id: xx, children [ { id: xx, children : [....] }, {....} ] }
Then somehow bind the children in the array to the element when I display it.
I have two problems with this second approach:
A: I'm constantly copying large chunks of the array for each child when I create it. (At least I think I am. Do I need to use deep copy? Can I make a reference?)
B: I'm not sure how to bind the data to the child element. Normally I build the display using html strings with onClicks, then append the entire thing. But onClicks can only take an ID, not a copy of an array.
I did something similar recently where I had a very large nested structure (over 2000 nodes) - which I did not want to bulk append to the DOM.
What I ended up doing was taking the ajax loaded data and converting it into a nested structure...
<node id="1" title="a">
<node id="2" title="b />
<node id="3" title="b">
<node id="4" title="d" />
</node>
</node> etc...
...and storing this as a jQuery object (nodes), but never appending it to the DOM.
I could then select the immediate children of a node as I needed them relatively easily, for converting into html elements and appending to the DOM, adding data, etc...
$("#"+ID+">node", nodes).each(function() {
var node = $(this);
//do whatever...
});
I don't know if this is the most memory-efficient approach, but it certainly makes it very easy to select and append the immediate children of a node to the DOM as you need them.
I would prefer to use the second approach, for the reason it has a better structure as well as you can write less code as recursive comes into play.
You say that your not sure how to bind the child elements to the array without actually creating dom elements, well if you use <!DOCTYPE html> for html you elements can have html-* attributes allowing you to store data in an element, example:
<ul id="lists">
<li class="parent" id="root_22" data-children="{some object}">A Root Elelment</li>
</ul>
the problem with this method is that you would have to store every children of children in the root element, which more than likely is a overhead.
Another way is to bind the data using jQuery.data method, this will keep the DOM clean but will atatch data to an element.
Store arbitrary data associated with the specified element.
#see: http://api.jquery.com/jQuery.data/