Can there be more than one DOM object in the Cycle.js drivers? - cyclejs

All the Cycle.js examples I've found so far, use a single DOM object, named "DOM", in the drivers argument to run(main, drivers). Is it possible to have more than one object, e.g., one named "DOM1" and another "DOM2"? The purpose of this would be to control two separate dynamic DOM areas within a single HTML page, in order to keep a third DOM area statically defined in index.html, and sandwiched between DOM1 and DOM2.
As a side question, the examples I've seen typically target an HTML div with an id of #app or #main-container, and then the sink is defined with a #cycle/dom div function, thus creating AFAICT an unnecessary div within a div. I haven't found a clear explanation or reference of how the virtual nodes are supposed to be defined. Say that DOM2 above targets an HTML form element and that is supposed to contain two input elements. Does it have to start with a div as in all the examples, or can the inputs be defined directly in the .map call, and if so, how?

There is nothing preventing you from having a DOM1 and DOM2 sinks in your app. bloodyKnuckles' example illustrate that perfectly https://esnextb.in/?gist=b54baa4131974b7f12d190fb63be8aeb
That being said, I am not sure I really see the point of doing this. If it's a matter of performance, I don't think you will gain much in splitting the rendering of your app into two DOMDrivers. The virtual DOM lib (in cycle's case snabbdom) is tailored to recognize pieces of DOM that haven't change from those which have and only updates the latter.
If it's a matter of responsibilities (those 2 pieces of DOM have very different purposes), then I would rather create two different cycle apps that both render in different parts of the DOM. (and then call run twice in your main file)
function app1(sources) {
return {
DOM: xs.of(div("hello from app1"))
}
}
function app2(sources) {
return {
DOM: xs.of(div("hello from app2"))
}
}
run(app1, {
DOM: makeDOMDriver("#app1")
})
run(app2, {
DOM: makeDOMDriver("#app2")
})
This way you have a clear separation of the concerns of both apps.
Now to answer your question about why a piece of virtual DOM needs to be wrapped in a div. It is because a piece of virtual DOM have to have a single root element. Said otherwise: a piece of virtual DOM have to be standalone (just like an HTML document only has a single <html> element which is the root).
It is, actually, a nice constraint to have because it forces you to have standalone components. In the example you give (with an <input> field), there is absolutely no problem in returning a vDOM like this:
DOM: xs.of(input(/*...*/))
But if your component has an input and a label, then you will need to wrap it in another vNode
DOM: xs.of(div([label(/*...*/), input(/*...*/)])

Related

Stencil js: update DOM element outside the component

I'm in the process of migrating some legacy pages to web components using StencilJS, so I'm in a situation where some elements are already handled with StencilJS, some are not, and migrating everything will take quite some time.
In this context I need to be able to update the contents of a target div not managed with StencilJS from a StencilJS component. This div is in a totally different branch of the DOM and it's impossible to move it into the component without rethinking the entire page.
So from my component I need to be able to do something like this:
render() {
const target = document.getElementById(this.targetDiv);
if (target) {
target.innerHTML = jsxToString(this.renderDivContents()) // obviously this doesn't work
}
}
renderDivContents() {
return (<p>Some JSX stuff</p>)
}
So, in other words I need to compile the JSX template immediately into a string. I'm not sure how to do that with StencilJS and if it is even possible. I'm under the impression that there is a way to achieve that because it looks very similar to what we do in tests, but all the resources I find on the topic are for JSX with React and does not really help with StencilJS
If this is not the correct approach, what do you suggest? I know injecting HTML into a DOM element is not ideal, but I'm just trying to find a temporary solution to be able to release my changes gradually.
PS: I know I can also use an overlay approach (generate a div into the component and give it the same position and size than the target div), but this sounds even uglier than innerHTML. This is not the answer I expect.

Vue slot is not working in rare and unpredictable cases (potential vue bug?)

I have this weird bug with a slot that is unreliable in certain unknown cases.
Components
There are 3 hierarchical components.
The grandchild (headlessTable), which offers a slot named arrayValue.
The child (collapsableCard), which passes the slot between grandchild and parent.
The parent (orderDataCard), who decides to render a link for that slot.
Problem: Instead of rendering the link of the parent, the default slot html of the child is being rendered when new data is loaded.
Datastructure (orderDetails)
process (obj)
mark (string)
common (obj)
additionalArguments (array)
category (string)
type (string)
name (string)
value (string)
salesOrganisation (obj)
invoices (array)
invoiceAgreementId (string)
paymentType (string)
Reproduction
Stackblitz or Codesandbox
Please look at the field additionalArguments, it contains a link.
Press ALT+M to simulate fetching new data. Now, instead of rendering a link, the default slot html for that named slot is rendered instead.
You can press ALT+J to load the original data, but this time there's no link.
Initial data (ALT+J)
Loaded data (ALT+M)
Type
Equal value
mark
str
false
common
common
obj
true
salesOrganisation
salesOrganisation
obj
true
invoices (empty)
invoices
arr
false
How 2 resolve
if you uncomment line 68 in app.js (or line 73 in App.vue if you're on codesandbox), which is the field called mark
if invoices is not initially empty in app.js
if mark is removed from html in orderDataCard
if salesOrganisation is removed from html in orderDataCard
if the html in the v-for template section for invoiceItems is empty in orderDataCard
Obviously, these are not solutions.
Notes
In any case, there is no dependence or anything between any of the fields, so it's hard for me to understand why this happens and I suspect this to be a bug with vue. I already created an issue for this. However, devs won't look at the reproduction, because they think it's not minimal as #lines > 100. As soon as I delete any more meaningful lines, the bug is resolved and the removed code is not faulty, so it's very frustrating to work on this. I could still remove lines that are not meaningful, but that would make it more difficult for everyone involved to understand what data is being rendered.
Is anyone able to acknowledge the fact that this is a problem with vue and that the code is not reducible OR (I would prefer this) is anyone able to fix this?
The problem is linked to Vue handling of multiple instances of the same component. In OrderDataCard.vue you have two instances of Collapsable-Card without unique keys. In this case:
Vue uses an algorithm that minimizes element movement and tries to
patch/reuse elements of the same type in-place as much as possible.
I don't quite know how these algorithms work, and why, apparently, it reused the second instance (without a defined slot content), but, setting a unique key for these components solved the issue.
See the working code sandbox: https://codesandbox.io/s/admiring-hamilton-5ytpp?file=/src/components/OrderDataCard.vue:133-149.
Note: I couldn't trigger keyboard events in my browser, so I triggered them on button click.
This may not be the solution, but could help find it:
Objects
I noticed you are working with objects and turning them into arrays. Objects properties can be problematic to work with, because unlike arrays updated properties are not propagated. This is a problem with JavaScript, not Vue. Vue was only possible because of observers introduced, but objects are still not part of that.
You might run into problems when an object is partially updated.
I would suggest looking at Vue.set.
Old code of mine invokes it explicitly by window.Vue.set() for changes in object properties so Vue can propagate them correctly.
That is kind of a bug in Vue, but again stems from JavaScript itself.
Computed arrays
I'm not entirely sure but the computed arrays don't save the above issue with working with objects.
I would go the safe route and use Vue.set() when updating objects and object properties. You can still use the computed arrays then.
Otherwise the obvious: Make real arrays out of the objects instead of working with objects half the time.
this.process
Is there a good reason you are using this.process explicitly instead of the component's props? Or is that a component from a library?
Slots
Have you tried the exact same code but without using the collapsable-card? Just output the link itself? It might point to slot problems in the collapsable-card component. Maybe also partially because of the objects thing from above.

Default-expand-all doesn't work for q-tree? Vue.Js

I have to code a web application and the most important element is the q-tree. I'm already able to load and show data (passing an array called list), but I want that all nodes are expanded.
The vue.js examples of the official documentation show that you're be able to do this with the 'default-expand-all' attribute but this isn't working for me.
It only shows me the root node with an arrow, where I have to expand the children nodes manually.
<q-tree
:nodes="list"
:selected.sync="selected"
#update:selected="onSelectionChangedNode"
node-key="NodeNr"
label-key="NodeTxt"
default-expand-all
></q-tree>
Taking a cue from the accepted answer, I realised that the dom has already been created with the tree component on first render.
In my use case, I want to update the Tree when data comes back from the server.
So, I had to force it to re-render with the expanded functionality using:
this.$nextTick(function () {
this.$refs.nodes.expandAll();
})
The nextTick function will update the dom in the next window of execution, by which time the nodes will get expanded by calling the expandAll function.
And NB: For those confused by the astericks on the ref attribute or how to add it to the component, here goes:
<q-tree :nodes="list"
:selected.sync="selected"
#update:selected="onSelectionChangedNode"
node-key="NodeNr"
label-key="NodeTxt"
ref="nodes"
>
Solved my problem as following:
I have added a ref attribute to the QTree DOM Element which makes it possible to access predefined methods of QTree API.
<q-tree
:nodes="list"
:selected.sync="selected"
#update:selected="onSelectionChangedNode"
node-key="NodeNr"
label-key="NodeTxt"
**ref="nodes"**
>
The function I have been using is expandAll().
updated() {
this.$refs.nodes.expandAll();
}
The most important thing for me was, I had to find out which lifecycle hook was the right one for me. The update() hook was the one I was looking for.
The reason:
Called after a data change causes the virtual DOM to be re-rendered and
patched.
The component’s DOM will have been updated when this hook is called, so you
can perform DOM-dependent operations here.
The default-expand-all is only applied on the first rendering of that Component.
So if your Component renders when the nodes aren't assigned they wont expand if assigned afterwards.
https://v1.quasar-framework.org/vue-components/tree
You have to work with scoped slots and an expanded attribute if you dont have the nodes on first rendering.

Initialize dynamic Component in Code using Vue.js

I am currently developing a web application that is used to display elements for events on a map provided by HERE Maps. I am using Vue.
I have some components, but the relevant component is the component HereMaps.vue which initializes the map using the HERE Maps Api.
The HERE Maps Api provides the possibility to place so called InfoBubbles on the map showing additional information. These InfoBubbles can be provided some HTML-code in order to customize their appearance.
Please refer to the documentation for additional information
Following the documentation the code looks something like this:
let bubble = new H.ui.InfoBubble(marker.getPosition(), {
content: "<div class='someClass'>Some Content</div>"
});
this.ui.addBubble(bubble)
This is happening after mount in the "mounted" method from Vue in the "HereMaps" component.
The Bubbles are added in a "closed" (hidden) form and dynamically "opened" to reveal their content when the corresponding marker icon on the map is clicked. Therefore the HTML-code is present on the DOM after the component is mounted and is not removed at a later stage.
Now instead of supplying custom code within each bubble added to the UI i want to just add a component like this:
let bubble = new H.ui.InfoBubble(marker.getPosition(), {
content: "<myDynamicComponent></myDynamicComponent>"
});
this.ui.addBubble(bubble)
It does not matter to me wether the component is initialized using props or if it is conditionally rendered depending on the state of a global variable. I just want to be able to use the "myDynamicComponent" in order to customize the appearance in a different file. Otherwise the design process gets very messy.
As far as i know this is not possible or at least i was not able to get it work. This is probably due to the fact that the "myDynamicComponent" is not used within the "template" of the "HereMaps" component und thus Vue does not know that it needs to render something here after the directive is added to the DOM in the "mounted" method.
This is what the InfoBubble looks using normal HTML as an argument:
This is what the InfoBubble looks using the component as an argument:
It appears to just be empty. No content of the "myDynamicComponent" is shown.
Does anyone have any idea how i could solve this problem.
Thank You.
Answer is a bit complicated and I bet you wouldn't like it:)
content param can accept String or Node value. So you can make new Vue with rendered your component and pass root element as content param.
BTW, Vue does not work as you think, <myDynamicComponent></myDynamicComponent> bindings, etc exists in HTML only in compile time. After that all custom elements(components) are compiled to render functions. So you can't use your components in that way.
Give us fiddle with your problem, so we can provide working example:)

Can Vue-Router handle clicks from normal anchors rather than router-link?

I have a scenario where there are two major components on a page; a frame-like component that contains common functionality for many applications (including a bookmark/tab bar) and my actual application code.
Since the frame doesn't actually own the page that it's included on, it seems like it would be incorrect for it to define any routes, however the current page may define their own routes that may match one of those links. In that case, I'd like vue-router to handle those anchor clicks and navigate appropriately rather than doing a full page reload.
Here's a simplified template of what this looks like:
Frame (an external dependency for my app):
<Frame>
<TabStrip>
</TabStrip>
<slot></slot>
<Frame>
App1:
<Frame>
<App>You're looking at: {{ pageId }}!</App>
</Frame>
So when any of the app1 domain links are clicked from that tab strip, I want my route definitions in app1 to pick that up rather than it causing a page load. Since that component is owned by the frame, I don't have access to write <router-link> since links to many different apps may co-exist there.
Any thoughts?
Whoo, this is an old one! However, since this question was high in my search results when I was researching this problem, I figured I should answer it.
My use-case was similar to the one in the comments: I needed to capture normal <a> links within rendered v-html and parse them through the router (the app is rendering Markdown with a light modification that generates internal links in some cases).
Things to note about my solution:
I'm using Vue3, not Vue2; the biggest difference is that this is the new Vue3 composition-style single page component syntax, but it should be easy to backport to Vue2, if necessary, because the actual things it's doing are standard Vue.
I stripped out the markdown logic, because it doesn't have anything to do with this question.
Note the code comment! You will very likely need to design your own conditional logic for how to identify links that need to be routed vs. other links (e.g. if the application in the original question has same-origin links that aren't handled by the Vue app, then copy/pasting my solution as-is won't work).
<script setup>
import { useRouter } from "vue-router"
const router = useRouter()
const props = defineProps({
source: {
type: String,
required: true,
},
})
function handleRouteLink(event) {
const target = event.target
// IMPORTANT! This is where you need to make a decision that's appropriate
// for your application. In my case, all links using the same origin are
// guaranteed to be internal, so I simply use duck-typing for the
// properties I need and compare the origins. Logic is inverted because I
// prefer to exit early rather than nest all logic in a conditional (pure
// style choice; works fine either way, and a non-inverted conditional is
// arguably easier to read).
if (!target.pathname || !target.origin || target.origin != window.location.origin) {
return
}
// We've determined this is a link that should be routed, so cancel
// the event and push it onto the router!
event.preventDefault()
event.stopPropagation()
router.push(target.pathname)
}
</script>
<template>
<div v-html="source" #click="handleRouteLink"></div>
</template>