In https://codepen.io/kurt_cagle/pen/xqoMBG I find a function walkData with the following statement:
var buf = Object.keys(data).map((key)=>`<details><summary id="${key}" ${Object.keys(data[key]).map((subkey)=>{return subkey != 'children'?`data-${subkey}="${data[key][subkey]}"`:' '}).join(' ')}><img class="icon" src="${me.imageBase}${data[key].icon?data[key].icon:data[key].children?'Folder.png':'Item.png'}"> </img>${data[key].label}</summary>
${data[key].children?me.walkData(data[key].children):""}</details>`);
As an old-school-non-functional-dinosaur, I find that an unholy mess of map and interpolation which
is almost impossible to follow, debug, or modify. The codepen "Format Javascript" button helps, but
not enough. (I would post that here but the formating defeats me)
Can this be reworked to use longhand loops, intermediate variables, and shorter lines
that ideally do just one thing each.
As a side issue, there are four ` in that one line, can anyone explain to me how the js manages to parse that?
Here, us this as a starting point:
const buf = []
for (const key in data) {
const t = Object.keys(data[key])
.map(subkey => {
return subkey != 'children'
? `data-${subkey}="${data[key][subkey]}"`
: ' '
})
.join(' ')
const u = `<details><summary id="${key}" ${t}><img class="icon" src="${
me.imageBase
}${
data[key].icon
? data[key].icon
: data[key].children
? 'Folder.png'
: 'Item.png'
}"> </img>${data[key].label}</summary>
${data[key].children ? me.walkData(data[key].children) : ''}</details>`
buf.push(u)
}
I left your original code in-tact, I just added appropriate new-lines and indentation.
All I added was:
A target element to render to
Some fake data that I reverse engineered using the function
A fake walkData function that just returns "CHILDREN"...
Note: I could actually refine this code a bit and remove the closing tag for the <img> element, since those are unnecessary. The subkey map function is also a one-liner, so it can actually be converted to a lambda with not braces nor an explicit return; just like the key mapper outside of it.
Example
I little bit of formatting goes a long way. Template literals are great, because you do not have to deal with a mess of string concatenation.
const data = {
'a': { label: 'Directory A', children: [] },
'b': { label: 'File B', children: null }
}
const me = {
imageBase: '/',
walkData: (children) => 'CHILDREN'
}
const buf = Object.keys(data).map((key) => `
<details>
<summary
id="${key}" ${Object.keys(data[key]).map((subkey) => {
return subkey != 'children'
? `data-${subkey}="${data[key][subkey]}"` : ' '
}).join(' ')}>
<img class="icon"
src="${me.imageBase}${data[key].icon
? data[key].icon : data[key].children
? 'Folder.png' : 'Item.png'}">
</img>
${data[key].label}
</summary>
${data[key].children ? me.walkData(data[key].children) : ""}
</details>
`)
document.getElementById('target').innerHTML = buf.join('')
<div id="target"></div>
Update
Here is another version that has separate function calls and come comments. It is functionally the same as the previous code.
There are no explicit return statements, since all the lambdas are all one-liners.
const data = {
'a': { label: 'Directory A', children: [] },
'b': { label: 'File B', children: null }
}
const me = {
imageBase: '/',
walkData: children => 'CHILDREN'
}
const main = () => {
document.getElementById('target').innerHTML = render(data)
}
const render = data =>
Object.keys(data).map(key =>
renderDetails(key, data)).join('')
const renderDetails = (key, data) =>
`<details>
<summary id="${key}" ${renderDataAttributes(data[key])}>
<img class="icon" src="${renderImageSource(data[key])}">
</img>
${data[key].label}
</summary>
${data[key].children ? me.walkData(data[key].children) : ""}
</details>`
const renderDataAttributes = detail =>
Object.keys(detail)
.filter(key => key !== 'children') // Filter-out children
.map(key => `data-${key}="${detail[key]}"`) // Map to a data attr
.join(' ') // Join the values
const renderImageSource = detail =>
me.imageBase + (
detail.icon /* If the detail has an icon, */
? detail.icon /* Use the defined icon */
: detail.children /* Else, does it have children? */
? 'Folder.png' /* If so, it's directory */
: 'Item.png' /* Else, it's a file */
)
main()
<div id="target"></div>
This is what I have ended up with (some var names may be a bit cringeworthy).
const buf = []
for (const key in data) {
const dk = data[key];
const s = [];
for (const subkey in dk) {
if (subkey != "children") {
s.push(`data-${subkey}="${dk[subkey]}"`);
};
};
const sj = s.join(" ");
const icn = dk.icon ? dk.icon : dk.children ? "Folder.png" : "Item.png";
const src = me.imageBase + icn;
const children = dk.children ? me.walkData(dk.children) : "";
const bi = `<details>
<summary id="${key}" ${sj}>
<img class="icon" src="${src}"></img>
${dk.label}
</summary>
${children}
</details>`;
buf.push(bi);
};
Related
I have a list of navlinks. When I'm on a certain page, that navlink should be highlighted. I also want the page up (only) one level to have its navlink highlighted as well, so:
All pages: /blogs, blogs/careers, blogs/authors
Page: /blogs/author
Highlight: /blogs/author, /blogs
Page: /blogs/author/Lauren-Stephenson
Highlight: /blogs/author/Lauren-Stephenson, blogs/authors
Here's how I'm doing it:
import React from 'react';
const navlinks = ["/blogs", "blogs/careers", "blogs/authors"]
const currentPath = "/blogs/authors/Lauren-Stephenson"
export function App(props) {
return (
<div className='App'>
{navlinks.map((links) =>
<div style={{color: currentPath.includes(links) ? 'green' : 'white'}}>{links}</div>
)}
</div>
);
}
But my code not only highlights /blogs/Authors/, it also highlights /blogs, which is incorrect, because I only want the page up one level to be highlighted.
How can I do this?
currentPage: /blogs/Authors/Lauren-Steph
/blogs (HIGHLIGHTED) INCORRECTLY HIGHLIGHTED!
/blogs/careers
/blogs/authors (HIGHLIGHTED) Correct
currentPage: /blogs/Authors
/blogs
/blogs/careers
/blogs/authors (Correct)
currentPage: /blogs
/blogs (Correct)
/blogs/careers
/blogs/authors
So your highlighting logic seems to be: highlight:
current page
page one level above
Provided that link formats are consistent using absolute path, you can explicitly check against the 2 paths that should match instead of using string .includes()
const navLinks = ['/blog', '/blog/parent', '/blog/parent/child', '/blog/parent/child/grandchild', '/blog/parent/sibling', '/blog/other', '/blog/other/subpage']
const currentPath = '/blog/parent/child/grandchild'
const getHighlightedPaths = (currentPath) => {
return [
currentPath,
currentPath.substring(0, currentPath.lastIndexOf('/'))
]
}
const highlightedPaths = getHighlightedPaths(currentPath)
console.log('1. ', highlightedPaths)
console.log('2. Should highlight paths?', navLinks.map(link => link + ' - ' + (highlightedPaths.includes(link) ? 'yes' : 'no')))
Console output:
1. [
"/blog/parent/child/grandchild",
"/blog/parent/child"
]
2. Should highlight paths? [
"/blog - no",
"/blog/parent - no",
"/blog/parent/child - yes",
"/blog/parent/child/grandchild - yes",
"/blog/parent/sibling - no",
"/blog/other - no",
"/blog/other/subpage - no"
]
You got it the other way around. It's called: Array.protoype.includes() - Notice the "Array". Therefore use an Array:
links.includes(currentPath)
But... given your currentPath is somewhat a longer path string, you might want additionally check for if some of the navlinks startsWith a specific String:
const navlinks = ["/blogs", "/blogs/careers", "/blogs/authors"];
const checkNavPath = (path) => navlinks.some(nav => path.startsWith(nav));
console.log(checkNavPath("/blogs"));
console.log(checkNavPath("/blogs/authors"));
console.log(checkNavPath("/blogs/authors/Lauren-Stephenson"));
console.log(checkNavPath("/recipes/food/mashed-potato"));
Also, make sure your paths start all with /
The easiest way is to check if the path starts with the link and the link only has one extra "/".
Since you don't have proper data (not all of the links/paths start with "/") I had to write a function to normalize them.
const navlinks = ["/blogs", "blogs/careers", "blogs/authors"];
const normalizePath = (path) => path.replace(/^(?!\/)/, "/"); // add the starting "/" if it isn't there
const shouldHighlightLink = (currentPath, link) => {
currentPath = normalizePath(currentPath);
link = normalizePath(link);
return currentPath === link ||
(currentPath.startsWith(link + "/") &&
currentPath.split("/").length - 1 === link.split("/").length);
}
console.log(
"/blogs/authors/Lauren-Stephenson",
navlinks.reduce((acc, cur) => (
acc[normalizePath(cur)] = shouldHighlightLink("/blogs/authors/Lauren-Stephenson",
cur
), acc), {})
);
console.log(
"/blogs/authors",
navlinks.reduce((acc, cur) => (
acc[normalizePath(cur)] = shouldHighlightLink("/blogs/authors",
cur
), acc), {})
);
interface ICard {
content: string,
blanks: Array<{word: string, hidden: boolean}>
}
function processCards():Array<any>{
if (cards !==null ){
const text = cards.map((card,cardIndex)=>{
var content = card.content
card.blanks.map((blank,blankIndex)=>{
// replace content
const visibility = (blank.hidden)?'hidden':'visible'
const click_blank = <span className={visibility} onClick={()=>toggleBlank(cardIndex,blankIndex)}>{blank.word}</span>
content = content.replace(blank.word,click_blank)
})
return content
})
return text
} else {
return []
}
}
I have an array of objects of type ICard.
Whenever card.blanks.word appears in card.content, I want to wrap that word in tags that contain a className style AND an onClick parameter.
It seems like I can't just replace the string using content.replace like I've tried, as replace() does not like the fact I have JSX in the code.
Is there another way to approach this problem?
You need to construct a new ReactElement from the parts of string preceding and following each blank.word, with the new span stuck in the middle. You can do this by iteratively building an array and then returning it wrapped in <> (<React.Fragment>). Here's a (javascript) example:
export default function App() {
const toggleBlankPlaceholder = (cardIndex, blankIndex) => {};
const cardIndexPlaceholder = 0;
const blanks = [
{ word: "foo", hidden: true },
{ word: "bar", hidden: false },
];
const content = "hello foo from bar!";
const res = [content];
for (const [blankIndex, { word, hidden }] of blanks.entries()) {
const re = new RegExp(`(.*?)${word}(.*)`);
const match = res[res.length - 1].match(re);
if (match) {
const [, prefix, suffix] = match;
res[res.length - 1] = prefix;
const visibility = hidden ? "hidden" : "visible";
res.push(
<span
className={visibility}
onClick={() =>
toggleBlankPlaceholder(cardIndexPlaceholder, blankIndex)
}
>
{word}
</span>
);
res.push(suffix);
}
}
return <>{res}</>;
}
The returned value will be hello <span class="hidden">foo</span> from <span class="visible">bar</span>!
A couple of things:
In your example, you used map over card.blanks without consuming the value. Please don't do that! If you don't intend to use the new array that map creates, use forEach instead.
In my example, I assumed for simplicity that each entry in blanks occurs 0 or 1 times in order in content. Your usage of replace in your example code would only have replaced the first occurrence of blank.word (see the docs), though I'm not sure that's what you intended. Your code did not make an ordering assumption, so you'll need to rework my example code a little depending on the desired behavior.
So currently I'm going for the effect of trying to display formatted code or in this case a graphql operation in my react app. Triggered by state, I want to display or remove certain variables.
const testing = `query {
getBooks {
${value.bookId?"id" : ""}
${value.bookEntry?"entry" : ""}
${value.bookTitle?"title" : ""}
}
}`
...
<div className="output">
<pre>
<code>{testing}</code>
</pre>
</div>
I'm stuck rendering something that looks like this!
There's probably a better way to go around this, but it's worth asking!
Add a filter to remove whitespaces before rendering.
Check my solution below:
// Solution:
function clearQueryString(queryString){
const result = []
const splitted = queryString.split('\n')
console.log({splitted})
for (let index = 0; index < splitted.length; index++) {
const element = splitted[index];
// regex credit: https://stackoverflow.com/questions/10261986/how-to-detect-string-which-contains-only-spaces/50971250
if (!element.replace(/\s/g, '').length) {
console.log(`#${index} string only contains whitespace!`);
} else {
result.push(element)
}
}
return result.join('\n')
}
// Usage:
const value = {}
const testing = `query {
getBooks {
${value.bookId?"id" : ""}
${value.bookEntry?"entry" : ""}
${value.bookTitle?"title" : ""}
author
otherfield
}
}`
document.getElementById('codeOutput').innerHTML = clearQueryString(testing);
<div className="output">
<pre>
<code id="codeOutput"></code>
</pre>
</div>
I know there are more elegant ways to define a string with variables included,
but if I want to add a conditional in pre ES6 I would do..
var a = "text"+(conditional?a:b)+" more text"
now with template literals I would do..
let a;
if(conditional) a = `test${a} more text`;
else a = `test${b} more text`;
Is there a more elegant way to implement this conditional? is it possible to include if shortcut?
Use this:
let a = `test${conditional ? a : b} more text`;
You can also expand this a bit further and use placeholders inside such a conditional.
It really depends on the use case you have which is the most readable.
Some examples:
// example 1
const title = 'title 1';
const html1 = `${title ? `<h2>${title}</h2>` : '<h2>nothing 1</h2>'}`
document.getElementById('title-container-1').innerHTML = html1;
// example 2
const title2= 'title 2';
const html2 = `
${title2 ?
`<h2>${title2}</h2>` :
"<h2>nothing 2</h2>"
}`
document.getElementById('title-container-2').innerHTML = html2;
// example 3
const object = {
title: 'title 3'
};
const html3 = `
${(title => {
if (title) {
return `<h2>${title}</h2>`;
}
return '<h2>Nothing 3</h2>';
})(object.title)
}
`;
document.getElementById('title-container-3').innerHTML = html3;
<div id="title-container-1"></div>
<div id="title-container-2"></div>
<div id="title-container-3"></div>
(source)
If you have a simple condition to check, use the ternary operator, as it as been said, but if you have more complex or nested conditions, just call a function in this way:
const canDrink = (age, hasToDrive) => {
if (age >= 18 && !hasToDrive ) {
return 'Yeah, no problem'
} else if ( age >= 18 && hasToDrive ){
return 'Maybe not that much'
} else {
return 'Not at all'
}
}
console.log(`Can you drink tonight? ${ canDrink(21, true) }`) // Can you drink tonight? Maybe not that much
let t1 = "hey";
let t2 = "there";
console.log(`${t1}${t2 ? `(${t2})` : ''}`);
You can even do this:
${a || b}
Means: If a is null, use b.
Is it possible to have a similar concept of ngFor or ng-repeat in jQuery or vanilla JS?
Want to do something similar but not in angular way.
<div *ngFor="let inputSearch of searchBoxCount" class="col-sm-12">
<textarea name="{{inputSearch.name}}" id="{{inputSearch.name}}" rows="2" class="search-area-txt" attr.placeholder="{{placeholder}} {{inputSearch.name}}">
</textarea>
</div>
maybe use with the data="" attribute, whichever make sense.
If you want it in javascript you have to create elements dynamically in a loop. *ngFor and ngRepeat in angular are directives that contains template bindings that we can't have either in javascript or jquery. Once the directives encounters angular try to render the respective templates till the loop ends. Anyway, by javascript we can do like this.
If you want to append 6 divs to the body element with respective id, You have to do like below.
var array = ['first', 'second', 'third', .......so on];
for(var i=0; i<array.length; i++){
var elem = document.createElement("div");
elem.setAttribute('id', array[i]);
document.body.appendChild(elem);
}
We can't do it as we did in Angular with ngFor.
const anArray = [
{ tittle: "One", body: "Example 1" },
{ tittle: "Two", body: "Example 2" }
]
function ngForFunctionality() {
let value = '';
anArray.forEach((post) => {
value += `<li>${post.tittle} - ${post.body}</li>`;
});
document.body.innerHTML = value;
};
ngForFunctionality();
<body></body>
In JQuery, I'd do something like this:
HTML:
<div class="searchbox-container col-sm-12">
</div>
JavaScript:
var textAreas = $.map(searchBoxCount, function(inputSearch) {
return $("<textarea></textarea")
.attr({
name: inputSearch.name,
id: inputSearch.name
rows: "2",
placeholder: placeholder + " " + inputSearch.name
})
.addClass("search-area-txt");
});
$('.searchbox-container').append(textAreas);
There is no easy way to get a similar ngFor by vanilla. But it's possible!
My implementation (You can make it better using more regex):
HTML code:
<ul id="my-list">
<li *for="let contact of contactsList">
<span class="material-icons">{{ contact.icon }}</span>
<span>{{ contact.value }}</span>
</li>
</ul>
JS code for implement *for like Angular's *ngFor:
/**
* (*for) cicle implementation
* #param element the root element from HTML part where you want to apply (*for) cicle. This root element cannot to use (*for). Just children are allowed to use it.
* #returns void
*/
function htmlFor(element) {
return replace(element, element);
}
/**
* Recursive function for map all descendants and replace (*for) cicles with repetitions where items from array will be applied on HTML.
* #param rootElement The root element
* #param el The mapped root and its children
*/
function replace(rootElement, el) {
el.childNodes.forEach((childNode) => {
if (childNode instanceof HTMLElement) {
const child = childNode;
if (child.hasAttribute('*for')) {
const operation = child.getAttribute('*for');
const itemsCommand = /let (.*) of (.*)/.exec(operation);
if (itemsCommand?.length === 3) {
const listName = itemsCommand[2];
const itemName = itemsCommand[1];
if (rootElement[listName] && Array.isArray(rootElement[listName])) {
for (let item of rootElement[listName]) {
const clone = child.cloneNode(true);
clone.removeAttribute('*for');
const htmlParts = clone.innerHTML.split('}}');
htmlParts.forEach((part, i, parts) => {
const position = part.indexOf('{{');
if (position >= 0) {
const pathTovalue = part
.substring(position + 2)
.replace(/ /g, '');
const prefix = part.substring(0, position);
let finalValue = '';
let replaced = false;
if (pathTovalue.indexOf('.') >= 0) {
const byPatternSplitted = pathTovalue.split('.');
if (byPatternSplitted[0] === itemName) {
replaced = true;
for (const subpath of byPatternSplitted) {
finalValue = item[subpath];
}
}
} else {
if (pathTovalue === itemName) {
replaced = true;
finalValue = item;
}
}
parts[i] = prefix + finalValue;
}
return part;
});
clone.innerHTML = htmlParts.join('');
el.append(clone);
}
}
}
el.removeChild(child);
}
replace(rootElement, child);
}
});
}
Finally, in your component code:
document.addEventListener('DOMContentLoaded', () => {
const rootElement = document.getElementById('my-list');
rootElement.contactsList = [
{
icon: 'icon-name',
value: 'item value here',
},
...
];
htmlFor(rootElement);
});
Finished. You have a *for on your vanilla code.
If anyone wants to experiment a performance comparison of this *for with the Angular's *ngFor, please share it with me, as I'm curious.
Code on stackblitz