Creating new elements at runtime using Mithril - javascript

Using Mithril, a Javascript framework, I am trying to add new elements after the initial body has been created and rendered.
Here is my problem in it's most basic form:
let divArray = [];
let newDivButton = m('button', { onclick: ()=> {
divArray.push(m('div', "NEW DIV PUSHED"));
}}, "Add new div");
divArray.push(newDivButton);
divArray.push(m('div', "NEW DIV PUSHED"));
let mainDiv = {
view: () => {
return divArray;
}
}
m.mount(document.body, mainDiv);
The code above will create a button and one line of text saying NEW DIV PUSHED. The button adds one new text element exactly like the 1st one. The problem here is that those additional elements are simply not rendered even if the view function is called. I stepped in the code and clearly see that my divArray is being populated even if they are not rendered.
One thing I noticed is that the initial text element (the one that is rendered) has it's dom property populated by actual div object. All the subsequent text elements in my array have their dom property set to undefined. I don't know how to fix this but I am convinced it is related to my problem.

Mithril has an optimization built into the render lifecycle - it won't re-render a DOM tree if the tree is identical to the last tree. Since divArray === divArray is always true the nodes are never re-rendering.
The simple, but non-ideal solution is to slice your array so you're always returning a new array from mainDiv#view and therefore, Mithril will always re-render the top-level array:
let mainDiv = {
view: () => {
return divArray.slice();
}
};
The more correct way to do this is to map over the data, creating vnodes at the view layer, rather than keeping a list of vnodes statically in your module scope:
let data = ["Data Available Here"];
let mainDiv = {
view: () => {
return [
m(
'button',
{ onclick: () => data.push("Data Pushed Here") },
"Add new div"
);
].concat(
data.map(datum => m('div', datum))
);
}
};

Related

Vue 3 directives – how to push array of items to directive

Since there's no IntersectionObserver library for Vue 3, I'd like to implement my own little solution.
I read about directives, and from what I understand it's the right direction. (?)
My local directive:
directives: {
'lazyload': {
mounted(el) {
if ('IntersectionObserver' in window) {
let intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazyImage = entry.target;
// set data-srcset as image srcset
lazyImage.srcset = lazyImage.getAttribute('data-srcset');
// add class after image has loaded
lazyImage.addEventListener('load', () => {
lazyImage.classList.add('is-lazyloaded');
};
// unobserve after
lazyLoadItemObserver.unobserve(lazyImage);
}
});
});
// observe every image
const lazyLoadItems = document.querySelectorAll('[lazyload]')
lazyLoadItems.forEach((lazyImage) => {
lazyLoadItemObserver.observe(lazyImage);
});
}
}
}
}
The vanilla way would be to make an array out of every <IMG> element that has the attribute lazyload for instance. The thing I don't get is how to make an array out of every <IMG> that has the v-lazyload binding.
Something like "if this image has the v-lazyload binding, put it into the IntersectionObserver's array." I might understand it wrong though.
So I'd like to come up with a directive which sets one single IntersectionObserver which observes an array of all the images that have the v-lazyload binding.
The v-lazyload directive will be on the target elements already, so no need to query the document.
In the directive's mounted hook, you can attach an IntersectionObserver instance to the parent node if it doesn't already exist. Then, use that instance to observe the target element (also unobserve the element in unmounted):
directives: {
mounted(el) {
el.parentNode.lazyLoadItemObserver = el.parentNode.lazyLoadItemObserver || new IntersectionObserver(/*...*/)
el.parentNode.lazyLoadItemObserver.observe(el)
},
unmounted(el) {
el.parentNode.lazyLoadItemObserver.unobserve(el)
},
}
demo

Array.push new objects, last object pushed

