Cannot access dynamically created element in Angular component - javascript

In my app using Angular 4, I am trying to dynamically create an element and get its position on button click (Both actions happen in one button click). However, since the element is not created when accessing its position, the element returns null.
This is the method executed on click:
addNodeWithEdge(node){
this.addNewNode();
this.currentNode = this.nodeList.length-1;
var elementGenerated = document.getElementById(this.currentNode); //returns null
var to = this.getCentreOfElement(elementGenerated);
}
addNewNode() {
let newNode = new SntNode("", []);
this.nodeList.push(newNode);
}
getCentreOfElement(el) {
var xPos = el.offsetLeft + el.offsetWidth/2;
var yPos = el.offsetTop - el.offsetHeight/2;
return {
x: xPos,
y: yPos
};
}
And on the view:
<a (click)="addNewNode()"></a>
<ng-container *ngFor="let node of nodeList; index as n">
<div class="node-block" id={{n}}>
<div class="node-cmp" (click)="onSelectNode(n)">
<a class="a-tag-circle">{{node.name}}</a>
</div>
<div class="node-opts"></div>
</div>
</ng-container>
A node is added to nodeList when the addNewNode() is triggered. In the view, all nodes in the nodeList are added to the view in a for loop (*ngFor). This addition to the view is not executed until the function addNewNodeWithEdge() is fully executed. Since document.getElementById is executed before the view is refreshed, it returns null.
How do I solve this? Should I refresh the component after creating the element? If so how can I refresh component in Angular? Any help is much appreciated.

Here, in below given line you need to provide ID of element instead of index.
var elementGenerated = document.getElementById(this.secondNode); //returns null

Related

Sorting array kills javascript instance on element (i.e. wysiwyg-editor)

