VueJS - Conditional component mouse events - javascript

New to Vue and frameworks in general, and may have my thinking not very "Vue-like".
Trying to make a "super" button component that takes a prop, which dictates the buttons behavior so I only have to maintain one button component. The ideal form when implementing would like something like this...
<super-button isType="string"></super-button>
The template is...
<button class="buttonLow" v-bind:class="{buttonHigh: isSelected}">
<slot></slot>
</button>
Where isType prop could be momentary momentary-multi toggle or toggle-multi.
I have a basic set of event emitters/methods and listeners that work regardless of the isType and simply makes the buttons state high or low / on or off using another prop isSelected.
The problem is trying to conditionally setup the mouse events depending on the isType. While figuring out the logic, I used the # syntax to setup the mouse events #click #mousedown #mouseup etc. and everything worked great by itself. For example, the events for a momentary button during testing looked like this...
<button #mousedown="emitButtonHigh()" #mouseup="emitButtonLow" #mouseleave="emitButonLow"></button>
However, a simple toggle button looked more like this...
<button #click="emitButtonToggle()"></button>
Obviously there is a bit of conflict there.
My attempted work around was to use a switch statement in created() that would take isType as the expression and conditionally register the appropriate mouse events...
created(){
switch(this.isType){
case ("momentary"):{
//attach on events
break;
}
case ("toggle"):{
//attach on events
break;
}
case ("toggle-multi"):{
//attach on events
break;
}
default:{
break;
}
}
}
While switch itself is working, I can't figure out how to attach the mouse events in this context. I can attach a custom event no problem using...
this.$root.$on('my-custom-event', ()=>{
//do stuff
});
but trying do something like...
this.$root.$on('click', ()=>{
//do stuff
});
or...
this.$on('click', ()=>{
//do stuff
});
Does not work, nor can I figure out any way to write it out that creates the same functionality as #click let alone #mousedown #mouseup or other built-in events.
TL;DR
How do you write out the # syntax or v-on syntax, for built-in events (click, mousedown, mouseup, etc.), using $on syntax, so the events actually fire?

You could attach all these component events on a single handler, determine the event.types as they're fired and emit your custom events from here, while optionally passing additional arguments.
const SuperButton = Vue.extend({
template: `
<button
#mousedown="emitCustomEvent"
#mouseup="emitCustomEvent"
#mouseleave="emitCustomEvent">
<slot></slot>
</button>
`,
props: {
isType: {
type: String,
default:
String
},
// ...
},
methods: {
emitCustomEvent(e) {
let type = e.type.substr('mouse'.length);
let args = {
type,
isSelected: type === 'down',
args: {
// Your additional args here
}
};
switch(this.isType) {
case ("momentary"):{
//attach on events
break;
}
// ...
}
// Or emit events regardless of the "isType"
this.$emit(`mouse:${type}`, args);
this.$emit('mouse', args);
}
}
});
new Vue({
el: '#app',
methods: {
mousing(args) {
console.log(`mouse:${args.type} from component.`);
},
mouseLeaving() {
console.log('Mouse leaving.');
}
},
components: {
SuperButton
}
});
Vue.config.devtools = false;
Vue.config.productionTip = false;
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<super-button #mouse="mousing">Super button</super-button>
<super-button #mouse:leave="mouseLeaving">Super button 2, detecting leaving only</super-button>
</div>

Related

How do you make an HTML element hear a document.body scroll on vue 3? [duplicate]