I am trying to loop through an array of objects, which depending on their type property, will create a different class and append it to an array. The problem is that the output is always just a list of duplicates of the last class created.
// Create Elements from Content
// The id's are created by UUIDV4 and are all different.
self._elements = new Array
let e;
self.content.page_data.forEach(cont => {
switch (cont.type) {
case 'paragraph':
e = new Paragraph()
console.log(e.element.id)
self._elements.push(e)
break;
case 'title':
console.log('title')
return
}
})
console.log(self._elements)
After troubleshooting I've found that the problem isn't e, as each instance is different, however once it is pushed / added to the array, the problem occurs. The problem only occurs when instances of Paragraph() are created. As other items in the array, like text, will remain the same while still duplicating the last class.
Please can someone explain what I'm missing here?
EDIT - Class for Paragraph
class Paragraph {
constructor(value = '') {
self._element = template_paragraph.cloneNode(true).content.children[0];
const quil = self._element.children[0].children[1].children[0];
self.quill = new Quill(quil, {
modules: {
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
[{ list: 'ordered' }, { list: 'bullet' }]
]
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
})
self._element.id = uuidv4()
}
get element() {
return self._element
}
set_content(content) {
// Set quill value
if (!content) return
//self.quill.setContents(content)
}
}
The quill interacts with my html clone as intended. I hope this will help.
The keyword is this in JavaScript. Not self (that's a Python thing.) Since self is not a keyword in JavaScript, some people use it by convention as a normal variable name by manually assigning var self = this; somewhere. But really, I think you just want to say this and use it the normal way.
Replace self with this in your code and you should be good to go.
Allow me to demonstrate a counterexample to your claim. You code seems to work correctly and the problem is elsewhere, most likely your Paragraph class.
By just changing the supporting framework (consisting of self and its content, page_data etc.) and the Paragraph class) I can demonstrate that your code (which I have used verbatim) works correctly, in that each element of self._elements is indeed different (most notably has a different id).
// [[[ MOCKED FRAMEWORK TO DEMONSTRATE THAT YOUR CODE WORKS
let self = { content: { page_data: [
{type:'title'},
{type:'paragraph'},
{type:'paragraph'},
] } };
let nextUnusedId = 101;
let Paragraph = function () { this.element = { id: nextUnusedId++ } }
// ]]]
// Create Elements from Content
// The id's are created by UUIDV4 and are all different.
self._elements = new Array
let e;
self.content.page_data.forEach(cont => {
switch (cont.type) {
case 'paragraph':
e = new Paragraph()
console.log(e.element.id)
self._elements.push(e)
break;
case 'title':
console.log('title')
return
}
})
console.log(self._elements)
Try just using a new variable declared inside the scope of the loop
// Create Elements from Content
// The id's are created by UUIDV4 and are all different.
self._elements = new Array
self.content.page_data.forEach(cont => {
switch (cont.type) {
case 'paragraph':
var e = new Paragraph()
console.log(e.element.id)
self._elements.push(e)
break;
case 'title':
console.log('title')
return
}
})
console.log(self._elements)
From your problem description, it sounds as if there is a single "reference" variable of 'e' and you are just creating an array of that reference over and over again, and whatever the loop last iterated over is what all those references point to

Mithril - Re-sort array of child components after child component modifies parent component data

