StencilJS Web Component: How to allow end-user to prevent default via custom click event? - web-component

Example Stencil.js web component:
import { Component, ComponentInterface, Event, EventEmitter, h, Host } from "#stencil/core";
#Component({
tag: 'foo-testwebcomponent'
})
export class TestWebComponent implements ComponentInterface {
#Event({
eventName: 'foo-click',
cancelable: true
}) fooClick: EventEmitter;
fooClickHandler() {
this.fooClick.emit();
}
render() {
return(
<Host>
<a href="#"
onClick={this.fooClickHandler.bind(this)}
>Testing</a>
</Host>
)
}
}
HTML:
<foo-testwebcomponent id="test"></foo-testwebcomponent>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('test')
.addEventListener('foo-click', event => {
event.preventDefault();
console.log(`Foo Test Web Component clicked!`);
});
});
</script>
Problem:
In the HTML implementation, the prevent default does not stop the link from working.
Question:
How can I allow the end-user of my web component to prevent default, and stop the link from working?
I know that I can add preventDefault() in the fooClickHandler() (see below), but that seems odd to me. I'd like to give the control to the end user of the web component.
#Event({
eventName: 'foo-click',
cancelable: true
}) fooClick: EventEmitter<MouseEvent>;
fooClickHandler(event: MouseEvent) {
event.preventDefault();
this.fooClick.emit();
}

There are two separate events:
The user-initiated click event
Your fooClick custom event
In your example you call preventDefault() on the custom event but you need to call it on the original click event to prevent the link from navigating.
I know of two ways to achieve this:
1: Track whether your custom event is canceled
You can check whether the user called preventDefault() on your custom event using the defaultPrevented property. The fooClick event handler can stay the same.
fooClickHandler(clickEvent: MouseEvent) {
const customEvent = this.fooClick.emit();
if (customEvent.defaultPrevented) {
clickEvent.preventDefault();
}
}
Check out this online demo.
2: Pass the click event
Pass the click event to the fooClick event handler so the user can cancel it.
fooClickHandler(clickEvent: MouseEvent) {
this.fooClick.emit({ originalEvent: clickEvent });
}
And in the handler:
element.addEventListener('foo-click', event => {
event.detail.originalEvent.preventDefault();
console.log(`Foo Test Web Component clicked!`);
});

One way would be to overload the addEventListener function and capture the function reference
(needs some more work to make it work with nested elements, you get drift)
Or use a custom method addClick(name,func) so the user can still add any listener
<script>
customElements.define(
"my-element",
class extends HTMLElement {
connectedCallback() {
this.clicked = (evt)=>{
document.body.append("component handler")
}
this.onclick = (evt) => {
this.clicked(evt);
}
}
addEventListener(name, func) {
this.clicked = func;
}
}
);
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('my-element')
.addEventListener('click', event => {
document.body.append(`user handler`);
});
});
</script>
<my-element>Hello Web Components World!</my-element>
You could also use good old onevent handlers:
<script>
customElements.define(
"my-element",
class extends HTMLElement {
connectedCallback() {
this.onclick = (evt) => console.log("component handler")
}
}
);
document.addEventListener('DOMContentLoaded', () => {
let el = document.querySelector('my-element');
el.onclick = event => console.log(`user handler`, el.onclick);
});
</script>
<my-element onclick="console.log('inline')">Hello Web Components World!</my-element>

Related

Detect change to modelValue in Vue 3

