Aframe Emitting Custom Events Between Components - javascript

I'm trying to create a custom event dataReady in a-frame which will emit when a few different data sources have successfully downloaded. I would like for multiple other components to listen for this event and then cause their element parents to become visible or perform other actions.
In the sample code below, the eventListener is being added before the event is emitted (tested using console.log()), but the listener function on the element is not being called when the event is emitted. I feel like all the pieces are in place, but it just isn't working correctly. Any thoughts would be greatly appreciated?
Component 1 attached to <a-camera data-controller></a-camera>
AFRAME.registerComponent('data-controller', {
init: function () {
var el = this.el;
},
update: function () {
async.eachSeries(['api-1', 'api-2'],
function (item, cb) {
console.log(item);
cb();
},
function (err) {
if (err) {
throw new Error('Error retrieving data from server');
}
console.log("emitting the event");
this.el.emit("dataReady", {value: 2}, false);
}.bind(this));
}
});
Component 2 attached to <a-entity geometry="primitive:plane" road></a-entity>
AFRAME.registerComponent('road', {
schema: {
width: {type: 'number', default: 4},
height: {type: 'number', default: 4}
},
init: function () {
var data = this.data;
var el = this.el;
// Update geometry props
el.setAttribute('geometry', 'width', data.width);
el.setAttribute('geometry', 'height', data.height);
console.log('adding an event listener');
el.addEventListener('click', function (event) {
console.log("we are ready to blast off b/c of event listened w/ detail value: " + event.detail.value);
});
}
});
index.html contents:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!--Vendor Scripts-->
<script src="./js/vendor/aframe.min.js"></script>
<script src="./js/vendor/async.min.js"></script>
<!--AFRAME Components-->
<script src="./js/components/road.js"></script>
<script src="./js/data-controller.js"></script>
</head>
<body>
<a-scene>
<a-entity geometry="primitive:plane" road></a-entity>
<a-camera data-controller></a-camera>
</a-scene>
</body>
</html>

