Adding deep nested object properties to Window in JS - javascript

I would like to clean up this block of code. Is there a way of setting deep object properties without using Lodash, Ramda or some horrible method that splits the object property string and loops through it?
export const initialiseBlackbox = (value = '') => {
if (window === undefined) {
window = { IGLOO }
}
if (window.IGLOO === undefined) {
window.IGLOO = {}
}
if (window.IGLOO.getBlackbox === undefined) {
window.IGLOO.getBlackbox = () => ({ blackbox: value })
}
}

Sure, but it's not pretty:
export const initialiseBlackbox = (value = '') =>
Object.assign(window.IGLOO || (window.IGLOO = {}),
{ getBlackbox: () => ({ backbox: value }) });

For a generic reusable function
const deepSet = (t, p, ...v) => v.length > 1 ? (t[p] = t[p] || {}) && deepSet(t[p], ...v) : t[p] = v[0]
To use
deepSet(window, "IGLOO", "getBlackbox", () => ({ blackbox: 'some value' }))

Related

Improve useEffect mutating array objects and get difference beetween

I need help to see if I can improve this code that I have, first I describe the final result that I hope and maybe this way you can find how to improve a little this useEffect:
I need to receive that data object, compare it with one previously stored in localstorage, and compare them to see if there is any new record in that object, if it finds new data, extract them, add the isNew property only to those key that found new and create a new array with the data I received and newArray that includes the modified data.
const localStorageKey = 'laboratoryData'
const oldData = localStorage.getItem(localStorageKey)
useEffect(() => {
if (data) {
if ([null, undefined].includes(oldData)) {
localStorage.setItem(localStorageKey, JSON.stringify(data.data))
} else {
const arrayIsEqual = (data, oldData) => {
data.data === oldData ||
(data.data.length === oldData.length &&
data.data.every(
(f, i) => f.id === oldData[i].id && f.name === oldData[i].name,
))
}
if (arrayIsEqual) {
localStorage.setItem(localStorageKey, JSON.stringify(data.data))
} else {
const diff = data.data
.filter((x) => !oldData.includes(x))
.concat(oldData.filter((x) => !data.data.includes(x)))
const newArray = diff.map((obj) => ({...obj, isNew: 'true'}))
const finalArray = {...data, ...newArray}
localStorage.setItem(localStorageKey, JSON.stringify(finalArray))
}
}
}
}, [data])
Maybe i`m duplicating some code or missing a custom hook with a better performance
Thanks!
without seeing your data and based on your code snippet, hopefully your data.data and oldData are always be arrays of objects that have always have key id and name.
const localStorageKey = 'laboratoryData';
const oldData = localStorage.getItem(localStorageKey);
useEffect(() => {
if (!data) return;
if (!oldData) {
localStorage.setItem(localStorageKey, JSON.stringify(data.data));
return;
}
if (data.data.length === oldData.length && data.data.every((d, i) => d.id === oldData[i].id && d.name === oldData[i].name)) return;
const newData = data.data.filter(d => oldData.findIndex(od => od.id === d.id && od.name === d.name) === -1).map(d => d["isNew"] = true);
const finalArray = oldData.map(od => od["isNew"] = false).concat(newData);
localStorage.setItem(localStorageKey, JSON.stringify(finalArray));
}, [data])

Is there an alternative to lodash .chain function?

How do I achieve this without lodash chain function?
const results = chain(sortedItems || allItems)
.filter((x) => this.filterItemsBySearchParam<T>(x, search))
.filter((x) => this.filterItemsByFacets<T>(x, facets))
.groupBy((x) => (groupBy ? [groupBy.split(',').map((path) => get(x, path))] : ''))
.map((filteredItems: any, key) => {
if (!isNaN(Number(limit))) {
filteredItems = [...filteredItems.slice(0, limit)];
}
return this.addData<T>(key, filteredItems.length, filteredItems);
})
.value();
I have tried using lodash flow, and some other ES6 functions, but none of them worked as expected. it could be that I'm not applying them correctly?
I have tried this:
const result = sortedItems || allItems
.filter((x) => this.filterItemsBySearchParam<T>(x, search))
.filter((x) => this.filterItemsByFacets<T>(x, facets))
.groupBy((x) => (groupBy ? [groupBy.split(',').map((path) => get(x, path))] : ''))
.map((filteredItems: any, key) => {
if (!isNaN(Number(limit))) {
filteredItems = [...filteredItems.slice(0, limit)];
}
return this.addData<T>(key, filteredItems.length, filteredItems);
});
But the groupBy keeps throwing an error: groupBy is not a type of T.
I tried flow from here: Better and more performant than lodash chain, but like I said, I could not get it to work. Is there a way to achieve this? By the way, the .filter, .map, and .groupBy are all built-in ts functions.
Try this:
const filteredItems = (sortedItems || allItems || []).filter(x => this.filterItemsBySearchParam<T>(x, search) && this.filterItemsByFacets<T>(x, facets));
const results = Object.entries(groupBy ? filteredItems.reduce((map, x) => {
const key = JSON.stringify((groupBy.match(/[^,]+/g) || []).map(path => get(x, path)));
(map[key] || (map[key] = [])).push(x);
return map;
}, {}) : {'': filteredItems}).map(([key, filteredItems]) => {
if (+limit > 0) {
filteredItems = filteredItems.slice(0, limit);
}
return this.addData<T>(key, filteredItems.length, filteredItems);
});
ask if you have any questions :)

Set state value using string path key from deeply nested object

I am trying to change a deeply nested value within an object using a string path of the key to access the object.
Setup:
const [payload, setPayload] = useState({
name: "test",
download: true,
downloadConfiguration: {
product: {
initialDownloadLength: 3,
}}})
When object value is changed:
const handleChange = (prop: any) => (e: React.ChangeEvent<HTMLInputElement>) => {
if (typeof e.target.value === "number") {
setPayload({ ...payload, [prop]: parseInt(e.target.value) });
}
if (typeof e.target.value === "string") {
setPayload({ ...payload, [prop]: e.target.value });
}
};
When I change values in the outermost layer of the object it works fine and updates the value e.g.
onChange={handleChange("name")}
but I am unable to access the initialDownloadLength key that is nested within product and downloadConfiguration.
I have tried "downloadConfiguration.product.initalDownloadLength" and using square brackets etc, but each time it creates a new object at the top most layer of the object.
You can use the solution in here dynamically-set-property-of-nested-object in your handleChange method like this:
// set method copied from the above link
function set(obj, path, value) {
var schema = obj;
var pList = path.split('.');
var len = pList.length;
for(var i = 0; i < len-1; i++) {
var elem = pList[i];
if( !schema[elem] ) schema[elem] = {}
schema = schema[elem];
}
schema[pList[len-1]] = value;
}
...
const handleChange = (prop) => (e) => {
let value;
if (typeof e.target.value === "number") {
value = parseInt(e.target.value);
}
if (typeof e.target.value === "string") {
value = e.target.value;
}
setPayload((prevState) => {
const newState = {...prevState};
set(newState, prop, value);
return newState;
})
};
....
onChange={handleChange("downloadConfiguration.product.initialDownloadLength")}

Making a recursive algorithm for converting URL string into JSON

I found myself having to process a string like:
foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar
Into:
{
"foo": "bar",
"foo1": {
"foo": "bar",
"foo2": {
"foo": "bar"
}
}
}
A real input example.
My best attempt is:
function parse(input) {
try {
const parsed = JSON.parse(input);
return parseJSON(parsed);
} catch (err) {
const decodedInput = decodeURIComponent(input);
if (input.includes("&") && input.includes("=")) {
return input.split("&").reduce((json, part) => {
const [key, value] = part.split("=");
const decodedValue = decodeURIComponent(value);
return { ...json, [key]: parsePrimitive(decodedValue) };
}, {});
}
return decodedInput;
}
}
function parsePrimitive(input) {
if (!isNaN(input)) {
return Number(input);
}
if (input === "true" || input === "false") {
return input === "true";
}
return parse(input);
}
function parseJSON(input) {
return Object.entries(input).reduce((json, [key, value]) => {
let object = {};
if (typeof value === "object") {
if (Array.isArray(value)) {
object[key] = value;
} else {
object[key] = parseJSON(value);
}
} else {
const decodedValue = decodeURIComponent(value);
if (decodedValue.includes("&") && decodedValue.includes("=")) {
object[key] = parse(decodedValue);
} else {
object[key] = parsePrimitive(decodedValue);
}
}
return { ...json, ...object };
}, {});
}
If you try to run it, you're supposed to call parse(input)
However, it does fail for certain inputs
How can I make the perfect recursive algorithm for this kind of problem?
Thanks!
You could take a recursive approach by checking the encoded = sign.
const getValues = string => string.split('&')
.reduce((r, pair) => {
let [key, value] = pair.split('=');
value = decodeURIComponent(value);
r[key] = value.includes('=')
? getValues(value)
: value;
return r;
}, {});
console.log(getValues('foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar'));
This seems to do it for your simple example and your more complex one (now updated to handle numbers and booleans):
const parse = (query) =>
query .startsWith ('{')
? JSON .parse (query)
: query .includes ('&') || query .includes ('=')
? Object .fromEntries (
query .split ('&')
.map (p => p .split ('='))
.map (([k, v]) => [k, parse (decodeURIComponent (v))])
)
: query .includes (',')
? query .split (',') .filter (Boolean) .map (parse)
: isFinite (query)
? Number (query)
: query .toLowerCase () == "true" || query .toLowerCase () == "false"
? query .toLowerCase () == "true"
: // else
query
const q = 'foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar'
console .log (parse(q))
console.log('fetching larger example...')
fetch ('https://gist.githubusercontent.com/avi12/cd1d6728445608d64475809a8ddccc9c/raw/030974baed3eaadb26d9378979b83b1d30a265a3/url-input-example.txt')
.then (res => res .text ())
.then (parse)
.then (console .log)
.as-console-wrapper {max-height: 100% !important; top: 0}
There are two parts that deserve attention.
First, this makes an assumption about commas: that they represent a separation between elements of an array. And, further, it assumes that empty strings aren't intended, turning
watermark=%2Chttps%3A%2F%2Fs.ytimg.com%2Fyts%2Fimg%2Fwatermark%2Fyoutube_watermark-vflHX6b6E.png
%2Chttps%3A%2F%2Fs.ytimg.com%2Fyts%2Fimg%2Fwatermark%2Fyoutube_hd_watermark-vflAzLcD6.png
into this:
watermark: [
"https://s.ytimg.com/yts/img/watermark/youtube_watermark-vflHX6b6E.png",
"https://s.ytimg.com/yts/img/watermark/youtube_hd_watermark-vflAzLcD6.png"
]
The original starts with an encoded comma (%2C), which would lead to an initial empty string, so we use .filter (Boolean) to remove it.
Second, the test for a string representing JSON is very naïve, only doing .startsWith ('{'). You can replace this with whatever you need, but it leads to a question of intentions. I'm not sure we can write this entirely generically in this manner.
Still, I think it's close. And the code is fairly clean.
I do have to wonder why, however. This much data is going to run into various url size limits. At this point, wouldn't putting this into a request body rather than url parameters make much more sense?
Node.js comes with a built-in "querystring" npm package utility, but here I used a better one called "qs". You can specify delimiters in an array instead of only using one with the former.
If you want to use the built-in "querystring" package, you need to remove the delimiter array when calling parse and do a check on the string to see what delimiter is used - the sample file you gave use a few different ones.
So try this:
const qs = require("qs");
let params = `foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar`;
const isObject = (param) => {
try {
let testProp = JSON.parse(param);
if (typeof testProp === "object" && testProp !== null) {
return true;
}
return false;
} catch (e) {
return false;
}
};
const isURL = (value) => {
try {
new URL(value);
} catch (e) {
return false;
}
return true;
};
const isQueryString = (value) => {
if (/[/&=]/.test(value) && !isURL(value)) {
return true;
} else {
return false;
}
};
const parseData = (data, parsed = false) => {
if (isQueryString(data) && !parsed) {
return parseData(qs.parse(data, { delimiter: /[;,/&]/ }), true);
} else if (isObject(data) || parsed) {
for (let propertyName in data) {
if (isObject(data[propertyName])) {
data[propertyName] = parseData(JSON.parse(data[propertyName]), true);
} else {
data[propertyName] = parseData(data[propertyName]);
}
}
return data;
} else {
return data;
}
};
let s = parseData(params);
console.log(JSON.stringify(s, null, 2));
I reworked the algorithm using Object.fromEntries(new URLSearchParams()).
function parse(query) {
try {
return JSON.parse(query);
} catch {
if (!isNaN(query)) {
return Number(query);
}
if (typeof query !== "string") {
const obj = {};
for (const queryKey in query) {
if (query.hasOwnProperty(queryKey)) {
obj[queryKey] = parse(query[queryKey]);
}
}
return obj;
}
if (!query) {
return "";
}
if (query.toLowerCase().match(/^(true|false)$/)) {
return query.toLowerCase() === "true";
}
const object = Object.fromEntries(new URLSearchParams(query));
const values = Object.values(object);
if (values.length === 1 && values[0] === "") {
return query;
}
return parse(object);
}
}
const q = 'foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar';
console.log(parse(q));
console.log('fetching larger example...');
fetch('https://gist.githubusercontent.com/avi12/cd1d6728445608d64475809a8ddccc9c/raw/030974baed3eaadb26d9378979b83b1d30a265a3/url-input-example.txt')
.then(response => response.text())
.then(parse)
.then(console.log);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Uncaught TypeError: ... is not a function

I would really appreciate another pair of eyes to review why this issue persists.
Basically, a component does some check with validation functions imported from another file
...
const {emailField, phoneFieldUS, postalCodeField} = validators;
class ReturnForm extends React.Component {
...
formHasAnyErrors = () => {
console.log("phoneFieldUS type ", typeof phoneFieldUS)
console.log("postalCodeField type ", typeof postalCodeField)
const zipCodeValid = postalCodeField(form.contactInfo.postalCode);
const emailValid = emailField(form.contactInfo.email);
const mobileValid = phoneFieldUS(form.contactInfo.mobile);
if (!zipCodeValid || !emailValid || !mobileValid) {
return true;
}
return Object.values(errors.contactInfo).find(el => !!el);
};
...
}
And the validators imported looks like this:
exports.default = exports.validators = exports.validatorFactory = exports.statuses = exports.layouts = void 0;
const validatorFactory = (test, pass = () => {}, fail = () => {}) => {
return value => {
const validValue = test(value);
if (validValue === true) {
pass();
} else {
fail();
}
return validValue;
};
};
exports.validatorFactory = validatorFactory;
const validators = {
emailField: validatorFactory(value => {
return /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+#[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(value); // eslint-disable-line max-len
}),
postalCodeField: validatorFactory(value => {
return ((value.length === 5 && /^\d+$/.test(value)) ^ (value.length === 6 && /\d/.test(value) && /[a-zA-Z]/.test(value)))
}),
emptyField: validatorFactory(value => value.length > 0 && !/^\s*$/.test(value)),
phoneFieldUS: validatorFactory(value => /^\(?(\d{3})\)?[-\. ]?(\d{3})[-\. ]?(\d{4})$/.test(value))
};
exports.validators = validators;
What I absolutely don't understand is
console.log("phoneFieldUS type ", typeof phoneFieldUS) // prints function
console.log("postalCodeField type ", typeof postalCodeField) // prints undefined
Hence every time the ReturnForm component is complaining about Uncaught TypeError: postalCodeField is not a function
Like how?????? They're both defined almost the same way?????? Please tell me if I'm missing something, any help appreciated!

Categories

Resources