Is there a way to detect change to modelValue in a custom component? I want to push the change to a wysiwyg editor.
I tried watching modelValue but emitting update for modelValue triggered that watch, which created circular data flow.
Code:
export default {
props: ['modelValue'],
watch: {
modelValue (val) {
this.editor.editor.loadHTML(val)
}
},
mounted () {
this.editor.editor.loadHTML(val)
this.editor.addEventListener('trix-change',
(event) => this.$emit('update:modelValue', event.target.value))
}
}
<TextEditor v-model="someHtml"></TextEditor>
In VueJS v3, the event name for custom v-model handling changed to 'update:modelValue'.
You can listen to these events like this: v-on:update:modelValue="handler"
For a more complete example, lets assume you have a Toggle component with these properties/methods:
...
props: {
modelValue: Boolean,
},
data() {
return {
toggleState: false,
};
},
methods: {
toggle() {
this.toggleState = !this.toggleState;
this.$emit('update:modelValue', this.toggleState);
}
}
...
You can use that Toggle component:
<Toggle v-model="someProperty" v-on:update:modelValue="myMethodForTheEvent"/>
As a side note, you could also v-model on a computed property with a setter; allowing you to internalise your state changes without using the update:modelValue event. In this example, it assumes you v-model="customProperty" on your custom Toggle component.
computed: {
customProperty: {
get() {
return this.internalProperty;
},
set(v) {
this.internalProperty = v;
console.log("This runs when the custom component 'updates' the v-model value.");
},
}
},
I had the same problem and solved it using a slight tweak to the way you call the watch function:
setup(props) {
watch(() => props.modelValue, (newValue) => {
// do something
})
}
Hence, the important thing is to add () => props.modelValue instead of just putting props.modelValue as the first argument of the watch function.
try that:
watch: {
...
modelValue: function(val) {
console.log('!!! model value changed ', val);
},
...

Firebase: Why value event gets fired before new child ref gets added

Following code, is a very simple Firebase - VueJS app, (codeSandBox demo)
app.vue
<template>
<div class="container">
<!-- Adding Quote -->
<add-quote/>
<!-- Display Quotes -->
<quote-list/>
</div>
</template>
<script>
import addQuote from "./components/AddQuote.vue";
import quoteList from "./components/QuoteList.vue";
export default {
components: {
addQuote,
quoteList
},
methods: {
get_allQuotes: function() {
// var vm = this;
var localArr = [];
quotesRef
.once("value", function(snapshot) {
snapshot.forEach(function(snap) {
localArr.push({
key: snap.key,
category: snap.val().category,
quoteTxt: snap.val().quoteTxt
});
});
})
.then(data => {
this.$store.commit("set_allQuotes", localArr);
});
}
},
mounted() {
this.get_allQuotes();
console.log("App: mounted fired");
}
};
</script>
store.js(vuex store)
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
quotesList: []
},
getters: {
get_quotesList(state) {
return state.quotesList;
}
},
mutations: {
set_allQuotes(state, value) {
state.quotesList = value;
}
}
});
AddQuote.vue
<template>
<div class="row quote-edit-wrapper">
<div class="col-xs-6">
<textarea v-model.lazy="newQuoteTxt"
rows="4"
cols="50"></textarea>
<button #click="addQuote">Add Quote</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
newQuoteTxt: '',
}
},
computed: {
allQuotes() {
return this.$store.getters.get_quotesList;
},
newQuoteIdx() {
var localArr = [...this.allQuotes]
if(localArr.length > 0) {
var highestKEY, currKEY
localArr.forEach((element, idx) => {
currKEY = parseInt(element.key)
if(idx == 0) {
highestKEY = currKEY
} else {
if(highestKEY < currKEY) {
highestKEY = currKEY
}
}
})
return highestKEY + 1
} else {
return 1
}
}
},
methods: {
// ADD new Quote in DB
addQuote: function() {
var vm = this
var localArr = [...this.allQuotes]
//1. First attach 'value' event listener,
// Snapshot will contain data from that ref
// when any child node is added/updated/delete
quotesRef.on('value', function (snapshot) {
snapshot.forEach(function(snap) {
var itemExists = localArr.some(function (item, idx) {
return item.key == snap.key
})
// If newly added item doesn't yet exists then add to local array
if (!(itemExists)) {
localArr.push({
key: snap.key,
category: snap.val().category,
quoteTxt: snap.val().quoteTxt })
vm.$store.commit('set_allQuotes', localArr)
}
})
})
//2. Second set/create a new quotes in Firebase,
// When this quote gets added in Firebase,
// value event (attached earlier) gets fired
// with
var newQuoteRef = quotesRef.child(this.newQuoteIdx)
newQuoteRef.set({
category: 'motivation',
quoteTxt: this.newQuoteTxt
})
}
}
}
</script>
quoteList.vue
<template>
<div class="row">
<div class="col-xs-12 quotes-list-wrapper">
<template v-for="(quote,idx) in allQuotes">
<!-- Quote block -->
<div class="quote-block-item">
<p class="quote-txt"> {{quote.quoteTxt}} </p>
</div>
</template>
</div>
</div>
</template>
<script>
export default {
computed: {
allQuotes() {
return this.$store.getters.get_quotesList;
}
}
}
</script>
Note: The main code of concern is of addQuote.vue
User enter newQuoteTxt that gets added to Firebase (addQuote()) as a quote item under quotesRef. As soon as quote is added (on firebase), Firebase client side SDK's value event fires, and adds the new quote (via callback) to localArray (allQuotes). VueJS then updates the DOM with newly added Quote.
The addQuote() method works in the following manner:
First, attach a callback/listener to 'value' event on quotesRef
quotesRef.on('value', function (snapshot) {
....
})
Next, A firebase ref (child of quotesRef) is created with a ID this.newQuoteIdx
var newQuoteRef = quotesRef.child(this.newQuoteIdx)
Then set() is called (on this newly created Ref) adding newquote to firebase RealTime DB.
value event gets triggered (attached from step 1) and listener /callback is called.
The callback looks for this new quote's key in existing list of items by matching keys of localArr and snap.key, if not found, adds the newly quote to localArr. localArr commits to a vuex store.
`vm.$store.commit('set_allQuotes', localArr)`
VueX then updates all subscriber component of this array. VueJS then adds the new quote to the existing list of quotes (updates the DOM)
While debugging the addQuote method, the problem I notice, the execution/flow of script (via F8 in chrome debugger) first steps into the listener/callback attached to value event before the code newQuoteRef.set({ ... }) that adds new quote (on firebase), which in turn will cause 'value' event to trigger.
I am not sure why this occurs. Can anybuddy explain why the listener/callback is called before the quotes is created.
Are child nodes (of QuotesRef) are cached at clientside such that 'value' fires even before new quote is added.
Thanks
If I correctly understand your question (Your code is not extremely easy to follow! :-)) it is the normal behaviour. As explained in the documentation:
The value event will trigger once with the initial data stored at
this location, and then trigger again each time the data
changes.
Your sandbox demo does not actually shows how the app works, but normally you should not set-up the listener in the method that saves a new node to the database. These two things should be decoupled.
One common approach is to set the listener in the created hook of a component (see https://v2.vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks and https://v2.vuejs.org/v2/api/#created) and then in your addQuote method you just write to the database. As soon as you write, the listener will be fired.

Removing a window event listener in Template.onDestroyed

I have two templates that each contain a Vimeo iframe player. I'm using FlowRouter to render the templates through {{> Template.dynamic template=main}} on the main layout.
In both templates I add listeners for video events in onCreated
Template.view.onCreated( function() {
var self = this;
if (window.addEventListener) {
window.addEventListener('message', function(event) {
viewMessageReceived(event, self)}, false);
} else {
window.attachEvent('onmessage', function(event){
viewMessageReceived(event, self)}, false);
}
});
and destroy them in onDestroyed
Template.view.onDestroyed( function() {
if (window.removeEventListener) {
console.log('view remove');
window.removeEventListener('message', function(event) {
viewMessageReceived(event, self)}, false);
} else {
window.detachEvent('onmessage', function(event){
viewMessageReceived(event, self)}, false);
}
});
And here is the function being called by the anonymous event handler:
function viewMessageReceived(event, self) {
// Handle messages from the vimeo player only
if (!(/^https?:\/\/player.vimeo.com/).test(event.origin)) {
return false;
}
if (self.playerOrigin === '*') {
self.playerOrigin = event.origin;
}
var data = JSON.parse(event.data);
switch (data.event) {
case "ready":
initializePlayer(self);
break;
case "playProgress":
self.playerTime.set(data.data.seconds);
if (self.duration === '*') self.duration = data.data.duration;
break;
case "play":
self.playerStatus.set("playing");
break;
case "pause":
self.playerStatus.set("paused");
break;
}
}
When I switch to a different template, onDestroyed runs and my console.log('view remove') fires, as expected.
But then when I navigate to the page that loads the other template with a video player, a Vimeo "playProgress" message arrives that is received by the event handler in the previous video template, which was supposed to have been removed a while ago. This throws an error because the previous template has been destroyed.
Uncaught TypeError: Cannot read property 'contentWindow' of undefined
which comes from the last line in this function:
function post(template, action, value) {
console.log('view action: %s value: %s', action, value);
var data = {method: action};
if (value) data.value = value;
var message = JSON.stringify(data);
template.player[0].contentWindow.postMessage(message, template.playerOrigin);
}
Each of the video-containing templates have their own .js file, so they each have a their own post function declaration. My understanding is that defining a function that way scopes the function just to the page.
It's only one message that arrives for the wrong player. After that, they arrive for the currently loaded player.
Why does the Vimeo message event arrive or get handled after I've already destroyed the template and when I've moved to another player?
A quote from the W3Schools website regarding the removeEventListener() method:
// Attach an event handler to <div>
document.getElementById("myDIV").addEventListener("mousemove", myFunction);
// Remove the event handler from <div>
document.getElementById("myDIV").removeEventListener("mousemove", myFunction);
Note: To remove event handlers, the function specified with the
addEventListener() method must be an external function, like in the
example above (myFunction).
Anonymous functions, like "element.removeEventListener("event",
function(){ myScript });" will not work.
For this to work, you'll need to move your function definition somewhere outside of the onRendered and onDestroyed events, and just pass the function name in the add/remove event listeners.

