KoLite knockout.command has incorrect context with inherited class - javascript

When a subclass viewmodel is bound to a View, that overrides a method in a base class in inherits from, "knockout.command" seems to be calling the base method instead of the child overridden method.
Here is a jsfiddle in javascript, although I'm using typescript.
Note a plain old click binding alerts "child action" while the knockout.command alerts "base action". How can I make this correctly call the overridden child method?
Typescript:
class BaseVm {
public commandAction: any;
constructor() {
this.commandAction = ko.asyncCommand({ execute: this.action, canExecute: (isExecuting) => true });
}
public clickAction = () => {
this.action(null, null, null);
}
public action = (val, event, complete) => {
alert("base action");
}
}
class ChildVm extends BaseVm {
constructor() {
super();
}
public action = (loc, event, complete) => {
alert("child action");
}
}
ko.applyBindings(new ChildVm());
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TEST</title>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-debug.js"></script>
<script type="text/javascript" src="https://cdn.rawgit.com/CodeSeven/KoLite/master/knockout.command.js"></script>
<script type="text/javascript" src="app.js"></script>
</head>
<body>
<button data-bind="command: { click: $root.commandAction }">COMMAND</button>
<button data-bind="click: $root.clickAction">CLICK</button>
<script>
ko.applyBindings(new ChildVm());
</script>
</body>
</html>

The problem isn't context, it's that base class constructors run before derived class constructors, and it's the derived class constructor where action gets overwritten by the derived class implementation.
When this line of code runs, this.action still refers to the base class implementation, and the value is captured at that moment.
constructor() {
this.commandAction = ko.asyncCommand({ execute: this.action, canExecute: (isExecuting) => true });
}
You can instead write this to dynamically get the value of this.action:
constructor() {
this.commandAction = ko.asyncCommand({ execute: (a, b, c) => this.action(a, b, c), canExecute: (isExecuting) => true });
}

Related

Vue.js 2: converting a string in a function call at an event handler

