Getting undefined when I expect otherwise - javascript

if you take a look at the following fiddle, you will see that I have created a class called: EventHandler which I then use as below (and in the fiddle):
/**
* Event Handler Class
*/
class EventHandler {
/**
* Core constructor.
*/
constructor() {
this.events = [];
this.beforeEvents = [];
this.afterEvents = [];
}
/**
* Register an event.
*
* #param {string} name
* #param {cb} callback
*/
register(name, cb) {
const event = {
name: name,
cb: cb
}
this.events.push(event);
}
/**
* Trigger an event based on name.
*
* #param {string} name
* #return {various}
*/
trigger(name) {
this.events.forEach((event) => {
if (event.name === name) {
return event.cb();
}
})
}
/**
* Get only the events.
*
* #return {array} events
*/
getEvents() {
return this.events;
}
}
const eventHandler = new EventHandler();
eventHandler.register('hello.world', () => {
return 'hello world';
});
alert(eventHandler.trigger('hello.world'));
When you run the fiddle, you get undefined, I expect to see 'hello world'.
Ideas?

In the trigger function you are returning undefined. Returning from inside a forEach callback does not return the parent function.
Try changing your code to something like this:
trigger(name) {
let msg = '';
this.events.forEach((event) => {
if (event.name === name) {
msg = event.cb();
}
});
return msg;
}

The problem is that you're alerting the response of eventHandler.trigger which doesn't actually return anything, hence the undefined. You could use something like Array.prototype.find to get find your event and then return the callbacks response like so:
trigger(name) {
let event = this.events.find(e => e.name === name);
if (event)
return e.cb();
return null;
}
A better method would be to store a key => value pair of event and callbacks, this way you won't have to find, you could just do something like:
trigger(event) {
return this.events[event] ? this.events[event]() : null;
}

Related

Why is my data property being updated by a computed property in Vuejs?