You could rely on the event bubbling and listen on the scene, and store the entity in the event detail so you know where it came from:
// data-controller component
this.el.emit('dataready', {value: 2, el: this.el})
// Event will bubble to the scene.
// road component
this.el.sceneEl.addEventListener('dataready', function () { // ... });
Or you can pass a reference with the selector property type to which entity to want to listen to:
// road component
schema: {
dataEl: {type: 'selector'},
width: {type: 'number', default: 4},
height: {type: 'number', default: 4}
},
init: function () {
// ...
this.data.dataEl.addEventListener('dataready', function () { // ... });
}
// <a-entity road="dataEl: [data-controller]"></a-entity>

Related

Vue 3 Passing Additional Data as a Parameter with Object Syntax Dynamic Events

Is there a way to pass additional data to event handlers when using the object structure syntax?
Example:
MySFC.vue:
<template>
<template v-for="action in actions" : key="action.id">
<button v-on="action.eventHandlers"> <!--Looking for some way to pass both event and additional data here-->
{{ action.name }}
</button>
</template>
</template>
<script>
import { actions } from '#/lib/my-actions.js';
export default {
setup(props, ctx){
return{
actions,
}
},
}
</script>
my-actions.js:
export const actions = {
name: 'my action',
eventHandlers: {
click: (event, myData) => {
console.log(myData);
},
mousedown: (event, myData) => {
console.log(myData);
}
}
}
Normally if you have a specific event you are trying to handle you can just do something like:
<button #click="myClickHandler($event, myData)"> </button>
But in this case I am trying to build a wrapper of sorts for plugins and do not know what events may need to be handled, so I can't pre-define with v-on which events to look for.
I saw some options for attaching dynamic events, but I only see examples where the event itself is passed, not with additional parameters.
You could use Function.prototype.bind() to bind the function's initial arguments. This can only be done with regular functions (not arrow functions).
// #/lib/my-actions.js
const myClickData = {
id: 'my click data',
}
const myMouseDownData = {
id: 'my mousedown data',
}
export const actions = [
{
name: 'my action',
eventHandlers: {
click: function (myData, event) {
console.log('click', { event, myData })
}.bind(undefined, myClickData),
mousedown: function (myData, event) {
console.log('mousedown', { event, myData })
}.bind(undefined, myMouseDownData),
},
},
]
demo

How to listen to event emitted from 'root' in vue js?

My app uses vue.js and has some integrations with vanilla js. In my vanilla js file, I create a new vue instance and emit an event from there called annotation-click. I can see this event in the dev tools tab.
Here is a custom event class I make:
customevent.js
import Vue from 'vue'
export default window.Event = class Event {
constructor () {
this.vue = new Vue()
}
fire (event, data = null) {
this.vue.$emit(event, data)
}
listen (event, callback) {
this.vue.$on(event, callback)
}
}
And in my vanilla js file I emit the event like this:
import Event from '../utils/customevent'
var test = new Event()
...
handleClick: (view, pos, event) => {
const { schema } = view.state
const attrs = getMarkAttrs(view.state, schema.marks.annotation)
if (attrs.href && event.target instanceof HTMLSpanElement) {
test.fire('notification', {
title: "annotation-joe"
})
}
},
...
The event is being emitted from my main vue.js application as so:
I am listening for the event in my vue.js component, but it seems my listener is not picking the event up.
How can I properly listen to the event being emitted by <Root>?
And my vue.js app that imports the npm package I listen for the event like this:
<script>
import Event from "mypackage/utils/customevent";
var test = new Event();
...
created() {
test.listen("notification", function(msg) {
console.log(msg);
});
}
I wrote an example to make it happen:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
{{message}}
</div>
<button onclick="test()">test</button>
<script src="https://cdn.jsdelivr.net/npm/vue#2/dist/vue.js"></script>
<script>
class MyClass extends EventTarget {
doSomething() {
this.dispatchEvent(new Event('something'));
}
}
document.getRootNode().instance = new MyClass()
function test() {
const root = document.getRootNode()
root.instance.doSomething();
}
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
created() {
const root = document.getRootNode()
root.instance.addEventListener('something', (e) => {
console.log('Instance fired "something".', e);
});
},
})
</script>
</body>
</html>

Why is the update function and "this" not providing expected data for component

I'm having trouble getting the data returned properly. When you first load this page, it logs a blank {} for oldData, and "this" returns the element itself. When you click, it then logs the mouseEvent for the oldData, and the window element for "this".
I expect it should consistently log data from the schema. I'm not sure if the object should be blank when the update first fires, but it depends what "oldData" is at that point. I also expect "this" to consistently be a reference to the component function, not the window. What am I not considering here?
<html lang="en">
<head>
<script src="https://aframe.io/aframe/dist/aframe-master.min.js"></script>
<script>
AFRAME.registerComponent('my-component', {
multiple: false,
schema: {
mood: {
type: 'string',
default: 'happy'
}
},
init() {
window.addEventListener('click', this.update)
},
update(oldData) {
console.log("oldData: ", oldData);
console.log("this: ", this);
},
})
</script>
</head>
<body>
<a-scene>
<a-sky color="blue"></a-sky>
<a-box id="bluebox" color="green" position="-1 0 -3" my-component></a-box>
<a-light position="-3 4 2" type="spot" target="#bluebox"></a-light>
</a-scene>
</body>
</html>
You are passing this.update as a callback and the 'this' will be under the context of the listener method. try:
init () {
this.update = this.update.bind(this)
window.addEventListener('click', this.update)
},
Two problems:
Methods are not bound to the component unless you call them as componentObj.methodName(). You can bound them explicitly with bind:
this.methodName = this.methodName.bind(this);
If the update method is called through the event listener the oldData parameter won't be passed. Better to create your own method onClick. You can rely on this.data instead of oldData in your method logic.
window.addEventListener('click', this.onClick.bind(this));
AFRAME.registerComponent('foo', {
schema: {},
init: function () {
window.addEventListener('click', this.onClick.bind(this));
},
update: function () {},
onClick: function () { ... this.data ... }
});
Okay this works correctly. Upon click (using bind method), the color attribute changes on the internal component, which fires the update method, which displays the oldData and the new data. It also changes the attribute of the external color component based on the internal color from this.data.
<script>
AFRAME.registerComponent('my-component', {
multiple: false,
schema: {
color: {default: '#' + Math.random().toString(16).slice(2, 8).toUpperCase()}
},
init: function () {
this.changeAtt = this.changeAtt.bind(this)
window.addEventListener('click', this.changeAtt);
this.el.setAttribute('color', this.data.color)
},
update: function (oldData){
console.log("oldData: ", oldData);
console.log("current Data: ", this.data);
},
changeAtt () {
el = this.el;
el.setAttribute('my-component', {color: '#' + Math.random().toString(16).slice(2, 8).toUpperCase()})
el.setAttribute('color', this.data.color)
}
})
</script>

Adding a tooltip in a Dojo Select

I would like to add a tooltip to the items in a Dojo Select. This code adds a tooltip when the store is contained in the script.
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
#import "https://ajax.googleapis.com/ajax/libs/dojo/1.9.1/dijit/themes/claro/claro.css";
#import "https://ajax.googleapis.com/ajax/libs/dojo/1.9.1/dojo/resources/dojo.css";
</style>
<script src="https://ajax.googleapis.com/ajax/libs/dojo/1.9.0/dojo/dojo.js" type="text/javascript" data-dojo-config="async: true"></script>
<script>
require(["dijit/form/Select",
"dojo/store/Memory",
"dojo/domReady!"
], function (Select, Memory) {
var store = new Memory({
data: [
{ id: "foo", label: '<div tooltip="Foo Tooltip" onmouseover="showTooltip(this)" onmouseout="hideTooltip(this)">FOO</div>' },
{ id: "bar", label: '<div tooltip="Bar Tooltip" onmouseover="showTooltip(this)" onmouseout="hideTooltip(this)">Bar</div>' }
]
});
var s = new Select({
store: store,
labelType: 'html',
labelAttr: 'label'
}, "target");
s.startup();
});
function showTooltip(el) {
dijit.showTooltip(el.getAttribute('tooltip'), el);
}
function hideTooltip(el) {
dijit.hideTooltip(el);
}
</script>
</head>
<body class="claro">
<div id="target"></div>
</body>
</html>
However, in my application, my store is in a separate module (stores.js).
define([], function () {
return {
priority: [
{ id: "foo", label: '<div tooltip="Foo Tooltip" onmouseover="showTooltip(this)" onmouseout="hideTooltip(this)">FOO</div>' },
{ id: "bar", label: '<div tooltip="Bar Tooltip" onmouseover="showTooltip(this)" onmouseout="hideTooltip(this)">Bar</div>' }
]
};
};
I set the module in the require ("modules/stores") and put the alias in the function (Stores) and create my select using this code.
new Select({
id: "cboPriority",
store: new Memory({ data: Stores.priority }),
labelType: 'html',
labelAttr: 'label'
}, "divPriority").startup();
I've tried adding the showTooltip and hideTooltip functions in the module, but I still get the console error "ReferenceError: showTooltip is not defined". What is the proper way of setting up the script and the module so I can show the tooltip?
You're attempting to set up inline onmouseover event handlers on elements via your label strings. This is going to attempt to call a global showTooltip function, and no such function exists - your showTooltip function is enclosed within your require factory function.
Given that you are creating an HTML label with a node containing an attribute indicating the text to display, a better option in this specific case would be to use dojo/on's event delegation to hook up a single event handler for mouseover and another for mouseout:
var dropdownNode = s.dropDown.domNode;
on(dropdownNode, '[data-tooltip]:mouseover', function () {
Tooltip.show(this.getAttribute('data-tooltip'), this);
});
on(dropdownNode, '[data-tooltip]:mouseout', function () {
Tooltip.hide(this);
});
(Tooltip in the above code refers to the dijit/Tooltip module, and I elected to use a data-attribute which would at least be valid HTML5.)
To be quite honest, I'd prefer avoiding embedding HTML in data to begin with, but this is likely the shortest path from where you are to where you want to be.

Add onLoad Listener to Sencha Touch List

I want to perform selection operation on List after the data has been loaded, because based on the data which I received I have to select one cell in that list and also need to update the detail view base on that.
Ext.define('WaxiApp.view.ProductViews.ProductList', {
extend: 'Ext.Container',
alias: "widget.ProductList",
requires: [
'Ext.Img',
],
config: {
layout: Ext.os.deviceType == 'Phone' ? 'fit' : {
type: 'hbox',
pack:'strech'
},
cls: 'product-list',
items: [{
xtype: 'list',
id:'product-list-view',
width: '100%',
height:'100%',
store: 'ProductsList',
infinite: true,
plugins: 'sortablelist',
itemCls: 'productList',
itemId:"product-item",
itemTpl: new Ext.XTemplate(
'<div class="list-content-div ',
'<tpl if="this.needSortable(isNeedInventry)">',
Ext.baseCSSPrefix + 'list-sortablehandle',
'</tpl>',
'">',
'<b>{UpcCode} {Name}</b>',
'<tpl if="isActive">',
'</div>',
'</tpl>',
{
// XTemplate configuration:
compiled: true,
// member functions:
needSortable: function (isneedInventry) {
return !isneedInventry;
},
}),
masked: { xtype: 'loadmask',message: 'loading' },
onLoad: function (store) {
this.unmask();
console.log('list loaded');
this.fireEvent("productListLoadedCommand", this,store);
},
}
],
listeners: [
{
delegate: "#product-list-view",
event: "itemtap",
fn: function (list, index, target, record) {
console.log(index);
console.log('list selection command fired');
this.fireEvent("productListSelectedCommand", this,index,record);
}
}
],
style: 'background-image: -webkit-gradient(linear, left top, right bottom, color-stop(0, #FDFDFD), color-stop(1, #DBDBDB));background-image: linear-gradient(to bottom right, #FDFDFD 0%, #DBDBDB 100%);'
}//End of config
});//End of Define
Above this actual view I used to display the list. My problem is I tried onLoad() method it work but i want do everything in my Controller to make it more clear.
As you saw my itemTap event has been handled in Controller by firing event. But same is not working for load event.
As mentioned by #Jimmy there is no onLoad method on list. However there are a few ways to work around it. My understanding of what you want to achieve is that when the store backing the list is loaded, you want an event to be fired from the ProductList instance (not the list) such that in the controller you can configure the control to be:
control: {
ProductList: {
productListSelectedCommand: 'productListSelectCommand',
productListLoadedCommand: 'productListLoadedCommand'
}
}
If so then we can modify the listeners in your existing ProductList class to do the following:
listeners: [
{
delegate: "#product-list-view",
event: "itemtap",
fn: function (list, index, target, record) {
console.log(index);
console.log('list selection command fired');
this.fireEvent("productListSelectedCommand", this,index,record);
}
},
{
delegate: "#product-list-view",
event: "show",
fn: function (list) {
var store = list.getStore();
var handler = function() {
list.unmask();
console.log('list loaded');
this.fireEvent("productListLoadedCommand", this, store);
};
if (store.isLoaded()) {
handler.apply(this)
} else {
list.getStore().on('load', handler, this);
}
}
}
]
What this is does is to what for the list to be shown and then get it's store, if the store has loaded then invoke the handler, otherwise register a load listener directly on it. Note that the this object here will be the ProductList not the product-list-view.
Per the Sencha Touch documentation, I do not see an onLoad function for Ext.dataview.List. However, there is a load event listener for the Ext.data.Store, which the list contains. So, your event listener should probably be on the data store, not necessarily the list itself.
Inside your controller's launch method, you could setup a listener for the Store's load event like so:
launch: function () {
// your store should be setup in your Ext.application
Ext.getStore('NameOfStore').on('load', this.productListLoadedCommand);
},
productListLoadedCommand: function(store, records, successful, operation, eOpts) {
// do some after load logic
}
You should set up your event listener for your list in the controller as well. There should be no need for you to create a listener in the view config only to call a fireEvent method in the Controller. Instead, do all event handling in the controller. To get a handle on your list in the Controller, add a xtype: 'productlist' inside the Ext.define for your WaxiApp.view.ProductViews.ProductList. Then, add your list to the Controller's config as a ref and attach the itemtap event for the list in the control like so:
config: {
ref: {
productList: 'productlist'
},
control: {
productList: {
itemtap: 'productListSelectCommand'
}
}
},
productListSelectCommand: function (list, index, target, record, e, eOpts) {
// do some itemtap functionality
}
In the end, your controller might look something like this:
Ext.define('MyApp.controller.Controller', {
extend: 'Ext.app.Controller',
requires: [
// what is required
],
config: {
ref: {
productList: 'productlist'
},
control: {
productList: {
itemtap: 'productListSelectCommand'
}
}
},
launch: function () {
// your store should be setup in your Ext.application
Ext.getStore('NameOfStore').on('load', this.productListLoadedCommand);
},
productListLoadedCommand: function(store, records, successful, operation, eOpts) {
// do some after load logic
// this.getProductList() will give you handle of productlist
},
productListSelectCommand: function (list, index, target, record, e, eOpts) {
// do some itemtap functionality
}
}
Finally, don't forget to add a xtype: 'productlist' inside the Ext.define for your WaxiApp.view.ProductViews.ProductList. I'm not sure of your overall experience with Sencha Touch application design, but here is a good reference for understanding their view, model, store, controller structure.
I found the solution exactly how to handle this scenario and posted my
own solution.
ProductList.js
Ext.define('WaxiApp.view.ProductViews.ProductList', {
extend: 'Ext.Container',
alias: "widget.ProductList",
requires: [
'Ext.Img',
],
initialize: function () {
this.add([
{
xtype: 'list',
id: 'product-list-view',
store: 'ProductsList',
masked: { xtype: 'loadmask', message: 'loading' },
//onLoad is not a listener it's private sencha touch method used to unmask the screen after the store loaded
onLoad: function (store) {
this.unmask();//I manually unmask, because I override this method to do additional task.
console.log('list loaded');
this.fireEvent("productListLoadedCommand", this, store);
}
,
//Painted is event so added it to listener, I saw fee document state that, add event like Painted and show should to added to the
//Component itslef is best practice.
listeners: {
order: 'after',
painted: function () {
console.log("Painted Event");
this.fireEvent("ProductListPaintedCommand", this);
},
scope: this
//This is also very important, because if we using this in card layout
//and changing the active view in layout cause this event to failed, so
//setting scope to this will help to receive the defined view instead of this list component.
}
}]);
},
config: {
listeners: [
{
delegate: "#product-list-view",
event: "itemtap",
fn: function (list, index, target, record) {
console.log(index);
console.log('list selection command fired');
this.fireEvent("productListSelectedCommand", this, index, record);
}
}
],
}//End of config
});//End of Define
ProductViewController.js
/// <reference path="../../touch/sencha-touch-all-debug.js" />
Ext.define("WaxiApp.controller.ProductsViewController", {
extend: "Ext.app.Controller",
listStoreNeedLoad:true,
config: {
refs: {
ProductContainer: "ProductList",
ProductList: "#product-list-view",
ProductDetailView:"ProductDetail"
},
control: {
ProductContainer:{
productListSelectedCommand: "onProductListSelected",
ProductListPaintedCommand: "onProductListPainted"
},
ProductList:{
productListLoadedCommand: "onProductListLoaded"
}
}//End of control
},//End of config
// Transitions
getstore:function(){
return Ext.ComponentQuery.query('#product-list-view')[0].getStore();
},
onProductListPainted:function(list)
{
//Check for if need to load store because painted event called every time your view is painted on screen
if (this.listStoreNeedLoad) {
var store = this.getstore();
this.getstore().load();
}
},
onProductListLoaded:function(list,store)
{
this.listStoreNeedLoad = false;
var index = 0;
//Iterate through record and set my select Index
store.each(function(record,recordid){
console.info(record.get("isNeedInventry"));
if (record.get("isNeedInventry")) {
return false;
}
index++;
});
console.log('list load event fired');
if(Ext.os.deviceType.toLowerCase()!='phone')
{
this.setRecord(store.getAt(index));
list.select(index);
}
}
})//End of define

Categories

Resources