In Vue.js 2 I would like to convert a string into a function call so that it can be set as an event handler.
I believe this would be very practical, specially when dynamically creating lots of elements (e.g. buttons) based on a list of objects.
new Vue({
el: "#app",
data: {
myArray: [
{ value: 1, fn: "firstMethod" },
{ value: 2, fn: "secondMethod" },
],
},
methods: {
firstMethod() {
console.log("'firstMethod' was executed.");
},
secondMethod() {
console.log("'secondMethod' was executed.");
},
},
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<template v-for="elem in myArray">
<button #click="elem.fn"> <!-- Here is where I am stucked. -->
<!-- <button> -->
{{elem.value}}
</button>
</template>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#2/dist/vue.js"></script>
<script src="script.js"></script>
</body>
</html>
My first attempt at doing this was setting the fn properties in myArray as sort of pointers to the corresponding functions with this (e.g. fn: this.firstMethod). The problem is, I believe, that at the time of the definition, these functions are still unkown, as I get: [Vue warn]: Invalid handler for event "click": got undefined.
Is what I am trying to achieve even possible? Is there a downside with this strategy that I am overlooking?
Try to create one method, which will be working with all buttons
new Vue({
el: "#app",
data: {
myArray: [
{ value: 1, fn: "firstMethod" },
{ value: 2, fn: "secondMethod" },
],
},
methods: {
basicMethod(name) {
console.log(`'${name}' was executed.`);
if(name === 'firstMethod') {
//some logic, and so on for other methods if u need
}
},
},
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<template v-for="elem in myArray">
<button #click="basicMethod(elem.fn)"> <!-- Here is where I am stucked. -->
<!-- <button> -->
{{elem.value}}
</button>
</template>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#2/dist/vue.js"></script>
<script src="script.js"></script>
</body>
</html>
You can use a generic method provided with the function name the call this[ fn ]();.
But for security reasons, you might want these custom methods to be in an object, not just on the main this, so other methods can't be called.
Also, you want to check if the method exists before calling it.
It would look something like this:
new Vue({
el: "#app",
data: {
myArray: [
{ value: 1, fn: "firstMethod" },
{ value: 2, fn: "secondMethod" },
{ value: 3, fn: "nonExistingMethod" }, // Won't throw an error
{ value: 4, fn: "someImportantSecureMethod" }, // Won't be called
],
customMethods: {
firstMethod: function() {
console.log("'firstMethod' was executed.");
},
secondMethod: function() {
console.log("'secondMethod' was executed.");
},
},
},
methods: {
callCustomMethod(fn) {
// Make sure it exists
if (typeof this.customMethods[fn] === "function") {
// Only methods inside the customMethods object are available
this.customMethods[fn]();
}
},
someImportantSecureMethod() {
console.log('The method may not be exposed to dynamic calling!');
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<template v-for="elem in myArray">
<button #click="callCustomMethod(elem.fn)">
<!-- <button> -->
{{elem.value}}
</button>
</template>
</div>
As a side note:
You might also considering using custom events (see docs) for this. Using $emit('custom-event-name') as the v-on:click handler and have your custom methods as event listeners. (Makes it easy when you later might want to make the items into separate components.)

JS in blazor component

I am trying create an alert message in within a Blazor component. I have no idea how to do this. I am running ASP.NET Core 3.1 Blazor server-side. Here's what I've tried
Component function:
private async Task ShowAlert()
{
await JSRuntime.InvokeAsync<object>("ShowMsg");
}
Javascript Interop:
function ShowMsg() {
success = "Success!";
return success;
}
File host.cshtml:
<script src="~/BlazorInterop.js"></script>
#page "/"
<button #onclick="MessageBox">Show Message</button>
#code
{
[Inject] IJSRuntime JSRuntime { get; set; }
protected async Task MessageBox()
{
await JSRuntime.InvokeVoidAsync("exampleJsFunctions.ShowMsg",
"Hello Blazor");
}
}
Put the following script tag beneath <script src="_framework/blazor.server.js"></script> in the _Host.cshtml file, like this:
<script src="_framework/blazor.server.js"></script>
<script>
window.exampleJsFunctions =
{
ShowMsg: function (message) {
window.alert(message);
}
};
</script>

Get data attribute as object in Vue JS: returning [object object] even with JSON.stringify

I'm trying to use Vue JS to achieve this: click a button with day name as the value, show shop open/close time. my idea is to attach the value in a data-day attribute, and assign the value to the div which displays the open/close time. I have the following code:
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="OpeningTimes">
<input type="button" v-for="(value, key) in times" :value="key" :data-day="value" #click="showOpeningTimes">
<div ref="info"></div>
</div>
</body>
<script type="module">
var app = new Vue({
el: '#OpeningTimes',
data: {
times: {
'Monday': {
'09:00': 'open', // Open
'12:20': 'closed', // Close for Lunch
'13:30': 'open', // Return from Lunch
'17:00': 'closed' // Close for the day
},
'Tuesday': {
'09:00': 'open', // Open
'12:20': 'closed', // Close for Lunch
'13:30': 'open', // Return from Lunch
'17:00': 'closed' // Close for the day
}
}
},
methods: {
showOpeningTimes: function (e) {
this.$refs.info.innerText = e.target.dataset.day;
}
}
})
</script>
</html>
It seems working cause I can see the "key" in the day object has been bound to the button value, but when I tried to access the data-day attribute in the method, it keeps giving me [object object]. I tried JSON.stringify(e.target.dataset.day), still gave me "[object object]", how can I display the content in the object? What am I missing here?
I think using key to get value in your showOpeningTimes function would be a better aproach. Also you could pass key directly in click binding:
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="OpeningTimes">
<input type="button" v-for="(value, key) in times" :value="key" #click="showOpeningTimes(key)">
<div ref="info"></div>
</div>
</body>
<script type="module">
var app = new Vue({
el: '#OpeningTimes',
data: {
times: {
'Monday': {
'09:00': 'open', // Open
'12:20': 'closed', // Close for Lunch
'13:30': 'open', // Return from Lunch
'17:00': 'closed' // Close for the day
},
'Tuesday': {
'09:00': 'open', // Open
'12:20': 'closed', // Close for Lunch
'13:30': 'open', // Return from Lunch
'17:00': 'closed' // Close for the day
}
}
},
methods: {
showOpeningTimes: function (key) {
this.$refs.info.innerText = JSON.stringify(this.times[key]);
}
}
})
</script>
</html>
As it was indicated in another answer, you can also pass value directly. It all depends on whether you need the value itself or the key you will need for something else in your function.

Command ignored. Unknown target: undefined when using Lit-Element

I would like to know how to use google analytics event code in lit-element,
When try to debug, getting the error as Command ignored. Unknown target: undefined and Referance Error: ga not defined.
I have implemented event tracking in the component, but its not working getting the following error,
//index.ejs
<!doctype html>
<html>
<head>
<script src="/node_modules/#webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/#webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<title>LitElement Example</title>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-XXXX-X"></script>
</head>
<body>
<service></service>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', "UA-XXXX-X");
</script>
</body>
</html>
//lit-component.js
import { LitElement, html, css } from 'https://cdn.pika.dev/lit-element';
export class Services extends LitElement {
constructor() {
super();
}
handleGA(e){
ga('click', 'event', {
eventCategory: 'site',
eventAction: 'click',
eventLabel: e.target.id
});
}
render(){
<p>Welcome To My Site</p>
<button class="btn btn-primary" id="service" #click=${(e)=>this.hanldeGA(e)}>Click Here</button>
}
}
customElements.define('service', Services);

Simple webcomponent with Polymer 2.0: Updating values from dropdown not working

I tried to implement a very basic web component, for learning purposes, with Polymer 2.0. This component should only display the selected value of a select-element, in an h1-element.
<link rel="import" href="../bower_components/polymer/polymer-element.html">
<dom-module id="test-basis">
<template>
<style>
</style>
<container class="content__column">
<h2>Test component</h2>
<div class="table-select-line mono">
<label class="form-field-label" for="test-key">Tabelle:
<select class="form-field dropdown" id="tables">
<option value="printer">My printer</option>
<option value="color">My color</option>
</select>
</label>
</div>
<h1 id="abc">[[currentTable]]</h1>
</container>
</template>
<script>
/**
* #customElement
* #polymer
*/
class TestBasisData extends Polymer.Element {
constructor() {
super();
}
ready() {
super.ready();
}
static get is() {
return 'test-component';
}
connectedCallback() {
super.connectedCallback();
console.log("BasisData created...");
//create shadow dom
var shadow = this.shadowRoot;
this._createTablesListener();
this._loadData();
this.currentTable = 'printer';
}
static get properties() {
return {
data: Object,
selectedTable: {
type: String,
notify: true
},
currentTable:{
type: String,
notify: true
}
}
}
static get observers() {
return [
'_handleTableUpdate(currentTable)'
];
}
_createTablesListener(){
let tables = this.shadowRoot.querySelector("#tables");
tables.addEventListener("change", this._handleTableSelect);
}
_handleTableSelect(item) {
console.log("item: " + item.target.value);
this.currentTable = item.target.value;
}
_loadData(table = "printer") {
let p = new Promise((resolve, reject) => {
resolve(this._loadDataFromService());
});
p.then(json => {
this.data = json;
}).catch(e => console.log("Cannot load data from service.", e));
}
async _loadDataFromService() {
let p = await fetch("../data.json");
return await p.json();
}
}
window.customElements.define(TestComponent.is, BasTestisData);
</script>
</dom-module>
I'm able to get the selected value in the _handleTableSelect listener, but from there: I don't know how to update the currentTable field.
I cannot use this.querySelector("#abc"), as within the _handleTableSelect method: this only refers to the select-element. I also tried the observers, but they are never called.
So I'm somehow stuck on how to tie those ends together.
PS: I tried to work e.g. through Polymer iron-pages, to find out how this is done, but that is even more confusing; as they use e.g. this.items which is nowhere created, assigned or defined within the whole code. But that's probably another question.
try to put value="{{currentTable::input}}"
ex.
<select class="form-field dropdown" id="tables"
value="{{currentTable::input}}">

Categories