I have a computed property (filteredMeasurementsByAttribute) that is supposed to provide a filtered version of a data property (measurementsByAttribute). However, the changes on the computed property are also taking place on the data property.
The filter is using the data from selectedAttributes (which are the options selected in drop downs on the front end) and then returning the matching data in measurementsByAttribute. This filter is working properly. The issue is that when I run the clearAttribute method which clears the data from selectedAttributes (this part works successfully) the measurementsByAttribute property is the filtered version so I can't get the old data back.
In short, I want to keep an original version of the measurementsByAttribute property so that I clear the selectedAttributes property and have all the original data available for filteredMeasurementsByAttribute to reset the drop down forms.
I've tried saving the data in a regular javascript variable, measurementsByAttributeMaster, and then setting the measurementsByAttribute to the master. Somehow they all end up just having the filtered values.
I've tried changing the way that I'm looping through the data (e.g. using forEach instead of filter or map) just to see if that was causing it to edit the original data. No luck. I've left the "original" versions using the filter and map in commented out code so you can see both versions.
Any insight or help is very much appreciated.
/**
* Select Product
*/
function filterMeasurements(attribute, measurementsAvailable) {
var filteredMeasurements = [];
attribute.measurements.forEach(function(measurement) {
if (measurementsAvailable.includes(measurement.id.toString())) {
filteredMeasurements.push(measurement);
}
});
// return attribute.measurements.filter(function(measurement) {
// return measurementsAvailable.includes(measurement.id.toString());
// });
return filteredMeasurements;
}
/**
* Check if the part matches the selected Attributes
*
* #param {array} selectedAttributes Array of all the currently selected attributes
* #param {object} part The part we are checking
* #retrun {array} the fitlered selectedAttributes
*/
function checkPartAttributes(selectedAttributes, part) {
var partAttributes = JSON.parse(part.attributes_json);
// Loop through each selected attribute to ensure there aren't any conflicts
return selectedAttributes.every(function(measurementID, attributeID) {
// if no measurement has been selected, it is acceptable
if (measurementID == "") {
return true;
}
// if the part does have this attribute, it needs to match
if (attributeID in partAttributes) {
return partAttributes[attributeID] == measurementID;
}
console.log('here');
// If the part doesn't have this attribute, it is acceptable
return true;
});
}
// var SelectSpecifications = require('./components/SelectProduct/SelectSpecifications.vue');
var vm = new Vue({
el: '#select-product',
components: {
SelectSpecifications
},
data () {
return {
allParts: allParts,
measurementsByAttribute: measurementsByAttribute,
selectedAttributes: selectedAttributes,
}
},
computed: {
/**
* combine all the attributes from the matching parts
*
* #return {array} The filtered attributes
*/
filteredAttributes: function() {
var filteredAttributes = JSON.parse("{}");
console.log('filteredAttributes');
// loop through each matching part and create new array of the matching attributes
this.matchingParts.forEach(function(part) {
var partAttributes = JSON.parse(part.attributes_json);
for (attributeID in partAttributes) {
// Add to the index if already exists
if (attributeID in filteredAttributes) {
filteredAttributes[attributeID].push(partAttributes[attributeID]);
filteredAttributes[attributeID] = [...new Set(filteredAttributes[attributeID])];
// create the index if it doesn't already exist
} else {
var tempArr = [partAttributes[attributeID]];
filteredAttributes[attributeID] = tempArr;
}
}
});
return filteredAttributes;
},
/**
* filter the measurements by selected values
* #return {object} All the filtered measurements sorted by attribute
*/
filteredMeasurementsByAttribute: {
get: function() {
console.log('filteredMeasurementsByAttribute');
var selected = this.selectedAttributes;
var filteredMeasurementsByAttribute = [];
var filteredAttributesKeys = Object.keys(this.filteredAttributes);
var filteredAttributes = this.filteredAttributes;
this.measurementsByAttribute.forEach(function (attribute, index) {
console.log(attribute);
var tempAttribute = attribute;
// filteredMeasurementsByAttribute[index] = tempAttribute;
if (filteredAttributesKeys.includes(tempAttribute.id.toString())) {
var filteredMeasurements = filterMeasurements(tempAttribute, filteredAttributes[tempAttribute.id]);
tempAttribute.measurements = filteredMeasurements;
filteredMeasurementsByAttribute[index] = tempAttribute;
}
});
// return measurementsByAttribute.map(function(attribute) {
// if (filteredAttributesKeys.includes(attribute.id.toString())) {
// attribute.measurements = filterMeasurements(attribute, this.filteredAttributes[attribute.id]);
// return attribute;
// }
// }, this);
return filteredMeasurementsByAttribute;
},
set: function(newMeasurementsByAttribute) {
console.log('setter working!');
this.measurementsByAttribute = newMeasurementsByAttribute;
}
},
// returns matching parts depending on what attributes are selected
matchingParts: function() {
console.log('matchingParts');
return this.allParts.filter(checkPartAttributes.bind(this, this.selectedAttributes));
},
},
methods: {
clearAttribute: function(attributeID) {
this.$set(this.selectedAttributes, attributeID, "");
var tempArray = [];
this.filteredMeasurementsByAttribute = tempArray.concat(measurementsByAttributeMaster);
// this.selectedAttributes.splice(0);
},
kebabTitle: function(title) {
if (title == null) {
return '';
}
return title.replace(/([a-z])([A-Z])/g, "$1-$2")
.replace(/\s+/g, '-')
.toLowerCase();
},
}
});
In JavaScript, setting a variable to an object only copies the reference to the object. In data(), you've initialized this.measurementsByAttribute to a local variable named measurementsByAttribute, so any changes to this.measurementsByAttribute would affect the original variable.
The problem is observed in the getter for filteredMeasurementsByAttribute, which modifies this.measurementsByAttribute and thus the original variable:
filteredMeasurementsByAttribute: {
get: function() {
this.measurementsByAttribute.forEach(function (attribute, index) {
// tempAttribute refers to original object in `this.measurementsByAttribute`
var tempAttribute = attribute;
if (filteredAttributesKeys.includes(tempAttribute.id.toString())) {
// ❌ this changes original object's prop
tempAttribute.measurements = filteredMeasurements;
}
});
//...
}
}
One solution would be to:
Apply Array.prototype.filter() to this.measurementsByAttribute in order to get the measurements that match the filter selection.
and use Array.prototype.map() and the spread operator to shallow-clone the original objects with their measurements prop modified.
filteredMeasurementsByAttribute: {
get: function() {
const filteredAttributesKeys = Object.keys(this.filteredAttributes)
return this.measurementsByAttribute
/* 1 */ .filter(m => filteredAttributesKeys.includes(m.id.toString()))
/* 2 */ .map(m => ({
...m,
measurements: filterMeasurements(m, this.filteredAttributes[m.id])
}))
}
}
You are initiating the change to the original attribute when the computed prop is changed.
// ...
filteredMeasurementsByAttribute: {
// ...
set: function(newMeasurementsByAttribute) {
console.log('setter working!');
this.measurementsByAttribute = newMeasurementsByAttribute;
}
},
//...
I would just have the compute prop apply filter and return the result, but do not have any
set on the compute.
If you want the original attribute but have reactive attributes everywhere in the component, it may help to use a different variable.
const measurementsByAttributeClone = {...measurementsByAttribute};
// any changes to `measurementsByAttribute` will not impact `measurementsByAttributeClone`

