A-Frame+Vue: can't bind "visible" attribute of a a-box - vue.js

I wanted to see how easily A-Frame and Vue can work together.
One of the example I met with a google search is this fiddle: https://jsfiddle.net/baruog/23sdtzgx/
But I didn't like the fact that, to change the properties of the a-box in the example, the functions needed to access the DOM.
Like, for example, in this function:
setBoxColor: function(color) {
document.querySelector('a-box').setAttribute('color', color)
},
So, I wondered, can I bind the attributes of the a-box and change them without accessing to the DOM?
And I changed the code as in this other fiddle: https://jsfiddle.net/fy83wr49/
that I copy below:
The HTML
<div id="vue-app">
<a-scene embedded>
<a-sky color="#000"></a-sky>
<a-entity camera look-controls wasd-controls position="0 1 3" rotation="-15 0 0"></a-entity>
<a-box v-bind:color="color_box" v-bind:opacity="op_box" v-bind:visible="v_box"></a-box>
</a-scene>
<p>Click a button to change the color of the box</p>
<div>
<button #click="setBoxColor('red')">Red</button>
<button #click="setBoxColor('blue')">Blue</button>
<button #click="setBoxColor('green')">Green</button>
<button #click="setVisibility(true)">True</button>
<button #click="setVisibility(false)">Flase</button>
<button #click="changeOpacity()">Opacity</button>
</div>
</div>
And the JS
Vue.config.ignoredElements = ['a-scene', 'a-sky'];
var colorButtons = new Vue({
el: '#vue-app',
data: {
color_box: "magenta",
v_box: false,
op_box: 0.5,
},
methods: {
setBoxColor: function(color) {
this.color_box = color;
},
setVisibility: function(isVisible) {
this.v_box = isVisible;
//document.querySelector('a-box').setAttribute('visible', isVisible)
},
changeOpacity: function() {
this.op_box += 0.1;
if (this.op_box > 1.0) this.op_box = 0.0;
}
}
})
What happens is that both the "color" binding and the "opacity" binding work properly, but the "visible" binding doesn't.
Initially I thought that maybe the bindings were supposed to work only with standard html attributes, and it working with the "color" attribute of the a-box was just a coincidence caused by a name collision.
But then I checked the html attributes list here https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes and "opacity" isn't listed, so I had to abandon that explanation.
Does anyone have an idea of the reasons that make only the first two bindings work?

According to a-frame docs: https://aframe.io/docs/0.8.0/components/visible.html#updating-visible - it seems that visible is a special attribute which needs to be updated using object3D or setAttribute on the element. Actually they call it 'component' visible - not an attribute, while opacity and color seems to be just an attributes. Simple vue binding seems to not work to 'visible component'.

Related

Vue class components dynamically add component depending on answer from backend

So from the backend I get a array of objects that look kind of like this
ItemsToAdd
{
Page: MemberPage
Feature: Search
Text: "Something to explain said feature"
}
So i match these values to enums in the frontend and then on for example the memberpage i do this check
private get itemsForPageFeatures(): ItemsToAdd[] {
return this.items.filter(
(f) =>
f.page== Pages.MemberPage &&
f.feature != null
);
}
What we get from the backend will change a lot over time and is only the same for weeks at most. So I would like to avoid to have to add the components in the template as it will become dead code fast and will become a huge thing to have to just go around and delete dead code. So preferably i would like to add it using a function and then for example for the search feature i would have a ref on the parent like
<SearchBox :ref="Features.Search" />
and in code just add elements where the ItemsToAdd objects Feature property match the ref
is this possible in Vue? things like appendChild and so on doesn't work in Vue but that is the closest thing i can think of to kind of what I want. This function would basically just loop through the itemsForPageFeatures and add the features belonging to the page it is run on.
For another example how the template looks
<template>
<div class="container-fluid mt-3">
<div
class="d-flex flex-row justify-content-between flex-wrap align-items-center"
>
<div class="d-align-self-end">
<SearchBox :ref="Features.Search" />
</div>
</div>
<MessagesFilter
:ref="Features.MessagesFilter"
/>
<DataChart
:ref="Features.DataChart"
/>
So say we got an answer from backend where it contains an object that has a feature property DataChart and another one with Search so now i would want components to be added under the DataChart component and the SearchBox component but not the messagesFilter one as we didnt get that from the backend. But then next week we change in backend so we no longer want to display the Search feature component under searchbox. so we only get the object with DataChart so then it should only render the DataChart one. So the solution would have to work without having to make changes to the frontend everytime we change what we want to display as the backend will only be database configs that dont require releases.
Closest i can come up with is this function that does not work for Vue as appendChild doesnt work there but to help with kind of what i imagine. So the component to be generated is known and will always be the same type of component. It is where it is to be placed that is the dynamic part.
private showTextBoxes() {
this.itemsForPageFeatures.forEach((element) => {
let el = this.$createElement(NewMinorFeatureTextBox, {
props: {
item: element,
},
});
var ref = `${element.feature}`
this.$refs.ref.appendChild(el);
});
}
You can use dynamic components for it. use it like this:
<component v-for="item in itemsForPageFeatures" :is="getComponent(item.Feature)" :key="item.Feature"/>
also inside your script:
export default {
data() {
return {
items: [
{
Page: "MemberPage",
Feature: "Search",
Text: "Something to explain said feature"
}
]
};
},
computed: {
itemsForPageFeatures() {
return this.items.filter(
f =>
f.Page === "MemberPage" &&
f.Feature != null
);
}
},
methods: {
getComponent(feature) {
switch (feature) {
case "Search":
return "search-box";
default:
return "";
}
}
}
};