If someone can think of a better title for this question, please let me know.
I just figured out the answer to this question after an insane amount of time. I'm sure someone else will make the same mistake, so I'll post the answer below.
The question will make more sense once I provide some background info. I have a parent component which has an array of objects with matching keys (e.g. ctrl.arrayOfObjects). I sort this array by the value of one of the keys shared by each object (e.g., ctrl.arrayOfObjects[0].keyToSortBy). Then, I pass each element in the now-sorted array to another component.
This child component has an onclick method which changes the data shared between the parent and the child, i.e., one of the objects in the parent's ctrl.arrayOfObjects. It changes the value of the key which was used to sort the parent component's ctrl.arrayOfObjects (e.g., sharedObject.keyToSortBy).
My problem is that, after triggering this onclick event, the parent component does not re-sort the array of objects. The onclick event does modify the data it's supposed to (sharedObject.keyToSortBy), which I know because the page does re-render the new value of sharedObject.keyToSortBy. But it does not re-sort the parent component's ctrl.arrayOfObjects.
The weird part is that if I do a console.log, the sort method is being called, but the page does not display any changes.
Here's what the code looks like (ctrl.arrayOfObjects is ctrl.jokes):
var JokeWidgetSeries = {
controller: function() {
var ctrl = this;
ctrl.jokes = [{
text: "I'm a joke",
votes: 3
}, {
text: "This is another joke",
votes: 1
}, {
text: "A third joke",
votes: 1
}]
},
view: function(ctrl) {
return m('#jokes-container',
m('#jokes',
ctrl.jokes.sort(function(a, b) {
// Sort by number of votes.
return (a.votes < b.votes) ? 1 : (a.votes > b.votes) ? -1 : 0;
}).map(function(joke) {
// Create a 'JokeWidget' for each object in 'ctrl.jokes'.
// Share the 'joke' object with the child component.
return m.component(JokeWidget, joke);
})
)
);
}
};
var JokeWidget = {
controller: function(inherited) {
var ctrl = this;
ctrl.joke = inherited;
},
view: function(ctrl) {
return m('.joke', [
m('.joke-text', ctrl.joke.text),
m('.joke-vote-count', ctrl.joke.votes),
m('.thumb-up', {
onclick: function(e) {
// Here, the page shows that the vote count has changed,
// but the array of 'JokeWidget' DOM elements do not re-sort.
ctrl.joke.votes += 1;
}
})
]);
}
};
After triggering the onclick method of the JokeWidget component, the .joke-vote-count DOM element which displays ctrl.joke.votes does increase by 1. But it does not move up in the list/array of widgets on the page like it should.
However, if I combine these two components into one, the array does get re-sorted like I want it. Like this:
var JokeWidgetSeries = {
controller: function() {
var ctrl = this;
ctrl.jokes = [{
text: "I'm a joke",
votes: 3
}, {
text: "This is another joke",
votes: 1
}, {
text: "A third joke",
votes: 1
}]
},
view: function(ctrl) {
return m('#jokes-container',
m('#jokes',
ctrl.jokes.sort(function(a, b) {
// Sort by number of votes.
return (a.votes < b.votes) ? 1 : (a.votes > b.votes) ? -1 : 0;
}).map(function(joke) {
// Create a 'JokeWidget' component instance for each object in 'ctrl.jokes'.
// Share the 'joke' object with the child component.
return m('.joke', [
m('.joke-text', joke.text),
m('.joke-vote-count', joke.votes),
m('.thumb-up', {
onclick: function(e) {
// Here, the array of 'JokeWidget' DOM elements
// DO re-sort. Why?
joke.votes += 1;
}
})
]);
})
)
);
}
};
How can I separate these components and still get this re-sorting method to work?
The solution is simple. When sharing the object between the parent and child components, pass the object to the child component's view method rather than the controller method.
When creating components in Mithril, you can pass data to both the controller and the view methods of the component. This data will be the first parameter of the controller method and the second parameter of the view method.
Instead of setting the shared object to ctrl.joke inside of the JokeWidget's controller method and then referencing it inside of its view method, just pass the shared object directly to the view method. Like this:
var JokeWidget = {
// Do not pass the object this way.
// controller: function(joke) {
// var ctrl = this;
// ctrl.joke = joke;
// },
// Pass the shared object as a second parameter to the 'view' method.
view: function(ctrl, joke) {
return m('.joke', [
m('.joke-text', joke.text),
m('.joke-vote-count', joke.votes),
m('.thumb-up', {
onclick: function(e) {
// Now, the array of 'JokeWidget' DOM elements will re-sort.
joke.votes += 1;
}
})
]);
}
};
This also removes an extra layer of abstraction, making it easier to read and understand.

When does a listView itemTemplate get inserted into the DOM?

I have created a custom itemTemplateFunction as described here. However I want to access attributes (such as height, or clientHeight) which are defined upon rendering in the DOM. This is to allow for a child element to be offset dynamically in the layout.
I've currently looked into two methods:
renderComplete - returned as an element in the itemPromise then return object. The event fired as expected, however the attributes (such as height) did not have set values, so it looks as though the item is not in the DOM at this point.
setInterval - although this would work it's a bad solution as I am relying on a constant time offset, something which ideally I don't want to do.
function itemTemplateFunction(itemPromise) {
return itemPromise.then(function (item) {
var div = document.createElement("div");
var img = document.createElement("img");
img.src = item.data.picture;
img.alt = item.data.title;
div.appendChild(img);
var childDiv = document.createElement("div");
var title = document.createElement("h4");
title.className += "title";
title.innerText = item.data.title;
childDiv.appendChild(title);
var desc = document.createElement("h6");
desc.innerText = item.data.text;
childDiv.appendChild(desc);
div.appendChild(childDiv);
return {
element: div,
renderComplete: itemPromise.then(function (item){
return item.ready;
}).then(function (item){
var height_a = div.querySelector(".title").clientHeight;
var height_b = WinJS.Utilities.query(".title", div).clientHeight;
})
}
});
};
With the first item created the attributes still haven't been finalised, despite the promise being passed back. If however I wrap the return of item.ready in a second promise with a timeout interval of 0 this does return as expected.
return {
element: div,
renderComplete: itemPromise.then(function (item){
return WinJS.Promise.timeout(0).then(function () {
return item.ready;
});
}).then(function (item){
var height_a = div.querySelector(".title").clientHeight;
var height_b = WinJS.Utilities.query(".title", div).clientHeight;
})
}
The hook you're looking for is the item.ready property, once you're inside the completed handler for itemPromise.then().
item.ready is itself a promise that's fulfilled when the item gets rendered, at which point all the layout-dependent attributes should be set.
To use it, return item.ready from your first completed handler for itemPromise.then, and then add another .then(function (item) {} ) to the chain. Inside that complete handler is where you can check the attributes.
I go into some detail on this in Chapter 7 of my free ebook, Programming Windows Store Apps in HTML, CSS, and JavaScript, 2nd Edition, specifically in the section at the end of the chapter called "Template Functions (Part 2): Optimized Item Rendering" and the discussion of multistage rendering. The same content also appeared on the older Windows 8 developer blog, although note that the "recycling placeholder" option discussed on the blog is for WinJS 1.0 only.