SemanticUI: how to trigger a form validation maually / in modal

When a modal window is started, there will be no form validation if you use the default way:
$('#someModalWindow')
.modal({
inline: true,
onDeny: function () {
// someting
},
onApprove: function () {
// some action
}
})
.modal('show');
How can a form validation be triggered manually or automatically in the modal window.
I am using meteor below SemanticUI
thanks
I figured out how to do it:
$('#someModalWindow')
.modal({
onDeny: function () {
// someting
},
onApprove: function () {
var validated = $('#myFormId').form('validate form');
if(!validated){
return false;
}
// some action
}
})
.modal('show');
hopefully this can help you.

Meteor: Most "Meteoric" way to clear form fields

I'm working on an example CRUD application with Meteor.js and am not sure how best to empty out the fields of a form. I need it in two places: when the Submit button is clicked, and when the Cancel button is clicked.
I implemented it this way by creating a utility function called clearFormFields() that just uses jQuery to empty their contents, but it doesn't feel as "Meteoric" as it should; I feel it should be scoped better so it doesn't have a global visibility. What am I doing wrong?
function clearFormFields() {
$("#description").val("");
$("#priority").val("");
}
Template.todoNew.events({
'click #cancel': function(event) {
event.preventDefault();
Session.set('editing', false);
clearFormFields();
},
'submit form': function(event) {
event.preventDefault();
var theDocument = {
description: event.target.description.value,
priority: event.target.priority.value
};
if (Session.get("editing")) {
Meteor.call("updateTodo", theDocument, Session.get('theDocumentId'))
}
else {
Meteor.call("insertTodo", theDocument);
}
Session.set('editing', false);
clearFormFields();
/* Could do this twice but hate the code duplication.
description: event.target.description.value = "";
priority: event.target.priority.value = "";
*/
}
});
You could use the native reset method of the DOM form node ?
"submit form":function(event,template){
event.preventDefault();
// ...
template.find("form").reset();
}
http://www.w3schools.com/jsref/met_form_reset.asp
The DOM object that originated the event can be accessed and reset from through event.target.reset();
"submit form":function(event){
event.preventDefault();
//...
event.target.reset();
}
http://docs.meteor.com/#/full/eventmaps

Resources