With JQuery, click event of the any item in the page can be captured as below.
$(document).click(function(event){
// event.target is the clicked element object
});
How to do the same with Vue.js?
The answer provided by M U is correct and works.
Yet if you don't like messing with your template (e.g. not put a lot of event handlers in it) or your Vue app is only a small part of a bigger application, it's also perfectly fine and acceptable to register event handlers manually.
To add global event handlers in your script the Vue way you should register them in the mounted and remove them in the beforeDestroy hooks.
Short example:
var app = new Vue({
el: '#app',
mounted: function () {
// Attach event listener to the root vue element
this.$el.addEventListener('click', this.onClick)
// Or if you want to affect everything
// document.addEventListener('click', this.onClick)
},
beforeDestroy: function () {
this.$el.removeEventListener('click', this.onClick)
// document.removeEventListener('click', this.onClick)
},
methods: {
onClick: function (ev) {
console.log(ev.offsetX, ev.offsetY)
}
}
})
All of the answers provided works, but none of them mimic the real behavior of $(document).click(). They catch just clicks on the root application element, but not on the whole document. Of course you can set your root element to height: 100% or something. But in case you want to be sure, it's better to modify Bengt solution and attach event listener directly to document.
new Vue({
...
methods: {
onClick() {},
}
mounted() {
document.addEventListener('click', this.onClick);
},
beforeDestroy() {
document.removeEventListener('click', this.onClick);
},
...
});
Remember you need to use #click.stop in children elements if you for some reason need to stop event propagation to the main handler.
Create div as top node, right after <body>
Make it main container and mount VueJS on it.
<div id='yourMainDiv' #click='yourClickHandler'>
In your VueJS <script> part use it:
methods: {
yourClickHandler(event) {
// event.target is the clicked element object
}
}
Also, if you need to track click event outside of specific element, you
can use vue-clickaway component. Example from the demo:
<div id="demo">
<p v-on-clickaway="away" #click="click" :style="{ color: color }">{{ text }}</p>
</div>
new Vue({
el: '#demo',
mixins: [VueClickaway.mixin],
data: {
text: 'Click somewhere.',
color: 'black',
},
methods: {
click: function() {
this.text = 'You clicked on me!';
this.color = 'green';
},
away: function() {
this.text = 'You clicked away...';
this.color = 'red';
},
},
});

Run Ember.run.later from action triggered

