I am trying to grab all the input elements that only exist after a boolean becomes true. So the div is wrapped around an *ngIf. I tried grabbing the elements using plain JavaScript, but it keeps returning empty. Here is my code:
test.component.html
<mat-checkbox (change)="toggleTest($event)">
Test check box
</mat-checkbox>
<div class="form-area" *ngIf="isTestChecked">
<input type="number">
<input type="text">
</div>
test.component.ts
isTestChecked = false;
toggleTest(event: any) {
this.isTestChecked = event.checked;
if (this.isTestChecked === true) {
const children = document.querySelectorAll('.form-area input');
console.log(children);
}
}
So the console.log always prints an empty array. However, if I manually type the query selector in the browser console after setting the boolean to true, then it returns both of the input elements.
What am I doing wrong? How come it won't get the input elements after they become added to the DOM? Any help would be appreciated!
Do not access the DOM this way. The Angular way is using ElementRef.
Take a look, too, at those threads that explain how to use:
Angular 2 #ViewChild in *ngIf
private contentPlaceholder: ElementRef;
#ViewChild('contentPlaceholder') set content(content: ElementRef) {
this.contentPlaceholder = content;
}
<div #contentPlaceholder *ngIf="isTestChecked">
<input type="number">
<input type="text">
</div>
Angular updates the DOM asynchronously, so you can't access the updated DOM elements in the same event loop. If you really need to manipulate the DOM directly, try add a timeout before the query selection.
this.isTestChecked = event.checked;
setTimeout(() => {
if (this.isTestChecked === true) {
const children = document.querySelectorAll('.form-area input');
console.log(children);
}
})
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 have following code:
export class ViTextfield extends LitElement
{
static get properties() {
return {
value: { type: String },
}
onChange(e) { console.log(e.target.value) }
render()
{
return html`
<div>
<div>
<input id="vi-input"
type="text"
value="${this.value}"
#change=${this.onChange} />
</div>
</div>
`
}
So everything is working fine for itself.
Now the developer who is using my component should be able to set the value thorugh the property e.g.
document.getElementById('myComponent').value = 1;
Now that brings 2 problems:
1) the value itself is not updated and 2) the onchange is not fired
Problem 1 I fixed with changing
value="${this.value}"
to
.value="${this.value}"
even I dont know why it is working (found this hack online).
But still the onChange is not firing...
The code doesn't work as you expect it to due to a couple of things:
Why does value not work when .value does?
lit-html uses the dot here to distinguish between assigning the value attribute or the property (value assigns the attribute and .value the property)
The easiest way of thinking about this is that attributes are those set on the HTML itself and properties are set to the Javascript object that represents that node.
Now, this is important in this case because the value property of an input element is only set from the attribute the when it's first rendered, if you want to change it later on you must set the property, not the attribute. Source
Why isn't the change event fired when the value property is changed from code?
This is because the change event is fired from the input only when the input's value changed due to some user input. Source
If you want to have some sort of side effect that gets fired not only when the user interacts when the input, but also when the property is modified in code, you probably want to use a setter. In your case that would look like this:
export class ViTextfield extends LitElement {
static get properties() {
return {
value: {
type: String
},
}
}
set value(value) {
const oldValue = this.value;
// do some side effect here
// set a pseudo-private property that will contain the actual value
this._value = value;
// call LitElement's requestUpdate so that a rerender is done if needed
this.requestUpdate('value', oldValue);
}
get value() {
// return the pseudo-private so that when vitextfield.value is accessed the correct value is returned
return this._value;
}
onChange(e) {
// update the property so that it keeps up with the input's current value
this.value = e.target.value;
}
render() {
return html `
<div>
<div>
<input id="vi-input"
type="text"
value="${this.value}"
#change=${this.onChange} />
</div>
</div>
`
}
}
For more info check this part of the LitElement guide
I am developing an app via Ionic Framework. I upgraded my app from Ionic 3 to Ionic 4. Now hyperlinks do not work anymore. The HTML content is loading dynamically based on the chosen page.
I've read I have to set new eventListeners for my clicks on my a elements.
I am trying:
ngOnInit()
{
this.objAnswerService.getAntworten(objFrage).then(arrAnswer =>
{
this.arrAnswers = arrAnswer;
}
}
ngAfterViewInit()
{
console.log('_enableDynamicHyperlinks');
this._enableDynamicHyperlinks();
}
private _enableDynamicHyperlinks()
{
let arrUrls = this._element.nativeElement.querySelectorAll('a');
console.log(JSON.stringify(arrUrls)); // No elements
console.log(arrUrls); // Correct elements
arrUrls.forEach((objUrl) =>{
console.log('do something'); // Never reached because 0 elements
});
}
answer.page.html
<div *ngIf="arrAnswers">
<div *ngFor="let objAnswer of arrAnswers"
class="antworten">
<div *ngIf="objAnswer"
class="antwort">
<div [innerHTML]="safeHtml(objAnswer.strText)"></div>
</div>
</div>
</div>
How can I wait for querySelectorAll() to find all existing elements?
since this.arrAnswers is initialized in a Promise it is undefined when the component fiirst loads. As a result of this <div *ngIf="arrAnswers"> evaluates to false and there are no elements for querySelectorAll to return on ngOnInit or ngAfterViewInit because they both gets called once during component lifecycle.
what you need is ngAfterViewChecked to be called when this.arrAnswers is initialized and dom is updated.
ngAfterViewChecked() {
console.log('_enableDynamicHyperlinks');
if (!this._element) return;
let arrUrls = this._element.nativeElement.querySelectorAll('p');
console.log("arrUrls.length:", arrUrls.length);
console.log("arrUrls:", arrUrls);
}
also do not forget to use { static: false } on #ViewChild as explained here.
here is a simple demo
The better way to handle this is using angular lifecycle hook.
If it doesn't work with ngOnInit() you can take a look at ngAfterViewInit()which respond after Angular initializes the component's views and child views / the view that a directive is in.
ngAfterViewInit() {
let arrUrls = this._element.nativeElement.querySelectorAll('a');
console.log(JSON.stringify(arrUrls)); // No elements
console.log(arrUrls); // Correct elements
arrUrls.forEach((objUrl) =>{
console.log('do smthng'); //Never reached because 0 elements
});
}
I'm new to React. This is absolutely baffling me. I can access the HTML element using event.target, and it shows value equal to some number, but every time I use event.target.value, I get undefined.
I've tried copying other code. I've tried Googling and searching Stack Overflow. I've tried using currentTarget instead of target and get the exact same results. This is extremely simple stuff; what is happening?.
class Calculator extends React.Component {
constructor() {
super();
this.state = {
input : "0"
}
this.handleNumber = this.handleNumber.bind(this);
}
handleNumber(event) {
event.preventDefault();
console.log(event.target);
console.log(event.target.value);
}
render() {
return (
<div id="calculatorLayout">
<div id="display">{this.state.input}</div>
<div className="calculatorButtons" id="zero" value="0" onClick={this.handleNumber}>0</div>
<div className="calculatorButtons" id="one" value="1" onClick={this.handleNumber}>1</div>
<div className="calculatorButtons" id="two" value="2" onClick={this.handleNumber}>2</div>
...etc.
I expect to get numbers when I access event.target.value, but every time I get undefined.
event.target returns:
<div class="calculatorButtons" id="zero" value="0">0</div>
event.target.value of the same element returns:
undefined
What am I doing wrong?
The .value property on DOM nodes is only for things like <input> or <textarea>. For other elements (like your <div>), you have to fetch the value via .getAttribute(). Thus
console.log(event.target.getAttribute("value"));
should do what you're asking.
I'm trying to use a jqueryui datepicker and I've almost got it working. I have a custom component that looks something like this (got some code online)
#inject(Element)
export class Checkbox {
#bindable({ defaultBindingMode: bindingMode.twoWay }) value = {};
#bindable datepicker;
#bindable label;
#bindable inputLabel;
element: Element = null;
constructor(element) {
this.element = element;
}
attached() {
$(".datepicker").datepicker();
$(".datepicker").datepicker()
.on("change", e => this.fireEvent(e.target, "input"));
}
createEvent(name) {
var event = document.createEvent("Event");
event.initEvent(name, true, true);
return event;
}
fireEvent(element, name) {
var event = this.createEvent(name);
element.dispatchEvent(event);
}
}
Here's the view-template:
<template>
<div class="checkbox">
<label><input type="checkbox" checked.bind="value.checked & twoWay">${label}</label>
</div>
<div show.bind="datepicker == 'true' && value.checked">
<label class="col-xs-1" style="padding: 0;">from</label>
<div class="col-xs-11">
<input class="form-control datepicker" type="text" value.bind="value.date & twoWay"/>
</div>
</div>
</template>
I'm using it like this:
<checkbox datepicker="true" label.bind="'Checkbox label'" value.bind="filters['MyObject']" change.delegate="AlertSomething()"></checkbox>
The issue is when I select a date with the datepicker. The change event isn't getting fired. If I manually type something in, it gets fired. It also gets fired when the checkbox gets changed. What am I missing here?
Chances are that the problem is you are firing a custom event on the input element in your custom element's template and not on the custom element itself.
The line
.on("change", e => this.fireEvent(e.target, "input"));
needs to become
.on("change", e => this.fireEvent(this.element, "change"));
Also, slightly off-topic, please note that using $(".datepicker") is likely to cause bugs in your application if this custom element is used in more than one place. Instead of doing that, you should use the ref custom attribute to create a property on the viewmodel for the element you want to pass to jQuery. So, in this case, you'd need to do this in your template:
<input class="form-control" ref="datepicker" type="text"
value.bind="value.date & twoWay"/>
And then change your VM code to use $(this.datepicker)