I have an html template that i'm using template literals for. The function looks like the below
// postCreator.js
export const blogPostMaker = ({ title, content, address, id }, deletePost) => {
const innerHtml = `
<blog-post>
<h1 class='title'>${title}</h1>
<p class='content'>${content}</p>
<p class='address'>${address}</p>
<button onclick='${() => deletePost(id)}'>Delete</button>
</blog-post>
`
return innerHtml
}
//Blog.js
postHelper.getSeriesOfPosts(10)
.then(resp => {
resp.forEach(post => (blogDiv.innerHTML += blogPostMaker(post, postHelper.deletePost)))
})
What I can't figure out is how to get the onclick to work. I've tried passing in an anon function in Blog.js to the postCreator as well with no luck.
Any ideas?
If you don't want to expose the event callback globally, you should attach it in the JS part of the code with addEventListener() :
// postCreator.js
export const blogPostMaker = ({ title, content, address, id }) =>
`
<blog-post id='${id}'>
<h1 class='title'>${title}</h1>
<p class='content'>${content}</p>
<p class='address'>${address}</p>
<button>Delete</button>
</blog-post>
`
//Blog.js
postHelper.getSeriesOfPosts(10).then(resp => {
resp.forEach(post => {
blogDiv.innerHTML += blogPostMaker(post)
blogDiv.querySelector(`blog-post[id="${post.id}"] > button`)
.addEventListener('click', () => postHelper.deletePost(post.id))
})
Note: it's not the most efficient way to do it but it keeps your file structure.
Instead I would create the <button> directly with createElement() and then add it to DOM with appendChild(), or I would use a DocumentFragment or a <template> in order to avoid querySelector() on all of the blogDiv.
If you absolutely want to use inline JS without exposing the helper you'll need to define your as a component (for example a Custom Element).
Miroslav Savovski's solution works but they did not explain why, so I thought I would add this answer with the reasoning behind that and a step-by-step of how it is actually working, and why the OP's solution was not working initially.
TLDR? Scroll to the last two code snippets.
With template literals when you put a function inside of them it executes that function, so let's say we have a simple function like this that just returns a string of 'blue':
const getBlueColor = () => 'blue'
And then we call this function inside of a template literal like this:
<div>${getBlueColor()}</div>
What happens is that the getBlueColor() is called right when that code is executed.
Now lets say we wanted to do this onclick instead like this:
<div onclick="${getBlueColor()}"></div>
What is happening here is that getBlueColor is not executed onclick, it's actually executed whenever this template literal is executed.
The way we fix this is to prevent the template literal from executing this function by simply removing the template literal:
<div onclick="getBlueColor()"></div>
But now let's say you want to pass in some parameters to a function like getOppositeColor(color) like this:
<div onclick="getOppositeColor(color)"><div>
This will not work because color won't be defined. So you need to wrap that with a template literal and in a string like this:
<div onclick="getOppositeColor('${color}')"><div>
Now with this you will be calling the onclick when the user clicks the button, and you will be passing it a string of the color like this:
getOppositeColor('blue')
const markUp = `
<button onclick="myFunction()">Click me</button>
`;
document.body.innerHTML = markUp;
window.myFunction = () => {
console.log('Button clicked');
};
Related
As a novice Javascript programmer, I'd like to create an html document presenting a feature very similar to the "reveal spoiler" used extensively in the Stack Exchange sites.
My document therefore has a few <div> elements, each of which has an onClick event listner which, when clicked, should reveal a hiddent text.
I already know that this can be accomplished, e.g., by
<div onclick="this.innerHTML='Revealed text'"> Click to reveal </div>
However, I would like the text to be revealed to be initially stored in a variable, say txt, which will be used when the element is clicked, as in:
<div onclick="this.innerHTML=txt"> Click to reveal </div>
Since there will be many such <div> elements, I certainly cannot store the text to be revealed in a global variable. My question is then:
Can I declare a variable that is local to a specific html element?
Yes you can. HTML elements are essentially just Javascript Objects with properties/keys and values. So you could add a key and a value to an HTML element object.
But you have to add it to the dataset object that sits inside the element, like this:
element.dataset.txt = 'This is a value' // Just like a JS object
A working example of what you want could look like this:
function addVariable() {
const myElement = document.querySelector('div')
myElement.dataset.txt = 'This is the extended data'
}
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
addVariable() // Calling this one immediately to add variables on initial load
<div onclick="showExtendedText(event)">Click to see more </div>
Or you could do it by adding the variable as a data-txt attribute right onto the element itself, in which case you don't even need the addVariable() function:
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
<div onclick="showExtendedText(event)" data-txt="This is the extended data">Click to see more </div>
To access the data/variable for the specific element that you clicked on, you have to pass the event object as a function paramater. This event object is given to you automatically by the click event (or any other event).
Elements have attributes, so you can put the information into an attribute. Custom attributes should usually be data attributes. On click, check if a parent element has one of the attributes you're interested in, and if so, toggle that parent.
document.addEventListener('click', (e) => {
const parent = e.target.closest('[data-spoiler]');
if (!parent) return;
const currentMarkup = parent.innerHTML;
parent.innerHTML = parent.dataset.spoiler;
parent.dataset.spoiler = currentMarkup;
});
<div data-spoiler="foo">text 1</div>
<div data-spoiler="bar">text 2</div>
That's the closest you'll get to "a variable that is local to a specific html element". To define the text completely in the JavaScript instead, one option is to use an array, then look up the clicked index of the spoiler element in the array.
const spoilerTexts = ['foo', 'bar'];
const spoilerTags = [...document.querySelectorAll('.spoiler')];
document.addEventListener('click', (e) => {
const parent = e.target.closest('.spoiler');
if (!parent) return;
const currentMarkup = parent.innerHTML;
const index = spoilerTags.indexOf(parent);
parent.innerHTML = spoilerTexts[index];
spoilerTexts[index] = currentMarkup;
});
<div class="spoiler">text 1</div>
<div class="spoiler">text 2</div>
There are also libraries that allow for that sort of thing, by associating each element with a component (a JavaScript function/object used by the library) and somehow sending a variable to that component.
// for example, with React
const SpoilerElement = ({ originalText, spoilerText }) => {
const [spoilerShown, setSpoilerShown] = React.useState(false);
return (
<div onClick={() => setSpoilerShown(!spoilerShown)}>
{ spoilerShown ? spoilerText : originalText }
</div>
);
};
const App = () => (
<div>
<SpoilerElement originalText="text 1" spoilerText="foo" />
<SpoilerElement originalText="text 2" spoilerText="bar" />
</div>
)
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>
Thanks everybody for your answers, which helped immensely! However, as a minimalist, I took all that I learned from you and came up with what I believe is the simplest possible code achieving my goal:
<div spoiler = "foo" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>
<div spoiler = "bar" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>
I am using Django templating engine and JavaScript. My HTML looks like this
<p class="content-card__address">{{ z.formatted_address|truncatewords:6 }}</p>
<div class="content-card-inner">
<p class="content-card__review">Отзывы ({{ z.post_relate.all.count }})</p>
<p class="content-card__phone">{{ z.international_phone_number }}</p>
<div class="div-shaddow"></div>
<p class="content-card__text">Показать</p>
</div>
Cards with text to be generated on the backend using a template engine. My JavaScript code only works on the first card and I need it to work on all cards. With JavaScript I add a class to the div elements. Here is my JavaScript
let call = document.querySelector('.content-card__text');
let divShadow = document.querySelector('.div-shaddow');
call.addEventListener('click', clickCall)
function clickCall() {
call.classList.add('visually-hidden');
divShadow.classList.add('visually-hidden');
}
This code returns you the first element in the DOM and you add click handlers only for it
document.querySelector('.content-card__text')
It will work for you:
const buttons = document.querySelectorAll('.content-card__text');
buttons.forEach(button => {
button.addEventListener('click', clickCall);
});
But please also note that you need to take this into account when working with .divShadow if this element is not alone on the page
Update: example based on your comment
const buttons = document.querySelectorAll('.content-card__text');
const divShadow = document.querySelectorAll('.div-shaddow');
buttons.forEach((button, index) => {
button.addEventListener('click', () => clickCall(index));
});
function clickCall(index) {
buttons[index].classList.add('visually-hidden');
divShadow[index].classList.add('visually-hidden');
}
I'm learning angular via youtube, but I'm trying to do something new, and I'm getting an error on that, my code is attached below, help me out.
I want to setAttribute like this div.setAttribute('(click)',"popUp($event)"); but I got error.
TypeScript
export class AppComponent {
createEl(){
console.time("timer");
for (let i = 0; i < 10; i++) {
let div = document.createElement("div");
div.textContent = `Hello, World! ${i}`;
div.setAttribute('(click)',"popUp($event)");
document.getElementById('divEl')?.appendChild(div);
};
console.timeEnd("timer");
}
HTML
<div id="divEl"></div>
<button (click)="createEl()">click me</button>
Error
This is not really the angular way of doing things. Try to avoid operations on document such as document.createElement.
A better way to achieve this would be to define what the repeating element would look like in the template and drive it from an array. That way we can keep the template doing display and the typescript doing processing, and Angular handling everything in between.
HTML
<div id="divEl">
<div *ngFor="let row of rows; index as i;" (click)="popUp($event)">
Hello, World! {{i}}
</div>
</div>
<button (click)="createEl()">click me</button>
Typescript
export class AppComponent {
rows: unknown[] = [];
createEl():void {
this.rows.push('something');
}
popUp(event:Event):void {}
}
More reading on loops: https://angular.io/api/common/NgForOf
That's right check below.
div.addEventListener('click', (e) => {
this.popUp(e);
});
Problem is you are trying to do angular stuff with pure javascript.
<div (click)="method()"> is angular.
In javascript you'd do someting like this <button onclick="myFunction()">Click me</button>
Other options are to use event handlers https://www.w3schools.com/js/js_htmldom_eventlistener.asp
Anyhow, angular doesn't recommend changes the DOM because then it won't recognize those changes. Here are multiple examples ho to properly change the dom
Correct way to do DOM Manipulation in Angular 2+
https://medium.com/#sardanalokesh/understanding-dom-manipulation-in-angular-2b0016a4ee5d
`
You can set the click event as shown below instead of using setAttribute
div.addEventListener('click', (e) => {
this.popUp(e);
});
(click) is not an html attribute, it is Angular event binding syntax
This syntax consists of a target event name within parentheses to the left of an equal sign, and a quoted template statement to the right.
You cannot use that with JavaScript. Use
div.onclick = popUp;
export class AppComponent {
createEl(){
console.time("timer");
for (let i = 0; i < 10; i++) {
let div = document.createElement("div");
div.textContent = `Hello, World! ${i}`;
div.addEventListener('click', (e) => {
this.popUp(e);
});
document.getElementById('divEl')?.appendChild(div);
};
console.timeEnd("timer");
}
I'm trying to calculate and set an element's max-height style programmatically based on the number of children it has. I have to do this on four separate elements, each with a different number of children, so I can't just create a single computed property. I already have the logic to calculate the max-height in the function, but I'm unable to pass an element from the template into a function.
I've tried the following solutions with no luck:
<div ref="div1" :style="{ maxHeight: getMaxHeight($refs.div1) }"></div>
This didn't work because $refs is not yet defined at the time I'm passing it into the function.
Trying to pass this or $event.target to getMaxHeight(). This didn't work either because this doesn't refer to the current element, and there was no event since I'm not in a v-on event handler.
The only other solution I can think of is creating four computed properties that each call getMaxHeight() with the $ref, but if I can handle it from a single function called with different params, it would be easier to maintain. If possible, I would like to pass the element itself from the template. Does anyone know of a way to do this, or a more elegant approach to solving this problem?
A cheap trick I learned with Vue is that if you require anything in the template that isnt loaded when the template is mounted is to just put a template with a v-if on it:
<template v-if="$refs">
<div ref="div1" :style="{ maxHeight: getMaxHeight($refs.div1) }"></div>
</template>
around it. This might look dirty at first, but the thing is, it does the job without loads of extra code and time spend and prevents the errors.
Also, a small improvement in code length on your expandable-function:
const expandable = el => el.style.maxHeight =
( el.classList.contains('expanded') ?
el.children.map(c=>c.scrollHeight).reduce((h1,h2)=>h1+h2)
: 0 ) + 'px';
I ended up creating a directive like was suggested. It tries to expand/compress when:
It's clicked
Its classes change
The element or its children update
Vue component:
<button #click="toggleAccordion($event.currentTarget.nextElementSibling)"></button>
<div #click="toggleAccordion($event.currentTarget)" v-accordion-toggle>
<myComponent v-for="data in dataList" :data="data"></myComponent>
</div>
.....
private toggleAccordion(elem: HTMLElement): void {
elem.classList.toggle("expanded");
}
Directive: Accordion.ts
const expandable = (el: HTMLElement) => el.style.maxHeight = (el.classList.contains("expanded") ?
[...el.children].map(c => c.scrollHeight).reduce((h1, h2) => h1 + h2) : "0") + "px";
Vue.directive("accordion-toggle", {
bind: (el: HTMLElement, binding: any, vnode: any) => {
el.onclick = ($event: any) => {
expandable($event.currentTarget) ; // When the element is clicked
};
// If the classes on the elem change, like another button adding .expanded class
const observer = new MutationObserver(() => expandable(el));
observer.observe(el, {
attributes: true,
attributeFilter: ["class"],
});
},
componentUpdated: (el: HTMLElement) => {
expandable(el); // When the component (or its children) update
}
});
Making a custom directive that operates directly on the div element would probably be your best shot. You could create a directive component like:
export default {
name: 'maxheight',
bind(el) {
const numberOfChildren = el.children.length;
// rest of your max height logic here
el.style.maxHeight = '100px';
}
}
Then just make sure to import the directive in the file you plan on using it, and add it to your div element:
<div ref="div1" maxheight></div>
I'm new to javascript and its ecosystem. I'm trying to build some components using mithril.js. My goal is to have a component that shows some properties and provides a couple of button for each of them. Just to learn about mithril.js and jsx. Here is what I did so far:
const m = require("mithril");
var Something = {
_increase: function(category) {
console.log("increase category: "+category);
},
_decrease: function(category) {
console.log("decrease category: "+category);
},
view: function(vnode) {
return <div>
{Object.keys(vnode.attrs.categories).map((category)=> {
return <div>
<label for={category}>{category}</label>
<input type="number" id={category} value={vnode.attrs.categories[category]} />
<button type="button" onclick="{this._increase(category)}">MORE</button>
<button type="button" onclick="{this._decrease(category)}">LESS</button>
</div>
})}
</div>
}
}
export default Something;
Well, component seems to work fine, node doesn't complain and labels and buttons and fields are displayed on page, but, when I click on a button, nothing happen. It looks like event isn't fired. What's wrong?
Two things: (1) I think you should just put the function into the onclick handler braces instead of encoding the function in a string. (2) It looks like you're immediately invoking the function, not declaring that the onclick handler is a function that uses the category argument. Try passing in an anonymous function with no arguments, that way you when the onclick event is fired it can take in the category as a parameter:
onclick={() => this._increase(category)}
onclick={() => this._decrease(category)}