Keep props updated with programatically generated components

Imagine an empty virtual bulletin board where an unknown number of virtual notes will be placed. The board is the parent component and the note is the child.
When I click the board a new note appears on the board. When I move the mouse the note should follow the mouse cursor (weird UI I know, but I'm simplifying for the sake of this post).
I'm generating a new note by instancing it and then adding it to the dom like this:
let NoteClass = Vue.extend(Note);
let note = new NoteClass({
propsData: { x: this.clientX, y: this.clientY },
});
note.$mount();
this.$refs.board.appendChild(note.$el);
Notice the mouse x/y is passed to the note via props. This causes the note to appear at the position of the mouse cursor when I click. Great.
However, once the Note is instanced it no longer updates the x/y props. The Note does not continuously read the position of the mouse cursor from its parent.
Here's the full code:
https://codesandbox.io/s/boring-wiles-pru1y?file=/src/App.vue
For comparison, check out this version where the Note is NOT generated in code. A single note is placed the typical way. It follows the cursor just fine:
https://codesandbox.io/s/proud-tree-xnthc?file=/src/App.vue
I found a way better solution thanks to Michal Levý's comment -- the data driven way:
<template>
<div ref="board" class="board" #click.self="onClick" #mousemove.prevent="drag">
<Note v-for="(note, key) in notes" :key="key" :x="clientX" :y="clientY"></Note>
</div>
</template>
<script>
import Note from '#/components/Note.vue';
export default {
name: 'Board',
data() {
return {
clientX: 0,
clientY: 0,
notes: []
}
},
components: {
Note
},
methods: {
onClick() {
this.notes.push({});
},
drag(event) {
this.clientX = event.clientX;
this.clientY = event.clientY;
}
}
}
</script>

In Vue, how to get the content of a textarea?

I want to keep the value of a variable identical with the content of a textarea.
I don't want to use v-bind or v-model, because I have already bound the textarea with another value.
This is a notebook app, and the textarea is used to display the content of a note, so it has been bound using v-bind with a note object, like
<textarea cols="30" rows="3" v-bind:value="note"></textarea>
Now, I want to add the "edit note" functionality. So when the content of the textarea changes, I want to store its value into a variable, and when the "submit" button is clicked, I pass the value of the variable, which contains the new content of the note, to backend to update the note.
My question is, how to store the textarea's content into the variable after each time the content changes?
I think I cannot use v-model because this way the note will be changed right after the content of the textarea is modified (though not sent to backend), but this is not what I want. What I want is the note to be changed only after the "submit" button is clicked. Thus, I cannot use v-model
Should I use v-on:change? If so, how to get the content of the textarea?
Like,
<textarea v-on:change="updateTheVariable(I need to get the content of the textarea here)"> ... </textarea>
methods: {
updateTheVariable(content of the textarea) {
this.variable = content of the textarea
}
}
Thanks
I'm assuming this thing only shows up when you click some kind of edit button which is why you don't want to alter note so try something like this instead
<button type="button" v-if="!editMode" #click="editNote">Edit</button>
<form v-if="editMode" #submit="handleSubmit">
<fieldset :disabled="saving">
<textarea v-model="editingNote"></textarea>
<button type="submit">Edit</button>
</fieldset>
</form>
export default {
data: () => ({
note: 'whatever', // maybe it's a prop, maybe assigned later, doesn't matter
editMode: false,
editingNote: null, // this will be used to bind the edited value
saving: false
}),
methods: {
editNote () {
this.editingNote = this.note
this.editMode = true
this.saving = false
},
async handleSubmit () {
this.saving = true // disables form inputs and buttons
await axios.post('/notes/update', { note: this.editingNote}) // just an example
this.note = this.editingNote // or maybe use data from the response ¯\_(ツ)_/¯
// or if it's a prop, this.$emit('updated', this.editingNote)
this.editMode = false
}
}
}
As #Phil indicated in a deleted post, the right way to do it is
<textarea #input="updateTheVariable($event.target.value)"></textarea>
.
.
.
methods:{
updateTheVariable(value){
this.variable = value
}
}

How to change contents of a virtual dom element in Mithril?

How do I access a virtual dom element to change its contents using Mithril? I am new to Mithril and still trying to figure things out. For example, I want to access the third div with id "three" and change it's contents to "Blue Jays" without touching any of the other div's.
Thanks.
<div id='main'>
<div id='one'>Yankees</div><br>
<div id='two'>Red Sox</div><br>
<div id='three'>Orioles</div>
</div>
In mithril, like in react/vue/angular, you dont act on the actual DOM directly. Instead, you define the outcome that you want, so for example, to render the DOM tree that you posted you would do something like this:
var my_view = {
view: vnode => m('div#main', [
m('div#one', 'Yankees'),
m('div#two', 'Red Sox'),
m('div#three', 'Orioles')
])
}
m.mount(root, my_view)
<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/2.0.4/mithril.js"></script>
<div id="root"></div>
the m(...) functions inside the array have a string as their second argument, that makes the output static, but we can change that to a variable:
var my_view = {
oninit: vnode => vnode.state.fave_team = 'Orioles',
view: vnode => m('div#main', [
m('div#one', 'Yankees'),
m('div#two', 'Red Sox'),
m('div#three', vnode.state.fave_team)
])
}
m.mount(root, my_view)
<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/2.0.4/mithril.js"></script>
<div id="root">
</div>
In this case I used the state property of the vnode argument, but you can also use a third party state manager like flux or any other.
Now that we have it as a variable, it will show the current value on every call m.redraw(), most of the times we dont have to do this call ourselves, for example:
var my_view = {
oninit: vnode => {
vnode.state.fave_team = 'Orioles'
},
view: vnode => m('div#main', [
m('div#one', 'Yankees'),
m('div#two', 'Red Sox'),
m('div#three', vnode.state.fave_team),
m('button', { onclick: () => vnode.state.fave_team = 'Dodgers' }, 'Change')
])
}
m.mount(root, my_view)
<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/2.0.4/mithril.js"></script>
<div id="root"></div>
And thats it, any dynamic content in your DOM elements you set it as a variable/property in an object.
One of the beautiful things about mithril is that it doesnt force you to do things one specific way, so if you really want to work on the actual DOM node, there are lifecycle events that you can attach to any virtual node ("vnode")
You can easily capture the HTMLElement (i.e., HTMLInputElement) with the Mithril Lifecycle event of oncreate(). This is an actual example from my code (in TypeScript) where I need to hook up a few event listneres after the canvas element was created and its underlying DOM is available to me at "raw" HTML level. Once you get a hold of dom, then I manipulate that element directly. Many people think that why not use oninit(), but oninit() is before the generation of dom, so you will not get the element back at that stage.
Now, if you just do that, you will likely be posting another question - "Why the browser views not updating?" And that's because you do have to manually do a m.redraw() in your event handlers. Otherwise Mithril would not know when the view diffs to be computed.
const canvas = m(`.row[tabIndex=${my.tabIndex}]`, {
oncreate: (element: VnodeDOM<any, any>) => {
const dom = element.dom;
dom.addEventListener("wheel", my.eventWheel, false);
dom.addEventListener("keydown", my.eventKeyDown, false);
}
},

How to position a dijit.menu relative to its trigger?

I've got a couple menus like this:
// Contextual Menu
// triggers
<div id="contextMenuTrigger0">0</div>
<div id="contextMenuTrigger1">1</div>
// menu
<div dojoType="dijit.Menu"
targetNodeIds="contextMenuTrigger0, contextMenuTrigger1"
leftClicktoOpen="true" style="display:none">
<div dojoType="dijit.MenuItem" class="first">Item One</div>
<div dojoType="dijit.MenuItem">Item Two</div>
<div dojoType="dijit.MenuItem">Item Three</div>
<div dojoType="dijit.MenuItem">Item Four is really, really long item.</div>
</div>
and this:
// Tools Menu
// trigger
<div id="toolsButton">Tools</div>
// menu
<div dojoType="dijit.Menu" class="toolsMenu"
targetNodeIds="toolsButton"
leftClicktoOpen="true" style="display:none">
<div dojoType="dijit.MenuItem" class="first">Item One</div>
<div dojoType="dijit.MenuItem">Item Two</div>
<div dojoType="dijit.MenuItem">Item Three</div>
<div dojoType="dijit.MenuItem">Item Four</div>
</div>
Right now, when the menu opens, it appears under the mouse. I want it to appear in a specific position relative to the trigger*. I found the startup and onOpen events and tried writing a function that sets the style of the menu's domNode in there, but they didn't seem to take effect.
Also, I didn't see a way of finding out which node was the trigger in the context case where there are multiple ones.
I saw this & this, but wasn't able to get much further with 'em.
* FWIW, I want them positioned so that the top-left corner of the menu is aligned with the top-right corner of the context triggers, and with the bottom-left corner of the Tools menu.
I found the following css override works nicely, if you just want a relative difference in the automated positioning:
.dijitMenuPopup {
margin-left: -25px !important;
margin-top: 15px !important;
}
It turns out that dojo.popup.open (which I guess Menu inherits from) has a parameter (orient) that you can use to orient a menu relative to a node. I wound up defining a custom trigger class that knows how to take advantage of that. (I also created sub-classes for other menu-types that have different orientations, but I'll leave those out for clarity's sake.)
UPDATE: according to this page, the variable substitution method I was using in the templateString isn't recommended. Instead, you're supposed to create an attributeMap, which I've done below.
http://docs.dojocampus.org/quickstart/writingWidgets
// Define a basic MenuTrigger
dojo.declare("my.MenuTrigger", [dijit._Widget, dijit._Templated], {
// summary:
// A button that shows a popup.
// Supply label and popup as parameter when instantiating this widget.
label: null,
orient: {'BL': 'TL', 'BR': 'TR'}, // see http://api.dojotoolkit.org/jsdoc/1.3.2/dijit.popup.__OpenArgs (orient)
templateString: "<a href='#' class='button enabled' dojoAttachEvent='onclick: openPopup' onClick='return false;' ><span dojoAttachPoint='labelNode'></span></a>",
disabled: false,
attributeMap: {
label: {
node: "labelNode",
type: "innerHTML"
}
},
openPopup: function(){
if (this.disabled) return;
var self = this;
dijit.popup.open({
popup: this.popup,
parent: this,
around: this.domNode,
orient: this.orient,
onCancel: function(){
console.log(self.id + ": cancel of child");
},
onExecute: function(){
console.log(self.id + ": execute of child");
dijit.popup.close(self.popup);
self.open = false;
}
});
this.open = true;
},
closePopup: function(){
if(this.open){
console.log(this.id + ": close popup due to blur");
dijit.popup.close(this.popup);
this.open = false;
}
},
toggleDisabled: function() {
this.disabled = !this.disabled
dojo.toggleClass(this.domNode, 'buttonDisabled');
dojo.toggleClass(this.domNode, 'enabled');
dojo.attr(this.domNode, 'disabled', this.disabled);
},
_onBlur: function(){
// summary:
// This is called from focus manager and when we get the signal we
// need to close the drop down
// (note: I don't fully understand where this comes from
// I couldn't find docs. Got the code from this example:
// http://archive.dojotoolkit.org/nightly/dojotoolkit/dijit/tests/_base/test_popup.html
this.closePopup();
}
});
// create some menus & triggers and put them on the page
dojo.addOnLoad(function(){
// MENU
cMenu = new dijit.Menu();
cMenu.addChild(new dijit.MenuItem({ label: "First Item" }));
cMenu.addChild(new dijit.MenuItem({ label: "Second Item" }));
cMenu.addChild(new dijit.MenuItem({ label: "Third Item" }));
cMenu.addChild(new dijit.MenuItem({ label: "Fourth Item is truly a really, really, really long item" }));
// TRIGGER
cTrigger = new my.MenuTrigger({
id: "cTrigger",
popup: cMenu
}).placeAt(dojo.body());
cTrigger = new my.MenuTrigger({
id: "cTrigger2",
popup: cMenu
}).placeAt(dojo.byId('contextTriggerContainer2'));
});
As I can see from dijit.Menu source code the feature you want isn't supported out of box.
What I can think of is declaring a new widget inheriting from dijit.Menu and override bindDomNode method. It binds _openMyself handler to onClick event like this:
dojo.connect(cn, (this.leftClickToOpen)?"onclick":"oncontextmenu", this, "_openMyself")
_openMyself handler takes coords from the event object that comes in as an argument.
So the idea is to pass a fabricated event object with the desired coords.
dojo.connect(cn, (this.leftClickToOpen)?"onclick":"oncontextmenu", this, function(){
var e = { target: desiredTarget, pageX: desiredX, pageY: desiredY };
this._openMyself(e);
});