I've got a CMS-like feature that has an article with multiple particles (called blocks). A particle can be a either a rich text field or a table. Based on the Block's discr attribute, a Quill or Handsontable instance should be initiated.
This works perfectly, until I reorder the blocks. When I've got a Quill instance and a Handsontable instance, after reordering them, the Quill gets a context menu from the Handsontable and the Quill instance gets a toolbar.
I'm new to Vue.js, but I already understand that happens. I've read List Rendering Caveats and Why isn’t the DOM updating?. The two div.chapterblock elements don't get reordered (like a jQuery-like application probably would do), but only their content changes. When I use the inspector, I see the .chapterblock#id and it's content changing, not moving. The (Quill/Handsontable/whatever) instance is bound to a specific DOM element and stays bound to the element, even if it changes.
But what I don't (yet) understand is how to solve the problem. How can I reorder items and keep the Quill/Handsontable instance on the right elements? Destroying and re-initializing the instances doesn't feel right.
My template:
<div class="chapterblock" v-for="(block, index) in blocks" v-bind:data-id="block.id">
<template v-if="block.discr == 'html'">
<div class="quill" v-html="block.content"></div>
</template>
<template v-if="block.discr == 'table'">
<script type="application/json" v-html="block.content"></script>
<div v-bind:id="'handsontable_' + block.id" class="handsontable-wrapper"></div>
</template>
<button v-if="index !== 0" v-on:click="move(block, 'up')">up</button>
<button v-if="index !== 1" v-on:click="move(block, 'down')">down</button>
</div>
Vue instance:
return new Vue({
//...
computed: {
blocks: function () {
return this.chapter.blocks.sort(function compare (a, b) {
if (a.position < b.position) {
return -1
}
if (a.position > b.position) {
return 1
}
return 0
})
}
},
methods: {
move: function (block, direction) {
if (direction === 'up') {
block.position = block.position - 1
} else if (direction === 'down') {
block.position = block.position + 1
}
// fetch to save position
}
}
Use the key attribute on the v-for loop to reorder the elements, instead of replacing their contents:
From https://v2.vuejs.org/v2/guide/list.html#key:
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item.

Using Javascript loop to create multiple HTML elements

I would like to use a javascript loop to create multiple HTML wrapper elements and insert JSON response API data into some of the elements (image, title, url, etc...).
Is this something I need to go line-by-line with?
<a class="scoreboard-video-outer-link" href="">
<div class="scoreboard-video--wrapper">
<div class="scoreboard-video--thumbnail">
<img src="http://via.placeholder.com/350x150">
</div>
<div class="scoreboard-video--info">
<div class="scoreboard-video--title">Pelicans # Bulls Postgame: E'Twaun Moore 10-8-17</div>
</div>
</div>
</a>
What I am trying:
var link = document.createElement('a');
document.getElementsByTagName("a")[0].setAttribute("class", "scoreboard-video-outer-link");
document.getElementsByTagName("a")[0].setAttribute("url", "google.com");
mainWrapper.appendChild(link);
var videoWrapper= document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video-outer-link");
link.appendChild(videoWrapper);
var videoThumbnailWrapper = document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video--thumbnail");
videoWrapper.appendChild(videoThumbnailWrapper);
var videoImage = document.createElement('img');
document.getElementsByTagName("img")[0].setAttribute("src", "url-of-image-from-api");
videoThumbnailWrapper.appendChild(videoImage);
Then I basically repeat that process for all nested HTML elements.
Create A-tag
Create class and href attributes for A-tag
Append class name and url to attributes
Append A-tag to main wrapper
Create DIV
Create class attributes for DIV
Append DIV to newly appended A-tag
I'd greatly appreciate it if you could enlighten me on the best way to do what I'm trying to explain here? Seems like it would get very messy.
Here's my answer. It's notated. In order to see the effects in the snippet you'll have to go into your developers console to either inspect the wrapper element or look at your developers console log.
We basically create some helper methods to easily create elements and append them to the DOM - it's really not as hard as it seems. This should also leave you in an easy place to append JSON retrieved Objects as properties to your elements!
Here's a Basic Version to give you the gist of what's happening and how to use it
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com" });
let div = create("div", { id: "myDiv" });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com", textContent: "this text is a Link in the div" });
let div = create("div", { id: "myDiv", textContent: "this text is in the div! " });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
div {
border: 3px solid black;
padding: 5px;
}
<div id="mainWrapper"></div>
Here is how to do specifically what you asked with more thoroughly notated code.
//get main wrapper
let mainWrapper = document.getElementById("mainWrapper");
//make a function to easily create elements
//function takes a tagName and an optional object for property values
//using Object.assign we can make tailored elements quickly.
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//document.appendChild is great except
//it doesn't offer easy stackability
//The reason for this is that it always returns the appended child element
//we create a function that appends from Parent to Child
//and returns the compiled element(The Parent).
//Since we are ALWAYS returning the parent(regardles of if the child is specified)
//we can recursively call this function to great effect
//(you'll see this further down)
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//these are the elements you wanted to append
//notice how easy it is to make them!
//FYI when adding classes directly to an HTMLElement
//the property to assign a value to is className -- NOT class
//this is a common mistake, so no big deal!
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
//here's where the recursion comes in:
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//keep in mind that it might be easiest to read the ac functions backwards
//the logic is this:
//Append videoImage to videoThumbnailWrapper
//Append (videoImage+videoThumbnailWrapper) to videoWrapper
//Append (videoWrapper+videoImage+videoThumbnailWrapper) to link
//Append (link+videoWrapper+videoImage+videoThumbnailWrapper) to mainWrapper
let mainWrapper = document.getElementById('mainWrapper');
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//pretty fancy.
//This is just to show the output in the log,
//feel free to just open up the developer console and look at the mainWrapper element.
console.dir(mainWrapper);
<div id="mainWrapper"></div>
Short version
Markup.js's loops.
Long version
You will find many solutions that work for this problem. But that may not be the point. The point is: is it right? And you may using the wrong tool for the problem.
I've worked with code that did similar things. I did not write it, but I had to work with it. You'll find that code like that quickly becomes very difficult to manage. You may think: "Oh, but I know what it's supposed to do. Once it's done, I won't change it."
Code falls into two categories:
Code you stop using and you therefore don't need to change.
Code you keep using and therefore that you will need to change.
So, "does it work?" is not the right question. There are many questions, but some of them are: "Will I be able to maintain this? Is it easy to read? If I change one part, does it only change the part I need to change or does it also change something else I don't mean to change?"
What I'm getting at here is that you should use a templating library. There are many for JavaScript.
In general, you should use a whole JavaScript application framework. There are three main ones nowadays:
ReactJS
Vue.js
Angular 2
For the sake of honesty, note I don't follow my own advice and still use Angular. (The original, not Angular 2.) But this is a steep learning curve. There are a lot of libraries that also include templating abilities.
But you've obviously got a whole project already set up and you want to just plug in a template into existing JavaScript code. You probably want a template language that does its thing and stays out of the way. When I started, I wanted that too. I used Markup.js . It's small, it's simple and it does what you want in this post.
https://github.com/adammark/Markup.js/
It's a first step. I think its loops feature are what you need. Start with that and work your way to a full framework in time.
Take a look at this - [underscore._template]
It is very tiny, and useful in this situation.
(https://www.npmjs.com/package/underscore.template).
const targetElement = document.querySelector('#target')
// Define your template
const template = UnderscoreTemplate(
'<a class="<%- link.className %>" href="<%- link.url %>">\
<div class="<%- wrapper.className %>">\
<div class="<%- thumbnail.className %>">\
<img src="<%- thumbnail.image %>">\
</div>\
<div class="<%- info.className %>">\
<div class="<%- info.title.className %>"><%- info.title.text %></div>\
</div>\
</div>\
</a>');
// Define values for template
const obj = {
link: {
className: 'scoreboard-video-outer-link',
url: '#someurl'
},
wrapper: {
className: 'scoreboard-video--wrapper'
},
thumbnail: {
className: 'scoreboard-video--thumbnail',
image: 'http://via.placeholder.com/350x150'
},
info: {
className: 'scoreboard-video--info',
title: {
className: 'scoreboard-video--title',
text: 'Pelicans # Bulls Postgame: E`Twaun Moore 10-8-17'
}
}
};
// Build template, and set innerHTML to output element.
targetElement.innerHTML = template(obj)
// And of course you can go into forEach loop here like
const arr = [obj, obj, obj]; // Create array from our object
arr.forEach(item => targetElement.innerHTML += template(item))
<script src="https://unpkg.com/underscore.template#0.1.7/dist/underscore.template.js"></script>
<div id="target">qq</div>

SAPUI5 TreeTable's getRows method returns empty array on the first call

I am trying to build an SAPUI5 application using TreeTable and I'm facing some problems to use its methods.
In my app, I have a button which triggers this method.
onChangeViewContext: function(oEvent) {
.........
.........
var aViewContext = oContext.oModel.getProperty(sPath + "/ViewContext");
var aDataModel = oContext.oModel.getProperty("/ApplicationCollection/" + sAppId + "/DataModel");
var oStructure = this._createParentChildStructure(aDataModel);
var oTreeModel = this.getView().getModel("treeModel");
oTreeModel.setData(oStructure);
this._oViewDetailLine = oSource.getParent().getParent().getParent();
this._oViewDetailLine.setVisible(false);
this.byId("idSelectElementsPanel").setVisible(true);
this._setSelectedItems(aViewContext, oTree);
}
What I'm trying to do here is only bind the rows with my treeModel, get tree table object and send it to my _setSelectedItems method which below.
_setSelectedItems: function(aViewContext, oTree) {
oTree.clearSelection();
var sElementName;
var aSelectedIndices = [];
var aElements = [];
var aRows = oTree.getRows();
aRows.forEach(function(row) {
if (row._oNodeState !== undefined) {
aElements.push(row.getCells()[0].getText());
}
});
I need to get rows array here because I will use it for setting selected items of tree table. The problem is when "onChangeViewContext" triggered, oTable.getRows() returns an empty array. But when I click cancel button (which just hides my tree table, nothing more) and then trigger "onChangeViewContext" function again, I can get the rows array completely.
Even on the first call when I try to get table's model, I can get the treeModel and its data correctly.
I've tried to refresh bindings, aggregations etc. But no luck.
By the way, I'm using row binding in my xml view like this :
<t:TreeTable id="idSelectElementsTree" rows="{path: 'treeModel>/'}" selectionMode="MultiToggle" enableSelectAll="false"
rowSelectionChange="onSelectElement">
I'm really drowning here so any any help would be appreciated.
Edit : rest of the setSelectedIndexes function :
aViewContext.forEach(function(name) {
sElementName = name;
if (aElements.indexOf(sElementName) !== -1) {
aSelectedIndices.push(aElements.indexOf(sElementName));
}
});
aSelectedIndices.forEach(function(idx) {
if (oTree.getRows()[idx]._bHasChildren) {
oTree.expand(idx);
}
oTree.addSelectionInterval(idx, idx);
});
What could help here is to add an event rowsUpdated="onRowsUpdated" to the table in the XML view. This event is triggered after the table has been loaded and will hence provide you with the data via;
this.getView().byId("sTableId").getRows();
The difference to your approach is that the event would not be triggered by the press of a button but automatically, as the table is rendered. You can then also use this function to trigger another one as per your use case.

React/Flux: strange error while rendering

I am getting this error while dynamically rendering a component in react
findComponentRoot(..., .0.1.1.0.1.0.5.0.1:4): Unable to find element.
This probably means the DOM was unexpectedly mutated (e.g., by the
browser), usually due to forgetting a when using tables,
nesting tags like <form>, <p>, or <a>, or using non-SVG elements in an
<svg> parent. Try inspecting the child nodes of the element with React
ID
I have checked out the markup and it seems fine. Sometimes the error changes on element, I mean, sometimes it appears to affect a certain element sometimes it appears on other elements.
Here is my render object:
render: function(){
var _slots = function(id, h, handleClick){
//react way to do this
var daysforSlot = this.props.workingDays.map(function(day, i){
var spanActive = this.state.appointments.map(function(appointment){
var _activeClass = null;
var _s = null;
if(h.hour+day === appointment.hour+appointment.day){
_activeClass = 'active'
}
if(_activeClass){
_s = <span className='active'>{appointment.patient.name}</span>
}
return ( {_s} )
});
return (
<td id={h.hour+day} onClick={handleClick.bind(null, {hour: h.hour, day:i})}>
{spanActive}
</td>
)
}.bind(this));
return(
<div>
<td>{h.text}</td>
<span>{daysforSlot}</span>
</div>
)
}.bind(this);
//creates the working hours for the days and bind the click
//handler to the hour slots
var _schedule = this.props.workingHours.map(function(h, i){
return (
<tr>
<span>{_slots(i, h, this.handleClick)}</span>
</tr>
)
}.bind(this));
return(
<span>
{_schedule}
</span>
)
}
Thanks in advance for any comments.
The html you're outputting is invalid. You have a td directly inside of a div - they must be the direct child of tr. Also you have a span as the child of tr, which is not valid.

How to pass an angular scope object to a newly created DOM

I have an angular object(model) created in controller.
$scope.deletedres = [];
I am trying to append a new DOM to the html body along with the angular object(modal) as shown below.
$('body').append('<span>'+restaurant.name+' have been removed.</span><a class="btn-flat yellow-text" href="#"; ng-click="addRestaurant($scope.deletedres[$scope.deletedres.length-1])">Undo<a>');
When I view it with google chrome dev tools, it shows that $scope.deletedres as [object Object] and addRestaurant() function receive nothing.
Can anyone enlighten me on this issue?
Is there any other ways to reference/pass an angular modal to a newly created DOM?
The way you are adding the DOM is wrong. Add the html inside the scope of controller. Use ng-show to show or hide the dom. JQuery is not necessary.
Example
<span ng-show="restaurant.delete">{{restaurant.name}} have been removed.</span>
<a class="btn-flat yellow-text" href="#"; ng-click="restaurant.delete=false">Undo<a>
This is just an example you can improve on
When you use jQuery to add fragments of HTML there is no way for angular to parse it. Thats the reason your angular code inside the html is working.
You can use $compile service.
var html = '<span>{{restaurant.name}} have been removed.</span><a class="btn-flat yellow-text" href="#"; ng-click="addRestaurant(deletedres[deletedres.length-1])">Undo</a>';
var linkFn = $compile(html);
var content = linkFn(scope);
$('body').append(content);
Still as noted by Harish it's wrong. All manipulations with DOM must be done in directives. You can create directive that will be responsible for showing some message (or custom html template) on button click.
Dmitry Bezzubenkov is right. If you want to manipulate DOM with Angular, you should do that with your custom directive, rather than do that in your controller directly. And to do so, you may refer to $compile service. Here's the official document for that.
However, in your case, I believe what you actually want to do is remove the item from a list while enable the item to be recovered from deletion. In this sense, you may try this approach with Angular:
In your controller, create a array for original restaurant list and another for deleted restaurant list. (Let's say, $scope.res and $scope.deletedres)
Register a delete function and bind that to delete button with ng-click. In this function, you will remove the item from $scope.res and then push the item to $scope.deletedres
Register another undo function. Basically do the same thing as delete function but in reverse. That is, move a item from $scope.deletedres to $scope.res. Bind this item to UNDO text in your message box.
use ng-repeat to show your $scope.res list in the main container, and $scope.deletedres in the message box container.
Thanks to the 2-way data binding from Angular, now you can delete or undo the action by clicking to different item.
It would be something like this:
angular
.module('modelTest', [])
.controller('MainCtrl', function($scope) {
$scope.res = [
{id: 1, name: 'Restaurant1'},
{id: 2, name: 'Restaurant2'},
{id: 3, name: 'Restaurant3'}
];
$scope.deletedres = [];
$scope.delete = function(id) {
var item, obj, i, j;
for(i = 0, j = $scope.res.length; i < j; i++) {
obj = $scope.res[i];
if(obj.id === id) {
$scope.deletedres.push(obj);
$scope.res.splice(i, 1);
}
}
};
$scope.undo = function(id) {
var item, obj, i, j;
for(i = 0, j = $scope.deletedres.length; i < j; i++) {
obj = $scope.deletedres[i];
if(obj.id === id) {
$scope.res.push(obj);
$scope.deletedres.splice(i, 1);
}
}
}
});
Here's the sample code.

Categories