Getting the value from a Map with zero or 1 entries?

I have a Map instance and it will either have zero or 1 entries in it. This is a demo:
class Todo {
constructor(
public id:string,
public title:string,
public description: string ) {}
}
const todo1 = new Todo('1', 't1', 'Sarah OConnor');
const m:Map<string, Todo> = new Map();
m.set('1', todo1);
const todoInMap= m.entries().next().value[1];
console.log("The todo in the map is: ", todoInMap);
This will log the todoInMap instance when it's in the Map, but if we m.clear() we will get an error.
This function can be used to avoid errors, I'm just wondering if Map API has a more simple way of doing this?
function getMapValue<E>(m:Map<any, E>) {
if (!m.entries().next().done) {
return m.entries().next().value;
}
return null;
}
Ended up putting adding a utility method to Slice Utilities
/**
* Gets the current active value from the `active`
* Map.
*
* This is used for the scenario where we are manging
* a single active instance. For example
* when selecting a book from a collection of books.
*
* The selected `Book` instance becomes the active value.
*
* #example
* const book:Book = getActiveValue(bookStore.active);
* #param m
*/
export function getActiveValue<E>(m:Map<any, E>) {
if (m.size) {
return m.entries().next().value[1];
}
return null;
}```
I think this option would be better:
function getMapValue<E>(m:Map<any, E>) {
if (m.size) {
return m.entries().next().value;
}
return null;
}
It's very weird to have size as a property, not length or size() but it how it is :)

How do I monitor changes to a specific field in Firebase database?

I have a database stored in Firebase and wanted to use the 'functions' to check whether a record in the database was > n days old. I understand the
onChange()
function in JavaScript could achieve this for me. Thank you!
In my project, we often use this pattern. Use onChange() to detect changes, and then a helper function to check if the change happened to a specific field. This helper function is for example called checkIfChangedHappenedToAtLeastOneOfProvidedPaths, and you can use it by providing the path to check, for example: checkIfChangedHappenedToAtLeastOneOfProvidedPaths(change.before.data(), change.after.data(), ['daysOld'])
checkIfChangedHappenedToAtLeastOneOfProvidedPaths uses 2 helper functions to achieve this (difference & getDifferenceCombine). Let me know what you think, here is the code (in typescript):
checkIfChangedHappenedToAtLeastOneOfProvidedPaths
/**
* The purpose of this function is to analyze the change between two objects.
* If a change happened to the prvoided path(s), return true, otherwise false,
* Usecase: Use this function if you only care about change at a certain path.
* Example: Run only update function if change happened to "stripe" object, at groups
* #export
* #param {*} oldObject
* #param {*} newObject
* #param {string[]} pathsToCheck
* #returns {boolean}
*/
export function checkIfChangedHappenedToAtLeastOneOfProvidedPaths(oldObject: object, newObject: object, pathsToCheck: string[]): boolean {
const combinedChanges = getDifferenceCombine(oldObject, newObject);
const flatObject = flat(combinedChanges);
let changeHappened = false;
pathsToCheck.forEach((path) => {
const inFlatObject = flatObject[path] != null;
const inCombinedObject = get(combinedChanges, path, null) != null;
if (inFlatObject || inCombinedObject) {
changeHappened = true;
}
});
return changeHappened;
}
Helper functions
export function difference(object: any, base: any): any {
return transform(object, (result, value, key) => {
if (base == null || object == null) {
return;
}
if (!isEqual(value, base[key])) {
result[key] = isObject(value) && isObject(base[key]) ? difference(value, base[key]) : value;
}
});
}
export function getDifferenceCombine(oldObject: object, newObject: object): object {
const diffObject = difference(newObject, oldObject);
const diffObject2 = difference(oldObject, newObject);
return {...diffObject, ...diffObject2};
}
Since everything updates in real time, you can query this specific field if you want to monitor it. For example, lets say you have a child node "createdAt" with a value of a Firebase.ServerValue.TIMESTAMP.
var n = 1452508000000; //random timestamp in milliseconds,
//since this is how firebase saves a server value timestamp or
//this can be Date.now() if you convert your createdAt value
//to whatever date format you prefer.
var ref = firebase.database().ref('path/to/your/data');
ref.orderByChild("createdAt").startAt(n).on("child_added", function(snapshot) {
console.log(snapshot.key);
//will list all keys that were created on or after the timestamp value
});
https://firebase.google.com/docs/reference/js/firebase.database.Query#startAt

Clarification on basics of OOP in JS and understanding variable scope in objects

I am trying to understand Magento 2's way of using knockoutJs and requireJs. This has brought me to a wider question of trying to understand how variable scope works within objects?
Look at this code picked up directly from Magento 2:
define([
'ko',
'jquery',
'Magento_Ui/js/lib/knockout/template/loader',
'mage/template'
], function (ko, $, templateLoader, template) {
'use strict';
var blockLoaderTemplatePath = 'ui/block-loader',
blockContentLoadingClass = '_block-content-loading',
blockLoader,
blockLoaderClass,
loaderImageHref;
templateLoader.loadTemplate(blockLoaderTemplatePath).done(function (blockLoaderTemplate) {
blockLoader = template($.trim(blockLoaderTemplate), {
loaderImageHref: loaderImageHref
});
blockLoader = $(blockLoader);
blockLoaderClass = '.' + blockLoader.attr('class');
});
/**
* Helper function to check if blockContentLoading class should be applied.
* #param {Object} element
* #returns {Boolean}
*/
function isLoadingClassRequired(element) {
var position = element.css('position');
if (position === 'absolute' || position === 'fixed') {
return false;
}
return true;
}
/**
* Add loader to block.
* #param {Object} element
*/
function addBlockLoader(element) {
element.find(':focus').blur();
element.find('input:disabled, select:disabled').addClass('_disabled');
element.find('input, select').prop('disabled', true);
if (isLoadingClassRequired(element)) {
element.addClass(blockContentLoadingClass);
}
element.append(blockLoader.clone());
}
/**
* Remove loader from block.
* #param {Object} element
*/
function removeBlockLoader(element) {
if (!element.has(blockLoaderClass).length) {
return;
}
element.find(blockLoaderClass).remove();
element.find('input:not("._disabled"), select:not("._disabled")').prop('disabled', false);
element.find('input:disabled, select:disabled').removeClass('_disabled');
element.removeClass(blockContentLoadingClass);
}
return function (loaderHref) {
loaderImageHref = loaderHref;
ko.bindingHandlers.blockLoader = {
/**
* Process loader for block
* #param {String} element
* #param {Boolean} displayBlockLoader
*/
update: function (element, displayBlockLoader) {
element = $(element);
if (ko.unwrap(displayBlockLoader())) {
addBlockLoader(element);
} else {
removeBlockLoader(element);
}
}
};
};
});
If someone could explain the OOP concept at play here and how these functions are part of a single class would be great.Also, any pointers on classes and objects in JS would be great!

JavaScript parameter value 'becomes' undefined when passed to constructor

I am creating a JavaScript component which I am creating instances of based on jQuery results however, the DOM element which I pass into the constructor, although populated when I step through the loop in the calling code, is undefined when passed to the constructor.
Here's my class and constructor...
export default class DeleteButton {
/**
* Creates an instance of DeleteButton.
*
* #param {object} element The DOM element to make into a delete button.
*
* #memberOf DeleteButton
*/
constructor(element) {
this.id = element.getAttribute("data-id");
if (!this.id) throw new Error("The 'data-id' attribute is required.");
this.deleteUri = element.getAttribute("data-delete-uri");
if (!this.deleteUri) throw new Error("The 'data-delete-uri' attribute is required.");
$(element).click(this.confirmRemove);
}
confirmRemove() { // does something }
}
and here's the calling code (This is a component manager that handles when to load components based on URLs / DOM state etc)...
export default class JsComponentManager {
constructor(onLoader) {
this._loader = onLoader;
this.select = {
deleteButtons: () => $(".js-delete-button")
}
this.result = 0;
}
bindComponents() {
const paths = new PathManager();
let $deleteButtons = this.select.deleteButtons()
if ($deleteButtons.length > 0) {
this._loader.add(this.renderDeleteButtons, $deleteButtons);
}
}
renderDeleteButtons($elements) {
$elements.each(() => {
document.DeleteButtons = document.DeleteButtons || [];
document.DeleteButtons.push(new DeleteButton(this));
});
}
}
This uses the following loader function to ensure that items are loaded...
/**
* Adds an event to the onload queue.
*
* #param {function} func The function to add to the queue.
* #param {any} param1 The first (optional) parameter to the function.
* #param {any} param2 The second (optional) parameter to the function.
*/
var AddLoadEvent = function (func, param1, param2) {
var oldonload = window.onload;
if (typeof window.onload !== "function") {
window.onload = () => { func(param1, param2); };
} else {
window.onload = () => {
if (oldonload) { oldonload(); }
func(param1, param2);
};
}
};
module.exports = {
add: AddLoadEvent
};
The onload management code seems to be running fine and, stepping through, code execustion is completely as expected until document.DeleteButtons.push(new DeleteButton(this)); - 'this' here is the DOM element, as I would expect, but as soon as the debugger steps into the controller the value is undefined.
Is this some odd scoping pain I've walked into?
renderDeleteButtons($elements) {
$elements.each(() => {
document.DeleteButtons = document.DeleteButtons || [];
document.DeleteButtons.push(new DeleteButton(this));
});
}
doesn't do what you think it does. jQuery relies on being able to set the this value of the callback function. But arrow functions don't have their own this, so jQuery cannot set the this value.
Inside the arrow function this will refer to whatever this refers to in renderDeleteButtons, which likely is an instance of JsComponentManager.
If you pass a function to another function and that function has to set the this value, you cannot use an arrow function. Use a function expression instead:
renderDeleteButtons($elements) {
$elements.each(function() {
document.DeleteButtons = document.DeleteButtons || [];
document.DeleteButtons.push(new DeleteButton(this));
});
}
See also: Arrow function vs function declaration / expressions: Are they equivalent / exchangeable?
Maybe this helps to demonstrate the difference between an arrow function and a function declaration/expression:
// our library:
function each(values, callback) {
for (var i = 0; i < values.length; i++) {
// we use `.call` to explicitly set the value of `this` inside `callback`
callback.call(values[i]);
}
}
// Function declaration/expression
var obj = {
someMethod() {
"use strict";
each([1,2,3], function() {
console.log('function declaration:', this);
});
},
};
// Because we use a function expression, `each` is able to set the value of `this`
// so this will log the values 1, 2, 3
obj.someMethod();
// Arrow function
obj = {
someMethod() {
each([1,2,3], () => {
"use strict";
console.log('arrow function:', this);
});
},
};
// `this` is resolved lexically; whatever `each` sets is ignored
// this will log the value of `obj` (the value of `this` inside `someMethod`)
obj.someMethod();
I've now got this working by abandonning jQuery.each (which seems to have serious scoping issues passing the element from the array to anything else due to the way it messes with 'this'). I solved this by using a JS forEach call instead as follows. Discovering jQuery's makeArray method was the key. This is similar to what I had started with originally but was banging my head against forEach not working on a jQuery object...
renderDeleteButtons($elements) {
$.makeArray($elements).forEach((el) => {
document.DeleteButtons = document.DeleteButtons || [];
document.DeleteButtons.push(new DeleteButton(el));
});
}
It also doesn't hurt my sensibilities by doing weird stuff with 'this' (for Felix)
See Felix's extra info on lexical scoping with 'this' at Arrow function vs function declaration / expressions: Are they equivalent / exchangeable?

Categories

Resources