KnockoutJS custom binding calling click event during bind, not on click

I`m attempting to bind an observable array of people two a two column responsive layout with click events using knockoutjs.
I have created a custom binding called TwoCol that loops through the array, and appends nodes to the DOM to create my suggested layout, but the click events are giving me trouble when I try to apply them in a custom binding nested in a loop.
I have played with it quite a bit, and encountered all types of results, but where I`m at now is calling my ~click~ event during binding, rather than on click.
http://jsfiddle.net/5SPVm/6/
HTML:
<div data-bind="TwoCol: Friends" id="" style="padding: 20px">
JAVASCRIPT:
function FriendsModel() {
var self = this;
this.Friends = ko.observableArray();
this.SelectedFriend = "";
this.SetSelected = function (person) {
alert(person);
self.SelectedFriend = person;
}
}
function isOdd(num) {
return num % 2;
}
ko.bindingHandlers.TwoCol = {
update: function (elem, valueAccessor) {
var i = 0;
var rowDiv;
var vFriends = ko.utils.unwrapObservable(valueAccessor());
$(elem).html('');
while (i < vFriends.length) {
//create row container every other iteration
if (!isOdd(i)) {
rowDiv = document.createElement("div");
$(rowDiv).addClass("row-fluid");
elem.appendChild(rowDiv);
}
//add column for every iteration
var colDiv = document.createElement("div");
$(colDiv).addClass("span6");
rowDiv.appendChild(colDiv);
//actual code has fairly complex button html here
var htmlDiv = document.createElement("div");
var htmlButton = vFriends[i]
htmlDiv.innerHTML = htmlButton;
colDiv.appendChild(htmlDiv);
//i think i need to add the event to the template too?
//$(htmlDiv).attr("data-bind", "click: { alert: $data }")
//it seems that the SetSelected Method is called while looping
ko.applyBindingsToDescendants(htmlDiv, { click: friends.SetSelected(vFriends[i]) });
i++;
}
return { controlsDescendantBindings: true };
}
}
var friends = new FriendsModel();
friends.Friends.push('bob');
friends.Friends.push('rob');
friends.Friends.push('mob');
friends.Friends.push('lob');
ko.applyBindings(friends);
I don't think you're using ko.applyBindingsToDescendants correctly. I admit I'm a little confused as to the meaning of some of the values in your code, so I may have interpreted something incorrectly.
Here's a fiddle where I think it's working the way you intended:
http://jsfiddle.net/5SPVm/7/
http://jsfiddle.net/5SPVm/8/
Notice if manually control descendant bindings (return { controlsDescendantBindings: true };), you need to set that up in the init callback, instead of update. The update callback is too late for that.
Quick rundown of the changes (edited):
Moved the controlsDescendantBindings into the init binding callback
Added the necessary parameter names to the binding param list to access additional values.
I re-enabled the html.attr call. Notice that now, because the binding context is set to the actual item, the SetSelected method doesn't exist at that level anymore, so it is necessary to use $parent.SetSelected.
$(htmlDiv).attr("data-bind", "click: $parent.SetSelected")
Fixed the ko.applyBindingsToDescendants call. This method takes a binding context, which is created from the current binding context, and also takes the element to apply the binding to. You don't want to reapply the binding, which is why this whole thing needs to be in the init handler.
var childBindingContext = bindingContext.createChildContext(vFriends[i]);
ko.applyBindingsToDescendants(childBindingContext, colDiv);

Categories

Resources