I am having a problem with action handling in Ember controller. I want to run some function continuously after edit button is clicked in hbs. I have tried it like this in action.
openEditWindow() {
this.set('someChangingValue', true);
},
Here is the function that reacts to action someChangingValue change.
someChangingValue: false,
someTestFunction: observer('someChangingValue', function() {
var test = this.get('someChangingValue');
if(test === true){
Ember.run.later((function() {
this.functionThatIsRunningEachTwoSeconds();
}), 2000);
} else {
console.log('this should not do anything');
}
}),
But this runs functionThatIsRunningEachTwoSeconds only once. Also tried the same functionality with changing someChangingValue to false if true and otherwise, that put me in an infinite loop of observing property.
Thanks!
Ember.run.later runs function only once. It is said clear in docs
Also, do you use very old version of ember? Ember.run.later is outdated and you supposed to use partial import import { later } from '#ember/runloop'; instead of that
As for your task, there is at least two ways
Using ember-concurrency addon
Install ember-concurrency and write in controller:
import { task, timeout } from 'ember-concurrency';
export default Controller.extend({
infiniteTask: task(function* () {
while(true) {
this.functionThatIsRunningEachTwoSeconds();
yield timeout(2000);
}
}).drop(),
});
Template:
{{#if infiniteTask.isIdle}}
<button onclick={{perform infiniteTask}}>Start</button>
{{else}}
<button onclick={{cancel-all infiniteTask}}>Stop</button>
{{/if}}
This addon is helpful in lot of situations, read it's docs to understand why you might need it
Creating a function that will recursively call itself
It's a classical JS approach to repeat some action, but in vanilla JS we use setTimeout instead of ember's later.
import { later, cancel } from '#ember/runloop';
export default Controller.extend({
infiniteFuction() {
this.functionThatIsRunningEachTwoSeconds();
this.set('infiniteTimer', later(this, 'infiniteFuction', 2000));
},
startInfiniteFunction() {
//clear timer as safety measure to prevent from starting few
//"infinite function loops" at the same time
cancel(this.infiniteTimer);
this.infiniteFuction();
},
stopInfiniteFunction() {
cancel(this.infiniteTimer);
this.set('infiniteTimer', undefined);
}
});
Template:
{{#unless infiniteTimer}}
<button onclick={{action startInfiniteFunction}}>Start</button>
{{else}}
<button onclick={{action stopInfiniteFunction}}>Stop</button>
{{/unless}}
Just to clarify what's wrong with your current code (and not necessarily promoting this as the solution), you must change the value for the observer to fire. If you set the value to true, and then set it to true again later without ever having set it to false, Ember will internally ignore this and not refire the observer. See this twiddle to see a working example using observers.
The code is
init(){
this._super(...arguments);
this.set('count', 0);
this.set('execute', true);
this.timer();
},
timer: observer('execute', function(){
//this code is called on set to false or true
if(this.get('execute')){
Ember.run.later((() => {
this.functionThatIsRunningEachTwoSeconds();
}), 2000);
// THIS IS SUPER IMPORTANT, COMMENT THIS OUT AND YOU ONLY GET 1 ITERATION
this.set('execute', false);
}
}),
functionThatIsRunningEachTwoSeconds(){
let count = this.get('count');
this.set('count', count + 1);
this.set('execute', true);
}
Now that you know what's wrong with your current approach, let my go back again on record and suggest that this is not a great, intuitive way to orchestrate a repeated loop. I recommend the Ember-Concurrency as well since it is Ember lifecycle aware
If you handled the edit button in the route, you could super cleanly cancel it on route change
functionThatIsRunningEachTwoSeconds: task(function * (id) {
try {
while (true) {
yield timeout(2000);
//work function
}
} finally {
//if you wanted code after cancel
}
}).cancelOn('deactivate').restartable()
Deactivate corresponds to the ember deactivate route hook/event:
This hook is executed when the router completely exits this route. It
is not executed when the model for the route changes.

JS, JQuery and Observable

I`m building third party application for specific sites with Jquery.
Recently I started to use rx.Observable in my project. However, I found to use of this new JS library sometimes is hard to understand. I have tried to convert next peace of code to use with Observables, but it is not working at all;
class EventsUtils {
constructor() {
this.observable = Rx.Observable;
}
bindUserLeavePageEvent() {
var self = this;
document.addEventListener('mouseleave', (e) => {
$JQ(document).trigger('mouseleave.mo');
}, false);
/*We cannot remove document mouse over event thus we trigger Jquery registered custom event and on remove we cancel it*/
$JQ(document).off('mouseleave.mo').on('mouseleave.mo', (e) => {
if (e.clientY < 0 && !self.loaded) {
console.log('loading from screen Leave');
$JQ('.fixed-button').trigger('click');
self.loaded = true;
}
});
}
$JQ variable is came from jquery.noConflict due to i am running not on my page.
To convert second expression to Observable I have tried to use next statement:
this.observable.fromEvent(document, 'mouseleave.mo').pluck('currentTarget').subscribe(x=>console.log(x));
}
But without success.
How to convert above event statements to use with Observable and what is common pattern to do this;
It seems as if jquery.trigger does not really work with custom events - you can only catch those events through $(elem).on as they are handles internally for browser-compatibility-reasons. (https://github.com/jquery/jquery/issues/2476)
But you can relatively easy dispatch custom events (unless you want to target IE<=8)
document.addEventListener("mouseleave", () => {
console.log("Original event: Leave");
// dispatching custom events with vanilla-js (should work all the way down to IE9)
const event = document.createEvent("CustomEvent");
event.initEvent("mo.leave", true, true);
document.dispatchEvent(event);
});
Rx.Observable
.fromEvent(document, "mo.leave")
.pluck("currentTarget")
.subscribe(target => console.info("Target is", target.nodeName));
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>

Click event not firing when React Component in a Shadow DOM

I have a special case where I need to encapsulate a React Component with a Web Component. The setup seems very straight forward. Here is the React Code:
// React Component
class Box extends React.Component {
handleClick() {
alert("Click Works");
}
render() {
return (
<div
style={{background:'red', margin: 10, width: 200, cursor: 'pointer'}}
onClick={e => this.handleClick(e)}>
{this.props.label} <br /> CLICK ME
</div>
);
}
};
// Render React directly
ReactDOM.render(
<Box label="React Direct" />,
document.getElementById('mountReact')
);
HTML:
<div id="mountReact"></div>
This mounts fine and the click event works. Now when I created a Web Component wrapper around the React Component, it renders correctly but the click event doesn't work. Here is my Web Component Wrapper:
// Web Component Wrapper
class BoxWebComponentWrapper extends HTMLElement {
createdCallback() {
this.el = this.createShadowRoot();
this.mountEl = document.createElement('div');
this.el.appendChild(this.mountEl);
document.onreadystatechange = () => {
if (document.readyState === "complete") {
ReactDOM.render(
<Box label="Web Comp" />,
this.mountEl
);
}
};
}
}
// Register Web Component
document.registerElement('box-webcomp', {
prototype: BoxWebComponentWrapper.prototype
});
And here is the HTML:
<box-webcomp></box-webcomp>
Is there something I'm missing? Or does React refuse to work inside a Web Component? I have seen a library like Maple.JS which does this sort of thing, but their library works. I feel like I'm missing one small thing.
Here is the CodePen so you can see the problem:
http://codepen.io/homeslicesolutions/pen/jrrpLP
As it turns out the Shadow DOM retargets click events and encapsulates the events in the shadow. React does not like this because they do not support Shadow DOM natively, so the event delegation is off and events are not being fired.
What I decided to do was to rebind the event to the actual shadow container which is technically "in the light". I track the event's bubbling up using event.path and fire all the React event handlers within context up to the shadow container.
I added a 'retargetEvents' method which binds all the possible event types to the container. It then will dispatch the correct React event by finding the "__reactInternalInstances" and seek out the respective event handler within the event scope/path.
retargetEvents() {
let events = ["onClick", "onContextMenu", "onDoubleClick", "onDrag", "onDragEnd",
"onDragEnter", "onDragExit", "onDragLeave", "onDragOver", "onDragStart", "onDrop",
"onMouseDown", "onMouseEnter", "onMouseLeave","onMouseMove", "onMouseOut",
"onMouseOver", "onMouseUp"];
function dispatchEvent(event, eventType, itemProps) {
if (itemProps[eventType]) {
itemProps[eventType](event);
} else if (itemProps.children && itemProps.children.forEach) {
itemProps.children.forEach(child => {
child.props && dispatchEvent(event, eventType, child.props);
})
}
}
// Compatible with v0.14 & 15
function findReactInternal(item) {
let instance;
for (let key in item) {
if (item.hasOwnProperty(key) && ~key.indexOf('_reactInternal')) {
instance = item[key];
break;
}
}
return instance;
}
events.forEach(eventType => {
let transformedEventType = eventType.replace(/^on/, '').toLowerCase();
this.el.addEventListener(transformedEventType, event => {
for (let i in event.path) {
let item = event.path[i];
let internalComponent = findReactInternal(item);
if (internalComponent
&& internalComponent._currentElement
&& internalComponent._currentElement.props
) {
dispatchEvent(event, eventType, internalComponent._currentElement.props);
}
if (item == this.el) break;
}
});
});
}
I would execute the "retargetEvents" when I render the React component into the shadow DOM
createdCallback() {
this.el = this.createShadowRoot();
this.mountEl = document.createElement('div');
this.el.appendChild(this.mountEl);
document.onreadystatechange = () => {
if (document.readyState === "complete") {
ReactDOM.render(
<Box label="Web Comp" />,
this.mountEl
);
this.retargetEvents();
}
};
}
I hope this works for future versions of React. Here is the codePen of it working:
http://codepen.io/homeslicesolutions/pen/ZOpbWb
Thanks to #mrlew for the link which gave me the clue to how to fix this and also thanks to #Wildhoney for thinking on the same wavelengths as me =).
I fixed a bug cleaned up the code of #josephvnu's accepted answer. I published it as an npm package here: https://www.npmjs.com/package/react-shadow-dom-retarget-events
Usage goes as follows
Install
yarn add react-shadow-dom-retarget-events or
npm install react-shadow-dom-retarget-events --save
Use
import retargetEvents and call it on the shadowDom
import retargetEvents from 'react-shadow-dom-retarget-events';
class App extends React.Component {
render() {
return (
<div onClick={() => alert('I have been clicked')}>Click me</div>
);
}
}
const proto = Object.create(HTMLElement.prototype, {
attachedCallback: {
value: function() {
const mountPoint = document.createElement('span');
const shadowRoot = this.createShadowRoot();
shadowRoot.appendChild(mountPoint);
ReactDOM.render(<App/>, mountPoint);
retargetEvents(shadowRoot);
}
}
});
document.registerElement('my-custom-element', {prototype: proto});
For reference, this is the full sourcecode of the fix https://github.com/LukasBombach/react-shadow-dom-retarget-events/blob/master/index.js
This answer is an update from five years after.
Bad news: answer by #josephnvu (accepted at the moment of writing) and the react-shadow-dom-retarget-events package no longer work correctly, at least with React 16.13.1 - haven't tested with earlier versions. Looks like something was changed in React internals, causing the code to invoke the wrong listener callback.
Good news:
In React 16.13.1 (again, not tested with earlier 16.x), it's possible to render directly into shadow root, without intermediate blocks. In this case, listeners would be attached to the shadow root and not to the document, so React is able to capture and dispatch all events correctly. The obvious tradeoff is that you can't add anything else to the same shadow root, since React will overwrite your elements with rendered JSX.
In React 17, React attaches its listeners to the rendering root, not to the document or shadow root, so everything works out of the box, no matter where we render to.
Replacing this.el = this.createShadowRoot(); with this.el = document.getElementById("mountReact"); just worked. Maybe because react has a global event handler and shadow dom implies event retargeting.
I've discovered another solution by accident. Use preact-compat instead of react. Seems to work fine in a ShadowDOM; Preact must bind to events differently?

Ember.js - I want an Action event (on="") to trigger when there is a transition to a new Route

I want an Action event (on="") to trigger when there is a transition to a new Route.
I've seen the list of Action event handlers and closest I could find is attaching the action to the largest HTML element on the page and triggering it with 'Mousemove". This is a terribly flawed away of going about what I want to do.
So just to draw it out.
<div {{action 'displayEitherHtml1or2'}} class="largestDOMelement">
{{#if showHtml1}}
// html 1 inside
{{/if}}
{{#if showHtml2}}
// html 2 inside
{{/if}}
</div>
'/objects' is a list of objects and clicking one leads to 'object/somenumber'. The action should automatically trigger when I enter the 'object/somenumber' page.
UPDATE: I've taken the contents from the previous update and dumped them into my DocRoute, but nothing it being triggered when I transition to 'doc' through {{#link-to 'doc' this.docID}} {{docTitle}}{{/link-to}}
VpcYeoman.DocRoute = Ember.Route.extend(VpcYeoman.Authenticated,{
toggleLetterSwitch: false,
togglePermitSwitch: false,
activate: function () {
var docTemplateID = this.get('docTemplateID');
if ( docTemplateID == 2) {
this.set('toggleLetterSwitch', true);
this.set('togglePermitSwitch', false);
console.log('docTemplateID equals 2');
} else {
this.set('toggleLetterSwitch', false);
this.set('togglePermitSwitch', true);
}
}
});
UPDATE DOS: setDocID is set in the DocsController to 1. Here's the whole thing.
VpcYeoman.DocsController = Ember.ArrayController.extend({
tempDocId: 1,
actions: {
addDoc: function (params) {
var docTitle = this.get('docTitle');
var docTemplateID = 1;
var docTemplateID = this.get('tempDocId');
console.log(this.get('tempDocId'));
var store = this.store;
var current_object = this;
var doc = current_object.store.createRecord('doc', {
docTitle:docTitle,
docTemplateID:docTemplateID
});
doc.save();
return true;
},
setDocId: function (param) {
this.set('tempDocId', param);
console.log(this.get('tempDocId'))
},
}
});
As #fanta commented, it seems like you're looking for the activate hook within your Route. This gets called when you enter the route where you define it. If you want to call it on every transition, you might consider defining a base route for your application and extending that instead of Em.Route:
App.BaseRoute = Em.Route.extend(
activate: function () {
// Do your thing
}
);
App.YourRoutes = App.BaseRoute.extend()
It's possible that there's a more appropriate place/time to do this, but without knowing quite what your action does, this is probably the best guess.
ETA: Looking at your edit, you won't want all your routes to extend App.BaseRoute the way I did it above; you should probably just include that activate hook explicitly in the routes which need it.

Categories

Resources